A/B-тестыстатистикааналитикаpython

SRM в A/B-тесте: как поймать перекос выборки

2026-07-11 10 мин

Если после запуска A/B-теста в группе A оказалось 51 300 пользователей, а в группе B — 48 700, хотя сплит был 50/50, — это sample ratio mismatch (SRM), и доверять такому тесту нельзя. SRM — это статистически значимое расхождение между тем соотношением размеров групп, которое вы задали в дизайне, и тем, что реально получилось в данных. Даже небольшой перекос (доли процента на больших выборках) означает, что механизм распределения сломан, и любой вывод о влиянии фичи может быть артефактом самого разбиения, а не эффектом эксперимента. Ловится SRM тестом хи-квадрат за пять строк кода, и это первое, что я проверяю в любом эксперименте — до того как вообще смотрю на метрики.

Что такое SRM в A/B-тесте?

SRM (sample ratio mismatch) — это ситуация, когда фактическое количество пользователей в вариантах теста значимо отличается от ожидаемого по дизайну. Вы планировали делить трафик пополам, значит ожидаете примерно равные группы. Слово «примерно» тут ключевое: случайные колебания нормальны, и при 100 тысячах пользователей разница в пару сотен между группами — это ок. А вот разница, которую случайностью не объяснить, — это красный флаг.

Формально мы сравниваем наблюдаемое распределение с ожидаемым. Если задан сплит 50/50 и всего в тест попало N человек, мы ожидаем по N / 2 в каждой группе. Хи-квадрат отвечает на вопрос: могло ли наблюдаемое отклонение возникнуть чисто по случайности при исправном механизме рандомизации? Если вероятность этого крошечная (обычно порог берут p < 0.001), значит механизм не исправен.

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

Почему sample ratio mismatch ломает результаты теста?

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

SRM говорит, что бросок монетки был нечестным. А раз механизм распределения сломан для количества, у вас нет гарантий, что он не сломан для качества. Классический пример: часть трафика с медленным интернетом отваливается на редиректе в вариант B, не догружает страницу и не логируется. В B попадает меньше людей — вот вам SRM. Но одновременно из B вымылись именно пользователи с плохим соединением, а это часто пользователи из регионов, с дешёвыми устройствами, с другим поведением. Группы стали несравнимы. И теперь если конверсия в B выше — это не заслуга фичи, это потому что из B выпали «сложные» пользователи.

Вот почему SRM — это стоп-фактор, а не «интересное наблюдение». При обнаруженном SRM правильная реакция не «поправим цифры», а «тест невалиден, ищем причину». Умение объяснить эту логику на собеседовании отличает аналитика, который понимает статистику, от того, кто просто запускает ttest. Похожие вопросы про валидность экспериментов постоянно всплывают на реальных собеседованиях.

Как обнаружить SRM с помощью хи-квадрата?

Берём фактические размеры групп, считаем ожидаемые по вашему сплиту и прогоняем через хи-квадрат критерий согласия (goodness of fit). В Python это делается через scipy.stats.chisquare:

from scipy.stats import chisquare

# фактические размеры групп
observed = [51300, 48700]

# ожидаемое по сплиту 50/50 при том же общем N
total = sum(observed)
expected = [total * 0.5, total * 0.5]

stat, p_value = chisquare(f_obs=observed, f_exp=expected)

print(f"chi2 = {stat:.2f}, p-value = {p_value:.6f}")
if p_value < 0.001:
    print("SRM обнаружен — тест невалиден, разбираемся с причиной")
else:
    print("Соотношение в норме")

Логика внутри простая: статистика считается как сумма (наблюдаемое - ожидаемое)^2 / ожидаемое по всем группам, а потом переводится в p-value по распределению хи-квадрат с одной степенью свободы (для двух групп). Порог 0.001, а не привычные 0.05, берут намеренно: SRM-чек прогоняется на каждом тесте, и при пороге 0.05 вы бы получали ложную тревогу в среднем на каждом двадцатом здоровом эксперименте. Жёсткий порог убирает шум.

Для неравного сплита меняется только строка с ожиданиями. Если вы лили 70/30, то expected = [total * 0.7, total * 0.3]. Хи-квадрат одинаково хорошо работает и для двух групп, и для многорукого теста с тремя-четырьмя вариантами — просто добавляете элементы в оба списка. Поэкспериментировать с кодом можно прямо в Python-тренажёре, не поднимая локальное окружение, а синтаксис scipy.stats под рукой лежит в справочнике по Python.

Как посчитать SRM прямо в SQL?

Часто первый сигнал удобнее ловить в SQL — там же, где лежат логи эксперимента, не выгружая ничего в Python. Хи-квадрат для двух групп раскладывается в обычную агрегацию:

WITH counts AS (
    SELECT
        variant,
        COUNT(DISTINCT user_id) AS n
    FROM experiment_exposures
    WHERE experiment_id = 'onboarding_v3'
    GROUP BY variant
),
totals AS (
    SELECT SUM(n) AS total FROM counts
)
SELECT
    c.variant,
    c.n                                   AS observed,
    t.total * 0.5                         AS expected,
    ROUND(
        POWER(c.n - t.total * 0.5, 2) / (t.total * 0.5),
        3
    )                                     AS chi_square_term
FROM counts c
CROSS JOIN totals t;

Сумма столбца chi_square_term по обеим группам и есть значение статистики хи-квадрат. Для сплита 50/50 держите в голове ориентир: значение статистики выше примерно 10.8 соответствует p < 0.001 при одной степени свободы. Превысило — поднимаете тревогу. Такой запрос легко зашить в мониторинг и гонять по расписанию. Собрать и проверить его на живых данных удобно в SQL-тренажёре, а если оконные функции и агрегации пока даются тяжело — курс по SQL как раз с этого начинается.

Один нюанс: считайте пользователей через COUNT(DISTINCT user_id), а не строки. Если у вас лог экспозиций, один пользователь может попасть в него несколько раз, и COUNT(*) даст перекос, которого в реальности нет, — это ложный SRM из-за кривого подсчёта.

Какие причины вызывают перекос выборки?

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

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

Что делать, если SRM обнаружен?

Не чинить p-value и не «докручивать» результат — тест невалиден, и точка. Но есть чёткий порядок действий, который превращает панику в расследование:

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

Как встроить проверку SRM в аналитический процесс?

SRM нельзя проверять «когда вспомнил». Это guardrail-метрика: она гоняется автоматически на каждом эксперименте и на каждом ключевом шаге воронки, а не только на финальном размере групп. У хорошей экспериментальной платформы SRM-алерт срабатывает сам и блокирует раскатку теста, чтобы аналитик физически не мог принять решение по сломанным данным.

Минимальный набор, который я советую собрать даже без большой платформы:

Если хочется набить руку на реалистичных данных, а не на игрушечных примерах, посмотрите тестовые задания по A/B-аналитике и разборы кейсов — там встречаются как раз сюжеты с испорченными экспериментами. А понимание базовых продуктовых метрик вроде DAU помогает быстрее находить, по какому срезу разъехались группы.

Какие ошибки чаще всего допускают с SRM?

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

Вторая ошибка — брать порог 0.05 как для обычных тестов. SRM-чек прогоняется на каждом эксперименте, и на потоке мягкий порог завалит вас ложными тревогами. Берите 0.001.

Третья — путать SRM с настоящим эффектом. Если пользователей в группе B меньше, это не «фича отпугнула людей» — на этапе рандомизации фича ещё никак на пользователя не влияла, он просто попал в бакет. Разница в размере групп до воздействия всегда про механику сплита, а не про продукт.

Четвёртая — паниковать при крошечном отклонении на маленькой выборке. При нескольких сотнях пользователей разница в десяток человек статистически нормальна, хи-квадрат это и покажет большим p-value. SRM — это про значимое отклонение, а не про любое.

И пятая — найдя SRM, чинить симптом вместо причины: подрезать группы, фильтровать «выбросы», крутить веса. Единственное правильное лечение — найти инженерный или дизайнерский корень и перезапустить тест на чистых данных.


SRM — маленькая проверка с огромной отдачей: пять строк кода экономят недели работы над выводами, которым нельзя верить. Освойте её один раз, встройте в свой процесс, и вы перестанете попадаться на красивые, но фальшивые результаты. А заодно получите сильный ответ на популярный вопрос с собеседований про валидность экспериментов.

Если хотите системно прокачать SQL, Python и статистику под собеседования аналитика — на платформе есть 425 SQL-задач, 402 Python-задачи, разборы кейсов и AI мок-собеседования. Часть доступна бесплатно, полный доступ открывается в Pro. Начните с тренажёров и загляните в справочник по SQL, когда понадобится быстро освежить синтаксис.

Отработай A/B на задачах
Разбери дизайн эксперимента и статзначимость на реальных кейсах — первые задачи бесплатно.
Открыть тренажёр →