Урок курса
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
Вы прогнозируете продажи на будущие дни. Какой вариант проверки соответствует моменту применения модели?
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
