Собесов

Сценарий ML: walk-forward CV для time series

ML / Data ScienceTime seriesСредняяMiddle

Условие

Почему random KFold не подходит для time series и как делать walk-forward validation?

Решение

Подход

Random KFold перемешивает наблюдения → train может содержать будущие точки → leakage. На time series CV должен уважать порядок времени.

Walk-forward (expanding window)

Окно train растёт, тестовое окно — следующий период.

[----train1----][test1]
[--------train2--------][test2]
[----------train3-----------][test3]

Walk-forward (sliding window)

Train фиксированной длины, скользит.

[----train1----][test1]
   [----train2----][test2]
      [----train3----][test3]

Реализация

from sklearn.model_selection import TimeSeriesSplit
import numpy as np
 
# Expanding
tscv = TimeSeriesSplit(n_splits=5, test_size=30)  # 30 точек на test
for tr_idx, te_idx in tscv.split(X):
    model.fit(X[tr_idx], y[tr_idx])
    preds = model.predict(X[te_idx])
    score(y[te_idx], preds)
 
# Sliding (вручную)
def sliding_cv(y, train_size=365, test_size=30, step=30):
    n = len(y)
    for start in range(0, n - train_size - test_size + 1, step):
        tr = np.arange(start, start + train_size)
        te = np.arange(start + train_size, start + train_size + test_size)
        yield tr, te

Gap (purged CV)

При forecast h>1 между train и test должен быть gap = h, иначе target из test видится через лаги:

def purged_split(n, train_size, test_size, gap, step):
    for start in range(0, n - train_size - gap - test_size + 1, step):
        tr = np.arange(start, start + train_size)
        te = np.arange(start + train_size + gap, start + train_size + gap + test_size)
        yield tr, te

Что репортить

  • Mean / median по folds (с CI).
  • Каждый fold отдельно — видно стабильность.
  • Trend в performance: если последние folds хуже — model drift.
scores = []
for tr, te in tscv.split(X):
    model.fit(X[tr], y[tr])
    s = mape(y[te], model.predict(X[te]))
    scores.append(s)
print(f"Mean MAPE: {np.mean(scores):.2f}, std: {np.std(scores):.2f}")
print(f"Per fold: {scores}")

Panel data (много серий)

GroupKFold по серии — не подходит для multi-series prediction. Лучше: train на 80% времени всех серий, test на 20% последних timesteps всех серий.

Подводные камни

  1. shuffle=True в KFold на time series — самая типичная ошибка. Метрика переоценена на 20-50%.
  2. Gap = horizon обязательно: при forecast h=7, между train и test должно быть 7 точек разрыва. Иначе таргет в test «протекает» через rolling features.
  3. Sliding vs expanding: sliding устойчив к concept drift (отрезает старые данные); expanding использует все данные.
  4. Hyperparameter tuning: nested CV — outer walk-forward, inner для tuning. Иначе tuning по test fold = leakage.
  5. Stationarity check: если серия меняется radically (COVID), CV scores нестабильны — отделите эту зону.

Эталонный ответ

Random KFold ломает time order → leakage. Walk-forward CV: expanding window (train растёт, sklearn TimeSeriesSplit) или sliding window. При forecast h>1 — gap = h между train и test (purged). Отчёт: mean ± std по folds, тренд per fold для отслеживания drift. Hyperparameter tuning — nested CV.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти