Проводил ли A/B тестирование и как анализировал результаты

«Проводил ли A/B тестирование и как анализировал результаты» — вопрос из категории A/B тестирование, который задают на 26% собеседований Data Scientist / ML Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. Проект: Оптимизация конверсии на лендинге

Задача: Сравнить две версии кнопки CTA (Call-to-Action)

  • Контрольная группа (A): Зеленая кнопка "Купить сейчас"
  • Тестовая группа (B): Оранжевая кнопка "Получить скидку"

Метрики:

  • Основная: Конверсия (клики/показы)
  • Вторичные: Время на странице, bounce rate

2. Статистический анализ в Python

import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.stats.api as sms
import matplotlib.pyplot as plt

# Генерация данных (пример)
np.random.seed(42)
control_size = 5000
treatment_size = 5000

# Контрольная группа (базовая конверсия 10%)
control_conversions = np.random.binomial(1, 0.10, control_size)
# Тестовая группа (улучшенная конверсия 12%)
treatment_conversions = np.random.binomial(1, 0.12, treatment_size)

# Базовые статистики
control_rate = control_conversions.mean()
treatment_rate = treatment_conversions.mean()
relative_improvement = (treatment_rate - control_rate) / control_rate

print(f"Control conversion rate: {control_rate:.3%}")
print(f"Treatment conversion rate: {treatment_rate:.3%}")
print(f"Relative improvement: {relative_improvement:.2%}")

3. Проверка статистической значимости

# Z-test для пропорций
from statsmodels.stats.proportion import proportions_ztest

count = np.array([control_conversions.sum(), treatment_conversions.sum()])
nobs = np.array([control_size, treatment_size])

z_stat, p_value = proportions_ztest(count, nobs, alternative='smaller')
print(f"Z-statistic: {z_stat:.3f}")
print(f"P-value: {p_value:.5f}")

# Доверительный интервал разницы
from statsmodels.stats.proportion import confint_proportions_2indep

ci_low, ci_high = confint_proportions_2indep(
    count1=treatment_conversions.sum(), nobs1=treatment_size,
    count2=control_conversions.sum(), nobs2=control_size,
    method='wald',
    compare='diff',
    alpha=0.05
)
print(f"95% CI for difference: [{ci_low:.4f}, {ci_high:.4f}]")

4. Расчет мощности теста и размера выборки

# Расчет необходимого размера выборки до запуска теста
effect_size = sms.proportion_effectsize(0.10, 0.12)  # Минимальный детектируемый эффект
power_analysis = sms.NormalIndPower()

required_n = power_analysis.solve_power(
    effect_size=effect_size,
    power=0.8,           # 80% вероятность обнаружить эффект
    alpha=0.05,         # Уровень значимости 5%
    ratio=1.0           # Равные группы
)

print(f"Required sample size per group: {int(required_n)}")

5. Анализ воронки и сегментация

# Анализ по сегментам пользователей
data = pd.DataFrame({
    'group': ['control'] * control_size + ['treatment'] * treatment_size,
    'converted': np.concatenate([control_conversions, treatment_conversions]),
    'segment': np.random.choice(['new', 'returning'], 
                               control_size + treatment_size,
                               p=[0.6, 0.4])
})

# Конверсия по сегментам
segment_analysis = data.groupby(['group', 'segment'])['converted'] 
                      .agg(['mean', 'count', 'sum'])
print(segment_analysis)

# Статистическая значимость по сегментам
for segment in ['new', 'returning']:
    segment_data = data[data['segment'] == segment]
    count = segment_data.groupby('group')['converted'].sum().values
    nobs = segment_data.groupby('group').size().values

    z_stat, p_value = proportions_ztest(count, nobs)
    print(f"Segment {segment}: p-value = {p_value:.4f}")

6. Визуализация результатов

import seaborn as sns

# Распределение конверсий по дням (для проверки временных эффектов)
daily_data = pd.DataFrame({
    'day': np.tile(range(14), control_size // 14 + treatment_size // 14)[:control_size + treatment_size],
    'group': data['group'],
    'converted': data['converted']
})

daily_conversion = daily_data.groupby(['day', 'group'])['converted'].mean().reset_index()

plt.figure(figsize=(12, 6))
sns.lineplot(data=daily_conversion, x='day', y='converted', hue='group')
plt.title('Daily Conversion Rates')
plt.xlabel('Day')
plt.ylabel('Conversion Rate')
plt.axhline(y=control_rate, color='blue', linestyle='--', alpha=0.5)
plt.axhline(y=treatment_rate, color='orange', linestyle='--', alpha=0.5)
plt.show()

7. Практические рекомендации из опыта:

  • Перед запуском:

    • Рассчитываю минимальный детектируемый эффект (MDE)
    • Определяю длительность теста с учетом сезонности
    • Настраиваю систему сбора данных и event tracking
  • Во время теста:

    • Мониторю равномерность распределения трафика
    • Проверяю отсутствие технических проблем
    • Отслеживаю вторичные метрики на предмет негативных эффектов
  • После теста:

    • Анализирую результаты по сегментам
    • Проверяю устойчивость результатов во времени
    • Документирую выводы и рекомендации для product-менеджеров

Пример вывода из реального проекта: "Тест показал статистически значимое увеличение конверсии на 18% (p-value = 0.012). Эффект особенно выражен среди новых пользователей (+25%). Рекомендую внедрить изменение и мониторить долгосрочные метрики удержания."