Условие
Пила сложности в match-3 игре — механизм постепенного увеличения уровня сложности, чтобы поддерживать интерес и вызов у игрока. Лёгкие уровни дают чувство уверенности, сложные — чувство вызова.
В файле saw.csv колонки:
SawStep— шаг пилы (номер уровня в порядке появления у игрока).cry_per_user— сколько хард-валюты («cry») в среднем тратит игрок на этом шаге.churn— процент игроков, ушедших на этом шаге (от тех, кто его начал).LRC— Level Retry Count в виде доли поражений от всех стартов на шаге (то есть LRC =losses / starts, мера сложности).
Задание: проанализировать пилу с точки зрения баланса хардвалюты и оттока.
Решение
Подход
«Баланс пилы» в game analytics — это поиск шагов, на которых:
- слишком высокий churn (игрок уходит) — обычно из-за слишком сложного уровня (
LRCвысок); - или слишком высокий расход хардвалюты на бустеры/доп-ходы (
cry_per_userвысок), что либо «съедает» пользователю free-currency запасы и провоцирует уход, либо принуждает его платить.
Цель — найти шаги-«киллеры» (где пользователи или платят слишком много, или уходят) и шаги-«выпадения» (где сложность резко падает и пользователь скучает).
Шаг 1. Базовая EDA
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("saw.csv").sort_values("SawStep").reset_index(drop=True)
# Воронка прохождения (сколько игроков доходит до шага N)
df["survival"] = (1 - df["churn"]).cumprod()
# Хардвалюта в расчёте на «дошедшего»
df["cry_per_arriving_user"] = df["cry_per_user"]
# Общий «слив» хардвалюты со всех игроков, дошедших до шага
df["cry_total"] = df["survival"] * df["cry_per_user"]
print(df.head())Графики, которые надо построить:
- Кривая выживаемости
survival(SawStep)— экспоненциальный спад? Или есть «обрывы»? churnпо шагам — где локальные пики? Они и есть проблемные уровни.LRCvschurn— корреляция: еслиLRCвысокий, игрок чаще проигрывает → больше шансов уйти.cry_per_userпо шагам — где пики расхода хардвалюты?
Шаг 2. Идентификация проблемных шагов
Простые правила-эвристики:
- «Стенка»: шаги с
churn > P95(churn)илиLRC > P95(LRC). Это места, где пользователи массово упираются. - «Дренаж»:
cry_per_user > P90(cry_per_user). Слишком дорогая страница: игроки тратят все накопления. - «Болото»: подряд несколько шагов с
LRC ≈ 0.05–0.10(слишком легко) — пользователь скучает, churn растёт без видимой сложности.
df["is_wall"] = df["churn"] > df["churn"].quantile(0.95)
df["is_drain"] = df["cry_per_user"] > df["cry_per_user"].quantile(0.90)
df["is_boring"] = df["LRC"].rolling(5).mean() < 0.10
print(df[df.is_wall | df.is_drain].sort_values("SawStep"))Шаг 3. Связь сложности и оттока
Если LRC и churn сильно коррелируют, гипотеза «слишком сложно — пользователи уходят» подтверждается. Но коррелировать может и cry_per_user:
- Если игрок не имеет хардвалюты для бустера, он чаще проигрывает →
LRCрастёт → уходит. - Если бустер слишком дешёвый, игрок «прожигает» хардвалюту и потом уходит, когда нет денег на следующий сложный уровень.
print(df[["LRC", "churn", "cry_per_user"]].corr())Хорошо использовать кросс-табы по группам шагов (1–10, 11–20 и т.д.): в начале игры LRC обычно низкий, в конце — высокий; нужно смотреть аномалии относительно ожидаемого среднего по группе.
Шаг 4. Скоринг шагов
Для приоритизации фиксов даём каждому шагу составной балл «проблемности»:
import numpy as np
z = lambda s: (s - s.mean()) / s.std()
df["impact_score"] = (
1.0 * z(df["churn"])
+ 0.5 * z(df["LRC"])
+ 0.5 * z(df["cry_per_user"])
)
df["arrived_share"] = df["survival"] # сколько % всех новичков туда доходит
df["business_score"] = df["impact_score"] * df["arrived_share"]
print(df.nlargest(10, "business_score")[["SawStep", "churn", "LRC", "cry_per_user", "business_score"]])Идея: если шаг 200 «убивает» 30% игроков, но до него доходит 0.5% — фикс не нужен. Если шаг 25 убивает 8%, но доходит 80% — это приоритет №1.
Шаг 5. Выводы и рекомендации
Структура отчёта:
- Состояние пилы: общая выживаемость, средняя сложность, расход cry на пройденного игрока.
- Топ-5 шагов-«стенок» с
LRC > Xи высокимchurn— снизить сложность (меньше препятствий, больше ходов). - Топ-5 шагов-«дренажей» — пересмотреть стоимость бустеров или сделать уровни менее зависимыми от них.
- «Болота» — добавить разнообразия / увеличить challenge.
- Приоритизация по
business_score— какие фиксы дадут наибольший прирост retention.
Подводные камни
- Симпсон в зеркале сезонности. Расход cry в среднем по шагу мог расти из-за акций, а не из-за сложности. Нужно фильтровать когорту (один период, одна версия игры).
- Survivorship bias. На дальних шагах остаются только хардкорные игроки — у них
cry_per_userниже иLRCвыше. Сравнивать средние «по всем» нельзя. - Ошибки определения churn.
churnна шаге = не «закончил играть в игру», а «не сделал следующий шаг в течение N дней». Указывайте окно явно. - Корреляция ≠ причинность. Высокий
cry_per_userможет быть следствием того, что игрок платит, чтобы пройти, а не причиной оттока. Для каузального анализа нужен A/B (см. вторую часть задания). - Метрика LRC искажается при коротких сессиях. Если игрок попробовал шаг один раз и ушёл,
LRC = 1.0— это не сложность, а «не успел разобраться». Срезайте «попытки=1, время<10s» как outliers. - Хардвалюта vs реальные деньги. «cry_per_user» сама по себе ничего не говорит про монетизацию — нужно сопоставлять с тем, какая доля cry куплена за деньги (
paid_cry_share).
Эталонный ответ (структура отчёта)
- Общая картина: график выживаемости, средние LRC/churn/cry по интервалам шагов.
- Топ проблемных шагов по
business_score=(z(churn) + 0.5·z(LRC) + 0.5·z(cry))·survival. - Категоризация проблем: стенка / дренаж / болото.
- Гипотезы фиксов для каждой категории.
- Связь со второй частью задания: предложить A/B-тест сильной гипотезы (см. отдельную задачу
awem-difficulty-curve-ab-test).
Главное на собесе — продемонстрировать декомпозицию «отток по причинам» и приоритизацию по бизнес-влиянию, а не «нарисовать красивые графики».