Продуктовый аналитик: Путешествие туда и обратно
Глава 2. Кузница оружия
2.4. Магический Посох: Python для сложных аналитических заклинаний
Наставник даёт вам первые инструменты. Здесь, в грохоте ETL-процессов и мерцании строк кода, вы куете своё оружие для будущих битв в загадочном и прекрасном мире аналитики больших данных.
- Основы Python
- Pandas
- Pivot table
- DuckDB (SQL в Python)
Python для анализа данных
В игровой аналитике Python давно стал стандартом. Но Python победил не потому, что он самый быстрый или самый мощный.
- Python самый удобный для исследователя данных, он заметно проще, например, языка для статистики R.
- В Python есть библиотеки под любую задачу аналитика: Pandas для таблиц, PySpark для Big Data, Scikit-learn для машинного обучения, TensorFlow и PyTorch для нейронных сетей и много других. И все это может работать вместе, единым стеком без швов! Python – настоящий «Магический Посох», которым вы можете овладеть.
- У Python огромное сообщество. И это выросло как снежный ком - чем больше людей используют Python, тем больше экспертов, библиотек, книг, вакансий.
- В одной странице Jupyter Notebook вы можете писать код кусками (ячейками) с комментариями, тут же видеть результат, корректировать и проверить вновь, вставлять графики, поделиться с коллегами аналитиками или передать свой код инженерам данных для встраивания в пайплайн (завернуть в Airflow DAG и запускать по расписанию).
2.4.1. Python: как начать
Для новичка ответ однозначен – установите себе на компьютер Anaconda. Для этого заходите на anaconda.com, скачиваете дистрибутив и устанавливаете (для учебных целей это бесплатно). Но для коммерческого использования в некоторых компаниях могут потребоваться лицензии, а на этапе обучения это лучший старт. После установки запустите Jupyter Lab.
Создайте новый ноутбук: File → New → Notebook.
-Ячейки бывают двух типов: Code (код) и Markdown (текст, заголовки, формулы).
-Shift + Enter — выполнить ячейку и перейти к следующей.
-Символ # экранирует строку после символа от запуска; можно добавить комментарии или выключить строку кода.
Мы будем использовать Python для работы с данными. Чтобы не перегружать статью, я пропускаю разбор операторов, циклов, функций, классов и наследования. Это все важная база, но её лучше изучать отдельно и фундаментальным курсом. Мы же сфокусируемся на прикладном анализе. Далее будут примеры «аналитической магии», обязательно попробуйте каждое «заклинание» в своем Jupyter Notebook.
Переменные и типы
player_id = 1234567 - int или целое число
player_name = “DarkKiller99” - str или строка (текст)
is_payer = True - bool или булевый оператор (Правда/Ложь)
session_duration = 125.5 – float или число с дробной частью
registration_date = “2026-02-15” - str, строка, а должна быть дата, но только пока
Коллекции данных
Список (List) для хранения последовательностей.
Например, список покупок игрока:
purchases = [100, 250, 99, 500]
Сумма всех элементов списка
sum(purchases) - в ответе будет 949
Длина списка
len(purchases) - в ответе будет 4
Словарь (Dict) - для хранения пар «ключ - значение». Вызываете «ключ» и получаете «его значение». Идеально для профиля игрока, например:
player_profile = {
"player_id": 1234567,
"level": 42,
"class": "Mage",
"guild": "FireDragons",
"last_payment": 500
}
Запрос значения для ключа guild
print(player_profile["guild"]) - в ответе будет FireDragons
List Comprehension
Допустим, у нас есть список покупок, и мы хотим оставить только те, что больше 200 рублей.
Сформируем новый список по этому фильтру:
purchases = [100, 250, 99, 500, 150, 300] – весь список
big_purchases = [x for x in purchases if x > 200] – формируем новый список
print(big_purchases) - в ответе получим новый список [250, 500, 300]
2.4.2. Pandas
Библиотека Pandas уже установлена в Anaconda (также там есть традиционные NumPy, Matplotlib). Pandas очень популярен для работы с табличными данными и умеет работать с таблицами (DataFrame).
Для использования в тетрадке уже установленных библиотек их необходимо вызвать. Импортируем Pandas в нашу текущую тетрадку Notebook.
import pandas as pd
Представим, что у нас есть CSV с логами событий в той же директории, где и сохранили наш файл-тетрадку (иначе будет необходимо указать путь к файлу).
CSV (Comma-Separated Values) это простой текстовый формат для хранения табличных данных, где каждая строка файла соответствует строке таблицы, а значения разделяются запятыми, точками с запятой или табуляцией.
После того как мы импортировали Pandas и назвали его коротко pd мы можем обращаться к его функциям как pd.функция Pandas. Например, создаем DataFrame на основе нашего CSV. Обратите внимание, что вызов функции Pandas делается с указанием на библиотеку, впереди пишем имя библиотеки в сокращенном формате (при импорте в начале мы дали ей имя pd).
df = pd.read_csv('game_logs_test1.csv')
У этой функции много аргументов, и полное описание этой функции (как и других) можно посмотреть в официальной документации Pandas. (https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html)
Когда вы получаете новый датасет, всегда начинайте с разведки вашей новой таблицы (DataFrame).
df.head() - первые 5 строк (5 по умолчанию) таблицы
df.info() - информация о типах колонок и количестве непустых значений
df.describe() - основная статистика по числовым колонкам
df.isnull().sum() – тут впервые применяем агрегирование для подсчета сколько пропусков в каждой колонке. Обратите внимание, что функции применяются последовательно слева-направо, одна за одной.
Базовые операции
Первые 4 команды вам уже знакомы. Остальные команды вы будете использовать также регулярно.
| Задача | Код (Pandas) |
|---|---|
| Первые N строк | df.head(N) |
| Информация о таблице | df.info() |
| Основная статистика по числовым колонкам | df.describe() |
| Сколько пропусков в каждой колонке | df.isnull().sum() |
| Уникальные значения в колонке | df['col'].unique() |
| Фильтр строк | df[df['col'] > 100] |
| Группировка и сумма | df.groupby('col').agg({'val': 'sum'}) |
| Создание столбца | df['new'] = df['a'] + df['b'] |
| Преобразовать в формат даты | pd.to_datetime(df['date']) |
| Сохранить в CSV | df.to_csv('out.csv', index=False) |
| Ниже мы рассмотрим эти и другие функции немного подробнее. |
Выбрать столбцы и/или строки таблицы
Важно, что у Python нумерация начинается с нулевой строки.
Выбрать один столбец
df['player_id']
Выбрать по индексам строк и названиям столбцов
df.loc[0:5, ['player_id', 'amount']]
Выбрать строки (первые 5) и столбцы (1 и 2, а 3 не включается)
df.iloc[0:5, 1:3]
Выбрать уникальные значения
Какие страны есть в колонке стран
df['country'].unique()
Сколько уникальных идентификаторов игроков
df['player_id'].nunique()
Топ-10 стран по количеству игроков (две функции последовательно)
df['country'].value_counts().head(10)
Фильтрация по строкам как WHERE в SQL
Выбрать строки, где платеж больше нуля (платящие игроки)
df[df['spent'] > 0]
Выбрать строки по двум условиям: игроки из России, которые потратили больше 500
Для сложных условий используйте & (И) и | (ИЛИ), каждое условие обязательно в скобках!
df[(df['country'] == 'RU') & (df['spent'] > 500)]
Выбрать строки, где игроки выше 30 уровня ИЛИ потратившие больше 1000
df[(df['level'] > 30) | (df['spent'] > 1000)]
Работа с датами
Даты в логах обычно приходят как строки. Например, дата выглядит как “2026-02-14 15:30:45” или даже “14.02.2026”. Python видит просто набор символов. А для анализа нам нужно, чтобы Python понимал, что это дата.
Datetime это специальный тип данных, который хранит дату и время не как строку, а как структурированный объект с полями: год, месяц, день, час, минута, секунда.
В Python есть два основных способа работать с датами:
-datetime.datetime из встроенной библиотеки datetime
-timestamp из библиотеки Pandas (надстройка над datetime)
Мы будем использовать Pandas Timestamp, потому что он лучше интегрирован с DataFrame. И самый простой способ сконвертировать это функция pd.to_datetime(). И Pandas понимает разные форматы дат:
pd.to_datetime(['2025-01-15', '2025-01-16']) когда дата в ISO формат
pd.to_datetime(['15/01/2025', '16/01/2025']) когда дата разделена через косую черту
pd.to_datetime(['2025.01.15', '2025.01.16']) когда используем точки вместо тире
pd.to_datetime(['15-Jan-2025', '16-Jan-2025']) когда называем месяц текстом
2.4.3. GROUPBY для группировки и агрегации
Если вы помните прошлую главу про SQL, то и здесь функция похожая - groupby. Но названия агрегирующих функций немного отличаются.
| Функция | Что делает | Пример применения |
|---|---|---|
sum() |
Сумма | df['sales'].sum() - общая сумма |
mean() |
Среднее | df.groupby('category')['price'].mean() - средняя цена по категориям |
median() |
Медианна | df['age'].median() - медианный возраст игроков |
min() |
Минимум | df['date'].min() - самая ранняя дата |
max() |
Максимум | df['revenue'].max() - максимальная выручка |
count() |
Количество непустых строк | df['user_id'].count() - сколько строк с заполненным ID |
size() |
Количество строк (включая пропуски) | df.groupby('country').size() - количество игроков по странам |
nunique() |
Количество уникальных значений | df['player_id'].nunique() - сколько уникальных игроков |
Важно: count() игнорирует пропуски (NaN), а size() считает все строки, включая пропущенные значения. |
Сделаем более сложные агрегации
Применим сразу несколько агрегирующих функций, но сгруппируем по одному столбцу (страна). Для каждой страны посчитаем количество игроков, сумму, среднее и медиану трат, и средний уровень.
df.groupby('country').agg({
'player_id': 'count', - считаем количество игроков
'spent': ['sum', 'mean', 'median'], - три выбранные функции передаем списком
'level': 'mean' - средний уровень
})
А если нам нужно агрегировать по двум столбцам, то передаем эти столбцы списком.
Посчитаем сумму трат по каждой стране и каждому дню недели.
df.groupby(['country', 'weekday'])['spent'].sum()
2.4.4. Merge это как JOIN в SQL
Предположим, у нас есть две таблицы с общей колонкой player_id: таблица игроков (players) и таблица покупки игроков (payments).
Сделаем для них аналог LEFT JOIN: все игроки + их покупки
df = pd.merge(players, payments, on='player_id', how='left')
Сделаем них аналог INNER JOIN: только те, кто покупал
payers_only = pd.merge(players, payments, on='player_id', how='inner')
Как связать таблицы, если ключи (общая колонка) называются по-разному в двух таблицах? Пусть так получилось, что у таблицы players колонка идентификаторов игроков называется id.
df = pd.merge(players, payments, left_on='id', right_on='player_id', how='left')
2.4.5. Сводные таблицы (pivot tables)
В прошлых главах мы говорили про концепцию Tidy Data 2.1. Формат ивентов. Это принцип, согласно которому каждая переменная это отдельный столбец, а каждое наблюдение это отдельная строка. Именно в таком формате данные удобно хранить и обрабатывать. Но для представления результатов, например, в когортном анализе, нам нужно «развернуть» таблицу обратно.
Когорта это группа пользователей, объединённых общим признаком (например, временем регистрации). Строки такой таблицы это когорты, а столбцы это, например, день или месяц после старта жизни когорты. Таким образом, по столбцам мы отслеживаем, как меняется метрика у выбранной когорты с течением времени, а по строкам сравниваем когорты друг с другом.
Сделаем сводную таблицу: сколько игроков когорты возвращается в игру через N дней после регистрации. Пусть у нас есть две таблицы: игроки (players_df содержит ID игроков и даты их регистрации reg_date) и сессии (sessions_df содержит ID игроков и даты всех входов session_date).
1.Определяем когорту (неделя регистрации)
Для каждого игрока определяем когорту (неделя регистрации dt.to_period(‘W’) в виде новой колонки в таблице players_df и называем ее cohort.
players_df['cohort'] = players_df['reg_date'].dt.to_period('W').astype(str)
Колонку неделя регистрации можно превратить обратно в строку .astype(str) для удобства.
2.Объединяем сессии с данными игроков
Объединяем таблицу сессий с таблицей об игроках и создаем новую sessions_with_players (как JOIN в SQL, по общей колонке player_id). Это нужно чтобы добавить к каждой сессии дату регистрации и когорту игрока (используем только 3 колонки из нее).
sessions_with_players = pd.merge(
sessions_df, - эту таблицу будем обогащать датами регистраций и когортами
players_df[['player_id', 'reg_date', 'cohort']], - используем для обогащения
on='player_id', - общая колонка для связки таблиц
how='inner' - выбрали inner join чтобы оставить только сессии зарегистрированных игроков
)
3.Считаем «день жизни» для каждой сессии
В новой таблице sessions_with_players делаем новый столбец day_number, где для каждой сессии считаем день жизни (разница между датой сессии и датой регистрации) и конвертируем разницу в дни. День 0 если день входа совпадает с днем регистрации.
sessions_with_players['day_number'] = (
sessions_with_players['session_date'] - sessions_with_players['reg_date']
).dt.days
4. Создаем сводную таблицу (абсолютные значения)
Пришло время создать сводную таблицу. Группирует данные по когортам (строки) и дням жизни (столбцы). Для каждой ячейки считает количество уникальных player_id. В итоге получается матрица сколько игроков из каждой когорты заходило в каждый день.
retention_raw = pd.pivot_table(
sessions_with_players,
values='player_id',
index='cohort',
columns='day_number',
aggfunc='nunique', - тут считаем уникальных игроков
fill_value=0 - замена пустых значений (тут это NaN) на 0
)
Но если нам нужны не абсолютные значения, а % как каждая когорта убывает в процентах по дням от даты старта.
5. Считаем размер каждой когорты (знаменатель для процентов)
Считаем сколько игроков (уникальных идентификаторов игроков) в каждой когорте. Потом будем делить на это значение чтобы в нулевой день было 100%, а в последующие видеть снижение в процентах.
cohort_sizes = players_df.groupby('cohort')['player_id'].nunique()
6. Получаем Retention в процентах
Разделим каждую строку (когорту) на размер этой когорты и умножим на 100. Поскольку размеры таблицы, сгруппированной по когортам cohort_sizes совпадают с размером итоговой сводной таблице retention_raw и порядок строк не менялся то сделать это очень просто. Указание axis=0 означает деление по строкам.
retention_pct = retention_raw.divide(cohort_sizes, axis=0) * 100
И еще для красоты цифр можно округлить все значения (до десятых).
retention_pct.round(1)
Теперь мы видим не просто абсолютные цифры убывания численности когорт, а уже метрику Retention.
2.4.6. DuckDB, когда SQL внутри Python
Аналитики учат Pandas, и его ждут от кандидата. Но правда в том, что Pandas начинает “задыхаться” на BigData. Условный CSV на 10 гигабайт или сложный merge нескольких таблиц приведут к тому, что ваш Python может зависнуть намертво. В отличие от Pandas, который хранит данные в оперативной памяти, DuckDB использует колоночное хранение и умную обработку (векторизацию), что позволяет ему работать с данными, которые больше доступной памяти. И самое главное для аналитика что DuckDB использует SQL!
Установка DuckDB в Python делается также как обычной библиотеки.
!pip install duckdb
После этого вызовем его в нашей тетрадке (import duckdb) и сделаем запрос прямо к CSV-файлу (даже не загружая его целиком в память!). Полученный результат сразу преобразуем в Pandas DataFrame.
result = duckdb.sql("""
SELECT country, SUM(amount) as total_revenue
FROM game_payments_2025.csv'
WHERE amount > 0
GROUP BY country
""").df() - и преобразуем результат сразу в Pandas DataFrame
А еще DuckDB может выполнять запросы к уже загруженным Pandas DataFrame.
Сделаем два DataFrame
df_payments = pd.read_csv('payments_small.csv')
df_players = pd.read_csv('players.csv')
А теперь выполняем SQL-запрос к ним!
result = duckdb.sql("""
SELECT country, SUM(amount) as revenue
FROM
(SELECT player_id, country
FROM df_players
) a
INNER JOIN
(SELECT player_id, amount
FROM df_payments
) b
ON a.player_id = b.player_id
GROUP BY country
""").df() – и опять преобразуем результат сразу в Pandas DataFrame
Когда использовать DuckDB
- Вы обрабатываете в Python действительно большие данные (от 1 ГБ до 1 ТБ).
- Вы применяете сложные SQL-запросы с множеством JOIN и оконных функций.
- Вы хотите смешивать SQL и Python в одной тетрадке, объединив мощь SQL для тяжёлых агрегаций и гибкость Pandas для финальной обработки.
Комбинирование DuckDB и Pandas даёт вам великую мощь:
1. DuckDB берёт на себя тяжёлую работу: фильтрацию, группировку, JOIN, оконные функции на данных, которые не влезают в память.
2. Pandas получает уже уменьшенный датасет, с которым может делать всё что угодно: сложные преобразования, визуализацию, статистический анализ.
Python это ваш Магический Посох с целым арсеналам аналитических заклинаний. Но чтобы овладеть ими необходима практика.
- Используйте Pandas , если вам нужны исследования данных в Python.
- Если данные становятся большими и сложными или вы любите сложные SQL преобразования, то подключайте DuckDB.
- Именно при помощи Python в дальнейшем в этом курсе вы сможете освоить Ритуал Священной проверки A/B-тест и несколько заклинаний из глубинной магии машинного обучения.
- А когда придет время перейти от исследований к production ваш Python код инженеры завернут в Airflow, и обогатят PySpark.
Постоянно упражняясь, вы овладеете базой (Pandas) и, совмещая это с SQL (включая DuckDB), вы станете настоящим магом аналитики данных в геймдеве!