ML clásicoProyecto final · Difícil · 35 min

Proyecto final: una alerta de ventas que puedas defender

Junta lo del bloque: un pronóstico semana a semana, residuos robustos para marcar lo raro, y precisión, recall y coste para elegir el límite con argumentos.

Combina los 8 ejercicios del bloque ML clásico. Se desbloquea al terminarlos, pero puedes intentarlo ya.

WEEKLY son 104 semanas de ventas de una tienda de alimentación (sintéticas, en EUR). Cinco semanas tuvieron una incidencia conocida (INCIDENTS): esas son las alertas que quieres, y ninguna más. Escribe cinco funciones:

oneStepForecasts(xs, k): para cada semana t, la mediana de las k semanas anteriores (t - k … t - 1); null en las k primeras. La mediana, no la media: una semana rara no debe estropear el pronóstico de la siguiente.

robustZ(residuals): (r - mediana) / (1,4826 · MAD), con MAD la mediana de |r - mediana|. Si el MAD es 0, devuelve ceros. Es el z-score del ejercicio de las facturas, pero sin dejarse arrastrar por los valores extremos.

alerts(xs, k, limit): residuo = real - pronóstico en las semanas con pronóstico; devuelve los índices de semana cuyo |z| supera limit.

evaluate(flagged, incidents): { tp, fp, fn, precision, recall } de las alertas frente a las incidencias conocidas, con 0 cuando un denominador es 0.

pickLimit(xs, incidents, k, limits, costFP, costFN): { limit, cost } del límite de limits con menor fp · costFP + fn · costFN; en empate, el límite más alto (menos alertas).

La vista previa pinta las semanas, las alertas de tu regla y las incidencias reales, y deja mover el límite y el coste relativo.

Retos 0/5

  • oneStepForecasts: la mediana de las k semanas anteriores, null al principio
  • robustZ: mediana y MAD, y el valor extremo no esconde a los demás
  • alerts con k = 4 y límite 3 encuentra cuatro de las cinco incidencias
  • evaluate: aciertos, falsas alarmas, incidencias sin aviso, precisión y recall
  • pickLimit elige según el coste: caro perder una incidencia baja el límite

function oneStepForecasts(xs, k) {
  // para la semana t: la mediana de las semanas t - k … t - 1; null en las k primeras
  return xs.map(() => null);
}

function robustZ(residuals) {
  // (r - mediana) / (1,4826 * MAD), con MAD la mediana de |r - mediana|; todo ceros si el MAD es 0
  return residuals.map(() => 0);
}

function alerts(xs, k, limit) {
  // residuo = real - pronóstico en las semanas con pronóstico; los índices de semana cuyo |z| supera limit
  return [];
}

function evaluate(flagged, incidents) {
  // tp, fp, fn, precision, recall de las semanas marcadas frente a las incidencias conocidas (0 si un denominador es 0)
  return { tp: 0, fp: 0, fn: 0, precision: 0, recall: 0 };
}

function pickLimit(xs, incidents, k, limits, costFP, costFN) {
  // el límite con menor fp * costFP + fn * costFN; en empate, el más alto (menos alertas)
  return { limit: limits[0], cost: 0 };
}

console.log(alerts(WEEKLY, 4, 3), pickLimit(WEEKLY, INCIDENTS, 4, LIMITS, 1, 5));
La consola aparece aquí (console.log).

La idea

Esto es lo que suele pedir una pyme cuando dice «quiero que la IA me avise si pasa algo raro». No hace falta un modelo grande: hace falta un pronóstico sencillo, una medida robusta de cuánto se desvía cada semana y una decisión explícita sobre cuántas falsas alarmas aceptas.

La mediana de las cuatro semanas anteriores como pronóstico tiene una virtud: la semana de la cámara rota no contamina el pronóstico de la siguiente. Con la media sí lo haría, y la semana después de una caída saldría marcada como subida. Lo mismo con el MAD frente a la desviación típica: la semana de 1771 EUR no infla la escala y las otras cuatro incidencias siguen siendo visibles.

El límite no se elige a ojo. Con las cinco incidencias conocidas puedes medir precisión y recall para cada límite, ponerle un coste a cada falsa alarma y a cada incidencia sin aviso, y quedarte con el más barato. Aquí, con el límite 3 no hay falsas alarmas pero el corte de luz pasa desapercibido; con el 2 salen las cinco a cambio de tres avisos de más. Esa tabla es la que enseñas cuando alguien pregunta por qué el sistema avisó, o por qué no avisó.

Cinco incidencias en dos años son pocas para fiarse mucho del número exacto. Es un límite honesto: el procedimiento vale, y la cifra mejora con cada año de datos.

Hazlo en tu máquina

Con Python y pandas, sobre el CSV de este ejercicio (CC BY 4.0). La misma regla en diez líneas:

curl -O https://vorluxai.com/data/labs/ventas-semanales-incidencias.csv
python3 -c "
import pandas as pd
df = pd.read_csv('ventas-semanales-incidencias.csv')
forecast = df.ventas_eur.shift(1).rolling(4).median()
res = df.ventas_eur - forecast  # keep the index aligned; NaN rows never alert
z = (res - res.median()) / (1.4826 * (res - res.median()).abs().median())
print(df[z.abs() > 3][['semana', 'ventas_eur', 'incidencia']])
"

Comprueba lo que has entendido

1. ¿Por qué el pronóstico usa la mediana de las cuatro semanas anteriores y no la media?
2. ¿Qué aporta el MAD frente a la desviación típica?
3. Una incidencia sin aviso cuesta como 20 falsas alarmas. ¿Qué límite eliges?
4. Solo hay cinco incidencias conocidas. ¿Qué te dice eso del límite elegido?

Para profundizar: la referencia de Forecasting: Principles and Practice (en inglés) →

¿Esto en producción, con tus datos? Hablemos 15 minutos →