Что это и зачем нужно
CUPED (Controlled-experiment Using Pre-Existing Data) — техника снижения variance в A/B-тестах с помощью пре-экспериментальных данных. Изобретено в Microsoft в 2013, опубликовано в paper Deng et al.
Зачем нужно: ARPU, GMV, time spent — это метрики с огромной дисперсией. 95% юзеров платят 0₸, 5% — большие суммы. Для A/B-теста на ARPU обычно нужны миллионы юзеров и месяцы. CUPED уменьшает variance в 2-3 раза → выборка в 2-3 раза меньше → быстрее эксперименты.
В Каспи / Halyk / Glovo, где product velocity критична, CUPED — стандартный инструмент data science команды.
Базовый пример
Формула:
$$Y_{CUPED} = Y - \theta(X - \bar{X})$$
где:
Y— метрика в эксперименте (например, ARPU в test period)X— pre-experiment covariate (ARPU того же юзера до теста)θ— оптимальный коэффициент =Cov(Y, X) / Var(X)X̄— среднееXпо всем юзерам в эксперименте
Интуиция:
Часть variance в Y объясняется тем что юзер уже был active платящим (X высокий). Если убрать этот компонент — оставшаяся variance отражает эффект эксперимента, а не background variation между юзерами.
Реализация:
import numpy as np
import pandas as pd
from scipy import stats
# Данные эксперимента
data = pd.DataFrame({
"user_id": [...],
"group": ["A" or "B"],
"Y": [...], # ARPU в test period
"X": [...], # ARPU в pre-period (60 дней до теста)
})
# Шаг 1: θ — оптимальный коэффициент
theta = np.cov(data["Y"], data["X"])[0, 1] / np.var(data["X"], ddof=1)
# Шаг 2: CUPED-transform
data["Y_cuped"] = data["Y"] - theta * (data["X"] - data["X"].mean())
# Шаг 3: обычный t-test на трансформированной метрике
A = data[data["group"] == "A"]["Y_cuped"]
B = data[data["group"] == "B"]["Y_cuped"]
t_stat, p_value = stats.ttest_ind(A, B)
print(f"CUPED p-value: {p_value:.4f}")
# Для сравнения — без CUPED
t_stat_old, p_value_old = stats.ttest_ind(
data[data["group"] == "A"]["Y"],
data[data["group"] == "B"]["Y"]
)
print(f"Original p-value: {p_value_old:.4f}")
print(f"Variance reduction: {1 - data['Y_cuped'].var() / data['Y'].var():.1%}")
Типичный результат: variance reduction 40-60% если pre-period сильно коррелирован с test-period.
Тонкости и pitfalls
1. Когда CUPED работает
- Continuous metrics с heavy variance: ARPU, GMV, time spent, sessions count
- Persistent users: retention высокая → pre-period predictive
- Корреляция Y-X не слишком слабая (хотя бы 0.3) — иначе variance reduction маленький
2. Когда НЕ работает
- Binary outcomes (conversion rate) — variance из биномиального распределения, pre-period covariate не сильно помогает
- Новые юзеры — нет pre-period данных. Для них используешь стандартный t-test без CUPED
- «Anti-features»: если фича призвана изменить паттерн юзера (например научить heavy spenders тратить меньше), pre-period больше не predictive — CUPED может ввести bias
3. Multi-period CUPED
Используешь не одну pre-period метрику, а несколько:
from sklearn.linear_model import LinearRegression
X_multi = data[["arpu_30d", "arpu_60d", "sessions_30d"]]
Y = data["Y"]
reg = LinearRegression()
reg.fit(X_multi, Y)
# Используешь линейную комбинацию пре-period covariates
data["Y_cuped_multi"] = data["Y"] - reg.predict(X_multi)
Variance reduction может вырасти до 70%+ — но требует регрессии и более сложного pipeline.
4. Pitfall: pre-period должен быть до randomization
Если pre-period overlap'ит со start теста — leak. Используй данные за период [start - 60d, start - 1d].
5. Pitfall: θ оценивается на обеих группах вместе
НЕ считать θ отдельно для A и B — это введёт bias. Используй pooled θ от всех юзеров эксперимента.
6. Pitfall: коррелированные covariates с outcome через treatment
Если pre-period сам зависит от того, какую группу юзер потом получит (а это не должно быть в честном randomized experiment — но может) — CUPED введёт bias.
Когда НЕ применять
-
Binary metrics (conversion). Variance reduction небольшой. Используй обычный chi-square или z-test.
-
Когда pre-period неинформативен — например, тестируешь onboarding для новых юзеров (у них pre-period не существует).
-
Когда хочешь измерить non-linear изменение — например, бимодальный эффект. CUPED предполагает линейные связи.
-
На очень малой выборке (< 1000 на группу). θ оценка нестабильна, может ввести шум вместо снижения variance.
Кейс из казахстанского бизнеса
В Каспи Pay был A/B-тест нового UI кошелька. Primary metric: monthly transaction volume per user (continuous, high variance).
Without CUPED:
- Variance per user: 2.5M ₸²
- For MDE = 5% (relative), n = ~50K per group
- 50K активных Каспи Pay юзеров — это 2 недели сбора
With CUPED (используя prev-month transaction volume):
- Correlation pre-period vs experiment: 0.72
- Variance after CUPED: 1.2M ₸² (-52%)
- n = ~24K per group
- 1 неделя сбора
Эффект на скорость продукта: команда могла запустить в 2 раза больше экспериментов в году — что напрямую транслировалось в product velocity и улучшение KPI.
Implementation:
-- Pre-period covariate из data warehouse
WITH pre_period AS (
SELECT user_id, SUM(amount) AS pre_volume
FROM transactions
WHERE created_at BETWEEN '2026-03-01' AND '2026-04-30'
GROUP BY user_id
),
experiment AS (
SELECT user_id, group_assignment,
SUM(amount) AS exp_volume
FROM transactions
WHERE created_at BETWEEN '2026-05-01' AND '2026-05-14'
GROUP BY user_id, group_assignment
)
SELECT e.user_id, e.group_assignment, e.exp_volume AS Y, COALESCE(p.pre_volume, 0) AS X
FROM experiment e
LEFT JOIN pre_period p ON p.user_id = e.user_id;
Дальше — pandas + scipy как в примере выше.
Senior data scientist в Каспи сказал бы: «CUPED — это самый дешёвый способ удвоить experimentation velocity в зрелой команде. Внедряется за 2 недели, окупается всё время.»