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

  1. Определить метрику эксперимента Y — то, что вы хотите сравнить между тестом и контролем (конверсия, revenue, количество сессий и т.д.).

  2. Выбрать ковариату X — значение той же (или связанной) метрики за период до эксперимента, за сопоставимое временное окно.

  3. Рассчитать theta по всей выборке (test + control):

theta = Cov(Y, X) / Var(X)
  1. Вычислить скорректированную метрику для каждого пользователя:
Y_cuped_i = Y_i - theta * (X_i - mean(X))
  1. Провести стандартный статистический тест (t-test, z-test) на Y_cuped вместо Y.

  2. Интерпретировать результат. Разница средних 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-метрик требуется линеаризация.

На собеседовании важно показать не просто знание формулы, а понимание: почему метод работает, когда не работает, и как вписывается в общий пайплайн экспериментов.

Читайте также

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-тестам и статистике в Карьернике — тренажёре для подготовки к собеседованиям аналитиков.