Собесов

Fenomen Games — A/B-тест: восстановление жизни 30 минут vs 3 минуты

Кейсы и метрикиA/B в gamedev и анализ метрикСложнаяMiddle

Условие

В проекте Fancy Blast (Match-3) проведён A/B-тест:

  • Когорта A — контрольная.
  • Когорта B — время восстановления одной жизни уменьшено в 10 раз30 мин до 3 мин).

Данные: до 7 дней жизни пользователя (день установки = day 0):

  • Retention — день жизни пользователя;
  • MaxLevelPassed — макс. пройденный уровень;
  • User_id, AB_Cohort;
  • SumRevenue, CountBuy;
  • CountAllStart / CountAllFinish — старты и победы на уровнях;
  • CountCleanStart / CountCleanFinish — без помощи (без буста, ходов, бонусов);
  • Get_* — золото из разных источников (Ads, Chapter, Buy, Faceb, TeamL, TeamT);
  • Spend_* — траты золота (BonLives, Bonus, Boost, Lives, Moves, TeamC).

Задача: проанализировать, как изменение времени восстановления жизней повлияло на метрики проекта.

Решение

Подход

Уменьшение времени восстановления жизней — фундаментальное изменение gating-механики F2P игры. Жизни — это и retention-tool (заставляет вернуться в игру), и monetization-tool (купи жизни / буст). Уменьшение в 10 раз должно дать:

Положительные эффекты (ожидаемо):

  • Sessions per DAU вырастут — юзер не «уходит на 30 мин».
  • Time-in-app per session вырастет / number of sessions per day вырастет.
  • Прогрессия по уровням ускорится (CountAllStart, MaxLevelPassed).
  • Возможно, retention вырастет (проще вернуться, проще играть).

Отрицательные эффекты (риск):

  • Spend_Lives упадёт — никто не купит жизни, если они почти бесплатные.
  • Revenue в нижней категории платежей просядет.
  • MaxLevelPassed может вырасти, и юзеры пройдут весь контент быстрее → выгорят раньше.
  • Sessions per day может вырасти, но сессии стать короче (микросессии вместо «длинных»).

Реализация — план анализа

import pandas as pd
import numpy as np
from scipy import stats
 
df = pd.read_excel("TZ_data.xlsx", sheet_name="Лист1")
 
# 1. Структура: одна строка на (user, day). Считаем сводные на уровне юзера.
agg_user = df.groupby(["User_id", "AB_Cohort"]).agg(
    days_active        = ("Retention",        "count"),
    max_level          = ("MaxLevelPassed",   "max"),
    total_revenue      = ("SumRevenue",       "sum"),
    n_purchases        = ("CountBuy",         "sum"),
    starts             = ("CountAllStart",    "sum"),
    finishes           = ("CountAllFinish",   "sum"),
    clean_starts       = ("CountCleanStart",  "sum"),
    clean_finishes     = ("CountCleanFinish", "sum"),
    spent_lives_gold   = ("Spend_Lives",      "sum"),
    spent_moves_gold   = ("Spend_Moves",      "sum"),
    spent_boost_gold   = ("Spend_Boost",      "sum"),
    earned_ads         = ("Get_Ads",          "sum"),
).reset_index()
 
agg_user["completion_rate"]      = agg_user["finishes"]      / agg_user["starts"].replace(0, np.nan)
agg_user["clean_completion_rate"] = agg_user["clean_finishes"] / agg_user["clean_starts"].replace(0, np.nan)
agg_user["arpu"]                  = agg_user["total_revenue"]

Ключевые метрики и сравнение групп

def compare(metric, df=agg_user):
    a = df.query("AB_Cohort == 'A'")[metric].dropna()
    b = df.query("AB_Cohort == 'B'")[metric].dropna()
    # Welch t-test (для бинарных метрик — z-test/chi2)
    t, p = stats.ttest_ind(a, b, equal_var=False)
    return {
        "A": a.mean(), "B": b.mean(),
        "lift_pct": (b.mean() / a.mean() - 1) * 100 if a.mean() else None,
        "p_value": p
    }
 
metrics = ["days_active", "max_level", "total_revenue", "n_purchases",
           "starts", "completion_rate", "spent_lives_gold", "spent_moves_gold",
           "spent_boost_gold", "earned_ads"]
 
results = {m: compare(m) for m in metrics}
print(pd.DataFrame(results).T)

Анализ / интерпретация — типовой результат

Метрика Ожидание Возможный реальный эффект
days_active (retention) вырастет в B +5–10% (положительно)
max_level вырастет в B +20–30% (быстрее проходят)
starts (попыток) сильно вырастет в B +50–100% (можно играть всегда)
completion_rate не должна меняться стабильна
total_revenue спорно возможна просадка на 10–20%
n_purchases спорно просадка из-за отсутствия покупки жизней
spent_lives_gold резко упадёт -80% (главный негатив)
spent_moves_gold вырастет +10–20% (играют чаще, чаще тратят на ходы)
earned_ads вырастет +20% (больше сессий → больше реклам)

Возможный итоговый вывод:

Сокращение времени восстановления жизней дало положительный эффект на engagement (сессии, уровни, retention), но значительно просадило монетизацию на покупке жизней. Net effect на total revenue — отрицательный (–15%), потому что недополученные деньги от Spend_Lives не компенсируются ростом игроков на других статьях. Рекомендация: не раскатывать прямолинейно. Альтернативы: уменьшить время до 10–15 минут (компромисс), или одновременно повысить cap жизней с 5 до 10 (engagement без потери монетизации), или повысить цены других предметов.

Дополнительный анализ

  1. Cohort retention по дням 0–7 для A и B — линейный график.
  2. Funnel «sessions per day» distribution — как изменилось распределение числа сессий.
  3. Сегментирование по платящим: отдельно платящие и нет. Эффект может быть ассиметричным.
  4. Распределение MaxLevelPassed — гистограмма. Если в B сильно сдвинулась вправо, юзеры скоро столкнутся с «потолком» контента.
  5. Earn vs Spend gold balance — в B может быть инфляция золота из-за простоты.

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

  1. Average revenue в gamedev. Среднее искажается китами — используйте median/trimmed mean или bootstrap.
  2. Нулевые юзеры в выборке: с retention=0 и Max=0 — это инсталлы, которые ни разу не сыграли. Их учитывать или нет — критично для метрики.
  3. Корреляция ретеншна и монетизации. Часто игроки, которые вернулись на день 7, — это «киты» из чарта первых дней. Не путайте «B даёт больше retention → значит, должно быть больше revenue» — пропорция платящих может упасть.
  4. CountClean* vs CountAll*. Clean — без буста, All — со всеми попытками. Сравнивать нужно сопоставимое: CR на clean vs CR на all.
  5. «7 дней» — мало для long-term retention. Эффект может стать ясным только на дне 30+.
  6. Sample Ratio Mismatch. Проверьте, что A и B сопоставимы по числу юзеров, дате установки.
  7. Confounders: если B запустили в другой период (другой UA, другой trafic mix), эффект смешан.
  8. Survivor bias в metric MaxLevelPassed. Юзер, ушедший на дне 1, имеет MaxLevel = MaxLevel дня 1 — навечно. В среднем не отражает «динамику», нужно «MaxLevel на дне X».

Альтернативы

  • CUPED на pre-period не применим (юзеры новые).
  • Difference-in-differences не нужен — A/B на новых.
  • Bootstrap на уровне юзера — для CI на ARPU/LTV.

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

Структура отчёта:

  1. Sanity check: размеры групп, sample-ratio.
  2. Engagement metrics: sessions, days_active, max_level, starts. Ожидаем рост в B.
  3. Monetization metrics: revenue, n_purchases, spent_lives. Ожидаем просадку в B (особенно spent_lives).
  4. Net effect на revenue — главное число для бизнеса.
  5. Сегментный анализ: платящие vs нет.
  6. Рекомендация: компромиссная конфигурация (например, 15 мин вместо 3) или раскатать с одновременным повышением cap жизней.

Главное — не смотреть только на retention: уменьшение жизневременного gate почти всегда улучшает retention и почти всегда ухудшает monetization. Net effect — индивидуален для проекта.

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

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

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