← Назад к вопросам

Какие статистические критерии использовать для A/B-тестов?

1.8 Middle🔥 121 комментариев
#Аналитика и метрики

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

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Статистические критерии для A/B-тестов

A/B-тестирование — это фундаментальный инструмент для принятия решений на основе данных. Data Engineer отвечает за подготовку инфраструктуры и обеспечение корректности статистических расчётов. Выбор правильного критерия — это не просто математика, это бизнес-решение.

Основной workflow

  1. Формулируем гипотезу (null hypothesis H0)
  2. Собираем данные из контрольной и экспериментальной групп
  3. Выбираем подходящий статистический критерий
  4. Вычисляем p-value и доверительные интервалы
  5. Принимаем решение на основе 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 comparisonsInflated alphaBonferroni correction
Not accounting for variantsWrong 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) заранее