Если в одном A/B-тесте ты смотришь не на одну метрику, а сразу на десять, то почти наверняка найдёшь «значимый» эффект там, где на самом деле ничего не изменилось. Это и есть проблема множественных сравнений: при фиксированном пороге alpha каждый новый тест добавляет свой шанс на ложное срабатывание, и суммарная вероятность ошибки растёт быстро. Лечится это поправками. Бонферрони действует в лоб — делит порог на число тестов и потому строг до консервативности. Бенджамини-Хохберг (BH) мягче: он контролирует не «хотя бы одну ошибку», а долю ошибок среди тех гипотез, что ты отверг. Ниже разберу, когда какую брать, и покажу на реальном наборе метрик.
Почему при множестве тестов растёт вероятность ложного открытия?
Одиночный тест на уровне alpha = 0.05 означает простую вещь: если верна нулевая гипотеза (эффекта нет), ты всё равно объявишь его существующим примерно в 5% случаев. Это встроенная плата за метод. Проблема в том, что 5% — это на один тест.
Проверяешь m независимых гипотез — и вероятность не ошибиться ни разу равна 0.95 в степени m. Значит, вероятность хотя бы одной ложной находки — это 1 - 0.95^m. Считается в две строчки:
alpha = 0.05
for m in [1, 5, 10, 20]:
fwer = 1 - (1 - alpha) ** m
print(m, round(fwer, 3))
# 1 0.05
# 5 0.226
# 10 0.401
# 20 0.642
Для 10 метрик это уже 40%, для 20 — около 64%. То есть на дашборде из двадцати метрик (от DAU до конверсии и выручки) почти наверняка что-то «позеленеет» просто от шума. И это ещё оптимистичная оценка: продуктовые метрики почти всегда скоррелированы — конверсия, ARPU и retention тянут друг друга, — поэтому реальная картина сложнее, но суть та же. Чем больше смотришь, тем выше риск принять шум за сигнал.
Что такое FWER и FDR и чем они отличаются?
Прежде чем выбирать поправку, нужно решить, что именно ты держишь под контролем. Есть две разные величины, и половина путаницы вокруг темы — от того, что их смешивают.
FWER (family-wise error rate) — вероятность совершить хотя бы одну ошибку первого рода во всём семействе тестов. Держишь FWER на уровне 0.05 — значит, с вероятностью 95% среди всех твоих выводов нет ни одной ложной находки. Гарантия строгая: одна ошибка на всю пачку уже считается провалом.
FDR (false discovery rate) — ожидаемая доля ложных открытий среди отвергнутых гипотез. Держишь FDR на 0.05 — значит, из тех метрик, что ты назвал значимыми, в среднем около 5% окажутся ложными. Здесь ты заранее миришься с небольшим процентом мусора ради того, чтобы не пропустить настоящие эффекты.
Разница чисто практическая. FWER уместен, когда цена ложного вывода высока и на каждом «открытии» строится решение. FDR — когда ты делаешь скрининг, гипотез много, и потерять реальный эффект хуже, чем протащить пару ложных. Запомнить легко: Бонферрони контролирует FWER, BH контролирует FDR.
Как работает поправка Бонферрони?
Самая простая поправка из всех. Хочешь удержать FWER на уровне alpha при m тестах — сравнивай каждый p-value не с alpha, а с alpha / m. Для пяти метрик и alpha = 0.05 порог становится 0.01. Эквивалентно можно умножить сами p-value на m и сравнивать с исходным alpha — это и есть «скорректированный» p-value.
Почему это работает: сумма вероятностей отдельных ошибок не превышает m * (alpha / m) = alpha. Метод не требует независимости тестов — он справедлив вообще всегда, при любой структуре зависимостей. В этом его сила и повод любить его за надёжность. Но за универсальность платишь мощностью: при большом m порог становится настолько жёстким, что реальные, но умеренные эффекты просто не проходят.
Когда Бонферрони слишком строгая?
Проблема вылезает, когда тестов десятки. Порог alpha / m не различает, скоррелированы гипотезы или нет, и потому переусердствует, если метрики связаны — а в продукте они связаны почти всегда. В итоге ты защищаешься от ложных находок ценой пропущенных настоящих: статистическая мощность падает, и часть реальных улучшений остаётся «незначимой». Для скрининга это плохой размен.
Если FWER всё-таки нужен, но Бонферрони душит, посмотри на метод Холма — это пошаговая (step-down) версия. p-value сортируются по возрастанию, самый маленький сравнивается с alpha / m, следующий с alpha / (m - 1), и так далее, пока не встретится первый непройденный порог. Холм даёт ровно ту же гарантию FWER, но отвергает не меньше гипотез, чем Бонферрони, а обычно больше. Если ты уже тянешься к Бонферрони — Холм почти всегда строго лучше и не стоит ничего лишнего.
Как устроена процедура Бенджамини-Хохберга?
BH меняет саму цель: не «ноль ошибок с вероятностью 95%», а «в среднем не больше 5% мусора среди находок». Алгоритм пошаговый и считается руками за минуту:
- Отсортируй все
mp-value по возрастанию:p(1) <= p(2) <= ... <= p(m). - Для каждого ранга
kпосчитай персональный порог(k / m) * alpha. - Найди самый большой
k, при которомp(k) <= (k / m) * alpha. - Отвергни все гипотезы с рангами от 1 до этого
kвключительно — даже те внутри, что не проходили свой личный порог.
Ключевой момент — шаг 4: BH отвергает всё до найденной границы разом. Метод контролирует FDR при независимых тестах и при так называемой положительной зависимости — а именно она обычно и есть у продуктовых метрик. Поэтому на практике BH применяют смело и без лишней паранойи.
Пример: пять метрик в одном A/B-тесте
Возьмём один тест новой формы онбординга. Мы заранее договорились смотреть пять метрик и получили такие p-value:
conversion_rate— 0.008arpu— 0.018retention_d7— 0.025avg_session_len— 0.028support_contacts— 0.620
Сами метрики по группам считаются обычным SQL — это удобно отрепетировать в SQL-тренажёре:
select
variant,
count(*) as users,
avg((converted = 1)::int) as conversion_rate,
avg(revenue) as arpu
from experiment_users
group by variant;
А дальше сравним три подхода — без поправки, Бонферрони и BH. Руками считать необязательно, всё есть в statsmodels:
import pandas as pd
from statsmodels.stats.multitest import multipletests
pvals = pd.Series({
'conversion_rate': 0.008,
'arpu': 0.018,
'retention_d7': 0.025,
'avg_session_len': 0.028,
'support_contacts': 0.620,
})
res = pd.DataFrame({'p': pvals})
for method in ['bonferroni', 'fdr_bh']:
reject, p_adj, _, _ = multipletests(pvals, alpha=0.05, method=method)
res[f'{method}_p_adj'] = p_adj.round(3)
res[f'{method}_sig'] = reject
print(res)
Что получается:
- Без поправки значимы четыре метрики из пяти (все, кроме обращений в поддержку). Но мы уже знаем, что при пяти тестах это риск — часть «значимости» может быть шумом.
- Бонферрони поднимает порог до
0.01и оставляет значимой только конверсию (0.008, скорректированный p-value0.04). ARPU с0.018уже не проходит, хотя эффект вполне может быть настоящим. - BH пересчитывает скорректированные p-value примерно к
0.035для первых четырёх метрик — и все четыре остаются значимыми, а поддержка отсеивается.
Разница наглядная: на одних и тех же данных Бонферрони находит один эффект, BH — четыре. Кто «прав», зависит от контекста. Если это финальное решение о раскатке и ошибка дорога — жёсткость Бонферрони оправдана. Если это исследовательский прогон, и победителей ты всё равно перепроверишь отдельным подтверждающим тестом — BH даёт меньше ложных «незначимо» и не даёт выбросить реальные улучшения. Прокрутить этот расчёт на своих числах можно в Python-тренажёре.
Бонферрони или BH — что выбрать?
Короткое правило, которым пользуюсь сам:
- Мало тестов (2–4), высокая цена ошибки, финальное решение о запуске — Бонферрони или Холм. Строгий контроль FWER, а потеря мощности при маленьком
mневелика. - Много тестов (десятки метрик, скрининг сегментов, куча срезов) — BH. Контроль FDR, разумный баланс, не выбрасывает реальные эффекты пачками.
- Одна заранее объявленная главная метрика — вообще без поправки. Если ты до старта зафиксировал одну primary-метрику, множественности нет, и корректировки не нужны. Остальные метрики становятся вторичными — для них можно отдельно прогнать BH как guardrail-скрининг.
Последний пункт важнее первых двух. Самая надёжная защита от проблемы множественных сравнений — не поправка, а дисциплина: одна главная метрика, объявленная до эксперимента. Поправки нужны там, где без множественности реально не обойтись, а не как способ задним числом спасти отчёт.
Частые ошибки при множественных сравнениях
Несколько граблей, на которые я насмотрелся:
- Подглядывание в тест (peeking). Каждый раз, когда ты проверяешь p-value по ходу набора данных и готов остановиться при «значимости», ты фактически проводишь много тестов во времени. Это отдельный вид множественности, и лечится он не Бонферрони, а последовательными методами (sequential testing, alpha-spending).
- Поправка задним числом. Нельзя посмотреть на десять метрик, выбрать самую красивую и «забыть» про остальные. Число тестов
m— это сколько ты реально проверял, а не сколько показал в презентации. - Смешивание семейств. Продуктовые метрики и технические логи — разные семьи гипотез. Не обязательно засовывать всё в один
m; честнее делить на осмысленные группы и корректировать внутри каждой. - BH на сильно отрицательно зависимых тестах. Гарантия FDR у BH держится при независимости или положительной зависимости. Встречается редко, но если метрики устроены как «одна вверх — другая строго вниз», стоит быть аккуратнее.
Как закрепить это на практике?
Теорию про FWER и FDR любят спрашивать на собеседованиях аналитиков — формулировка «у тебя 20 метрик в тесте, как не обмануться» звучит почти дословно. Чтобы это осело, лучше один раз прогнать руками: посчитать метрики по группам в SQL-тренажёре, прокрутить multipletests на своих p-value, разобрать пару похожих ситуаций в кейсах и порешать задачи по статистике. Если хочется системно — от базовых запросов в курсе SQL до типичных вопросов на собеседованиях.
Полный доступ ко всем задачам, кейсам и AI-разбору собеседований открывается в Pro, но начать можно бесплатно — и Бонферрони с BH точно попадут в бесплатную часть тренажёра.