Привет, Хабр!

Сегодня рассмотрим типичные грабли, на которые наступает каждый второй новичок, когда берется за A/B‑тесты.

Ошибка №1: «Мы не проверили корректность рандомизации»

Типичная ситуация: запускаем тест: есть группа А и группа B. В группе А — 10% пользователей, в группе B — тоже 10%. Вроде все ровно. А потом выясняется, что в А у нас почему‑то парни 18–25 лет, а в B — дамы 40+. Не то чтобы это плохо, но сравнивать их уже как‑то странно. Причина? Некорректная рандомизация или неправильная сегментация. Например, вы просто берете Math.random() на фронте и решаете: «Если > 0.5 — в группу А, иначе в B». Но оказывается, что из‑за особенностей потока или кеширования группы распределились не так, как хотелось.

Как исправить:

  1. Делайте рандомизацию на бэкенде.

  2. Используйте стойкие идентификаторы (например, хэш от user_id) для распределения по группам. Это дает некую повторяемость и предсказуемость.

  3. Проверяйте корректность распределения ещё до запуска основного теста.

Пример:

import hashlib

def assign_group(user_id: str):
    # Генерим хэш от user_id, превращаем в число и берём модуль
    # Допустим, хотим 50% в A, 50% в B
    user_hash = hashlib.md5(user_id.encode()).hexdigest()
    user_val = int(user_hash, 16) % 100
    # Если число < 50 — идёт в группу A, иначе в B
    return 'A' if user_val < 50 else 'B'

Так один и тот же юзер всегда в одной группе, а распределение близко к равномерному. Проверяйте статистику перед началом теста — np.bincount() по массиву из 100 000 хэшей даст вам примерно ровное деление.

Ошибка №2: «Мы меняем функционал на ходу»

Типичный сценарий: решили протестить новый дизайн карточки товара. Запускаем тест, часть пользователей видят старый дизайн, часть — новый. Две недели тестим. На третьей неделе менеджер говорит: «Слушай, давай добавим туда еще новую акцию». И вот вы меняете B‑вариант по ходу теста! Проблема? Конечно. Ведь мы уже начитали данные, а тут внезапно B меняется, и сравнение становится некорректным.

Как исправить:

  1. Не менять вариант B во время теста.

  2. Если уж нужно, останавливайте тест и запускайте новый эксперимент.

  3. Пишите код так, чтобы вариант B был изолирован в отдельный компонент. Тогда изменения в основной код не затронут группу B.

Пример:

// Предположим, мы условно проверяем фичу:
function ProductCard({ userGroup }) {
  // Вариант A
  const renderA = () => (
    <div className="product-card">
      <h2>Старый дизайн</h2>
      <p>Обычная цена: 1000 руб.</p>
    </div>
  );

  // Вариант B — выделен отдельно
  // Важно: Не меняем логику по ходу теста, если хотим модифицировать — перезапускаем тест
  const renderB = () => (
    <div className="product-card-b">
      <h2>Новый дизайн</h2>
      <p>Обычная цена: 1000 руб. (Со скидкой 900 руб.)</p>
    </div>
  );

  return userGroup === 'A' ? renderA() : renderB();
}

Ошибка №3: «Мы останавливаем тест, как только видим разницу»

Часто слышал: «О, через три дня видим +5% к конверсии в B. Выключаем тест, всё понятно!». Ну уж нет. Есть такая штука, как статистическая значимость. Возможно, через неделю разницы уже не будет или она сменит знак. Важно дождаться окончания теста с заранее определенными критериями. Без четкого плана остановки эксперимента вы рискуете получить ложноположительные результаты.

Как исправить:

  1. Определить длительность теста и критерии остановки ещё до запуска.

  2. Использовать статистические методы, например, t‑тест или Z‑тест, и убедиться, что p‑value достаточно низкое.

  3. Применять поправки на множественные сравнения, если мы запускаем много тестов.

Пример:

import scipy.stats as stats
import numpy as np

# Пример: CTR для группы A и B
ctr_A = 0.1
ctr_B = 0.12
n_A = 10000
n_B = 10000

conversions_A = int(ctr_A * n_A)
conversions_B = int(ctr_B * n_B)

# Проверим, что у нас есть 2 набора данных: успехи/неуспехи
data_A = [1]*conversions_A + [0]*(n_A - conversions_A)
data_B = [1]*conversions_B + [0]*(n_B - conversions_B)

# Выполним двусторонний t-тест для пропорций
# В реальности для пропорций лучше использовать z-тест, но для примера сгодится и так.
t_stat, p_val = stats.ttest_ind(data_A, data_B)

print("t-статистика:", t_stat)
print("p-значение:", p_val)

# Дальше решаем: если p < 0.05 (или строже, 0.01), считаем, что разница значима.
# Но если мы остановили тест слишком рано, можем получить некорректные выводы.

Ошибка №4: «Мы игнорируем доверительные интервалы и размер эффекта»

Некоторые смотрят только на p‑value. Это ошибка. Предположим, разница статистически значима, но эффект микроскопический. Вы потратили усилия, внедрили новый дизайн, а в итоге получили +0.5% к конверсии. Оно того стоило? Может быть, а может и нет. Нужно смотреть на доверительные интервалы и оценивать величину эффекта.

Поэтому нужно:

  1. Считать не только p‑value, но и доверительные интервалы для метрик.

  2. Оценивать размер эффекта. Иногда стоит задать порог, типа «Мы внедрим новое решение только если конверсия вырастет минимум на 2%.»

Пример доверительных интервалов:

from math import sqrt

def proportion_confidence_interval(conversions, n, z=1.96):
    p = conversions / n
    se = sqrt(p*(1-p)/n)
    ci_lower = p - z*se
    ci_upper = p + z*se
    return p, ci_lower, ci_upper

p_A, lower_A, upper_A = proportion_confidence_interval(conversions_A, n_A)
p_B, lower_B, upper_B = proportion_confidence_interval(conversions_B, n_B)

print("A:", p_A, "CI:", (lower_A, upper_A))
print("B:", p_B, "CI:", (lower_B, upper_B))

# Сравним интервалы. Если интервалы сильно пересекаются — эффект сомнительный.

Даже если p‑value говорит о «значимости», но интервалы перекрывают большинство возможных значений, выгода может быть чисто теоретической.

Ошибка №5: «Мы не учитываем сезонность и другие внешние факторы»

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

Как исправить:

  1. Планируйте тесты на стабильные периоды.

  2. Используйте блокировку — разбивайте пользователей по сегментам с учетом сезонов, гео или канала трафика.

  3. Делайте несколько итераций теста в разные периоды, чтобы исключить влияние временных факторов.

Пример стратификации по сегментам:

# Представим, что есть список пользователей с их гео и у нас разное поведение по странам.
# Нужно распределять попарно из каждого сегмента, чтобы не исказить распределение.
import random

users = [
    {"user_id": "u1", "country": "RU"},
    {"user_id": "u2", "country": "US"},
    {"user_id": "u3", "country": "RU"},
    {"user_id": "u4", "country": "RU"},
    {"user_id": "u5", "country": "US"},
]

# Разобьём пользователей по стране
by_country = {}
for u in users:
    c = u["country"]
    by_country.setdefault(c, []).append(u["user_id"])

# Теперь внутри каждого сегмента рандомим группы A/B
groups = {}
for c, user_list in by_country.items():
    for uid in user_list:
        # Хэшируем, чтобы было детерминированно
        group = assign_group(uid)
        groups[uid] = group

print(groups)
# Видим, что для каждого сегмента распределение примерно ровное.

Подводя итоги

Резюмируем:

  1. Некачественная рандомизация: нужно использовать стойкие идентификаторы, хэширование и проверять распределение до старта эксперимента.

  2. Изменение функционала B‑варианта по ходу теста: нельзя делать, если хотим чистых результатов. Прекращаем тест и запускаем новый.

  3. Преждевременное прекращение теста: без статистической значимости и выжидания достаточного объема выборки можно получить ошибочные выводы.

  4. Игнорирование доверительных интервалов: p‑value — еще не всё. Смотрим на размер эффекта и доверительные интервалы, чтобы понять практическую значимость.

  5. Неучет сезонности и внешних факторов: анализируем данные в стабильные периоды, используем стратификацию и блочное рандомизированное разделение на группы.

Реальность такова, что хороший эксперимент требует терпения и методологической точности.


Освоить мощные навыки анализа данных (анализ требований, статистика, BI) можно на онлайн-курсе «Аналитик данных».

19 декабря в рамках курса пройдет открытый урок, на котором разберем основные ошибки при создании визуализаций данных. Если интересно, записывайтесь по ссылке.

Комментарии (0)