new-lvl.pro · Статьи · A/B
Статья // 13 мин чтения

CUPED простыми словами:
как ускорить A/B-тест
в 1.5–2 раза

Метод, который во всех приличных A/B-платформах включён по умолчанию. Не требует магии — только истории пользователя за месяц до теста. Разбираем, что вычитается из метрики, формулу без ужаса, реализацию в 10 строк Python и где он молча ломается.

Зачем нужно ускорять A/B-тест

Типичный продуктовый A/B длится 2–4 недели. Из них первая — пилот на маленьком трафике, потом раскатка, потом ждём, пока наберётся MDE. На сложных метриках (revenue, session_time, retention) выборка нужна большая, а поток юзеров не бесконечный — приходится сидеть и ждать.

Цена ожидания — реальная. Пока твой тест крутится:

01 / Очередь
Другие тесты ждут
A/B-платформа делит трафик. Каждый медленный тест блокирует слот для следующего.
02 / Контаминация
Растёт risk наложения
Чем дольше тест, тем выше шанс, что параллельный релиз изменит ландшафт. Чистота данных страдает.
03 / Принятие решений
Бизнес теряет темп
Месяц на тест × 12 итераций = год роста, отложенный по календарю. Скорость = деньги.

CUPED (Controlled-experiment Using Pre-Experiment Data) — способ снизить шум в данных, не трогая дизайн эксперимента. Меньше шум — раньше видно эффект — короче тест. Метод придуман в Microsoft в 2013, описан в оригинальной статье Deng et al., и сейчас включён по умолчанию в A/B-платформах Microsoft, Netflix, Booking, Авито, Яндекса.

Идея в одном предложении

Из метрики во время эксперимента вычитается её предсказуемая часть — то, что мы могли знать о юзере до теста. Остаётся «чистый сигнал»: эффект самого эксперимента + случайный шум, но без шума, который мог быть предсказан.

Аналогия. Хочешь измерить, насколько новая пробежка ускоряет бегунов. Просто замерить время после — шумно: одни от природы быстрые, другие медленные. Замерить разницу «время после − среднее время до» — точнее. Ты убрал индивидуальные различия, осталось только то, что добавила тренировка.

CUPED делает примерно то же самое, но в более общей форме: вычитает не «среднее до», а оптимально подобранную поправку, которая учитывает связь между preperiod-поведением и поведением во время эксперимента.

Без CUPED
Метрика юзера = пользовательский шум + эффект
Шум большой, потому что юзеры по природе разные. Эффект тонет в индивидуальных колебаниях. Тест долгий.
С CUPED
Метрика − предсказуемая часть = (мелкий) шум + эффект
Шум мелкий, потому что предсказуемую часть мы вычли. Эффект виден на меньшей выборке. Тест короче.

Формула без ужаса

CUPED трансформирует исходную метрику Y (поведение во время эксперимента) с помощью ковариаты X (поведение до эксперимента) в новую метрику Y_cuped:

Ycuped = Y θ · (X E[X])
Y — метрика во время эксперимента. X — preperiod-метрика. θ — коэффициент, который подбирает оптимальную поправку.

Параметр θ (тета) — это не магия, а оптимальное значение, которое максимально снижает дисперсию. Считается через ковариацию и дисперсию:

θ = Cov(Y, X) / Var(X)
Это просто коэффициент линейной регрессии Y на X. Любая библиотека статистики посчитает за миллисекунды.

Дальше — главное. Сколько шума мы вычитаем? Ответ:

Var(Ycuped) = Var(Y) · (1 − ρ²)
ρ — корреляция между Y (метрикой во время эксперимента) и X (метрикой до эксперимента).

Из этой формулы и берётся вся практическая магия CUPED. Чем сильнее метрика юзера сегодня связана с его метрикой месяц назад — тем больше шума мы вычитаем — тем меньше выборка нужна.

Сколько даёт практически

Размер выборки в A/B обратно пропорционален квадрату MDE и пропорционален дисперсии. Если дисперсия упала в 1 − ρ² раз — выборка пропорционально уменьшается. Цифры в реальных задачах:

Корреляция ρ Снижение дисперсии Экономия выборки Где встречается
0.3 9% ~9% слабо коррелированные клики
0.5 25% ~25% session_duration коротких сессий
0.7 49% ~50% revenue, время в продукте
0.85 72% ~70% стабильные daily-метрики у активных юзеров
// Запомни одну цифру
На большинстве «нормальных» продуктовых метрик (revenue, время, заказы) корреляция между preperiod и экспериментом — 0.5–0.8. Это даёт реальную экономию выборки в 25–60%. Условно: тест, который шёл бы 4 недели, заканчивается за 2.

Когда CUPED работает, а когда нет

CUPED — не серебряная пуля. Эффективность определяется тем, насколько preperiod-поведение юзера предсказывает его поведение во время эксперимента. Если предсказывает плохо — выгоды нет.

Работает хорошо

Метрики с автокорреляцией
Revenue, ARPU, время в продукте, заказы/мес.
Юзер, который много тратил месяц назад, скорее всего много потратит и в этом. Связь между периодами сильная.
Активная аудитория с историей
Юзеры с поведением ≥ 30 дней
Чем длиннее история — тем точнее ковариата. Минимум — preperiod 14–30 дней. Меньше — слишком шумно.

Работает слабо или не работает

Новые юзеры
У них нет preperiod
Если эксперимент на новых регистрациях — CUPED применить просто не к чему. Решение: либо взять прокси (трафик-канал, гео), либо стратифицировать.
Бинарные клики со слабой связью
CTR на новый элемент
Если элемент в продукте только что появился — у юзера нет preperiod-CTR на нём. Корреляция близка к нулю — эффект минимальный.
Ratio-метрики
Конверсия, retention rate, средний чек
CUPED применять напрямую к ratio некорректно. Нужна delta-method поверх — иначе оценка дисперсии получится неверной.
Метрики с резким изменением природы
После крупного релиза
Если за preperiod продукт сильно поменялся, связь metrics-then vs metrics-now ослабла. CUPED сработает, но эффект будет меньше ожидаемого.

Реализация на Python в 10 строк

Никаких специальных библиотек — достаточно numpy и pandas. Ключевая функция:

cuped.py
import numpy as np
import pandas as pd

def apply_cuped(df: pd.DataFrame, pre_col: str, exp_col: str) -> np.ndarray:
    """
    Y_cuped = Y - theta * (X - mean(X))
    Theta считается на ОБЪЕДИНЁННОЙ группе (control + treatment),
    иначе появляется bias.
    """
    pre = df[pre_col].values
    exp = df[exp_col].values
    theta = np.cov(exp, pre, ddof=1)[0, 1] / np.var(pre, ddof=1)
    return exp - theta * (pre - pre.mean())

Использование в обычном пайплайне A/B:

analyze_ab.py
# df: одна строка на юзера, колонки: group ('control'/'treatment'),
#     revenue_pre (за 30 дней до теста), revenue_exp (за период теста)
df['revenue_cuped'] = apply_cuped(df, 'revenue_pre', 'revenue_exp')

# Дальше — обычный двухвыборочный t-тест на cuped-метрике
from scipy import stats

control = df[df.group == 'control']['revenue_cuped']
treatment = df[df.group == 'treatment']['revenue_cuped']

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

print(f"Эффект: {effect:.2f}, p-value: {p_value:.4f}")

# Сравнение: дисперсия до и после CUPED
ratio = df['revenue_cuped'].var() / df['revenue_exp'].var()
print(f"Дисперсия после CUPED: {ratio:.2%} от исходной")

Если на твоих данных ratio получился около 0.5 — CUPED дал двукратное ускорение. Если около 0.9 — эффект минимальный, корреляция между периодами слабая.

// Тонкий момент
θ считается на объединённой выборке (control + treatment), не отдельно по группам. Если посчитать только на контроле — в θ попадёт случайный шум контрольной группы и оценка эффекта получит смещение. Если на объединённой — смещения нет, потому что preperiod-метрика к моменту назначения групп уже зафиксирована.

CUPED vs стратификация

Стратификация — другой способ снизить шум в A/B: разбиваешь юзеров на группы по дискретным признакам (платформа, гео, тариф), считаешь эффект внутри каждой страты и потом усредняешь. На собесах часто спрашивают, чем отличается от CUPED.

CUPED Стратификация
Тип ковариаты Непрерывная (preperiod-метрика) Дискретная (платформа, гео, тариф)
Что использует Поведение юзера до эксперимента Признак, известный на момент сплита
Снижение дисперсии 1 − ρ² (зависит от корреляции) Разница between-strata variance / total
Работает на новых юзерах? Нет (нет preperiod) Да (платформа известна)
Сложность реализации Низкая (10 строк) Средняя (правильное взвешивание)
Можно ли комбинировать Да — CUPED поверх страт даёт ещё больший выигрыш

Практика: на «зрелой» аудитории с историей — CUPED обычно даёт больше. На новых юзерах или когда метрика бинарная и слабо автокоррелирована — стратификация. На больших платформах применяют оба метода одновременно.

Преподготовь данные для CUPED в SQL
90% работы с CUPED — это собрать preperiod и экспериментальную метрику в одну таблицу. В SQL-тренажёре есть задачи на оконные функции и периоды — те самые, что нужны для подготовки данных под CUPED.
Открыть тренажёр

Где CUPED тихо ломается

Метод выглядит безобидным — добавил поправку, дисперсия упала, тест ускорился. Но есть четыре ситуации, когда CUPED даёт неверные результаты, не выдавая ошибки. Их проверяй явно перед запуском.

// грабли 01
θ посчитана на одной группе
Если theta считаешь только на контроле или только на treatment — в неё попадает случайный шум одной выборки. Эффект эксперимента получает смещение. На больших данных оно мало, на маленьких — может развернуть направление.
всегда считай theta на объединённой группе (control + treatment вместе). Преперриод от теста не зависит — bias не появится.
// грабли 02
Контаминация preperiod-окна
Preperiod должен быть строго до начала эксперимента. Если в нём оказался период, когда часть юзеров уже видела treatment (например, из-за пилотного запуска или утечки), ковариата перестаёт быть «чистой» — она уже отражает эффект.
сделай зазор: preperiod заканчивается за 1–2 дня до старта эксперимента. Проверь, не было ли в этом окне раскаток.
// грабли 03
Применение к ratio-метрике в лоб
Конверсия = заказы / визиты. Если применять CUPED к каждому юзеру как «его конверсия = заказ_у/визит_у», получаешь некорректную дисперсию: конверсия — это отношение, и её разброс считается через delta-method, а не через обычный t-тест. CUPED можно применять, но к числителю и знаменателю отдельно, с пересчётом дисперсии на финале.
для ratio — либо delta-method (правильный путь), либо линеаризованная метрика (Yandex-подход): перейди от ratio к линеаризованной форме, и CUPED применится корректно.
// грабли 04
Слепое применение к новым юзерам
У новых юзеров preperiod-метрика = 0 для всех. theta * 0 = 0 — никакой поправки. CUPED технически отработает, но никакого ускорения не даст. На новых юзерах ты «думаешь, что ускорился», но шум остался прежним.
сегментируй заранее: на старых юзерах — CUPED, на новых — стратификация по каналу/гео или просто без поправки. Анализируй сегменты раздельно или с правильным взвешиванием.

Как валидировать: A/A на CUPED

Прежде чем доверять CUPED-оценкам в продакшене, прогони на исторических данных офлайновый A/A-тест. Случайно разбей юзеров на две группы (никакого реального воздействия), посчитай CUPED-эффект, повтори N раз. Если процент «значимых» результатов на уровне α=0.05 не равен 5% — где-то баг.

aa_validation.py
import numpy as np
from scipy import stats

def aa_test(df, n_sim=1000):
    significant_count = 0
    for _ in range(n_sim):
        # случайный сплит 50/50
        df['group'] = np.random.choice(['A', 'B'], size=len(df))
        df['cuped'] = apply_cuped(df, 'pre', 'exp')
        a = df[df.group == 'A']['cuped']
        b = df[df.group == 'B']['cuped']
        _, p = stats.ttest_ind(a, b, equal_var=False)
        if p < 0.05:
            significant_count += 1
    return significant_count / n_sim

# Должно быть в окрестности 0.05 (5%)
print(aa_test(historical_data))

Если получаешь 7–10% — значит, в трансформации или в сборе данных есть систематическое смещение. Если 2–3% — где-то лишний консерватизм, оценка дисперсии завышена. Идеально — 4–6%.

// Больше про A/A
Подробно про валидацию платформы экспериментов через A/A-тесты — в отдельной статье «A/A-тесты: зачем нужны и что делать, если падает значимость» (готовится к выходу).

CUPED в кейс-интервью

На middle/senior-собесах в продуктовой аналитике CUPED — практически обязательный пункт. Его не всегда проверяют формулой, но обязательно — пониманием идеи и границ применения.

🎤 Что могут спросить
// Шаблон ответа
Структура: (1) идея в одном предложении (вычитаем предсказуемую часть метрики) — (2) формула Y_cuped = Y − θ(X − E[X]), что значат буквы — (3) сколько даёт практически (зависит от ρ, на нормальных продуктовых метриках 25–60% экономии) — (4) где не работает (новые юзеры, ratio в лоб). Этого достаточно на 95% собесов.

Частые вопросы

Что такое CUPED простыми словами?
CUPED — метод снижения шума в A/B-тесте за счёт информации о пользователе до начала эксперимента. Из метрики во время эксперимента вычитается её предсказуемая часть, остаётся только эффект самого теста. Это уменьшает разброс данных, и тот же эффект становится виден на меньшей выборке — обычно за 1.5–2 раза меньшее время.
Когда CUPED не работает?
Когда у пользователя нет истории до эксперимента (новые пользователи), когда метрика бинарная и плохо коррелирует сама с собой между периодами (клик на новый баннер), и когда метрика — это ratio (например, конверсия). Для последнего случая нужны другие методы — delta-method или линеаризация.
CUPED увеличивает чувствительность или сокращает время теста?
И то и другое — это две стороны одной медали. На фиксированной выборке CUPED ловит более мелкие эффекты. На фиксированном MDE — позволяет провести тест быстрее. Конкретно сколько — зависит от корреляции метрики между preperiod и экспериментом: ρ=0.7 даёт примерно 50% экономии, ρ=0.5 — около 25%, ρ<0.3 — почти ничего.
Чем CUPED отличается от стратификации?
Стратификация работает с дискретными признаками (платформа, гео, тариф) и снижает шум за счёт разбиения на однородные группы. CUPED работает с непрерывной ковариатой (preperiod-метрика) и снижает шум линейной поправкой. Они не взаимоисключающие — комбинация даёт ещё больший эффект.
Почему θ нужно считать на объединённой группе?
Если θ считать только на контроле, в неё попадает шум контрольной группы как отдельной выборки — и при оценке эффекта появляется смещение. Если на объединённой (control + treatment) — смещения нет, потому что preperiod-метрика к моменту назначения групп уже зафиксирована и не зависит от теста. Это математически доказано в оригинальной статье Deng et al.
Какой preperiod выбирать — 7 / 14 / 30 дней?
Для большинства продуктовых метрик — 14–30 дней. Меньше — слишком шумная ковариата, корреляция упадёт. Больше — велик риск, что пользователь успел измениться, поведение «давно» не похоже на «сейчас». Если активность сильно сезонная (выходные/будни) — обязательно бери целое число недель.

Связанные материалы

Главное про CUPED

CUPED — это бесплатное ускорение A/B-теста: ничего не меняешь в дизайне эксперимента, добавляешь 10 строк кода — и тест завершается на 25–60% быстрее. Цена входа минимальная, выгода реальная.

Запомни три ограничения: (1) метрика должна автокоррелировать с собой между периодами (revenue — да, бинарные клики на новый элемент — нет), (2) у юзера должна быть история (новые юзеры — мимо), (3) для ratio-метрик — отдельная техника (delta-method или линеаризация).

Следующий шаг: возьми любой свой прошлый A/B-тест с revenue-метрикой, рассчитай CUPED-версию и сравни ширину доверительного интервала. Обычно — впечатляет. И сразу понятно, почему методу 12 лет, а его всё ещё активно внедряют.

АТ
Андрей Тарасенко
// Продуктовый аналитик · Авито · Ментор

CUPED — первый метод, который я показываю, когда менти жалуется на «слишком долгий A/B». 10 строк кода, бесплатные 30-50% к скорости. На собесах в BigTech про него спрашивают всегда — заведи в голове четыре пункта (идея, формула, ρ→экономия, ограничения), и любой вопрос разложится.

Написать в Telegram