Код — forecasting/model.py.
Одна модель градиентного бустинга [17] на все 3 347 рядов (HistGradientBoostingRegressor
из scikit-learn [18]) с пуассоновской функцией потерь, обученная на продажах, нормированных
на масштаб ряда. На каждый горизонт 1–4 недели — отдельная модель.
| Подход | Плюсы | Почему не выбран |
|---|---|---|
| ARIMA, экспоненциальное сглаживание по каждому ряду [1] | классика, интерпретируемость, интервалы прогноза | 3 347 отдельных моделей; на рядах, где 59 % недель нулевые, они вырождаются в среднее; не используют общих закономерностей и справочников; у части рядов меньше года истории |
| Метод Кростона и его варианты [4, 6] | созданы специально для прерывистого спроса | дают плоский прогноз без сезонности и календаря, по ряду отдельно. Полезны как дополнительный ориентир |
| Случайный лес (дипломная работа) | устойчив, мало настроек | глубокие деревья на 650 тыс. строк медленнее и тяжелее; бустинг на табличных данных обычно точнее [12]; нет ранней остановки |
| LightGBM, XGBoost, CatBoost | то же семейство, что выбранная модель, быстрее на больших данных | HistGradientBoostingRegressor — реализация того же алгоритма гистограммного бустинга, что в LightGBM [5], уже входящая в scikit-learn; на 650 тыс. строк разница в скорости несущественна, а лишней зависимости нет. Переход возможен без изменения остального кода |
| Нейросети (DeepAR, N-BEATS, TFT) | учат сложные зависимости, общие для рядов | нужны большие данные и GPU, трудно объяснить прогноз; на табличных данных деревья по-прежнему не уступают [12]; победители M5 использовали бустинг [3] |
| Градиентный бустинг, одна модель на все ряды | выбран | см. ниже |
- Глобальная модель учится на всех рядах сразу. Отдельный ряд с прерывистым спросом даёт слишком мало информации, но закономерности вида «после недели вдвое выше среднего следует возврат к среднему» или «в мае спрос на кровлю растёт» общие для многих рядов. Глобальная модель переносит их между рядами; показано, что она не уступает локальным даже на разнородных рядах [8]. В соревновании M5 (42 840 рядов розничных продаж Walmart) первые места заняли глобальные модели градиентного бустинга на лагах и календарных признаках [3].
- Сам находит взаимодействия. Влияние сезона разное для кровли и метизов, для гипермаркета и дачного минимаркета. Дерево учитывает это разбиениями, не требуя заранее перечислять взаимодействия, как в регрессии.
- Работает с пропусками и категориями. У нового ряда нет лага 52, у недели после закрытия
базы — части лагов.
HistGradientBoostingRegressorобрабатывает NaN сам (для пропусков выбирается лучшая ветвь), а категории — категорию товара, формат базы, единицу — разбивает без one-hot. - Ранняя остановка. Число деревьев выбирается по валидационной выборке, а не задаётся вручную.
- Неотрицательный прогноз при пуассоновской функции потерь — см. ниже.
Используется пуассоновская функция потерь (отклонение Пуассона) с логарифмической связью: модель предсказывает ln ŷ, а ŷ = exp(F(x)) > 0.
Почему. Продажи — неотрицательные величины, близкие к счётным, с большим числом нулей и дисперсией, растущей с уровнем. Для таких данных квадратичная ошибка плохо подходит: она требует, чтобы разброс не зависел от уровня, и может давать отрицательный прогноз. Пуассоновская потеря соответствует данным такого типа, гарантирует ŷ > 0, а логарифмическая связь делает влияние признаков мультипликативным — «сезон повышает спрос на 30 %», а не «на 30 штук», — что естественно для рядов разного масштаба.
Что показал эксперимент. С нормировкой (см. ниже) квадратичная функция потерь дала ту же точность: WAPE 0,847, bias +8,3 %, R² 0,542 против 0,543. Но 0,04 % её прогнозов отрицательны и требуют обрезки. Пуассоновская функция оставлена по теоретическим причинам при равном качестве.
Ряды различаются по масштабу на четыре порядка: средние продажи от 0,14 до 1 095 единиц
в неделю (1-й и 99-й процентили). Дерево делит признак по порогу, и порог «лаг > 50»
означает «продажи выше обычного» для метизов и «продажи упали» для арматуры. Поэтому
в DemandModel:
- масштаб ряда s =
qty_mean26— средние продажи за 26 известных недель (если он 0, s = 1); - все признаки истории продаж и целевая переменная делятся на s;
- добавляется признак
log_scale= ln(1 + s) — чтобы модель знала, насколько ряд крупный; - вес строки при обучении равен s;
- прогноз умножается обратно на s.
Почему вес равен масштабу. Отклонение Пуассона D(y, μ) = 2(y ln(y/μ) − y + μ) обладает свойством D(y/s, μ/s) = D(y, μ) / s. Значит, с весом s функция потерь на нормированных данных в точности равна функции потерь в исходных единицах: крупные ряды по-прежнему важнее мелких, как и должно быть для склада. Меняется не цель обучения, а то, что видят деревья: признак «лаг / среднее > 1,5» означает «на прошлой неделе на 50 % выше обычного» для любого ряда, и закономерность, найденная на одних рядах, применима к другим.
Что показал эксперимент (горизонт 1, тест 30.03–27.09.2026):
| Вариант | WAPE | Bias | R² | Итераций |
|---|---|---|---|---|
| ориентир «среднее за 4 недели» | 0,872 | −0,1 % | 0,489 | — |
| бустинг без нормировки | 0,877 | +9,8 % | 0,365 | 230 |
| бустинг с нормировкой | 0,847 | +8,3 % | 0,543 | 97 |
Без нормировки бустинг проигрывает простому среднему — та же модель, те же признаки. С нормировкой он выигрывает у всех ориентиров на всех горизонтах.
| Параметр | Значение | Почему |
|---|---|---|
loss |
poisson |
см. выше |
learning_rate |
0,05 | небольшой шаг: точнее, чем шаг по умолчанию 0,1, а число деревьев всё равно ограничивает ранняя остановка |
max_iter |
2 000 | верхняя граница; фактически останавливается на 91–97 деревьях |
early_stopping, n_iter_no_change |
да, 50 | остановка, если 50 деревьев подряд не улучшили потерю на валидации |
max_leaf_nodes |
63 | вдвое больше значения по умолчанию: 650 тыс. строк позволяют более сложные деревья, нужные для взаимодействий «сезон × категория × формат» |
min_samples_leaf |
100 | лист не может описывать единичный крупный заказ — защита от подгонки под выбросы |
l2_regularization |
1,0 | сглаживает значения в листьях с малым числом строк |
random_state |
0 | воспроизводимость |
Параметры выбраны из этих соображений, а не перебором по сетке. Систематический подбор — предмет отдельного эксперимента: при нынешнем разбросе результата между тестовыми окнами (см. ниже) выигрыш от подбора на одном окне был бы ненадёжен.
| Часть | Целевые недели | Строк (h = 1) | Для чего |
|---|---|---|---|
| обучение | 11.10.2021 — 22.12.2025 | 653 264 | обучение деревьев |
| валидация | 29.12.2025 — 23.03.2026, 13 недель | 37 786 | ранняя остановка |
| тест | 30.03 — 21.09.2026, 26 недель | 82 347 | оценка качества, один раз |
- По времени, а не случайно. При случайном разбиении в обучении оказались бы недели после тестовых, и модель «знала» бы будущее: уровень продаж, начало сезона, дефицит. Для временных рядов допустимо только обучение на прошлом и проверка на будущем [1, 10].
- 26 недель теста — полгода: покрывают весенний подъём и летний пик, на которых ошибка стоит дороже всего.
- Отдельная валидация нужна, чтобы ранняя остановка не подглядывала в тест.
- Итоговая модель переобучается на всех данных с найденным числом деревьев: последние недели — самые информативные для прогноза будущего, и выбрасывать их нельзя. Метрики при этом описывают модель, обученную без теста.
Модель сравнивается с тремя наивными прогнозами:
| Ориентир | Прогноз на неделю T |
|---|---|
| как h недель назад | продажи последней известной недели |
| как год назад | продажи недели T − 52 |
| среднее за 4 недели | средние продажи за 4 последние известные недели |
Почему. Метрика сама по себе ничего не говорит: WAPE 85 % может быть отличным или провальным результатом в зависимости от данных. Ценность модели — в том, насколько она лучше простого правила, которое не требует обучения [1]. В M5 заметная часть участников не обогнала простые ориентиры [3]. Среднее за 4 недели — самый сильный из трёх: для прерывистого спроса усреднение надёжнее последнего значения.
Признаки week_sin, week_cos, working_days убирались по одному и вместе, модель
переобучалась на трёх окнах по 26 недель (тест в конце данных, сдвинутый на 26 и на 52 недели
назад). Горизонт 1, WAPE / bias:
| Тестовое окно | Сезон | Полная модель | Без sin/cos | Без рабочих дней | Среднее за 4 нед. |
|---|---|---|---|---|---|
| 31.03 — 22.09.2025 | весна — лето | 0,822 / +1,9 % | 0,798 / −7,0 % | 0,823 / +2,3 % | 0,852 / −3,7 % |
| 29.09.2025 — 23.03.2026 | осень — зима | 0,907 / −3,0 % | 1,017 / +17,2 % | 0,920 / −1,3 % | 1,002 / +4,1 % |
| 30.03 — 21.09.2026 | весна — лето | 0,847 / +8,3 % | 0,834 / −1,5 % | 0,846 / +7,9 % | 0,872 / −0,1 % |
| среднее по окнам | 0,859 | 0,883 | 0,863 | 0,909 |
Горизонт 4 ведёт себя так же.
Выводы.
- Сезонные признаки нужны. Без них модель не предвидит осенний спад и в осенне-зимнем окне завышает прогноз на 17 % и проигрывает даже среднему за 4 недели (1,017 против 1,002). В среднем по году полная модель лучше.
- Весной и летом они дают завышение. Модель добавляет сезонный подъём поверх лагов, которые уже выросли вместе с сезоном. Это кандидат на улучшение: например, сезонный профиль категории вместо общей синусоиды.
- Рабочие дни почти не влияют, кроме зимнего окна с новогодними праздниками, где они немного помогают.
- Одного тестового окна мало. Вывод «календарь вредит», сделанный по последнему окну, был бы ошибкой. Оценку стоит перевести на скользящее начало прогноза (rolling origin) — несколько окон с усреднением метрик [1, 10].
Численная неустойчивость. В одном из вариантов эксперимента (без всех календарных признаков, окно 2025 года) прогноз разошёлся: несколько строк получили астрономические значения, WAPE около 10¹⁷. Логарифмическая связь и умножение на масштаб не ограничивают прогноз сверху. В рабочей конфигурации на шести прогонах этого не случилось, но защиту — ограничение прогноза, например кратным максимуму истории ряда, — стоит добавить.
save() записывает модель (joblib), описание в JSON (признаки, коды категорий, периоды,
параметры, метрики) и прогнозы теста. Коды категорий сохраняются, потому что
HistGradientBoostingRegressor различает категории по номерам: если при прогнозе формат базы
«минимаркет» получит другой номер, чем при обучении, модель молча применит правила другого
формата. DemandModel приводит категории при прогнозе к порядку обучения, и это проверяется тестом.
По одному изменению со сравнением метрик до и после:
- оценка на нескольких окнах (rolling origin) вместо одного;
- учёт дефицита: исключение или пометка недель с нулевым остатком, оценка спроса с цензурированием [15];
- ограничение прогноза сверху;
- цена и индекс цен на металл как признаки;
- иерархия: продажи категории на базе и позиции по сети;
- разделение спроса на розницу и юрлиц;
- интервалы прогноза (квантильная функция потерь): для расчёта страхового запаса нужна не только средняя оценка, но и разброс.