**Контекст:** Senior-собес в ML/growth-команду (Яндекс, Ozon, Авито). CUPED — метод уменьшения дисперсии метрики A/B-теста за счёт использования данных пользователя ДО эксперимента.
**Задача:** реализовать CUPED и сравнить с обычным t-test на симулированных данных.
**Данные:**
[см. код в задании]
**Задание:**
1. Сделать обычный t-test на `y_control` vs `y_treatment` — получить p-value и CI.
2. Применить CUPED: вычислить theta, посчитать скорректированные Y_cuped, снова t-test.
3. Показать во сколько раз уменьшилась дисперсия и уменьшился CI.
**Ожидаемый вывод:**
- Без CUPED: CI ±1.2, p=0.001
- С CUPED: CI ±0.6, p<0.0001 (при той же выборке значимость выше)
Структура для ориентира — реальные значения из эталонного решения.
import numpy as np
from scipy.stats import ttest_ind
import pandas as pd
def cuped_correction(y: np.ndarray, x: np.ndarray, theta: float) -> np.ndarray:
"""
CUPED correction: Y_cuped = Y - theta * (X - X.mean())
"""
return y - theta * (x - x.mean())
def analyze_ab(y_ctrl, y_trt, x_ctrl=None, x_trt=None, label='') -> dict:
"""
Обычный t-test или CUPED t-test.
"""
if x_ctrl is not None:
# Считаем theta на объединённых данных — более точно
y_all = np.concatenate([y_ctrl, y_trt])
x_all = np.concatenate([x_ctrl, x_trt])
theta = np.cov(y_all, x_all, ddof=1)[0, 1] / np.var(x_all, ddof=1)
# Применяем correction
y_ctrl_adj = cuped_correction(y_ctrl, x_ctrl, theta)
y_trt_adj = cuped_correction(y_trt, x_trt, theta)
else:
y_ctrl_adj, y_trt_adj = y_ctrl, y_trt
theta = None
# t-test
t_stat, p_value = ttest_ind(y_trt_adj, y_ctrl_adj, equal_var=False)
# Средние и эффект
mean_diff = y_trt_adj.mean() - y_ctrl_adj.mean()
se = np.sqrt(y_trt_adj.var(ddof=1) / len(y_trt_adj) +
y_ctrl_adj.var(ddof=1) / len(y_ctrl_adj))
ci_low = mean_diff - 1.96 * se
ci_high = mean_diff + 1.96 * se
# Дисперсия разности средних
variance = se ** 2
return {
'label': label,
'mean_diff': mean_diff,
'p_value': p_value,
'ci_low': ci_low,
'ci_high': ci_high,
'ci_half_width': 1.96 * se,
'variance': variance,
'theta': theta,
}
# ───── Запуск на симулированных данных ─────
np.random.seed(42)
n = 10_000
base_level = np.random.normal(100, 30, n)
pre_metric = base_level + np.random.normal(0, 20, n)
post_metric_control = base_level + np.random.normal(0, 20, n)
post_metric_treatment = base_level + np.random.normal(0, 20, n) + 3
is_treatment = np.random.binomial(1, 0.5, n).astype(bool)
y_control = post_metric_control[~is_treatment]
y_treatment = post_metric_treatment[is_treatment]
x_control = pre_metric[~is_treatment]
x_treatment = pre_metric[is_treatment]
# Без CUPED
r1 = analyze_ab(y_control, y_treatment, label='Plain t-test')
# С CUPED
r2 = analyze_ab(y_control, y_treatment, x_control, x_treatment, label='CUPED')
# Сравнение
print(pd.DataFrame([r1, r2])[['label', 'mean_diff', 'ci_half_width', 'p_value', 'variance']])
# Variance reduction
vr = 1 - r2['variance'] / r1['variance']
print(f"\nVariance reduction: {vr*100:.1f}%")
print(f"Theta: {r2['theta']:.3f}")
# Корреляция между pre и post (объясняет redukция)
rho = np.corrcoef(np.concatenate([pre_metric]), np.concatenate([post_metric_control]))[0, 1]
print(f"ρ(pre, post) = {rho:.3f}")
print(f"Теоретическая redukция: {(1 - (1 - rho**2))*100:.1f}% = {rho**2*100:.1f}%")
cuped variance-reduction ab-test python statsmodels
Это задание для уровня Senior. Senior-уровень — глубокое понимание темы, опыт решения нестандартных задач, обсуждение trade-off на собеседовании.
Подобные задания в категории «A/B-тесты» регулярно дают на собеседованиях аналитика данных в Яндекс, Сбер, Ozon, Авито, Тинькофф, Wildberries, T-Bank, X5, ВТБ и других крупных IT-компаниях. Тематика: cuped, variance-reduction, ab-test, python, statsmodels.
На реальном собеседовании на подобную задачу отводится 30-60 минут с обсуждением подходов, оптимизаций и trade-off. Для тренировки рекомендуем сначала решить самостоятельно, потом сверить с эталонным решением и подсказками.
На zasqlpython.ru есть другие задания в категории «A/B-тесты», продуктовые кейсы, справочник метрик, AI мок-собеседование с разбором ваших ответов.
← Все задания