Урок курса

TimeSeriesSplit: валидация модели по времени без утечки

ML Engineer / Data Scientist: табличные модели в продакшн

Зачем модели проверка по времени

Если модель предсказывает будущие продажи, качество нужно измерять на данных, которые идут позже обучения. Случайное разделение строк может поместить записи из будущего в обучение, а более ранние — в проверку. Тогда тест не воспроизводит реальный момент применения модели.

Это не означает, что случайный split запрещён для любой таблицы с датой. Сначала сформулируйте задачу: какую величину, для какого объекта и на какой момент вы предсказываете? Для прогноза будущего временной порядок обычно является частью контракта проверки.

Пример: прогноз спроса на следующий день

Есть 12 последовательных дней наблюдений, по одной записи в день. Признаки строятся на информации, доступной перед прогнозируемым днём. Хотим несколько раз обучить модель на прошлом и проверить её на следующих двух днях.

Схема expanding window увеличивает обучающее окно: история накапливается, а проверка сдвигается вперёд. В отличие от sliding window старые наблюдения из train автоматически не удаляются.

Добавим gap=1: одно наблюдение между концом train и началом validation не используется в обучении. Это промежуток в строках, а не магическое универсальное лечение утечки.

Индексы train и validation на Python

Следующий пример показывает разбиение без сторонних библиотек. Он рассчитан именно на последовательность из 12 регулярно расположенных наблюдений и фиксированный размер проверки:

days = list(range(1, 13))
n_splits = 3
test_size = 2
gap = 1

first_test = len(days) - n_splits * test_size
for fold, start in enumerate(range(first_test, len(days), test_size), 1):
    train = days[:start - gap]
    validation = days[start:start + test_size]
    print(fold, "train:", train, "validation:", validation)

Результат:

1 train: [1, 2, 3, 4, 5] validation: [7, 8]
2 train: [1, 2, 3, 4, 5, 6, 7] validation: [9, 10]
3 train: [1, 2, 3, 4, 5, 6, 7, 8, 9] validation: [11, 12]

В первом разбиении день 6 попал в gap, во втором — день 8, в третьем — день 10. Более ранняя проверочная запись может попасть в обучение следующего фолда: к более позднему моменту времени она уже стала прошлым.

То же через sklearn.model_selection.TimeSeriesSplit

Если scikit-learn установлен в вашем окружении, аналогичное разбиение задаётся готовым классом:

from sklearn.model_selection import TimeSeriesSplit

days = list(range(1, 13))
splitter = TimeSeriesSplit(n_splits=3, test_size=2, gap=1)
for train_idx, valid_idx in splitter.split(days):
    print([days[i] for i in train_idx], [days[i] for i in valid_idx])

n_splits задаёт число разбиений, test_size — размер проверочного окна, gap — количество исключённых перед ним наблюдений. max_train_size позволяет ограничить длину обучения. Класс работает с порядком строк: сортировку по времени и проверку качества дат нужно сделать заранее. Для сравнения качества по одинаковым временным интервалам наблюдения должны быть расположены с одинаковым временным шагом.

Если за один день есть тысячи строк, gap=1 исключит одну строку, а не один день. Для таких панельных данных часто нужны собственные границы по дате: все записи одной даты должны оказаться по нужную сторону границы. Не заменяйте границу «до 1 мая» границей «первые 80% строк», пока не проверили распределение объектов во времени.

Правильный split не спасает от неправильных признаков

Представьте, что признак monthly_total посчитан по всему месяцу, хотя прогноз делается в его первый день. В таблице будущая сумма уже известна, но в рабочей системе она ещё не могла существовать. Модель воспользуется этой подсказкой даже при строгом временном разделении.

Другой пример — признак «итоговый статус заказа» для прогноза в момент оформления. Такой статус появляется позже. Перед обучением для каждого поля стоит записать время доступности: когда приложение реально могло получить значение?

Преобразования, которым требуется обучение, тоже выполняются внутри каждого фолда. Среднее для заполнения пропусков, кодировщик категорий или подбор признаков нельзя сначала вычислить на train+validation, а затем оценивать на validation. Проверочные записи преобразуются уже обученным на train преобразователем.

Лаги требуют такой же аккуратности. Продажи вчера могут быть доступным признаком для завтрашнего прогноза, а продажи завтра — нет. Если прогнозируется сумма за несколько будущих дней, около границы train может потребоваться больший промежуток, потому что целевая величина частично захватывает проверочный период. Размер gap обосновывают горизонтом и моментами доступности, а не выбирают только ради красивой метрики.

Как сравнивать модели на этих фолдах

Обучайте каждую модель отдельно на train текущего фолда и оценивайте на его validation. Используйте одинаковые границы для baseline и более сложной модели. Кроме среднего качества смотрите результат каждого временного окна: ошибка может резко увеличиваться в определённый сезон или после смены продукта.

Для регрессии можно начать с MAE и простого baseline вроде прошлого доступного значения. Отдельный самый поздний test-период оставьте для финальной проверки: если постоянно выбирать параметры по нему, он перестанет быть независимым контролем.

Этот урок — бесплатный открытый материал. В платном курсе табличных моделей в продакшн можно посмотреть дальнейшую программу. Если нужна базовая разница между train, validation и test, сначала разберите открытый урок о разделении данных.

Параметры и ограничения класса: TimeSeriesSplit в документации scikit-learn.

Задача 1

Вы прогнозируете продажи на будущие дни. Какой вариант проверки соответствует моменту применения модели?

Выберите один вариант
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку