📊 Reto 2 — Ciencia de Datos

Predictor de
Demanda

Datos sucios, series temporales y forecasting. Limpia un historial de ventas roto y genera predicciones con intervalo de confianza para el equipo de compras.

3 niveles de dificultad
Chart.js · Series temporales
Holt-Winters · ARIMA · IC
Ver reto
Retos del día

Reto 2: Predictor de Demanda

El área de logística te entrega un historial de ventas con dos años de datos sucios. Tu misión: limpiarlos, modelarlos y proyectar la demanda futura con intervalo de confianza.

Este taller simula un escenario real de negocio: el área de logística de una empresa te entrega un historial de ventas semanales con dos años de datos, pero el archivo está lleno de problemas. Tu misión es construir un pipeline completo que limpie esos datos y genere una predicción confiable.

1
🎯Objetivo Central

Dado un dataset histórico con huecos temporales y anomalías, construir una visualización interactiva que:

  • Diagnostique los problemas del dataset
  • Limpie e impute los datos faltantes
  • Ajuste un modelo de serie temporal
  • Proyecte la demanda futura con intervalo de confianza
2
🏭Contexto del Escenario

Una empresa distribuidora necesita planificar inventario para las próximas 8–12 semanas. El historial exportado del ERP tiene los siguientes problemas:

  • Semanas sin registro (NaN) por cierres de sistema o errores de carga
  • Un bloque de 3–7 semanas consecutivas completamente perdidas
  • Valores negativos generados por devoluciones mal contabilizadas
  • Picos anormalmente altos por duplicación de registros o promociones
  • Ruido aleatorio y tendencia estacional no documentada
"Los datos del mundo real siempre llegan rotos. El valor del científico de datos está en saber qué hacer con ellos."
3
🚫Lo que NO es este reto

No se trata de memorizar un algoritmo.

  • Entender por qué los datos llegan rotos
  • Saber cómo diagnosticarlos antes de tocarlos
  • Aprender cómo repararlos con criterio
  • Comunicar la incertidumbre de cualquier predicción

El reto tiene tres niveles. Elige según tu experiencia — todos parten del mismo dataset generado automáticamente en el browser.

Nivel 1
🌱 Explorador
⏱ aprox. 1.5 horas
  • Cargar array de datos (≥ 52 semanas)
  • Identificar y contar NaN y outliers
  • Imputar con forward fill o media simple
  • Graficar original vs. limpio en el mismo eje
  • Mostrar media y desviación estándar
Nivel 2
🔧 Constructor
⏱ aprox. 2.5 horas
  • Dos métodos de imputación comparables
  • Holt-Winters triple o ARIMA simplificado
  • Calcular MAE y MAPE sobre el histórico
  • Forecast 8–12 semanas con IC al 90%
  • Gráfica interactiva con leyenda y tooltip
Nivel 3
🏗️ Arquitecto
⏱ aprox. 3 horas
  • Comparar ≥ 3 modelos, elegir por MAPE
  • Controles en tiempo real: método + horizonte
  • Histograma de residuos para diagnóstico
  • Tabla: semana, central, IC inf., IC sup.
  • Log de anomalías con tipo, semana y valor

💡 ¿Qué nivel elijo?

  • Si es tu primera vez con series temporales → Nivel 1
  • Si ya sabes JavaScript y quieres aplicar modelos reales → Nivel 2
  • Si quieres construir un dashboard completo con controles interactivos → Nivel 3
  • Todos los niveles parten del mismo dataset — puedes subir de nivel en cualquier momento

El pipeline tiene cuatro etapas secuenciales. Cada una es un módulo independiente que alimenta al siguiente.

1
🔍Datos crudos — Exploración y diagnóstico

Antes de limpiar, hay que entender qué está mal.

  • Cargar o generar el array de datos semanales (≥ 52 puntos)
  • Contar valores NaN, negativos y outliers por tipo
  • Detectar el bloque de semanas consecutivas faltantes
  • Visualizar la serie cruda con marcadores de anomalía en rojo
  • Generar un log de incidencias con semana, tipo y valor
Array.filter()Math.abs()Chart.js scatter
2
🧹Limpieza e imputación

Reemplazar valores inválidos con estimaciones razonables.

  • Detectar outliers con umbral de desviaciones estándar (σ configurable)
  • Eliminar negativos — tratarlos como NaN
  • Imputar con: forward fill, media móvil, interpolación lineal o spline
  • Mostrar original vs. limpio en el mismo eje con colores distintos
  • Calcular métricas antes/después: completitud, media, desviación estándar
Forward fillMedia móvil (w=5)Interpolación lineal
3
🤖Modelo — Ajuste a la serie limpia

Entrenar un modelo de serie temporal sobre los datos limpios.

  • Holt-Winters triple: captura nivel, tendencia y estacionalidad — parámetros α, β, γ
  • ARIMA simplificado: trabaja con diferencias de la serie
  • Regresión estacional: tendencia lineal + factor estacional periódico
  • Ensemble: promedio de los tres — reduce la varianza
  • Evaluar con MAE, MAPE, RMSE y R²
Holt-WintersARIMAMAE · MAPE · R²
4
📡Forecast — Proyección con intervalo de confianza

Proyectar la demanda futura comunicando la incertidumbre.

  • Generar predicción para 4–52 semanas hacia adelante
  • Intervalo de confianza al 80%, 90% o 95%
  • El IC crece con el horizonte — esto es intencional y matemáticamente correcto
  • Gráfica: histórico + forecast + banda del IC
  • Tabla con columnas: semana, valor central, IC inferior, IC superior
IC = z·RMSE·√(1+h/n)Chart.js fill

💡 Entregables mínimos

  • Dataset de al menos 52 semanas con anomalías identificadas y loggeadas
  • Serie limpia con 100% de completitud y sin valores negativos
  • Modelo ajustado con MAPE calculado y mostrado al usuario
  • Forecast de 8–12 semanas con banda de intervalo de confianza visible

Conceptos técnicos clave para implementar cada etapa del pipeline correctamente en JavaScript puro.

1
🎲Generar datos sucios en el browser

No necesitas un servidor — simula el dataset completo en JavaScript.

  • Usa un RNG determinista: s = (s * 9301 + 49297) % 233280
  • Combina tendencia lineal + estacionalidad sinusoidal + ruido
  • Inserta NaN: if (r() < 0.12) val = null
  • Agrega un bloque consecutivo faltante en posición aleatoria (3–7 semanas)
2
📊Detección de outliers con Z-score

Identifica valores que se alejan demasiado de la media.

  • Fórmula: z = (valor - media) / std
  • Si |z| > umbral (ej. 2.5σ) → outlier → tratar como NaN
  • Los negativos son siempre inválidos, sin importar el σ
  • Permite al usuario ajustar el umbral en tiempo real con un slider
3
🌊Holt-Winters — Triple suavizado exponencial

El modelo más completo para series con tendencia y estacionalidad.

  • Parámetros: α nivel, β tendencia, γ estacionalidad
  • Periodo estacional configurable: 4 sem (trimestral), 12 (mensual), 52 (anual)
  • Predicción h pasos: (S + B·h) · I[h % periodo]
  • Valores de arranque típicos: α=0.3, β=0.1, γ=0.2
α nivelβ tendenciaγ estacional
4
📐Métricas de evaluación del modelo

Mide qué tan bien ajusta el modelo antes de predecir.

  • MAE = media(|real - fitted|) — error absoluto medio
  • MAPE = media(|real - fitted| / real) · 100 — error porcentual interpretable
  • RMSE = √media((real - fitted)²) — penaliza errores grandes
  • R² = 1 - SS_res / SS_tot — bondad de ajuste (1 = perfecto)
  • Referencia: MAPE < 10% → excelente · 10–20% → aceptable · >20% → revisar
5
📉Intervalo de confianza que crece con el horizonte

El IC debe ampliarse a medida que predices más lejos — es correcto matemáticamente.

  • Fórmula: margen[h] = z · RMSE · √(1 + h / steps)
  • Donde z depende del nivel: 80% → 1.28 · 90% → 1.645 · 95% → 1.96
  • IC inferior: max(0, predicción - margen)
  • IC superior: predicción + margen
  • En Chart.js usa fill: '+1' para rellenar la banda entre curvas
Chart.js fill:+1z-score IC

⚠️ Errores frecuentes a evitar

  • Limpiar antes de explorar — primero diagnostica, luego interviene
  • Usar la media global para imputar bloques consecutivos — usa interpolación o media local
  • No reportar el MAPE — es la métrica que el equipo de compras puede interpretar
  • Olvidar que el IC crece con el horizonte — si no crece, algo está mal en tu fórmula

Estas tres preguntas forman el reporte de una página. No tienen respuesta única — se evalúa el razonamiento, no la cifra exacta.

Pregunta 1 de 3
¿Qué tipo de anomalía fue la más frecuente en tu dataset y qué decisión tomaste para tratarla? ¿Por qué ese método y no otro?
Pista: compara forward fill vs. interpolación lineal para bloques consecutivos. ¿Cuál preserva mejor la tendencia?
Pregunta 2 de 3
¿Qué modelo elegiste y cómo interpretas el valor de MAPE que obtuviste? ¿Es un resultado aceptable para el contexto de inventario?
Pista: en logística, un MAPE < 15% es generalmente aceptable. ¿El tuyo lo supera? ¿A qué se debe la diferencia?
Pregunta 3 de 3
¿Cómo cambia el intervalo de confianza del forecast al aumentar el horizonte de predicción (de 4 a 12 semanas)? ¿Qué implica esto para el equipo de compras?
Pista: observa la banda del IC en la gráfica. ¿Qué pasa con el ancho entre semana 4 y semana 12? ¿Cómo afecta eso a las decisiones de compra?

💡 Criterios de evaluación del reporte

  • Claridad del razonamiento — ¿se entiende por qué tomaste esa decisión?
  • Conexión con el contexto de negocio — ¿hablas de inventario, no solo de números?
  • Reconocimiento de limitaciones — ¿sabes qué mejorarías con más tiempo?
  • Extensión máxima: una página. La concisión es parte del ejercicio.

Explora la solución de referencia (Nivel 3). Cambia los parámetros del sidebar, ejecuta el pipeline completo y observa cómo cambian diagnóstico, limpieza, modelo y forecast.

predictor_demanda.html — Demo de referencia (Nivel 3)
Reto anterior
Reto 1 — Estimador de Proyectos
HTML · CSS · JavaScript vanilla · DOM · Chart.js · Exportación TXT
← Ver Reto 1
dataset: 104 semanas
Serie temporal — unidades/semana (raw)
real outliers NaN/missing
Log de anomalías detectadas
Original vs. limpio
original limpio imputado
Estadísticas de calidad
Real vs. fitted
real fitted
Distribución de residuos
Proyección con intervalos de confianza
histórico forecast IC
Tabla de predicciones
`; let demoLoaded = false; function switchTab(id, btn) { document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.getElementById('tab-' + id).classList.add('active'); btn.classList.add('active'); if (id === 'demo') loadDemo(); } function loadDemo() { if (demoLoaded) return; demoLoaded = true; document.getElementById('demo-frame').srcdoc = DEMO_HTML; } function toggleAcc(card) { card.classList.toggle('open'); } if (window.location.hash === '#demo-section') { const demoBtn = document.querySelector('[onclick*=\'demo\']'); if (demoBtn) demoBtn.click(); }