Условие
В Match-3 игре каждый уровень имеет сложность. Кривая сложности — динамика изменения сложности в последовательности уровней. Команда хочет проверить две гипотезы:
- Снижение сложности первых уровней уменьшит отток игроков, но снизит монетизацию на этих уровнях (юзеры не будут «застревать» и покупать буст).
- Постепенное усложнение дальних уровней компенсирует просадку в монетизации.
Провели A/B-тест на новых игроках (join_date == install_date). Длительность — 60 дней.
Данные:
ab_users.csv:event_user, abgroup, join_date.ab_data.csv:event_user, date, revenue, attempts(дневное количество сыгранных уровней).
Задания:
- Интерпретировать результаты теста и дать рекомендацию.
- Какие данные/метрики ещё хотели бы посмотреть.

Решение
Подход
Тест с двумя «дорожками изменений» (упростили начало + усложнили дальше). Это не два независимых эксперимента, а один, чтобы увидеть нетто-эффект.
Гипотезы можно увидеть в данных через:
- Retention: должен вырасти в test (меньше «застряли» в начале).
- ARPU: на ранних днях, скорее всего, упадёт (меньше «фрустрированных» покупок); на поздних — должен компенсировать.
- Attempts (попытки = engagement): рост — хороший сигнал.
- LTV-60 (decision metric) — нетто-эффект.
Реализация — план анализа
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
ab = pd.read_csv("ab_users.csv", parse_dates=["join_date"])
data = pd.read_csv("ab_data.csv", parse_dates=["date"])
df = data.merge(ab, on="event_user")
df["day_after_install"] = (df["date"] - df["join_date"]).dt.days
# 1. Retention curve по группе
def retention_curve(df, group, days=range(0, 61)):
sub = df[df["abgroup"] == group]
n_total = sub["event_user"].nunique()
out = []
for d in days:
active = sub[sub["day_after_install"] == d]["event_user"].nunique()
out.append({"day": d, "retention": active / n_total})
return pd.DataFrame(out)Метрика 1. Retention
ret_a = retention_curve(df, "A")
ret_b = retention_curve(df, "B")
plt.plot(ret_a["day"], ret_a["retention"], label="A control")
plt.plot(ret_b["day"], ret_b["retention"], label="B test")
plt.xlabel("Day after install"); plt.ylabel("DAU/installs"); plt.legend()Если retention в B выше → гипотеза 1 «уменьшаем отток на старте» подтверждается.
Метрика 2. ARPU и LTV
# Cumulative LTV
df["cumrev_user"] = df.sort_values(["event_user", "date"]).groupby("event_user")["revenue"].cumsum()
ltv = (df.groupby(["abgroup", "day_after_install"])["revenue"].sum()
.div(df.groupby("abgroup")["event_user"].nunique(), level="abgroup")
.reset_index(name="ltv_per_day"))
# Кумулятивный LTV(60)
ltv["cum_ltv"] = ltv.groupby("abgroup")["ltv_per_day"].cumsum()Если в B cum_ltv@60 ≥ cum_ltv@60_A → нетто-эффект положительный или нейтральный, гипотеза 2 подтверждается.
Метрика 3. Attempts (engagement)
attempts = df.groupby(["abgroup", "day_after_install"])["attempts"].mean().reset_index()Если в B больше attempts на каждый день → engagement выше.
Стат-критерии
- Retention(D7), Retention(D30): пропорции → z-test.
- LTV-60 per user: непрерывная, heavy-tailed → bootstrap (нельзя t-test без сильных предположений).
- Attempts per day: можно усреднять до уровня юзера и t-test, или mixed-effects.
# LTV-60 per user
ltv60 = df.groupby(["event_user", "abgroup"])["revenue"].sum().reset_index()
# Bootstrap CI на разность средних
def bootstrap_diff(a, b, n_iter=10000, seed=42):
rng = np.random.default_rng(seed)
diffs = []
for _ in range(n_iter):
a_s = rng.choice(a, len(a), replace=True)
b_s = rng.choice(b, len(b), replace=True)
diffs.append(b_s.mean() - a_s.mean())
return np.array(diffs)
a = ltv60[ltv60["abgroup"] == "A"]["revenue"].values
b = ltv60[ltv60["abgroup"] == "B"]["revenue"].values
diffs = bootstrap_diff(a, b)
print(f"Mean diff: {diffs.mean():.4f}")
print(f"95% CI: [{np.percentile(diffs, 2.5):.4f}, {np.percentile(diffs, 97.5):.4f}]")
print(f"P(B > A) = {(diffs > 0).mean():.3f}")Анализ / интерпретация
Возможные сценарии и рекомендации:
| Retention | LTV-60 | Решение |
|---|---|---|
| B выше | B выше | Раскатывать B. Win-win. |
| B выше | B = A | Раскатывать B (long-term retention важнее). |
| B выше | B ниже | Сложно. Нужен LTV-180/365 — может быть, retention сдвинет монетизацию дальше. Если бизнесу важна окупаемость — может потребоваться доп. эксперимент. |
| B = A | B ниже | Не раскатывать. Гипотеза не сработала. |
| B ниже | B ниже | Не раскатывать. |
Фактически, наиболее вероятный исход — B даёт лучший retention и сравнимый/лучший LTV-60, потому что начальные уровни при сильной фрустрации больше теряют, чем приносят (большинство «застрявших» уходят, а не покупают).
Дополнительные данные / метрики
- % юзеров, прошедших уровень N для всех N. Покажет, где именно «бутылочное горлышко» в обеих кривых.
- Time on level N — сколько раз пробуют, сколько времени тратят. Связь с фрустрацией.
- Spent currency per level — на каких уровнях покупают буст.
- Конверсия в первую покупку (CR to first IAP) — главный leading indicator монетизации.
- Сегмент платящих vs нет: эффект может быть только на платящих или только на не-платящих.
- UA-источник: качество трафика влияет, иногда эффект только на organic.
- D90, D180 retention (если можно ждать): 60 дней мало для match-3, у которой LTV-кривая длинная.
- Distribution of revenue per user: 60-day LTV — это среднее, но в gamedev доход распределён по power law. Median revenue — отличный sanity-check.
- Cancellation после первого buy — если в B «сделал одну покупку и больше не возвращается», это плохой сигнал.
Подводные камни
- Только конверсия первой покупки. «B даёт больше первых покупок» — не значит больше LTV-60. Считайте LTV.
- Среднее vs медиана. В gamedev 1% китов даёт 50% дохода. Среднее по 1000 юзеров может полностью определяться 1 китом. Используйте bootstrap или trimmed mean.
- Sample Ratio Mismatch. Особенно если test — это разная игровая экономика; различные клиенты могут по-разному обновляться.
- Sample selection. Тест на новых юзерах — но «новые» через 60 дней могут отличаться (изменился UA, сезон). Сравните только когорты в одном временнóм окне.
- Незрелые юзеры в выборке: те, кто установился в последний день, имеют только 1 день истории. Анализ LTV-60 — только для тех, у кого есть 60 дней.
- Игнорирование «нулевых» юзеров. Игрок, не открывший игру после установки, обычно не попадает в
ab_data.csv. Если вab_users.csvони есть — это 0% retention для них; учитывайте. - Day 0 vs Day 1. «retention D1» — кто вернулся следующий календарный день, а не «через 24 часа». Tz важен.
- «Установка = попадание в тест». Если разнесено — нужно фильтровать тех, у кого
join_date != install_date(но в задаче это совпадает).
Эталонный ответ
Структура отчёта:
- Sanity check групп (размер, distribution UA-каналов, гео).
- Retention curve A vs B → ключевой график.
- Cumulative LTV A vs B → decision-метрика.
- Attempts per day A vs B → engagement.
- Bootstrap CI на разность средних LTV-60.
- Рекомендация с условиями.
Дополнительно хотим посмотреть: % прошедших уровень N, time on level, conversion to first IAP, D90/D180 retention, медианный revenue, segmentation по UA.
Главное: A/B с двумя одновременными изменениями (уменьшили старт + усложнили дальше) — нельзя вычленить эффект каждого по отдельности. Нужен либо 4-арм тест (A, A+easy_start, A+hard_late, A+both), либо последовательные A/B.