// От гипотезы до решения

A/B тесты:
как не ошибиться
проверяя идеи

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

Теория гипотез Дизайн теста Размер выборки Анализ результатов Ловушки и ошибки CUPED / Bootstrap
Гипотезы
Статистика
Дизайн
Анализ
Эффективность
Ловушки
Содержание
00 Зачем нужны A/B тесты 01 Проверка гипотез 01.1 H0 и H1: нулевая и альтернативная 01.2 p-value: что это на самом деле 01.3 Ошибки I и II рода 01.4 Мощность теста 02 Как работает A/B тест 02.1 Механика и рандомизация 02.2 Выбор метрик 03 Как правильно провести тест 03.1 Пошаговый процесс 03.2 Расчёт размера выборки 03.3 Какой статтест применять 03.4 AA-тест: зачем нужен 04 Как повысить чувствительность 04.1 CUPED 04.2 Стратификация 04.3 Bootstrap 05 Когда A/B тесты не работают 05.1 Peeking problem 05.2 Novelty effect 05.3 Network effects 05.4 Другие ловушки 06 Альтернативы A/B тестам 07 Чек-лист перед запуском

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

Проблема интуиции в продукте

Люди плохо предсказывают, что понравится другим людям. Даже опытные продакты и дизайнеры ошибаются в 60–70% случаев, когда прогнозируют результат изменения без данных. Это подтверждают внутренние исследования Microsoft, Google и Booking.com.

«В Booking.com около 90% A/B тестов не показывают значимого эффекта. Это не провал — это система обучения.»
— Booking.com Engineering Blog

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

01
// Фундамент
Проверка статистических гипотез
Без этого A/B тест — просто подбрасывание монеты

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

⚖️
Теория

Нулевая и альтернативная гипотезы

В основе любого теста лежат две гипотезы. Нулевая гипотеза (H₀) — это позиция «ничего не изменилось, эффекта нет». Альтернативная гипотеза (H₁) — это то, что мы хотим доказать.

Ключевой момент: мы не доказываем H₁. Мы лишь пытаемся опровергнуть H₀. Это принципиально важно для понимания того, что означают результаты теста.

Формулировка гипотез для A/B теста
H₀: конверсия(A) = конверсия(B) // нет разницы между группами H₁: конверсия(A) ≠ конверсия(B) // разница есть (двухсторонний тест) // или для одностороннего теста: H₁: конверсия(B) > конверсия(A) // B лучше A
// Пример: редизайн кнопки оформления заказа

Идея: поменяем текст кнопки с «Оформить заказ» на «Купить сейчас»

H₀: конверсия в покупку одинакова в обеих группах (нет эффекта)

H₁: конверсия в покупку различается (есть эффект)

Тест позволит нам решить: достаточно ли данных, чтобы отвергнуть H₀ с разумным уровнем уверенности?

// Важно понять

«Не отвергнуть H₀» ≠ «H₀ истинна». Если тест не нашёл эффекта, это может означать: а) эффекта нет, б) выборка была слишком маленькой, чтобы его увидеть. Разница критическая.

🎲
Теория

p-value: самая неправильно понимаемая концепция

p-value — это вероятность получить наблюдаемый результат (или более экстремальный) при условии, что нулевая гипотеза истинна. Звучит запутанно — разберём конкретно.

Определение
p-value = P(данные такие экстремальные или хуже | H₀ истинна) // Если p-value = 0.03: // "Если бы на самом деле никакого эффекта не было, // то наблюдать такую разницу случайно — вероятность 3%"
// Аналогия с монетой

Вы подозреваете, что монета нечестная. Бросаете 10 раз — выпадает орёл 9 раз.

H₀: монета честная (вероятность орла = 0.5)

p-value: вероятность получить 9 или 10 орлов при честной монете ≈ 1%. Это очень мало — значит, либо нам невероятно повезло, либо монета всё-таки нечестная.

Мы отвергаем H₀ на уровне значимости 5% (потому что 1% < 5%).

Традиционно используют порог α = 0.05 (5%). Это значит: мы готовы ошибиться в 1 случае из 20, случайно «найдя» эффект там, где его нет.

// Частые ошибки в интерпретации p-value

«p=0.03 значит вероятность что H₀ истинна — 3%» — неверно. p-value не говорит о вероятности гипотез.

«p=0.05 значит эффект точно есть» — неверно. Это лишь означает, что мы решаем отвергнуть H₀ при принятом пороге.

«p=0.06 значит результат незначим и эффекта нет» — неверно. Граница 0.05 условна. Разница между p=0.049 и p=0.051 статистически несущественна.

Правильно: p-value — это степень несовместимости наблюдаемых данных с нулевой гипотезой. Чем меньше — тем меньше шанс, что мы видим случайный шум.

🎯
Теория

Ошибки I и II рода: ложные тревоги и пропущенные сигналы

Когда мы принимаем решение по тесту, мы можем ошибиться двумя способами. Важно понимать оба, потому что они находятся в постоянном противоречии.

Реальность: эффекта НЕТ
Реальность: эффект ЕСТЬ
Тест говорит: эффекта нет
✓ Верное решение
Правильно не внедрили
✗ Ошибка II рода (β)
Пропустили реальное улучшение
Тест говорит: эффект есть
✗ Ошибка I рода (α)
Внедрили бесполезное изменение
✓ Верное решение
Правильно внедрили

Ошибка I рода (α) — ложноположительный результат. Мы решили, что эффект есть, а его нет. Уровень значимости α = 0.05 означает, что мы допускаем такую ошибку в 5% случаев. Это «ложная тревога».

Ошибка II рода (β) — ложноотрицательный результат. Эффект реально есть, но мы его не увидели — слишком маленькая выборка или слабый эффект. Типичное значение: β = 0.20 (20%).

// Пример из медицины (для наглядности)

H₀: лекарство не работает. H₁: лекарство работает.

Ошибка I рода: решили что лекарство работает, а оно не работает → внедрили бесполезное лекарство.

Ошибка II рода: решили что лекарство не работает, а оно работает → отказались от хорошего лекарства.

В зависимости от цены каждой ошибки выбирают разные значения α. В A/B тестах продукта α=0.05, β=0.20 — стандарт индустрии.

Теория

Мощность теста: способность найти эффект, если он есть

Мощность (power) = 1 - β. Это вероятность обнаружить эффект, если он реально существует. Стандарт: мощность ≥ 80% (β ≤ 0.20).

На мощность влияют три вещи: размер выборки, величина эффекта (MDE), и дисперсия данных. Именно поэтому расчёт выборки — обязательный шаг перед запуском.

Три кита мощности
Мощность ↑ если: • Размер выборки (n) ↑ — больше данных → лучше видим эффект • Эффект (MDE) ↑ — большой эффект легче заметить • Дисперсия (σ²) ↓ — «тише» данные → чище сигнал
// Интуиция

Мощность теста — это как острота зрения. Если вы в очках (большая выборка, чистые данные) — увидите маленькую букву (слабый эффект). Без очков (маленькая выборка) — увидите только крупные буквы (сильные эффекты).

02
// Механика
Как работает A/B тест
Рандомизация, группы и измерение эффекта
🔀
Практика

Механика: две группы, одна разница

A/B тест — это контролируемый эксперимент. Мы случайным образом делим пользователей на две группы: контрольную (A) и экспериментальную (B). Группа A видит старую версию, группа B — новую. Всё остальное — одинаково.

Ключевое слово — случайным образом. Рандомизация гарантирует, что группы в среднем похожи по всем характеристикам (возраст, платформа, частота визитов). Без неё мы не можем утверждать, что разница в метриках вызвана именно изменением.

Все пользователи Random Split Группа A Старая версия Группа B Новая версия // сравниваем CR_A vs CR_B p-value, CI → решение
Схема A/B теста: рандомизация → две группы → сравнение метрик → решение

Как происходит рандомизация на практике: обычно хешируют user_id или device_id и делят на N бакетов. Пользователь с четным хешем → группа A, нечётным → группа B. Это гарантирует: один пользователь всегда видит одну версию.

# Простая рандомизация через хеш user_id import hashlib def assign_group(user_id: str, experiment_name: str, split: float = 0.5) -> str: # Добавляем имя эксперимента — разные тесты дают разные разбивки key = f"{experiment_name}:{user_id}" hash_val = int(hashlib.md5(key.encode()).hexdigest(), 16) bucket = hash_val % 100 # 0–99 if bucket < split * 100: return "control" # группа A else: return "treatment" # группа B # Пример: assign_group("user_42", "button_color_test") # → "treatment" assign_group("user_77", "button_color_test") # → "control"
📊
Практика

Выбор метрик: что именно мерить

Метрика — это сердце A/B теста. Неправильно выбранная метрика сделает весь тест бессмысленным, даже если статистика безупречна. Используют три типа метрик одновременно.

1
Primary metric — основная метрика
Та одна метрика, по которой принимается решение. Должна быть одна — иначе возникает проблема множественного тестирования. Пример: конверсия в покупку, DAU, ARPU. Выбирается заранее и не меняется по ходу теста.
2
Secondary metrics — дополнительные метрики
Помогают понять почему изменилась основная метрика и нет ли нежелательных побочных эффектов. Пример: CTR, глубина просмотра, время на странице. По ним не принимается решение, но они дают контекст.
3
Guardrail metrics — ограничительные метрики
Метрики, которые не должны ухудшиться. Даже если primary метрика выросла, тест нельзя запускать, если guardrail нарушен. Пример: если конверсия выросла, но retention упал — это тревожный сигнал.
// Пример: тест нового онбординга в приложении

Primary: доля пользователей, завершивших онбординг (activation rate)

Secondary: время прохождения онбординга, число кликов, drop-off по шагам

Guardrail: Day-7 retention не должен упасть — онбординг не должен обещать то, чего нет в продукте

03
// Процесс
Как правильно провести A/B тест
Шаги, расчёт выборки, выбор стат-теста
🗺️
Практика

Пошаговый процесс проведения теста

1
Сформулируйте гипотезу и метрику
Плохо: «давайте протестируем зелёную кнопку». Хорошо: «Мы предполагаем, что изменение CTA с "Оформить" на "Купить" повысит конверсию, потому что более короткий текст снижает когнитивную нагрузку». Гипотеза должна содержать механизм — почему это должно работать.
2
Определите MDE и рассчитайте выборку
MDE (Minimum Detectable Effect) — минимальный эффект, который нам важно зафиксировать. Если конверсия сейчас 5%, то интересен рост хотя бы до 5.25% (+5%). Задаёте α, β, MDE → получаете минимальный размер выборки. Никогда не запускайте тест без этого расчёта.
3
Проведите AA-тест
Запустите тест, в котором обе группы видят одинаковую версию. Если AA-тест показывает «значимую» разницу — в системе что-то не так: рандомизация сломана, есть утечка трафика или систематическое смещение. Только после успешного AA можно запускать AB.
4
Запустите тест и не трогайте его
Определите длительность заранее (на основе расчёта выборки) и не смотрите на промежуточные результаты для принятия решений. Это ловушка peeking problem — о ней подробно в главе 5.
5
Проанализируйте результаты
После того как набрана нужная выборка: считаем статтест, смотрим p-value и доверительный интервал, проверяем secondary и guardrail метрики. Принимаем решение по заранее установленному протоколу.
6
Задокументируйте и сделайте выводы
Даже «неудачный» тест (нет эффекта) — это ценное знание. Задокументируйте: гипотезу, результаты, возможные причины. Через год это может изменить приоритеты команды.
🔢
Практика

Расчёт размера выборки: главный шаг перед запуском

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

Формула для двух пропорций (конверсия, CTR)
n = 2 × (z_α/2 + z_β)² × p̄(1-p̄) / (p_A - p_B)² где: z_α/2 = 1.96 (α=0.05, двухсторонний тест) z_β = 0.84 (β=0.20, мощность 80%) p̄ = (p_A + p_B) / 2 — средняя конверсия p_A - p_B = MDE — минимальный эффект
# Расчёт выборки на Python from statsmodels.stats.power import TTestIndPower, NormalIndPower import numpy as np # Параметры baseline_cr = 0.05 # текущая конверсия 5% mde = 0.005 # хотим увидеть +0.5 п.п. (т.е. 5% → 5.5%) alpha = 0.05 # уровень значимости power = 0.80 # мощность 80% # effect size для пропорций (Cohen's h) p1 = baseline_cr p2 = baseline_cr + mde h = 2 * np.arcsin(np.sqrt(p1)) - 2 * np.arcsin(np.sqrt(p2)) h = abs(h) analysis = NormalIndPower() n = analysis.solve_power( effect_size=h, alpha=alpha, power=power, alternative='two-sided' ) print(ff"Нужно {n:.0f} пользователей в каждой группе") # → Нужно ~12 000 пользователей в каждой группе (24k всего) # Сколько дней нужно ждать? daily_users = 3000 # среднедневной трафик days_needed = (2 * n) / daily_users print(ff"Продолжительность: ~{days_needed:.0f} дней")
// Правило больших MDE

Чем меньший эффект вы хотите обнаружить — тем больше нужна выборка. Хотите поймать изменение конверсии с 5.0% до 5.1% (+2%)? Нужно ~100 000 пользователей в группу. Хотите поймать изменение с 5% до 6% (+20%)? Хватит ~5 000.

Практический вывод: устанавливайте MDE исходя из того, при каком минимальном эффекте внедрение оправдано бизнесом. Если изменение не окупается при росте ниже 10% — ставьте MDE = 10%.

🧮
Практика

Какой статистический тест использовать

Выбор теста зависит от типа метрики. Вот шпаргалка для основных случаев:

Тип метрики Примеры Тест Когда использовать
Пропорция Конверсия, CTR, Retention Z-тест Большие выборки (n > 1000), нормальное приближение работает
Среднее (норм.) Session length, время отклика t-тест Стьюдента Данные близки к нормальному распределению
Среднее (скошен.) Revenue, LTV, чек Mann-Whitney или Bootstrap Тяжёлые хвосты, выбросы (большие покупки). t-тест ненадёжен
Ratio-метрика ARPU = revenue/users, CTR = clicks/shows Delta-метод Числитель и знаменатель — разные случайные величины
Категориальная Тип девайса, категория Chi-square Сравнение частотных распределений
# Z-тест для конверсий (наиболее частый случай) from statsmodels.stats.proportion import proportions_ztest import numpy as np # Данные по группам conversions_A = 450; users_A = 10000 # CR_A = 4.5% conversions_B = 510; users_B = 10000 # CR_B = 5.1% count = np.array([conversions_A, conversions_B]) nobs = np.array([users_A, users_B]) z_stat, p_value = proportions_ztest(count, nobs, alternative='two-sided') print(ff"z-статистика: {z_stat:.3f}") print(ff"p-value: {p_value:.4f}") print(ff"Результат: {'значимо' if p_value < 0.05 else 'незначимо'}") # Доверительный интервал для разницы в конверсиях from statsmodels.stats.proportion import confint_proportions_2indep ci_low, ci_high = confint_proportions_2indep( conversions_A, users_A, conversions_B, users_B, method='wald' ) print(ff"95% ДИ для разницы: [{ci_low:.4f}, {ci_high:.4f}]") # → 95% ДИ: [0.0021, 0.0099] → B лучше A на 0.2–1.0 п.п.
🪞
Практика

AA-тест: как проверить, что всё настроено правильно

AA-тест — это A/B тест, в котором обе группы видят одинаковую версию продукта. Никаких изменений нет. Цель — убедиться в том, что система рандомизации работает корректно.

Логика: если группы действительно равнозначны, статтест при AA должен давать p-value, равномерно распределённый от 0 до 1. Если вы регулярно видите p < 0.05 на AA тестах — в системе есть сбой.

// Что проверяет AA-тест

🔧 Систематическое смещение — один сегмент пользователей всегда попадает в одну группу (например, iOS → контроль, Android → тест).

🔧 Утечку трафика (SRM) — Sample Ratio Mismatch: в группах не 50/50, а, например, 52/48. Это признак проблемы в рандомизации.

🔧 Плохую изоляцию — пользователи «перетекают» между группами (очистили куки, сменили устройство).

# Проверка SRM — Sample Ratio Mismatch from scipy.stats import chi2_contingency import numpy as np users_A = 9850 # ожидали 10000 users_B = 10150 # ожидали 10000 expected_ratio = 0.5 observed = [users_A, users_B] expected = [(users_A + users_B) * expected_ratio] * 2 chi2, p_value = chi2_contingency([[users_A, users_B], expected])[:2] if p_value < 0.01: print(f"⚠️ SRM обнаружен! p={p_value:.4f}. Проверьте рандомизацию.") else: print(f"✅ Соотношение групп в норме. p={p_value:.4f}")
04
// Продвинутые техники
Как повысить чувствительность теста
CUPED, стратификация, bootstrap — инструменты зрелой экспериментации

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

✂️
Продвинутый

CUPED: убираем шум с помощью предэкспериментальных данных

CUPED — Controlled-experiment Using Pre-Experiment Data. Метод из Microsoft Research (2013), сейчас стандарт в Яндексе, Авито, Booking.com.

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

Формула CUPED
Y_cuped = Y - θ × X_pre где: Y — метрика во время эксперимента X_pre — та же метрика ДО эксперимента (ковариата) θ = Cov(Y, X_pre) / Var(X_pre) — оптимальный коэффициент Снижение дисперсии: Var(Y_cuped) = Var(Y) × (1 - ρ²) ρ — корреляция между Y и X_pre При ρ = 0.7 → дисперсия снижается на 51%!
# CUPED на Python import numpy as np import pandas as pd from scipy.stats import ttest_ind # df содержит: user_id, group, metric_post (во время теста), metric_pre (до теста) def apply_cuped(df, metric_col, covariate_col): # Считаем θ на всех данных (не разделяя группы) cov_matrix = np.cov(df[metric_col], df[covariate_col]) theta = cov_matrix[0, 1] / cov_matrix[1, 1] # Корректируем метрику df['metric_cuped'] = ( df[metric_col] - theta * (df[covariate_col] - df[covariate_col].mean()) ) return df, theta df, theta = apply_cuped(df, 'revenue_post', 'revenue_pre') # Сравниваем скорректированные значения control = df[df['group'] == 'control']['metric_cuped'] treatment = df[df['group'] == 'treatment']['metric_cuped'] t_stat, p_value = ttest_ind(control, treatment) print(ff"CUPED p-value: {p_value:.4f} (θ = {theta:.3f})")
// Практический эффект CUPED

В задачах с хорошей корреляцией (доход пользователя неделя к неделе, ρ ≈ 0.7–0.8) CUPED позволяет сократить нужную выборку вдвое при той же мощности. Это означает, что тест, который раньше шёл 4 недели, теперь идёт 2.

📋
Продвинутый

Стратификация: контролируем состав групп

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

Стратификация решает это: мы делим пользователей на страты (сегменты) и рандомизируем внутри каждой страты. Это гарантирует, что соотношение сегментов в группах будет одинаковым.

// Пример стратификации

Страты: платформа (iOS / Android / Web), тип пользователя (новый / вернувшийся / активный).

Без стратификации: случайно в тестовую группу попало 60% новых пользователей, в контроль — только 40%. Результат теста смешан с эффектом новизны.

Со стратификацией: и в контроле, и в тесте ровно 45% новых пользователей. Группы сопоставимы по составу.

Стратификация — это «страховка» рандомизации. Особенно важна при небольших выборках (< 10 000 пользователей) и когда метрика сильно зависит от сегмента.

🔁
Продвинутый

Bootstrap: статтест без предположений о распределении

Классические тесты (t-тест, z-тест) предполагают, что данные нормально распределены или что выборка достаточно большая (ЦПТ). Но метрики вроде дохода, LTV, среднего чека — с тяжёлыми хвостами и выбросами. Здесь t-тест ненадёжен.

Bootstrap — непараметрический метод. Мы многократно «переиспользуем» данные, каждый раз случайно сэмплируя из них, и строим эмпирическое распределение статистики.

# Bootstrap для сравнения средних доходов import numpy as np def bootstrap_test(group_a, group_b, n_iterations=10000, metric_fn=np.mean): observed_diff = metric_fn(group_b) - metric_fn(group_a) # Объединяем под нулевой гипотезой (нет разницы) combined = np.concatenate([group_a, group_b]) n_a, n_b = len(group_a), len(group_b) bootstrap_diffs = [] for _ in range(n_iterations): sample_a = np.random.choice(combined, size=n_a, replace=True) sample_b = np.random.choice(combined, size=n_b, replace=True) bootstrap_diffs.append(metric_fn(sample_b) - metric_fn(sample_a)) bootstrap_diffs = np.array(bootstrap_diffs) # p-value: доля случаев, когда случайная разница >= наблюдаемой p_value = np.mean(np.abs(bootstrap_diffs) >= np.abs(observed_diff)) # 95% доверительный интервал для разницы ci = np.percentile(bootstrap_diffs, [2.5, 97.5]) return {'diff': observed_diff, 'p_value': p_value, 'ci': ci} result = bootstrap_test(revenue_control, revenue_treatment) print(ff"Разница: +{result['diff']:.2f} руб.") print(ff"p-value: {result['p_value']:.4f}") print(ff"95% ДИ: [{result['ci'][0]:.2f}, {result['ci'][1]:.2f}]")
Когда bootstrap нужен
  • Метрика с тяжёлыми хвостами (доход, чек)
  • Нестандартные метрики (медиана, перцентили)
  • Малые выборки, ЦПТ не применима
  • Нужен точный доверительный интервал
Ограничения
  • Вычислительно дороже (10 000 итераций)
  • Медленнее при огромных выборках
  • Требует корректной реализации
05
// Ловушки
Когда A/B тесты не работают
Ошибки, которые делают большинство команд
👀
Ловушка

Peeking problem: нельзя смотреть на результаты до конца теста

Самая распространённая ошибка в A/B тестировании. Peeking — это когда вы смотрите на промежуточные результаты и останавливаете тест, как только увидели p < 0.05.

Проблема в том, что p-value в процессе теста случайно болтается туда-сюда. При случайных данных (без реального эффекта) p-value рано или поздно случайно опустится ниже 0.05 — и если вы остановите тест в этот момент, вы совершите ошибку I рода.

p=0.05 ⚠ тут нельзя останавливать! нет эффекта есть эффект p-value
Синяя линия — реальный эффект (в итоге значим). Серая — нет эффекта. Останавливаться нужно в заранее назначенный день, а не при первом p<0.05
// Насколько это серьёзно

Симуляции показывают: если смотреть на результаты каждый день и останавливать тест при p<0.05, реальный уровень ложных срабатываний может достигать 30% вместо заявленных 5%. Шесть из двадцати «открытий» окажутся случайностью.

Решения:

1. Фиксированный горизонт: считаете нужную выборку заранее, запускаете, смотрите результат ровно один раз в конце. Это классический подход.

2. Sequential testing (mSPRT): математически корректный способ смотреть на тест в любой момент. Использует always-valid p-values. Применяется в Spotify, Yandex.

Ловушка

Novelty effect: пользователи реагируют на новизну, а не на изменение

Когда вы меняете что-то в интерфейсе, часть пользователей взаимодействует с новым элементом просто потому что он новый. Этот эффект временный: через 2–3 недели поведение возвращается к норме.

// Пример

Вы добавили новый раздел «Рекомендации» в навигацию. В первую неделю его CTR — 15%. Через месяц — 3%. Тест, проведённый только первую неделю, показал бы «значимый» эффект, который в реальности не устойчив.

Обратный случай (Novelty suppression): постоянные пользователи избегают нового элемента, потому что привыкли к старому. Эффект кажется нулевым или отрицательным, но на новых пользователях он положительный.

Решение: разделяйте анализ по сегментам — новые пользователи vs вернувшиеся vs очень активные. Эффект у новых пользователей (у которых нет привычки) — более надёжный сигнал. Для очень активных пользователей прогоняйте тест дольше — 2–4 недели.

🕸️
Ловушка

Network effects: пользователи влияют друг на друга

Классический A/B тест предполагает независимость пользователей: то, что видит пользователь A, не влияет на пользователя B. Но в социальных сетях, маркетплейсах и двусторонних платформах это не так.

// Примеры нарушения независимости

👥 Соцсети: если пользователи группы B видят новый формат постов и начинают постить больше — это влияет на пользователей группы A (их лента тоже меняется). Контроль и тест «заражают» друг друга.

🛒 Маркетплейсы: продавец попадает в тест с новым ранжированием → его товары видят покупатели из контроля. SUTVA нарушен — изменение цены или ранга влияет на весь рынок, а не только на тестовую группу.

🚗 Uber/Яндекс.Такси: если водителям в группе B показывать другие стимулы, они могут «перетечь» в зоны, где больше пассажиров из группы A — и тест загрязнён.

Решение для маркетплейсов — Switchback experiments: вместо разделения по пользователям, тест разделяется по времени или гео-зонам. Одна гео-зона в чётные часы видит версию A, в нечётные — версию B. Пользователи внутри зоны всегда в одной группе.

Для соцсетей — Ego network / Cluster-based randomization: рандомизируем не пользователей, а кластеры социального графа. Все друзья попадают в одну группу — интерференция минимальна.

⚠️
Ловушки

Другие типичные ошибки

ОшибкаЧто происходитКак избежать
Multiple testing Тестируете сразу 20 метрик → одна случайно окажется значимой (5% × 20 ≈ 1 ложная тревога) Одна primary метрика. Поправка Бонферрони для вторичных: α/n
Слишком короткий тест Не учтена недельная сезонность: пользователи по будням и выходным разные Минимум 1 полная неделя. Лучше 2–4. Проверяйте по дням недели
Post-hoc сегментация Результат незначим → ищете «выигрывающий» сегмент. Это p-hacking Сегментация планируется ДО теста и прописывается в протоколе
Survivor bias Анализируете только пользователей, которые остались активны. Ушедшие игнорируются Анализируйте всех пользователей, попавших в тест с момента старта
Изменение теста на ходу Меняете версию B в процессе теста → данные несравнимы Зафиксируйте версию перед запуском. Любые изменения = новый тест
HARKing Hypothesis After Results Known: формулируете гипотезу под уже полученный результат Пре-регистрируйте гипотезу и метрику до запуска теста
06
// Альтернативы
Когда A/B тест не подходит
И что делать вместо него

A/B тест — мощный инструмент, но не для каждой ситуации. Вот три случая, когда нужно другое решение.

🎰
Альтернатива

Multi-Armed Bandit: когда важно минимизировать потери

Классический A/B тест «тратит» 50% трафика на явно проигрывающий вариант до самого конца. Multi-Armed Bandit (MAB) адаптируется: направляет больше трафика на лучшую версию по мере накопления данных.

Алгоритмы: ε-greedy, Thompson Sampling, UCB. Используется там, где каждый «неоптимальный» показ стоит денег: рекламные баннеры, ценообразование, email-рассылки.

Минус: сложнее интерпретировать, не подходит для долгосрочных метрик (retention, LTV), адаптация «загрязняет» данные для классического анализа.

📡
Альтернатива

Quasi-experiments: когда нельзя рандомизировать

Иногда рандомизация невозможна: функция выкатывается на всех сразу, или нельзя дать разным пользователям разные цены. Для таких случаев есть методы причинно-следственного вывода без рандомизации.

Difference-in-differences (DiD): сравниваем изменение в группе до/после с изменением в контрольной группе, которую не затронуло изменение. Используется для оценки влияния маркетинговых акций и фичей, выкатываемых по регионам.

Regression Discontinuity Design (RDD): используем «пороговые» правила — например, скидка только тем, кто зарегистрировался до определённой даты. Пользователи прямо около порога — почти случайно разделены.

Causal Impact (Google): байесовский подход на основе временных рядов — строим прогноз что было бы без изменения, и сравниваем с реальностью.

🔭
Альтернатива

Holdout groups и долгосрочные эффекты

Обычный A/B тест длится 2–4 недели и измеряет краткосрочный эффект. Но что если изменение влияет на retention через 3 месяца? Или на LTV через год?

Holdout group — это постоянная контрольная группа (обычно 5–10% пользователей), которая не получает никаких новых фич. Сравнивая их с остальными через 3–6 месяцев, можно оценить кумулятивный эффект от всех изменений.

Практикуют LinkedIn, Netflix, Airbnb. Требует дисциплины — holdout нельзя «загрязнять» промежуточными тестами.

Чек-лист перед запуском A/B теста

Гипотеза с механизмом: «Изменение X повысит метрику Y, потому что Z» — прописана и согласована командой.
Primary метрика определена заранее: одна, чёткая, измеримая. Secondary и guardrail — тоже прописаны.
Размер выборки рассчитан: задан MDE, α=0.05, power=0.80. Определена длительность теста.
AA-тест пройден: рандомизация работает корректно, SRM не обнаружен.
Дата окончания зафиксирована: не «когда увидим p<0.05», а конкретная дата или конкретный объём выборки.
Нет запланированных изменений: версия B не будет меняться в ходе теста, нет других тестов на тех же пользователях.
Логирование проверено: события корректно пишутся, user_id проставляется, нет потери событий.
Учтена сезонность: тест охватывает минимум полную неделю, не попадает на нетипичный период (праздники, акции).
Решение зафиксировано заранее: прописано, при каком результате внедряем, а при каком — нет. Не решаем «по ситуации» после получения p-value.

Главное, что нужно запомнить

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

За рамками этого гайда остались: байесовский подход к A/B тестированию, sequential testing (mSPRT, always-valid confidence intervals), interleaving для ранжирования, долгосрочные holdout-эксперименты и платформы для экспериментов (Statsig, GrowthBook, Eppo). Эти темы — для следующего уровня после освоения основ.