Как выполнять A/B тестирование средствами Python

Раздел: Разработка на 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]

A/B тестирование в Python - comments

En
A b test python (python)