Даты и время в pandas: периоды, недели и когорты без ошибок
Как работать с датами в pandas: преобразовать строки, задать границы периода, группировать по неделям и не потерять события из-за часового пояса.
Содержание статьи
Строка “2026-08-01 00:30” выглядит как дата, пока не выясняется, в какой таймзоне она записана и входит ли в отчёт за июль. В pandas ошибки времени обычно не ломают код: запрос просто считает не тот период. Поэтому даты нужно привести к типу, явно задать границы и проверить результат на нескольких строках.
Коротко
Для аналитического периода используй левую включённую и правую исключённую границу: >= start и < next_start. Строки превращай в datetime через pd.to_datetime, недели и месяцы группируй после приведения к единой таймзоне, а неполный текущий период не сравнивай с полным прошлым.
- Сначала проверь dtype и диапазон дат.
- Не смешивай naive datetime и timezone-aware datetime.
- Для недель зафиксируй, с какого дня начинается неделя.
- Возраст когорты считай относительно даты старта пользователя, а не календарной даты отчёта.
Преобразовать строки в datetime
Не полагайся на автоматическое угадывание формата, если источник нестабилен. errors="coerce" помогает найти нераспарсенные значения через isna, но не должен незаметно превращать ошибки в пропуски. После преобразования проверь минимум, максимум и число новых NaT.
orders['created_at'] = pd.to_datetime(
orders['created_at'],
errors='coerce',
utc=True,
)
invalid_dates = orders['created_at'].isna().sum()
print('invalid dates:', invalid_dates)
print(orders['created_at'].min(), orders['created_at'].max())Период: месяц, неделя и день
Период лучше задавать границей следующего периода. Так июль включает все события до 1 августа, включая последний день с любым временем. dt.to_period удобен для подписи и группировки, а date_range или явные границы лучше подходят для фильтров.
start = pd.Timestamp('2026-07-01', tz='UTC')
next_start = pd.Timestamp('2026-08-01', tz='UTC')
july = orders.loc[
(orders['created_at'] >= start)
& (orders['created_at'] < next_start)
].copy()
july['week'] = july['created_at'].dt.to_period('W-MON').astype(str)
weekly = july.groupby('week', as_index=False).agg(
orders=('order_id', 'nunique'),
)Когорты и возраст пользователя
Для retention важен не только день события, но и расстояние от регистрации. Пользователь, который сделал действие через семь дней после signup, относится к D7 независимо от календарного месяца. В pandas для этого удобно привести обе даты к началу дня и посчитать разницу в днях.
events['event_date'] = events['occurred_at'].dt.floor('D')
users['signup_date'] = users['signup_at'].dt.floor('D')
events = events.merge(users[['user_id', 'signup_date']], on='user_id', how='left')
events['day_age'] = (events['event_date'] - events['signup_date']).dt.days
d7 = events.loc[events['day_age'].eq(7)]
d7_users = d7['user_id'].nunique()Часовой пояс — часть определения метрики
Если продукт работает в нескольких странах, “день” может означать UTC, локальный день пользователя или бизнесовую таймзону компании. Для событий около полуночи эти варианты дадут разные DAU и выручку. Выбери правило до расчёта и запиши его в названии или описании отчёта.
- Храни исходное время с timezone, если источник его передаёт.
- Не локализуй naive datetime, пока не знаешь, в какой зоне он записан.
- Проверь несколько событий около полуночи после конвертации.
- Сравни период с SQL-версией расчёта на контрольной выборке.
Полуинтервалы защищают границы периода
Для дней, недель и месяцев используй правило [start, end): начало включается, правая граница не включается. Так соседние периоды не пересекаются, а timestamp ровно в полночь попадает только в один срез. Запись created_at >= start и created_at < next_start надёжнее, чем ручное вычисление последней секунды месяца.
Не сравнивай даты как строки, пока не проверил формат. Строки вида 2026-08-10 сортируются удачно, но смешение 10.08.2026 и ISO ломает порядок. Приведи источник к datetime один раз, сохрани timezone и только затем группируй.
Для отчётов за полный месяц отличай календарное окно от rolling window. Последние 30 дней не равны календарному месяцу: состав дней меняется ежедневно, и сравнение с августом будет методологически нечестным.
events['occurred_at'] = pd.to_datetime(
events['occurred_at'], utc=True, errors='coerce'
)
start = pd.Timestamp('2026-08-01', tz='UTC')
next_start = pd.Timestamp('2026-09-01', tz='UTC')
august = events.loc[
events['occurred_at'].ge(start)
& events['occurred_at'].lt(next_start)
]Группировка по неделям должна иметь договорённость
Неделя может начинаться в понедельник или воскресенье, а label периода может означать начало, конец или номер недели. Зафиксируй это до сравнения. Для бизнеса часто удобнее использовать начало недели как ключ и показывать рядом диапазон дат. Иначе две команды построят разные «недельные» графики из одного источника.
resample удобен для временного индекса и регулярного ряда, Grouper — когда дата остаётся обычной колонкой и нужно добавить канал или платформу. Оба подхода могут дать пустые периоды. Реши, должны ли они отображаться как ноль, NA или отсутствующая дата.
После агрегации отсортируй даты и проверь число периодов. Если в августе четыре строки вместо пяти, это может быть корректно для выбранного правила недели, а может означать потерянный день. Контроль диапазона помогает отличить эти случаи.
| Задача | Подход | Что зафиксировать |
|---|---|---|
| календарные месяцы | Grouper(freq="MS") | начало месяца как label |
| rolling 30 days | date window | день расчёта и правая граница |
| неделя по понедельникам | Grouper(freq="W-MON") | смысл label и timezone |
| когортный возраст | разность дат | календарные или полные дни |
Когорты: дата регистрации и дата активности — разные поля
Для когорты возьми дату первого значимого события: регистрации, первой покупки или первой ценности. Для возврата используй дату последующего события. Если перепутать эти поля, retention начнёт отвечать на вопрос о календарном объёме, а не о поведении пользователей после старта.
Считай возраст в понятных единицах. D1 может означать следующий календарный день или 24 часа после регистрации — это разные методологии. В статье и отчёте назови правило, а в тесте добавь события в 23:59 и 00:01, чтобы увидеть эффект часового пояса.
Не сравнивай свежую когорту по D30 с исторической, если у первой ещё нет полного окна наблюдения. Неполная когорта должна быть помечена или исключена из сравнения. Иначе retention будет выглядеть хуже только потому, что данные ещё не дозрели.
Назови timezone, границы периода, начало недели и смысл D1/D7. Без этих договорённостей одинаковый код может дать разные бизнес-ответы.
Практика: сверить pandas и SQL на одной выборке
Возьми небольшой срез событий за два дня и посчитай DAU в pandas и SQL. Сначала приведи timestamp к одному часовому поясу, затем используй одинаковые полуинтервалы и одинаковое определение active action. Сравни не только итог, но и список пользователей на границе периода.
Если ответы отличаются, не округляй числа и не меняй фильтр на глаз. Выгрузи промежуточную таблицу с user_id, исходным timestamp и локальной датой. В большинстве случаев проблема находится в timezone, включённой правой границе или разном событии, которое считается активностью.
После сверки закрепи правило в функции и тестовой фикстуре. Тогда будущий рефакторинг pandas или SQL не будет незаметно менять календарь продукта.
- Привести timestamp к явной timezone.
- Использовать полуинтервал для каждого периода.
- Разделять календарные периоды и rolling windows.
- Не считать незрелые когорты полными.
- Сверять граничные события в pandas и SQL.
Материалы по теме

Как тестировать аналитические расчёты на Python и pandas
Практический гайд по тестам для аналитика: проверить метрики на маленьком датасете, поймать регрессию и защитить расчёт от тихих изменений.

Как ускорить pandas: память, типы и обработка больших файлов
Что делать, если pandas медленно работает или не помещает файл в память: категории, downcast, chunksize, Parquet и контроль размера данных.

Pandas apply и векторизация: как писать быстрее и понятнее
Когда использовать apply, почему векторные операции быстрее и как переписать медленный построчный расчёт в pandas.