CUPEDA/B-тестыvariance reductionPythonстатистикааналитика данных

CUPED для аналитика: variance reduction в A/B-тестах с Python кодом

2026-06-04 18 мин

CUPED (Controlled pre-Experiment Data) — техника variance reduction в A/B-тестах, разработанная Microsoft в 2013 году. Снижает необходимый sample size в 1.5-2 раза без изменения статистических гарантий. Используется в Microsoft / Netflix / Uber / Yandex / Avito. В этом гайде разберу математику + Python код + конкретный кейс с benchmarks.

Главное про CUPED
CUPED работает потому что **пре-экспериментальные данные** предсказывают пост-экспериментальные. Если юзер был активным до теста, скорее всего и после — поэтому вычитаем baseline и анализируем «остаточную» вариацию. **Variance reduction 30-60% типично, sample size 2-3x меньше**.

Зачем нужен CUPED

Проблема обычного A/B: многие метрики (ARPU, sessions, GMV) имеют высокую вариацию между юзерами. Один power user может смазать результат целой группы.

Пример:

CUPED идея: если юзер был «power» до эксперимента, скорее всего он будет «power» и после — независимо от treatment. Вычитаем baseline, анализируем только incremental изменение.

→ Расчёт sample size A/B-теста

Математика CUPED

Базовая формула

Стандартный t-test:

$$t = \frac{\bar{Y}_{treatment} - \bar{Y}_{control}}{SE}$$

CUPED-adjusted variable:

$$Y^*_i = Y_i - \theta \cdot (X_i - \bar{X})$$

где:

**Затем стандартный t-test применяется на $Y^*$ вместо $Y$.**

Variance reduction

$$Var(Y^*) = Var(Y) \cdot (1 - \rho^2)$$

где $\rho$ — корреляция между $X$ и $Y$.

Practical:

Для большинства user-level метрик корреляция pre/post 0.6-0.8 → variance reduction 36-64%.

Python implementation

\\\python

import numpy as np

import pandas as pd

from scipy import stats

def cuped(pre_data, post_data, treatment_indicator):

"""

CUPED adjustment for A/B test.

pre_data: array of pre-experiment metric values

post_data: array of post-experiment metric values

treatment_indicator: 0 (control) or 1 (treatment)

"""

# Step 1: compute theta

theta = np.cov(pre_data, post_data)[0, 1] / np.var(pre_data)

# Step 2: compute Y* = Y - theta * (X - X_mean)

pre_mean = np.mean(pre_data)

post_adjusted = post_data - theta * (pre_data - pre_mean)

# Step 3: standard t-test on adjusted variable

control = post_adjusted[treatment_indicator == 0]

treatment = post_adjusted[treatment_indicator == 1]

t_stat, p_value = stats.ttest_ind(treatment, control, equal_var=False)

# Effect size

lift = (np.mean(treatment) - np.mean(control)) / np.mean(control)

# Variance reduction vs no CUPED

raw_var = np.var(post_data)

cuped_var = np.var(post_adjusted)

var_reduction = 1 - (cuped_var / raw_var)

return {

'theta': theta,

't_stat': t_stat,

'p_value': p_value,

'lift': lift,

'variance_reduction': var_reduction,

}

\\\

Benchmark на реальных данных

Симулирую данные и сравниваю CUPED vs обычный t-test:

\\\python

np.random.seed(42)

n = 10000

# Generate user-level data with high pre/post correlation

user_baseline = np.random.exponential(scale=100, size=n) # baseline ARPU

random_noise = np.random.normal(0, 30, size=n)

pre_arpu = user_baseline + random_noise # pre-experiment

# Post-experiment: same baseline + new noise + treatment effect

post_noise = np.random.normal(0, 30, size=n)

treatment = np.random.binomial(1, 0.5, size=n)

effect_size = 5 # 5 рублей lift для treatment

post_arpu = user_baseline + post_noise + treatment * effect_size

# Run both tests

result_no_cuped = stats.ttest_ind(

post_arpu[treatment == 1],

post_arpu[treatment == 0],

equal_var=False

)

result_cuped = cuped(pre_arpu, post_arpu, treatment)

print(f"Без CUPED p-value: {result_no_cuped.pvalue:.4f}")

print(f"С CUPED p-value: {result_cuped['p_value']:.4f}")

print(f"Variance reduction: {result_cuped['variance_reduction']:.1%}")

\\\

Результаты (на 10K юзеров):

То есть CUPED позволил detect лифт 5₽ на тех же данных где обычный test не справился.

Когда CUPED работает (и когда нет)

✅ Когда работает

❌ Когда CUPED не помогает

Расширение: CUPED++

Multivariate CUPED — использовать несколько covariates:

\\\python

from sklearn.linear_model import LinearRegression

def multivariate_cuped(X_pre, Y_post, treatment):

"""X_pre is matrix (n_samples, n_features) of pre-experiment metrics."""

lr = LinearRegression()

lr.fit(X_pre, Y_post)

Y_predicted = lr.predict(X_pre)

Y_adjusted = Y_post - Y_predicted + np.mean(Y_predicted)

# Standard t-test on adjusted

return stats.ttest_ind(

Y_adjusted[treatment == 1],

Y_adjusted[treatment == 0],

equal_var=False

)

\\\

Дополнительные covariates:

Multivariate CUPED обычно даёт дополнительные 5-10% variance reduction против uni-CUPED.

Common mistakes

❌ Использовать post-experiment data в качестве covariate

Это нарушает statistical validity. Pre/post должны быть строго временно разделены.

❌ Применять CUPED только в treatment группе

CUPED применяется ко всем (control + treatment) одинаково. Это unbiased adjustment.

❌ Не проверять distribution pre-experiment

Если pre-experiment был во время аномалии (Black Friday, COVID) — covariate может вводить в заблуждение.

Когда использовать в практике

Production setup в Yandex / Microsoft:

FAQ

CUPED работает с binary метриками?

Минимальный gain. Binary metrics уже low variance.

Сколько недель pre-experiment data нужно?

Минимум 4, оптимально 8-12. Меньше — недостаточно signal, больше — outdated.

CUPED vs stratification — что лучше?

Complementary. Stratification разделяет на homogeneous bins (covariates known). CUPED работает на raw data (covariates measured). Можно совмещать.

Какой θ типичный?

0.5-0.9. Если близко к 0 — pre data не предсказывает post (CUPED не нужен). Если близко к 1 — perfect correlation (что необычно, проверьте).

Что дальше

Источники

Тренируй A/B-тесты на 612 заданиях
Реальные A/B задачи + AI разбор. Бесплатно 5 задач.
Открыть задания →