Перейти к содержимому
TelegramGitHub

A/B-тест

A/B-тест — эксперимент. Пользователей случайно делят на две группы. Группа A видит старую версию, группа B — новую. Потом сравнивают метрику.

Пример. Мы поменяли кнопку «Купить». Файл ab_test.csv: 2000 пользователей в каждой группе, converted = 1, если пользователь купил.

Случайное деление важно. Только так группы похожи, и разницу можно связать с изменением, а не с чем-то ещё. Это и есть тот эксперимент, которого не хватает корреляции.

import csv
with open("data/ab_test.csv") as f:
ab = list(csv.DictReader(f))
a = [int(r["converted"]) for r in ab if r["grp"] == "A"]
b = [int(r["converted"]) for r in ab if r["grp"] == "B"]
print(len(a), sum(a), sum(a) / len(a))
print(len(b), sum(b), sum(b) / len(b))

Ответ:

2000 200 0.1
2000 250 0.125

Конверсия A — 10%, B — 12,5%. Разница 2,5 процентного пункта. Это эффект кнопки или случайность? В другой день группа A могла бы набрать 11%.

  • H0 (нулевая гипотеза): разницы нет. Кнопка не влияет. Разница — случайность.
  • H1: разница есть.

Мы не доказываем H1. Мы спрашиваем: если H0 верна, как часто мы увидели бы такую разницу или больше? Эту вероятность называют p-value.

  • p-value маленькое (меньше 0,05) → такая разница при H0 очень редка → отвергаем H0. Результат статистически значим.
  • p-value большое → разницу легко объяснить случайностью → H0 не отвергаем.

Порог 0,05 — общее соглашение. Его выбирают до теста.

Считаем:

  1. Общую долю покупателей: p = (200 + 250) / 4000 = 0,1125.
  2. Стандартную ошибку разницы: SE = √( p × (1 − p) × (1/n_A + 1/n_B) ).
  3. z-оценку: z = (p_B − p_A) / SE.
  4. p-value по нормальному распределению: 2 × (1 − cdf(|z|)). Двойка — потому что разница может быть в обе стороны.
import math
from statistics import NormalDist
n_a, n_b = len(a), len(b)
p_a, p_b = sum(a) / n_a, sum(b) / n_b
p = (sum(a) + sum(b)) / (n_a + n_b)
se = math.sqrt(p * (1 - p) * (1 / n_a + 1 / n_b))
z = (p_b - p_a) / se
p_value = 2 * (1 - NormalDist().cdf(abs(z)))
print(round(z, 2), round(p_value, 4))

Ответ:

2.5 0.0124

z = 2,5 больше 1,96, значит, p-value < 0,05. Точное значение из Python: 0,0124.

Если кнопка не влияет, такая разница (или больше) случается примерно в 1,2% случаев. Это редко. Мы отвергаем H0: разница статистически значима.

p-value говорит только «есть ли разница». Размер эффекта показывает доверительный интервал разницы:

разница ± 1,96 × √( p_A(1−p_A)/n_A + p_B(1−p_B)/n_B )
se_diff = math.sqrt(p_a * (1 - p_a) / n_a + p_b * (1 - p_b) / n_b)
low = (p_b - p_a) - 1.96 * se_diff
high = (p_b - p_a) + 1.96 * se_diff
print(round(low, 4), round(high, 4))
0.0054 0.0446

Читаем: новая кнопка добавляет от 0,5 до 4,5 процентного пункта конверсии. Ноль в интервал не попал. Это то же самое, что p-value < 0,05.

Интервал широкий: от «чуть-чуть» до «очень хорошо». Чтобы уточнить, нужно больше пользователей.

ОшибкаПочему плохо
Остановить тест, как только p < 0,05Если смотреть каждый день, рано или поздно p-value случайно упадёт ниже 0,05. Решите размер выборки заранее
Тест на малом числе пользователейЭффект не заметен. Разница есть, а p-value большое
Считать значимое = важноеПри миллионе пользователей значима и разница в 0,01%. Смотрите на размер эффекта
Много метрик сразуПроверяя 20 метрик, одна покажет p < 0,05 случайно
p-value = «вероятность, что H0 верна»Нет. Это вероятность таких данных при условии, что H0 верна

Что будет, если в группе B было бы не 250, а 230 покупателей (11,5%)? Посчитайте z и p-value.

Проверка: z = 1,53, p-value = 0,1257. Разница не значима: H0 не отвергаем.

Решение
conv_a, conv_b = 200, 230
p_a, p_b = conv_a / 2000, conv_b / 2000
p = (conv_a + conv_b) / 4000
se = math.sqrt(p * (1 - p) * (2 / 2000))
z = (p_b - p_a) / se
print(round(z, 2), round(2 * (1 - NormalDist().cdf(abs(z))), 4))

Разница 1,5 пункта при таких группах не отличима от случайной. Небольшое изменение цифр меняет вывод. Поэтому смотрите не только на «значимо / не значимо», но и на доверительный интервал.

Посчитайте относительный рост конверсии (lift): на сколько процентов B лучше A?

Проверка: (0,125 − 0,10) / 0,10 = 25%. Конверсия выросла в относительном выражении на 25%. Абсолютно — на 2,5 процентного пункта.

Решение
print(round((p_b - p_a) / p_a * 100, 1))

Если вы уже меняли p_a и p_b в задании 1, верните значения 0,10 и 0,125.

Новую кнопку запустили всем. В месяц приходит 100 000 посетителей. Сколько дополнительных заказов в месяц мы ждём, исходя из нижней и верхней границы интервала разницы (0,54% и 4,46%)?

Проверка: от 540 до 4 460 дополнительных заказов в месяц.

Решение
visitors = 100_000
print(round(visitors * 0.0054), round(visitors * 0.0446))

Разброс в 8 раз. Хороший пример, почему бизнесу показывают диапазон, а не одно число 2,5%.

Дальше: Итоговая практика.