Как посчитать Holt-Winters в SQL
Содержание:
Зачем Holt-Winters
Holt-Winters — расширение exponential smoothing для рядов с трендом и сезонностью. Подходит, когда DAU/revenue имеет недельный паттерн (выше в будни) и при этом растёт со временем. Простой baseline до того, как идти в ARIMA / Prophet.
Три компоненты
L_t— уровеньT_t— тренд (прирост за период)S_t— сезонная составляющая (длина сезонаm)
Управляющие параметры: α, β, γ ∈ (0, 1).
Additive vs multiplicative
- Additive:
Y_t ≈ L_t + T_t + S_{t−m}. Подходит, если амплитуда сезона не растёт. - Multiplicative:
Y_t ≈ (L_t + T_t) × S_{t−m}. Подходит, если сезонные пики растут вместе с уровнем (типично для revenue).
Формулы (additive):
L_t = α(Y_t − S_{t−m}) + (1−α)(L_{t−1} + T_{t−1})
T_t = β(L_t − L_{t−1}) + (1−β) T_{t−1}
S_t = γ(Y_t − L_t) + (1−γ) S_{t−m}
Y_hat_{t+h} = L_t + h T_t + S_{t−m+h}Holt-Winters в SQL
Из-за рекурсии в трёх состояниях SQL-имплементация громоздкая. Базовый каркас (additive, m=7):
WITH RECURSIVE base AS (
SELECT
DATE,
value,
ROW_NUMBER() OVER (ORDER BY DATE) AS rn
FROM daily_metrics
WHERE metric_name = 'dau'
),
init_season AS (
SELECT
value - AVG(value) OVER () AS s_init,
rn
FROM base
WHERE rn <= 7
),
hw AS (
-- t = 8: используем init seasons и flat trend
SELECT
b.DATE, b.value, b.rn,
AVG(b.value) OVER ()::NUMERIC AS l,
0::NUMERIC AS t,
s.s_init AS s
FROM base b
JOIN init_season s ON b.rn = s.rn
WHERE b.rn = 8
UNION ALL
SELECT
b.DATE, b.value, b.rn,
0.3 * (b.value - prev.s) + 0.7 * (prev.l + prev.t) AS l,
0.1 * (0.3 * (b.value - prev.s) + 0.7 * (prev.l + prev.t) - prev.l) + 0.9 * prev.t AS t,
0.2 * (b.value - 0.3 * (b.value - prev.s) - 0.7 * (prev.l + prev.t)) + 0.8 * prev.s AS s
FROM base b
JOIN hw prev ON b.rn = prev.rn + 1
)
SELECT DATE, value, l + t AS hw_state, l, t, s
FROM hw
ORDER BY DATE;Параметры α=0.3, β=0.1, γ=0.2 — стандартные стартовые. Подбор — вне SQL.
В этом упрощённом примере сезонная компонента берётся с шага t−1, а не с t−m. Для корректного Holt-Winters нужен LAG(s, m) через дополнительный CTE или плоская реализация в Python — это место в чистом SQL хрупкое, используйте код выше только как иллюстрацию идеи.
Прогноз с сезонностью
WITH last_state AS (
SELECT l, t, s, rn FROM hw ORDER BY rn DESC LIMIT 1
),
seasonal_last AS (
SELECT rn, s FROM hw
WHERE rn > (SELECT rn FROM last_state) - 7
),
forecast AS (
SELECT
CURRENT_DATE + h AS forecast_date,
ls.l + h * ls.t
+ (SELECT s FROM seasonal_last
WHERE rn = (SELECT rn FROM last_state) - (((7 - h % 7) % 7))) AS y_hat
FROM last_state ls
CROSS JOIN generate_series(1, 14) h
)
SELECT * FROM forecast;Логика сезонной индексации в SQL — самое неудобное место. На практике вынесите расчёт в Python и сохраняйте только результат прогноза.
Частые ошибки
Ошибка 1. Длина сезона не угадана.
m=7 для дневной, m=12 для месячной, m=24 для часовой. Использовать FFT / ACF, чтобы найти m, а не угадывать.
Ошибка 2. Multiplicative на данных с нулями. Деление на S_{t-m} = 0 ломает recursion. Если y_t может быть 0, используйте additive.
Ошибка 3. Init seasonal как value - AVG.
Грубый старт. В библиотеках (statsmodels) используют сезонную декомпозицию first season.
Ошибка 4. Не подбирать α, β, γ. Стартовые 0.3/0.1/0.2 разумны, но оптимальные через optimize вне SQL.
Ошибка 5. Прогноз horizon > 2 сезона. HW лучше всего на коротком horizon (1-2 сезона). На длинном — Prophet / SARIMA.
Связанные темы
- Как посчитать exponential smoothing в SQL
- Как посчитать seasonality в SQL
- Holt-Winters на собесе DS
- Time series forecasting для аналитика
FAQ
Additive или multiplicative?
Multiplicative — если амплитуда растёт с уровнем (revenue). Additive — если постоянная (DAU стабильного продукта).
Длина сезона m?
7 дней, 12 месяцев, 24 часа — типичные. Найти через autocorrelation.
Holt-Winters vs Prophet?
Prophet удобнее, обрабатывает holidays. Holt-Winters — классика, меньше параметров.
SQL медленно?
Да. На больших рядах считайте в Python, в SQL храните результаты.
Как оценить ошибку?
Train/test split (последние 4 недели — test). MAPE / MAE на test.