CUPED (Controlled-experiment Using Pre-Existing Data) — это способ ускорить A/B-тест, не трогая ни трафик, ни продукт. Вы берёте данные о пользователе за период до эксперимента, вычитаете их влияние из целевой метрики и за счёт этого убираете часть «шума». Метрика становится менее разбросанной, доверительный интервал сжимается, и тот же эффект вы видите на меньшей выборке или за меньшее число дней. На практике это часто означает сокращение нужной аудитории на 30-50% без потери мощности. Дальше разберу, почему это работает, покажу формулу словами и дам рабочий код на Python.
Почему дисперсия решает исход A/B-теста?
Когда мы сравниваем контроль и тест, мы смотрим не на голую разницу средних, а на разницу относительно её же случайного разброса. Формула нужного размера выборки на одну группу выглядит так (словами): выборка растёт пропорционально дисперсии метрики и обратно пропорционально квадрату эффекта, который вы хотите поймать. То есть n ~ variance / effect^2.
Эффект вы менять не можете — его определяет продукт. А вот дисперсию — можете. Если снизить variance вдвое, нужная выборка тоже падает примерно вдвое. Это и есть вся идея вариационных методов: не увеличивать числитель (трафик), а уменьшать знаменатель шума.
Проблема продуктовых метрик в том, что они дико разбросаны. Выручка на пользователя, число заказов, время в приложении — у них тяжёлые хвосты: большинство приносит копейки, а пара китов тянет среднее на себя. Из-за этого дисперсия огромная, и чтобы поймать прирост в 1-2%, приходится копить недели трафика. Если вы хотите потренироваться считать такие метрики руками, загляните в каталог метрик — там формулы и типичные ловушки.
Что такое CUPED простыми словами?
Представьте, что вы измеряете выручку за две недели теста. Пользователь, который и до эксперимента тратил много, скорее всего потратит много и сейчас — независимо от того, в какую группу он попал. Эта «предсказуемая» часть выручки не несёт информации о вашем изменении, но раздувает дисперсию.
CUPED говорит: давайте возьмём поведение пользователя ДО эксперимента (назовём это ковариатой) и вычтем из метрики ту часть, которую эта ковариата объясняет. Останется «очищенный» остаток — та вариация, которую предпериод не смог предсказать. Именно в этом остатке и живёт эффект вашего теста, а лишний шум уходит.
Ключевое условие: ковариата берётся из прошлого. Она физически не могла зависеть от вашего изменения, потому что изменения тогда ещё не было. Поэтому среднее ковариаты в контроле и тесте одинаково (рандомизация это гарантирует), и, вычитая её, вы не сдвигаете оценку эффекта — только чистите дисперсию. Это делает CUPED несмещённым: правильный ответ остаётся тем же, просто вы находите его быстрее.
Как ковариата снижает дисперсию?
Механизм проще, чем кажется. Вы строите линейную связь между целевой метрикой Y (например, выручка во время теста) и ковариатой X (выручка до теста). Часть Y, которую можно предсказать по X, вы отрезаете, оставляя остаток.
Насколько сильно упадёт дисперсия — зависит ровно от одного числа: от корреляции между Y и X. Соотношение такое (словами): дисперсия скорректированной метрики равна исходной дисперсии, умноженной на (1 − корреляция в квадрате).
Отсюда простое правило:
- корреляция 0.3 → дисперсия падает на 9% (толку почти нет);
- корреляция 0.5 → падение на 25%;
- корреляция 0.7 → падение на 49% (выборку можно почти вдвое ужать);
- корреляция 0.9 → падение на 81% (мечта, но редкость).
Вот почему лучшая ковариата — это обычно та же самая метрика за предыдущий период. Выручка сейчас сильнее всего коррелирует с выручкой раньше, а не с полом, городом или устройством. Корреляция «метрика с собой из прошлого» у стабильных пользователей легко доходит до 0.6-0.8.
Формула CUPED словами
Разложу без единого доллара и латеха, обычным текстом.
- Считаем коэффициент
theta: этоковариация(Y, X), делённая надисперсию(X). По сути это наклон прямой, которой мы приближаемYчерезX— тот же коэффициент, что даёт обычная линейная регрессияYнаX.
- Строим скорректированную метрику для каждого пользователя:
Y_cuped = Y − theta * (X − среднее(X)).
- Дальше делаем ровно тот же t-тест или бутстрап, но уже по
Y_cupedвместоY.
Почему из X вычитается его среднее? Чтобы не сдвинуть общее среднее метрики. В точке X = среднее(X) поправка равна нулю, поэтому средняя выручка по всей выборке остаётся прежней — двигается только разброс вокруг неё. theta при этом считается один раз на объединённых данных обеих групп, а не отдельно в контроле и тесте, иначе можно занести смещение.
Если формулы через регрессию для вас пока тёмный лес — начните с базы SQL и агрегаций в бесплатном курсе, а статистику удобно закреплять на живых Python-задачах.
Как выбрать ковариату для CUPED?
Хорошая ковариата обязана удовлетворять трём условиям.
Во-первых, она должна быть измерена до старта эксперимента. Любое поведение во время теста уже заражено эффектом — если взять его в ковариату, оценка поедет. Это ошибка номер один.
Во-вторых, она должна коррелировать с целевой метрикой. Нет корреляции — нет снижения дисперсии, CUPED просто ничего не сделает.
В-третьих, она должна быть определена для большинства пользователей. Если половина аудитории — новички без истории, их ковариата пустая, и вы либо теряете этих людей, либо заполняете нулями (что режет корреляцию).
Практический чек-лист выбора:
- Метрика-цель: выручка → ковариата: выручка за 2-4 недели до теста.
- Метрика-цель: число заказов → ковариата: число заказов в предпериоде.
- Метрика-цель: активность (сессии, DAU) → ковариата: те же сессии до старта.
Для чистых новичков, у которых истории нет, CUPED слабее — им нечего вычитать. Тут иногда берут прокси: канал привлечения, стартовый сегмент, первую сессию. Но чем дальше ковариата от самой метрики, тем ниже корреляция и тем меньше выигрыш.
Пример CUPED на Python
Соберём синтетический пример, где предпериод честно коррелирует с метрикой теста, и посмотрим, как схлопывается дисперсия.
import numpy as np
import pandas as pd
from scipy import stats
rng = np.random.default_rng(42)
n = 20000
# ковариата: выручка до эксперимента
x_pre = rng.gamma(shape=2.0, scale=50, size=n)
# метрика теста: сильно завязана на предпериод + свой шум
noise = rng.normal(0, 40, size=n)
y = 0.8 * x_pre + noise
# случайное разбиение и небольшой истинный эффект +3 в тесте
group = rng.integers(0, 2, size=n) # 0 = control, 1 = treatment
y = y + group * 3.0
df = pd.DataFrame({"group": group, "y": y, "x_pre": x_pre})
Теперь сам CUPED. Считаем theta на всех данных сразу и корректируем метрику.
def apply_cuped(df, metric="y", covariate="x_pre"):
x = df[covariate].to_numpy()
y = df[metric].to_numpy()
theta = np.cov(y, x, ddof=1)[0, 1] / np.var(x, ddof=1)
y_cuped = y - theta * (x - x.mean())
return y_cuped, theta
df["y_cuped"], theta = apply_cuped(df)
print("theta:", round(theta, 3))
corr = np.corrcoef(df["y"], df["x_pre"])[0, 1]
print("corr(Y, X):", round(corr, 3))
print("var reduction:", round(1 - df["y_cuped"].var() / df["y"].var(), 3))
Сравним t-тест по сырой метрике и по скорректированной.
def welch(df, col):
a = df.loc[df.group == 0, col]
b = df.loc[df.group == 1, col]
t, p = stats.ttest_ind(b, a, equal_var=False)
return b.mean() - a.mean(), p
print("raw :", welch(df, "y"))
print("cuped:", welch(df, "y_cuped"))
Разница средних в обоих случаях будет около одного и того же истинного эффекта (примерно +3) — CUPED не врёт про величину. Но p-value по y_cuped заметно меньше, а дисперсия падает примерно на квадрат корреляции. При корреляции около 0.7 вы увидите снижение дисперсии близко к 50%. Прогоните этот код у себя в Python-тренажёре и поменяйте коэффициент 0.8 на 0.3 — сразу почувствуете, как слабая ковариата убивает весь выигрыш. Синтаксис pandas и numpy под рукой в Python-справочнике.
На сколько CUPED реально сокращает выборку?
Здесь красивая математика: раз выборка пропорциональна дисперсии, а дисперсия падает в (1 − корреляция^2) раз, то и нужная выборка падает во столько же раз.
- корреляция 0.5 → минус 25% выборки или дней теста;
- корреляция 0.7 → почти минус 50%;
- корреляция 0.8 → минус ~64%.
Что это значит в жизни. Если тест на исходной метрике набирал мощность за 4 недели, то с ковариатой корреляции 0.7 он добьёт до той же мощности примерно за 2 недели. Для команды, которая крутит десятки экспериментов в квартал, это буквально удвоение пропускной способности A/B-платформы без единого лишнего пользователя.
Но не ждите чудес там, где корреляции нет. На метриках вроде конверсии в первую покупку у новичков предпериодных данных попросту не существует, и CUPED выродится в ноль. Реалистичная вилка для зрелых продуктовых метрик — снижение выборки на 20-50%.
Какие ошибки чаще всего ломают CUPED?
Разберу грабли, на которые наступают почти все, кто внедряет метод впервые.
Ковариата из периода теста. Самая опасная и самая частая. Если ваш X хоть частично захватывает время после старта, он заражён эффектом, и оценка получает смещение. Берите строго предпериод.
Разный theta в группах. Считайте один коэффициент на объединённой выборке. Отдельные theta для контроля и теста подгоняют под шум и заносят смещение в разницу средних.
Ковариата, на которую влиял прошлый эксперимент. Если пользователи уже сидели в другом тесте, менявшем ту же метрику, их «прошлое» не нейтрально. На пересекающихся экспериментах это отдельная головная боль.
Слепое доверие к нормальности. CUPED снижает дисперсию, но не чинит тяжёлые хвосты. По остаткам всё равно полезно гонять бутстрап, особенно на выручке. Проверить понимание таких нюансов можно на тренировочных вопросах с собеседований — вариационные методы стабильно спрашивают на позициях аналитика и DS.
Применение к метрикам отношений. Для метрик вида «сумма X / сумма Y» (ratio-метрики) наивный CUPED работает некорректно — там нужна дельта-метод-версия. Начинать лучше с простых средних на пользователя.
Когда CUPED не стоит применять?
Метод почти бесплатный, но есть ситуации, где он не окупается или вреден.
Нет истории пользователей. Продукт запущен месяц назад, ретеншн низкий, большинство в тесте — свежие когорты. Предпериода нет — вычитать нечего.
Слабая корреляция цели с любым доступным прошлым. Если самая сильная ковариата даёт корреляцию 0.2, выигрыш меньше 5%. Проще накинуть чуть трафика, чем городить пайплайн.
Метрика меняет саму природу пользователей. Если тест сдвигает состав аудитории (например, резко меняет привлечение), связь предпериода с настоящим рвётся, и допущения CUPED шатаются.
Во всех остальных случаях — стабильный продукт, метрики на пользователя, есть 2-4 недели истории — CUPED почти всегда даёт бесплатное ускорение и должен стоять по умолчанию в вашей A/B-платформе. Если хотите отработать весь путь от SQL-выгрузки предпериода до финального теста, соберите когорты в SQL-тренажёре и прогоните аналитику на реальных продуктовых кейсах.
CUPED — один из тех приёмов, что отделяют аналитика, который «умеет считать p-value», от того, кто реально ускоряет принятие решений в продукте. Такие вещи любят спрашивать на собеседованиях в сильные команды, и умение объяснить механизм на пальцах ценится выше, чем зазубренная формула.
Если хотите системно прокачать A/B-тесты, статистику и SQL под реальные собеседования аналитиков — на Pro открыты все 400+ Python-задач, разборы кейсов, конспекты по статистике и безлимитные AI-мок-собесы. Это самый быстрый способ довести вариационные методы и продуктовую аналитику до автоматизма.