Ждать, пока A/B‑тест прокрасится, — плохая идея. Объясняю на цифрах

У каждого продуктового аналитика есть знакомый сценарий. Фичу раскатывают лесенкой 5% → 25% → 50% → 100%, на каждой ступеньке смотрят метрики, а на 100% начинается ожидание «прокраса». Сегодня p = 0.07 — «почти, подождём». Через пару дней p = 0.03, но эффект ниже MDE — «подождём, вдруг дорастёт». Ещё через неделю p = 0.04 — «прокрасилось», скриншот в чат.

Каждый шаг здесь выглядит разумно, но вместе они ломают статистику. Ниже разберём, насколько сильно ломают, почему, и какие есть честные способы смотреть в тест часто, даже если заранее неизвестно, сколько раз это придётся делать.

Что на самом деле считает z‑тест

Возьмём метрику со средним в контролеmu_A и в тестеmu_B. Послеn пользователей в каждой группе статистика выглядит так:

Z_n = frac{bar X_B - bar X_A}{sqrt{2sigma^2 / n}}

При верной нулевой гипотезе (mu_A = mu_B)Z_n sim N(0, 1), и правило «отвергаем при  1.96″ alt=»|Z_n| > 1.96″ src=»https://habrastorage.org/extt/9a/ab/8b/9aab8b3013571106a513d7bb2b8a5254.svg» width=»106″ height=»22″ data-width=»10.991″ data-height=»2.26″ data-vertical-align=»-0.564″ style=»width: 10.991ex; height: 2.26ex; vertical-align: -0.564ex» sizes=»(max-width: 780px) 100vw, 50vw» srcset=»https://habrastorage.org/extt/9a/ab/8b/9aab8b3013571106a513d7bb2b8a5254.svg 780w, https://habrastorage.org/extt/9a/ab/8b/9aab8b3013571106a513d7bb2b8a5254.svg 781w» loading=»lazy» decode=»async»>» ошибается с вероятностью ровно 5%.

Важная оговорка: это верно для одногоn, выбранного заранее. Если же смотреть наZ_{n_1}, Z_{n_2}, dots, Z_{n_K} и останавливаться при первом пересечении порога, то ошибка первого рода считается уже по‑другому:

1.96 mid H_0right)» alt=»alpha_{text{факт}} = Pleft(exists k le K: |Z_{n_k}| > 1.96 mid H_0right)» src=»https://habrastorage.org/getpro/habr/formulas/4/43/438/4385ce26cd066f46fe8a6ec1bbb99753.svg» width=»305″ height=»19″ data-width=»38.146″ data-height=»2.417″ data-vertical-align=»-0.643″ style=»width: 38.146ex; height: 2.417ex; vertical-align: -0.643ex» sizes=»(max-width: 780px) 100vw, 50vw» srcset=»https://habrastorage.org/getpro/habr/formulas/4/43/438/4385ce26cd066f46fe8a6ec1bbb99753.svg 780w, https://habrastorage.org/getpro/habr/formulas/4/43/438/4385ce26cd066f46fe8a6ec1bbb99753.svg 781w» loading=»lazy» decode=»async»>

И это число всегда больше 5%.

Насколько больше? Цифры

Если бы взгляды были независимы, ответ был бы простым:

alpha_{text{факт}} = 1 - (1 - 0.05)^K

При 10 взглядах это 40%, при 21 — 66%. Но взгляды зависимы: сегодняшние данные — это вчерашние плюс ещё немного. Корреляция между двумя взглядами равна

<img class="formula" source="text{corr}(Z_{n_j}, Z_{n_k}) = sqrt{frac{n_j}{n_k}}, quad n_j < n_k" alt="text{corr}(Z_{n_j}, Z_{n_k}) = sqrt{frac{n_j}{n_k}}, quad n_j

Например, взгляды на 20-й и 21-й день скоррелированы на ~0.98. Поэтому реальная инфляция меньше, чем при независимости, но всё равно большая.

Это можно проверить моделированием на A/A‑тестах, где эффекта нет по построению. Типичная лесенка: 3 дня на 5% трафика, 4 дня на 25%, неделя на 50%, дальше 100%, плановая длительность 3 недели. Каждый день считаем z‑тест и останавливаемся при первом p < 0.05. Повторяем 100 000 раз:

Как смотрим

Ложных «прокрасов»

Один раз, в конце 3-й недели

5.1%

Каждый день, 1 неделя

19.8%

Каждый день, 2 недели

28.1%

Каждый день, 3 недели

33.6%

Каждый день, 6 недель («подождём ещё»)

40.3%

Вместо 5% ошибок получается каждый третий тест.

Почему так происходит: случайное блуждание

Удобнее всего смотреть не наZ_n, а на накопленную суммуS_n = Z_n sqrt{n}. ПриH_0 её приращения независимы, и в непрерывном пределеS ведёт себя как броуновское движениеW(t)(тут чуть‑чуть душно, но не переживайте:) ).

Для броуновского движения есть закон повторного логарифма:

limsup_{n to infty} frac{S_n}{sqrt{2n ln ln n}} = 1 quad text{почти наверное}

В переводе на язык z‑статистики:

limsup_{n to infty} frac{|Z_n|}{sqrt{2 ln ln n}} = 1

Знаменатель растёт бесконечно, хоть и очень медленно. Значит,|Z_n| рано или поздно пересечёт любой фиксированный порог, в том числе 1.96. Если ждать прокраса без ограничения по времени, вероятность ложного прокраса стремится к 100%.

Отдельно про «p = 0.07, подождём»

Этот сценарий стоит разобрать отдельно, потому что он самый частый. Из тех же A/A‑тестов берём те, у которых в конце 3-й недели p оказался между 0.05 и 0.10. Это примерно 5% тестов, классическое «почти». Продлеваем их ещё на 3 недели с ежедневными взглядами.

Прокрашиваются 53.5%.

Объяснение простое. При p = 0.07 статистика уже стоит у самого порога (|Z| approx 1.8), и случайному блужданию достаточно небольшого толчка. Ожидание не даёт новой информации об эффекте, оно только даёт шуму больше попыток.

С «p низкий, но эффект ниже MDE» та же проблема, только в другой форме. Остановка в момент, когда оценка впервые дотянулась до MDE, отбирает реализации, где шум сдвинул её вверх:

Delta» alt=»Eleft[hatDelta mid text{остановились рано}right] > Delta» src=»https://habrastorage.org/getpro/habr/formulas/3/37/37b/37b896e41af1c4f676cc151a729b4138.svg» width=»282″ height=»33″ data-width=»35.199″ data-height=»4.07″ data-vertical-align=»-1.469″ style=»width: 35.199ex; height: 4.07ex; vertical-align: -1.469ex» sizes=»(max-width: 780px) 100vw, 50vw» srcset=»https://habrastorage.org/getpro/habr/formulas/3/37/37b/37b896e41af1c4f676cc151a729b4138.svg 780w, https://habrastorage.org/getpro/habr/formulas/3/37/37b/37b896e41af1c4f676cc151a729b4138.svg 781w» loading=»lazy» decode=»async»>

Это проклятие победителя (winner’s curse): эффект, с которым тест «победил», систематически завышен.

‑‑-

Почему нельзя просто «не смотреть»

Классическое решение называется fixed horizon. Заранее считаем выборку под MDEdelta:

N = frac{2sigma^2 (z_{1-alpha/2} + z_{1-beta})^2}{delta^2}

и смотрим один раз. Это корректно, но при поэтапной раскатке неприменимо: лесенка и нужна для того, чтобы смотреть. Если фича на 5% роняет конверсию, об этом надо узнать на 5%.

Значит, нужна процедура, в которой взгляды разрешены, а итоговая ошибка остаётся 5%. Таких подходов три.

Подход 1. Границы O’Brien‑Fleming

Идея: на каждом взглядеk используем свой порогc_k вместо 1.96, подобранный так, чтобы

c_k mid H_0right) = alpha» alt=»Pleft(exists k: |Z_{n_k}| > c_k mid H_0right) = alpha» src=»https://habrastorage.org/getpro/habr/formulas/8/8c/8c3/8c31a5c9f1733a21660d9467fa53fb4d.svg» width=»217″ height=»18″ data-width=»27.069″ data-height=»2.289″ data-vertical-align=»-0.579″ style=»width: 27.069ex; height: 2.289ex; vertical-align: -0.579ex» sizes=»(max-width: 780px) 100vw, 50vw» srcset=»https://habrastorage.org/getpro/habr/formulas/8/8c/8c3/8c31a5c9f1733a21660d9467fa53fb4d.svg 780w, https://habrastorage.org/getpro/habr/formulas/8/8c/8c3/8c31a5c9f1733a21660d9467fa53fb4d.svg 781w» loading=»lazy» decode=»async»>

O’Brien и Fleming (1979) предложили такую форму дляK равноотстоящих взглядов:

c_k = C_{OBF}(K, alpha) cdot sqrt{frac{K}{k}}

КонстантаC_{OBF} подбирается численно. ДляK = 4, alpha = 0.05 она равна 2.024:

Взглядk

1

2

3

4

Порогc_k

4.05

2.86

2.34

2.02

Номинальный p

0.00005

0.004

0.019

0.043

У этой формы есть интуитивный смысл. Граница по суммеS_n постоянна, а поZ убывает как1/sqrt{k}. Ранние взгляды почти ничего не тратят, и на финальном пороге мы теряем совсем мало: 2.02 вместо 1.96. Плата за 4 взгляда — увеличение выборки всего примерно на 2%.

Для раскатки это идеальная форма: на ступеньках 5% и 25% данных мало, и любой «прокрас» там почти наверняка шум.

Почему не Бонферрони? 

Бонферрони даёт порог alpha / K = 0.0125 ( 2.50″ alt=»|z| > 2.50″ src=»https://habrastorage.org/extt/86/75/e4/8675e484d9f8b25bc9b99c230921c565.svg» width=»90″ height=»22″ data-width=»9.35″ data-height=»2.26″ data-vertical-align=»-0.564″ style=»width: 9.35ex; height: 2.26ex; vertical-align: -0.564ex» sizes=»(max-width: 780px) 100vw, 50vw» srcset=»https://habrastorage.org/extt/86/75/e4/8675e484d9f8b25bc9b99c230921c565.svg 780w, https://habrastorage.org/extt/86/75/e4/8675e484d9f8b25bc9b99c230921c565.svg 781w» loading=»lazy» decode=»async»>) на каждом взгляде, включая последний.

Он игнорирует корреляциюsqrt{n_j / n_k} и потому сильно консервативен. O’Brien‑Fleming эту корреляцию учитывает.

Ограничение: классические границы требуют заранее знатьK и моменты взглядов. На практике после раскатки на 100% никто не знает, сколько раз откроет отчёт.

Подход 2. Alpha‑spending: число взглядов знать не нужно

Lan и DeMets (1983) предложили тратитьalpha не по номеру взгляда, а по доле накопленной информации.

Информация — это обратная дисперсия оценки эффекта,I_n = 1 / text{Var}(hatDelta_n). Для простого теста она пропорциональна числу пользователей. Информационная доля:

t = frac{I_n}{I_{max}} approx frac{n}{N_{max}}

гдеN_{max} — выборка под MDE из формулы выше (обычно с запасом 3–5%).

Дальше задаётся функция тратalpha(t): неубывающая,alpha(0) = 0,alpha(1) = alpha. Она говорит, сколько ошибки разрешено «израсходовать» к моментуt. Аналог O’Brien‑Fleming:

alpha(t) = 2 - 2Phileft(frac{z_{1-alpha/2}}{sqrt{t}}right)

Посмотрим, как она тратит бюджет:

t

0.1

0.25

0.5

0.75

1.0

alpha(t)

~0.000000

0.0001

0.0056

0.0236

0.05

На каждом взглядеk порогc_k находится из условия

c_k, |Z_j| le c_j forall j c_k, |Z_j| le c_j forall j

то есть ровно столько, сколько разрешено потратить с прошлого взгляда.

Главное свойство: границы зависят только от фактическихt_1, t_2, dots, а не от заранее заданного $K$. Можно посмотреть три раза или пятнадцать, можно пропустить неделю. Итоговая ошибка остаётсяle alpha.

Нюанс раскатки:t считается по пользователям, а не по дням. День на 5% трафика даёт в 20 раз меньше информации, чем день на 100%. Для типичной лесенки:

День

Ступенька

t

Порогc_k

Номинальный p

3

5%

0.01

недостижим

—

7

25%

0.10

> 4.4

< 0.00001

14

50% → 100%

0.40

3.13

0.0018

16

100%

0.57

2.65

0.008

18

100%

0.74

2.40

0.017

21

100%

1.00

2.14

0.032

Что показывает моделирование:

  • ложных прокрасов 5.0% при 10 взглядах (против 33.6% при наивном подходе);

  • при 4 взглядах границы пересчитываются автоматически (∞; 3.13; 2.44; 2.02), и снова 5.0%;

  • мощность 77.6% против 80.1% у fixed horizon;

  • при реальном эффекте тест в среднем останавливается на 18-й день вместо 21-го.

Два‑три процентных пункта мощности — это цена права смотреть сколько угодно раз и в среднем заканчивать раньше.

Подход 3. mSPRT: без горизонта вообще

У alpha‑spending есть горизонтN_{max}: приt = 1 бюджет закончился. Если эксперимент может длиться неограниченно, нужны методы always valid inference.

mSPRT (mixture Sequential Probability Ratio Test; Robbins, 1970; Johari и соавторы, 2017) сравнивает правдоподобие данных приH_0 и при «смеси» альтернативDelta sim N(0, tau^2). Для нормальной статистики отношение правдоподобия выписывается явно:

Lambda_n = sqrt{frac{V}{V + ntau^2}} expleft(frac{n^2 tau^2 hatDelta_n^2}{2V(V + ntau^2)}right), quad V = 2sigma^2

ПриH_0 процессLambda_n — мартингал сE[Lambda_n] = 1. По неравенству Вилля

Pleft(exists n: Lambda_n ge frac{1}{alpha} mid H_0right) le alpha

Отсюда always valid p‑value:

p_n = minleft(1, frac{1}{max_{m le n} Lambda_m}right)

Его можно проверять хоть после каждого пользователя, бесконечно долго, и ошибка останетсяle alpha. Параметрtau обычно берут порядка MDE.

Цифры моделирования (ежедневные взгляды до 6 недель):

  • ложных прокрасов 1.8% (метод консервативен, гарантия ≤ 5%);

  • мощность 48% к 3-й неделе и 96% к 6-й.

За свободу платят длительностью: при той же мощности тест идёт в 1.5–2 раза дольше, чем fixed horizon.

Правило выбора: если можно назватьN_{max} и дедлайн, берите alpha‑spending. Если нет — mSPRT.

Ступеньки раскатки проверяют не то, что кажется

На ступеньках 5%, 25% и 50% вопрос не «стало ли лучше», а «не стало ли хуже». Это другая гипотеза:

<img class="formula" source="H_0^{text{вред}}: Delta ge -delta_{text{harm}} quad text{против} quad H_1: Delta < -delta_{text{harm}}" alt="H_0^{text{вред}}: Delta ge -delta_{text{harm}} quad text{против} quad H_1: Delta

Её проверяют односторонним тестом по защитным (guardrail) метрикам, со своим бюджетомalpha_{text{harm}}, обычно 0.01–0.02.

Ступенька

Вопрос

Инструмент

5%

Ничего не сломали?

Guardrails, односторонний тест на вред

25%

Нет ли сильного вреда?

То же + граница OBF по ключевой метрике

50%

Не стало ли хуже?

То же

100%

Стало ли лучше?

Alpha‑spending до $N_{max}$

«Катим дальше» означает «вреда не нашли». «Фича победила» — только на 100% и только при пересечении границы.

Ещё одна ловушка — неоднородность ступенек. Пользователи с 5% к концу теста находятся в нём дольше остальных, а ступеньки приходятся на разные дни недели. Если эффект меняется во времени (эффект новизны), простое объединение данных смещает оценку. Помогает стратифицированная оценка по ступенькам $s$:

hatDelta = frac{sum_s w_s hatDelta_s}{sum_s w_s}, quad w_s = frac{1}{text{Var}(hatDelta_s)}

Второй вариант — накапливать информацию для финального теста только с момента, когда доля трафика перестала меняться.

Три вердикта вместо двух

Желание «подождать» во многом рождается из бинарности: «прокрасилось / не прокрасилось». Второй исход воспринимается как провал. Ситуацию заметно упрощают три вердикта по доверительному интервалу[L, U] для эффекта:

  1. Значимо: граница пересечена иhatDelta ge delta. Катим.

  2. Значимо, но ниже MDE: граница пересечена, но<img class="formula inline" source="hatDelta < delta" alt="hatDelta . Эффект есть, но меньше заложенного. Это полноценный результат, и дальше вопрос к экономике фичи, а не к статистике.

  3. Меньше MDE: выборкаN_{max} набрана, граница не пересечена,<img class="formula inline" source="U < delta" alt="U . Эффекта размера MDE нет. Продлевать не нужно.

Короткое правило: p = 0.07 приt = 1 — это не «почти прокрасилось», а вердикт «меньше MDE».

Чек‑лист, что можно попробовать в своих экспериментах если есть время:)

  1. До старта посчитатьN_{max} под MDE с запасом 3–5% и зафиксировать функцию тратalpha(t).

  2. На 5–50% проверять guardrails односторонним тестом на вред.

  3. На 100% смотреть сколько угодно, на каждом взгляде пересчитываяt иc_k.

  4. Приt = 1 вынести один из трёх вердиктов.

  5. Если горизонта нет, с самого начала использовать mSPRT.

  6. Продлевать тест после t = 1 только по заранее записанной процедуре (например, адаптивный пересчёт выборки), а не «потому что почти».

  7. После ранней остановки указывать, что оценка эффекта, скорее всего, завышена (или приводить median‑unbiased оценку).

Ограничения

  • Моменты взглядов не должны выбираться по данным. 

    • «Смотрим каждый вторник» — норм.

    • «Посмотрим ещё раз, раз вчера было 0.06» формально нарушает гарантии alpha‑spending (mSPRT к этому устойчив).

  • Метрики‑отношения (средний чек, конверсия на сессию) работают, ноtext{Var}(hatDelta) нужно считать дельта‑методом или линеаризацией.

  • CUPED совместим: дисперсия падает, информация растёт, иt нужно считать поI_n, а не по числу пользователей.

  • Много метрик — отдельная проблема. Последовательные методы защищают от многих взглядов, но не от многих метрик. Для этого нужны Holm/Benjamini‑Hochberg или схема «одна ключевая + guardrails».

Код

Границы alpha‑spending для произвольных моментов взглядов, методом Монте‑Карло:

import numpy as npfrom scipy.stats import normdef obf_spending(t, alpha=0.05): """Функция трат Lan-DeMets типа O'Brien-Fleming.""" return 2 - 2 * norm.cdf(norm.ppf(1 - alpha / 2) / np.sqrt(t))def boundaries_mc(info_fracs, alpha=0.05, n_sim=200_000, seed=0): rng = np.random.default_rng(seed) t = np.asarray(info_fracs, dtype=float) inc = np.diff(np.r_[0, t]) S = np.cumsum(rng.standard_normal((n_sim, len(t))) * np.sqrt(inc), axis=1) Z = np.abs(S / np.sqrt(t)) alive, spent, bounds = np.ones(n_sim, bool), 0.0, [] for k in range(len(t)): need = int(round((obf_spending(t[k], alpha) - spent) * n_sim)) z_alive = np.sort(Z[alive, k])[::-1] c = np.inf if need = c spent += hit.mean() alive &= ~hit bounds.append(c) return np.array(bounds)print(boundaries_mc([0.10, 0.40, 0.66, 1.0])) # примерно [inf, 3.13, 2.44, 2.02]

Для боевого использования лучше точный расчёт через рекурсивное интегрирование: gsDesign или rpact в R.

Итого

  • Ежедневное подглядывание с порогом 0.05 превращает 5% ошибки в 30–40%. Если ждать без ограничения, ошибка стремится к 100% (закон повторного логарифма).

  • «Подождём, пока прокрасится» при p ≈ 0.07 даёт ложный прокрас в половине A/A‑тестов.

  • Если N‑max известен, а число взглядов нет, подходит alpha‑spending Lan‑DeMets с функцией O’Brien‑Fleming: ошибка 5%, потеря мощности 2–3 п.п.

  • Если горизонта нет, подходит mSPRT: смотреть можно всегда, плата — длительность.

  • Ступеньки раскатки ищут вред, победу фиксирует только 100%.

  • Три вердикта вместо двух убирают главный мотив ждать.

А как в вашей команде решают, когда останавливать тест?

Ждёте прокраса, фиксируете длительность или уже используете последовательные тесты? Интересно почитать в комментариях, особенно про то, как это объясняют продактам.

Литература: O’Brien & Fleming (1979), Biometrics; Lan & DeMets (1983), Biometrika; Jennison & Turnbull, «Group Sequential Methods with Applications to Clinical Trials» (2000); Robbins (1970), «Statistical methods related to the law of the iterated logarithm»; Johari, Pekelis, Walsh, «Always Valid Inference» (arXiv:1512.04922); Howard et al., «Time‑uniform confidence sequences» (arXiv:1810.08240).

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев