Что это и зачем нужно

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 по всем юзерам в эксперименте

Интуиция:

Часть 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.


Когда НЕ применять

  1. Binary metrics (conversion). Variance reduction небольшой. Используй обычный chi-square или z-test.

  2. Когда pre-period неинформативен — например, тестируешь onboarding для новых юзеров (у них pre-period не существует).

  3. Когда хочешь измерить non-linear изменение — например, бимодальный эффект. CUPED предполагает линейные связи.

  4. На очень малой выборке (< 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 недели, окупается всё время.»