Собесов

World of Tanks — анализ A/B-теста изменений в игровых настройках

Кейсы и метрикиАнализ A/B и отчётностьСложнаяMiddle

Условие

В файле data_1.csv — данные о боях двух групп игроков, разделённых по доступности изменений в настройках/конфигурации:

  • Group A — изменения доступны с 2022-10-27 по 2022-11-29.
  • Group B — контрольная, изменения не доступны.

Период данных: 2022-10-012022-11-29 (т.е. ~4 недели до изменений и 5 недель после).

Поля:

  • player_id, battle_id, dt — день боя;
  • player_group (A или B, не меняется в течение периода);
  • in_battle_presence_time — секунды в бою;
  • damage_dealt — урон;
  • kills_made — уничтоженные танки;
  • vehicle_lvl — уровень техники;
  • account_created_at — дата создания аккаунта.

Задача: проанализируйте перформанс изменений и выводы для бизнеса.

Решение

Подход

Это A/B-тест с pre-period (4 недели «до» + 5 недель «после»). Идеальная ситуация для:

  1. Проверки эквивалентности групп в pre-period (sanity check).
  2. DiD (difference-in-differences) для оценки эффекта.
  3. Сегментного анализа (разные техники, разные «возрасты» аккаунтов реагируют по-разному).

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

Шаг 1. Sanity-check групп

import pandas as pd
import numpy as np
from scipy import stats
 
df = pd.read_csv("data_1.csv", parse_dates=["dt", "account_created_at"])
df["period"] = np.where(df["dt"] >= "2022-10-27", "post", "pre")
 
# Размеры групп
print(df.groupby("player_group")["player_id"].nunique())
 
# Распределение vehicle_lvl, account_age в группах в pre-period
pre = df[df["period"] == "pre"]
ks = stats.ks_2samp(
    pre[pre["player_group"] == "A"]["vehicle_lvl"],
    pre[pre["player_group"] == "B"]["vehicle_lvl"]
)
print(f"KS for vehicle_lvl: p = {ks.pvalue:.3f}")

Если в pre-period группы значимо разные — нельзя просто сравнивать «А vs Б после изменений», нужны DiD или CUPED.

Шаг 2. Ключевые метрики

Метрика Что показывает
Battles per player per day engagement
Damage per battle core gameplay performance
Kills per battle агрессивность игры
In-battle time per session продолжительность вовлечения
DAU retention
Win rate (если есть) баланс игры

Шаг 3. Difference-in-differences

metric = "damage_dealt"
# Усредняем по игроку и периоду
agg = (df.groupby(["player_id", "player_group", "period"])
         [metric].mean()
         .reset_index())
 
# Pivot: pre / post
wide = agg.pivot_table(index=["player_id", "player_group"],
                       columns="period", values=metric).dropna().reset_index()
wide["delta"] = wide["post"] - wide["pre"]
 
# Сравниваем delta между группами
deltaA = wide[wide["player_group"] == "A"]["delta"]
deltaB = wide[wide["player_group"] == "B"]["delta"]
t_stat, p = stats.ttest_ind(deltaA, deltaB, equal_var=False)
print(f"DiD effect on {metric}: A={deltaA.mean():.2f}, B={deltaB.mean():.2f}, p={p:.4f}")

DiD устраняет систематические различия между группами и общий тренд во времени.

Шаг 4. Сегментный анализ

Эффект может различаться для:

  • Новых vs опытных игроков (account_age = dt - account_created_at).
  • Разных уровней техники (low-tier vs mid-tier vs top-tier).
  • «Тяжёлых» vs «лёгких» игроков (по числу боёв в pre-period).
df["account_age_days"] = (df["dt"] - df["account_created_at"]).dt.days
df["account_seg"] = pd.cut(df["account_age_days"],
                           bins=[-1, 30, 365, np.inf],
                           labels=["new", "active", "veteran"])
 
# Эффект по сегментам
for seg in df["account_seg"].cat.categories:
    sub = df[df["account_seg"] == seg]
    # ... повторить DiD ...

Шаг 5. Визуализация

  • Time-series по дням, две линии (A vs B), вертикальная линия — момент включения изменений. Видно, отскочила ли А вверх после 2022-10-27.
  • Распределения «delta» по игрокам в каждой группе — гистограмма / boxplot.
  • Heatmap по сегментам: строки — сегменты, столбцы — метрики, ячейки — лифт A vs B.

Анализ / интерпретация

В отчёте для бизнеса:

Изменения повысили урон за бой на X% в группе A статзначимо (p = 0.001, 95% CI [Y; Z]). Эффект концентрирован в сегменте «активных» игроков (1 мес — 1 год) — у них рост Q%, у новых и ветеранов рост незначим. Drop-off по DAU не зафиксирован (гард). Рекомендация: раскатывать на 100% игроков с дополнительным мониторингом сегмента «новых» — для них эффект незначим, и важно убедиться, что фича не ухудшит их опыт.

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

  1. Сравнивать post-period напрямую без проверки pre-эквивалентности — частая ошибка. Используйте DiD или CUPED.
  2. Многократное тестирование. На 5+ метриках с уровнем α=0.05 можно получить ложные срабатывания. Поправка Бонферрони / BH.
  3. Корреляция внутри игрока. Один игрок участвует в десятках боёв → нужно усреднять до уровня игрока перед t-test, иначе nominal p-value занижен.
  4. Сегмент «новых» игроков. В pre-period они могли только зарегистрироваться → данных мало, дисперсия большая.
  5. Survivor bias. В post-period могут отсутствовать игроки, ушедшие из игры до изменений — это часть эффекта (или нет?).
  6. Выбросы. «Bot detection» через очень высокие damage / battles per day. Винзоризация по 99%-перцентилю или фильтр аномалий.
  7. Дисбаланс типов техники. Если в группе A случайно больше игроков на low-tier, средний урон будет ниже — нужен страт или контроль на vehicle_lvl.

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

  • CUPED с pre-period как ковариантой — снижает дисперсию эффекта.
  • Bootstrap на уровне игрока для непараметрических CI.
  • Mixed-effects model (lme4 / pymer4) — учитывает random effect игрока.

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

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

  1. Sanity-check групп в pre-period.
  2. DiD по 4–5 ключевым метрикам (battles per player, damage per battle, kills, time-in-battle, DAU).
  3. Сегментный анализ (новые / активные / ветераны, разные tier).
  4. Визуализации: time-series A vs B с маркером запуска, гистограмма deltas, heatmap по сегментам.
  5. Бизнес-вывод с рекомендацией (раскатать / не раскатать / докрутить) и условиями (мониторинг сегмента X, гарды).

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

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

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