Все материалы
Продуктовая аналитикагайдстарт

Даты и время в pandas: периоды, недели и когорты без ошибок

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

КПКейсПрактика1 августа 2026 г.18 мин

Строка “2026-08-01 00:30” выглядит как дата, пока не выясняется, в какой таймзоне она записана и входит ли в отчёт за июль. В pandas ошибки времени обычно не ломают код: запрос просто считает не тот период. Поэтому даты нужно привести к типу, явно задать границы и проверить результат на нескольких строках.

Коротко

Для аналитического периода используй левую включённую и правую исключённую границу: >= start и < next_start. Строки превращай в datetime через pd.to_datetime, недели и месяцы группируй после приведения к единой таймзоне, а неполный текущий период не сравнивай с полным прошлым.

  • Сначала проверь dtype и диапазон дат.
  • Не смешивай naive datetime и timezone-aware datetime.
  • Для недель зафиксируй, с какого дня начинается неделя.
  • Возраст когорты считай относительно даты старта пользователя, а не календарной даты отчёта.

Преобразовать строки в datetime

Не полагайся на автоматическое угадывание формата, если источник нестабилен. errors="coerce" помогает найти нераспарсенные значения через isna, но не должен незаметно превращать ошибки в пропуски. После преобразования проверь минимум, максимум и число новых NaT.

pythonРазобрать дату и найти невалидные строки
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 или явные границы лучше подходят для фильтров.

pythonПолный месяц и календарная неделя
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 для этого удобно привести обе даты к началу дня и посчитать разницу в днях.

pythonРассчитать возраст события относительно регистрации
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 дней не равны календарному месяцу: состав дней меняется ежедневно, и сравнение с августом будет методологически нечестным.

pythonНадёжный фильтр полного месяца
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 daysdate 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.
Продолжить чтение
Вся библиотека