Коротко: чтобы работать с датами в 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.
Тот же принцип «первое событие как якорь» работает для сессионизации логов, воронок и расчёта времени до второго заказа. Аккуратно распарсенная дата — фундамент всех этих расчётов.
Какие ошибки чаще всего ломают парсинг дат?
Собрал грабли, на которые аналитики наступают чаще всего:
- Забыли проверить
dtype. Послеto_datetimeколонка иногда остаётсяobject— например, из-за одного битого значения приerrors='raise'или из-за смешанных форматов. Всегда печатайтеdf['col'].dtypeпосле парсинга. - Перепутали день и месяц. Формат
05.06.2026неоднозначен. Если это 6 мая, а не 5 июня — задавайтеdayfirst=Trueили явныйformat='%d.%m.%Y', не полагайтесь на угадывание. - Сравнение datetime со строкой в неудачном формате.
df['created_at'] > '05.01.2026'может тихо распарситься иначе, чем вы ждёте. Пишите границы в ISO:'2026-01-05'. <=на верхней границе периода. Теряете заказы последнего дня после полуночи. Используйте полуоткрытый интервал[начало, конец следующего периода).- Смешение tz-aware и tz-naive. Вычитание дат с таймзоной и без падает. Приводите обе колонки к одному виду через
.dt.tz_localize(None). - Медленный парсинг без
format. На миллионах строк автоопределение формата в разы медленнее явногоformat. Задавайте его, когда знаете структуру. .dtна неотпарсенной колонке. Обращениеdf['col'].dt.yearкobject-колонке выдастAttributeError. Сначалаto_datetime, потом.dt.
Быстрый чек-лист под рукой удобно держать в шпаргалке по pandas. А чтобы довести работу с датами до автоматизма — прогоните её на реальных задачах. В Python-тренажёре есть блок с заказами и логами: парсинг to_datetime, извлечение компонентов через .dt, разница дат и нарезка когорт — всё на живых данных с проверкой результата. Первые пять задач открыты бесплатно, дальше доступ по подписке. Системно тему дат и векторизации даёт курс по pandas.
Когда почувствуете, что синтаксис перестал мешать, проверьте себя в боевом режиме: соберите мок-собеседование с AI-интервьюером или пройдите подборку вопросов для аналитика — работа с датами там встречается и в SQL-, и в Python-секциях. Если хочется сравнить оба стека на одной задаче, тот же расчёт срока и когорт полезно повторить в SQL-тренажёре: переключение между .dt в pandas и DATE_TRUNC в SQL — ровно то, что отличает уверенного аналитика от новичка.