Eyisto Aguilar Trejo
Volver al portafolio
Challenge técnico · Forecasting

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

Resto de las transacciones (retail) Pedidos grandes (>300 u.)

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íaVenta realPredicciónError abs.Error²
110824
20111
31520525
40000
51007030900
Σ12538930

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. 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. 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. 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.

Fold 1
Fold 2
Fold 3
Fold 4
Fold 5test
Train Validación Test (reservado)
Pasá el mouse por una franja para ver las fechas exactas del fold.

Resultados (WAPE, menor es mejor)

Validación (folds 1–4)0.707
Test (fold 5, nunca visto)resultado final0.730
LightGBM · evaluación a 28 días0.784
Seasonal Naive · evaluación a 28 díasbaseline1.062

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.

-92 u.-32 u.27 u.87 u.146 u.LunesMartesMiércolesJuevesViernesSábadoDomingo

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

Histórico real Forecast a 7 días

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

Verificado en GCP

ventas.csv · productos.csv

Transacciones y catálogo de los 20 SKUs.

Verificado en GCP

Cloud Storage

Bucket con los datos y los artefactos del modelo.

Verificado en GCP

Entrenamiento

Pipeline: LightGBM signed-log sobre todo el histórico.

Verificado en GCP

Vertex AI Model Registry

Modelo versionado y registrado. No tiene costo mientras no haya endpoint.

2 · Del modelo al forecast

Pendiente

Contenedor de serving

Imagen custom para servir las predicciones.

Pendiente

Vertex AI Endpoint

Sin desplegar a propósito: un endpoint factura por hora.

Objetivo final

Forecast consumido

Pronóstico de 7 días por SKU para reposición.

3 · Operación continua

Pendiente

Cloud Scheduler

Reentrena cada semana, el mismo ciclo que el horizonte de 7 días.

Pendiente

Monitoring / Logging

Detecta drift comparando el WAPE semanal real contra el de test (0.730).

Implementado y verificado en GCP Pendiente para producción Objetivo final

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.