Туториал: Запуск A/B-тестов с помощью aboba
Этот туториал знакомит с ключевыми понятиями библиотеки aboba на практических примерах. Вы научитесь настраивать эксперименты, запускать тесты и анализировать результаты.
Ключевые понятия
Перед тем как перейти к примерам, разберём основные компоненты:
- Тест (Test): статистический тест, который используется для проверки гипотез (t-тест, HSD и т.д.)
- Пайплайн (Pipeline): последовательность обработчиков и сэмплеров, подготавливающих данные
- Сэмплер (Splitter): определяет, как данные разбиваются на группы
- Обработчик (Processor): преобразует данные (например, CUPED, бакетизация)
- Модификатор эффектов (Effect Modifier): добавляет синтетические эффекты для анализа мощности
- Эксперимент (Experiment): управляет сериями запусков тестов и визуализацией результатов
Пример 1: Базовый эксперимент на синтетических данных
Начнём с простого примера на синтетических данных, чтобы понять общий рабочий процесс.
Шаг 1: Генерация данных
Сначала создадим две группы из одного и того же распределения N(0, 1):
import numpy as np
import pandas as pd
import scipy.stats as sps
from aboba import tests, splitters, effect_modifiers, experiment, pipeline
# Генерация синтетических данных
n = 1000
data_a = sps.norm.rvs(size=n, loc=0, scale=1)
data_b = sps.norm.rvs(size=n, loc=0, scale=1)
# Собираем DataFrame с двумя группами
data = pd.DataFrame({
'value': np.concatenate([data_a, data_b]),
'b_group': np.concatenate([
np.repeat(0, 1000),
np.repeat(1, 1000)
])
})
Шаг 2: Создание пайплайна
Пайплайн определяет, как происходит выборка данных. Здесь мы будем брать по 100 наблюдений из каждой группы:
group_size = 100
data_pipeline = pipeline.Pipeline([
(splitters.GroupSplitter(size=group_size, column='b_group'), 'GroupSplitter'),
])
Шаг 3: Задание теста
Используем абсолютный независимый t-тест:
Шаг 4: Создание эксперимента
Эксперимент управляет несколькими группами тестов и визуализацией результатов:
Шаг 5: Запуск AA-теста (проверка корректности)
Сначала запустим AA-тест, чтобы убедиться, что тест работает корректно (обе группы из одного распределения):
aa_group = exp.group(
"AA Test",
test=test,
data=data,
data_pipeline=data_pipeline,
n_iter=100,
joblib_kwargs={"n_jobs": -1, "backend": "threading"}
).run()
AA-тест должен показать равномерное распределение значений p-value от 0 до 1, что подтверждает отсутствие ложных срабатываний.
Шаг 6: Запуск AB-теста с синтетическим эффектом
Теперь добавим синтетический эффект в группу 1 и запустим тест:
ab_group = exp.group(
"AB Test (effect=0.3)",
test=test,
data=data,
data_pipeline=data_pipeline,
synthetic_effect=effect_modifiers.GroupModifier(
effects={1: 0.3}, # Прибавить 0.3 к группе 1
value_column='value',
group_column='b_group',
),
n_iter=100,
).run()
Шаг 7: Визуализация результатов
Будут показаны распределения значений p-value для AA- и AB-тестов.
Для AB-теста большинство значений p-value должно быть близко к 0, что указывает на успешное обнаружение эффекта.
Получение результатов
Детальные результаты каждой группы доступны напрямую:
Пример 2: Реальные данные и CUPED
Теперь рассмотрим работу с реальными данными и применение CUPED (Controlled-experiment Using Pre-Experiment Data) для снижения дисперсии.
Что такое CUPED
CUPED — это метод снижения дисперсии, который использует данные до эксперимента (ковариаты) для повышения чувствительности теста. Целевая метрика корректируется с учётом коррелированной ковариаты.
Шаг 1: Загрузка реальных данных
Шаг 2: Создание собственного обработчика данных
Так как CUPED требует доступ ко всему датасету до сэмплирования, создадим обработчик для формирования групп:
import aboba
class RandomGroupAssigner(aboba.base.BaseDataProcessor):
def __init__(self, groups_n=2, column_name='group'):
self.groups_n = groups_n
self.column_name = column_name
def transform(self, data: pd.DataFrame):
n = data.shape[0]
groups = np.random.randint(0, self.groups_n, size=n)
data[self.column_name] = groups
return data, None
Шаг 3: Построение CUPED-пайплайна
sample_size = 100
covariate = 'totsp' # Общая площадь как ковариата
cuped_pipeline = pipeline.Pipeline([
RandomGroupAssigner(groups_n=2),
aboba.processing.EnsureColsProcessor(['price', 'group', covariate]),
aboba.processing.CupedProcessor(
value_column='price',
covariate_columns=covariate,
result_column='price_cuped',
group_column='group',
group_test=1,
group_control=0,
),
splitters.GroupSplitter(column='group', size=sample_size),
aboba.processing.EnsureColsProcessor(['price_cuped']),
])
Шаг 4: Запуск тестов с CUPED
exp = experiment.AbobaExperiment()
cuped_test = tests.AbsoluteIndependentTTest(
value_column='price_cuped',
)
# AA-тест с применением CUPED
exp.group(
"AA, CUPED",
test=cuped_test,
data=data,
data_pipeline=cuped_pipeline,
n_iter=100,
).run()
# AB-тест с CUPED
exp.group(
"AB, CUPED (effect=10)",
test=cuped_test,
data=data,
data_pipeline=cuped_pipeline,
synthetic_effect=effect_modifiers.GroupModifier(
effects={1: 10},
value_column='price_cuped',
group_column='group',
),
n_iter=100,
).run()
exp.draw()
Как правило, CUPED обеспечивает большую статистическую мощность по сравнению с обычным t-тестом, позволяя обнаруживать меньшие эффекты при том же размере выборки.
Пример 3: Создание собственных тестов
Вы можете реализовать собственный тест, унаследовавшись от BaseTest. Ниже приведён пример относительного t-теста:
class RelativeIndependentTTest(aboba.base.BaseTest):
def __init__(self, value_column="target", alternative="two-sided"):
super().__init__()
self.value_column = value_column
self.alternative = alternative
assert alternative in {"two-sided", "less", "greater"}
def test(self, groups, artefacts):
control_group, test_group = groups
Y, X = control_group[self.value_column], test_group[self.value_column]
var_1, var_2 = np.var(X, ddof=1), np.var(Y, ddof=1)
a_1, a_2 = np.mean(X), np.mean(Y)
# Рассчитываем относительную разницу
R = (a_1 - a_2) / a_2
var_R = var_1 / (a_2**2) + (a_1**2) / (a_2**4) * var_2
n = len(test_group)
stat = np.sqrt(n) * R / np.sqrt(var_R)
if self.alternative == "two-sided":
pvalue = 2 * min(sps.norm.cdf(stat), sps.norm.sf(stat))
pvalue = min(pvalue, 1)
elif self.alternative == "less":
pvalue = sps.norm.cdf(stat)
elif self.alternative == "greater":
pvalue = sps.norm.sf(stat)
return aboba.base.TestResult(
pvalue=pvalue,
effect=R,
effect_type="relative_control"
)
Использование собственного теста
relative_test = RelativeIndependentTTest(value_column='price')
exp.group(
"AB, Relative Test",
test=relative_test,
data=data,
data_pipeline=random_pipeline,
synthetic_effect=effect_modifiers.GroupModifier(
effects={1: 10},
value_column='price',
group_column='group',
),
n_iter=100,
).run()
exp.draw()
Продвинутый уровень: Гибкие модификаторы эффектов
Модификаторы эффектов поддерживают несколько способов задания эффектов:
1. Эффект, заданный константой
effect_modifiers.GroupModifier(
effects={1: 0.3}, # Добавить константу 0.3 к группе 1
value_column='value',
group_column='b_group',
)
2. Эффект, заданный функцией
def my_effect(obj):
obj['value'] += 0.3
return obj
effect_modifiers.GroupModifier(
effects={0: my_effect},
value_column='value',
group_column='b_group',
)
3. Эффект, заданный распределением
effect_modifiers.GroupModifier(
effects={
0: 0.9,
1: sps.norm(0.3, 0.001) # Случайный эффект из нормального распределения
},
value_column='value',
group_column='b_group',
)
Следующие шаги
- Изучите API Reference для всех доступных тестов
- Ознакомьтесь с обработчиками данных для продвинутых преобразований
- Посмотрите сэмплеры для различных стратегий выборки
- Обратите внимание на тесты для сравнения нескольких групп для сравнения более чем двух групп