Skip to content

Latest commit

 

History

History
197 lines (156 loc) · 18.8 KB

model.md

File metadata and controls

197 lines (156 loc) · 18.8 KB

Модель

Код — 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:

  1. масштаб ряда s = qty_mean26 — средние продажи за 26 известных недель (если он 0, s = 1);
  2. все признаки истории продаж и целевая переменная делятся на s;
  3. добавляется признак log_scale = ln(1 + s) — чтобы модель знала, насколько ряд крупный;
  4. вес строки при обучении равен s;
  5. прогноз умножается обратно на 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 приводит категории при прогнозе к порядку обучения, и это проверяется тестом.

Что дальше

По одному изменению со сравнением метрик до и после:

  1. оценка на нескольких окнах (rolling origin) вместо одного;
  2. учёт дефицита: исключение или пометка недель с нулевым остатком, оценка спроса с цензурированием [15];
  3. ограничение прогноза сверху;
  4. цена и индекс цен на металл как признаки;
  5. иерархия: продажи категории на базе и позиции по сети;
  6. разделение спроса на розницу и юрлиц;
  7. интервалы прогноза (квантильная функция потерь): для расчёта страхового запаса нужна не только средняя оценка, но и разброс.