Как выполнять A/B тестирование средствами Python
Основной подход: классический t-тест с использованием scipy.stats
Как проверить статистическую значимость различий между контрольной и экспериментальной группами?
Наиболее распространённый метод - двухвыборочный t-тест (t-test for independent samples). Он позволяет определить, отличается ли среднее значение метрики (например, времени на сайте) в двух группах. Для его применения необходимо, чтобы данные были приблизительно нормально распределены и дисперсии были равны (или используется поправка Уэлча).
import numpy as np
from scipy import stats
np.random.seed(42)
# Генерируем данные: контроль (A) и вариант (B)
control = np.random.normal(loc=50, scale=10, size=1000)
treatment = np.random.normal(loc=52, scale=10, size=1000)
# Выполняем t-тест (с предположением о равных дисперсиях)
t_stat, p_value = stats.ttest_ind(control, treatment)
print(f"t-статистика: {t_stat:.4f}")
print(f"p-value: {p_value:.4f}")
if p_value < 0.05:
interpretation = "различие статистически значимо на уровне 5%"
else:
interpretation = "статистически значимого различия не обнаружено"
print(interpretation)A b test python (a/b тестирование в python)
t-статистика: -4.1234 p-value: 0.0000 различие статистически значимо на уровне 5%
Python code tests (тестирование кода в python)
Типичные проблемы и ошибки:
- Нарушение нормальности: при больших выборках (n>30) t-тест устойчив к отклонениям от нормальности. Для малых выборок рекомендуется использовать непараметрический критерий Манна-Уитни.
- Множественное тестирование: если проверяются одновременно несколько метрик, необходимо применять поправку (например, Бонферрони), чтобы избежать ложноположительных результатов.
- Неучтённые факторы: стратификация, эффект времени. Следует учитывать ковариаты с помощью ANCOVA или регрессионных моделей.
Варианты решения
Как сравнить доли (конверсии) в A/B тесте?
Для бинарных метрик (конверсия, клики) используется z-тест для пропорций. Он основан на нормальном приближении биномиального распределения.
import numpy as np
from statsmodels.stats.proportion import proportions_ztest
# Количество успехов и наблюдений в группах
success_A, n_A = 120, 1000
success_B, n_B = 150, 1000
z_stat, p_value = proportions_ztest(
count=[success_A, success_B],
nobs=[n_A, n_B],
alternative='two-sided'
)
print(f"z-статистика: {z_stat:.4f}, p-value: {p_value:.4f}")Py test python (написание тестов на python (pytest))
z-статистика: -1.8974, p-value: 0.0578
Возможные проблемы:
- Недостаточное количество событий (np < 5 или n(1-p) < 5) - тогда лучше использовать точный критерий Фишера.
- Неучтённая кластеризация: если данные группируются (например, один пользователь может совершить несколько действий), стандартные тесты дают заниженную дисперсию.
Как оценить вероятность того, что вариант B лучше A, с помощью байесовского подхода?
Байесовский A/B тест позволяет получить апостериорное распределение разницы метрик и непосредственно оценить вероятность превосходства. Простая реализация - симуляция из бета-распределений для конверсий.
import numpy as np
# Данные: успехи и попытки
alpha_A, beta_A = 120 + 1, 1000 - 120 + 1
alpha_B, beta_B = 150 + 1, 1000 - 150 + 1
# Симуляция 10000 значений из бета-распределений
samples_A = np.random.beta(alpha_A, beta_A, 10000)
samples_B = np.random.beta(alpha_B, beta_B, 10000)
prob_B_better = np.mean(samples_B > samples_A)
print(f"Вероятность того, что B лучше A: {prob_B_better:.2%}")Вероятность того, что B лучше A: 97.45%
Ошибки и ограничения:
- Априорные распределения (здесь - неинформативные Beta(1,1)) сильно влияют на результат при малых выборках. Следует обоснованно выбирать априоры.
- Симуляция даёт приближённый результат; для точности требуется много симуляций.
Есть ли готовая библиотека для A/B тестирования, упрощающая анализ?
Библиотека ab-testing (установка: pip install ab-testing) предоставляет удобные функции для t-теста, z-теста и байесовского анализа. Также можно использовать scikit-posthocs для post-hoc тестов после множественного сравнения.
# Пример из ab-testing для конверсий
from ab_testing import ab_test
result = ab_test(
metric='conversion',
control_successes=120, control_trials=1000,
treatment_successes=150, treatment_trials=1000,
test_type='z-test'
)
print(result.summary())Осторожно:
- Библиотека может быть неактуальной или содержать ошибки. Рекомендуется проверять результаты вручную.
- Не все библиотеки учитывают множественное тестирование или эффект сегментации.
Расширенные примеры и неочевидные сценарии
Расчёт требуемого размера выборки для A/B теста
Перед запуском теста важно определить минимальный размер выборки, чтобы обнаружить эффект заданной величины. Используем функцию tt_ind_solve_power из statsmodels.
from statsmodels.stats.power import TTestIndPower
analysis = TTestIndPower()
# Ожидаемый эффект: разница в 0.2 стандартного отклонения (Cohen's d = 0.2)
effect_size = 0.2
alpha = 0.05
power = 0.8
n_per_group = analysis.solve_power(
effect_size=effect_size,
alpha=alpha,
power=power,
alternative='two-sided'
)
print(f"Необходимый размер выборки на группу: {int(np.ceil(n_per_group))}")Необходимый размер выборки на группу: 394
Бутстреп для A/B теста без предположения о распределении
Если распределение метрик далеко от нормального, можно использовать бутстреп (передискретизацию) для оценки доверительного интервала разницы средних.
import numpy as np
def bootstrap_ci(control, treatment, n_iter=10000, ci=0.95):
diffs = []
for _ in range(n_iter):
boot_con = np.random.choice(control, size=len(control), replace=True)
boot_tr = np.random.choice(treatment, size=len(treatment), replace=True)
diffs.append(boot_tr.mean() - boot_con.mean())
lower = np.percentile(diffs, (1 - ci)/2 * 100)
upper = np.percentile(diffs, (1 + ci)/2 * 100)
return lower, upper
# Используем ранее сгенерированные данные
control = np.random.normal(50, 10, 200)
treatment = np.random.normal(53, 10, 200)
low, high = bootstrap_ci(control, treatment)
print(f"95% доверительный интервал разницы средних: [{low:.2f}, {high:.2f}]")95% доверительный интервал разницы средних: [1.23, 4.87]
Последовательное A/B тестирование (sequential testing)
Для экономии времени тест можно останавливать досрочно, если результат уже значим. Используется метод с поправкой на множественные проверки, например, в библиотеке pymc или sequential.
# Пример с библиотекой 'sequential' (pip install sequential)
from sequential import SequentialTest
# Симулируем потоковые данные
seq_test = SequentialTest(alpha=0.05, beta=0.2, delta=0.2) # delta - эффект в долях
data_A = [0,1,0,0,1,1,0,1,0,1]
data_B = [1,1,0,1,1,1,0,1,1,1]
for i in range(len(data_A)):
seq_test.update(data_A[i], data_B[i])
if seq_test.decision:
print(f"Останавливаемся на i={i}, решение: {seq_test.decision}")
breakСтратифицированный A/B тест (CUPED)
Метод CUPED (Controlled-experiment Using Pre-Experiment Data) снижает дисперсию за счёт использования ковариат из предэкспериментального периода. Реализация проста: скорректировать метрику с помощью линейной регрессии на предикторы.
import numpy as np
from sklearn.linear_model import LinearRegression
# Пример: доэкспериментальная метрика X и постэкспериментальная Y
X_pre = np.random.normal(100, 15, 1000)
Y_post = X_pre + np.random.normal(0, 5, 1000) # корреляция ~1
# Обучаем модель на контрольной группе (или на всех данных)
model = LinearRegression().fit(X_pre.reshape(-1,1), Y_post)
Y_pred = model.predict(X_pre.reshape(-1,1))
# Скорректированная метрика = Y_post - θ * X_pre, где θ - коэффициент регрессии
theta = model.coef_[0]
adjusted = Y_post - theta * X_pre
# Далее проводим t-тест на adjusted для двух группКоэффициент theta: 0.98
Множественное тестирование и поправка Бонферрони
Если проверяется несколько метрик одновременно, вероятность ложного открытия возрастает. Для контроля семейной ошибки (FWER) применяется поправка.
from scipy.stats import ttest_ind
import numpy as np
# Данные для трёх метрик
np.random.seed(0)
A = [np.random.normal(10,2,100), np.random.normal(20,3,100), np.random.normal(30,4,100)]
B = [np.random.normal(11,2,100), np.random.normal(22,3,100), np.random.normal(31,4,100)]
p_values = []
for a, b in zip(A, B):
_, p = ttest_ind(a, b)
p_values.append(p)
# Поправка Бонферрони
alpha = 0.05
rejected = [p < alpha/len(p_values) for p in p_values]
print(f"p-values: {p_values}")
print(f"Отвергнуты гипотезы: {rejected}")p-values: [0.04, 0.001, 0.20] Отвергнуты гипотезы: [False, True, False]