pandasдатыto_datetimedt-аксессоркогорты

Даты в pandas: to_datetime, dt и разница дат

2026-07-10 11 мин

Коротко: чтобы работать с датами в pandas, сначала переведите строковую колонку в тип datetime через pd.to_datetime(df['col']). После этого станут доступны компоненты через аксессор .dt (.dt.year, .dt.month, .dt.dayofweek), а разница двух дат посчитается обычным вычитанием и вернёт Timedelta, из которого дни достаются как .dt.days. Всё остальное — фильтр по периоду, нарезка когорт по дате первого заказа — строится поверх этих трёх операций.

Строковая дата вроде '2026-03-14' для pandas — просто текст. Пока колонка имеет тип object, вы не можете ни отсортировать её хронологически, ни вычесть одну дату из другой, ни сгруппировать по месяцу. Первый шаг всегда один: превратить текст в настоящий datetime64[ns]. Ниже — рабочий путь от парсинга до когорт на данных заказов и логов.

Как превратить строку в дату через to_datetime?

Основной инструмент — pd.to_datetime. Он принимает Series, список или скаляр и возвращает значения типа datetime64[ns]. Как только колонка стала datetime, сортировка, арифметика и группировка по времени начинают работать корректно.

import pandas as pd

orders = pd.DataFrame({
    'order_id': [1, 2, 3, 4, 5],
    'user_id': [10, 10, 22, 22, 31],
    'created_at': ['2026-01-05 09:12:00', '2026-02-18 14:03:00',
                   '2026-01-05 11:40:00', '2026-03-02 08:00:00',
                   '2026-01-20 19:25:00'],
    'amount': [1200, 890, 3400, 750, 1580],
})

orders['created_at'] = pd.to_datetime(orders['created_at'])
print(orders['created_at'].dtype)   # datetime64[ns]

Проверять dtype полезно всегда: если после to_datetime вы видите object, значит парсинг молча провалился и колонка осталась текстом. По умолчанию pandas распознаёт ISO-формат (YYYY-MM-DD) и большинство привычных вариантов автоматически. Разбор строковых преобразований в широком контексте есть в конспекте pandas apply, str, dt и векторизация — эта статья углубляет именно ветку дат.

Если данные приходят из базы, часто удобнее распарсить даты сразу при чтении:

orders = pd.read_csv('orders.csv', parse_dates=['created_at'])

Параметр parse_dates избавляет от отдельного шага и гарантирует правильный тип с самого начала. Основы чтения источников — в части про чтение и запись CSV, Excel, SQL.

Как обработать ошибки и разные форматы дат?

Реальные логи редко бывают чистыми: где-то 2026/03/14, где-то 14.03.2026, где-то пустая строка или мусор. У to_datetime есть три параметра, которые закрывают почти все случаи: format, errors и dayfirst.

Если формат известен заранее, задавайте format явно — это и быстрее, и надёжнее автоопределения:

s = pd.Series(['14.03.2026', '02.04.2026', '20.12.2026'])
dates = pd.to_datetime(s, format='%d.%m.%Y')

Коды формата стандартные: %d — день, %m — месяц, %Y — четырёхзначный год, %H:%M:%S — время. Когда день и месяц можно перепутать (европейский 05.06.2026), параметр dayfirst=True говорит трактовать первое число как день.

Битые значения обрабатываются через errors:

raw = pd.Series(['2026-01-05', 'не дата', '2026-03-02', ''])

# вернёт NaT (Not a Time) вместо падения
parsed = pd.to_datetime(raw, errors='coerce')
print(parsed.isna().sum())   # 2 — сколько строк не распарсилось
NaT — это datetime-аналог NaN. Он не ломает последующие вычисления, а найти проблемные строки можно через .isna(). Что делать с пропусками дальше (удалить, заполнить, пометить) — разобрано в части про пропуски и дубли: fillna, dropna.

Три значения errors в одной таблице:

ЗначениеПоведение при битой строкеКогда использовать
raise (по умолчанию)падает с исключениемкогда данные обязаны быть чистыми
coerceставит NaTбоевой разбор грязных логов
ignoreвозвращает вход без измененийпочти никогда, ведёт к скрытым багам

На практике для аналитики в 90% случаев нужен errors='coerce' плюс явная проверка доли NaT после парсинга.

Как извлечь год, месяц и день недели через .dt?

После того как колонка стала datetime, аксессор .dt открывает доступ к любому компоненту даты. Это векторизованные операции — они применяются сразу ко всей колонке, без циклов.

orders['year'] = orders['created_at'].dt.year
orders['month'] = orders['created_at'].dt.month
orders['day'] = orders['created_at'].dt.day
orders['hour'] = orders['created_at'].dt.hour
orders['dow'] = orders['created_at'].dt.dayofweek   # 0 = понедельник, 6 = воскресенье
orders['week'] = orders['created_at'].dt.isocalendar().week
orders['is_weekend'] = orders['created_at'].dt.dayofweek >= 5

Ключевой момент про день недели: .dt.dayofweek даёт число от 0 (понедельник) до 6 (воскресенье). Если нужно название, есть .dt.day_name() — вернёт строки вроде Monday. Проверка на выходной сводится к сравнению dayofweek >= 5, потому что суббота и воскресенье — это индексы 5 и 6.

Часто задача — свести заказы к началу месяца или недели для группировки. Для этого удобен .dt.to_period или .dt.normalize:

# первый день месяца заказа — удобно для помесячной группировки
orders['order_month'] = orders['created_at'].dt.to_period('M').dt.to_timestamp()

# срезаем время, оставляем только дату (полночь)
orders['order_date'] = orders['created_at'].dt.normalize()

monthly_revenue = orders.groupby('order_month')['amount'].sum()

Полезные компоненты .dt в одной таблице:

АтрибутЧто возвращаетПример значения
.dt.yearгод2026
.dt.monthмесяц числом3
.dt.dayдень месяца14
.dt.dayofweekдень недели (0–6)4
.dt.day_name()название дняFriday
.dt.quarterквартал1
.dt.hourчас19
.dt.to_period('M')период-месяц2026-03

Тот же разбор в SQL — через EXTRACT и DATE_TRUNC — лежит в статье про дату и время в SQL. Логика одинаковая, отличается только синтаксис, и на собеседовании полезно уметь показать оба.

Как посчитать разницу между двумя датами?

Разница двух datetime-значений в pandas — это Timedelta. Вы просто вычитаете одну колонку из другой, а затем достаёте нужную единицу через .dt.

deliveries = pd.DataFrame({
    'order_id': [1, 2, 3],
    'created_at': pd.to_datetime(['2026-01-05', '2026-01-10', '2026-01-12']),
    'delivered_at': pd.to_datetime(['2026-01-08', '2026-01-11', '2026-01-20']),
})

deliveries['delta'] = deliveries['delivered_at'] - deliveries['created_at']
deliveries['days_to_deliver'] = deliveries['delta'].dt.days
print(deliveries[['order_id', 'days_to_deliver']])
# 1 -> 3 дня, 2 -> 1 день, 3 -> 8 дней

Формула читается так: срок доставки = дата доставки минус дата заказа. Из Timedelta дни достаются через .dt.days, а секунды — через .dt.total_seconds(). Если нужны, например, часы, берите delta.dt.total_seconds() / 3600.

Сравнить дату с «сейчас» тоже частая задача — например, посчитать возраст аккаунта или recency в RFM-анализе:

now = pd.Timestamp('2026-07-10')
orders['days_since_order'] = (now - orders['created_at']).dt.days

Здесь days_since_order — это давность последнего заказа, база для recency в сегментации и для оценки оттока клиентов. Разбор полной RFM-сегментации на SQL есть в отдельном гайде — RFM-сегментация пошагово.

Важная деталь: если у одной колонки есть таймзона, а у другой нет, вычитание упадёт с ошибкой Cannot subtract tz-naive and tz-aware. Приводите обе к одному виду через .dt.tz_localize(None) или наоборот .dt.tz_convert.

Как отфильтровать данные по нужному периоду?

Когда колонка — настоящий datetime, фильтр по периоду пишется как обычное сравнение с граничными датами. Сравнивать datetime со строкой можно напрямую — pandas сам распарсит строку.

mask = (orders['created_at'] >= '2026-01-01') & (orders['created_at'] < '2026-02-01')
january = orders[mask]

Обратите внимание на границы: >= для начала и < для конца следующего месяца. Такой полуоткрытый интервал начало, конец) избавляет от классической ошибки с последними секундами дня — если написать <= '2026-01-31', то заказ в 2026-01-31 23:59:00 не попадёт, потому что строка '2026-01-31' трактуется как полночь. Полуоткрытый интервал закрывает эту дыру.

Если поставить дату в индекс, доступен ещё более лаконичный синтаксис срезов:

ts = orders.set_index('created_at').sort_index()
q1 = ts['2026-01':'2026-03']       # весь первый квартал
one_day = ts.loc['2026-01-05']      # все заказы за 5 января

Строковые срезы по datetime-индексу — одна из самых удобных вещей в pandas: '2026-01' автоматически разворачивается во весь январь. Подробнее о временных рядах, resample и rolling — в части про [reshape и временные ряды. Базовый арсенал фильтрации масками и query — в статье про фильтрацию: маски и query.

Как нарезать когорты по дате первого заказа?

Когортный анализ строится на дате первого действия пользователя. В pandas это два шага: найти для каждого пользователя дату первого заказа через groupby.transform('min'), затем свести её к месяцу — это и есть метка когорты.

# 1. дата первого заказа каждого пользователя, разложенная обратно по строкам
orders['first_order'] = orders.groupby('user_id')['created_at'].transform('min')

# 2. когорта = месяц первого заказа
orders['cohort'] = orders['first_order'].dt.to_period('M')

# 3. номер месяца жизни: сколько месяцев прошло от первого заказа
order_period = orders['created_at'].dt.to_period('M')
orders['month_number'] = (order_period - orders['cohort']).apply(lambda x: x.n)

# 4. таблица удержания: строки — когорты, столбцы — месяц жизни
cohort_sizes = orders.groupby(['cohort', 'month_number'])['user_id'].nunique()
retention = cohort_sizes.unstack(fill_value=0)
print(retention)

Почему transform('min'), а не agg: transform возвращает результат той же длины, что исходная колонка, поэтому метка первого заказа приклеивается к каждой строке пользователя без отдельного merge. Дальше вычитание двух периодов даёт целое число — номер месяца жизни, а unstack разворачивает результат в привычную треугольную матрицу удержания.

Дальше по этой матрице считают классическую метрику: Retention = число активных в месяце N / размер когорты * 100%. Строки таблицы — месяцы привлечения, столбцы — сколько месяцев прошло. Именно так рождается кривая удержания, которую спрашивают почти на каждом продуктовом собесе. Метрику по шагам разбирают статьи про когортный анализ retention в SQL и удержание и кривую retention, а определения метрик — карточки retention D7 и DAU.

Тот же принцип «первое событие как якорь» работает для сессионизации логов, воронок и расчёта времени до второго заказа. Аккуратно распарсенная дата — фундамент всех этих расчётов.

Какие ошибки чаще всего ломают парсинг дат?

Собрал грабли, на которые аналитики наступают чаще всего:

Быстрый чек-лист под рукой удобно держать в шпаргалке по pandas. А чтобы довести работу с датами до автоматизма — прогоните её на реальных задачах. В Python-тренажёре есть блок с заказами и логами: парсинг to_datetime, извлечение компонентов через .dt, разница дат и нарезка когорт — всё на живых данных с проверкой результата. Первые пять задач открыты бесплатно, дальше доступ по подписке. Системно тему дат и векторизации даёт курс по pandas.

Когда почувствуете, что синтаксис перестал мешать, проверьте себя в боевом режиме: соберите мок-собеседование с AI-интервьюером или пройдите подборку вопросов для аналитика — работа с датами там встречается и в SQL-, и в Python-секциях. Если хочется сравнить оба стека на одной задаче, тот же расчёт срока и когорт полезно повторить в SQL-тренажёре: переключение между .dt в pandas и DATE_TRUNC в SQL — ровно то, что отличает уверенного аналитика от новичка.

Потренируйся на реальных данных
Разбор — это половина дела. Закрепи парсинг дат и .dt на задачах с заказами и логами: первые 5 бесплатно, дальше по подписке.
Открыть Python-тренажёр →