TimeSeriesSplit: как выбрать gap при прогнозе на несколько дней вперёд

Вы прогнозируете продажи через три дня. Таблица отсортирована по дате, для проверки выбран TimeSeriesSplit, и тест всегда находится позже обучения. Кажется, временной порядок соблюдён. Но если строка за понедельник имеет целевое значение за четверг, то в последней обучающей строке перед тестом может оказаться результат, который на момент первого тестового прогноза ещё не наступил. Индексы строк идут правильно, а момент доступности метки — уже нет.

Проверьте время, когда становится известен ответ

Для каждого объекта запишите две даты: когда вы делаете прогноз и когда узнаёте правильный ответ. При горизонте в три дня объект с датой прогноза 4 имеет метку из дня 7. Если тест начинается утром дня 8, этот ответ уже известен. Объект с датой 5 несёт метку дня 8; утром дня 8 её ещё нельзя использовать при обучении. Именно это различие нужно заложить в разделение.

Параметр gap у TimeSeriesSplit исключает указанное число строк с конца train перед началом test. Если строки идут по одной на день, прогноз делается до появления измерения за тестовый день, а целевая метка относится к дню t + horizon, безопасное в этой постановке значение — gap = horizon. Последняя обучающая строка окажется на четыре дня раньше первой тестовой: её метка успеет созреть вчера. Вывод привязан к расписанию данных, а не к магическому числу в библиотеке.

Посмотрите на границу каждого фолда

Ниже ряд содержит измерения за 17 дней. Для 14 дат можно построить цель с горизонтом три дня. В качестве признака взят только номер дня недели — он известен утром, когда делается прогноз, до измерения за этот день. Код создаёт временные фолды и печатает последнюю дату обучения, дату её целевого значения и первую дату теста. Проверка assert закрепляет правило: даже самая поздняя метка обучения должна быть известна до тестового прогноза.

import numpy as np
from sklearn.model_selection import TimeSeriesSplit

horizon = 3
series = np.arange(17)  # измерения по дням 0..16
origins = np.arange(14)  # для этих дат уже известна цель через 3 дня
X = (origins % 7).reshape(-1, 1)  # день недели известен утром
y = series[origins + horizon]

cv = TimeSeriesSplit(n_splits=3, test_size=2, gap=horizon)
for train, test in cv.split(X):
    last_train = int(origins[train[-1]])
    first_test = int(origins[test[0]])
    last_known_target = last_train + horizon
    assert last_known_target < first_test
    print(f"train до {last_train}, цель до {last_known_target}, test с {first_test}")

Получаются три границы: train до 4, цель до 7, test с 8; затем train до 6, цель до 9, test с 10; наконец train до 8, цель до 11, test с 12. Если поставить gap=0, первый train закончится днём 7, а его метка придёт только в день 10 — позже начала теста. Модель получает знание будущего даже при безупречно отсортированной таблице.

Сам TimeSeriesSplit видит лишь номера строк. Он не знает дату публикации признака, время поступления позднего события или то, когда сформирована метка. Для дневного примера индексы соответствуют дням один к одному. Если строк бывает несколько в день, gap=3 означает три строки, а не три дня. Тогда сначала строят фолды по календарным границам и проверяют реальные timestamps, либо подготавливают по одному объекту на интервал.

Что ещё может протечь через временную границу

После выбора фолдов проверьте каждый признак относительно времени прогноза. Скользящее среднее спроса должно брать наблюдения, уже доступные в момент запуска модели; агрегат «продажи за день» нельзя подставлять утром того же дня. Если данные приходят с двухдневной задержкой, границу обучения может определять именно задержка поступления, а не один лишь горизонт цели. Полезно хранить рядом с признаком время события и время его фактической доступности. Для каждой тестовой строки можно программно проверить feature_available_at <= prediction_at.

Предобработку тоже нужно обучать внутри каждого фолда. Если заполнить пропуски медианой по всей таблице до разделения, информация о будущем окажется в train через параметры преобразования. Удобный способ — объединить преобразования и модель в Pipeline и передать splitter в оценку. В этой статье мы проверяли именно временную границу метки; качество реальной модели потребует отдельного прогона такого пайплайна и метрики на отложенных датах.

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

Куда двигаться дальше

В курсе «Data Scientist: от данных до деплоя моделей» вы пройдёте от проверки источников и построения признаков до выбора стратегии валидации, сравнения baseline и более сильной модели, анализа ошибок и сервиса предсказаний. На временной задаче это превращается в воспроизводимый эксперимент: сохранить срез данных, явно задать момент прогноза, проверить фолды и только затем сравнивать качество. Откройте программу курса, чтобы увидеть дальнейшие шаги.

Продолжить на практике

  • Data Scientist: от данных до деплоя моделей ↗

    Практический курс по Data Science: от постановки ML-задачи и работы с данными до inference API, CI/CD и мониторинга моделей в продакшне. Вы пройдёте EDA, feature engineering, обучение и оценку моделей, experiment tracking, error analysis и deployment — чтобы собирать полноценный рабочий контур вокруг ML-решения.