Time series CV и фичи на собеседовании Data Scientist

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Карьерник — Duolingo для аналитиков: 10 минут в день тренируй SQL, Python, A/B, статистику, метрики и ещё 3 темы собеса. 1500+ вопросов в Telegram-боте. Бесплатно.

Зачем нужна особая CV

В обычной CV (k-fold) данные перемешиваются и делятся на k частей. На time series это сломано: модель «видит будущее» во время обучения и кажется идеальной на CV. На проде падает.

Главная боль без правильной CV — DS показывает 0.95 R² на CV, продакт счастлив, через месяц на проде — 0.4. Причина: модель училась на завтра, предсказывая вчера. На собесе классический вопрос: «как делать CV для time series?» — ответ должен быть про walk-forward / expanding window и обязательно gap.

Walk-forward и expanding window

Walk-forward (rolling window):

Fold 1: train [t1..t6], val [t7..t8]
Fold 2: train [t3..t8], val [t9..t10]
Fold 3: train [t5..t10], val [t11..t12]

Окно скользит вперёд, размер train фиксированный. Хорошо при concept drift — старые данные могут быть менее релевантны.

Expanding window (anchored):

Fold 1: train [t1..t6], val [t7..t8]
Fold 2: train [t1..t8], val [t9..t10]
Fold 3: train [t1..t10], val [t11..t12]

Train растёт от начала. Хорошо, когда больше данных = лучшая модель и нет drift.

В sklearn — TimeSeriesSplit:

from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5, gap=7)
for train_idx, val_idx in tscv.split(X):
    ...

TimeSeriesSplit — expanding window. Walk-forward — нужно делать руками.

Gap для предотвращения leakage

Gap — пропуск между train и val. Зачем: при создании lag-фич с lag=7 нельзя тренироваться на дне T-1 и валидироваться на T, потому что лаги для T рассчитываются по T-1.

train: [..., t-9, t-8]   gap: [t-7..t-1]   val: [t]

Размер gap зависит от используемых лагов и rolling windows. Если есть rolling_30, gap должен быть ≥ 30 дней.

Альтернатива: сразу при splitting'е определить, какие lags безопасно использовать на каждом фолде. Сложнее, но точнее.

Lag features

Самые мощные фичи в time series — значения таргета и других переменных в прошлом.

df['sales_lag_1']  = df['sales'].shift(1)   # вчерашние продажи
df['sales_lag_7']  = df['sales'].shift(7)   # неделю назад (то же weekday)
df['sales_lag_30'] = df['sales'].shift(30)

Какие лаги:

  • 1, 7, 14, 28, 365 — типичные для daily-данных
  • Период seasonality (год для daily, неделя для hourly)
  • Доменные значения (для retail — Q1/Q2/Q3 backshift)

Грабля: target в прошлом. На validation/test эти лаги нужно строить осторожно — нельзя использовать ground truth, который мы пытаемся предсказать.

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Rolling stats и calendar features

Rolling statistics — агрегаты в скользящем окне:

df['sales_roll_7_mean']  = df['sales'].shift(1).rolling(7).mean()
df['sales_roll_7_std']   = df['sales'].shift(1).rolling(7).std()
df['sales_roll_30_max']  = df['sales'].shift(1).rolling(30).max()

shift(1) обязателен: иначе строка с date=t содержит rolling, который включает sales[t] = тот самый таргет. Утечка.

Calendar features:

  • День недели (dayofweek)
  • День месяца, день года
  • Месяц, квартал
  • Праздник (флаг + days_to_next_holiday)
  • Сезон, выходной/будний

Cyclical encoding для периодических: dayofweek_sin, dayofweek_cos — чтобы модель видела «вс → пн» как близкие, не как 6 → 0:

df['dow_sin'] = np.sin(2 * np.pi * df['dayofweek'] / 7)
df['dow_cos'] = np.cos(2 * np.pi * df['dayofweek'] / 7)

Внешние сигналы: погода, праздники, акции, цены конкурентов. Часто дают +5–15% к качеству.

Target leakage в time series

Target leakage — фича содержит информацию из будущего относительно момента предсказания.

Самые частые источники:

  • Использование mean(target) без shift(1) (включает текущее значение)
  • Future-aware aggregation: «средний таргет за следующие 7 дней» в фиче
  • Forward-fill from future
  • Encoded statistics on full dataset before split (groupby().transform('mean') на всех данных)

Профилактика:

  • Все aggregates считать только по past
  • Encode категории через target encoding с CV-разбиением (out-of-fold target encoding)
  • На каждой строке t использовать только данные с date < t

Если CV даёт сильно лучшие метрики, чем продакшн — almost always leakage.

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

KFold вместо TimeSeriesSplit. Случайное перемешивание для time series = модель видит будущее. Только walk-forward / expanding.

Без gap при использовании rolling_N. Train кончается на t-1, val начинается на t. Rolling_30 на t использует [t-30..t-1] — может включать данные из train. Gap = max(rolling_window).

StandardScaler на full dataset. Mean/std считается по всем данным, включая будущее → leakage. Скейлить только на train, применять к val.

target encoding без out-of-fold. Считаем mean(target) by category на всём датасете → leakage. Use out-of-fold encoding или smoothed (Bayesian).

Использовать timestamp напрямую. Модель учится на «дате» как непрерывной фиче — будет плохо экстраполировать на будущее. Декомпозировать в calendar features.

Не учитывать concept drift. Старая модель деградирует. Регулярный retrain или online learning.

Сравнивать модели на одном fold. Time series fold-to-fold variance высокая. Усреднять по нескольким fold.

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

FAQ

Можно ли использовать k-fold на time series?

Только если задача не temporal (не зависит от времени). Если предсказываем будущее — никогда. K-fold даст оптимистичные метрики.

Walk-forward или expanding — что выбирать?

Зависит от concept drift. Если поведение меняется со временем (рынок, тренды) — walk-forward. Если данные стабильны и больше = лучше — expanding. На практике пробовать оба.

Что такое purged k-fold?

Вариант k-fold, в котором между train и val убираются образцы с overlapping информацией. Используется в финансовых time series (López de Prado).

Как тестировать модель на проде?

Backtest: применять модель «как если бы она была живой» к историческим периодам. На каждый момент t — train на [..., t-1], predict t. Метрики усреднять по большому периоду.

Time series CV для multi-horizon (предсказывать неделю вперёд)?

Использовать direct (одна модель на каждый horizon) или recursive (модель на 1 шаг, рекурсивно). Direct надёжнее на длинных horizons; recursive накапливает ошибки.

Это официальная информация?

Нет. Статья основана на работах по time series ML (López de Prado, Hyndman) и документации sklearn / Darts.


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