A/B-тестыA/A-тествалидация

A/A-тест: зачем нужен и что он на самом деле проверяет

2026-07-11 11 мин

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

Я гоняю A/A каждый раз, когда меняется что-то в инфраструктуре тестов: новый способ хеширования, новая метрика, новый источник событий. Это дешёвая страховка. Час работы против недели споров о том, был ли эффект реальным.

Что такое A/A-тест и чем он отличается от A/B?

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

Именно нулевой эффект делает A/A таким удобным диагностическим инструментом. Раз разницы нет по построению, любая «разница», которую вы видите, — это либо нормальный шум, либо баг. И то, и другое полезно измерить. Шум говорит вам про дисперсию метрики. Баг — про то, что систему нельзя пускать в прод.

Есть ещё близкий сценарий — офлайн A/A на исторических данных: берёте прошлый период, случайно делите пользователей пополам и смотрите на метрику, никого не трогая в проде. Это быстрее, но не проверяет живой сервис раздачи вариантов. Онлайн A/A на реальном трафике честнее, зато дороже по времени.

Что именно проверяет A/A-тест?

Здесь у A/A три разные работы, и их важно не смешивать:

Дальше разберу каждую по отдельности, с кодом.

Как поймать SRM в сплитовании?

SRM (Sample Ratio Mismatch) — это расхождение фактического размера групп с ожидаемым. Вы задумывали 50/50, а получили 50 200 против 49 100. Кажется, мелочь. На деле это красный флаг: если рандомизация перекошена, то и метрика между группами несравнима, потому что в одну из них могли систематически попасть «не те» пользователи.

Первым делом на живых данных смотрю фактическое распределение по группам. Считаю уникальных пользователей в каждой ветке:

SELECT
    variant,
    COUNT(DISTINCT user_id) AS users
FROM experiment_assignments
WHERE experiment_id = 'aa_checkout_2026_06'
GROUP BY variant;

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

SELECT COUNT(*) AS leaked_users
FROM (
    SELECT user_id
    FROM experiment_assignments
    WHERE experiment_id = 'aa_checkout_2026_06'
    GROUP BY user_id
    HAVING COUNT(DISTINCT variant) > 1
) t;

Если leaked_users больше нуля — дальше можно не смотреть, сплит сломан. Формально отклонение размеров проверяют критерием хи-квадрат: берёте наблюдаемые и ожидаемые доли и считаете p-value. Правило простое: если p-value SRM-теста меньше 0.001, эксперимент невалиден, и любые выводы по метрикам смысла не имеют.

from scipy import stats

observed = [50200, 49100]          # фактические размеры групп
total = sum(observed)
expected = [total * 0.5, total * 0.5]

chi2, p = stats.chisquare(observed, f_exp=expected)
print(round(chi2, 2), round(p, 4))
# 12.11 0.0005  -> p < 0.001, есть SRM, эксперимент под подозрением

Разбор частых причин SRM — перехеширование при редеплое, боты, попавшие в одну ветку, фильтрация «выбросов» уже после сплита — это отдельная большая тема. Логику агрегации по группам удобно отрабатывать на SQL-тренажёре, а если хочется собрать базу с нуля — есть пошаговый SQL-курс и SQL-справочник под рукой.

Почему доля ложных срабатываний должна быть около alpha?

Это про калибровку. Договоримся: alpha — это доля случаев, когда мы готовы ошибочно объявить эффект там, где его нет. Обычно берут 0.05. Так вот, в мире, где истинного эффекта нет (а в A/A он ровно нулевой), корректный стат-критерий должен выдавать p-value меньше 0.05 примерно в 5% прогонов. Не в 1%, не в 15%, а около пяти.

Один A/A-тест этого не покажет — с одного прогона вы получаете одно p-value, и оно ничего не доказывает. Калибровку проверяют симуляцией: гоняют сотни синтетических A/A на своих данных и смотрят на распределение p-value. Если критерий честный, p-value распределены равномерно от 0 до 1, а доля значимых равна alpha.

import numpy as np
from scipy import stats

rng = np.random.default_rng(42)
n_sim = 2000
n_per_group = 5000
alpha = 0.05
false_positives = 0

for _ in range(n_sim):
    # обе выборки из ОДНОГО распределения — истинного эффекта нет
    a = rng.normal(100, 20, n_per_group)
    b = rng.normal(100, 20, n_per_group)
    _, p = stats.ttest_ind(a, b)
    if p < alpha:
        false_positives += 1

print(round(false_positives / n_sim, 3))
# ~0.05 -> критерий откалиброван правильно

Если бы вместо честных 5% получилось, скажем, 0.12 — это сигнал, что t-тест здесь применять нельзя. Типичная причина — метрика на уровне пользователя, а не на уровне наблюдения: доход на пользователя с длинным хвостом, ratio-метрики (клики на показ), кластеризация по сессиям. В таких случаях t-тест недооценивает дисперсию, и нужен либо бутстрап, либо дельта-метод, либо агрегация до уровня рандомизации. Симуляции удобно крутить в Python-песочнице, а синтаксис scipy держать открытым в Python-справочнике.

Как A/A-тест помогает оценить дисперсию и рассчитать выборку?

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

Грубая логика такая: размер группы растёт пропорционально дисперсии метрики и падает пропорционально квадрату MDE. Хотите различать эффект вдвое меньше — выборка вырастает вчетверо. Метрика шумная — выборка тоже больше.

import numpy as np
from scipy import stats

# метрика на пользователя, взятая из A/A-периода
revenue = np.array([0, 0, 0, 199, 0, 990, 0, 0, 350, 0, 0, 1990])

baseline_mean = revenue.mean()
variance = revenue.var(ddof=1)
mde_rel = 0.03                       # хотим ловить сдвиг среднего на 3%
effect = baseline_mean * mde_rel

z_alpha = stats.norm.ppf(1 - 0.05 / 2)
z_beta = stats.norm.ppf(0.80)        # мощность 80%

n_per_group = ((z_alpha + z_beta) ** 2 * 2 * variance) / (effect ** 2)
print(int(np.ceil(n_per_group)))

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

Как читать результат A/A-теста?

Главное правило: у A/A нет «победителя», и хороший результат выглядит скучно. Что я смотрю по порядку:

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

Что делать, если A/A вдруг «значим»?

Сначала не паниковать. Одно значимое p-value при 5% уровне — это ожидаемое событие, оно случается в среднем раз на двадцать проверок и само по себе ничего не ломает. Проблема начинается, когда значимость воспроизводится, когда её слишком много или когда рядом горит SRM.

Порядок разбора у меня такой:

Если после всего этого разница всё ещё уверенно держится и воспроизводится на новом A/A — значит, в системе экспериментов реальный баг, и это ровно та ситуация, ради которой A/A и запускают. Лучше найти её здесь, чем на настоящем тесте ценой неверного решения.

Когда A/A можно не гонять и где чаще всего ошибаются

A/A нужен не перед каждым запуском. Если платформа не менялась, метрика та же и дисперсия по ней уже известна — постоянно гонять A/A расточительно, вы просто жжёте трафик. Я запускаю его при изменениях в инфраструктуре: новая логика сплита, новая метрика, новый ETL событий, переезд на другой стат-движок.

Частые грабли, на которые стоит смотреть в оба:

Хотите закрепить это руками — соберите SRM-проверку и симуляцию калибровки на своих данных: подойдут SQL-задачи для агрегаций по группам и практические задания на A/B-логику. Если готовитесь к собесу в продуктовую аналитику, где A/A и SRM спрашивают почти всегда, полный доступ ко всем задачам, кейсам и AI-разборам открывается в Pro — но начать проверять свои гипотезы можно и на бесплатных задачах прямо сейчас.

Готовься к собесу аналитика
Вопросы по статистике и A/B с разбором — попробуй бесплатно.
Банк вопросов →