Условие
Дан лог events(user_id, ts, event_name). Нужно склеить события в сессии: новая сессия начинается, если с предыдущего события прошло больше 30 минут. Получить session_id, длительность сессии и число событий в сессии.
Решение
Идея
В рамках пользователя считаем разницу между соседними ts. Если она > 30 мин — это начало новой сессии. Кумулятивная сумма таких флагов даёт session_id внутри пользователя.
Реализация
import pandas as pd
events = events.sort_values(['user_id', 'ts']).copy()
# Разница со следующим (предыдущим) событием внутри пользователя
events['gap'] = events.groupby('user_id')['ts'].diff()
# Флаг новой сессии: первое событие пользователя или gap > 30 мин
events['new_session'] = (events['gap'].isna()) | (events['gap'] > pd.Timedelta('30min'))
# session_id уникальный по всему датасету
events['session_id'] = events['new_session'].cumsum()
# Агрегат по сессиям
sessions = (
events.groupby(['user_id', 'session_id'], as_index=False)
.agg(
start=('ts', 'min'),
end=('ts', 'max'),
n_events=('event_name', 'size'),
)
)
sessions['duration_min'] = (sessions['end'] - sessions['start']).dt.total_seconds() / 60Вариант: глобальный session_id через cumsum на user-уровне
Можно cumsum внутри groupby — получите session-номер внутри пользователя:
events['session_num'] = events.groupby('user_id')['new_session'].cumsum()Подводные камни
- Без
sort_values(['user_id', 'ts'])diff()посчитает мусор. cumsumбезgroupbyдаёт глобально уникальный id (хорошо); внутри groupby — локальный (тоже бывает нужен).- Первое событие у каждого пользователя должно начать новую сессию — отсюда
gap.isna() | gap > 30min. - Длительность одно-событийной сессии = 0 — это не баг, но часто фильтруется.
- Таймзоны: если разные
tsв разных tz — гэп будет считаться неверно. Сначала привести к одной tz.
Эталонный ответ
gap = ts.diff() по пользователю → new_session = gap > 30min → session_id = new_session.cumsum().