A/B-тестирование остается основным инструментом для принятия продуктовых решений в технологических компаниях. Главная проблема — высокая дисперсия метрик, из-за которой требуется несколько недель или даже месяцев, чтобы достичь статистической значимости. Длительные эксперименты замедляют итерации и увеличивают альтернативные издержки.
Классический подход к снижению дисперсии — метод CUPED. Он использует исторические значения целевой метрики для корректировки результатов. Метод CUPAC (Control Using Predictions As Covariates) расширяет эту идею: вместо исторических данных используются предсказания машинного обучения. Это позволяет задействовать более широкий набор признаков и достичь большего снижения дисперсии.
Проблема variance в экспериментах
Статистическая мощность A/B теста определяется размером эффекта, объемом выборки и дисперсией метрики. Первые два параметра часто находятся вне контроля исследователя: изменения в продукте дают тот эффект, который дают, а увеличение трафика ограничено масштабом бизнеса. Остается третий параметр — дисперсия.
Почему высокая дисперсия замедляет тесты
Стандартная ошибка среднего пропорциональна:
σ/√n
где:
- σ — стандартное отклонение метрики;
- n — размер выборки.
Для достижения статистической значимости при фиксированном эффекте требуемый размер выборки растет квадратично с увеличением дисперсии.
Типичный пример: метрика Выручка на пользователя (RPU) обладает высокой дисперсией, потому что в выборке одновременно присутствуют неактивные пользователи с нулевой выручкой и клиенты, которые тратят очень много. Из-за этого распределение сильно скошено вправо, что приводит к длительным экспериментам даже при заметных изменениях в продукте.
Практические последствия высокой дисперсии:
- Эксперименты длятся 4-8 недель вместо 1-2 недель;
- Невозможность тестировать небольшие улучшения, которые в сумме дают значительный эффект;
- Увеличение риска внешних конфаундеров (сезонность, конкурентные действия, технические инциденты);
- Замедление скорости итераций и продуктовых релизов.
Снижение дисперсии в 4 раза сокращает требуемую длительность эксперимента в 4 раза или позволяет обнаруживать эффекты в 2 раза меньшей величины при той же длительности.
Традиционные подходы к снижению дисперсии
Стратификация — классический метод из теории выборки. Пользователи разбиваются на однородные группы (страты) по заранее известным признакам, внутри каждой страты проводится рандомизация. Дисперсия итоговой оценки снижается за счет исключения между-стратовой вариации.
Ограничения стратификации в онлайн-экспериментах:
- Необходимость отбора признаков для стратификации до начала эксперимента;
- Сложность с непрерывными признаками, требующими дискретизации;
- Комбинаторный рост числа страт при использовании нескольких признаков;
- Малые размеры некоторых страт приводят к проблемам с оценкой.
Постстратификация (post-stratification) частично решает проблему высокой дисперсии, но требует точного знания размеров страт в генеральной совокупности. Метод регрессионной корректировки (regression adjustment) использует линейную регрессию для уточнения оценок, однако его эффективность ограничена допущением о линейности связей.
Подход CUPED стал настоящим прорывом, объединив простоту использования и строгие теоретические гарантии. Метод не требует предварительной стратификации и может применяться post hoc к результатам практически любого эксперимента.
CUPED: фундамент для CUPAC
CUPED использует ковариатную корректировку для снижения дисперсии экспериментальных метрик. Основная идея: если известна переменная, коррелирующая с целевой метрикой но не зависящая от воздействия (treatment), ее можно использовать для уменьшения шума в оценках.
Механика работы CUPED
Пусть Y — метрика в период эксперимента, X — та же метрика в предэкспериментальный период (pre-experiment period). Скорректированная метрика вычисляется как:
Y_cv = Y — θ(X — E[X])
где:
- Y_cv — скорректированное значение метрики;
- Y — наблюдаемое значение в эксперименте;
- X — значение ковариаты (pre-experiment метрика);
- θ — оптимальный коэффициент корректировки;
- E[X] — среднее значение ковариаты по всей выборке.
Параметр θ выбирается для минимизации дисперсии скорректированной метрики. Оптимальное значение:
θ* = Cov(Y, X) / Var(X)
Это стандартный коэффициент из линейной регрессии Y на X. Дисперсия скорректированной метрики:
Var(Y_cv) = Var(Y)(1 — ρ²)
где ρ — корреляция между Y и X.
При ρ = 0.7 дисперсия снижается на 51%, что эквивалентно удвоению размера выборки.
Ключевое свойство метода CUPED состоит в том, что он не вносит смещения (bias) в оценку эффекта воздействия (treatment effect). Корректирующий член θ · (X − E[X]) по построению имеет нулевое математическое ожидание, поэтому выполняется равенство E[Y_cv] = E[Y]. Благодаря рандомизации распределение X одинаково в контрольной и тестовой группах, и значит корректировка не влияет на разницу между ними.
Ограничения метода:
- CUPED требует исторических данных той же метрики для каждого пользователя, поэтому новые пользователи не могут быть корректно обработаны — для них либо используют нули, снижая эффективность, либо исключают из корректировки;
- Метод чувствителен к нестационарности: если поведение пользователей меняется между pre- и post- периодами, корреляция падает, и эффективность CUPED снижается;
- Кроме того, CUPED работает только с одной ковариатой и не позволяет использовать дополнительную информацию о пользователях; расширение на несколько признаков приводит к нестабильности оценок и может увеличить дисперсию.
CUPAC: использование ML-предсказаний
CUPAC решает ограничения CUPED через замену исторических значений метрики на предсказания машинного обучения. Вместо корреляции между pre- и post-experiment значениями одной метрики используется предиктивная модель, обученная на множестве признаков.
Ключевые отличия от CUPED
Архитектурное изменение заключается в разделении на два этапа: обучение предиктивной модели и применение ковариатной корректировки. Модель обучается предсказывать целевую метрику Y по набору признаков X, которые доступны до начала эксперимента. Предсказания ŷ = f(X) затем используются как ковариата в формуле корректировки.
Преимущества подхода:
- Возможность использовать любые признаки пользователей, не только историю целевой метрики;
- Работа с новыми пользователями через предсказания по демографическим и первым взаимодействиям;
- Адаптация к нестационарности через регулярное переобучение модели;
- Большее снижение дисперсии за счет более точных предсказаний.
Качество предсказаний напрямую влияет на размер снижения дисперсии. Если модель объясняет 60% дисперсии целевой метрики (R² = 0.6), теоретический предел снижения дисперсии через CUPAC составляет 60%. Для сравнения, CUPED с корреляцией 0.7 между периодами дает снижение на 49%.
Выбор признаков для модели имеет ключевое значение. Все используемые признаки должны быть доступны до начала эксперимента и не зависеть от назначения в контрольную или тестовую группу. Это исключает использование конкурентных фичей (concurrent features) — характеристик, измеряемых одновременно с целевой метрикой и потенциально затрагиваемых воздействием.
Математический аппарат
Корректировка в CUPAC использует ту же формулу, что и CUPED:
Y_cv = Y — θ(ŷ — E[ŷ])
где:
- Y_cv — скорректированное значение метрики;
- Y — наблюдаемое значение;
- ŷ — предсказание ML-модели;
- θ — коэффициент корректировки;
- E[ŷ] — среднее предсказание.
Оптимальный коэффициент:
θ* = Cov(Y, ŷ) / Var(ŷ)
Это эквивалентно регрессии остатков на предсказания. Дисперсия скорректированной метрики:
Var(Y_cv) = Var(Y)(1 — ρ²_Yŷ)
где ρ_Yŷ — корреляция между фактическими значениями и предсказаниями.
Эта корреляция связана с R² модели: ρ²_Yŷ = R², следовательно максимальное снижение дисперсии определяется качеством предсказаний модели.
Важное свойство метода: корректировка остается несмещенной при любом качестве модели. Даже полностью случайные предсказания не приведут к систематической ошибке в оценке treatment effect — в худшем случае они просто не уменьшат дисперсию. Благодаря этому CUPAC остается устойчивым к ошибкам моделирования.
Практическая реализация требует особого внимания к оценке коэффициента θ. Наивный подход — вычислять θ на тех же данных, на которых обучалась модель — это приводит к переобучению корректировки. Правильная практика — использовать кросс-валидационные предсказания (cross-fitted predictions), когда для каждого пользователя предсказание получено моделью, обученной без его данных.
Выбор предикторов для ковариат
Эффективный набор признаков для CUPAC модели балансирует между предсказательной силой и практичностью сбора данных. Категории признаков по убыванию важности:
- Исторические значения целевой метрики остаются наиболее сильными предикторами. Агрегаты за разные временные окна (7, 14, 30 дней) захватывают как недавние тренды, так и долгосрочные паттерны. Для метрик с сезонностью добавляются значения из аналогичных периодов прошлого года;
- Поведенческие метрики включают частоту использования продукта, глубину взаимодействия, давность последнего визита. Эти признаки особенно ценны для новых пользователей, у которых нет длинной истории целевой метрики;
- Демографические и контекстные признаки: возраст, география, тип устройства, источник привлечения. Их предсказательная сила обычно ниже, но они доступны для всех пользователей, включая новых;
- Признаки из смежных метрик расширяют информацию. Если целевая метрика — выручка на пользователя, то признаки из количества сессий, времени в продукте, метрик вовлеченности добавляют ортогональную информацию.
Реализация CUPAC на Python
Практическое применение CUPAC требует интеграции нескольких компонентов: сбор и подготовка данных, обучение предиктивной модели, применение корректировки в эксперименте, валидация результатов.
Подготовка данных и обучение модели
Временная структура данных для CUPAC включает три периода:
- training period для обучения модели;
- prediction period для генерации предсказаний;
- experiment period для проведения теста.
Training period должен быть достаточно длинным для накопления статистики (обычно 60-90 дней) и располагаться до начала эксперимента.
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class CUPACPipeline:
def __init__(self, training_days=60, prediction_lag=7):
"""
training_days: длина периода для обучения модели
prediction_lag: задержка между prediction и experiment периодами
"""
self.training_days = training_days
self.prediction_lag = prediction_lag
self.model = None
self.scaler = None
self.theta = None
def prepare_training_data(self, df, experiment_start_date):
"""
Подготовка данных для обучения модели
"""
# Определение временных границ
prediction_date = experiment_start_date - timedelta(days=self.prediction_lag)
training_end = prediction_date
training_start = training_end - timedelta(days=self.training_days)
# Фильтрация данных
training_data = df[
(df['date'] >= training_start) &
(df['date'] < training_end) ].copy() # Создание целевой переменной (будущие значения) target_data = df[ (df['date'] >= prediction_date) &
(df['date'] < experiment_start_date) ].copy() target_agg = target_data.groupby('user_id').agg({ 'revenue': 'sum', 'sessions': 'count' }).reset_index() target_agg.columns = ['user_id', 'target_revenue', 'target_sessions'] return training_data, target_agg, prediction_date def engineer_features(self, df, reference_date): """ Создание признаков для модели """ features_list = [] # Агрегация по временным окнам for window in [7, 14, 30]: window_start = reference_date - timedelta(days=window) window_data = df[ (df['date'] >= window_start) &
(df['date'] < reference_date) ] user_agg = window_data.groupby('user_id').agg({ 'revenue': ['sum', 'mean', 'std', 'count'], 'sessions': ['count', 'mean'], 'session_duration': ['mean', 'max'] }) user_agg.columns = [f'{col[0]}_{col[1]}_{window}d' for col in user_agg.columns] user_agg = user_agg.reset_index() if len(features_list) == 0: features_list.append(user_agg) else: features_list[0] = features_list[0].merge( user_agg, on='user_id', how='outer' ) features_df = features_list[0] # Признаки recency last_activity = df.groupby('user_id')['date'].max().reset_index() last_activity['recency_days'] = ( reference_date - last_activity['date'] ).dt.days features_df = features_df.merge( last_activity[['user_id', 'recency_days']], on='user_id', how='left' ) # Категориальные признаки categorical_features = df.groupby('user_id').agg({ 'device_type': lambda x: x.mode()[0] if len(x) > 0 else 'unknown',
'country': lambda x: x.mode()[0] if len(x) > 0 else 'unknown'
}).reset_index()
features_df = features_df.merge(
categorical_features,
on='user_id',
how='left'
)
# One-hot encoding для категориальных признаков
features_df = pd.get_dummies(
features_df,
columns=['device_type', 'country'],
drop_first=True
)
# Заполнение пропусков
features_df = features_df.fillna(0)
return features_df
def fit(self, training_data, target_data, reference_date):
"""
Обучение CUPAC модели
"""
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import KFold
# Создание признаков
features = self.engineer_features(training_data, reference_date)
# Объединение с целевой переменной
model_data = features.merge(target_data, on='user_id', how='inner')
X = model_data.drop(['user_id', 'target_revenue', 'target_sessions'], axis=1)
y = model_data['target_revenue']
# Масштабирование
self.scaler = StandardScaler()
X_scaled = self.scaler.fit_transform(X)
# Cross-fitted predictions
kf = KFold(n_splits=5, shuffle=True, random_state=42)
cv_predictions = np.zeros(len(y))
for train_idx, val_idx in kf.split(X_scaled):
model_fold = GradientBoostingRegressor(
n_estimators=150,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
min_samples_leaf=50,
random_state=42
)
model_fold.fit(X_scaled[train_idx], y.iloc[train_idx])
cv_predictions[val_idx] = model_fold.predict(X_scaled[val_idx])
# Финальная модель на всех данных
self.model = GradientBoostingRegressor(
n_estimators=150,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
min_samples_leaf=50,
random_state=42
)
self.model.fit(X_scaled, y)
# Метрики качества
self.r_squared = 1 - np.var(y - cv_predictions) / np.var(y)
self.correlation = np.corrcoef(y, cv_predictions)[0, 1]
# Сохранение имен признаков для предсказаний
self.feature_names = X.columns.tolist()
return cv_predictions
def predict(self, experiment_data, experiment_start_date):
"""
Генерация предсказаний для периода эксперимента
"""
# Создание признаков на момент начала эксперимента
prediction_date = experiment_start_date - timedelta(days=self.prediction_lag)
features = self.engineer_features(experiment_data, prediction_date)
# Выравнивание признаков с обучающими данными
X = features.drop('user_id', axis=1)
# Добавление отсутствующих колонок
for col in self.feature_names:
if col not in X.columns:
X[col] = 0
# Удаление лишних колонок и сортировка
X = X[self.feature_names]
X_scaled = self.scaler.transform(X)
predictions = self.model.predict(X_scaled)
return features['user_id'], predictions
Давайте рассмотрим, что делает этот код:
- Мы создаем класс CUPACPipeline для построения и применения пайплайна CUPAC;
- Метод __init__ задает параметры обучения и инициализирует пустые атрибуты модели и масштабирования;
- Метод prepare_training_data формирует тренировочные данные и целевую переменную по пользователям. Затем рассчитывает временные границы для тренировочного окна и периода предсказания на основе даты эксперимента, и фильтрует исходный датафрейм по датам, плюс агрегирует метрики пользователей для обучения модели;
- Метод engineer_features создает признаки: агрегаты за разные временные окна, давность пользователей (recency) и категориальные признаки с one-hot кодированием;
- Метод fit обучает модель на признаках и целевой переменной с масштабированием и cross-fitted предсказаниями. Затем рассчитывает метрики качества модели, такие как R² и корреляцию между предсказаниями и реальными значениями;
- Метод predict создает признаки для экспериментальных данных и генерирует предсказания по обученной модели.
Модель машинного обучения GradientBoostingRegressor обучается на исторических признаках пользователей, чтобы предсказывать будущую выручку за период до эксперимента. Она используется как корректирующий инструмент CUPAC, уменьшая дисперсию метрики без внесения смещения в оценку эффекта воздействия.
Параметры градиентного бустинга настроены консервативно: умеренная глубина деревьев (6), низкий learning rate (0.05), большой min_samples_leaf (50). Это снижает риск переобучения, что важнее максимизации R² на валидации.
Применение корректировки в эксперименте
После генерации предсказаний они используются для корректировки экспериментальных метрик. Процесс применяется после завершения эксперимента, когда уже собраны фактические значения метрик.
# Генерация синтетических данных
np.random.seed(42)
# Параметры
n_users = 5000 # 5000 пользователей
date_range = pd.date_range('2025-01-01', '2025-11-30', freq='D')
# Создание исторических данных пользователей
user_data = []
for user_id in range(n_users):
# Базовые характеристики пользователя
base_revenue = np.random.lognormal(mean=2, sigma=1)
device_type = np.random.choice(['mobile', 'desktop', 'tablet'], p=[0.6, 0.3, 0.1])
country = np.random.choice(['US', 'UK', 'DE', 'FR'], p=[0.4, 0.3, 0.2, 0.1])
# Генерация активности по дням (весь исторический период до эксперимента)
for date in date_range[:120]: # До 1 мая (120 дней с 1 января)
if np.random.random() < 0.3: # 30% дней пользователь активен sessions = np.random.poisson(2) revenue = base_revenue * np.random.gamma(2, 0.5) * sessions session_duration = np.random.exponential(300) * sessions user_data.append({ 'user_id': user_id, 'date': date, 'revenue': revenue, 'sessions': sessions, 'session_duration': session_duration, 'device_type': device_type, 'country': country }) df_historical = pd.DataFrame(user_data) # Данные эксперимента (1 мая - 31 октября, 184 дня) experiment_start = pd.Timestamp('2025-05-01') experiment_end = pd.Timestamp('2025-10-31') experiment_data = [] for user_id in range(n_users): # Рандомизация в тест/контроль treatment = np.random.binomial(1, 0.5) # Treatment effect: 8% увеличение revenue treatment_multiplier = 1.08 if treatment == 1 else 1.0 # Получение базовых характеристик пользователя user_history = df_historical[df_historical['user_id'] == user_id] if len(user_history) > 0:
base_revenue = user_history['revenue'].mean()
device_type = user_history['device_type'].iloc[0]
country = user_history['country'].iloc[0]
else:
# Новый пользователь
base_revenue = np.random.lognormal(mean=2, sigma=1)
device_type = np.random.choice(['mobile', 'desktop', 'tablet'])
country = np.random.choice(['US', 'UK', 'DE', 'FR'])
# Генерация данных эксперимента (184 дня)
experiment_days = pd.date_range(experiment_start, experiment_end, freq='D')
for date in experiment_days:
if np.random.random() < 0.35: sessions = np.random.poisson(2) revenue = base_revenue * treatment_multiplier * np.random.gamma(2, 0.5) * sessions session_duration = np.random.exponential(300) * sessions experiment_data.append({ 'user_id': user_id, 'date': date, 'revenue': revenue, 'sessions': sessions, 'session_duration': session_duration, 'device_type': device_type, 'country': country, 'treatment': treatment }) df_experiment = pd.DataFrame(experiment_data) # Агрегация метрик эксперимента по пользователям experiment_results = df_experiment.groupby('user_id').agg({ 'revenue': 'sum', 'sessions': 'sum', 'treatment': 'first' }).reset_index() print(f"Historical data: {len(df_historical)} записей, {df_historical['user_id'].nunique()} пользователей") print(f"Experiment data: {len(experiment_results)} пользователей") print(f"Control group: {(experiment_results['treatment']==0).sum()} пользователей") print(f"Treatment group: {(experiment_results['treatment']==1).sum()} пользователей") # Функция подготовки данных def prepare_training_data(df, experiment_start_date, training_days=60, prediction_window=7): """ Исправленная подготовка данных для обучения """ # Prediction period находится перед экспериментом prediction_end = experiment_start_date - timedelta(days=1) prediction_start = prediction_end - timedelta(days=prediction_window) # Training period находится перед prediction period training_end = prediction_start - timedelta(days=1) training_start = training_end - timedelta(days=training_days) print(f"Training period: {training_start.date()} to {training_end.date()}") print(f"Prediction period: {prediction_start.date()} to {prediction_end.date()}") print(f"Experiment starts: {experiment_start_date.date()}") # Данные для обучения (признаки) training_data = df[ (df['date'] >= training_start) &
(df['date'] < training_end) ].copy() # Данные для целевой переменной (что мы хотим предсказать) target_data = df[ (df['date'] >= prediction_start) &
(df['date'] <= prediction_end)
].copy()
# Агрегация целевой переменной по пользователям
target_agg = target_data.groupby('user_id').agg({
'revenue': 'sum',
'sessions': 'count'
}).reset_index()
target_agg.columns = ['user_id', 'target_revenue', 'target_sessions']
# Для признаков используем дату конца training периода
reference_date = training_end
return training_data, target_agg, reference_date
# Обучение CUPAC модели
pipeline = CUPACPipeline(training_days=60, prediction_lag=7)
# Используем функцию подготовки данных
training_data, target_data, reference_date = prepare_training_data(
df_historical,
experiment_start,
training_days=60,
prediction_window=7
)
print(f"\nTraining period records: {len(training_data)}")
print(f"Target period users: {len(target_data)}")
print(f"Users with both training and target data: {len(target_data[target_data['user_id'].isin(training_data['user_id'].unique())])}")
if len(target_data) == 0:
raise ValueError("No target data available. Check date ranges.")
# Создание признаков и обучение
features = pipeline.engineer_features(training_data, reference_date)
model_data = features.merge(target_data, on='user_id', how='inner')
print(f"Model training data: {len(model_data)} пользователей")
if len(model_data) < 100:
raise ValueError(f"Insufficient training data: {len(model_data)} users")
# Обучение модели
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import KFold
X = model_data.drop(['user_id', 'target_revenue', 'target_sessions'], axis=1)
y = model_data['target_revenue']
pipeline.scaler = StandardScaler()
X_scaled = pipeline.scaler.fit_transform(X)
# Cross-fitted predictions
kf = KFold(n_splits=5, shuffle=True, random_state=42)
cv_predictions = np.zeros(len(y))
for train_idx, val_idx in kf.split(X_scaled):
model_fold = GradientBoostingRegressor(
n_estimators=150,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
min_samples_leaf=50,
random_state=42
)
model_fold.fit(X_scaled[train_idx], y.iloc[train_idx])
cv_predictions[val_idx] = model_fold.predict(X_scaled[val_idx])
# Финальная модель
pipeline.model = GradientBoostingRegressor(
n_estimators=150,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
min_samples_leaf=50,
random_state=42
)
pipeline.model.fit(X_scaled, y)
pipeline.r_squared = 1 - np.var(y - cv_predictions) / np.var(y)
pipeline.correlation = np.corrcoef(y, cv_predictions)[0, 1]
pipeline.feature_names = X.columns.tolist()
print(f"\nModel R²: {pipeline.r_squared:.3f}")
print(f"Model correlation: {pipeline.correlation:.3f}")
# Генерация предсказаний для периода эксперимента
# Используем данные до начала эксперимента для признаков
prediction_reference_date = experiment_start - timedelta(days=1)
features_experiment = pipeline.engineer_features(df_historical, prediction_reference_date)
X_experiment = features_experiment.drop('user_id', axis=1)
# Выравнивание признаков
for col in pipeline.feature_names:
if col not in X_experiment.columns:
X_experiment[col] = 0
X_experiment = X_experiment[pipeline.feature_names]
X_experiment_scaled = pipeline.scaler.transform(X_experiment)
predictions = pipeline.model.predict(X_experiment_scaled)
predictions_df = pd.DataFrame({
'user_id': features_experiment['user_id'],
'prediction': predictions
})
print(f"\nGenerated predictions for {len(predictions_df)} пользователей")
# Анализ эксперимента с CUPAC
analysis = CUPACAnalysis(predictions_df, experiment_results)
# Применение корректировки
var_reduction = analysis.compute_correction('revenue')
print(f"\nVariance Reduction: {var_reduction:.2%}")
print(f"Theta: {analysis.theta:.4f}")
# Сравнение результатов
results = analysis.run_ab_test('revenue')
print("\n" + "="*60)
print("РЕЗУЛЬТАТЫ A/B ТЕСТА")
print("="*60)
print("\nOriginal Metric:")
print(f" Control mean: ${results['original']['control_mean']:.2f}")
print(f" Treatment mean: ${results['original']['treatment_mean']:.2f}")
print(f" Lift: {results['original']['lift']:.2%}")
print(f" P-value: {results['original']['p_value']:.4f}")
print(f" T-statistic: {results['original']['t_statistic']:.2f}")
print(f" 95% CI: [${results['original']['ci_lower']:.2f}, ${results['original']['ci_upper']:.2f}]")
print("\nCUPAC Metric:")
print(f" Control mean: ${results['cupac']['control_mean']:.2f}")
print(f" Treatment mean: ${results['cupac']['treatment_mean']:.2f}")
print(f" Lift: {results['cupac']['lift']:.2%}")
print(f" P-value: {results['cupac']['p_value']:.4f}")
print(f" T-statistic: {results['cupac']['t_statistic']:.2f}")
print(f" 95% CI: [${results['cupac']['ci_lower']:.2f}, ${results['cupac']['ci_upper']:.2f}]")
print("\n" + "="*60)
print("IMPROVEMENT METRICS")
print("="*60)
print(f"T-statistic improved by: {results['improvement']['t_stat_ratio']:.2f}x")
print(f"CI width reduced by: {results['improvement']['ci_width_reduction']:.2%}")
# Интерпретация результатов
if results['original']['p_value'] < 0.05 and results['cupac']['p_value'] < 0.05: print("\n✓ Обе метрики показывают статистически значимый эффект") elif results['original']['p_value'] >= 0.05 and results['cupac']['p_value'] < 0.05:
print("\n✓ CUPAC обнаружил значимый эффект, пропущенный оригинальной метрикой")
elif results['original']['p_value'] < 0.05 and results['cupac']['p_value'] >= 0.05:
print("\n⚠ Несогласованность результатов - требуется дополнительная проверка")
else:
print("\n○ Обе метрики не обнаружили статистически значимого эффекта")
Historical data: 179863 записей, 5000 пользователей
Experiment data: 5000 пользователей
Control group: 2573 пользователей
Treatment group: 2427 пользователей
Training period: 2025-02-21 to 2025-04-22
Prediction period: 2025-04-23 to 2025-04-30
Experiment starts: 2025-05-01
Training period records: 89866
Target period users: 4719
Users with both training and target data: 4719
Model training data: 4719 пользователей
Model R²: 0.315
Model correlation: 0.562
Generated predictions for 5000 пользователей
Variance Reduction: 71.60%
Theta: 41.7089
============================================================
РЕЗУЛЬТАТЫ A/B ТЕСТА
============================================================
Original Metric:
Control mean: $3164.59
Treatment mean: $3430.84
Lift: 8.41%
P-value: 0.0410
T-statistic: 2.04
95% CI: [$10.96, $521.54]
CUPAC Metric:
Control mean: $3192.58
Treatment mean: $3401.17
Lift: 6.59%
P-value: 0.0026
T-statistic: 3.01
95% CI: [$72.88, $344.30]
============================================================
IMPROVEMENT METRICS
============================================================
T-statistic improved by: 1.47x
CI width reduced by: 46.84%
✓ Обе метрики показывают статистически значимый эффект
Класс CUPACAnalysis выполняет статистический анализ с корректировкой: вычисляется оптимальный коэффициент θ, применяется корректировка, а затем проводится t-test для исходной и скорректированной версии метрики. Результаты включают не только p-values, но и относительное улучшение (improvement) в t-статистике и ширине доверительных интервалов.
Sensitivity analysis проверяет устойчивость результатов к выбору θ. Если p-value сильно меняется при небольших вариациях θ, это сигнализирует о возможных проблемах с данными или моделью.
Результаты CUPAC показывают, что корректировка существенно повысила точность оценки эффекта эксперимента. Исходный lift составил 8,41% с p-value 0,041, тогда как после корректировки lift стал 6,59%, но p-value снизилось до 0,0026, а t-статистика выросла с 2,04 до 3,01. Это сопровождается уменьшением ширины доверительного интервала почти вдвое (−46,84%), при этом дисперсия метрики снизилась на 71,6%.
Вывод: модель хорошо предсказывает поведение пользователей (R²=0,315, корреляция=0,562). В целом CUPAC делает выводы более стабильными и статистически убедительными.
Оценка эффективности снижения дисперсии (variance reduction)
Количественная оценка эффективности CUPAC включает несколько метрик. Прямое измерение — процент снижения дисперсии. Косвенные метрики — изменение статистической мощности и требуемого размера выборки.
import matplotlib.pyplot as plt
import seaborn as sns
def evaluate_cupac_effectiveness(original_metric, cupac_metric, treatment_indicator):
"""
Комплексная оценка эффективности CUPAC
"""
# Variance reduction
var_original = np.var(original_metric)
var_cupac = np.var(cupac_metric)
var_reduction = (var_original - var_cupac) / var_original
# Эффект на статистическую мощность
control_mask = treatment_indicator == 0
treatment_mask = treatment_indicator == 1
# Стандартные ошибки
se_original = np.sqrt(
var_original / treatment_mask.sum() +
var_original / control_mask.sum()
)
se_cupac = np.sqrt(
var_cupac / treatment_mask.sum() +
var_cupac / control_mask.sum()
)
se_reduction = (se_original - se_cupac) / se_original
# Minimum detectable effect (MDE)
alpha = 0.05
beta = 0.20 # 80% power
z_alpha = stats.norm.ppf(1 - alpha/2)
z_beta = stats.norm.ppf(1 - beta)
mde_original = (z_alpha + z_beta) * se_original
mde_cupac = (z_alpha + z_beta) * se_cupac
mde_improvement = (mde_original - mde_cupac) / mde_original
# Sample size reduction
# n_new / n_old = (sigma_new / sigma_old)^2
sample_size_reduction = 1 - (np.sqrt(var_cupac) / np.sqrt(var_original))**2
results = {
'variance_reduction': var_reduction,
'se_reduction': se_reduction,
'mde_improvement': mde_improvement,
'sample_size_reduction': sample_size_reduction
}
return results
def plot_distribution_comparison(original_metric, cupac_metric, treatment_indicator):
"""
Визуализация распределений оригинальной и CUPAC метрики
"""
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
control_mask = treatment_indicator == 0
treatment_mask = treatment_indicator == 1
# Распределения для контрольной группы
axes[0, 0].hist(original_metric[control_mask], bins=50, alpha=0.7,
color='blue', label='Control', density=True)
axes[0, 0].hist(original_metric[treatment_mask], bins=50, alpha=0.7,
color='red', label='Treatment', density=True)
axes[0, 0].set_title('Original Metric Distribution')
axes[0, 0].legend()
axes[0, 0].set_xlabel('Metric Value')
axes[0, 0].set_ylabel('Density')
axes[0, 1].hist(cupac_metric[control_mask], bins=50, alpha=0.7,
color='blue', label='Control', density=True)
axes[0, 1].hist(cupac_metric[treatment_mask], bins=50, alpha=0.7,
color='red', label='Treatment', density=True)
axes[0, 1].set_title('CUPAC Metric Distribution')
axes[0, 1].legend()
axes[0, 1].set_xlabel('Metric Value')
axes[0, 1].set_ylabel('Density')
# Q-Q plots для проверки нормальности
stats.probplot(original_metric, dist="norm", plot=axes[1, 0])
axes[1, 0].set_title('Q-Q Plot: Original Metric')
stats.probplot(cupac_metric, dist="norm", plot=axes[1, 1])
axes[1, 1].set_title('Q-Q Plot: CUPAC Metric')
plt.tight_layout()
return fig
def bootstrap_variance_reduction(original_metric, cupac_metric, n_bootstrap=1000):
"""
Bootstrap доверительный интервал для variance reduction
"""
var_reductions = []
for _ in range(n_bootstrap):
idx = np.random.choice(len(original_metric), size=len(original_metric), replace=True)
var_orig_boot = np.var(original_metric[idx])
var_cupac_boot = np.var(cupac_metric[idx])
vr = (var_orig_boot - var_cupac_boot) / var_orig_boot
var_reductions.append(vr)
var_reductions = np.array(var_reductions)
ci_lower = np.percentile(var_reductions, 2.5)
ci_upper = np.percentile(var_reductions, 97.5)
return var_reductions.mean(), ci_lower, ci_upper
# Визуализация эффективности CUPAC
print("\n" + "="*60)
print("ДОПОЛНИТЕЛЬНЫЙ АНАЛИЗ")
print("="*60)
# Оценка эффективности
effectiveness = evaluate_cupac_effectiveness(
analysis.data['revenue'].values,
analysis.data['revenue_cupac'].values,
analysis.data['treatment'].values
)
print(f"\nVariance Reduction: {effectiveness['variance_reduction']:.2%}")
print(f"Standard Error Reduction: {effectiveness['se_reduction']:.2%}")
print(f"MDE Improvement: {effectiveness['mde_improvement']:.2%}")
print(f"Sample Size Reduction: {effectiveness['sample_size_reduction']:.2%}")
# Визуализация распределений
fig1 = plot_distribution_comparison(
analysis.data['revenue'].values,
analysis.data['revenue_cupac'].values,
analysis.data['treatment'].values
)
plt.show()
# Bootstrap анализ variance reduction
vr_mean, vr_lower, vr_upper = bootstrap_variance_reduction(
analysis.data['revenue'].values,
analysis.data['revenue_cupac'].values,
n_bootstrap=1000
)
print(f"\nBootstrap Variance Reduction: {vr_mean:.2%}")
print(f"95% CI: [{vr_lower:.2%}, {vr_upper:.2%}]")
# Визуализация bootstrap результатов
fig2, ax = plt.subplots(figsize=(10, 6))
var_reductions_boot = []
for _ in range(1000):
idx = np.random.choice(len(analysis.data), size=len(analysis.data), replace=True)
var_orig = np.var(analysis.data['revenue'].values[idx])
var_cupac = np.var(analysis.data['revenue_cupac'].values[idx])
var_reductions_boot.append((var_orig - var_cupac) / var_orig)
ax.hist(var_reductions_boot, bins=50, color='steelblue', alpha=0.7, edgecolor='black')
ax.axvline(vr_mean, color='red', linestyle='--', linewidth=2, label=f'Mean: {vr_mean:.2%}')
ax.axvline(vr_lower, color='orange', linestyle='--', linewidth=1.5, label=f'2.5%: {vr_lower:.2%}')
ax.axvline(vr_upper, color='orange', linestyle='--', linewidth=1.5, label=f'97.5%: {vr_upper:.2%}')
ax.set_xlabel('Variance Reduction')
ax.set_ylabel('Frequency')
ax.set_title('Bootstrap Distribution of Variance Reduction')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
============================================================
ДОПОЛНИТЕЛЬНЫЙ АНАЛИЗ
============================================================
Variance Reduction: 71.60%
Standard Error Reduction: 46.71%
MDE Improvement: 46.71%
Sample Size Reduction: 71.60%
Bootstrap Variance Reduction: 71.54%
95% CI: [67.52%, 75.70%]
Представленный выше код выполняет дополнительный анализ эффективности CUPAC:
- Вычисляет метрики улучшения — уменьшение дисперсии (variance reduction), уменьшение стандартной ошибки (SE reduction), улучшение минимально обнаруживаемого эффекта (MDE improvement) и потенциальное сокращение размера выборки;
- Строит графики распределений исходной и скорректированной метрики для контрольной и тестовой групп, а также Q-Q графики для проверки нормальности;
- Проводит bootstrap-анализ дисперсии, оценивая доверительный интервал для variance reduction.

Рис. 1: Сравнение распределений оригинальной и CUPAC метрики. Верхние панели демонстрируют гистограммы распределения выручки для контрольной (синий) и тестовой (красный) групп. Левая панель показывает оригинальную метрику с высокой дисперсией и значительным перекрытием распределений между группами. Правая панель отображает CUPAC-скорректированную метрику со снижением дисперсии на 71.6% при сохранении разделения между группами. Нижние панели содержат Q-Q plots для проверки нормальности распределений. Отклонения от диагональной линии на хвостах указывают на наличие тяжелых хвостов, характерных для метрик выручки, что не влияет на валидность t-теста при больших выборках
![Бутстрап-распределение снижения дисперсии. Гистограмма демонстрирует распределение оценок снижения дисперсии, полученных методом bootstrap с 1000 итераций. Красная пунктирная линия обозначает среднее значение variance reduction (71.54%), оранжевые линии показывают границы 95% доверительного интервала [67.52%, 75.70%]. Узкий доверительный интервал (ширина 8.18%) свидетельствует о высокой стабильности оценки и робастности CUPAC корректировки. Симметричная форма распределения подтверждает отсутствие систематических смещений в методе](https://mlgu.ru/wp-content/uploads/2025/11/z14.jpg)
Рис. 2: Бутстрап-распределение снижения дисперсии. Гистограмма демонстрирует распределение оценок снижения дисперсии, полученных методом bootstrap с 1000 итераций. Красная пунктирная линия обозначает среднее значение variance reduction (71.54%), оранжевые линии показывают границы 95% доверительного интервала [67.52%, 75.70%]. Узкий доверительный интервал (ширина 8.18%) свидетельствует о высокой стабильности оценки и робастности CUPAC корректировки. Симметричная форма распределения подтверждает отсутствие систематических смещений в методе
Визуализация распределений помогает выявлять потенциальные проблемы: если метрика CUPAC имеет тяжелые хвосты, либо мультимодальное распределение, это может указывать на ошибки модели или наличие подгрупп пользователей с разными характеристиками.
Бутстрап-доверительные интервалы для variance reduction оценивают стабильность результатов: широкий интервал сигнализирует о высокой вариативности и необходимости увеличить объем тренировочных данных, либо улучшить модель.
Практические аспекты применения
Внедрение CUPAC в продакшен среду требует решения нескольких практических задач: выбор архитектуры модели, обработка нестационарности, интеграция в существующий пайплайн проведения экспериментов.
Выбор архитектуры предиктивной модели
Модели градиентного бустинга (XGBoost, LightGBM, CatBoost) на сегодняшний день являются наиболее популярными в работе с методом CUPAC благодаря хорошему сочетанию точности, скорости обучения и интерпретируемости.
Линейные модели (Ridge, Lasso) подходят для высокоразмерных данных с преимущественно линейными зависимостями: они эффективны вычислительно и устойчивы к переподгонке при правильной регуляризации, однако не улавливают нелинейные паттерны и взаимодействия признаков.
Нейронные сети оправданы для очень больших датасетов с сложными нелинейными зависимостями; многослойные архитектуры с dropout и batch normalization дают качество, сопоставимое с градиентным бустингом, но требуют больше времени на обучение и настройку гиперпараметров.
Обработка нестационарности данных
Поведенческие паттерны пользователей меняются со временем из-за сезонности, маркетинговых кампаний, изменений продукта и внешних факторов, из-за чего модель, обученная на старых данных, теряет точность и снижает эффективность CUPAC.
Стратегии борьбы с нестационарностью:
- Регулярное переобучение: частота зависит от скорости изменений паттернов — для стабильных продуктов достаточно раз в месяц, для быстро меняющихся — еженедельно. Сигнал к переобучению — падение корреляции на holdout ниже порога;
- Скользящее окно обучения: используется только недавний период (например, последние 60 дней), что адаптирует модель к актуальным паттернам, но сокращает объем данных;
- Temporal features: добавление времени (день недели, месяц, праздники, время с последнего релиза) позволяет учитывать систематические временные паттерны.
Интеграция в пайплайны A/B тестирования
Внедрение CUPAC в продакшен требует интеграции с существующей экспериментальной инфраструктурой, включая хранилище моделей, генерацию предсказаний и статистический анализ.
Архитектура пайплайна:
- Offline — периодическое обучение и валидация моделей на исторических данных с сохранением в model registry;
- Near-realtime — генерация и кеширование предсказаний для пользователей;
- Post-experiment — применение CUPAC к завершенным экспериментам и сравнение с оригинальными метриками.
Ключевые требования:
- Версионирование моделей с метаданными (дата, R², признаки, гиперпараметры) для воспроизводимости и отката;
- Кеширование предсказаний для экономии ресурсов при длительных экспериментах;
- Мониторинг качества через корреляцию предсказаний и фактических значений: падение ниже порога инициирует переобучение или алерт;
- A/B тестирование самого CUPAC на подмножестве экспериментов для проверки консистентности и стабильности результатов.
Таким образом, предсказания лучше генерировать заранее, затем сохранять в кеш, а после завершения запускать CUPAC анализ. Для анализа результатов можно использовать дашборды с преднастроенными алертами. Оптимально реализовать CUPAC как микросервис с API для постепенной интеграции без переписывания существующего кода.
Заключение
Метод CUPAC превращает машинное обучение из вспомогательного инструмента маркетинга и продуктовой аналитики в важный элемент экспериментальной инфраструктуры:
- Снижение дисперсии на 40-70% — это не просто улучшение метрики, а качественный сдвиг в способности компаний принимать решения;
- Эксперименты, которые раньше требовали месяцев накопления данных, завершаются за недели;
- Изменения, которые терялись в шуме, становятся видимыми и измеримыми.
Метод работает там, где классический CUPED упирается в потолок: новые пользователи без истории, нестационарные паттерны поведения, необходимость использовать десятки признаков одновременно. Предиктивная модель агрегирует всю доступную информацию о пользователе в единую оценку будущего поведения, которая служит идеальной ковариатой для корректировки. При этом математические гарантии несмещенности сохраняются независимо от качества модели — плохие предсказания не навредят, просто не помогут.
Практическое внедрение CUPAC требует инвестиций в инфраструктуру: дополнительный компьют, регулярное обучение моделей, мониторинг деградации, интеграция в платформу для проведения экспериментов. Тем не менее, для компаний, проводящих десятки A/B тестов одновременно, эти затраты окупятся многократно через ускорение итераций и способность обнаруживать тонкие, но крайне важные эффекты в данных пользователей.