Условие
Абстрактный казуальный Match-3 проект (как Candy Crush Saga). Карта с цепочкой уровней.
Проблема: retention rate с D1 по D6 — нормальный (плавно угасает: 40 / 30 / 26 / 24 / 22 / 20%). На D7 резкий обвал до 15%. Хорошему ретеншну свойственно плавное угасание; резкое падение сигнализирует о продуктовых проблемах.
С технической стороны всё гладко. Логично предположить, что есть проблемы с уровнями (например, со сложностью или интересностью).
Задача: придумать стратегию выдвижения гипотез, какие номера уровней проблемные, начиная с какого номера резко проседает retention rate.
Вы не ограничены в инструментах — можете добавить любые события на client/server side в неагрегированную аналитику (a-la ClickHouse) и строить любые агрегации.
Решение
Подход
Главная идея: связать «момент ухода» юзера с уровнем, на котором он находился, и найти уровни-аномалии.
Сразу несколько важных гипотез:
- «Уровень-стенка»: один или несколько уровней слишком сложные → юзер пытается, не проходит, бросает.
- «Уровень-скука»: уровень слишком лёгкий или однотипный → потеря интереса.
- «Уровень-paywall»: после уровня N экономика игры (жизни / ходы) меняется так, что без покупки нельзя продвигаться.
- «День 7» — это не «уровень N», а время-ориентированная проблема: например, на день 7 кончаются daily-rewards / интро-бонус.
Шаг 1. Какие события логировать
Если можем добавить события — нужны:
| Событие | Параметры | Зачем |
|---|---|---|
level_started |
level_id, attempt_number, lives_left, moves_available, boosters_owned, ts | старт уровня |
level_failed |
level_id, attempt_number, moves_used, score, reason | поражение |
level_completed |
level_id, attempt_number, moves_used, score, stars_earned | победа |
level_quit |
level_id, attempt_number, time_in_level, ts | юзер вышел из уровня |
app_close |
last_screen, last_level_id, ts | момент закрытия игры |
purchase_offered |
offer_id, level_id, context | паyoll-предложение |
purchase_completed / declined |
— | конверсия в покупку |
tutorial_step |
step_id | для воронки туториала |
daily_reward_claimed |
day, reward_value | связь с D7 |
chapter_completed |
chapter_id, level_id | прогресс по главам |
Шаг 2. Гипотеза «уровень-стенка»
Метрики:
- CR (level pass rate) =
level_completed / level_startedдля каждого уровня. Аномально низкий = «стенка». - Average attempts per level =
level_started / level_completed_unique_users. > 5 = слишком много попыток. - Drop-off after level N =
% юзеров, никогда не вернувшихся после level_started(N). > 30% = стенка.
-- Pass rate по уровням (за выбранный период)
SELECT
level_id,
COUNT(*) AS total_starts,
COUNT(DISTINCT user_id) AS unique_starters,
COUNT(*) FILTER (WHERE event = 'level_completed') AS completes,
COUNT(*) FILTER (WHERE event = 'level_completed') * 1.0
/ COUNT(*) FILTER (WHERE event = 'level_started') AS pass_rate
FROM level_events
GROUP BY level_id
ORDER BY level_id;И визуализация: график pass_rate по level_id с подсветкой «дропов» (где pass_rate резко падает относительно соседей).
Шаг 3. Гипотеза «связь дня 7 с конкретным уровнем»
Самое прямое: на каком уровне находится «средний» юзер на день 7?
-- Уровень, на котором стоит юзер в момент ухода
WITH last_level AS (
SELECT
user_id,
MAX(level_id) AS last_level,
DATE(MAX(ts)) - DATE(MIN(ts)) AS days_to_quit
FROM level_events
WHERE event IN ('level_started', 'level_failed', 'level_completed')
GROUP BY user_id
)
SELECT
days_to_quit,
AVG(last_level) AS avg_last_level,
MEDIAN(last_level) AS median_last_level
FROM last_level
GROUP BY days_to_quit
ORDER BY days_to_quit;Если на дне 7 медианный «последний уровень» — это уровень N, наиболее вероятно, проблема — на N или близких уровнях.
Шаг 4. Гипотеза «уровень-paywall»
SELECT
level_id,
COUNT(DISTINCT CASE WHEN event = 'purchase_offered' THEN user_id END) AS offered,
COUNT(DISTINCT CASE WHEN event = 'purchase_completed' THEN user_id END) AS bought,
COUNT(DISTINCT CASE WHEN event = 'purchase_declined' THEN user_id END) AS declined,
COUNT(DISTINCT CASE WHEN event = 'app_close'
AND last_screen = 'paywall' THEN user_id END) AS quit_at_paywall
FROM events
GROUP BY level_id;Уровень с высоким quit_at_paywall — проблемный paywall.
Шаг 5. Гипотеза «не уровни, а время»
Возможные не-уровневые причины обвала на день 7:
- End of intro-promo — закончились бесплатные бустеры новичка → стало сложнее, юзер ушёл.
- First рекламный push — слишком навязчивая на день 7.
- Daily-reward chain reset — cycle ребута бонусов даёт «нечего делать».
- Жизневременной gate: на день 7 у юзера набралось много прогресса и жизней «не хватает».
Для проверки — построить timeline пользователя на день 7 и найти типовую причину закрытия:
SELECT
EXTRACT(EPOCH FROM (last_event_ts - first_event_ts)) / 3600 / 24 AS days_to_quit,
last_screen,
COUNT(*)
FROM (
SELECT user_id,
MIN(ts) AS first_event_ts,
MAX(ts) AS last_event_ts,
LAST_VALUE(screen) OVER (PARTITION BY user_id ORDER BY ts
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_screen
FROM events
GROUP BY user_id
) t
GROUP BY days_to_quit, last_screen
ORDER BY days_to_quit, COUNT(*) DESC;Шаг 6. Визуализации
- Pass rate vs level_id (line + scatter) — найти «уровни-стенки».
- Distribution последнего уровня по days_to_quit (boxplot) — на каком уровне «застревают» в день 7.
- Cohort retention — посмотреть, не зависит ли паттерн от UA-канала / гео.
- Timeline event_count per day — может, юзеры просто играют меньше на день 7 (вечером пятницы устают?).
Анализ / интерпретация
Шаги действий:
- Сначала добавить события для уровневой телеметрии.
- Построить «pass rate per level» — найти 1–3 «уровня-стенки» с резко низким CR.
- Построить «median last level on day 7» — если совпадает с уровнем-стенкой — диагноз ясен.
- Если нет — переходить к гипотезе «time-based» и проверять paywall, daily-reward, ad-load.
Подводные камни
- Не путать «retention» с «day-X retention». Retention D7 = «вернулся ли юзер на 8-й день»? Если резко упал — это потеря на D7. Если на D6 — потеря между D6 и D7.
- Ловушка усреднения: «средний юзер на дне 7 на уровне 20» — но среди них могут быть те, кто на уровне 5 (плохой) и те, кто на 35 (хороший). Используйте distribution, а не среднее.
- Survivor bias. На день 7 остались «лучшие» — для них «уровень-стенка» может быть на 30, а для среднего юзера — на 15.
- Cohort effect: разные UA-каналы дают разные retention-кривые. Анализируйте сегментно.
- Корреляция ≠ причина. Юзер «уходит на уровне 17» может потому, что 17-й сложный, а может потому, что 16-й вечно показывал ad. Нужны explicit события
app_closeс контекстом. - «15% retention D7» — мало это или много? Бенчмарк в casual для match-3 — 20-25%. То есть проблема реальная, +5 пп — это огромный эффект.
- Время логирования событий. Если события агрегируются раз в час, можно потерять «момент ухода». Нужны realtime-события.
Альтернативы
- A/B-тест с упрощением «уровня-стенки»: если кривая retention перестала просаживаться → диагноз верный, можно раскатывать.
- Funnel analysis по уровням в Amplitude / Mixpanel — дешёвая альтернатива кастомным запросам.
Эталонный ответ
Шаги стратегии:
- Логи:
level_started/failed/completed/quit,app_close (с контекстом),purchase_offered/completed/declined,daily_reward_claimed. - Метрики: pass rate per level (главный график), avg attempts, drop-off rate, median last_level by days_to_quit.
- Гипотезы в порядке проверки: уровни-стенки → paywall → time-based (ивенты, daily, ad) → UI-баг.
- Решающий артефакт: график pass_rate vs level с обведёнными «уровнями-стенками» + распределение «последнего уровня» по days_to_quit.
Главное — измеримая декомпозиция: связать день обвала с конкретными уровнями через медиану last_level_id на дне 7, и проверить pass rate каждого уровня для подтверждения.