Фрея · риск-менеджер необанка «Монетка»
Продуктовая команда прислала новый отчёт по кешбэку. Смотрели результат каждый день, разрезали по городам, сегментам и возрастам и нашли: у клиентов 26-35 лет кешбэк работает, p = 0,026. В группах 1006 и 1024 клиента, пишут «почти поровну». Чему тут верить?
Каждое утверждение отчёта по отдельности выглядит честно: тест настоящий, p-value посчитан правильно. Подводит то, как ими пользовались. Сегодня разберём четыре ловушки, в которые попадают даже аккуратные команды, и на каждую найдём проверку.
Подглядывание: сто попыток вместо одной
В уроке 68 Сири ворчала, что смотреть p-value каждый день нельзя. Посмотрим, насколько это плохо. Разыграем 2000 A/A-тестов: обе группы получают одно и то же, конверсия 10%, по 100 новых клиентов в каждую группу в день, 28 дней. Каждый день считаем z-тест по накопленным данным:
rng = np.random.default_rng(68)
sims, days, daily, p = 2000, 28, 100, 0.10
x_a = rng.binomial(daily, p, size=(sims, days)).cumsum(axis=1)
x_b = rng.binomial(daily, p, size=(sims, days)).cumsum(axis=1)
n = daily * np.arange(1, days + 1) # клиентов в группе к концу дня
pooled = (x_a + x_b) / (2 * n)
z = (x_b - x_a) / n / np.sqrt(pooled * (1 - pooled) * 2 / n)
p_values = 2 * stats.norm.sf(np.abs(z)) # матрица 2000 тестов на 28 дней
(p_values[:, -1] < 0.05).mean() # 0.0555: смотрим только в конце
(p_values < 0.05).any(axis=1).mean() # 0.2785: смотрим каждый день
Если смотреть один раз в конце, ложная тревога случается в 5,6% тестов, как и обещает alpha. Если останавливаться в первый день с p < 0,05, тест без всякого эффекта «срабатывает» в 27,9% случаев. Впятеро чаще.
Почему так? z по накопленным данным гуляет, как пьяный по коридору: шаг вправо, шаг влево. Вероятность оказаться за порогом 1,96 в конкретный день мала, но за 28 дней пьяный успевает хоть раз дотронуться до стены. 22% тестов из 2000 касались порога в какой-то день и возвращались обратно к концу срока. Команда, которая остановилась бы в этот день, записала бы ложную победу.
Реже смотреть тоже помогает лишь отчасти: при проверке раз в неделю ложных тревог 12,8%. Честных способов два. Первый: срок теста фиксируют до старта по расчёту из урока про мощность и решение принимают один раз. Второй: последовательные тесты, где порог на каждый взгляд заранее строже. Их в курсе не разбираем, но знай, что «смотреть можно» только с ними.
Много срезов: кто-нибудь да выиграет
Отчёт команды: главная метрика, доля клиентов с кафе, не дала значимого результата (p = 0,053, урок 68). Тогда её разрезали: 14 городов, 2 сегмента, 5 возрастов, всего 21 срез. В двух p < 0,05: возраст 26-35 (p = 0,026) и сегмент mass (p = 0,041).
Каждый срез это отдельный тест с шансом ложной тревоги 5%. Шанс, что из 21 теста хоть один ложно сработает, называют FWER:
1 - 0.95 ** 21 # 0.659
66%. При 21 проверке в среднем 1,05 среза сработают просто так, а у нас сработали два. Ничего необычного.
Защита: поправка на множественные сравнения. Самая простая, поправка Бонферрони: делим alpha на число тестов. Порог становится 0,05 / 21 = 0,0024, и ни один срез его не проходит.
Бонферрони строг: при сотне метрик он пропустит и настоящие эффекты. Поправка Холма так же защищает FWER, но мягче. Сортируем p-value по возрастанию. Самое маленькое сравниваем с alpha / m, второе с alpha / (m - 1), третье с alpha / (m - 2) и так далее, пока очередное не провалится: на нём и на всех следующих останавливаемся. Удобнее считать скорректированные p-value: k-е по порядку умножают на m - k + 1, и каждое следующее не может быть меньше предыдущего. Для среза 26-35 это 0,0256 * 21 = 0,54. Больше 0,05, эффект не доказан. Эту поправку ты соберёшь в квесте ★★.
Третий вариант мягче обоих: поправка Бенджамини-Хохберга. Она защищает не от любой ложной тревоги, а держит долю ложных среди найденных, её называют FDR. В scipy 1.14 она есть готовая:
stats.false_discovery_control(p_values) # для наших срезов минимум 0.342
Даже по ней ни один срез не значим. Срез «26-35» годится как гипотеза для следующего теста, а не как вывод этого.
урок из курса
Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.