Почти любая продуктовая метрика привязана ко времени: DAU, выручка по неделям, retention. Ошибки с датами самые коварные: код работает, а цифры неверны на часовой пояс или на неделю.
Превратить строку в дату
import pandas as pd
df['ts'] = pd.to_datetime(df['ts']) # угадает формат
df['ts'] = pd.to_datetime(df['ts'], format='%d.%m.%Y') # явный формат
df['ts'] = pd.to_datetime(df['ts'], errors='coerce') # мусор станет NaT
- Явный
formatбыстрее и надёжнее: дата03.04.2026в американской логике это 4 марта, а не 3 апреля. Для формата «день первым» без явного формата естьdayfirst=True. errors='coerce'превращает нераспознанные значения вNaT(пропуск для дат). После этого проверьте, сколько NaT получилось.- Unix-время:
pd.to_datetime(df['ts'], unit='s').
Аксессор .dt
df['date'] = df['ts'].dt.date # объект date, удобно для печати
df['day'] = df['ts'].dt.normalize() # полночь того же дня, остаётся datetime
df['hour'] = df['ts'].dt.hour
df['dow'] = df['ts'].dt.dayofweek # понедельник = 0
df['month'] = df['ts'].dt.to_period('M') # период "2026-10"
Для группировки по дням лучше dt.normalize() или dt.floor('D'): колонка остаётся datetime и дальше работает resample.
Разница дат: Timedelta
(df['end'] - df['start']).dt.days # целые дни, отбрасывает часы
(df['end'] - df['start']).dt.total_seconds() / 3600 # часы с дробью
dt.days у Timedelta это целая часть суток, а не округление: 1 день 23 часа даст 1.
resample: группировка по времени
Работает, когда время в индексе (или через on=):
daily = df.set_index('ts')['amount'].resample('D').sum()
weekly = df.resample('W-MON', on='ts')['amount'].sum()
Главное отличие от groupby(dt.date): resample создаёт все периоды в диапазоне, включая пустые. Для суммы пустой день получит 0, для среднего NaN. Это важно для графиков и скользящих средних.
Псевдонимы частот в pandas 2.2+: 'D' день, 'h' час, 'min' минута, 'W' неделя (по умолчанию неделя заканчивается в воскресенье), 'MS' начало месяца, 'ME' конец месяца. Старые 'M' и 'H' устарели в pandas 2.2, а в pandas 3.0 выдают ошибку.
Недели
Неделя в России начинается с понедельника. df['ts'].dt.to_period('W') по умолчанию даёт неделю, заканчивающуюся в воскресенье, то есть с понедельника по воскресенье: это то, что нужно. А вот resample('W') подписывает неделю датой воскресенья, её конца. Если в отчёте нужна дата начала недели, используйте dt.to_period('W').dt.start_time.
Часовые пояса
s = pd.to_datetime(df['ts'], utc=True) # явно UTC
s.dt.tz_convert('Europe/Moscow') # перевести в Москву
naive.dt.tz_localize('Europe/Moscow') # приписать пояс наивному времени
tz_localizeговорит «эти часы были по Москве»,tz_convertпересчитывает уже известное время в другой пояс.- Нельзя сравнивать или вычитать aware и naive даты: pandas выбросит ошибку.
- События в базе обычно хранятся в UTC. Если считать «заказы за день» по UTC, московский день сдвинется на 3 часа, и ночные заказы уедут в соседние сутки.
Когорты и периоды
df['cohort'] = df.groupby('user')['ts'].transform('min').dt.to_period('M')
df['age'] = (df['ts'].dt.to_period('M') - df['cohort']).apply(lambda x: x.n)
Разность двух периодов это смещение, его .n даёт число месяцев.
Частые ошибки
- Даты остались строками, и сортировка идёт по тексту:
"10.01"раньше"9.01". - Перепутаны день и месяц.
- Подсчёт «по дням» в UTC вместо местного времени.
groupby(dt.date)теряет пустые дни, график проваливается в линию без нулей.- Неделя, подписанная воскресеньем, принята за начало недели.
Что спросят на собесе
- Как перевести строку в дату и что делать с битыми значениями?
- Чем
resampleотличается отgroupbyпо дате? - Как посчитать DAU по московскому времени, если события в UTC?
- Чем
tz_localizeотличается отtz_convert? - Как посчитать номер месяца жизни пользователя для когорт?