Привет, Хабр!
В A/B тестах можно ошибиться ещё до того, как первая строчка кода теста будет написана. А последствия этих ошибок сказываются не только на результатах одного эксперимента, а на всей продуктовой стратегии компании: на найме, на развитии функциональности, на распределении инвестиций.
Сегодня рассмотрим пять системных ошибках, которые делают ваши A/B тесты ненадёжными — даже если снаружи всё выглядит корректно.
Недостаточная мощность теста
Одна из самых коварных ошибок в A/B тестах происходит ещё до того, как первые пользователи увидят новую версию продукта. Допустим, вы выкатываете новое оформление корзины: оно чище, компактнее, всё выглядит логичнее. Через неделю анализируете результаты и видите прирост конверсии на 1%. С одной стороны — вроде бы плюс, с другой — величина кажется настолько маленькой, что вы решаете: «ну, это неэффект». И откатываете обновление.
Именно в этом месте совершается незаметная, но очень тяжёлая ошибка.
То, что прирост кажется маленьким или неубедительным, ещё ничего не значит. Проблема может быть не в слабости эффекта, а в том, что сам тест изначально был обречён его не заметить. Причина проста: недостаточная мощность теста.
Мощность теста — это вероятность обнаружить реальное различие между группами, если оно действительно существует. Проще говоря, это шанс, что тест увидит эффект, а не пропустит его.
Если выборка слишком маленькая, мощность теста падает. При этом возрастает вероятность совершить ошибку второго рода: заключить, что эффекта нет, когда на самом деле он есть.
Причем реальная трагедия даже не в том, что один хороший тест будет испорчен. А в том, что вы начинаете системно отбрасывать реальные улучшения, полагая, что они не работают. Это ломает развитие продукта на уровне стратегии.
Мощность теста зависит от трёх факторов:
от размера ожидаемого эффекта
effect size
— насколько сильно вы ожидаете изменить метрику;от желаемого уровня мощности (обычно 80–90%);
от уровня значимости
alpha
, который вы выбрали для теста (обычно 5%).
И если тест рассчитан на то, чтобы увидеть только крупные эффекты (скажем, +10%), а реальный прирост скромнее (например, +1–2%), он гарантированно останется незамеченным.
Чем сильнее оптимизирована воронка или функциональность, тем мельче становятся эффекты от новых изменений. Ожидать прыжков на +10% в зрелом магазине или сервисе — наивно.
Посмотрим пример расчета:
from statsmodels.stats.power import TTestIndPower
analysis = TTestIndPower()
required_sample = analysis.solve_power(effect_size=0.2, alpha=0.05, power=0.8, alternative='two-sided')
print(f"Минимальный размер выборки на группу: {int(required_sample)} пользователей.")
Рассчитываем, сколько пользователей нужно в каждой группе, чтобы заметить эффект размером 0.2 (что соответствует умеренному изменению, по критериям Коэна) с вероятностью 80% при уровне значимости 5%.
Получим примерно 393 пользователя на группу. То есть 786 пользователей суммарно.
Если вы собрали только 150 пользователей в каждую группу, мощность теста падает ниже 40%. Это значит, что шанс зафиксировать реальный эффект становится меньше, чем шанс его пропустить. Иными словами, тест в этом случае — просто шум и потраченные впустую ресурсы.
Когда тест лучше не начинать
Иногда чтобы увидеть разницу, вам нужно собрать, например, 10 000 пользователей в каждую группу. Если трафик страницы составляет 500 пользователей в месяц — разумнее честно признать, что тест нерентабелен.
Или ставить себе другую цель: измерять только крупные изменения (скажем, +15% и выше), пусть даже за счёт упрощения гипотез.
В противном случае тест будет идти месяцами без внятного результата, а любой шум будет восприниматься как возможный сигнал.
Ранняя остановка теста
Ещё одна ошибка, которую встречал десятки раз. Запускаем тест, через два дня видим: «вариант B на 8% лучше варианта A». Руководство требует немедленных выводов, команда выкатывает победителя. Через месяц выясняется, что ничего лучше не стало. Почему?
Потому что поймали случайный флуктуационный скачок. И остановили тест, когда он прыгнул вверх. В реальности разницы не было.
Например:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# Моделируем 50000 пользователей без реальной разницы
n = 50000
control = np.random.binomial(1, 0.1, n)
variant = np.random.binomial(1, 0.1, n)
# Кумулятивная разница
cumulative_diff = (np.cumsum(variant) / np.arange(1, n + 1)) - (np.cumsum(control) / np.arange(1, n + 1))
plt.plot(cumulative_diff)
plt.axhline(0, color='red', linestyle='--')
plt.xlabel('Пользователи')
plt.ylabel('Кумулятивная разница в конверсии')
plt.title('Как флуктуации "рисуют" ложные эффекты')
plt.show()

Местами разница достигает нескольких процентов. При этом реальной разницы между группами нет вообще. Это чистый шум.
Когда вы останавливаете тест по «временным» всплескам, вы по сути зафиксировали фальшивую победу. Именно поэтому корректные тесты или идут до заранее установленной даты, или используют последовательные методы с корректировкой уровней значимости.
Ранняя остановка теста по промежуточным данным уничтожает доверие к результатам.
Но даже если вы рассчитали мощность правильно, вас поджидает другая ловушка — ошибка ранней остановки теста.
Неверная целевая метрика
Иногда тесты фейлятся ещё на уровне постановки задачи. Вроде всё правильно: есть контроль, есть вариант, собираем данные. Но метрика выбрана неправильная.
Типичный пример: тест нового баннера. Смотрят на клики по баннеру — выросли. Отлично. Запускаем на всех. Через два месяца метрики бизнеса падают. Почему?
Потому что клик по баннеру — это прокси‑метрика. А бизнесу нужна покупка.
Посмотрим практический кусок данных:
import pandas as pd
df = pd.DataFrame({
'user_id': range(10000),
'group': np.random.choice(['control', 'variant'], 10000),
'purchase': np.random.binomial(1, 0.05, 10000),
'banner_clicks': np.random.poisson(2, 10000)
})
conversion_rates = df.groupby('group')['purchase'].mean()
print("Конверсия в покупку:")
print(conversion_rates)
Конверсия в покупку:
group
control 0.049333
variant 0.048279
Name: purchase, dtype: float64
Две средние по группам: вероятность покупки для control
и для variant
. Даже если баннер кликали чаще, если конверсии в покупку нет — эффект отрицательный.
Если бизнес‑метрика — это покупки или выручка, то только на них и надо смотреть как на primary metric. Всё остальное — вспомогательные объясняющие признаки.
Без адекватной привязки метрик к бизнес‑результатам любое тестирование превращается в самообман.
Переходим к следующей проблеме.
Игнорирование источников трафика и когортных различий
Ещё одна ошибка, тихая и почти незаметная. Вы думаете, что рандомизация работает идеально: пользователей равномерно раскидало по группам. На самом деле нет.
Допустим, в одну группу случайно попало больше пользователей из SEO‑трафика, а в другую — из платной рекламы. Эти пользователи ведут себя радикально по‑разному. Если вы не контролируете этот фактор, тест будет искажен.
Смотрим:
import numpy as np
import pandas as pd
# Генерируем базовый датафрейм
df = pd.DataFrame({
'user_id': np.arange(10000),
'group': np.random.choice(['control', 'variant'], 10000)
})
# Добавляем источник трафика
df['source'] = np.random.choice(['organic', 'paid', 'referral'], size=10000, p=[0.6, 0.3, 0.1])
# Задаём вероятность конверсии в зависимости от источника
def simulate_conversion(row):
if row['source'] == 'paid':
return np.random.binomial(1, 0.15) # платный трафик конвертится лучше
elif row['source'] == 'referral':
return np.random.binomial(1, 0.08) # рефералы средние
else:
return np.random.binomial(1, 0.04) # органика слабее
df['conversion'] = df.apply(simulate_conversion, axis=1)
# Пивотируем таблицу для анализа
pivot = df.pivot_table(index='source', columns='group', values='conversion', aggfunc='mean')
print(pivot)
Получаем
group control variant
source
organic 0.033445 0.043101
paid 0.164799 0.149900
referral 0.056452 0.073559
В итоге видим среднюю конверсию пользователей из разных источников organic
, paid
, referral
в каждой группе control
, variant
. Видно, что пользователи, пришедшие через платный трафик paid
, показывают самую высокую конверсию: около 16% в контрольной группе и 15% в тестовой. Пользователи из органики organic
конвертируются заметно хуже — только 3–4%. Реферальные пользователи referral
занимают промежуточное положение с конверсией в диапазоне 5–7%. Эта картина сразу демонстрирует проблему: если одна из групп случайно получила больше платного трафика, то её средняя конверсия будет выше вне зависимости от реального эффекта тестируемого изменения.
Стратификация по источникам, устройствам и когортам обязательна для валидности.
А теперь о множественном тестирование без коррекции.
Множественное тестирование без коррекции
Когда вы запускаете A/B тест, вы проверяете гипотезу: действительно ли между контрольной и тестовой группой есть различие. Для этого обычно считают p-value
— вероятность получить наблюдаемое или более экстремальное значение при условии, что в реальности различия нет. В классической практике используется порог 0.05: если p-value < 0.05
, говорят о статистической значимости.
На небольших тестах это работает приемлемо. Но по правде говоря продуктовый процесс устроен иначе: команды тестируют десятки изменений одновременно. Новая форма логина, новая цена доставки, новая анимация на кнопке, тест баннера, тест воронки на мобильных пользователях. Всё это запускается параллельно.
И тут возникает проблема: чем больше гипотез вы проверяете одновременно, тем выше вероятность получить хотя бы один ложный результат.
Если вы проводите 20 независимых тестов с уровнем значимости 0.05, вероятность того, что хотя бы один из них покажет значимый результат случайно, составляет примерно 64%. Это не баг статистики — это её естественное поведение.
То есть вы делаете 20 экспериментов и получаете якобы успех в одном из них. Но этот успех — чисто случайный, шум. И вы тратите силы на раскатывание фейкового улучшения, которое на самом деле портит продукт или ничего не меняет.
Напишем небольшой код:
from statsmodels.stats.multitest import multipletests
import numpy as np
# Симулируем 20 тестов без реального эффекта
p_values = np.random.uniform(0, 1, 20)
# Применяем коррекцию Benjamini-Hochberg
corrected_pvals = multipletests(p_values, method='fdr_bh')[1]
print("Оригинальные p-значения:", p_values)
print("После коррекции по Benjamini-Hochberg:", corrected_pvals)
Оригинальные p-значения: [0.02054711 0.97642876 0.86502454 0.75200579 0.79367722 0.17835206
0.60653254 0.64450488 0.76183256 0.87859927 0.62274928 0.89011043
0.3482894 0.06490344 0.74247471 0.44905614 0.06622755 0.28928467
0.81680972 0.34115871]
После коррекции по Benjamini-Hochberg: [0.4109422 0.97642876 0.93695834 0.93695834 0.93695834 0.89176028
0.93695834 0.93695834 0.93695834 0.93695834 0.93695834 0.93695834
0.93695834 0.44151697 0.93695834 0.93695834 0.44151697 0.93695834
0.93695834 0.93695834]
На выходе получаем два массива: первый — это сырые p‑значения, которые выглядят достаточно разбросанными по интервалу от 0 до 1. Где‑то среди них наверняка окажутся значения меньше 0.05, создавая иллюзию статистической значимости.
Второй массив — это скорректированные p‑значения после применения процедуры Benjamini‑Hochberg, которая контролирует уровень ложных открытий. После коррекции многие из значимых p‑значений вырастают выше 0.05, и победные результаты исчезают.
Без коррекции вы почти гарантированно сделаете ложный вывод хотя бы в одном из тестов. Причём чем больше параллельных гипотез, тем больше фальшивых победителей у вас будет.
Заключение
A/B тестирование требует уважения к случайности, к структуре выборки, к метрикам. Оно требует понимания того, что любой быстрый результат почти наверняка фальшив.
Ошибки, о которых я рассказал, происходят не потому, что мы глупы. Они происходят потому, что мы устроены верить глазам и первому впечатлению, а не холодной статистике.
Но продукт, который вырастает из правильных тестов, строится на реальности. А не на иллюзиях.
Ошибки в A/B тестах часто коренятся в слабом анализе данных, неправильной постановке бизнес-метрик и незнании альтернативных методов тестирования. Если хотите укрепить эти компетенции, обратите внимание на эти три открытых урока в Otus:
30 апреля — Важные навыки аналитика
12 мая — Формирование бизнес-модели продукта на примере Business Model Canvas
2 июня — Байесовское A/B тестирование
Больше актуальных навыков по аналитике вы можете получить в рамках практических онлайн-курсов от экспертов отрасли.