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

Почему ваш A/B-тест врёт: 5 ловушек p-value

3 минуты чтения
Содержание

p-value — это не вероятность того, что ваша гипотеза верна. И не вероятность, что наблюдаемое различие появилось случайно. p-value отвечает на очень узкий вопрос: «если бы нулевая гипотеза была правдой, насколько экстремальные данные мы бы увидели?»

Формально:

p=P(TtobsH0)p = P(T \geq t_{\text{obs}} \mid H_0)

Где TT — статистика критерия, tobst_{\text{obs}} — её наблюдаемое значение. Всё. Из этого определения сразу вытекают ловушки.

Ловушка 1: подглядывание (peeking)

Запустили тест, каждое утро открываете дашборд и смотрите, не пробило ли p<0.05p < 0.05. Как только пробило — останавливаете. Кажется логичным? А на деле вы только что сломали контроль ошибок первого рода.

Если проверять тест kk раз и останавливаться при первом p<αp < \alpha, реальная вероятность ошибки растёт примерно как:

αreal1(1α)k\alpha_{\text{real}} \approx 1 - (1 - \alpha)^k

При 10 проверках и α=0.05\alpha = 0.05 реальная вероятность ошибочно отвергнуть H0H_0 уже около 40%.

Что делать: фиксировать размер выборки до старта теста либо использовать sequential testing (mSPRT, Always Valid p-values).

Ловушка 2: множественные сравнения

Тестируете 5 метрик разом и где-то увидели p=0.04p = 0.04. Вероятность, что хоть одна из 5 независимых метрик случайно пробьёт порог при истинной H0H_0:

P(хотя бы одно p<0.05)=10.9550.23P(\text{хотя бы одно } p < 0.05) = 1 - 0.95^5 \approx 0.23

23%, не 5%. Бонферрони (делите α\alpha на число тестов) — простое, но консервативное решение. FDR (Benjamini–Hochberg) мягче и чаще уместнее в продуктовой аналитике.

Ловушка 3: недостаточная мощность

Мощность 1β1 - \beta — вероятность обнаружить эффект, если он действительно есть. При маленькой выборке даже реальный эффект в 2% конверсии может не пробиться через шум — и вы сделаете ложный вывод «эффекта нет».

Размер выборки для сравнения двух пропорций (приближённо):

n2(z1α/2+z1β)2pˉ(1pˉ)δ2n \approx \frac{2 \cdot (z_{1-\alpha/2} + z_{1-\beta})^2 \cdot \bar{p}(1-\bar{p})}{\delta^2}

Python:

from math import ceil
from scipy.stats import norm

def sample_size(p1: float, mde: float, alpha: float = 0.05, power: float = 0.8) -> int:
    p2 = p1 + mde
    p_bar = (p1 + p2) / 2
    z_alpha = norm.ppf(1 - alpha / 2)
    z_beta = norm.ppf(power)
    n = (2 * (z_alpha + z_beta) ** 2 * p_bar * (1 - p_bar)) / (mde ** 2)
    return ceil(n)

print(sample_size(p1=0.10, mde=0.01))  # ≈ 31 200 на группу

Увидели нужное число пользователей — значит, эксперимент спланирован корректно. Нет — отложите запуск или поднимите базу.

Ловушка 4: эффект выжившего в метрике

Метрика «средний чек среди заплативших» выглядит невинно, но когортирует по результату теста. Вы сравниваете не случайные группы, а самоотобранных людей, и тест ломается — рандомизация разрушена.

Считайте метрики по намерению лечить (ITT): знаменатель — все пользователи группы, включая не-конвертировавшихся.

Ловушка 5: стат. значимость ≠ практическая значимость

При n=107n = 10^7 любое микроскопическое различие бьёт порог p<0.05p < 0.05. Перед тестом всегда определяйте MDE — minimum detectable effect, который имеет смысл для бизнеса. Если реальный эффект меньше MDE — даже «значимый» результат не заслуживает релиза.

Что в итоге

  1. Планируйте выборку и MDE до старта, не после.
  2. Не подглядывайте или используйте sequential-методы.
  3. Корректируйте α\alpha при множественных сравнениях.
  4. Считайте метрики по ITT, не по подвыборке.
  5. Проверяйте не только значимость, но и размер эффекта с доверительным интервалом.

p-value — полезный инструмент, но не оракул. Если вы помните его определение буквально, большая часть «парадоксов» A/B-тестов перестаёт быть парадоксами.