Собесов

ВК/ОК: анализ A/B-теста на доход (revenue per user)

A/B-тестыАнализ результатов A/BСредняяMiddle

Условие

На сайте запущен A/B-тест с целью увеличить доход. В Excel-файле сырые данные: user_id, тип выборки variant_name, доход пользователя revenue. Проанализируйте результаты и напишите рекомендации менеджеру. Приложите скрипт.

Решение

Подход

Revenue per user (RPU) — типичная тяжёлохвостая метрика: 95% пользователей платят 0, 5% платят, среди платящих — лог-нормальное распределение. Прямой t-test слаб; стандартное решение:

  1. Sanity-check: размеры групп (SRM), распределение revenue.
  2. Decompose: CR в платёж + ARPPU (revenue per paying user).
  3. Тест на RPU: bootstrap или CUPED (если есть pre-data).
  4. 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, но смещён вверх — слабый сигнал, нужна большая выборка.

Рекомендации менеджеру

  1. Главный вывод по RPU + CI: «+1.5 руб/user на доверительном интервале [+0.5; +2.4]».
  2. Декомпозиция: «Прирост идёт за счёт CR (+0.3 п.п.), ARPPU не изменился».
  3. Сегменты: где эффект сильнее (платформа, новые vs старые).
  4. Гарды: retention, NPS, time-on-site не упали.
  5. Бизнес-эффект: +1.5 руб × N юзеров/мес = +X руб/мес.

Подводные камни

  1. Тяжёлые хвосты revenue. Один кит может определить эффект. T-test/нормальные методы плохо работают; используйте bootstrap, mean-trimming или log(1+x) трансформацию (с осторожностью).
  2. SRM. Перед всем — проверка размеров групп.
  3. Right-censoring. Пользователи, попавшие в конец теста, имели мало времени для платежа. Окно anchor «N дней с момента входа в эксперимент».
  4. Outlier-фильтрация. Плохая практика «отрезать топ 0.1%» — может скрыть истинный эффект. Лучше: показать результаты с outliers и без, обсудить с менеджером.
  5. Multiple testing. Если смотрите на CR, ARPPU, RPU, retention — поправка (BH или Bonferroni).
  6. Decision rule. Не «p < 0.05 → выкатываем», а «эффект > MDE и p < α → выкатываем». MDE должен быть прибит до теста.

Эталонный ответ

Скрипт делает: SRM-чек → CR + z-test → ARPPU → RPU с bootstrap-CI → Mann-Whitney как стресс-тест. Рекомендация — на основе декомпозиции RPU = CR × ARPPU и направления изменений. В отчёте менеджеру: эффект в деньгах с CI, сегментация, гарды, бизнес-эффект — а не голое p-value.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти