TGStat
TGStat
Введите текст для поиска
Расширенный поиск каналов
  • flag Russian
    Язык сайта
    flag Russian flag English flag Uzbek
  • Вход на сайт
  • Каталог
    Каталог каналов и чатов Региональные подборки Тематические подборки Платные каналы Поиск каналов
    Добавить канал/чат
  • Рейтинги
    Рейтинг каналов Рейтинг чатов Рейтинг публикаций
    Рейтинги брендов и персон
  • Аналитика
  • Поиск по публикациям
  • Мониторинг Telegram
  • Продвижение
    Реклама через Яндекс Бизнес Реклама в каналах через TGStat Agency Реклама на сайте TGStat.ru
Мир аналитика данных

12 Aug, 11:04

Открыть в Telegram Поделиться Пожаловаться

📊 A/B-тест: что делаем после окончания эксперимента? Считаем итоги! 🚀

Как вы можете заметить у меня в канале реально много полезного кода. Что ж продолжим эту тенденцию! 😄

В прошлом посте я показала, как до запуска теста рассчитать минимальный детектируемый эффект (MDE) для бинарной метрики (конверсии).
Мы выяснили, что при базовой конверсии ~4.94% и выборке 50 000 пользователей мы сможем «увидеть» только изменения больше 10.99% относительного прироста.
А теперь давайте смоделируем реальный тест – сгенерируем данные, где в тестовой группе конверсия реально выше, и посмотрим, что получится.
Спойлер: наш фактический эффект окажется достаточно большим, чтобы его обнаружить при таком размере выборки. А статистическую значимость мы проверим отдельно. 😉

🎯 Настройки эксперимента

Общий размер выборки: 51 500 пользователей (разбивка 50/50)
Базовая конверсия в контроле: 4,9% (control_conversion = 0.049)
Ожидаемая конверсия в тесте: 5,5% (test_conversion = 0.055)
Относительный прирост: (0,055 – 0,049) / 0,049 ≈ 12,2%

Это тот эффект, который мы хотим обнаружить. Но сможет ли тест его увидеть? Давайте проверим!


🧬 Генерируем синтетические данные
Фиксируем seed=42 для воспроизводимости.
import numpy as np
import pandas as pd
from scipy.stats import chisquare, norm
from statsmodels.stats.proportion import proportions_ztest
import math

n = 51500
control_conversion = 0.049
test_conversion = 0.055
np.random.seed(42)

# Генерируем ID и метку группы (50/50)
active_user_id = np.arange(1, n + 1)
test_id = np.random.binomial(1, 0.5, size=n)

df = pd.DataFrame({'active_user_id': active_user_id, 'test_id': test_id})

# Генерируем конверсии отдельно для каждой группы
df['is_converted'] = 0
df.loc[df['test_id'] == 0, 'is_converted'] = np.random.binomial(1, control_conversion,
size=(df['test_id'] == 0).sum())
df.loc[df['test_id'] == 1, 'is_converted'] = np.random.binomial(1, test_conversion,
size=(df['test_id'] == 1).sum())

df.head()
🔍 Шаг 1. Проверяем разбивку на группы (SRM)
Перед тем как смотреть на конверсию, сначала проверяем, правильно ли пользователи распределились между группами.
Почему это важно?
Если вместо ожидаемых 50/50 мы получили, например, 60/40, результат эксперимента уже вызывает вопросы.
Ожидаем, что в каждой группе будет примерно по 25 750 пользователей.
from scipy.stats import chisquare

control = df[df['test_id']==0]['active_user_id'].nunique()
test = df[df['test_id']==1]['active_user_id'].nunique()

# Ожидаемое распределение (50/50)
total = control + test
print('В тесте присвоены номера групп для:',total,'устройств')
expected = total / 2

# Критерий хи-квадрат
chi2_stat, p_value = chisquare([control, test], f_exp=[expected, expected])

print(f"Хи-квадрат статистика: {chi2_stat:.4f}")
print(f"P-value: {p_value:.6f}")

# Вывод решения
alpha = 0.05
if p_value < alpha:
print("SRM обнаружен — тест ненадёжен.")
else:
print("SRM не обнаружено. Разбивка на группы правильная.")
Результат:
Контроль: 25767, Тест: 25733
Хи-квадрат: 0.0224, p-value: 0.8809
✅ Разбивка корректна – можно продолжать.

📈 Шаг 2. Фактические конверсии – и вот сюрприз!

Считаем реальные доли (в жизни то, что получилось после двух-трех недель томительного ожидания, у нас - то что нагенерировалось после np.random.binomial:
control_users = len(df[df['test_id'] == 0])
test_users = len(df[df['test_id'] == 1])

control_conv = df[df['test_id'] == 0]['is_converted'].sum()
test_conv = df[df['test_id'] == 1]['is_converted'].sum()

cr_control = control_conv / control_users
cr_test = test_conv / test_users

uplift = (cr_test / cr_control - 1) * 100
effect_pp = (cr_test - cr_control) * 100

print(f'Control users: {control_users}')
print(f'Control conversions: {control_conv}')
print(f'CR control: {cr_control:.2%}')
print('==============================')
print(f'Test users: {test_users}')
print(f'Test conversions: {test_conv}')
print(f'Test control: {cr_test:.2%}')
print('==============================')
print(f'Effect: +{effect_pp:.2f} п.п.')
print(f'uplift: {uplift:.2f}%')
Вывод (примерный, при seed=42):
Control users: 25767
Control conversions: 1176
CR control: 4.56%
===================================
Test users: 25733
Test conversions: 1407
Test control: 5.47%
===================================
Effect: +0.90 п.п.
📈 uplift: 19.8%

Почему контроль не 4,9%, а 4,56%? Это нормально. Когда мы задаём: control_conversion = 0.049 мы не говорим: «В контроле будет ровно 4,9% конверсий». Мы говорим: «Каждый пользователь имеет вероятность 4,9% сконвертироваться».
А np.random.binomial() генерирует случайную выборку из этого распределения.
Поэтому фактическая конверсия в конечной выборке может немного отличаться от заданной.
Это ровно то, что происходит и в реальном эксперименте: ожидаемый baseline и фактический baseline — не обязательно одно и то же!

И здесь у нас: ожидали: 4,9% получили: 4,56%
А в тесте: ожидали: 5,5% получили: 5,47%

Именно с фактическими значениями мы дальше и работаем.

🧮 Шаг 3. Считаем фактический эффект. Проводим z‑тест для двух пропорций

Теперь главный вопрос:
Можем ли мы считать разницу между 4,56% и 5,47% статистически значимой?

Для двух независимых конверсий используем z-тест для двух пропорций.
stat, p_value = proportions_ztest(count=[test_conv, control_conv],
nobs=[test_users, control_users], alternative='two-sided')
print(f'z-stat: {stat:.4f}')
print(f'p-value: {p_value:.10f}')

z-stat: 4.6981
p-value: 0.0000026

p_value < 0.05 поэтому отвергаем нулевую гипотезу.🎉 Тест прокрасился! Эффект статистически значим.

🔁 Шаг 4. Сравниваем с MDE, пересчитанным после теста на фактическом baseline и размере выборки
И вот здесь возвращаемся к тому, с чего начали.
MDE показывает, какой минимальный эффект способен обнаружить наш тест при заданных параметрах.
После завершения эксперимента мы можем дополнительно пересчитать его, используя:
- фактический baseline - 4,56%;
- фактический общий размер выборки - 51 500.
Это не «новый правильный MDE», а дополнительная оценка чувствительности теста на фактических данных.

n_total = control_users + test_users
mde_abs, mde_rel, lower, upper = mde_for_conversion(cr_control, n_total)

# Фактический эффект в процентных пунктах
effect_pp = (cr_test - cr_control) * 100

print(f"Конверсия в контроле: {cr_control:.2%}")
print(f"Конверсия в тесте: {cr_test:.2%}")
print(f"Фактический эффект: +{effect_pp:.2f} п.п.")

print("MDE: ±", round(mde_abs*100, 2), "п.п. - абсолютный (", round(mde_rel, 2), "% - относительный)")

print(f'uplift: {uplift:.2f}%')
if uplift > mde_rel:
print("✅ Наш эффект превышает MDE")
else:
print("❌ Эффект меньше MDE")
Получим:
Конверсия в контроле: 4.56%
Конверсия в тесте: 5.47%

Фактический эффект: +0.90 п.п.

MDE: ± 0.52 п.п. - абсолютный ( 11.29 % - относительный)

uplift: 19.80%

✅ Наш фактический эффект (19.80%) превышает MDE(11,29%)
Это означает, что эффект такого масштаба находится в пределах чувствительности теста.

🏁 Итого

Мы прошли полный путь от сырых данных до вывода:
1️⃣ Проверили SRM → разбивка 50/50 корректна
2️⃣ Посчитали фактический CR → 4,56% vs 5,47%
3️⃣ Посчитали эффект → +0,9 п.п. / +19,80%
4️⃣ Проверили статистическую значимость → p-value < 0,05
5️⃣ Сравнили эффект с MDE → 19,80% > 11,29%

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

p-value:
Достаточно ли данных, чтобы считать наблюдаемую разницу статистически значимой?

📎 Вот ноутбук, чтобы Вы смогли поэкспериментировать.

847 0 11 2 9
Каталог
Каталог каналов и чатов Подборки каналов Поиск каналов Добавить канал/чат
Рейтинги
Рейтинг каналов Telegram Рейтинг чатов Telegram Рейтинг публикаций Рейтинги брендов и персон
API
API статистики API поиска публикаций API Callback
Наши каналы
@TGStat @TGStat_Chat @telepulse @TGStatAPI
Почитать
Академия TGStat Исследование Telegram 2019 Исследование Telegram 2021 Исследование Telegram 2023
Контакты
Справочный центр Поддержка Почта Вакансии
Всякая всячина
Пользовательское соглашение Политика конфиденциальности Публичная оферта
Наши боты
@TGStat_Bot @SearcheeBot @TGAlertsBot @tg_analytics_bot @TGStatChatBot