Predice las ventas de tu tienda
De la media móvil a Holt-Winters sobre tres años de ventas mensuales. Lo que importa no es la fórmula: es medir el error en meses que el modelo no ha visto.
SALES son 36 meses de ventas de una tienda (sintéticos, en EUR), con subida lenta, pico en diciembre y bajón en agosto. Escribe cuatro funciones:
movingAverage(xs, k, h): h copias de la media de los últimos k valores. Es la vara de medir: si tu modelo no la supera, no lo uses.
holtWinters(xs, alpha, beta, gamma, m, h): suavizado exponencial triple aditivo. Inicia level con la media de la primera estación (m valores), trend con (media de la segunda - media de la primera) / m y seasonal[i] = xs[i] - level para i < m. Después, para cada t desde m:
level = α · (xs[t] - seasonal[t - m]) + (1 - α) · (level + trend)
trend = β · (level - levelAnterior) + (1 - β) · trend
seasonal[t] = γ · (xs[t] - level) + (1 - γ) · seasonal[t - m]
Devuelve { level, trend, forecast }, donde el paso i (desde 1) del pronóstico es level + i · trend + seasonal[n - m + ((i - 1) % m)].
mae(actual, predicted): la media de |actual[i] - predicted[i]|.
backtest(xs, holdout, forecaster): entrena con todo menos los últimos holdout valores, llama a forecaster(train, holdout) y devuelve { forecast, mae } frente a lo que pasó de verdad.
La vista previa pinta la serie y los dos pronósticos sobre los seis meses reservados; mueve α, β y γ y mira el error.
Retos 0/4
- movingAverage repite la media de los últimos k valores
- holtWinters: inicialización, las tres reglas y el pronóstico con la estación
- mae es la media del error absoluto
- backtest sobre los 6 meses reservados: Holt-Winters gana a la media móvil
function movingAverage(xs, k, h) {
// h copias de la media de los últimos k valores
return [];
}
function holtWinters(xs, alpha, beta, gamma, m, h) {
// level = media de la primera estación; trend = (media de la segunda - media de la primera) / m; seasonal[i] = xs[i] - level
// después, para t desde m: level, trend y seasonal[t] con las tres reglas de actualización
// paso i del pronóstico (desde 1): level + i * trend + seasonal[n - m + ((i - 1) % m)]
return { level: 0, trend: 0, forecast: [] };
}
function mae(actual, predicted) {
// media de |actual[i] - predicted[i]|
return 0;
}
function backtest(xs, holdout, forecaster) {
// entrena con todo menos los últimos `holdout` valores, pronostica esos y puntúa frente a lo que pasó
return { forecast: [], mae: 0 };
}
console.log(backtest(SALES, HOLDOUT, (train, h) => movingAverage(train, 3, h)).mae);La idea
Un pronóstico es una apuesta con el error escrito al lado. La media móvil es la apuesta más simple: «el mes que viene será como los últimos tres». No sabe de tendencia ni de Navidad, pero es difícil de batir con pocos datos, y por eso es la vara de medir.
Holt-Winters descompone la serie en tres piezas que se actualizan cada mes: el nivel (dónde estamos), la tendencia (hacia dónde vamos) y la estación (qué le pasa a este mes todos los años). α, β y γ dicen cuánto peso tiene el mes nuevo frente a lo que ya se creía. Con α alto el modelo reacciona rápido y se asusta con cualquier mes raro; con α bajo es tranquilo y tarda en enterarse de un cambio.
Lo importante no es la fórmula sino cómo la mides. Si ajustas α, β y γ mirando el error sobre los mismos meses que usaste para entrenar, siempre te saldrá bien. La vista previa reserva los últimos seis meses, entrena con los 30 anteriores y mide ahí. Ese número sí te dice cuánto fiarte el mes que viene.
Con tres años de datos y una estación de doce meses el modelo va justo: solo ve cada mes del año tres veces. Es un límite honesto, no un fallo.
Hazlo en tu máquina
Con Python y statsmodels, sobre el CSV de este ejercicio (CC BY 4.0). Mismo reparto: 30 meses para entrenar, 6 para medir:
curl -O https://vorluxai.com/data/labs/ventas-tienda-mensual.csv
python3 -c "
import pandas as pd
from statsmodels.tsa.holtwinters import ExponentialSmoothing
s = pd.read_csv('ventas-tienda-mensual.csv').ventas_eur
train, test = s[:-6], s[-6:]
fit = ExponentialSmoothing(train, trend='add', seasonal='add', seasonal_periods=12).fit()
print('MAE', (fit.forecast(6).values - test.values).__abs__().mean())
" Para profundizar: la referencia de Forecasting: Principles and Practice (en inglés) →
¿Esto en producción, con tus datos? Hablemos 15 minutos →