Прогнозирование спроса — задача, где соблазн сразу взять «модель посложнее» особенно велик, а цена ошибки измеряется не в метриках, а в замороженных запасах и упущенных продажах. Поэтому ниже мы идём по иерархии методов снизу вверх: каждый следующий уровень должен доказать своё право на существование, обыграв предыдущий на честной валидации. Если не обыгрывает — его сложность не оправдана. Это и есть главная дисциплина прикладного прогноза, на которой строится наша работа по прогнозу спроса.
Наивный прогноз: почему baseline обязателен всегда
Самый простой прогноз — наивный: завтра будет столько же,
сколько сегодня (ŷ_t = y_{t−1}). Для рядов
с выраженной сезонностью его расширяют до сезонного наивного:
прогноз на этот вторник равен спросу прошлого вторника, прогноз на декабрь —
спросу прошлого декабря (ŷ_t = y_{t−m},
где m — период сезонности). Это не «плохая модель» — это
измерительная линейка.
Смысл baseline в том, что любая ошибка прогноза бессмысленна в отрыве от сравнения. «MAPE 12 %» сам по себе не говорит ничего: для ряда с сильной недельной сезонностью сезонный наивный метод нередко даёт ту же ошибку без единой строки моделирования. Если ваша SARIMA или бустинг не обыгрывают сезонный наивный на отложенной выборке — они не работают, как бы красиво ни выглядела внутренняя статистика. Поэтому baseline считают первым и сравнивают с ним всё остальное — это не формальность, а защита от самообмана.
Сглаживание: от скользящего среднего до Holt-Winters
Следующий уровень — методы сглаживания. Скользящее среднее
усредняет последние k наблюдений; оно гасит шум, но отстаёт от тренда
и не умеет работать с сезонностью. Экспоненциальное сглаживание
(simple exponential smoothing) — шаг вперёд: оно даёт убывающие веса прошлым
наблюдениям через параметр сглаживания α,
реагируя на свежие данные быстрее.
Практически полезное семейство — ETS (error, trend, seasonality), в частном случае Holt-Winters. Метод Хольта добавляет компоненту тренда, а Хольта-Уинтерса — ещё и сезонность, причём в двух формах: аддитивной (сезонные колебания примерно постоянны по амплитуде) и мультипликативной (амплитуда растёт пропорционально уровню ряда). Выбор между ними содержательный: если в высокий сезон спрос отклоняется от среднего не на «+200 штук», а на «+30 %» — нужна мультипликативная сезонность. Для большинства гладких рядов с устойчивой сезонностью ETS/Holt-Winters даёт крепкий результат при минимуме параметров и остаётся сильным базовым кандидатом.
SARIMA: когда она оправдана
SARIMA (seasonal ARIMA) — это ARIMA с явными сезонными
членами, записываемая как
SARIMA(p,d,q)(P,D,Q)_m: несезонные порядки
авторегрессии, интегрирования и скользящего среднего плюс их сезонные аналоги с
периодом m. Модель опирается на предположение о (после взятия разностей)
стационарности ряда и линейной зависимости от собственного прошлого и прошлых
ошибок.
SARIMA оправдана, когда у вас один ряд с выраженной автокорреляционной структурой и регулярной сезонностью, относительно немного экзогенных факторов и есть ценность в статистически обоснованных интервалах прогноза. Платой становится чувствительность к спецификации: порядки подбирают по ACF/PACF и информационным критериям (AIC/BIC), а нарушение стационарности или структурные сдвиги быстро ломают качество. На практике SARIMA часто проигрывает не в точности, а в трудозатратах — когда рядов сотни (по одному на SKU), ручная диагностика каждого нереалистична, и инженеры уходят либо в автоматический подбор, либо в ML-подход с общей моделью на все ряды сразу.
ML и фиче-инжиниринг: когда бустинг даёт прирост
Градиентный бустинг по деревьям (LightGBM, CatBoost, XGBoost) выигрывает в задачах прогноза спроса не магией алгоритма, а тем, что позволяет естественно подмешать множество признаков и обучить одну модель сразу на все SKU. Сила здесь — в фиче-инжиниринге временного ряда:
- Лаги целевой переменной (спрос неделю, две, год назад) — они несут автокорреляцию, которую SARIMA моделирует явно.
- Скользящие статистики (среднее, медиана, стандартное отклонение за окно) — сглаженный уровень и волатильность недавнего спроса.
- Календарные признаки — день недели, неделя года, месяц, праздники, длинные выходные.
- Промо и цена — флаг акции, глубина скидки, относительная цена к конкурентам или к собственной средней.
Бустинг даёт прирост там, где есть нелинейности и взаимодействия признаков (например, эффект промо зависит от дня недели и от категории), много экзогенных факторов и достаточно данных, чтобы их выучить. Но честный ответ — он помогает не всегда. На коротких, гладких, сильно сезонных рядах с малым числом внешних драйверов ETS или сезонный наивный нередко не уступают бустингу, а интерпретируются проще. Деревья к тому же плохо экстраполируют тренд за пределы обучающей выборки: если спрос структурно растёт, прогноз «упрётся в потолок» виденных значений, и тренд приходится выносить в отдельный признак или детрендировать ряд заранее. Сложность модели — это обязательство, а не достижение; брать её стоит только когда она окупается на валидации.
Метрики ошибки: MAPE, WAPE, MASE
Метрика — это не техническая деталь, а то, что определяет, какую модель вы в итоге выберете. Самая популярная — MAPE (mean absolute percentage error), средняя абсолютная процентная ошибка. У неё есть удобство (выражена в процентах, понятна менеджменту) и серьёзные изъяны:
- Деление на ноль и взрыв на низких продажах. При
y_t = 0MAPE не определена, а при малых продажах (1–2 штуки) одна штука промаха даёт 100 %+ ошибки. Для прерывистого спроса MAPE практически непригодна. - Асимметрия. MAPE сильнее штрафует перепрогноз, чем недопрогноз, и потому систематически толкает модели к занижению.
Более устойчивые альтернативы:
- WAPE (weighted absolute percentage error), он же MAD/Mean-ratio — сумма абсолютных ошибок, делённая на сумму фактического спроса. Он не взрывается на нулях отдельных периодов и взвешивает ошибку по объёму, что ближе к деньгам: промах по ходовому SKU весит больше, чем по неходовому.
- MASE (mean absolute scaled error) — ошибка, нормированная на ошибку наивного прогноза на обучающей выборке. MASE < 1 означает, что модель в среднем точнее наивного метода, MASE > 1 — что хуже него. Это, по сути, встроенное сравнение с baseline, что делает MASE особенно честной метрикой.
И отдельно — бизнес-метрика не равна статистической. Стоимость единицы излишка (хранение, списание, заморозка капитала) почти никогда не равна стоимости единицы дефицита (упущенная маржа, отток клиента). Симметричная MAPE/WAPE этого не видит. Если перепрогноз и недопрогноз стоят по-разному, оптимизировать нужно асимметричную функцию потерь (например, pinball loss на нужном квантиле) и измерять результат в деньгах, а не только в процентах ошибки. Об этой связке метрик и решений мы подробно говорим в контексте бизнес-аналитики.
Валидация временных рядов: главная техническая честность
Здесь сосредоточена бо́льшая часть тихих провалов прогноза спроса. Обычная перекрёстная проверка (k-fold) для временных рядов некорректна: она перемешивает наблюдения и позволяет модели учиться на будущем, предсказывая прошлое. Метрики на такой «валидации» оптимистичны и не воспроизводятся в проде.
Правильный подход — бэктест с расширяющимся (или скользящим) окном, он же rolling-origin / walk-forward: обучаемся на данных до момента T, прогнозируем на горизонт, сдвигаем T вперёд, повторяем. Модель всегда видит только прошлое относительно точки прогноза — ровно как в реальной эксплуатации. Подробнее о том, почему это и есть честная проверка качества, — на странице валидации моделей.
Вторая ловушка — утечка будущего через признаки (target/feature leakage). Классический пример: флаг промо. На обучении вы знаете, что акция была; но в момент прогноза будущей недели вы должны использовать только то, что известно на этот момент — плановую акцию, а не фактическую. Любая скользящая статистика, лаг или агрегат, посчитанные с заглядыванием вперёд, завышают качество на бэктесте и рушатся в проде. Правило простое: каждый признак в строке за дату t должен быть вычислим, используя только данные до t.
Третий случай — интермиттентный (прерывистый) спрос: редкие SKU, где продажи происходят нерегулярно, а между ними — нули. Здесь и MAPE бессмысленна, и обычные модели плохи. Классический инструмент — метод Кростона (Croston) и его варианты (SBA), которые отдельно моделируют размер ненулевого спроса и интервал между продажами. Для запасов по таким позициям важнее не точечный прогноз, а корректная оценка распределения и нужного квантиля.
Воспроизводимый код: baseline и rolling-origin бэктест
Ниже минимальный исполнимый пример на Python: синтетический ряд с трендом и недельной сезонностью, сезонный наивный baseline, простой набор лаговых и скользящих признаков с защитой от утечки и бэктест с расширяющимся окном. Числа в примере синтетические и иллюстративные — данные генерируются кодом для воспроизводимости без внешних файлов; на реальных данных результаты будут другими.
# Синтетический пример: данные генерируются для воспроизводимости.
# Числа иллюстративны и не отражают реальных бизнес-показателей.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 365 * 2 # два года дневных данных
t = np.arange(n)
trend = 50 + 0.05 * t # медленный рост
weekly = 8 * np.sin(2 * np.pi * t / 7) # недельная сезонность
noise = rng.normal(0, 3, n)
demand = np.maximum(0, trend + weekly + noise).round()
idx = pd.date_range("2024-01-01", periods=n, freq="D")
df = pd.DataFrame({"y": demand}, index=idx)
# Метрики. WAPE устойчива к нулям отдельных периодов; MASE сравнивает с наивным.
def wape(y_true, y_pred):
return np.abs(y_true - y_pred).sum() / np.abs(y_true).sum()
def mase(y_true, y_pred, y_train, m=7):
# знаменатель — ошибка сезонного наивного на обучающей выборке
# (сезонная разность с лагом m: |y_t − y_(t−m)|, а не разность m-го порядка)
naive_err = np.abs(y_train[m:] - y_train[:-m]).mean()
return np.abs(y_true - y_pred).mean() / naive_err
Признаки строим только из прошлого: лаги и скользящие средние сдвигаем так, чтобы строка за дату t не «видела» спрос самого дня t. Затем прогоняем бэктест с расширяющимся окном — это и есть корректная для рядов валидация.
# Фиче-инжиниринг без утечки: всё считается со сдвигом на 1 шаг назад.
def make_features(s):
out = pd.DataFrame(index=s.index)
for lag in (1, 7, 14):
out[f"lag_{lag}"] = s.shift(lag)
# rolling считаем по уже сдвинутому ряду -> не подсматриваем текущий день
out["roll7_mean"] = s.shift(1).rolling(7).mean()
out["roll7_std"] = s.shift(1).rolling(7).std()
out["dow"] = out.index.dayofweek
return out
from sklearn.ensemble import HistGradientBoostingRegressor
X_all = make_features(df["y"])
y_all = df["y"]
# Бэктест rolling-origin: расширяющееся окно, горизонт 7 дней.
horizon, step = 7, 7
start = 400 # минимальная история до первого прогноза
rows = []
for origin in range(start, n - horizon, step):
tr = slice(0, origin)
te = slice(origin, origin + horizon)
Xtr, ytr = X_all.iloc[tr].dropna(), y_all.iloc[tr]
ytr = ytr.loc[Xtr.index]
model = HistGradientBoostingRegressor(max_iter=300, random_state=0)
model.fit(Xtr, ytr)
pred_ml = model.predict(X_all.iloc[te])
# baseline: сезонный наивный (значение неделей ранее)
pred_naive = y_all.shift(7).iloc[te].values
yt = y_all.iloc[te].values
rows.append({
"wape_ml": wape(yt, pred_ml),
"wape_naive": wape(yt, pred_naive),
"mase_ml": mase(yt, pred_ml, ytr.values),
})
res = pd.DataFrame(rows).mean()
print(res.round(3))
# Решение принимаем по сравнению: ML оправдан, только если wape_ml < wape_naive
# и mase_ml < 1 устойчиво по окнам, а не в среднем по счастливому прогону.
Какой метод выбрать
Универсального ответа нет — выбор зависит от структуры ряда, числа SKU и доступных внешних факторов. Ориентир ниже стоит читать как стартовую точку, а не как догму: финальное решение всегда принимает бэктест.
| Метод | Когда применять | Типичная ситуация |
|---|---|---|
| Наивный / сезонный наивный | Всегда — как baseline для сравнения. | Любой ряд; точка отсчёта, которую обязаны обыграть остальные. |
| ETS / Holt-Winters | Гладкий ряд с устойчивым трендом и сезонностью, мало внешних факторов. | Стабильный спрос на зрелый товар с понятной недельной/годовой сезонностью. |
| SARIMA | Один ряд с выраженной автокорреляцией; нужны статистические интервалы. | Агрегированный спрос по категории, ручная диагностика реалистична. |
| Бустинг (LightGBM/CatBoost) | Много SKU, нелинейности, промо/цена/календарь как признаки. | Розница/FMCG: одна модель на сотни позиций, сильное влияние акций. |
| Croston / SBA | Прерывистый спрос с частыми нулями между продажами. | Редкие SKU, запчасти, «длинный хвост» ассортимента. |
Практический порядок работы
- Зафиксировать горизонт, гранулярность (SKU/категория, день/неделя) и бизнес-цель прогноза — от этого зависит и метрика, и метод.
- Привести данные к регулярной частоте, разобраться с пропусками, нулями, выбросами и календарём промо.
- Построить наивный и сезонный наивный baseline и зафиксировать его ошибку.
- Выбрать устойчивую метрику (WAPE/MASE) и, если потери асимметричны, добавить денежную/квантильную оценку.
- Настроить корректную валидацию: rolling-origin бэктест, признаки без утечки будущего.
- Наращивать сложность модели только пока она устойчиво обыгрывает baseline на бэктесте; зафиксировать ограничения и вынести результат в решение.
Если нужен прогноз спроса под конкретную задачу — с честным baseline, воспроизводимым кодом, корректной валидацией и отчётом для руководства — это формат, в котором мы работаем в StatGazer.