CUPED: как снизить дисперсию и ускорить A/B-тесты
Зачем снижать дисперсию в A/B-тестах
Чем выше дисперсия метрики, тем дольше нужно ждать результатов A/B-теста. Два способа ускорить эксперимент: увеличить трафик или снизить шум в данных. Трафик часто ограничен, а вот шум можно убрать математически.
CUPED (Controlled-experiment Using Pre-Experiment Data) — метод, предложенный командой Microsoft в 2013 году (Deng, Xu, Kohavi, Walker). Идея: использовать данные о поведении пользователей до эксперимента, чтобы вычесть предсказуемую часть вариации из метрики эксперимента. Результат — та же точность при меньшем размере выборки или более точный результат при том же размере.
На практике CUPED сокращает необходимую длительность теста на 20–50%. В крупных компаниях (Яндекс, Booking, Netflix, Microsoft) это стандартный шаг пайплайна экспериментов.
Математическая интуиция
Суть CUPED — классическая техника из статистики: reduction of variance через ковариату.
Пусть Y — метрика эксперимента (например, revenue per user за время теста), а X — та же метрика за период до эксперимента (pre-period). Мы строим скорректированную метрику:
Y_cuped = Y - theta * (X - E[X])где theta — коэффициент, минимизирующий дисперсию Y_cuped:
theta = Cov(Y, X) / Var(X)Это коэффициент линейной регрессии Y на X. Вычитая theta * (X - E[X]), мы убираем из Y ту часть вариации, которая объясняется доэкспериментальным поведением.
Почему E[X] вычитается: при случайной рандомизации E[X] одинаково в контроле и тесте, поэтому вычитание theta * (X - E[X]) не смещает оценку среднего эффекта. Мы убираем шум, но не сигнал.
Дисперсия после коррекции:
Var(Y_cuped) = Var(Y) * (1 - corr(Y, X)^2)Если корреляция между Y и X равна 0.7, дисперсия снижается на 49%. Если 0.9 — на 81%. Чем сильнее коррелированы метрика до и во время эксперимента, тем больше выигрыш.
Пошаговый алгоритм применения CUPED
Определить метрику эксперимента
Y— то, что вы хотите сравнить между тестом и контролем (конверсия, revenue, количество сессий и т.д.).Выбрать ковариату
X— значение той же (или связанной) метрики за период до эксперимента, за сопоставимое временное окно.Рассчитать
thetaпо всей выборке (test + control):
theta = Cov(Y, X) / Var(X)- Вычислить скорректированную метрику для каждого пользователя:
Y_cuped_i = Y_i - theta * (X_i - mean(X))Провести стандартный статистический тест (t-test, z-test) на
Y_cupedвместоY.Интерпретировать результат. Разница средних
Y_cupedмежду группами равна разнице среднихY, но доверительный интервал уже.
Как выбирать ковариаты
Выбор ковариаты — ключевой момент. Плохая ковариата не поможет, а в редких случаях даже увеличит дисперсию.
Главное правило: ковариата должна быть измерена до начала эксперимента и не зависеть от назначения в группу. Это гарантирует отсутствие смещения.
Что хорошо работает:
- Та же метрика за pre-period. Revenue за 2 недели до теста как ковариата для revenue во время теста — почти всегда лучший выбор.
- Количество визитов/сессий/заказов за pre-period.
- Средний чек до эксперимента для метрики среднего чека во время.
Длина pre-period: обычно берут окно, равное длительности эксперимента. Если тест идёт 14 дней, ковариату считают за 14 дней до старта. Слишком короткое окно — мало данных, слишком длинное — ковариата «размывается» и корреляция падает.
Множественные ковариаты: можно использовать несколько. В этом случае CUPED обобщается до многомерной регрессии:
Y_cuped = Y - X_vec * theta_vecгде theta_vec — вектор коэффициентов из OLS-регрессии Y ~ X1 + X2 + ... + Xk. На практике 1–2 ковариаты дают основной эффект, добавление третьей редко существенно помогает.
Новые пользователи: для тех, кто пришёл уже во время эксперимента, pre-period данных нет. Стандартный подход — присвоить им X = E[X] (среднее). Тогда коррекция для них равна нулю, и они анализируются как без CUPED. Альтернатива — анализировать новых и существующих пользователей отдельно.
Python-пример с реальными числами
Сгенерируем данные и покажем, как CUPED сужает доверительный интервал.
import numpy as np
from scipy import stats
np.random.seed(42)
n_control = 5000
n_treatment = 5000
# Базовая «склонность» пользователя к покупкам
user_baseline_control = np.random.normal(50, 20, n_control)
user_baseline_treatment = np.random.normal(50, 20, n_treatment)
# Pre-period метрика (revenue до эксперимента)
X_control = user_baseline_control + np.random.normal(0, 10, n_control)
X_treatment = user_baseline_treatment + np.random.normal(0, 10, n_treatment)
# Метрика эксперимента: treatment даёт +2 к revenue
noise_control = np.random.normal(0, 10, n_control)
noise_treatment = np.random.normal(0, 10, n_treatment)
Y_control = user_baseline_control + noise_control
Y_treatment = user_baseline_treatment + 2.0 + noise_treatment # эффект = +2
# --- Обычный t-test ---
t_stat, p_value = stats.ttest_ind(Y_treatment, Y_control)
diff_raw = Y_treatment.mean() - Y_control.mean()
se_raw = np.sqrt(Y_control.var()/n_control + Y_treatment.var()/n_treatment)
print(f"Без CUPED:")
print(f" Разница средних: {diff_raw:.3f}")
print(f" SE: {se_raw:.3f}")
print(f" 95% CI: [{diff_raw - 1.96*se_raw:.3f}, {diff_raw + 1.96*se_raw:.3f}]")
print(f" p-value: {p_value:.4f}")
# --- CUPED ---
X_all = np.concatenate([X_control, X_treatment])
Y_all = np.concatenate([Y_control, Y_treatment])
theta = np.cov(Y_all, X_all)[0, 1] / np.var(X_all)
X_mean = X_all.mean()
Y_cuped_control = Y_control - theta * (X_control - X_mean)
Y_cuped_treatment = Y_treatment - theta * (X_treatment - X_mean)
t_stat_cuped, p_value_cuped = stats.ttest_ind(Y_cuped_treatment, Y_cuped_control)
diff_cuped = Y_cuped_treatment.mean() - Y_cuped_control.mean()
se_cuped = np.sqrt(
Y_cuped_control.var()/n_control + Y_cuped_treatment.var()/n_treatment
)
print(f"\nС CUPED:")
print(f" theta: {theta:.3f}")
print(f" Разница средних: {diff_cuped:.3f}")
print(f" SE: {se_cuped:.3f}")
print(f" 95% CI: [{diff_cuped - 1.96*se_cuped:.3f}, {diff_cuped + 1.96*se_cuped:.3f}]")
print(f" p-value: {p_value_cuped:.4f}")
reduction = 1 - (se_cuped / se_raw)
print(f"\nСнижение SE: {reduction:.1%}")
corr = np.corrcoef(Y_all, X_all)[0, 1]
print(f"Корреляция Y и X: {corr:.3f}")
print(f"Теоретическое снижение дисперсии: {corr**2:.1%}")Вывод (seed=42):
Без CUPED:
Разница средних: 1.808
SE: 0.452
95% CI: [0.922, 2.695]
p-value: 0.0001
С CUPED:
theta: 0.819
Разница средних: 2.013
SE: 0.266
95% CI: [1.492, 2.534]
p-value: 0.0000
Снижение SE: 41.2%
Корреляция Y и X: 0.808
Теоретическое снижение дисперсии: 65.3%Доверительный интервал сузился на 41% — с ширины 1.77 до 1.04. Разница средних осталась близкой к заданному эффекту +2 — CUPED не вносит смещения.
SQL: подготовка данных для CUPED
На практике CUPED начинается с подготовки таблицы «пользователь — ковариата — метрика — группа». Вот пример для e-commerce.
WITH experiment_users AS (
SELECT
user_id,
experiment_group -- 'control' / 'treatment'
FROM experiment_assignments
WHERE experiment_id = 'checkout_redesign_v2'
),
-- Метрика эксперимента: revenue за время теста
experiment_metric AS (
SELECT
user_id,
COALESCE(SUM(revenue), 0) AS y_revenue
FROM orders
WHERE order_date BETWEEN '2026-03-01' AND '2026-03-14'
GROUP BY user_id
),
-- Ковариата: revenue за pre-period (те же 14 дней до теста)
pre_period_metric AS (
SELECT
user_id,
COALESCE(SUM(revenue), 0) AS x_revenue
FROM orders
WHERE order_date BETWEEN '2026-02-15' AND '2026-02-28'
GROUP BY user_id
),
-- Собираем всё вместе
user_data AS (
SELECT
eu.user_id,
eu.experiment_group,
COALESCE(em.y_revenue, 0) AS y,
COALESCE(pm.x_revenue, 0) AS x
FROM experiment_users eu
LEFT JOIN experiment_metric em USING (user_id)
LEFT JOIN pre_period_metric pm USING (user_id)
),
-- Считаем theta и среднее X
cuped_params AS (
SELECT
-- Cov(Y, X) / Var(X)
(AVG(y * x) - AVG(y) * AVG(x))
/ NULLIF(AVG(x * x) - AVG(x) * AVG(x), 0) AS theta,
AVG(x) AS x_mean
FROM user_data
)
-- Финальная таблица с Y_cuped
SELECT
ud.user_id,
ud.experiment_group,
ud.y,
ud.x,
ud.y - cp.theta * (ud.x - cp.x_mean) AS y_cuped
FROM user_data ud
CROSS JOIN cuped_params cp;После этого группируете по experiment_group, считаете среднее y_cuped и стандартную ошибку — и проводите обычный тест.
Агрегированные статистики в SQL:
WITH cuped_data AS (
-- ... запрос выше ...
)
SELECT
experiment_group,
COUNT(*) AS n,
AVG(y_cuped) AS mean_y_cuped,
STDDEV(y_cuped) AS std_y_cuped,
STDDEV(y_cuped) / SQRT(COUNT(*)) AS se_y_cuped
FROM cuped_data
GROUP BY experiment_group;Когда CUPED не помогает
CUPED — не универсальное решение. Есть ситуации, где метод бесполезен или неприменим.
Нет pre-period данных. Если все пользователи эксперимента — новые (например, A/B-тест на лендинге для привлечения), ковариату взять неоткуда. CUPED не применим.
Низкая корреляция. Если corr(Y, X) < 0.3, снижение дисперсии составит менее 9% — выигрыш минимален и не оправдывает усложнение пайплайна.
Метрика сильно меняется со временем. Если бизнес-сезонность или внешние факторы между pre-period и экспериментом радикально отличаются (Чёрная пятница vs обычная неделя), корреляция будет низкой.
Ratio-метрики. Для метрик вида «конверсия = заказы / визиты» CUPED требует аккуратного применения. Некорректно считать CUPED по конверсии напрямую — нужно работать с числителем и знаменателем через линеаризацию (delta method + CUPED).
Нарушение рандомизации. CUPED предполагает, что назначение в группу не зависит от ковариаты. Если рандомизация некорректна (например, в одну группу попали более активные пользователи), CUPED не спасёт — он уберёт шум, но не систематическое смещение.
Сравнение с другими методами снижения дисперсии
| Метод | Суть | Снижение дисперсии | Сложность | Ограничения |
|---|---|---|---|---|
| CUPED | Вычитание ковариатной коррекции | 20–80% (зависит от корреляции) | Средняя | Нужны pre-period данные |
| Стратификация | Разбиение на страты, взвешенная оценка | 10–30% | Низкая | Ограничено числом страт |
| CUPAC | Regression adjustment с ML-моделью | 30–90% | Высокая | Риск переобучения |
| Обрезка выбросов (winsorization) | Обрезка экстремальных значений | 5–40% | Низкая | Теряем информацию о хвостах |
| Linearization (delta method) | Линеаризация ratio-метрик | Зависит от метрики | Средняя | Только для ratio-метрик |
CUPED vs стратификация. Стратификация (post-stratification) разбивает пользователей на группы по характеристикам (платформа, страна, сегмент) и считает взвешенную оценку эффекта. Она проще в реализации, но снижение дисперсии обычно скромнее. CUPED и стратификацию можно комбинировать: сначала стратифицировать, потом применить CUPED внутри каждой страты.
CUPED vs CUPAC. CUPAC (Controlled-experiment Using Predictions As Covariates) — обобщение CUPED, где вместо сырых pre-period метрик используют предсказание ML-модели. Модель обучается на pre-period данных предсказывать метрику эксперимента. Предсказание — более сильная ковариата, чем сырое значение, потому что модель может учесть нелинейности и взаимодействия. Но есть риск утечки данных при неаккуратном построении модели.
На собеседовании спрашивают
"Объясните принцип работы CUPED простыми словами."
Пользователи изначально разные: кто-то покупает на 100 рублей в неделю, кто-то на 10 000. Эта разница создаёт шум, который мешает увидеть эффект эксперимента. CUPED использует данные о поведении до эксперимента, чтобы «вычесть» эту предсказуемую разницу. Мы не меняем метрику — мы убираем шум.
"Почему CUPED не вносит смещение?"
Потому что ковариата измерена до эксперимента и не зависит от назначения в группу. При случайной рандомизации среднее ковариаты одинаково в тесте и контроле, поэтому вычитание theta * (X - E[X]) не сдвигает оценку разницы между группами.
"Как выбрать ковариату?"
Лучшая ковариата — та же метрика за аналогичный период до эксперимента. Длина pre-period обычно равна длительности теста. Если метрика — revenue за 14 дней теста, ковариата — revenue за 14 дней до теста. Можно попробовать несколько ковариат и выбрать ту, что даёт максимальное снижение дисперсии (по корреляции с Y).
"Что делать с новыми пользователями в CUPED?"
Стандартный подход — подставить среднее значение ковариаты. Тогда коррекция для новых пользователей равна нулю, и они анализируются как обычно. Альтернатива — исключить их из CUPED-анализа и проанализировать отдельно.
"Можно ли применять CUPED к конверсии?"
Напрямую — нет. Конверсия — ratio-метрика, и для неё CUPED в чистом виде некорректен. Нужно сначала линеаризовать метрику через delta method, а потом применять CUPED к линеаризованной версии. Либо применять CUPED к числителю (количество целевых действий), если знаменатель (количество пользователей) фиксирован по дизайну эксперимента.
"В чём разница между CUPED и просто добавлением ковариаты в регрессию?"
По сути — то же самое. CUPED формально эквивалентен OLS-регрессии Y ~ treatment + X. Но CUPED удобнее в пайплайне: он отделяет этап «коррекция метрики» от этапа «статистический тест», что позволяет использовать стандартную инфраструктуру тестирования.
"Когда CUPED бесполезен?"
Когда корреляция ковариаты с метрикой эксперимента низкая (< 0.3). Это бывает, если метрика нестабильна, pre-period слишком далёк от эксперимента, или все пользователи — новые и pre-period данных нет.
Итого
CUPED — стандартный инструмент для ускорения A/B-тестов в продуктовой аналитике. Математика простая (по сути — линейная регрессия), реализация — десяток строк на Python или SQL. Главное — правильно выбрать ковариату и помнить об ограничениях: нужны pre-period данные, нужна достаточная корреляция, для ratio-метрик требуется линеаризация.
На собеседовании важно показать не просто знание формулы, а понимание: почему метод работает, когда не работает, и как вписывается в общий пайплайн экспериментов.
Читайте также
- P-value простыми словами
- Доверительный интервал простыми словами
- Guardrail-метрики в A/B-тестах
- Проверка гипотез
FAQ
Что такое CUPED в A/B-тестах?
CUPED (Controlled-experiment Using Pre-Experiment Data) — метод снижения дисперсии, который использует данные о поведении пользователей до эксперимента, чтобы убрать предсказуемую часть шума из метрики. Это позволяет получить более точный результат A/B-теста при том же размере выборки или сократить длительность эксперимента на 20-50%.
Как CUPED снижает дисперсию?
CUPED вычитает из метрики эксперимента ковариатную коррекцию на основе pre-period данных. Снижение дисперсии зависит от корреляции между ковариатой и метрикой: при корреляции 0.7 дисперсия снижается на 49%, при 0.9 — на 81%.
Как выбрать ковариату для CUPED?
Лучшая ковариата — та же метрика за аналогичный период до эксперимента. Если тест идёт 14 дней, берите revenue за 14 дней до старта. Ковариата должна быть измерена строго до начала эксперимента и не зависеть от назначения в группу.
Когда CUPED не помогает?
CUPED бесполезен, когда нет pre-period данных (все пользователи новые), когда корреляция ковариаты с метрикой ниже 0.3 или когда между pre-period и экспериментом радикально меняются условия (например, сезонность). Для ratio-метрик вроде конверсии нужна предварительная линеаризация.
Потренируйтесь решать задачи по A/B-тестам и статистике в Карьернике — тренажёре для подготовки к собеседованиям аналитиков.