← Назад к вопросам
Какие статистические критерии использовать для A/B-тестов?
1.8 Middle🔥 121 комментариев
#Аналитика и метрики
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Статистические критерии для A/B-тестов
A/B-тестирование — это фундаментальный инструмент для принятия решений на основе данных. Data Engineer отвечает за подготовку инфраструктуры и обеспечение корректности статистических расчётов. Выбор правильного критерия — это не просто математика, это бизнес-решение.
Основной workflow
- Формулируем гипотезу (null hypothesis H0)
- Собираем данные из контрольной и экспериментальной групп
- Выбираем подходящий статистический критерий
- Вычисляем p-value и доверительные интервалы
- Принимаем решение на основе alpha (обычно 0.05)
1. T-test (для непрерывных метрик)
import scipy.stats as stats
import numpy as np
def t_test_ab(control_group, experiment_group, alpha=0.05):
"""
Независимый t-test для сравнения средних значений
H0: control_mean == experiment_mean
"""
# Вычисляем статистики
control_mean = np.mean(control_group)
exp_mean = np.mean(experiment_group)
control_std = np.std(control_group, ddof=1)
exp_std = np.std(experiment_group, ddof=1)
# Независимый t-test
t_stat, p_value = stats.ttest_ind(
control_group,
experiment_group,
equal_var=False # Welch's t-test если дисперсии разные
)
# Доверительный интервал (95%)
se = np.sqrt(
(control_std**2 / len(control_group)) +
(exp_std**2 / len(experiment_group))
)
mean_diff = exp_mean - control_mean
ci_lower = mean_diff - 1.96 * se
ci_upper = mean_diff + 1.96 * se
# Решение
is_significant = p_value < alpha
return {
'control_mean': control_mean,
'exp_mean': exp_mean,
'mean_difference': mean_diff,
't_statistic': t_stat,
'p_value': p_value,
'ci_lower': ci_lower,
'ci_upper': ci_upper,
'is_significant': is_significant,
'effect_size': (exp_mean - control_mean) / control_std # Cohen's d
}
# Пример
control_revenue = [100, 105, 98, 102, 110, ...] # 10000 пользователей
exp_revenue = [105, 110, 103, 108, 115, ...] # 10000 пользователей
result = t_test_ab(control_revenue, exp_revenue)
print(f"Mean diff: {result['mean_difference']:.2f}")
print(f"P-value: {result['p_value']:.4f}")
print(f"95% CI: [{result['ci_lower']:.2f}, {result['ci_upper']:.2f}]")
print(f"Significant: {result['is_significant']}")
Когда использовать: revenue, time-on-page, load time (непрерывные метрики)
2. Chi-square test (для категориальных данных)
from scipy.stats import chi2_contingency
def chi_square_test(contingency_table, alpha=0.05):
"""
Chi-square тест для категориальных данных
H0: нет зависимости между переменными
"""
# Contingency table: [control_converted, control_not_converted],
# [exp_converted, exp_not_converted]
chi2, p_value, dof, expected = chi2_contingency(contingency_table)
# Вычисляем конверсию
control_cr = contingency_table[0][0] / contingency_table[0].sum()
exp_cr = contingency_table[1][0] / contingency_table[1].sum()
# Relative Lift
lift = (exp_cr - control_cr) / control_cr * 100
# Доверительный интервал для разницы пропорций
p_control = contingency_table[0][0] / contingency_table[0].sum()
p_exp = contingency_table[1][0] / contingency_table[1].sum()
n_control = contingency_table[0].sum()
n_exp = contingency_table[1].sum()
se_diff = np.sqrt(
(p_control * (1 - p_control) / n_control) +
(p_exp * (1 - p_exp) / n_exp)
)
return {
'control_cr': control_cr,
'exp_cr': exp_cr,
'lift_percent': lift,
'chi2_stat': chi2,
'p_value': p_value,
'is_significant': p_value < alpha
}
# Пример
contingency = np.array([
[500, 9500], # control: 500 converted из 10000
[550, 9450] # experiment: 550 converted из 10000
])
result = chi_square_test(contingency)
print(f"Control CR: {result['control_cr']:.4f} (5.0%)")
print(f"Exp CR: {result['exp_cr']:.4f} (5.5%)")
print(f"Lift: {result['lift_percent']:.1f}%")
print(f"P-value: {result['p_value']:.4f}")
print(f"Significant: {result['is_significant']}")
Когда использовать: conversion rate, click-through rate, binary outcomes
3. Mann-Whitney U test (non-parametric альтернатива t-test)
def mann_whitney_test(control_group, experiment_group, alpha=0.05):
"""
Используй если данные не нормально распределены
или есть выбросы (outliers)
"""
statistic, p_value = stats.mannwhitneyu(
control_group,
experiment_group,
alternative='two-sided'
)
return {
'u_statistic': statistic,
'p_value': p_value,
'is_significant': p_value < alpha
}
# Пример: для данных с выбросами (например, revenue с очень дорогими покупками)
revenue_with_outliers = [10, 15, 20, 25, 30, ..., 50000] # один outlier
Когда использовать: когда данные skewed или содержат outliers
4. Bayesian approach (альтернатива frequentist)
from scipy.stats import beta
def bayesian_ab_test(control_converted, control_total,
exp_converted, exp_total):
"""
Bayesian A/B тест для конверсии
Используем Beta распределение
"""
# Prior: Beta(1, 1) = uniform distribution
# Posterior: Beta(alpha + successes, beta + failures)
control_posterior = beta(1 + control_converted, 1 + control_total - control_converted)
exp_posterior = beta(1 + exp_converted, 1 + exp_total - exp_converted)
# Вероятность что experiment лучше control
n_simulations = 100000
control_samples = control_posterior.rvs(n_simulations)
exp_samples = exp_posterior.rvs(n_simulations)
prob_exp_better = (exp_samples > control_samples).mean()
# Ожидаемый лифт
expected_lift = (exp_samples.mean() - control_samples.mean()) / control_samples.mean() * 100
return {
'prob_exp_better': prob_exp_better,
'expected_lift': expected_lift,
'can_stop': prob_exp_better > 0.95 # обычный порог
}
# Пример
result = bayesian_ab_test(
control_converted=500, control_total=10000,
exp_converted=550, exp_total=10000
)
print(f"Probability experiment is better: {result['prob_exp_better']:.1%}")
print(f"Expected lift: {result['expected_lift']:.2f}%")
print(f"Can stop test: {result['can_stop']}")
Плюсы: более интуитивен, можно остановить тест раньше
Минусы: требует выбора prior, менее стандартизирован
5. Sample size расчёты (перед тестом)
from scipy.stats import norm
def calculate_sample_size(baseline_cr, expected_lift, alpha=0.05, power=0.80):
"""
Вычисляем нужный размер выборки для каждой группы
baseline_cr: текущая конверсия (например 0.05 = 5%)
expected_lift: ожидаемый лифт (например 0.10 = +10%)
alpha: вероятность Type I error (false positive)
power: 1 - beta, где beta = Type II error (false negative)
"""
# Новая конверсия с лифтом
new_cr = baseline_cr * (1 + expected_lift)
# Z-values
z_alpha = norm.ppf(1 - alpha / 2) # двусторонний тест
z_beta = norm.ppf(power)
# Pooled proportion
p_pool = (baseline_cr + new_cr) / 2
# Формула для proportion test
n = ((z_alpha + z_beta) ** 2 * 2 * p_pool * (1 - p_pool)) / \
((new_cr - baseline_cr) ** 2)
return int(np.ceil(n))
# Пример
n = calculate_sample_size(baseline_cr=0.05, expected_lift=0.10)
print(f"Need {n:,} users per group")
# output: Need 3,847 users per group (total 7,694 for A/B test)
Практический SQL расчёт для data warehouse
-- Таблица результатов A/B теста
CREATE TABLE ab_test_results AS
WITH metrics AS (
SELECT
CASE WHEN variant = 'control' THEN 'control' ELSE 'experiment' END as group_name,
COUNT(*) as n,
COUNT(CASE WHEN converted = 1 THEN 1 END) as conversions,
SUM(revenue) as total_revenue,
AVG(revenue) as avg_revenue,
STDDEV(revenue) as stddev_revenue
FROM ab_test_events
WHERE test_id = 'checkout_redesign'
GROUP BY group_name
)
SELECT
group_name,
n,
conversions,
conversions::float / n as conversion_rate,
total_revenue,
avg_revenue,
-- Доверительный интервал для конверсии (95%)
(conversions::float / n) - 1.96 * sqrt((conversions::float / n) * (1 - conversions::float / n) / n) as ci_lower,
(conversions::float / n) + 1.96 * sqrt((conversions::float / n) * (1 - conversions::float / n) / n) as ci_upper
FROM metrics;
Частые ошибки в A/B тестировании
| Ошибка | Последствие | Решение |
|---|---|---|
| Peek'ing (смотрение результатов раньше) | False positive | Установить размер выборки заранее |
| Multiple comparisons | Inflated alpha | Bonferroni correction |
| Not accounting for variants | Wrong sample size | Выбрать метрику ДО теста |
| Outliers не удаляются | Неверный результат | Robust statistics или Mann-Whitney |
| Неправильный baseline | Неправильный расчёт лифта | Проверить историческую конверсию |
Best Practices
- Вычисли sample size перед началом теста
- Не смотри результаты до окончания теста (peek'ing bias)
- Используй двусторонний тест (two-tailed) по умолчанию
- Проверь normality assumption для t-test
- Документируй все метрики до теста
- Используй intent-to-treat анализ (все кто попал в тест)
- Мониторь external validity (результаты в production?)
- Для Bayesian: выбирай reasonable prior'ы
- Рассчитай minimum detectable effect (MDE)
- Фиксируй alpha (0.05) и beta (0.20) заранее