← Назад к вопросам
Какие знаешь способы ускорения A/B-тестов?
1.7 Middle🔥 191 комментариев
#Статистика и A/B тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Способы ускорения A/B-тестов: Практические методы
Время — деньги. В стартапах и крупных компаниях я постоянно сталкивался с давлением: сделать выводы быстрее, не теряя статистическую достоверность. Существует несколько проверенных подходов для ускорения A/B-тестов, которыми я активно пользуюсь.
1. Sequential Testing (Последовательное тестирование)
Вместо ожидания фиксированного размера выборки, мы проверяем результаты во время теста и останавливаемся, когда достаточно доказательств.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
def sequential_ab_test(control_data, test_data, alpha=0.05, beta=0.2, expected_effect=0.1):
"""
Sequential test с использованием метода Вальда (SPRT - Sequential Probability Ratio Test)
"""
control_conversions = np.sum(control_data)
test_conversions = np.sum(test_data)
# Базовая конверсия
p0 = control_conversions / len(control_data)
# Ожидаемая конверсия в тестовой группе
p1 = p0 * (1 + expected_effect)
# Границы Вальда
A = (1 - beta) / alpha
B = beta / (1 - alpha)
a = np.log(A) / np.log(p1 * (1 - p0) / (p0 * (1 - p1)))
b = np.log(B) / np.log(p1 * (1 - p0) / (p0 * (1 - p1)))
print(f"\n===== SEQUENTIAL TESTING =====")
print(f"Контрольная группа: {control_conversions}/{len(control_data)} = {p0:.2%}")
print(f"Тестовая группа: {test_conversions}/{len(test_data)} = {test_conversions/len(test_data):.2%}")
print(f"\nГраницы принятия решения:")
print(f"Граница A (Continue): {a:.2f}")
print(f"Граница B (Accept H1): {b:.2f}")
# Отношение вероятностей
lr = (p1 * (1 - p0) / (p0 * (1 - p1))) ** (test_conversions - control_conversions) * \
((1 - p1) / (1 - p0)) ** (len(test_data) - test_conversions - len(control_data) + control_conversions)
log_lr = np.log(lr)
print(f"\nТекущее log-отношение вероятностей: {log_lr:.2f}")
if log_lr >= np.log(A):
return "STOP: Тестовая группа лучше (H1 отклонена)"
elif log_lr <= np.log(B):
return "STOP: Нет значимого различия (H0 отклонена)"
else:
return "CONTINUE: Собирайте больше данных"
# Пример использования
control = np.random.binomial(1, 0.10, 2000)
test = np.random.binomial(1, 0.115, 2000) # На 15% лучше
result = sequential_ab_test(control, test, expected_effect=0.15)
print(f"\nРезультат: {result}")
2. Power Analysis и минимальный размер выборки
from statsmodels.stats.power import proportions_ztest
def calculate_sample_size(baseline_conversion, min_effect, alpha=0.05, power=0.80):
"""
Вычисляет минимальный размер выборки для достижения нужной мощности
"""
# Минимальная конверсия в тестовой группе
test_conversion = baseline_conversion * (1 + min_effect)
# Используем встроенную функцию
effect_size = abs(test_conversion - baseline_conversion) / \
np.sqrt(baseline_conversion * (1 - baseline_conversion))
# Примерный расчет (более точный через statsmodels)
from statsmodels.stats.power import tt_ind_solve_power
# Для пропорций используем z-тест
z_alpha = stats.norm.ppf(1 - alpha/2)
z_beta = stats.norm.ppf(power)
n = 2 * ((z_alpha + z_beta) / effect_size) ** 2
return int(np.ceil(n))
# Пример расчета
baseline = 0.05 # 5% базовая конверсия
min_effect = 0.20 # Хотим обнаружить 20% улучшение
n_required = calculate_sample_size(baseline, min_effect, power=0.80)
print(f"\nТребуемый размер выборки (per group): {n_required:,}")
print(f"Общее время при 100 users/day: {n_required * 2 / 100:.1f} дней")
3. Bayesian A/B Testing (Байесовский подход)
from scipy.stats import beta as beta_dist
def bayesian_ab_test(control_conversions, control_samples,
test_conversions, test_samples,
prior_alpha=1, prior_beta=1):
"""
Байесовский A/B тест с Beta-Bernoulli моделью
Позволяет получить вероятность того, что одна группа лучше другой
"""
# Апостериорные параметры (Beta распределение)
alpha_control = control_conversions + prior_alpha
beta_control = control_samples - control_conversions + prior_beta
alpha_test = test_conversions + prior_alpha
beta_test = test_samples - test_conversions + prior_beta
# Симуляция из апостериорного распределения
n_simulations = 100000
samples_control = np.random.beta(alpha_control, beta_control, n_simulations)
samples_test = np.random.beta(alpha_test, beta_test, n_simulations)
# Вероятность того, что тест лучше контроля
prob_test_better = np.mean(samples_test > samples_control)
# Ожидаемое улучшение
expected_lift = np.mean(samples_test - samples_control)
print(f"\n===== BAYESIAN A/B TEST =====")
print(f"Контрольная группа: {control_conversions}/{control_samples} = {control_conversions/control_samples:.2%}")
print(f"Тестовая группа: {test_conversions}/{test_samples} = {test_conversions/test_samples:.2%}")
print(f"\nВероятность того, что тест лучше контроля: {prob_test_better:.1%}")
print(f"Ожидаемое улучшение (lift): {expected_lift:.2%}")
print(f"\nDOMAIN:")
# 95% credible interval для lift
percentile_025 = np.percentile(samples_test - samples_control, 2.5)
percentile_975 = np.percentile(samples_test - samples_control, 97.5)
print(f"95% Credible Interval для lift: [{percentile_025:.2%}, {percentile_975:.2%}]")
# Визуализация
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Распределения конверсий
x = np.linspace(0, max(samples_control.max(), samples_test.max()), 100)
axes[0].hist(samples_control, bins=50, alpha=0.5, density=True, label='Контроль')
axes[0].hist(samples_test, bins=50, alpha=0.5, density=True, label='Тест')
axes[0].set_xlabel('Конверсия')
axes[0].set_ylabel('Плотность')
axes[0].set_title('Апостериорные распределения конверсий')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Распределение разницы
lift_dist = samples_test - samples_control
axes[1].hist(lift_dist, bins=50, alpha=0.7, edgecolor='black')
axes[1].axvline(0, color='r', linestyle='--', linewidth=2, label='Нет разницы')
axes[1].axvline(expected_lift, color='g', linestyle='--', linewidth=2, label='Ожидаемый lift')
axes[1].set_xlabel('Lift')
axes[1].set_ylabel('Частота')
axes[1].set_title('Распределение lift (Тест - Контроль)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return prob_test_better, expected_lift
# Пример использования
prob_better, lift = bayesian_ab_test(200, 2000, 250, 2000)
print(f"\nВывод: {'Тест лучше' if prob_better > 0.95 else 'Результаты неубедительны'}")
4. Stratified Sampling (Стратифицированная выборка)
def stratified_ab_test(data, stratify_column, treatment_column, outcome_column):
"""
Стратифицированный A/B тест уменьшает дисперсию
и позволяет получить результаты быстрее
"""
results = {}
overall_effect = 0
print(f"\n===== STRATIFIED A/B TEST =====")
strata = data[stratify_column].unique()
for stratum in strata:
stratum_data = data[data[stratify_column] == stratum]
control = stratum_data[stratum_data[treatment_column] == 0][outcome_column].mean()
test = stratum_data[stratum_data[treatment_column] == 1][outcome_column].mean()
effect = test - control
weight = len(stratum_data) / len(data)
results[stratum] = {'control': control, 'test': test, 'effect': effect, 'weight': weight}
overall_effect += effect * weight
print(f"\nСтрата '{stratum}':")
print(f" Контроль: {control:.4f}")
print(f" Тест: {test:.4f}")
print(f" Эффект: {effect:.4f} (вес: {weight:.1%})")
print(f"\nОбщий эффект (weighted): {overall_effect:.4f}")
return results, overall_effect
# Пример
data = pd.DataFrame({
'device': np.repeat(['mobile', 'desktop'], 2000),
'treatment': np.tile(np.repeat([0, 1], 1000), 2),
'conversion': np.random.binomial(1, np.repeat([0.05, 0.06], 4000))
})
results, overall_effect = stratified_ab_test(data, 'device', 'treatment', 'conversion')
5. Использование variance reduction techniques (CUPED)
def cuped_analysis(pre_experiment, post_experiment, control, test_group):
"""
CUPED (Controlled experiment Using Pre-Experiment Data)
Использует исторические данные для уменьшения дисперсии
"""
# Вычисляем ковариацию между pre и post
covariate = np.cov(pre_experiment, post_experiment)[0, 1]
var_pre = np.var(pre_experiment)
# CUPED коэффициент
theta = covariate / var_pre if var_pre > 0 else 0
# Скорректированные значения
adjusted_post = post_experiment - theta * (pre_experiment - pre_experiment.mean())
# Вычисляем эффект на скорректированных данных
control_adjusted = adjusted_post[~test_group].mean()
test_adjusted = adjusted_post[test_group].mean()
effect_adjusted = test_adjusted - control_adjusted
variance_adjusted = np.var(adjusted_post[~test_group]) + np.var(adjusted_post[test_group])
# Сравниваем с обычным анализом
control_unadjusted = post_experiment[~test_group].mean()
test_unadjusted = post_experiment[test_group].mean()
effect_unadjusted = test_unadjusted - control_unadjusted
variance_unadjusted = np.var(post_experiment[~test_group]) + np.var(post_experiment[test_group])
print(f"\n===== CUPED ANALYSIS =====")
print(f"Без CUPED:")
print(f" Эффект: {effect_unadjusted:.4f}")
print(f" Стандартная ошибка: {np.sqrt(variance_unadjusted / len(post_experiment)):.4f}")
print(f"\nС CUPED:")
print(f" Эффект: {effect_adjusted:.4f}")
print(f" Стандартная ошибка: {np.sqrt(variance_adjusted / len(post_experiment)):.4f}")
print(f"\nУменьшение дисперсии: {(1 - variance_adjusted/variance_unadjusted)*100:.1f}%")
print(f"Ускорение теста: {variance_unadjusted/variance_adjusted:.1f}x")
return effect_adjusted, variance_adjusted
# Пример использования
n = 5000
pre_data = np.random.randn(n)
post_data = pre_data * 0.7 + np.random.randn(n) * 0.3 + np.repeat([0, 0.05], n // 2) # Лечение
test_group = np.repeat([False, True], n // 2)
cuped_analysis(pre_data, post_data, ~test_group, test_group)
6. Практические рекомендации по ускорению
Таблица сравнения подходов:
| Метод | Ускорение | Сложность | Применимость |
|---|---|---|---|
| Sequential Testing | 30-50% | Средняя | Высокая (если мало конвесрий) |
| Bayesian | 20-40% | Средняя | Часто (более гибкий) |
| Stratification | 15-30% | Средняя | При выраженной гетерогенности |
| CUPED | 25-50% | Высокая | При наличии исторических данных |
| Power Analysis | Подготовка | Низкая | Всегда |
Мой подход:
- Всегда начните с Power Analysis — определите реалистичный размер выборки
- Используйте Sequential Testing для быстрого отклонения очевидно неработающих гипотез
- Для долгосрочных тестов применяйте Bayesian подход и CUPED
- Стратифицируйте по ключевым демографическим признакам
Ловушки:
- Не используйте Peeking (промежуточные проверки) в частотистском подходе — увеличивает риск Type I ошибки
- Байесовский подход более tolerant, но требует осмысленные prior-ы
- CUPED требует стабильной связи между pre и post