Калибровка A/B-методов симуляцией: как проверить метод до продакшена
Кратко
Статистический метод продаётся на асимптотических обещаниях: «при больших выборках t-критерий ведёт себя так-то». Но в продуктовом A/B-тесте вы почти никогда не работаете в тех условиях, для которых эти обещания доказаны: смотрите на данные по ходу эксперимента, сравниваете ratio-метрики (CTR, RPC), добавляете ковариаты, режете по сегментам.
Единственный честный способ узнать, как метод поведёт себя на ваших данных — прогнать его на симуляции, где истинный эффект известен. Два обязательных теста на каждый модуль:
- A/A-тест под нулевой гипотезой: эффекта нет, проверяем, что доля «значимых» результатов ≈ α (обычно 0.05), а не 0.12.
- Power-кривая под реальным эффектом: эффект есть, проверяем, что метод его ловит с ожидаемой вероятностью, а не молчит.
Пример
A/A-симуляция на 1000 прогонах: грузим две одинаковые группы, считаем долю p < 0.05.
import numpy as np
from scipy import stats
rng = np.random.default_rng(42)
N = 10_000
alpha = 0.05
hits = 0
for _ in range(1_000):
a = rng.normal(0, 1, N)
b = rng.normal(0, 1, N)
_, p = stats.ttest_ind(a, b)
hits += p < alpha
print(f"Type I error: {hits / 1_000:.3f} (target <= {alpha})")
Power-кривая: тот же цикл, но группе B добавляем эффект d и строим долю значимых от d.
effects = np.linspace(0, 0.15, 16)
powers = []
for d in effects:
hits = 0
for _ in range(500):
a = rng.normal(0, 1, N)
b = rng.normal(d, 1, N)
_, p = stats.ttest_ind(a, b)
hits += p < alpha
powers.append(hits / 500)
По кривой видно: эффект 0.05 при N = 10 000 ловится почти всегда, эффект 0.01 — почти никогда. Это и есть ответ на вопрос «хватит ли выборки», без асимптотики.
Как пишется
- Генератор данных с известной истиной. Всегда задаётся seed, известен истинный эффект (0 или
d), повторяемые прогоны. - Null-проверка (A/A). Прогон при эффекте = 0. Доля значимых должна быть в пределах шума вокруг α. Отклонение вверх = метод мнит значимость, которой нет.
- Power/coverage-проверка. Прогон при эффекте
dиз реалистичного диапазона. Доля значимых = power. Плюс «coverage» для доверительных интервалов: интервал должен накрывать истинное значение в ~95% прогонов. - Прогон спец-кейсов из вашего реального pipeline. Не абстрактный t-критерий, а именно те трансформации, что в проде: CUPED, ratio-метрика, сегментные резы, поправка на множественность.
Как понять
Почему наивный peeking раздувает Type I
Если смотреть на данные каждый день и останавливаться при p < 0.05, шанс увидеть «значимость» под нулём растёт. К 100-му дню «значимость» случится почти наверняка. Формальные решения — alpha-spending границы (Pocock, O’Brien–Fleming) или always-valid методы (mSPRT), которые не штрафуют за раннюю остановку.
Как CUPED реально помогает
CUPED использует ковариат, измеренный до рандомизации (retention до теста, число сессий), чтобы убрать шум из пост-метрики. Дисперсия эффекта падает в (1 − ρ²) раз, где ρ — корреляция ковариата с метрикой. При ρ = 0.7 SE сокращается примерно на 29% — это как добавить ~2× выборки без её роста.
Ratio-метрики ломают наивный t-критерий
CTR = клики / показы. Если взять сумму кликов и сумму показов и проверить разницу «по юзерам», стандартная ошибка окажется смещённой — denominator случайный. Правильный путь — delta-method: асимптотика через разложение в ряд Тейлора. Наивный per-unit t-test будет выдавать «значимость» там, где её нет.
mSPRT: можно смотреть когда угодно
mSPRT (mixture Sequential Probability Ratio Test) устроен так, что вероятность ложной значимости при любом количестве промежуточных просмотров остаётся под контролем. Не «смотри реже», а «можно смотреть сколько угодно».
Подсказки
- A/A — это не ритуал, а калибровка: если type I = 0.12, метод сломан, не «просто разброс».
- Симулируйте на своём распределении (логнормальные выручки, дискретные показы), а не на гауссиане — иначе получите калибровку для чужого продукта.
- Проверяйте и «хороший» сценарий, и деградацию: малая выборка, редкое событие, дисбаланс групп.
- Coverage интервалов проверяйте отдельно от power: интервал может быть «узким и мимо».
- После каждого изменения метода перезапускайте всю калибровку — один твит в SE ломает всё остальное.
На практике
В проекте A/B Testing Methodology Toolkit каждый из 15 модулей собран по этому рецепту: генератор с seed, A/A-проверка, power/coverage-кривые. На выходе — не «используйте t-критерий», а набор проверенных решений: SRM-чек на входе, CUPED, delta-method для CTR, alpha-spending границы, сегментные ATE с поправкой Benjamini–Hochberg, проверка на novelty/primacy. Тесты лежат в репозитории и перезапускаются в CI — метод не может «протухнуть» без того, чтобы это заметили.
На собеседовании
❓ Как убедиться, что новый метод A/B-анализа работает?
Прогнать на симуляции с известной истиной. Первый тест — A/A: при эффекте 0 доля «значимых» результатов должна быть в пределах шума вокруг α (обычно 0.05). Второй — power/coverage: при реалистичном эффекте метод должен ловить его с ожидаемой вероятностью, а доверительные интервалы — накрывать истинное значение в ~95% прогонов. Калибровать надо на распределении своих данных, включая спец-кейсы продакшена (ratio-метрики, peeking, сегментные резы).
— Nikita Boyarkin
❓ Почему naive peeking опасен и что с ним делать?
При постоянном мониторинге p-value вероятность увидеть «значимость» под нулевой гипотезой растёт с каждым просмотром — к концу эксперимента она далека от заявленных 5%. Лечится либо alpha-spending границами (Pocock, O’Brien–Fleming), которые заранее расходуют бюджет ошибки по просмотрам, либо always-valid методами вроде mSPRT, которые позволяют смотреть когда угодно без штрафа. Оба подхода проверяются той же A/A-симуляцией.
— Nikita Boyarkin
Ссылки
- A/B Testing Methodology Toolkit — 15 калиброванных модулей с симуляциями
- Volta Neobank — боевое применение: CUPED + AA-тест + Bonferroni в одном кейсе
- Почему мы перешли на байесовское A/B-тестирование — альтернативный взгляд на peeking