Аналитика данных A/B-тесты: подглядывание, множественные сравнения, мощность
0%

A/B-тесты: подглядывание, множественные сравнения, мощность

A/B-тесты: подглядывание, множественные сравнения, мощность

A/B-тест — единственный инструмент в арсенале аналитика, который даёт право произнести слово «потому что». Всё остальное — корреляции, воронки, когорты, дашборды — описывает мир. Эксперимент его меняет и смотрит, что вышло. Это качественно другой уровень доказательства, и именно поэтому его так легко испортить: результату теста доверяют куда больше, чем он заслуживает по построению.

Типичная история. Тест запустили в понедельник. В среду продакт открыл дашборд: «+8 %, p = 0,03, работает!». Раскатили на всех. Через месяц метрика не отличается от того, что было до релиза. Никто не соврал: и +8 %, и p = 0,03 были на экране. Просто эксперимент измерял не то, что все думали, — он измерял, сколько нужно смотреть на шум, чтобы шум стал похож на сигнал.

Эта глава — про механику вокруг формулы: что покупает рандомизация, сколько нужно пользователей и откуда берётся это число, почему ежедневный взгляд превращает порог 0,05 в фикцию, как проверить, что эксперимент вообще состоялся, и когда метод ломается настолько, что от него надо отказаться. Сам z-тест, доверительные интервалы и смысл p-значения предполагаются известными: https://courses.digitable.life/post/data-analytics/07-uncertainty/ и https://courses.digitable.life/post/data-analytics/08-hypothesis-testing/.

Что покупает рандомизация — и что не покупает

Механизм, из-за которого эксперимент работает, помещается в одно предложение: подбрасывание монетки делает две группы одинаковыми по всем признакам сразу — и по тем, которые вы измерили, и по тем, о которых не подозреваете.

Не «примерно одинаковыми» в смысле «мы постарались подобрать похожих», а одинаковыми в статистическом смысле: любое различие между группами до вмешательства — случайная флуктуация известного размера. Возраст, платформа, платёжеспособность, склонность покупать по четвергам, мотивация — всё распределено между A и B одинаково, потому что распределяла монетка, а не пользователь и не менеджер. Значит, разница в исходе после вмешательства объясняется либо вмешательством, либо случайностью, и второе мы умеем ограничивать интервалом. Ровно это отличает эксперимент от наблюдения: «пользователи, включившие новую фичу, конвертируются на 30 % лучше» — сравнение самоотобравшихся групп, и никакая статистика этого не чинит (https://courses.digitable.life/post/data-analytics/10-causality/).

Рандомизация не покупает Почему
Внешнюю валидность Эффект измерен на вашем трафике, в этот сезон, при этой цене. Перенос на другой рынок — гипотеза, а не вывод
Долгосрочный эффект Две недели измеряют две недели. Новизна, привыкание, выгорание аудитории лежат за горизонтом теста
Независимость пользователей Если A и B взаимодействуют — маркетплейс, соцсеть, общий склад, — рандомизация нарушена самим продуктом
Корректность метрики Тест честно измерит метрику, которая ведёт компанию не туда (https://courses.digitable.life/post/data-analytics/14-metrics-and-goodhart/)
Правильность реализации Если раскатка сломала логирование в одной группе, тест измерит поломку логирования

Последний пункт — самый частый источник «выдающихся» результатов. Закон Тваймана: любая цифра, которая выглядит интересной, обычно неверна. Эксперимент с эффектом +40 % почти всегда оказывается багом инструментовки, а не гениальной идеей.

Дизайн, который пишется до старта

Половина ошибок совершается до первого пользователя. Документ дизайна — абзац в тикете, но в нём шесть пунктов, каждый из которых после старта менять уже нельзя:

  1. Решение — что мы сделаем при каком результате. Если ответ «посмотрим», теста нет, есть сбор данных (https://courses.digitable.life/post/data-analytics/01-question-first/).
  2. Единица рандомизации — пользователь, устройство, магазин, город. От неё зависит и корректность, и размер выборки.
  3. Главная метрика (OEC) — ровно одна. Всё остальное — guardrails и разведка.
  4. Порог практической значимости — «внедряем, если нижняя граница интервала выше +1 % относительно базы», в процентах или рублях, но до данных.
  5. Размер выборки и длительность — считаются, а не назначаются «на две недельки».
  6. Правило остановки — фиксированный горизонт или объявленная последовательная процедура. Третьего не дано.

Обратите внимание на разделение петель: guardrails и SRM смотрят каждый день, главную метрику — один раз в конце. Это не двойной стандарт, а разные задачи. Guardrail — защита от вреда: ложная тревога стоит одного расследования, пропуск катастрофы стоит денег. Главная метрика — принятие решения: здесь ложная тревога и есть та ошибка, ради предотвращения которой всё затевалось. Механика раскатки — фиче-флаги, кольца, автооткат — в безопасных релизах.

Присвоение варианта должно быть детерминированным, воспроизводимым задним числом и независимым от истории пользователя:

import hashlib

def assign(user_id: str, experiment_id: str, weights: dict[str, int]) -> str:
    """Веса в промилле, сумма ровно 1000. Соль включает experiment_id."""
    key = f"{experiment_id}:{user_id}".encode()
    bucket = int(hashlib.md5(key).hexdigest()[:8], 16) % 1000
    edge = 0
    for variant, weight in weights.items():
        edge += weight
        if bucket < edge:
            return variant
    raise ValueError("веса не суммируются в 1000")

Три детали, каждая из которых ломала реальные эксперименты. Соль по эксперименту обязательна: без неё корзины во всех тестах совпадают и параллельные тесты перестают быть независимыми. Хеш вместо счётчика: user_id % 2 даёт систематический перекос, если идентификаторы выдаются не случайно. Момент логирования экспозиции — не момент назначения: писать в лог нужно тогда, когда пользователь дошёл до изменённого места, иначе в анализ попадут миллионы людей, которые новый чекаут в глаза не видели.

Мощность: единственное, что нельзя докупить потом

Мощность — вероятность увидеть эффект, если он есть. Недомощный тест не даёт ни «да», ни «нет», он даёт «мы не знаем», и три недели работы уходят в никуда. Расчёт выборки — не формальность, а решение «стоит ли вообще это запускать». Для бинарной метрики число пользователей на каждую группу:

$$n = \frac{\big(z_{1-\alpha/2} + z_{1-\beta}\big)^2 \cdot \lbrack p_1(1-p_1) + p_2(1-p_2) \rbrack}{(p_2 - p_1)^2}$$

При стандартных $\alpha = 0{,}05$ и мощности 0,8 множитель $(1{,}96 + 0{,}842)^2 = 7{,}85$. Для непрерывной метрики то же через дисперсию: $n \approx 16\sigma^2 / \Delta^2$.

from math import ceil
from statistics import NormalDist

def sample_size_proportions(p1: float, mde_rel: float,
                            alpha: float = 0.05, power: float = 0.8) -> int:
    """Пользователей на группу для обнаружения относительного эффекта mde_rel."""
    p2 = p1 * (1 + mde_rel)
    z_alpha = NormalDist().inv_cdf(1 - alpha / 2)
    z_beta = NormalDist().inv_cdf(power)
    n = (z_alpha + z_beta) ** 2 * (p1 * (1 - p1) + p2 * (1 - p2)) / (p2 - p1) ** 2
    return ceil(n)

for rel in (0.025, 0.05, 0.10, 0.20):
    print(f"MDE {rel:.1%}: {sample_size_proportions(0.04, rel):>8,} на группу")
# MDE 2.5%: 610 007 | MDE 5.0%: 154 301 | MDE 10.0%: 39 472 | MDE 20.0%: 10 313

Размер выборки против MDE в логарифмических осях: прямая с наклоном минус два

Картинка содержит главный практический факт про эксперименты: выборка растёт как квадрат обратного эффекта. Ловить вдвое меньший эффект — платить вчетверо большим трафиком. Отсюда неприятное следствие: если продукт даёт 30 000 новых пользователей в сутки, эффекты меньше 5 % относительных вы не измерите никогда — не потому что их нет, а потому что на них нужно полтора месяца чистого трафика на один тест.

Значит, MDE — не пожелание, а бюджетное ограничение, и правильный разговор с продактом звучит так: «за две недели мы поймаем эффект от 8 % и больше; если ожидаемый выигрыш меньше — запускать этот тест не надо, надо либо менять что-то крупнее, либо принимать решение без эксперимента».

Что съедает мощность незаметно

Разбавление (dilution). Если изменение видит только доля $d$ попавших в эксперимент, наблюдаемый эффект равен $d$, умноженному на настоящий эффект среди затронутых, а требуемая выборка растёт как $1/d^2$: при $d = 0{,}2$ — в 25 раз. Лечится триггерным анализом — в выборку включаются только дошедшие до изменённого места, и в контроле те, кто дошёл бы; второе требует логировать точку триггера в обеих группах, что предусматривают в коде заранее.

Единица анализа крупнее, чем кажется. Рандомизация по пользователю, а метрика по сессиям — эффективный размер выборки меньше числа строк в $\text{DEFF}$ раз (https://courses.digitable.life/post/data-analytics/08-hypothesis-testing/). Для метрик-отношений вида «клики на показ» дисперсию считают дельта-методом:

$$\widehat{\operatorname{Var}}\left(\frac{\bar{X}}{\bar{Y}}\right) \approx \frac{1}{n\,\bar{Y}^{2}}\left(s_X^2 - 2\frac{\bar{X}}{\bar{Y}}s_{XY} + \frac{\bar{X}^{2}}{\bar{Y}^{2}}s_Y^2\right)$$

Тяжёлый хвост. Выручка на пользователя — метрика, где один клиент даёт больше тысячи остальных (https://courses.digitable.life/post/data-analytics/06-distributions/), дисперсия огромна, мощность близка к нулю. Приёмы: винзоризация верхнего перцентиля с порогом, объявленным до эксперимента; переход к бинарной версии («заплатил хоть что-то»); анализ на логарифме с честной оговоркой, что это уже другая величина.

CUPED — обратная операция, возвращающая мощность бесплатно. Если у пользователя есть метрика до эксперимента, она предсказывает метрику во время эксперимента, и предсказанную часть можно вычесть из шума:

$$Y_{\text{cuped}} = Y - \theta\,(X - \bar{X}), \qquad \theta = \frac{\operatorname{Cov}(X, Y)}{\operatorname{Var}(X)}, \qquad \operatorname{Var}(Y_{\text{cuped}}) = \operatorname{Var}(Y)\,(1 - \rho^2)$$

При корреляции $\rho = 0{,}5$ дисперсия падает на 25 %, при $\rho = 0{,}7$ — на 51 %; столько же экономится трафика или времени. Коэффициент $\theta$ оценивают по объединённым данным обеих групп, иначе поправка сама начнёт зависеть от варианта. Метод описан в работе Deng, Xu, Kohavi, Walker, Improving the Sensitivity of Online Controlled Experiments by Utilizing Pre-Experiment Data (WSDM 2013), материалы платформы Microsoft — на exp-platform.com. Ограничение: CUPED требует предэкспериментальных данных, то есть работает для существующих пользователей и бесполезен для теста на онбординге.

Подглядывание: как порог 0,05 перестаёт что-либо значить

Эксперимент идёт две недели, дашборд обновляется каждый час, и человек, который его открывает, принимает решение всякий раз, когда видит зелёную цифру. Правило «останавливаемся, когда p впервые упало ниже 0,05» кажется безобидным — но оно превращает одну проверку гипотезы в десятки.

import random, math

rng = random.Random(1)

def experiment_with_peeks(peeks: list[int], p: float = 0.04) -> bool:
    """A/A-эксперимент: обе группы с одинаковой конверсией p.
    True, если хотя бы на одном из взглядов |z| превысил 1,96."""
    k_a = k_b = prev = 0
    for n in peeks:
        for _ in range(n - prev):        # добираем пользователей до очередного взгляда
            k_a += rng.random() < p
            k_b += rng.random() < p
        prev = n
        p_pool = (k_a + k_b) / (2 * n)
        se = math.sqrt(p_pool * (1 - p_pool) * (2 / n))
        if se and abs(k_b / n - k_a / n) / se > 1.96:
            return True
    return False

N = 40_000
for looks in (1, 2, 5, 10):
    peeks = [N // looks * i for i in range(1, looks + 1)]
    rate = sum(experiment_with_peeks(peeks) for _ in range(4000)) / 4000
    print(f"{looks:>2} взглядов: доля ложных срабатываний {rate:.1%}")

Результат совпадает с классическим расчётом Armitage, McPherson, Rowe, Repeated Significance Tests on Accumulating Data (JRSS A, 1969):

Сколько раз смотрели 1 2 5 10 20 каждый день без конца
Вероятность ложной находки 5 % 8 % 14 % 19 % 25 % стремится к 100 %

Последняя ячейка — не риторика. Под нулевой гипотезой z-статистика ведёт себя как случайное блуждание, нормированное на корень из накопленной выборки; такой процесс рано или поздно пересекает любой фиксированный порог. Смотрите достаточно долго — найдёте значимость в чистом шуме гарантированно.

Траектории z-статистики трёх A/A-экспериментов и границы остановки

На картинке три эксперимента, в каждом из которых разницы между группами нет по построению. Два из трёх в какой-то момент заходят за 1,96 и оба к концу набора возвращаются к нулю. Останови их кто-нибудь в момент красной точки — компания получила бы «подтверждённый» эффект, которого не существует. При двухстах взглядах доля таких траекторий — примерно треть.

Подглядывание — не признак недобросовестности, оно встроено в организационную механику: дашборд эксперимента существует и виден всем, решение нужно к четвергу, выключить успешный тест раньше — экономия трафика, выключить провальный раньше — забота о пользователях. Каждый мотив разумен, а сумма ломает статистику.

Асимметрия в этой схеме опаснее самого подглядывания: положительный результат останавливает тест, отрицательный — «наверное баг, посмотрим ещё». Даже без формальной остановки такая политика систематически завышает найденные эффекты. Явление называют проклятием победителя: среди тестов, остановленных по первому достижению значимости, оценка смещена вверх, и после раскатки реальный выигрыш оказывается меньше обещанного, иногда вдвое.

Четыре честных варианта

1. Фиксированный горизонт. Считаете выборку заранее, набираете целиком, смотрите один раз. Скучно, максимально мощно при заданном n, ничего не требует от инструментов. Дашборд при этом либо не показывает главную метрику до конца набора, либо показывает её без p-значения и без цветовой индикации.

2. Групповые последовательные границы. Заранее объявляете $K$ моментов проверки и используете пороги, в сумме дающие $\alpha = 0{,}05$:

from math import sqrt

def obf_boundaries(K: int, z_final: float = 2.04) -> list[float]:
    """Границы O'Brien — Fleming для K равных взглядов, двусторонний тест, alpha = 0,05."""
    return [round(z_final / sqrt(k / K), 2) for k in range(1, K + 1)]

print(obf_boundaries(5))   # [4.56, 3.23, 2.63, 2.28, 2.04]

Pocock (1977) тратит альфу равномерно: одинаковый порог 2,41 на каждом из пяти взглядов — легко объяснить, но финальные 2,41 вместо 1,96 заметно съедают мощность, если эффект проявится только в конце. O’Brien — Fleming (1979) почти всю альфу оставляет на финал: рано остановиться можно лишь при разгромном результате, зато итоговый порог 2,04 почти не отличается от обычного. Для продуктовых экспериментов второе обычно уместнее: ранняя остановка нужна как страховка от катастрофы, а не как способ быстрее объявить победу.

3. Всегда-валидные (anytime-valid) методы. Последовательный тест отношения правдоподобия и его смешанная версия (mSPRT), а также доверительные последовательности дают p-значение и интервал, корректные в любой момент времени: смотреть можно непрерывно и останавливаться когда угодно. Плата — интервалы шире фиксированных примерно на 15–30 % при том же объёме. Подход популяризован статьёй Johari, Koomen, Pekelis, Walsh, Peeking at A/B Tests: Why It Matters and What to Do About It (KDD 2017); теория — в Always Valid Inference.

4. Байесовская постановка — не индульгенция. Фраза «вероятность, что B лучше A, равна 96 %» валидна в любой момент при заданном априорном распределении. Но правило «останавливаемся, когда эта вероятность впервые превысит 95 %» — решающая процедура, и её частотные свойства никто автоматически не гарантирует: при слабом априорном она ведёт себя почти так же плохо, как подглядывание с p-значением. Байес меняет форму ответа, а не физику накопления данных.

Отдельно: правило «целое число недель» не имеет отношения к статистике, но обязательно. Поведение пользователей различается по дням, и тест, набравший нужное n за четыре дня, всё равно продлевается до семи — иначе выборка систематически смещена в сторону будних или выходных.

Отложенные конверсии — отдельная ловушка: если оплата происходит в среднем через три дня после первого визита, последние дни эксперимента всегда выглядят хуже, потому что их конверсии ещё не наступили (https://courses.digitable.life/post/data-analytics/03-data-quality/). Окно наблюдения должно быть одинаковым для всех: «7 дней с момента экспозиции», а не «до даты остановки теста».

Множественные сравнения внутри одного эксперимента

Механизм разобран в https://courses.digitable.life/post/data-analytics/08-hypothesis-testing/: при $m$ проверках вероятность хотя бы одной ложной находки равна $1 - (1-\alpha)^m$, и для двадцати проверок это 64 %. В A/B-тесте множественность приходит из четырёх источников, и они перемножаются: метрики (главная, guardrails, разведочные, шаги воронки) × варианты (каждый против контроля и между собой) × сегменты (платформа, страна, новые и старые, канал) × моменты времени (ежедневные взгляды, промежуточные отчёты, пересчёт после дозагрузки). Четыре метрики, три варианта, шесть сегментов и пять взглядов — это 360 проверок, и вероятность найти «значимое» при полном отсутствии эффекта практически равна единице.

Механическое применение Бонферрони ко всем 360 убьёт мощность и сделает эксперимент бессмысленным. Работает разделение по ролям:

  • Главная метрика — одна, без поправки. Она объявлена заранее, проверка ровно одна, порог 0,05 (или строже, если решение дорогое) сохраняет смысл.
  • Guardrails — односторонние проверки на ухудшение, без поправки и с широким порогом. Цель обратная: не пропустить вред. Ложная тревога стоит расследования, пропуск — денег, так что разумно ослабить порог до 0,1.
  • Разведочные метрики и сегменты — Бенджамини — Хохберг, и в отчёте они помечены как гипотезы для следующего эксперимента: не «нашли эффект на Android», а «предполагаем эффект на Android, проверим отдельным тестом».
  • Взгляды во времени — только через объявленную последовательную процедуру.

Много вариантов сразу. A/B/C/D — не четыре независимых теста, а три сравнения с общим контролем, поэтому они скоррелированы. Поправка Даннета это учитывает: для трёх вариантов против контроля критическое значение около 2,35 против 2,39 у Бонферрони. Выигрыш небольшой, ценность в том, что метод корректен по построению, а не консервативен с запасом. Распределение трафика тоже не «поровну между всеми»: контроль участвует во всех сравнениях, поэтому его стандартная ошибка важнее, и при $k$ тестовых вариантах оптимально дать контролю долю в $\sqrt{k}$ раз больше, чем каждому варианту (для трёх вариантов — 36,6 % контролю и по 21,1 % остальным). И главное: каждый дополнительный вариант делит трафик, так что четыре варианта вместо двух увеличивают MDE в $\sqrt{2}$ раз. Тест «сравним пять оттенков кнопки» обычно не имеет мощности ни на один из них.

Проверки, без которых числа не имеют смысла

SRM: несовпадение долей

Sample Ratio Mismatch — расхождение фактического деления трафика с задуманным. Задумали 50/50, получили 50,9 % на 49,1 % — это не мелочь, а сигнал, что механизм попадания в группы сломан. А если сломан отбор в группы, то группы больше не сопоставимы, и весь анализ недействителен независимо от красоты p-значения.

$$\chi^2 = \sum_i \frac{(O_i - E_i)^2}{E_i}$$

-- Проверка SRM. Считается по УНИКАЛЬНЫМ пользователям, а не по событиям экспозиции.
WITH per_user AS (
    SELECT DISTINCT user_id, variant
    FROM experiment_exposure
    WHERE experiment_id = 'checkout_v2'
),
counts AS (
    SELECT
        COUNT(*) FILTER (WHERE variant = 'control')   AS n_a,
        COUNT(*) FILTER (WHERE variant = 'treatment') AS n_b
    FROM per_user
)
SELECT
    n_a,
    n_b,
    ROUND(n_b::numeric / (n_a + n_b), 5) AS share_b,
    ROUND(
        POWER(n_a - (n_a + n_b) / 2.0, 2) / ((n_a + n_b) / 2.0)
      + POWER(n_b - (n_a + n_b) / 2.0, 2) / ((n_a + n_b) / 2.0)
    , 3) AS chi2                       -- одна степень свободы
FROM counts;

Читать так: $\chi^2 > 12{,}1$ соответствует $p < 0{,}0005$ — порог, рекомендуемый для автоматических алертов, потому что проверка запускается на каждом эксперименте каждый день и обычные 0,05 давали бы поток ложных тревог. Числовой пример на 100 000 пользователей: деление 50 100 / 49 900 даёт $\chi^2 = 2 \cdot 100^2 / 50000 = 0{,}4$, то есть $p \approx 0{,}53$ — норма; деление 50 900 / 49 100 даёт $\chi^2 = 2 \cdot 900^2 / 50000 = 32{,}4$, то есть $p \approx 1{,}3 \cdot 10^{-8}$. Разница в 1,8 процентного пункта выглядит невинно, а вероятность получить её случайно — одна на восемьдесят миллионов.

Ключевая мысль: SRM почти никогда не бывает «случайным перекосом рандомизации». Это всегда следствие того, что условия попадания в лог различались между вариантами — редирект, лишний сетевой запрос, другой момент срабатывания трекера, фильтр ботов, иначе реагирующий на новую вёрстку. То есть SRM — частный случай сквозной темы трека: способ сбора данных определяет, что из них можно заключить. Систематическая диагностика — у Fabijan и соавторов, Diagnosing Sample Ratio Mismatch in Online Controlled Experiments (KDD 2019).

A/A-тест: проверка не гипотезы, а инфраструктуры

A/A-тест — эксперимент, где обе группы получают одно и то же. Ожидаемый результат «различий нет» — не тавтология: A/A проверяет вашу платформу и ловит рассинхрон логирования между группами, утечку пользователей в обе группы сразу, смещение в самой метрике и, главное, заниженные стандартные ошибки. Правильная форма проверки — не «прогнали один A/A, p > 0,05, всё хорошо» (это не доказывает ничего), а распределение p-значений по сотне A/A-сравнений: оно должно быть равномерным на отрезке от 0 до 1. Скос влево означает, что платформа завышает уверенность и все прошлые выводы были смелее, чем позволяли данные.

SQL: разбор эксперимента от экспозиции до интервала

Корректный анализ состоит в основном из аккуратного сведения к одной строке на пользователя, а не из статистики. Приёмы работы с окнами и агрегатами — в https://courses.digitable.life/post/data-analytics/04-sql-for-analysis/.

WITH exposure AS (
    -- Шаг 1: одна строка на пользователя — первый показ и назначенный вариант.
    SELECT
        user_id,
        MIN(ts)                 AS exposed_at,
        MIN(variant)            AS variant,
        COUNT(DISTINCT variant) AS variants_seen
    FROM experiment_exposure
    WHERE experiment_id = 'checkout_v2'
      AND ts >= TIMESTAMP '2026-06-01 00:00:00'
      AND ts <  TIMESTAMP '2026-06-15 00:00:00'
    GROUP BY user_id
),
clean AS (
    -- Шаг 2: пользователи, увидевшие оба варианта, — это поломка, а не шум.
    -- Их не «чинят» выбором первого варианта: их считают и расследуют.
    SELECT user_id, exposed_at, variant FROM exposure WHERE variants_seen = 1
),
outcome AS (
    -- Шаг 3: метрика по фиксированному окну ОТ МОМЕНТА ЭКСПОЗИЦИИ,
    -- одинаковому для всех, а не «до даты остановки теста».
    SELECT
        c.user_id,
        c.variant,
        MAX(CASE WHEN o.order_id IS NOT NULL THEN 1 ELSE 0 END) AS converted
    FROM clean AS c
    LEFT JOIN orders AS o
           ON o.user_id = c.user_id
          AND o.created_at >= c.exposed_at
          AND o.created_at <  c.exposed_at + INTERVAL '7 days'
    GROUP BY c.user_id, c.variant
),
agg AS (
    SELECT
        COUNT(*)       FILTER (WHERE variant = 'control')   AS n_a,
        SUM(converted) FILTER (WHERE variant = 'control')   AS k_a,
        COUNT(*)       FILTER (WHERE variant = 'treatment') AS n_b,
        SUM(converted) FILTER (WHERE variant = 'treatment') AS k_b
    FROM outcome
)
SELECT
    n_a, n_b,
    ROUND(p_a, 5)                                AS conv_control,
    ROUND(p_b, 5)                                AS conv_treatment,
    ROUND(p_b - p_a, 5)                          AS diff_abs,
    ROUND((p_b - p_a) / p_a, 4)                  AS diff_rel,
    ROUND((p_b - p_a) / se_pooled, 3)            AS z,
    ROUND(p_b - p_a - 1.959964 * se_unpooled, 5) AS ci_low,
    ROUND(p_b - p_a + 1.959964 * se_unpooled, 5) AS ci_high
FROM agg,
LATERAL (
    SELECT k_a::numeric / n_a                 AS p_a,
           k_b::numeric / n_b                 AS p_b,
           (k_a + k_b)::numeric / (n_a + n_b) AS p_pool
) AS r,
LATERAL (
    -- Для ТЕСТА стандартная ошибка считается при объединённой доле (так велит H0),
    -- для ИНТЕРВАЛА — без объединения: здесь равенство долей уже не предполагается.
    SELECT sqrt(r.p_pool * (1 - r.p_pool) * (1.0 / n_a + 1.0 / n_b))  AS se_pooled,
           sqrt(r.p_a * (1 - r.p_a) / n_a + r.p_b * (1 - r.p_b) / n_b) AS se_unpooled
) AS s;

Три места, где этот запрос отличается от того, что пишут обычно. variants_seen = 1 вместо тихого «взять первый вариант»: пользователи в обеих группах означают смену устройства, разлогин, сброс cookie или баг назначения, и их доля — диагностика качества эксперимента, а не строчка, которую надо убрать. Окно exposed_at + 7 days одинаково для всех: иначе пользователи первого дня имеют две недели на конверсию, а последнего — один час. Две разные стандартные ошибки — объединённая для z и раздельная для интервала; разница обычно в третьем знаке, но путать их — признак того, что формулу скопировали, а не поняли.

Накопительную z-статистику по дням легко получить оконной суммой SUM(k_b) OVER (ORDER BY day ROWS UNBOUNDED PRECEDING) — это полезно для разбора после теста и опасно как основание для остановки во время. Построив такую кривую для собственного эксперимента, итог которого оказался нулевым, вы увидите ровно ту картинку с траекториями, что выше; она лечит от подглядывания лучше любых объяснений.

Когда A/B ломается

Интерференция между пользователями. Тест предполагает, что исход пользователя зависит только от его собственного варианта. В маркетплейсе это неверно: если группа B получила лучшую выдачу и раскупила товар, группе A достался худший ассортимент, и часть «выигрыша» B — это отобранное у A. То же в соцсетях, шеринге, системах с общими лимитами и очередями. Лечится кластерной рандомизацией: единицей становится город, склад, регион или компания-клиент. Цена — резкое падение мощности: сотня городов даёт выборку из ста единиц, а не из миллиона пользователей.

Общий ресурс во времени. Для алгоритмов ценообразования, доставки, распределения курьеров применяют переключающиеся эксперименты (switchback): весь регион работает на варианте A полчаса, потом на B полчаса, много раз подряд. Единица анализа — интервал времени; единиц немного, зато интерференция исчезает.

Новизна и привыкание. Первое завышает результат (пользователи тыкают в новое), второе занижает (привыкшие к старому интерфейсу временно теряют скорость); оба затухают за одну-три недели. Диагностика простая: посмотрите эффект по неделям с момента экспозиции для каждой когорты отдельно (https://courses.digitable.life/post/data-analytics/11-cohorts-and-retention/). Если в первую неделю эффект вдвое больше, чем во вторую, вы измерили новизну.

Долгосрочные эффекты. Двухнедельный тест не покажет, что агрессивные пуши поднимают недельную активность и убивают удержание за полгода. Промышленное решение — долгоживущий холдаут: 1–5 % пользователей не получают изменений месяцами, и по ним измеряют накопленный эффект всех раскаток сразу. Дорого и организационно неудобно, но иначе долгосрочная динамика просто не наблюдаема.

Мало трафика. B2B-продукт с двумя тысячами клиентов, внутренний инструмент, редкое событие вроде оформления ипотеки. Здесь честный ответ — «эксперимент невозможен», и дальше идут другие методы: разность разностей, синтетический контроль, переключение во времени, качественное исследование. Это содержание следующей главы.

Этические и юридические ограничения. Нельзя экспериментировать с ценами для защищённых категорий, с медицинскими рекомендациями вне соответствующей процедуры, с уведомлениями безопасности. Ограничения на обработку данных — в приватности и комплаенсе.

Как читать результат

Исходов ровно четыре, и три из них не «победа».

Что получилось Что это значит Что делать
Интервал целиком выше порога внедрения Эффект есть и практически важен Внедрять, зафиксировав ожидаемый размер эффекта для проверки после раскатки
Интервал целиком внутри зоны безразличия Эффект если и есть, то мал Закрыть гипотезу. Это результат, а не неудача
Интервал накрывает и ноль, и важный эффект Мощности не хватило Продлить или признать вопрос неразрешимым этим методом
Интервал целиком ниже нуля Стало хуже Откатить и разобраться: это самая ценная информация из всех

Вторая строка требует упора. Большинство экспериментов заканчиваются ничем, и это норма отрасли, а не признак плохой команды. Kohavi приводит данные Microsoft: примерно треть идей улучшает целевую метрику, треть не даёт эффекта, треть ухудшает; Booking.com называл долю успешных экспериментов около 10 %. Если у вас «побеждают» восемь тестов из десяти, наиболее вероятное объяснение — не гениальность продуктовой команды, а подглядывание, разрезы после факта и отсутствие проверок достоверности.

Отчёт содержит те же четыре части, что и любой статистический вывод: эффект, интервал, решение, ограничение — плюс два пункта, специфичных для A/B: результат проверки SRM и фактическая мощность относительно объявленного MDE. Без них таблица с процентами не подлежит интерпретации. Почему это редко попадает на дашборд эксперимента — в https://courses.digitable.life/post/data-analytics/13-dashboards/; как график с обрезанной осью превращает незначимую разницу в убедительный рост — в https://courses.digitable.life/post/data-analytics/12-visualization/.

Типичные ошибки

Ошибка Что происходит на самом деле Как заметить
Остановка по первому p < 0,05 Порог 0,05 превращается в 14–25 % Есть ли в тикете правило остановки, записанное до старта?
«Продлим, раз почти значимо» То же подглядывание, вид сбоку Продление объявлено заранее или придумано по результату?
Размер выборки не считался Ответа не будет ни при каком исходе Какой MDE у теста при текущем трафике?
Рандомизация по сессии, метрика по пользователю Единицы не совпадают, SE занижена Сравните число сессий и число пользователей
Экспозиция логируется при назначении, а не при показе Разбавление, выборка нужна в 1/d² раз больше Какая доля попавших в тест реально видела изменение?
SRM не проверяется Группы несопоставимы, анализ недействителен Посчитайте хи-квадрат: это один запрос
Пять вариантов «на всякий случай» Мощности нет ни у одного Разделите трафик на число вариантов и пересчитайте MDE
Разрезы по сегментам после факта FWER взлетает, находки не воспроизводятся Сегмент был объявлен до данных?
Окно конверсии «до даты остановки» Пользователи последних дней недооценены Окно отсчитывается от экспозиции
Тест на неделю без выходных Смещение по дню недели Целое число недель, всегда
Эффект новизны принят за эффект Результат не воспроизведётся через месяц Разложите эффект по неделям с момента экспозиции
Маркетплейс без кластерной рандомизации Часть «выигрыша» отобрана у контроля Могут ли группы конкурировать за один ресурс?

Мини-итог

  • Рандомизация покупает право говорить «потому что» — и ничего больше. Внешнюю валидность, долгосрочность и корректность метрики она не обеспечивает.
  • Мощность считается до эксперимента. Выборка растёт как квадрат обратного эффекта: вдвое меньший MDE стоит вчетверо большего трафика. Если MDE больше ожидаемого выигрыша, тест не нужно запускать.
  • Разбавление и тяжёлые хвосты съедают мощность тише всего, а CUPED возвращает её бесплатно там, где есть предэкспериментальные данные.
  • Подглядывание — это множественные сравнения во времени. Пять взглядов превращают 5 % в 14 %, непрерывный мониторинг — в стопроцентную находку. Лечится фиксированным горизонтом, границами O’Brien — Fleming или anytime-valid методами; байесовская форма ответа сама по себе не лечит.
  • Множественность перемножается: метрики × варианты × сегменты × взгляды. Ответ — одна главная метрика без поправки, guardrails односторонние, разведка через FDR и с пометкой «гипотеза».
  • SRM и A/A-тест проверяют не гипотезу, а вас. Перекос долей означает, что условия попадания в лог различались между вариантами, и тогда сравнивать нечего.
  • Три исхода из четырёх — не победа, и это нормально. Доля успешных экспериментов в индустрии от 10 до 33 %; аномально высокая доля побед означает сломанный процесс.

Дополнительное чтение: Ronald Kohavi, Diane Tang, Ya Xu, Trustworthy Online Controlled Experiments (Cambridge University Press, 2020) — исчерпывающая практика от людей, проводивших эксперименты тысячами; Evan Miller, How Not To Run An A/B Test — короткая классическая заметка про подглядывание с наглядными симуляциями; Georgi Georgiev, Statistical Methods in Online A/B Testing — систематический разбор последовательных методов; инженерные блоги Netflix и Booking — про то, как это устроено в масштабе тысяч одновременных тестов.

Что дальше

Корреляция и причинность: что делать, когда эксперимент невозможен — про ситуации, где рандомизация недоступна: контроль на наблюдаемые различия, разность разностей, естественные эксперименты, инструментальные переменные, синтетический контроль — и, что важнее, про честную границу, за которой причинный вывод из наблюдательных данных получить нельзя вообще.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов