Множественные сравнения в A/B: Бонферрони, Холм и FDR
Почему пять проверок дают ложные находки чаще одной, как работают Бонферрони, Холм и Бенджамини — Хохберг. Расчёт по пяти каналам учебного A/B-теста.
Содержание статьи
Множественные сравнения нужны, когда команда проверяет несколько метрик, вариантов или сегментов и готова объявить победой любой маленький p-value. При пяти независимых истинных нулевых гипотезах вероятность хотя бы одного p<0,05 составляет 1−0,95⁵ ≈ 22,6%, а не 5%. Бонферрони и Холм ограничивают вероятность хотя бы одного ложного открытия в заранее заданной семье; процедура Бенджамини — Хохберга контролирует ожидаемую долю ложных открытий среди сделанных при своих предпосылках. Поправка не исправляет подглядывание во времени или выбор семьи после просмотра результата.
Почему «посмотрели ещё четыре разреза» меняет риск
Продакт получил значимый общий D7-результат и просит показать разницу по каналам. Аналитик открывает пять разрезов и отмечает любой, где p<0,05. Даже если реальных сегментных эффектов нет, больше проверок дают больше шансов встретить маленькое p случайно. Пример 22,6% получается из 1−(1−0,05)⁵ только при независимых проверках и всех истинных нулях; реальные метрики и сегменты часто коррелированы, поэтому эту цифру нельзя переносить на любой отчёт.
Задайте семью гипотез исходя из решения: какие результаты будут считаться альтернативными основаниями для одного и того же запуска или сегментного действия? Одна заранее объявленная основная метрика может иметь отдельную процедуру принятия решения, а пять разведочных каналов — отдельную семью. Если тестируются пять метрик и десять сегментов с правом выбрать любую красивую ячейку, нельзя незаметно объявить семь удачных ячеек пятью проверками.
Практический тест для семейства: представьте, что все проверяемые эффекты на самом деле нулевые. Какие из выписанных результатов позволили бы команде нажать кнопку запуска? Если достаточно любой одной зелёной ячейки, эти ячейки объединены одним риском решения. Если один primary уже определяет запуск, а остальные служат только диагностикой механизма без права переписать вывод, правила другие. Сохраните список гипотез и версию протокола рядом с экспериментом: слово «семейство» без перечня не позволяет проверить, сколько попыток было сделано.
FWER и FDR отвечают на разные риски
FWER — вероятность сделать хотя бы одно ложное отклонение в семье гипотез. Если ошибочный релиз в одном сегменте дорог, контроль FWER подходит как защитная цель. FDR — математическое ожидание доли ложных отклонений среди всех отклонённых: E[V/max(R,1)], где V — ложные, R — все объявленные находки. При большом потоке разведочных гипотез команда может принять контролируемую долю ошибок ради обнаружения большего числа сигналов, которые потом перепроверит.
Различие особенно видно при нескольких реальных открытиях: FDR может разрешать более мягкие пороги, чем FWER. Но если найден всего один сигнал, даже маленькое рассчитанное q не заменяет регистрацию теста и качество данных. Значения FDR не означают «вероятность, что именно этот канал ложный»; это свойство процедуры повторяемых семей проверок при условиях метода.
| Цель | Формальное выражение | Типичное применение |
|---|---|---|
| FWER | P(V≥1) ≤ α | одно ошибочное действие нежелательно |
| FDR | E[V/max(R,1)] ≤ q | много разведочных гипотез с повторной проверкой |
Бонферрони: самый простой порог
При m заранее перечисленных гипотезах сравнивайте каждое исходное p с α/m; эквивалентно умножьте p на m и ограничьте результат единицей. При пяти проверках и α=0,05 порог становится 0,01. Даже если проверки зависимы, неравенство объединения событий даёт контроль FWER при корректных отдельных p-value. Цена простоты — потеря мощности, особенно если проверки положительно связаны и могли бы использовать более эффективную процедуру.
Поправка относится к семье, а не к количеству колонок в таблице как таковых. Если до запуска объявлена одна основная метрика и четыре диагностических guardrail с отдельными правилами неухудшения, механическое умножение всех p на пять может быть не тем решением. Если после отчёта выбирается любая из двадцати колонок как «основная», поправка на пять уже слишком мягкая. Сначала опишите правила принятия решения.
p_adj = min(1, m · p)Порог для исходного p: α/m; m — число гипотез семьи.
Холм: последовательный контроль FWER
Метод Холма сортирует p по возрастанию: p₍₁₎≤…≤p₍ₘ₎. Сначала сравните наименьшее с α/m, следующее — с α/(m−1) и так далее. Как только проверка не проходит порог, дальше не отклоняйте. Для скорректированных p умножьте p₍ᵢ₎ на m−i+1 и возьмите накопленный максимум слева, ограничив единицей. В отличие от простого Бонферрони, последующие шаги могут быть мягче при тех же гарантиях FWER.
Холм применим при произвольной зависимости p-value, если отдельные p корректны. Его преимущество не означает, что он всегда обнаружит больше в конкретном отчёте: иногда результаты Бонферрони и Холма совпадают. В нашем примере единственный очень маленький p стоит первым, а следующий уже далеко от порога. Поэтому решения по пяти каналам у этих процедур одинаковы.
Бенджамини — Хохберг: когда нужна FDR
Для BH отсортируйте p и найдите наибольший индекс k, для которого p₍ₖ₎≤(k/m)·q. Отклоните гипотезы с индексами до k включительно. Скорректированные p получают проходом справа налево через минимум m·p₍ᵢ₎/i. При независимых тестах BH контролирует FDR на уровне q; результат распространяется и на некоторые формы положительной зависимости при дополнительных условиях. Для произвольной зависимости без дополнительных поправок такого общего обещания нет.
Если цель — выбрать конкретный канал для немедленной дорогой раскатки, FDR может быть неподходящим риском, даже если расчёт сделан верно. Если цель — сформировать очередь кандидатов для повторного эксперимента, FDR полезнее. Название процедуры не сообщает, какое именно действие разрешено; это зависит от цены ложной находки и плана подтверждения.
p₍ᵢ₎ ≤ (i/m) · qВыбирается наибольший проходящий индекс; перечисление семьи фиксируется до чтения p.
Пять каналов DragonKeep: назначение и исход
Возьмём учебный эксперимент onboarding_v2. Вариант берётся из exposures.variant, канал установки — из users.channel, D7 — наличие хотя бы одного сеанса ровно через семь дней после install_date в объединённых sessions.csv и дополнительном файле D7. Каждый игрок принадлежит одному каналу, поэтому пять каналов не пересекаются по людям. Общий первичный D7-анализ сделан отдельно; эти пять контрастов — разведочный пример, а не заранее зарегистрированная сегментная семья.
Проверяем в каждом канале двустороннюю разницу долей z-тестом с объединённой нулевой оценкой вероятности. Корректируем пять полученных p. Если бы эту семью закрепили до просмотра данных, методы можно было бы использовать для заявленных гарантий. Здесь постфактум применение поправки показывает арифметику и более честный масштаб множественности, но не превращает увиденный сигнал в подтверждённый заранее сегментный эффект.
Каналы непересекаются по пользователям, но оценки всё равно привязаны к одному экспериментальному запуску, его логированию и календарю. Не нужно автоматически объявлять их идеально независимыми только потому, что строки пользователей разные. Для Бонферрони и Холма независимость p не требуется; для обычного BH условия зависимости имеют значение. Эта разница помогает выбирать метод без спорного предположения о каждом канале. Она не устраняет другую проблему: если исследователь выбирал сами каналы после просмотра таблицы, число попыток больше пяти.
| Канал | Control → test | Δ, п.п. | p | Бонферрони | Холм | BH |
|---|---|---|---|---|---|---|
| paid_vk | 344/3990 → 449/3943 | +2,766 | 0,000040 | 0,000201 | 0,000201 | 0,000201 |
| paid_google | 158/1118 → 189/1126 | +2,653 | 0,082252 | 0,411260 | 0,329008 | 0,205630 |
| organic | 359/1844 → 376/1796 | +1,467 | 0,270378 | 1 | 0,811133 | 0,380997 |
| referral | 206/880 → 239/938 | +2,071 | 0,304798 | 1 | 0,811133 | 0,380997 |
| influencer | 73/702 → 79/666 | +1,463 | 0,389455 | 1 | 0,811133 | 0,389455 |
Код: одна семья и три процедуры
Скрипт заново берёт назначение, установки и события; показатели не подставлены вручную. Для краткости выводит скорректированные p первого канала, но рассчитывает все пять. Функция multipletests могла бы заменить ручные три строки при установленном statsmodels; здесь алгоритм написан явно, чтобы читатель видел порядок и накопленные min/max. Проверка также страхует от случая, когда файл D7-сеансов забыли включить.
import numpy as np
import pandas as pd
from scipy.stats import norm
base='dataset/gamedev/'
e=pd.read_csv(base+'exposures.csv',parse_dates=['exposure_date'])
u=pd.read_csv(base+'users.csv',parse_dates=['install_date'])
s=pd.concat([pd.read_csv(base+'sessions.csv',parse_dates=['session_date']),pd.read_csv(base+'extra_sessions_onboarding_v2.csv',parse_dates=['session_date'])])
a=e.loc[e.exp_id.eq('onboarding_v2'),['user_id','variant']].merge(u[['user_id','channel','install_date']],on='user_id')
d7=set(zip(s.user_id,s.session_date))
a['hit']=[int((uid,day+pd.Timedelta(days=7)) in d7) for uid,day in zip(a.user_id,a.install_date)]
rows=[]
for name,g in a.groupby('channel'):
t=g.groupby('variant').hit.agg(['sum','count'])
x,n=t.loc['control']; y,m=t.loc['test']
pooled=(x+y)/(n+m)
z=(y/m-x/n)/np.sqrt(pooled*(1-pooled)*(1/n+1/m))
rows.append((name,2*norm.sf(abs(z))))
rows.sort(key=lambda v:v[1])
p=np.array([v[1] for v in rows]); m=len(p); rank=np.arange(1,m+1)
bonf=np.minimum(1,m*p)
holm=np.maximum.accumulate(np.minimum(1,p*(m-rank+1)))
bh=np.minimum.accumulate(np.minimum(1,p*m/rank)[::-1])[::-1]
print(rows[0][0],round(p[0],8),round(bonf[0],8),round(holm[0],8),round(bh[0],8))
assert rows[0][0]=='paid_vk' and abs(bh[0]-.0002011)<1e-7Что означает один оставшийся сигнал
При пороге 0,05 только paid_vk проходит все три поправки: скорректированное p около 0,000201. Остальные каналы не проходят даже исходный порог. Общий D7-тест тоже имеет маленькое p, но это не доказательство, что эффект именно paid_vk отличается от эффектов других каналов. Проверка «один сегмент значим, другой нет» не является тестом разности двух сегментных эффектов.
Если бизнес хочет отдельную раскатку по каналам, нужен заранее сформулированный вопрос о взаимодействии канал×вариант, его интервал и план последующей проверки. На этой таблице разумно сказать: общий результат улучшился, а paid_vk — интересная гипотеза для подтверждения. Не используйте эту разведку как безусловный сертификат исключительного эффекта в рекламном канале.
Разница между «эффект есть в канале» и «эффект в этом канале сильнее» принципиальна. Например, paid_vk показывает +2,766 п.п., paid_google +2,653 п.п.: точечные эффекты похожи, хотя p сильно различаются из-за размеров групп и базового уровня. Фраза «работает только во ВКонтакте» по такой таблице особенно плохо обоснована. Прямое сравнение эффектов должно оценить разность +2,766−2,653=+0,113 п.п. и её неопределённость, а не просто сравнить два p-value. Отдельный сегментный релиз требует ещё и оценки того, изменится ли состав канала после запуска.
Категории — каналы; показан BH. Для решения сравнивайте с горизонтальным мысленным порогом 0,05 и помните разведочный статус.
Какая поправка нужна для какого решения
Если одно ошибочное срабатывание означает дорогостоящую персональную кампанию, выберите контроль FWER и часто — Холма: при корректных p он не слабее Бонферрони по мощности и сохраняет гарантию при зависимости. Для простого заранее заданного одиночного primary вовсе не обязательно смешивать его с каждой диагностикой в один мешок; опишите семейства и иерархию проверки. Для разведки большого перечня идей, каждая из которых потом пройдёт независимую проверку, рассмотрите BH и явно назовите уровень q.
Ни одна поправка не выбирает семейство за аналитика. Новые каналы, добавленные после просмотра первых p, меняют исходную процедуру. Если планировали пять, но успели посмотреть двадцать и отчитались о пяти «интересных», ни одно из показанных скорректированных p не отражает полного поиска. В хабе A/B-тестирования можно вернуться к выбору основной метрики и протоколу до запуска.
Поправка не спасает от повторного подглядывания
Просмотр одного и того же p каждый день с правом остановиться на красивом результате создаёт ещё один источник ложных находок — последовательные проверки по времени. Бонферрони на пять каналов не делает такой мониторинг корректным, если не учтены все моменты и правило остановки. Нужен заранее определённый конец теста либо последовательный дизайн с соответствующими границами и моделью.
SRM, потери событий и неправильно измеренная метрика ломают отдельные p до любой коррекции. Если menu_color имеет поломанный 54/46 сплит, сортировка p по каналам не возвращает ему честное назначение. Разбор SRM, peeking и multiple checks показывает порядок диагностики: сначала качество эксперимента, затем статистика решения.
Пять ошибок в отчёте о множественных сравнениях
Первая: поправить только найденные маленькие p и забыть проверенные, но не опубликованные гипотезы. Риск ложного открытия вырастет. Вторая: назвать BH контролем вероятности хотя бы одной ошибки. Это FWER, а BH контролирует ожидаемую долю среди находок. Третья: трактовать p=0,03 в одном канале и p=0,08 в другом как доказательство разной силы эффекта. Для этого нужен прямой тест взаимодействия.
Четвёртая: считать шесть ежедневных просмотров одним финальным анализом. Это подглядывание, не исправляемое поправкой на сегменты. Пятая: добавить после просмотра ещё десять метрик и сохранить старый знаменатель m=5. Шестая: применять BH при произвольной зависимости и обещать строгий FDR без проверки условий. Каждая ошибка меняет не красоту таблицы, а вероятность ошибочного решения.
Частые вопросы
Сколько p-value можно смотреть без поправки? Вопрос не в запрете смотреть, а в том, какие из них дают право объявить успех. Предзаданный одиночный primary отличается от поиска любого удачного результата среди десятков.
Холм всегда лучше Бонферрони? При тех же корректных исходных p и FWER-порогах он не менее мощен, но в конкретной таблице число находок может совпасть.
BH даёт вероятность ошибки каждой находки? Нет. FDR относится к ожидаемой доле ложных решений при повторении процедуры, а не к posterior probability отдельной гипотезы.
Если общий тест значим, можно не корректировать сегменты? Общая значимость не делает отдельные сегментные заявления подтверждёнными и не доказывает различие эффектов между каналами.
Решение и источники метода
По DragonKeep не меняйте продукт для одного paid_vk только на основании постфактум сегментного p. Зафиксируйте этот сигнал как гипотезу и проверьте взаимодействие или повторите тест. Общую раскатку оценивайте по заранее заданной основной метрике и guardrail. В отчёте перечислите всё семейство, порог, выбранную процедуру, исходные и скорректированные p, а также дату, когда семья была определена.
Первичные публикации: Holm, 1979 о последовательном контроле FWER и Benjamini & Hochberg, 1995 о FDR. Для практической подготовки семей гипотез начните с основной метрики и guardrail, а размеры следующего подтверждающего теста прикиньте в калькуляторе выборки.
Материалы по теме

Ошибки первого и второго рода в A/B-тесте: цена решения
Что такое α, β и мощность, как размер выборки меняет риск и почему p-value не доказывает отсутствие эффекта. Симуляция 1 000 экспериментов на данных DragonKeep.

Бутстреп в статистике: интервалы для метрик A/B-теста
Как построить процентильный бутстреп по пользователям: разница средней выручки, медиана и ARPPU на учебном A/B-тесте, код Python и ограничения метода.

Ratio-метрики в A/B-тесте: дельта-метод и кластеры
Как считать отношение сумм, дельта-метод, линеаризацию и бутстреп по пользователям. На сессиях учебного A/B-теста среднее пользовательских долей меняет знак.