Как работать с датами в pandas

Почему это важно

Работа с датами — 30% времени аналитика. Когортный анализ, MoM-динамика, retention, скользящие средние — всё это про даты. Pandas имеет отличную поддержку временных рядов, но её нужно знать.

Основной класс — Timestamp для отдельных моментов и DatetimeIndex для последовательностей. Timedelta — для интервалов.

Преобразование в дату

Текстовые колонки с датами нужно сначала конвертировать:

import pandas as pd

df['date'] = pd.to_datetime(df['date'])

Pandas обычно автоматически распознаёт формат. Для нестандартных явно указывайте:

df['date'] = pd.to_datetime(df['date'], format='%d.%m.%Y')

Если есть некорректные значения, используйте errors='coerce' — они превратятся в NaT (Not a Time) вместо ошибки:

df['date'] = pd.to_datetime(df['date'], errors='coerce')

Для Unix timestamps с миллисекундами:

df['created_at'] = pd.to_datetime(df['ts_ms'], unit='ms')

Извлечение компонентов

Через .dt-аксессор:

df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['weekday'] = df['date'].dt.dayofweek  # 0 = понедельник
df['is_weekend'] = df['weekday'].isin([5, 6])
df['quarter'] = df['date'].dt.quarter
df['week'] = df['date'].dt.isocalendar().week

Полезно для сегментации по времени: «продажи по дням недели», «выручка по кварталам».

Особый случай — day_name:

df['day_name'] = df['date'].dt.day_name()
# 'Monday', 'Tuesday', etc.

На русском через locale:

df['day_name'] = df['date'].dt.day_name(locale='ru_RU')
# 'понедельник', 'вторник'

Группировка по периоду

Для агрегации по месяцам, неделям, кварталам используйте Grouper или конвертацию через to_period:

# По месяцам
monthly = df.groupby(pd.Grouper(key='date', freq='M'))['amount'].sum()

# По неделям (с понедельника)
weekly = df.groupby(pd.Grouper(key='date', freq='W-MON'))['amount'].sum()

Альтернатива — period:

df['month'] = df['date'].dt.to_period('M')
monthly = df.groupby('month')['amount'].sum()

Period даёт более читаемый результат: 2026-04 вместо 2026-04-30.

Если хочется сразу закрепить тему на практике — открой тренажёр в Telegram. 10 минут в день — и синтаксис в пальцах.

Resample — пересчёт временного ряда

Похоже на groupby, но более мощное для time series:

df.set_index('date')['amount'].resample('D').sum()

Это дневная агрегация. Параметр freq:

  • D — дни.
  • W — недели.
  • M — конец месяца.
  • MS — начало месяца.
  • H — часы.
  • Q — кварталы.

Resample может заполнять пропущенные дни:

daily = df.set_index('date')['amount'].resample('D').sum().fillna(0)

Это делает continuous ряд без пропусков — важно для графиков.

Rolling — скользящие метрики

Для скользящих средних и сумм:

df = df.sort_values('date')
df['rolling_mean_7d'] = df['amount'].rolling(7).mean()
df['rolling_sum_30d'] = df['amount'].rolling(30).sum()

Rolling по количеству строк. Для rolling по времени (например, «последние 7 дней»):

df = df.set_index('date')
df['mean_last_7d'] = df['amount'].rolling('7D').mean()

Полезно для сглаживания волатильных метрик вроде DAU или revenue.

Арифметика с датами

Разница между датами даёт Timedelta:

df['days_since_signup'] = (df['date'] - df['signup_date']).dt.days
df['hours_waiting'] = (df['completed_at'] - df['started_at']).dt.total_seconds() / 3600

Добавление / вычитание периода:

df['next_week'] = df['date'] + pd.Timedelta(days=7)
df['three_months_later'] = df['date'] + pd.DateOffset(months=3)

Отличие: Timedelta — фиксированное количество времени (ровно 30 дней). DateOffset — календарное (месяц — это 28-31 день в зависимости от даты).

Фильтрация по датам

Просто и интуитивно:

# После определённой даты
df[df['date'] >= '2026-04-01']

# Диапазон
df[(df['date'] >= '2026-04-01') & (df['date'] < '2026-05-01')]

# За последние 30 дней
cutoff = pd.Timestamp.now() - pd.Timedelta(days=30)
df[df['date'] >= cutoff]

Для работы с set index быстрее:

df = df.set_index('date')
df.loc['2026-04']  # всё за апрель 2026
df.loc['2026-04-15':'2026-04-30']  # диапазон

Slice через строки работает только для sorted DatetimeIndex.

Работа с timezone

Pandas поддерживает timezone-aware datetime:

df['utc_time'] = df['timestamp'].dt.tz_localize('UTC')
df['msk_time'] = df['utc_time'].dt.tz_convert('Europe/Moscow')

Если данные приходят без tz, localize назначает tz. Если уже с tz, convert меняет.

Осторожно: некоторые операции (merge, groupby) могут не работать при смешивании naive (без tz) и aware (с tz).

Чтобы не только читать теорию, но и решать реальные задачи — загляните в бот Карьерника. Там по каждой теме подборка вопросов с разборами.

Генерация последовательностей

Для тестов и заполнения пропусков:

# Даты месяца
pd.date_range('2026-04-01', '2026-04-30')

# По рабочим дням
pd.bdate_range('2026-04-01', '2026-04-30')

# По часам
pd.date_range('2026-04-15', periods=24, freq='H')

Типичные ошибки

Забыть конвертировать в datetime. Операции > или < над строками работают лексикографически, а не как даты. Результат — нелогичная сортировка.

Сравнивать timezone-aware с naive. Падает с ошибкой. Всегда приводите обе стороны к одному виду.

Использовать Grouper без параметра key при column groupby. Работает только как индекс.

Ожидать, что resample('M') даст начало месяца. Нет, это конец месяца. Используйте MS для начала.

Читайте также

FAQ

to_datetime или astype('datetime64')?

Оба работают. to_datetime — мощнее, поддерживает errors, format, unit. Обычно используйте его.

Как получить начало месяца?

df['date'].dt.to_period('M').dt.to_timestamp() или df['date'] - pd.offsets.MonthBegin(1).

Как резать по работа-дневным неделям?

Grouper(freq='W-FRI') для недели, заканчивающейся пятницей.

Скорость работы с датами?

Быстрая, если колонка типа datetime64. Медленная — если object (строки). Всегда конвертируйте сразу.