Условие
На сайте запущен A/B-тест с целью увеличить доход. В Excel-файле сырые данные: user_id, тип выборки variant_name, доход пользователя revenue. Проанализируйте результаты и напишите рекомендации менеджеру. Приложите скрипт.
Решение
Подход
Revenue per user (RPU) — типичная тяжёлохвостая метрика: 95% пользователей платят 0, 5% платят, среди платящих — лог-нормальное распределение. Прямой t-test слаб; стандартное решение:
- Sanity-check: размеры групп (SRM), распределение revenue.
- Decompose: CR в платёж + ARPPU (revenue per paying user).
- Тест на RPU: bootstrap или CUPED (если есть pre-data).
- CI на эффект в денежных единицах.
Реализация
import pandas as pd
import numpy as np
from scipy import stats
from statsmodels.stats.proportion import proportions_ztest
df = pd.read_excel("ab_data.xlsx")
# Ожидаемые поля: user_id, variant_name (control/test), revenue (NaN или число)
df["revenue"] = df["revenue"].fillna(0)
df["paid"] = (df["revenue"] > 0).astype(int)
# 1. SRM
sizes = df.groupby("variant_name")["user_id"].nunique()
chi2, p_srm = stats.chisquare(sizes, f_exp=[sizes.sum()/2]*2)
print("Group sizes:", sizes.to_dict(), "SRM p =", round(p_srm, 4))
# 2. Конверсия в платёж
ct = df.groupby("variant_name")["paid"].agg(["sum", "count"])
z, p_cr = proportions_ztest(ct["sum"].values, ct["count"].values)
cr_lift = ct.loc["test","sum"]/ct.loc["test","count"] \
- ct.loc["control","sum"]/ct.loc["control","count"]
print(f"CR lift = {cr_lift:.4%}, p = {p_cr:.4f}")
# 3. ARPPU (только платящие)
payers_c = df.loc[(df["variant_name"]=="control") & (df["paid"]==1), "revenue"]
payers_t = df.loc[(df["variant_name"]=="test") & (df["paid"]==1), "revenue"]
print(f"ARPPU control: {payers_c.mean():.2f}, test: {payers_t.mean():.2f}")
# 4. RPU и bootstrap CI
def boot_diff(a, b, n=10000, seed=42):
rng = np.random.default_rng(seed)
diffs = np.empty(n)
for i in range(n):
diffs[i] = (rng.choice(b, size=len(b), replace=True).mean()
- rng.choice(a, size=len(a), replace=True).mean())
return diffs
ctrl = df.loc[df["variant_name"]=="control", "revenue"].values
test = df.loc[df["variant_name"]=="test", "revenue"].values
diffs = boot_diff(ctrl, test, n=10000)
ci = np.percentile(diffs, [2.5, 97.5])
print(f"RPU lift = {diffs.mean():.2f} (95% CI [{ci[0]:.2f}, {ci[1]:.2f}])")
# Также Mann-Whitney на полной выборке (стресс-тест)
u, p_mw = stats.mannwhitneyu(ctrl, test, alternative="two-sided")
print(f"Mann-Whitney p = {p_mw:.4f}")Анализ результата
Логика интерпретации:
| CR | ARPPU | RPU | Решение |
|---|---|---|---|
| ↑ значимо | ≈ | ↑ | Выкатываем — больше платящих, чек тот же |
| ≈ | ↑ | ↑ | Выкатываем, если рост чека устойчив (не один кит) |
| ↑ | ↓ | ↑ слабо | Аккуратнее: проверять long-term LTV |
| ↑ | ↓ значимо | ≈ | Не выкатываем — расширили low-value сегмент |
| ↓ | ↑ | ≈ | Не выкатываем — отпугнули мелких платящих |
Если 95%-CI на RPU не пересекает 0 и нижняя граница > 0 — есть статистически значимый прирост. Если CI пересекает 0, но смещён вверх — слабый сигнал, нужна большая выборка.
Рекомендации менеджеру
- Главный вывод по RPU + CI: «
+1.5 руб/userна доверительном интервале[+0.5; +2.4]». - Декомпозиция: «Прирост идёт за счёт CR (
+0.3 п.п.), ARPPU не изменился». - Сегменты: где эффект сильнее (платформа, новые vs старые).
- Гарды: retention, NPS, time-on-site не упали.
- Бизнес-эффект:
+1.5 руб × N юзеров/мес = +X руб/мес.
Подводные камни
- Тяжёлые хвосты revenue. Один кит может определить эффект. T-test/нормальные методы плохо работают; используйте bootstrap, mean-trimming или log(1+x) трансформацию (с осторожностью).
- SRM. Перед всем — проверка размеров групп.
- Right-censoring. Пользователи, попавшие в конец теста, имели мало времени для платежа. Окно anchor «N дней с момента входа в эксперимент».
- Outlier-фильтрация. Плохая практика «отрезать топ 0.1%» — может скрыть истинный эффект. Лучше: показать результаты с outliers и без, обсудить с менеджером.
- Multiple testing. Если смотрите на CR, ARPPU, RPU, retention — поправка (BH или Bonferroni).
- Decision rule. Не «
p < 0.05→ выкатываем», а «эффект > MDE иp < α→ выкатываем». MDE должен быть прибит до теста.
Эталонный ответ
Скрипт делает: SRM-чек → CR + z-test → ARPPU → RPU с bootstrap-CI → Mann-Whitney как стресс-тест. Рекомендация — на основе декомпозиции RPU = CR × ARPPU и направления изменений. В отчёте менеджеру: эффект в деньгах с CI, сегментация, гарды, бизнес-эффект — а не голое p-value.