Как работать с time series в pandas

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Зачем это нужно

Любой продуктовый аналитик работает с временными рядами: ежедневные DAU, monthly revenue, weekly cohort retention. Базовые операции — resample в разные частоты, moving average для сглаживания, shift для сравнения с прошлым периодом — нужно делать быстро и правильно.

На собеседовании для middle-аналитика вопросы по time series в pandas обязательны: «как посчитать 7-day moving average», «как сравнить с предыдущим периодом», «как выделить сезонность». Без этих знаний дашборды вы не построите.

В статье:

  • datetime index и почему он важен
  • resample — изменение частоты
  • rolling — скользящие окна
  • shift — сравнение с прошлым
  • Декомпозиция тренда и сезонности

1. Установить datetime index

import pandas as pd

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

С datetime index все time series операции работают.

2. Resample — изменить частоту

Daily → weekly

weekly = df['revenue'].resample('W').sum()

Частоты: D, W, M, Q, Y, H, 15min.

Upsample (daily → hourly)

hourly = df.resample('H').ffill()  # forward-fill пропуски

3. Rolling window (moving average)

# 7-day moving average
df['revenue_ma7'] = df['revenue'].rolling(7).mean()

# 30-day
df['revenue_ma30'] = df['revenue'].rolling(30).mean()

# sum
df['revenue_sum7'] = df['revenue'].rolling(7).sum()

По времени (нужен datetime index):

df['ma_7d'] = df['revenue'].rolling('7D').mean()  # ровно 7 дней

4. Shift — сравнение с прошлым

# значение день назад
df['revenue_prev_day'] = df['revenue'].shift(1)

# DoD change
df['dod_change'] = df['revenue'] - df['revenue'].shift(1)
df['dod_pct'] = df['revenue'].pct_change()

# неделю назад
df['revenue_prev_week'] = df['revenue'].shift(7)
df['wow_pct'] = df['revenue'].pct_change(7)

# месяц назад
df['revenue_prev_month'] = df['revenue'].shift(30)
df['mom_pct'] = df['revenue'].pct_change(30)

# год назад (YoY)
df['yoy_pct'] = df['revenue'].pct_change(365)

5. Cumulative sum

# running total
df['revenue_cumsum'] = df['revenue'].cumsum()

# YTD (Year-to-date)
df['revenue_ytd'] = df.groupby(df.index.year)['revenue'].cumsum()

6. Date range и reindex

Заполнить пропущенные даты:

full_range = pd.date_range(df.index.min(), df.index.max(), freq='D')
df = df.reindex(full_range)
df['revenue'] = df['revenue'].fillna(0)  # дни без продаж = 0

7. Фильтр по дате

# последние 30 дней
df.last('30D')

# первые 7 дней
df.first('7D')

# по диапазону
df.loc['2026-01':'2026-04']

8. Выделить сезонность

from statsmodels.tsa.seasonal import seasonal_decompose

result = seasonal_decompose(df['revenue'], period=7)  # weekly seasonality
result.plot()

Выдаст 4 графика: original, trend, seasonal, residual.

9. Forecasting (простейшее)

# naive (вчера = сегодня)
df['forecast'] = df['revenue'].shift(1)

# moving average
df['forecast'] = df['revenue'].rolling(7).mean().shift(1)

# exponential smoothing
df['forecast'] = df['revenue'].ewm(alpha=0.3).mean().shift(1)

10. Обработка пропусков

# forward fill
df['revenue'].ffill()

# backward fill
df['revenue'].bfill()

# interpolation
df['revenue'].interpolate()

# линейная
df['revenue'].interpolate(method='time')

11. Агрегация по частям времени

df['year'] = df.index.year
df['month'] = df.index.month
df['day_of_week'] = df.index.day_name()
df['hour'] = df.index.hour

# revenue по дням недели
df.groupby('day_of_week')['revenue'].mean()

12. Correlation between time series

# корреляция между двумя series
df['revenue'].rolling(30).corr(df['ad_spend'])

Частые ошибки

1. Не sort_index

Rolling / shift работают по порядку. Без sort — мусор.

2. Shift в groupby

# неправильно
df['prev'] = df.groupby('user_id')['revenue'].shift(1)
# — работает, но если не отсортировать по дате → неверный prev

Правильно:

df = df.sort_values(['user_id', 'date'])
df['prev'] = df.groupby('user_id')['revenue'].shift(1)

3. Resample на нечисловой колонке

Нужна числовая для агрегации. Для текста — first() / last() / mode().

4. Таймзоны

Без явного tz-localize операции могут сдвигаться на ±3 часа.

Связанные темы

FAQ

resample vs groupby?

Resample требует datetime index — удобнее. groupby универсальнее.

shift vs LAG в SQL?

Эквивалент. shift(1) в pandas = LAG(1) в SQL.

Как rolling с custom window?

df.rolling('7D', on='date').mean() с explicit колонкой.

Моделирование seasonality?

Для advance — SARIMA, Prophet, tbats.


Тренируйте pandas — откройте тренажёр с 1500+ вопросами для собесов.