Forecasting de ventas por SKU
LightGBM con backtesting honesto: validación y test separados, análisis de errores por día y arquitectura en Google Cloud.
WAPE en test
0.730
Fold 5, nunca visto, evaluación recursiva a 7 días
Mejora vs. baseline
−26 %
WAPE 0.784 vs. 1.062 de Seasonal Naive (variante a 28 días)
Panel modelado
20 × 373
20 SKUs por 373 días = 7,460 filas
Features
14
Calendario, lags y estadísticos móviles, sin leakage
El problema
Una tienda con un año de transacciones (diciembre de 2010 a diciembre de 2011) y 20 productos necesita anticipar sus ventas. Definí el problema como unidades netas por producto y por día con un horizonte de 7 días, alineado a la estacionalidad semanal que aparece en los datos.
Trabajé con dos archivos: las transacciones línea por línea (24,215 registros) y el catálogo con la categoría de cada SKU.
La solución explicada en 10 minutos
Presentación en video de todo el recorrido: datos, EDA, features, backtesting, resultados y arquitectura.
Abrir en YouTube ↗Calidad y limpieza de datos
- Solo CustomerID tiene nulos (~20 % de las filas) y no se usa como feature, así que no bloquea nada.
- Eliminé 182 duplicados exactos (0.75 %) y excluí el último día porque estaba truncado a media jornada.
- Agregué a ventas netas diarias por SKU con grilla completa de fechas: un día sin ventas queda en cero, nunca en NaN.
Distribución de ventas netas diarias
Muy asimétrica (skew de 14), con cola larga y valores negativos por devoluciones.
Días-SKU por rango de unidades vendidas por día · eje Y logarítmico
Escala logarítmica en Y: cada marca vale 10 veces la anterior. Así se ve la cola de pedidos grandes y devoluciones (1 a 7 observaciones por rango) junto al pico de ~6,900.
Serie temporal agregada
Tendencia creciente hacia fin de año y oscilación semanal muy regular, en los 373 días del panel.
Ventas netas totales por día, dic 2010 – dic 2011 (373 días). Recorré la serie para ver el valor exacto.
¿Errores o mayoristas?
Los puntos rojos (>300 unidades) del SKU 85123A no son ruido: son compras y cancelaciones repetidas de las mismas cuentas a lo largo del año — demanda real, no un error de captura.
SKU 85123A · todas las transacciones del año
Ventas por día de la semana
Los sábados el negocio no opera: no es un dato faltante, es una regla de calendario.
Por qué WAPE
Con 24 % de días en cero, MAPE se rompe por división por cero. WAPE suma primero todos los errores y todas las ventas y divide una sola vez, así que nunca se rompe y es comparable entre SKUs de distinta escala. MAE y RMSE quedan como referencia en unidades.
Ejemplo con 5 días de un SKU (unidades). Elegí una métrica para ver qué columnas usa.
| Día | Venta real | Predicción | Error abs. | Error² |
|---|---|---|---|---|
| 1 | 10 | 8 | 2 | 4 |
| 2 | 0 | 1 | 1 | 1 |
| 3 | 15 | 20 | 5 | 25 |
| 4 | 0 | 0 | 0 | 0 |
| 5 | 100 | 70 | 30 | 900 |
| Σ | 125 | 38 | 930 |
38 / 125 = 30.4 %
Suma todo primero y divide una sola vez al final: los ceros individuales no rompen nada. Es un número relativo y comparable.
Features y modelo
- 1
Features sin fuga de información
Calendario (día de la semana, semana del año, mes), identidad del producto, lags de 1, 7, 14 y 28 días y medias y desvíos móviles de 7, 14 y 28. Toda feature usa solo información anterior al día predicho, y lo verifiqué recalculando rolling_mean_7 de forma independiente.
- 2
Target signed-log
El target es muy sesgado; entrené LightGBM sobre una transformación signed-log que conserva ceros y negativos, comprime los extremos sin descartar transacciones y es reversible (verificado con un assert de ida y vuelta).
- 3
Hiperparámetros fijos
Sin búsqueda automática: el foco es el método de evaluación, no exprimir la última décima de WAPE.
Backtesting: validación y test separados
No usé un split aleatorio porque mezclaría fechas. Usé expanding window con 5 folds y horizonte de 7 días. Los folds 1 a 4 son la validación donde construí todo el análisis; el fold 5 quedó intocado hasta el final y se evaluó una sola vez. La evaluación es recursiva —predice un día, lo inserta en el historial, recalcula features y sigue— porque es el escenario real de producción.
Resultados (WAPE, menor es mejor)
El WAPE de validación es un poco optimista porque es donde se construyó la intuición; el de test es el número que defiendo.
Real vs. predicho (folds 1–4)
La mayoría de los puntos se agrupa cerca de la diagonal, donde vive la mayor parte de la demanda; los picos grandes (pedidos mayoristas) se subestiman, esperable porque signed-log comprime esos extremos.
560 predicciones (folds 1–4, validación). La línea roja es la predicción perfecta; la mayoría se agrupa cerca de la diagonal y los picos grandes se subestiman.
Residuos por día de la semana
Sábado es casi perfecto (siempre predice cero y siempre es cero). Lunes concentra los errores grandes: 18 de 80 lunes superaron las 100 unidades, con un pico de 1,844 en un pedido mayorista puntual. Miércoles es el día más variable y martes el más limpio de los hábiles. La mediana es levemente positiva en casi todos los días: el modelo subestima algo más de lo que sobreestima.
Caja = rango intercuartílico (folds 1–4). Los puntos son outliers según la regla de Tukey; los que superan el rango visible quedan marcados en el borde (el peor, un lunes, llega a 1,844 u.).
Forecast final a 7 días, por SKU
Con el resultado ya reportado, reentrené con todo el histórico y generé el pronóstico recursivo de 7 días para los 20 SKUs: 140 predicciones sin nulos, infinitos, duplicados ni valores negativos, con los sábados forzados a cero por regla de negocio (todo verificado con asserts). Elegí un producto para ver su historial reciente y el forecast.
SKU
Kitchen · #5 en volumen de los 20
Interpretabilidad
Con SHAP, day_of_week domina, seguido de StockCode, lag_7 y rolling_mean_28.
Importancia media de cada variable (valor absoluto de SHAP) sobre el modelo final, todo el histórico.
Arquitectura en Google Cloud
No es un plan teórico: es el estado verificado en GCP. Los datos y el modelo están en Cloud Storage y el modelo está registrado en Vertex AI Model Registry. No hay un endpoint desplegado a propósito, porque el registro no tiene costo y un endpoint genera facturación por hora. El reentrenamiento semanal sigue el mismo ciclo que el horizonte, y el monitoreo de drift compara el WAPE real semanal contra el de test (0.730), no el de validación.
1 · Del dato al modelo
ventas.csv · productos.csv
Transacciones y catálogo de los 20 SKUs.
Cloud Storage
Bucket con los datos y los artefactos del modelo.
Entrenamiento
Pipeline: LightGBM signed-log sobre todo el histórico.
Vertex AI Model Registry
Modelo versionado y registrado. No tiene costo mientras no haya endpoint.
2 · Del modelo al forecast
Contenedor de serving
Imagen custom para servir las predicciones.
Vertex AI Endpoint
Sin desplegar a propósito: un endpoint factura por hora.
Forecast consumido
Pronóstico de 7 días por SKU para reposición.
3 · Operación continua
Cloud Scheduler
Reentrena cada semana, el mismo ciclo que el horizonte de 7 días.
Monitoring / Logging
Detecta drift comparando el WAPE semanal real contra el de test (0.730).
Limitaciones que dejé explícitas
- Un horizonte de 7 días es más corto que un ciclo típico de reposición de stock; para esa decisión un horizonte mayor sería más relevante.
- La variante a 7 días no incluye baseline propio; la comparación contra Seasonal Naive proviene de la variante a 28 días.
- Hiperparámetros fijos y un único modelo global para los 20 SKUs.