Cerramos el módulo 7 con el taller montado: un paquete src/novamarket/ con datos.py, entrenar.py y predecir.py, tests que pasan, un entorno reproducible y un modelo guardado con su JSON de métricas. Lo que faltaba, decíamos, era un método: cómo se lleva un caso de uso desde la idea hasta un sistema que funciona en producción y sigue funcionando meses después. Esta lección lo da. Retomamos el diagrama del flujo de trabajo que presentamos en 04-01 como mapa (problema → datos → preparación → entrenamiento → evaluación → despliegue → monitorización) y lo desarrollamos como proyecto, con las preguntas que hay que responder en cada fase, los roles que participan y los errores que hunden proyectos enteros. Y lo hacemos sobre el caso que nos acompaña desde el módulo 4: Marta y Diego llevan el predictor de devoluciones (caso de uso 3) de la idea a producción dentro de novamarket_ia/, con un documento de definición, una decisión de despliegue por lotes con banda de revisión humana, una model card generada desde entrenar.py, un predecir_lote.py que reparte los pedidos de cada noche en tres colas y un chequeo de deriva que avisa cuando toca reentrenar. Es importante porque la mayoría de proyectos de IA no fracasan por el algoritmo, sino por saltarse alguna de estas fases; y porque en 09-04 harás tú el mismo recorrido con tu propio proyecto: aquí tienes el método y el ejemplo resuelto.

Contenido

  1. Del mapa al método: el ciclo de vida y los marcos CRISP-DM y CRISP-ML(Q)
  2. Fase 1: definir el problema de negocio y el éxito
  3. Fase 2: datos (fuentes, disponibilidad, fugas, división temporal, gobernanza)
  4. Fase 3: exploración y análisis
  5. Fase 4: prototipo rápido (línea base → modelo simple → iterar)
  6. Fase 5: evaluación con criterio de negocio y con las partes interesadas
  7. Fase 6: despliegue
  8. Fase 7: monitorización y mantenimiento
  9. Fase 8: documentación y comunicación de resultados
  10. Roles del equipo y errores típicos de proyecto
  11. Ejemplo resuelto: el predictor de devoluciones en novamarket_ia/
  12. Errores Comunes y Consejos
  13. Ejercicios
  14. Conclusión

  1. Del mapa al método: el ciclo de vida y los marcos CRISP-DM y CRISP-ML(Q)

En 04-01 dibujamos el flujo de trabajo del aprendizaje automático como una secuencia con vueltas atrás. Aquí lo redibujamos como ciclo de vida de un proyecto, añadiendo lo que entonces dejamos en el aire: la definición de negocio al principio, el despliegue y la vigilancia al final, y el hecho de que las flechas de vuelta no son excepciones sino la norma.

flowchart LR
    A[1. Problema de negocio<br/>y definición del éxito] --> B[2. Datos]
    B --> C[3. Exploración<br/>y análisis]
    C --> D[4. Prototipo:<br/>línea base → modelo → iterar]
    D --> E[5. Evaluación<br/>con criterio de negocio]
    E -->|no compensa| A
    E -->|faltan datos| B
    E -->|compensa| F[6. Despliegue]
    F --> G[7. Monitorización<br/>y mantenimiento]
    G -->|deriva| B
    G -->|cambia el negocio| A
    H[8. Documentación y comunicación] -.-> A & E & F & G

Este ciclo no es una invención del curso: es la forma que han tomado dos marcos muy usados en la industria.

  • CRISP-DM (Cross-Industry Standard Process for Data Mining, finales de los años noventa) define seis fases: comprensión del negocio, comprensión de los datos, preparación de los datos, modelado, evaluación y despliegue. Su gran acierto fue poner la comprensión del negocio antes que los datos y dibujar las flechas de vuelta.
  • CRISP-ML(Q) es una actualización pensada para el aprendizaje automático moderno: añade la monitorización y el mantenimiento como fase propia (un modelo desplegado se degrada) y una garantía de calidad (Q) transversal: en cada fase se definen requisitos, riesgos y comprobaciones. Nuestro ciclo de ocho pasos es esencialmente CRISP-ML(Q) con la documentación explicitada como fase 8.
Fase de nuestro ciclo CRISP-DM CRISP-ML(Q) Lección del curso donde se vio la técnica
1. Problema y éxito Comprensión del negocio Comprensión de negocio y datos 01-03 (valor/viabilidad/riesgo), 02-01 (medida de rendimiento), 02-04 (checklist)
2. Datos Comprensión de los datos Comprensión de negocio y datos 02-03, 04-03
3. Exploración Comprensión de los datos Ingeniería de datos 07-02
4. Prototipo Preparación + modelado Ingeniería del modelo 04-04, 04-06, 05-x
5. Evaluación Evaluación Evaluación del modelo 04-05
6. Despliegue Despliegue Despliegue 07-03, 07-04
7. Monitorización (no explícita) Monitorización y mantenimiento esta lección
8. Documentación transversal Garantía de calidad (Q) esta lección

Una idea que conviene fijar desde ahora: el tiempo de un proyecto de IA no se reparte a partes iguales. En proyectos reales es habitual que la definición y los datos consuman más de la mitad del esfuerzo, el modelado una fracción pequeña, y el despliegue y la monitorización el resto. Quien planifica "dos semanas de datos y dos meses de modelo" está planificando el proyecto al revés.

  1. Fase 1: definir el problema de negocio y el éxito

Es la fase que más proyectos salva y más se salta. Se trata de responder por escrito, en una página, a estas preguntas antes de tocar un dato:

  1. ¿Qué pregunta responde el sistema? No "predecir devoluciones", sino "¿qué probabilidad tiene este pedido de ser devuelto, conocida en el momento de prepararlo?".
  2. ¿Qué decisión cambia? Si la predicción no cambia ninguna acción, el proyecto no vale nada por buena que sea la métrica. En NovaMarket la decisión es: aprobar el pedido sin más, pasarlo a una persona o preparar de antemano la logística inversa (embalaje reutilizable, etiqueta de devolución, no cerrar la reposición de stock).
  3. ¿Cuál es la métrica de negocio y cuál la técnica? La de negocio se expresa en la unidad de quien decide: euros (coste de falsos positivos y falsos negativos, 04-05), horas de trabajo, clientes retenidos. La técnica (AUC, F1, MAE) es un medio. Hay que escribir las dos y la relación entre ellas.
  4. ¿Cuál es la línea base? Lo que se hace hoy: nada, una regla de Diego (">300 €"), un proceso manual. Sin línea base no se puede decir si el modelo aporta.
  5. ¿Qué restricciones hay? Legales (RGPD: base jurídica, decisiones automatizadas; AI Act: nivel de riesgo, 02-04), éticas (grupos afectados, proxies), operativas (¿cuántos pedidos puede revisar una persona al día?), técnicas (¿en cuánto tiempo hace falta la respuesta?).
  6. ¿Cuándo consideraremos que ha funcionado? Un criterio de éxito verificable: "reducir el coste de devoluciones no anticipadas al menos un 25 % respecto a la línea base en tres meses, sin superar 300 revisiones manuales al día".

Aquí es donde se aplica la checklist ética de nueve puntos de 02-04 y, si el sistema decide sobre personas, la evaluación de impacto. No como trámite posterior: sus respuestas condicionan qué datos se pueden usar (punto 2), qué modelo (explicabilidad, punto 4) y qué revisión humana hace falta (punto 5).

El resultado de la fase es un documento de definición: una tabla de una página firmada por negocio y datos. Lo veremos hecho para el predictor de devoluciones en la sección 11.

  1. Fase 2: datos (fuentes, disponibilidad, fugas, división temporal, gobernanza)

Con el problema definido, toca inventariar los datos. Las preguntas:

  • Fuentes: ¿en qué sistemas están? (pedidos.csv y clientes.csv del ERP, incidencias.csv del CRM, logs de la web). ¿Con qué frecuencia se actualizan? ¿Quién es el propietario? ¿Hay que unirlos (SQL, 07-01) y con qué clave?
  • Disponibilidad en el momento de predecir: la pregunta más importante y más olvidada. Cada característica debe existir cuando se va a usar el modelo, no cuando se entrena. motivo_devolucion existe en el histórico, pero solo después de la devolución: es una fuga (04-03). dias_entrega real tampoco se conoce al preparar el pedido; se conoce el prometido. Y pedidos_previos de un cliente debe calcularse contra su historial hasta ese día, no contra todo el fichero.
  • Etiquetas: ¿cómo se obtiene la verdad? Una devolución se registra semanas después de la compra: el modelo se entrena con pedidos "maduros" y en producción la etiqueta llega con retraso, lo que afecta a cómo se mide (fase 7).
  • División temporal: en un problema con fechas, el test debe ser posterior al entrenamiento (TimeSeriesSplit, 04-05); una división aleatoria deja que el modelo "vea el futuro" y da métricas optimistas.
  • Volumen y calidad: cuántos casos positivos hay (16,4 % de 3.000 pedidos son 492 devoluciones: suficiente para una logística, poco para una red profunda), NaN, duplicados, atípicos (04-03).
  • Gobernanza: base jurídica y minimización (RGPD), quién accede a qué, dónde se guardan los datos crudos (nunca en Git, 07-04), qué se anonimiza, cuánto tiempo se conservan. Y una ficha de datos (datasheet): origen, periodo, columnas, sesgos conocidos, restricciones de uso.

Una regla que ahorra disgustos: construye el conjunto de datos de entrenamiento con la misma consulta que usarás en producción. Si el entrenamiento sale de un Excel manual y la producción de una consulta SQL, las columnas terminan significando cosas distintas (el llamado training-serving skew). Lo veremos con dias_desde_inicio en la sección 11.

  1. Fase 3: exploración y análisis

Antes de modelar, mirar. La exploración (07-02) tiene tres objetivos en un proyecto:

  1. Confirmar que el problema existe en los datos: ¿la tasa de devolución varía de verdad con el importe, la categoría, el tipo de cliente? Si nada varía, no hay señal que aprender.
  2. Detectar problemas de calidad y de fuga: columnas con demasiados NaN, valores imposibles (el importe de 99.999 €), columnas que predicen "demasiado bien" (sospecha de fuga).
  3. Alimentar la definición: la exploración a menudo cambia la pregunta. Si el 60 % de las devoluciones son de electrónica, quizá el primer despliegue debería limitarse a esa categoría.

El entregable es un cuaderno de exploración (Jupyter, 07-04) con gráficos y una lista de hallazgos y de decisiones que provocan. Es la fase adecuada para el notebook; lo que madure pasa a src/.

  1. Fase 4: prototipo rápido (línea base → modelo simple → iterar)

El error clásico es empezar por el modelo más potente. El orden correcto:

  1. Línea base sin modelo: "nunca se devuelve" (3.690 € en el test de 750 pedidos), la regla de Diego ">300 €" (3.195 €). Cualquier modelo tiene que batir eso en euros.
  2. Modelo simple, interpretable, con el pipeline completo: la regresión logística dentro de un Pipeline que imputa, escala y codifica (04-03, 04-04). Que funcione de extremo a extremo antes de mejorar nada.
  3. Iterar con un presupuesto: cada iteración cambia una cosa (una característica nueva, un algoritmo, un hiperparámetro, 04-06) y se anota con su métrica. Marta se dio un presupuesto de dos semanas para el prototipo: la logística dio AUC 0,844; el bosque ajustado 0,838; el MLP 21→16→8→1, 0,834. Ninguno mejoró de forma significativa a la logística, que además explica sus decisiones. Se para: el presupuesto de tiempo protege del "una prueba más".
Iteración Cambio AUC test Coste con umbral 0,2 Decisión
0 Línea base "nunca" — 3.690 € (sin umbral) referencia
0b Regla ">300 €" — 3.195 € referencia operativa
1 Logística + pipeline (21 columnas) 0,844 1.610 € candidata
2 Bosque aleatorio ajustado 0,838 similar no mejora
3 MLP 21→16→8→1 0,834 similar no mejora, opaco

Regla práctica: el prototipo termina cuando dos iteraciones seguidas no mueven la métrica de negocio, no cuando se acaban las ideas.

  1. Fase 5: evaluación con criterio de negocio y con las partes interesadas

La evaluación técnica (04-05) es necesaria pero no suficiente. En un proyecto se evalúa con las personas que van a vivir con el sistema:

  • Métrica de negocio sobre el test reservado: coste 2.485 € con umbral 0,5 → 1.610 € con umbral 0,2 (un 35 % menos), frente a 3.690 € sin hacer nada.
  • Capacidad operativa: con umbral 0,2 se marcan 200 pedidos de 750 (el 27 %); Diego dice que su equipo no puede revisar tantos. De ahí la banda de revisión humana 0,2-0,5: 60 pedidos automáticos, 140 a revisar, 550 aprobados.
  • Equidad: paridad de tasas de marcado entre zonas de código postal (02-04) sobre el test.
  • Casos concretos: enseñar a Diego diez pedidos de cada cola con la explicación (los coeficientes de la logística) para que juzgue si el modelo "tiene sentido". Este paso descubre fugas y errores que ninguna métrica enseña.
  • Riesgos y plan B: qué pasa si el modelo falla (se apaga y se vuelve al proceso manual, ver reversión en la fase 6).

La salida es una decisión explícita: seguir al despliegue, volver a datos, o parar. Parar es un resultado válido de un proyecto bien hecho; lo que no es válido es desplegar porque ya se ha invertido mucho.

  1. Fase 6: despliegue

Desplegar es integrar el modelo en un flujo real. Las decisiones:

Decisión Opciones Criterio NovaMarket (devoluciones)
Cuándo se predice Por lotes (batch: cada noche, cada hora) vs tiempo real (por petición) ¿Cuánto puede esperar la decisión? El pedido se prepara al día siguiente: lote nocturno basta
Cómo se integra API (FastAPI, servir.py de 07-03) vs fichero/tabla que consume el proceso existente ¿Quién consume la predicción y cómo trabaja hoy? Un CSV/tabla de colas que carga la herramienta de almacén; la API queda para el futuro asistente
Grado de automatización Totalmente automático vs human-in-the-loop por banda Impacto en personas, coste de errores (02-04) Tres colas: aprobar / revisar / marcar
Cómo se prueba en real Modo sombra (predice pero no actúa; se compara con lo que pasa) → A/B o despliegue gradual (un almacén primero) → total Riesgo del cambio Dos semanas en sombra en Getafe, luego banda de revisión en Getafe, luego Zaragoza
Plan de reversión Interruptor para volver al proceso anterior; versión previa del modelo guardada Siempre Variable de configuración MODELO_ACTIVO=off devuelve todo a "aprobar" y la regla de Diego
Empaquetado Script en cron, contenedor Docker (07-04), servicio Infraestructura disponible Script predecir_lote.py lanzado por el planificador nocturno

Dos apuntes. Modo sombra significa ejecutar el modelo en producción sin que sus salidas afecten a nada, solo para registrarlas y comparar después con la realidad: es la forma más barata de descubrir que los datos de producción no son como los del entrenamiento. Y la prueba A/B (una parte de los pedidos con el modelo, otra sin él, comparando el coste) es la única manera de medir el impacto causal; a nivel conceptual basta con saber que existe y que exige asignar los grupos al azar y esperar a tener suficientes casos.

  1. Fase 7: monitorización y mantenimiento

Un modelo desplegado empieza a caducar el primer día. Hay que vigilar tres cosas:

  1. Salud técnica: ¿se ejecutó el lote? ¿cuántos pedidos? ¿cuánto tardó? ¿hubo errores o columnas ausentes?
  2. Deriva de datos (data drift): la distribución de las entradas cambia (importes más altos en Navidad, más clientes nuevos tras una campaña, una nueva categoría). Se detecta comparando la distribución reciente con la del entrenamiento: diferencia de medias, un índice como el PSI (Population Stability Index) por tramos, o simplemente la tasa de marcado del modelo (si de repente marca al 50 % de los pedidos, algo ha cambiado).
  3. Deriva de concepto (concept drift): cambia la relación entre entradas y salida (una nueva política de devoluciones gratuitas hace que los mismos pedidos se devuelvan más). Solo se detecta cuando llegan las etiquetas reales, con retraso: hay que recalcular AUC y coste sobre los pedidos de hace unas semanas cuyas devoluciones ya se conocen.

Y actuar: alertas con umbrales (aviso / alerta), un calendario de reentrenamiento (periódico o disparado por deriva), versionado de modelos (v1.0.0, v1.1.0, con su JSON de métricas) y un registro de decisiones (qué modelo puntuó qué lote, cuántos pedidos fueron a cada cola, qué decidió la revisora): sirve para auditar, para explicar a un cliente y para reentrenar con etiquetas humanas. Las herramientas específicas de este terreno (MLflow, paneles de deriva) se mencionaron en 07-03; aquí nos basta con entender qué se mide y con programarlo a mano.

  1. Fase 8: documentación y comunicación de resultados

Dos documentos cortos y un hábito:

  • Model card (ficha del modelo): qué es, para qué se puede usar y para qué no, con qué datos se entrenó, cómo rinde (métricas técnicas y de negocio, por grupos si procede), limitaciones conocidas, responsable, versión y fecha. Nació como propuesta académica y hoy es práctica habitual y, para sistemas de riesgo, parte de lo que pide la regulación (02-04). La generaremos automáticamente desde entrenar.py.
  • Ficha de datos (datasheet): origen, periodo, columnas, cómo se etiquetó, sesgos conocidos, base jurídica.
  • Comunicación: a Diego no se le lleva un AUC; se le lleva "1.610 € frente a 3.690 € por cada 750 pedidos, 140 revisiones por cada 750, y una lista de casos que puedes mirar". A dirección, tres cifras y un riesgo. Al equipo técnico, el repositorio y la model card.

  1. Roles del equipo y errores típicos de proyecto

Incluso en una empresa de 180 personas, un proyecto de IA necesita cuatro sombreros (a veces sobre pocas cabezas):

Rol Quién en NovaMarket Responsabilidad
Negocio / propietario Diego Define la decisión, los costes, la capacidad operativa; acepta o rechaza
Datos / modelado Marta Datos, exploración, prototipo, evaluación, model card
Ingeniería equipo de sistemas Integración, planificador nocturno, entorno, reversión, alertas técnicas
Legal / cumplimiento asesoría externa RGPD, AI Act, evaluación de impacto, conservación de datos

Y los errores que más proyectos hunden:

  1. Empezar por el modelo ("vamos a hacer deep learning") sin decisión ni métrica de negocio.
  2. Sin línea base: un AUC de 0,84 no significa nada si la regla de Diego ya conseguía casi lo mismo.
  3. Métrica equivocada: optimizar exactitud con clases desequilibradas (04-05), o una métrica técnica que no se mueve en euros.
  4. Datos que no existirán en producción: fugas y columnas calculadas de forma distinta al servir.
  5. No cerrar el ciclo: desplegar sin monitorizar, sin reentrenamiento, sin registro; o quedarse en un notebook que nunca llega a producción.
  6. No implicar a quien decide: Diego se entera del sistema el día que le llegan 641 pedidos a revisar.

  1. Ejemplo resuelto: el predictor de devoluciones en novamarket_ia/

Recorremos las ocho fases sobre el proyecto que ya tenemos en novamarket_ia/. Todo el código de esta sección se ha ejecutado en el entorno del curso; los ficheros nuevos son predecir_lote.py, deriva.py, las funciones de model card en entrenar.py y un test.

11.1 Documento de definición (fase 1)

Campo Contenido
Pregunta ¿Qué probabilidad tiene un pedido de ser devuelto, conocida al prepararlo?
Decisión que cambia Cada noche, cada pedido del día va a una de tres colas: aprobar (flujo normal), revisar (una persona decide si prepara logística inversa o contacta al cliente), marcar (se prepara logística inversa y no se cierra la reposición)
Métrica de negocio Coste = 5 € por falsa alarma + 30 € por devolución no anticipada (+ 3 € por revisión manual) sobre pedidos maduros
Métrica técnica AUC (comparación de modelos); coste por umbral (elección del umbral)
Línea base "Nunca": 3.690 € / 750 pedidos. Regla de Diego ">300 €": 3.195 €
Criterio de éxito ≥ 25 % menos de coste que la línea base en 3 meses; ≤ 300 revisiones/día; ratio de impacto entre zonas ≥ 0,8
Restricciones RGPD: no es decisión automatizada con efectos jurídicos (banda de revisión, sin denegar compras); AI Act: riesgo limitado/mínimo; no usar codigo_postal_zona para decidir sin revisión; datos crudos fuera de Git
Datos pedidos.csv (3 meses), clientes.csv (historial); etiqueta = devolución en 30 días
Despliegue Lote nocturno; salida = tabla de colas; sombra 2 semanas en Getafe; reversión por configuración
Propietario / responsable Diego (negocio), Marta (modelo), sistemas (integración), asesoría (legal)
Presupuesto Prototipo: 2 semanas; piloto: 6 semanas

11.2 Datos, exploración, prototipo y evaluación (fases 2-5)

Estas fases ya están hechas en los módulos 4 y 7 y no las repetimos: datos.py genera y prepara los pedidos (fugas fuera, Pipeline de 21 columnas), y la logística ganó el prototipo con AUC 0,844 y coste 1.610 € con umbral 0,2. Solo una corrección que aparece al preparar el despliegue, típica de la fase 2: preparar_pedidos calculaba dias_desde_inicio respecto al primer pedido del fichero. En entrenamiento eso es el 1 de septiembre de 2025; en un lote nocturno del 1 de diciembre sería... el 1 de diciembre, y la columna valdría 0 en lugar de 91. Es un caso de libro de training-serving skew. La corrección: un parámetro fecha_inicio que en producción se pasa explícitamente.

# src/novamarket/datos.py (fragmento modificado)
def preparar_pedidos(sucio, fecha_inicio=None):
    """... fecha_inicio: fecha de referencia para 'dias_desde_inicio'. En entrenamiento se toma el
    primer pedido del histórico; en producción DEBE pasarse la misma fecha del entrenamiento."""
    limpio = sucio.drop_duplicates()
    ...
    if fecha_inicio is None:
        fecha_inicio = limpio["fecha_pedido"].min()
    limpio["dias_desde_inicio"] = (limpio["fecha_pedido"] - pd.Timestamp(fecha_inicio)).dt.days
    ...

También movemos a datos.py la función coste_negocio(matriz, coste_fp=5, coste_fn=30) de 04-05, porque la model card la necesita.

11.3 Código (a): la model card generada desde entrenar.py (fase 8)

Ampliamos entrenar.py para que, además del modelo y su JSON de versiones, escriba la ficha del modelo en dos formatos (JSON para máquinas, Markdown para personas) y una referencia de deriva que usará la monitorización. entrenar() devuelve ahora un tercer valor con el contexto (particiones y probabilidades de test).

# src/novamarket/entrenar.py (partes nuevas; el resto es el de 07-04)
from datetime import date
import numpy as np
from sklearn.metrics import confusion_matrix, roc_auc_score
from novamarket.datos import coste_negocio, ...

UMBRAL_BAJO, UMBRAL_ALTO = 0.2, 0.5          # banda de revisión humana decidida en 04-05
VERSION_MODELO = "1.0.0"

def entrenar(n=3000, semilla=42):
    """Genera los datos, entrena y devuelve (pipeline, AUC de test, contexto para la ficha)."""
    X, y = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(n, semilla), semilla))
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=semilla, stratify=y)
    pipe = Pipeline([("prep", crear_preparacion()),
                     ("modelo", LogisticRegression(max_iter=1000))]).fit(Xtr, ytr)
    prob = pipe.predict_proba(Xte)[:, 1]
    auc = roc_auc_score(yte, prob)
    return pipe, auc, {"Xtr": Xtr, "Xte": Xte, "yte": yte, "prob_test": prob}

def escribir_model_card(auc, contexto, ruta_dir, n, semilla):
    """Model card mínima: qué es, con qué datos, cómo rinde, límites. JSON + Markdown."""
    yte, prob = contexto["yte"], contexto["prob_test"]
    costes = {}
    for u in (0.5, UMBRAL_BAJO):                                  # coste de negocio con los dos umbrales
        m = confusion_matrix(yte, (prob >= u).astype(int))
        costes[f"umbral_{u}"] = {"coste_eur": coste_negocio(m), "marcados": int(m[:, 1].sum())}
    ficha = {
        "nombre": "Predictor de devoluciones NovaMarket (caso de uso 3)",
        "version": VERSION_MODELO,
        "fecha_entrenamiento": date.today().isoformat(),
        "responsable": "Marta (datos); propietario de negocio: Diego (operaciones)",
        "uso_previsto": "Puntuar cada noche los pedidos del día y repartirlos en tres colas: "
                        "aprobar (<0,2), revisar por una persona (0,2-0,5) y marcar (>=0,5).",
        "uso_no_previsto": "Denegar compras, penalizar clientes o decidir sin revisión humana "
                           "por encima de la banda.",
        "algoritmo": "Pipeline scikit-learn: imputación + escalado + one-hot (21 columnas) "
                     "+ regresión logística",
        "datos": {"origen": "pedidos.csv sintético (generar_pedidos_ml)", "n_total": n,
                  "n_entrenamiento": int(len(contexto["Xtr"])), "n_test": int(len(yte)),
                  "tasa_devolucion_test": round(float(yte.mean()), 4), "semilla": semilla,
                  "caracteristicas": list(contexto["Xtr"].columns)},
        "metricas": {"auc_test": round(float(auc), 4), "coste_negocio_test": costes,
                     "coste_fp_eur": 5, "coste_fn_eur": 30},
        "limitaciones": [
            "Entrenado con datos sintéticos de tres meses: no ha visto Black Friday ni rebajas.",
            "'dias_desde_inicio' extrapola en producción; se vigila y se reentrena periódicamente.",
            "'pedidos_previos' y 'tasa_devolucion_previa' deben calcularse contra el histórico "
            "completo del cliente, no solo dentro del lote.",
            "codigo_postal_zona no se usa para decidir sin revisión: se mide la paridad de tasas "
            "entre zonas cada mes (02-04).",
        ],
        "entorno": {"python": platform.python_version(), "scikit_learn": sklearn.__version__},
    }
    ruta_dir = Path(ruta_dir)
    (ruta_dir / "model_card.json").write_text(json.dumps(ficha, indent=2, ensure_ascii=False))
    md = [f"# Model card: {ficha['nombre']} v{ficha['version']}", "",
          f"- **Fecha**: {ficha['fecha_entrenamiento']}  |  **Responsable**: {ficha['responsable']}",
          f"- **Uso previsto**: {ficha['uso_previsto']}", f"- **Uso NO previsto**: {ficha['uso_no_previsto']}",
          f"- **Algoritmo**: {ficha['algoritmo']}",
          f"- **Datos**: {ficha['datos']['n_entrenamiento']} pedidos de entrenamiento, "
          f"{ficha['datos']['n_test']} de test (tasa de devolución {ficha['datos']['tasa_devolucion_test']:.1%})",
          "", "## Métricas", "", "| Métrica | Valor |", "|---|---|",
          f"| AUC (test) | {ficha['metricas']['auc_test']} |"]
    for k, v in costes.items():
        md.append(f"| Coste de negocio {k.replace('_', ' ')} | {v['coste_eur']} € ({v['marcados']} marcados) |")
    md += ["", "## Limitaciones", ""] + [f"- {l}" for l in ficha["limitaciones"]]
    (ruta_dir / "model_card.md").write_text("\n".join(md) + "\n")
    return ficha

def escribir_referencia_deriva(contexto, ruta_dir):
    """Guarda cómo eran los datos y las salidas al entrenar, para compararlos cada semana (deriva.py)."""
    importe = contexto["Xtr"]["importe"].dropna()
    bordes = np.quantile(importe, np.linspace(0, 1, 11))          # 10 tramos con el 10 % de los pedidos cada uno
    ref = {"importe_media": round(float(importe.mean()), 2),
           "importe_bordes": [None] + [round(float(b), 2) for b in bordes[1:-1]] + [None],  # None = infinito
           "importe_frecuencias": [0.1] * 10,
           "cliente_nuevo_tasa": round(float(contexto["Xtr"]["cliente_nuevo"].mean()), 4),
           "tasa_marcado": round(float((contexto["prob_test"] >= UMBRAL_BAJO).mean()), 4),
           "tasa_marcado_alto": round(float((contexto["prob_test"] >= UMBRAL_ALTO).mean()), 4)}
    (Path(ruta_dir) / "referencia_deriva.json").write_text(json.dumps(ref, indent=2))
    return ref

if __name__ == "__main__":
    ...                                                             # argparse igual que en 07-04
    pipe, auc, ctx = entrenar(args.n, args.semilla)
    meta = guardar(pipe, auc, args.salida, args.n, args.semilla)
    ficha = escribir_model_card(auc, ctx, Path(args.salida).parent, args.n, args.semilla)
    ref = escribir_referencia_deriva(ctx, Path(args.salida).parent)
    print(f"AUC test: {auc:.3f}  ->  {args.salida}")
    print("Coste de negocio en test:", ficha["metricas"]["coste_negocio_test"])

Explicación de las piezas nuevas:

  • La ficha reúne en un solo sitio lo que un auditor, un compañero nuevo o Diego necesitan: uso previsto y no previsto (la frase "no denegar compras" es una decisión ética convertida en documento), datos, métricas técnicas y de negocio, limitaciones honestas y versión.
  • La referencia de deriva guarda cómo era el importe en el entrenamiento (media y los diez tramos que dejan el 10 % de pedidos en cada uno), la proporción de clientes nuevos y qué fracción de pedidos marcaba el modelo en el test. Es lo que la monitorización comparará cada semana.
  • Los bordes extremos se guardan como None (infinito): así un pedido más caro que cualquiera del entrenamiento cae en el último tramo en lugar de perderse.

Salida real de python -m novamarket.entrenar en el entorno del curso:

AUC test: 0.844  ->  models/modelo_devoluciones.joblib
Coste de negocio en test: {'umbral_0.5': {'coste_eur': 2485, 'marcados': 60}, 'umbral_0.2': {'coste_eur': 1610, 'marcados': 200}}

Y models/model_card.md queda así (extracto):

# Model card: Predictor de devoluciones NovaMarket (caso de uso 3) v1.0.0

- **Fecha**: 2026-08-18  |  **Responsable**: Marta (datos); propietario de negocio: Diego (operaciones)
- **Uso previsto**: Puntuar cada noche los pedidos del día y repartirlos en tres colas: aprobar (<0,2), revisar por una persona (0,2-0,5) y marcar (>=0,5).
- **Uso NO previsto**: Denegar compras, penalizar clientes o decidir sin revisión humana por encima de la banda.
- **Datos**: 2249 pedidos de entrenamiento, 750 de test (tasa de devolución 16.4%)

| Métrica | Valor |
|---|---|
| AUC (test) | 0.8444 |
| Coste de negocio umbral 0.5 | 2485 € (60 marcados) |
| Coste de negocio umbral 0.2 | 1610 € (200 marcados) |

Las cifras son exactamente las de 04-05 (2.485 € → 1.610 €, 60 y 200 marcados): la model card no inventa nada, documenta lo que el código calcula. models/ contiene ahora modelo_devoluciones.joblib, modelo_devoluciones.json, model_card.json, model_card.md y referencia_deriva.json; los tres JSON y el Markdown sí van a Git (07-04).

11.4 Código (b): predecir_lote.py, las tres colas de cada noche (fase 6)

El lote nocturno recibe un CSV con los pedidos del día ya preparados por la misma consulta que alimenta el entrenamiento (id_pedido más las 15 columnas de preparar_pedidos), los puntúa con el modelo guardado y produce las colas.

# src/novamarket/predecir_lote.py
"""Puntúa por lotes (cada noche) un CSV de pedidos nuevos y los reparte en tres colas.
Uso:  python -m novamarket.predecir_lote --entrada data/raw/pedidos_2025-12-01.csv \
          --salida data/processed/colas_2025-12-01.csv"""
import argparse, json
from datetime import datetime
from pathlib import Path
import pandas as pd
from novamarket.predecir import cargar

UMBRAL_BAJO, UMBRAL_ALTO = 0.2, 0.5          # banda de revisión humana (04-05); misma que en entrenar.py

def asignar_cola(prob, bajo=UMBRAL_BAJO, alto=UMBRAL_ALTO):
    """Traduce una probabilidad en una de las tres colas operativas de Diego."""
    if prob >= alto:
        return "marcar"                    # se prepara la logística inversa; nadie lo revisa a mano
    if prob >= bajo:
        return "revisar"                   # una persona decide (human-in-the-loop, 02-04)
    return "aprobar"                       # sigue el flujo normal

def puntuar_lote(modelo, pedidos):
    """pedidos: DataFrame con id_pedido + las 15 columnas de preparar_pedidos. Devuelve id, prob y cola."""
    columnas = [c for c in pedidos.columns if c != "id_pedido"]
    prob = modelo.predict_proba(pedidos[columnas])[:, 1]
    salida = pd.DataFrame({"id_pedido": pedidos["id_pedido"].values,
                           "prob_devolucion": prob.round(4)})
    salida["cola"] = salida["prob_devolucion"].map(asignar_cola)
    # De mayor a menor riesgo: si el equipo de Diego no llega a toda la cola "revisar", empieza por lo peor
    return salida.sort_values("prob_devolucion", ascending=False).reset_index(drop=True)

def registrar_decision(resumen, ruta_log="logs/decisiones.jsonl"):
    """Registro de decisiones: una línea JSON por lote (qué modelo, cuándo, cuántos a cada cola)."""
    Path(ruta_log).parent.mkdir(parents=True, exist_ok=True)
    with open(ruta_log, "a") as f:
        f.write(json.dumps(resumen) + "\n")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Puntúa un lote de pedidos y genera las colas")
    parser.add_argument("--entrada", required=True)
    parser.add_argument("--salida", required=True)
    parser.add_argument("--modelo", default="models/modelo_devoluciones.joblib")
    args = parser.parse_args()

    modelo = cargar(args.modelo)
    version = json.loads(Path(args.modelo).with_suffix(".json").read_text()).get("version", "?")
    pedidos = pd.read_csv(args.entrada)
    colas = puntuar_lote(modelo, pedidos)
    Path(args.salida).parent.mkdir(parents=True, exist_ok=True)
    colas.to_csv(args.salida, index=False)

    conteo = colas["cola"].value_counts().to_dict()
    resumen = {"fecha_ejecucion": datetime.now().isoformat(timespec="seconds"), "lote": args.entrada,
               "modelo_version": version, "n": int(len(colas)),
               "aprobar": conteo.get("aprobar", 0), "revisar": conteo.get("revisar", 0),
               "marcar": conteo.get("marcar", 0), "umbrales": [UMBRAL_BAJO, UMBRAL_ALTO]}
    registrar_decision(resumen)
    print(f"{len(colas)} pedidos puntuados con el modelo v{version} -> {args.salida}")
    for cola in ("aprobar", "revisar", "marcar"):
        print(f"  {cola:8s}: {conteo.get(cola, 0):4d}  ({conteo.get(cola, 0) / len(colas):.1%})")

Explicación:

  • asignar_cola es la banda de revisión humana de 04-05 y 06-04 hecha código: tres zonas y dos umbrales, definidos como constantes con nombre para que un cambio se haga en un solo sitio (y quede en Git).
  • puntuar_lote no toca el modelo: el Pipeline guardado ya imputa, escala y codifica (por eso insistimos en 04-03 en meter la preparación dentro). Ordena por riesgo descendente: si el equipo solo llega a 300 revisiones, revisa las 300 más arriesgadas.
  • registrar_decision es el registro de decisiones de la fase 7: una línea por lote con la versión del modelo (leída del JSON que dejó guardar), la fecha y los recuentos. Es lo mínimo para auditar y para dibujar la evolución semana a semana.
  • La salida es un CSV que la herramienta de almacén ya sabe cargar: integrar en el flujo existente en lugar de obligar a Diego a llamar a una API.

Para probarlo sin datos reales, simulamos el fichero que produciría la consulta SQL para el 1 de diciembre de 2025 (3.000 pedidos, un día de NovaMarket), sin la etiqueta devuelto, pasando la misma fecha_inicio que el entrenamiento:

# simular_dia.py (solo para el curso; en producción este CSV lo produce SQL sobre el almacén de datos)
import numpy as np, pandas as pd
from novamarket.datos import generar_pedidos_ml, ensuciar_pedidos, preparar_pedidos

FECHA_INICIO_ENTRENAMIENTO = "2025-09-01"      # la misma referencia que usó entrenar.py

def simular_dia(fecha, n=3000, semilla=1, factor_importe=1.0, tasa_nuevos=None, salida=None):
    pedidos = generar_pedidos_ml(n, semilla)
    if factor_importe != 1.0:                                   # escenario de deriva: pedidos más caros
        pedidos["importe"] = (pedidos["importe"] * factor_importe).round(2)
    if tasa_nuevos is not None:                                 # escenario de deriva: campaña de captación
        rng = np.random.default_rng(semilla)
        pedidos["cliente_nuevo"] = (rng.random(n) < tasa_nuevos).astype(int)
    sucio = ensuciar_pedidos(pedidos, semilla)
    sucio["fecha_pedido"] = pd.Timestamp(fecha)                  # todos los pedidos son del mismo día
    X, _ = preparar_pedidos(sucio, fecha_inicio=FECHA_INICIO_ENTRENAMIENTO)   # ¡misma referencia!
    X.insert(0, "id_pedido", sucio.loc[X.index, "id_pedido"].values)
    if salida:
        X.to_csv(salida, index=False)
    return X

simular_dia("2025-12-01", semilla=2025, salida="data/raw/pedidos_2025-12-01.csv")   # día normal
simular_dia("2025-12-08", semilla=2026, factor_importe=1.4, tasa_nuevos=0.45,        # día con deriva
            salida="data/raw/pedidos_2025-12-08.csv")

Salida real del lote del día normal:

$ python -m novamarket.predecir_lote --entrada data/raw/pedidos_2025-12-01.csv --salida data/processed/colas_2025-12-01.csv
2999 pedidos puntuados con el modelo v1.0.0 -> data/processed/colas_2025-12-01.csv
  aprobar : 2050  (68.4%)
  revisar :  641  (21.4%)
  marcar  :  308  (10.3%)

Y las primeras filas de colas_2025-12-01.csv (ordenadas por riesgo) y la línea del registro:

id_pedido,prob_devolucion,cola
P101388,0.9978,marcar
P102496,0.9818,marcar
P102743,0.9817,marcar
...
{"fecha_ejecucion": "2026-08-18T01:00:31", "lote": "data/raw/pedidos_2025-12-01.csv", "modelo_version": "1.0.0", "n": 2999, "aprobar": 2050, "revisar": 641, "marcar": 308, "umbrales": [0.2, 0.5]}

Fíjate en el número que Diego ve primero: 641 pedidos a revisar en un día. En el test de 750 pedidos, la banda daba 140 revisiones (el 19 %); a escala de 3.000 pedidos diarios son más de 600, el doble de lo que su equipo puede absorber (300, según el documento de definición). Esto no invalida el modelo: es exactamente el tipo de hallazgo para el que existe el modo sombra, y se resuelve en la evaluación con las partes interesadas: subir el umbral inferior (0,25 o 0,3) a cambio de dejar escapar más devoluciones, o mantener 0,2 y revisar en orden de riesgo hasta agotar la capacidad. En el ejercicio 1 lo cuantificarás.

11.5 Código (c): deriva.py, el chequeo semanal (fase 7)

Cada lunes se compara el último lote (o la unión de los siete lotes de la semana) con la referencia guardada al entrenar. Medimos cuatro cosas: el PSI del importe, la media del importe, la tasa de clientes nuevos y la tasa de marcado del modelo (proporción de pedidos que no van a "aprobar").

# src/novamarket/deriva.py
"""Chequeo semanal de deriva: compara el lote reciente con la referencia guardada al entrenar.
Uso:  python -m novamarket.deriva --entrada data/raw/pedidos_2025-12-01.csv --colas data/processed/colas_2025-12-01.csv"""
import argparse, json
from pathlib import Path
import numpy as np
import pandas as pd

PSI_AVISO, PSI_ALERTA = 0.10, 0.25            # convención habitual: <0,1 estable, 0,1-0,25 vigilar, >0,25 actuar
DIF_TASA_MARCADO = 0.05                        # 5 puntos porcentuales de diferencia en la tasa de marcado

def psi(frec_ref, frec_nuevo, eps=1e-4):
    """Population Stability Index entre dos distribuciones por tramos (listas de frecuencias que suman 1)."""
    ref = np.clip(np.asarray(frec_ref, float), eps, None)
    nuevo = np.clip(np.asarray(frec_nuevo, float), eps, None)
    return float(np.sum((nuevo - ref) * np.log(nuevo / ref)))

def frecuencias_por_tramo(valores, bordes):
    """Reparte 'valores' en los tramos definidos por 'bordes' (None = infinito) y devuelve frecuencias."""
    b = [-np.inf if x is None else x for x in bordes]
    b[-1] = np.inf
    conteo, _ = np.histogram(pd.Series(valores).dropna(), bins=b)
    return conteo / conteo.sum()

def chequear_deriva(pedidos, colas, referencia):
    """Devuelve una lista de indicadores con su valor, la referencia y el nivel: ok / aviso / alerta."""
    informe = []
    frec = frecuencias_por_tramo(pedidos["importe"], referencia["importe_bordes"])
    v = psi(referencia["importe_frecuencias"], frec)
    informe.append({"indicador": "PSI importe", "valor": round(v, 3), "referencia": 0.0,
                    "nivel": "alerta" if v > PSI_ALERTA else "aviso" if v > PSI_AVISO else "ok"})
    media = float(pedidos["importe"].mean())
    informe.append({"indicador": "importe medio", "valor": round(media, 2),
                    "referencia": referencia["importe_media"],
                    "nivel": "aviso" if abs(media / referencia["importe_media"] - 1) > 0.15 else "ok"})
    nuevos = float(pedidos["cliente_nuevo"].mean())
    informe.append({"indicador": "tasa cliente_nuevo", "valor": round(nuevos, 3),
                    "referencia": referencia["cliente_nuevo_tasa"],
                    "nivel": "aviso" if abs(nuevos - referencia["cliente_nuevo_tasa"]) > 0.10 else "ok"})
    marcado = float((colas["cola"] != "aprobar").mean())          # revisar + marcar = prob >= 0,2
    dif = abs(marcado - referencia["tasa_marcado"])
    informe.append({"indicador": "tasa de marcado (>=0,2)", "valor": round(marcado, 3),
                    "referencia": referencia["tasa_marcado"],
                    "nivel": "alerta" if dif > 2 * DIF_TASA_MARCADO else "aviso" if dif > DIF_TASA_MARCADO else "ok"})
    return informe

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Chequeo de deriva del predictor de devoluciones")
    parser.add_argument("--entrada", required=True)
    parser.add_argument("--colas", required=True)
    parser.add_argument("--referencia", default="models/referencia_deriva.json")
    args = parser.parse_args()
    referencia = json.loads(Path(args.referencia).read_text())
    informe = chequear_deriva(pd.read_csv(args.entrada), pd.read_csv(args.colas), referencia)
    print(pd.DataFrame(informe).to_string(index=False))
    niveles = {fila["nivel"] for fila in informe}
    if "alerta" in niveles:
        print("\nALERTA: deriva significativa -> abrir tarea de reentrenamiento y avisar a Marta y Diego")
    elif "aviso" in niveles:
        print("\nAVISO: vigilar la semana que viene; si se repite, reentrenar")
    else:
        print("\nOK: sin deriva apreciable")

Explicación:

  • El PSI compara dos histogramas con los mismos tramos: para cada tramo, la diferencia de frecuencias multiplicada por el logaritmo de su cociente, sumado. Vale 0 si son idénticos y crece con la diferencia; la convención de la industria (heredada del scoring de crédito) es que por debajo de 0,1 no pasa nada, entre 0,1 y 0,25 hay que vigilar y por encima de 0,25 hay que actuar. Los tramos son los deciles del entrenamiento (10 % de pedidos en cada uno), así que la referencia es simplemente diez veces 0,1.
  • La tasa de marcado es el indicador más barato y más útil: no necesita etiquetas y refleja cualquier cambio en las entradas que afecte al modelo. Si de repente marca al 51 % en vez de al 27 %, el equipo de revisión se desborda y algo ha cambiado.
  • Los umbrales de aviso y alerta son decisiones, no leyes: se fijan con Diego y se anotan en el documento.

Salida real de las dos ejecuciones:

$ python -m novamarket.deriva --entrada data/raw/pedidos_2025-12-01.csv --colas data/processed/colas_2025-12-01.csv
              indicador   valor  referencia nivel
            PSI importe   0.007      0.0000    ok
          importe medio 124.800    126.1500    ok
     tasa cliente_nuevo   0.308      0.3024    ok
tasa de marcado (>=0,2)   0.316      0.2667    ok

OK: sin deriva apreciable

$ python -m novamarket.deriva --entrada data/raw/pedidos_2025-12-08.csv --colas data/processed/colas_2025-12-08.csv
              indicador   valor  referencia  nivel
            PSI importe   0.224      0.0000  aviso
          importe medio 175.130    126.1500  aviso
     tasa cliente_nuevo   0.453      0.3024  aviso
tasa de marcado (>=0,2)   0.510      0.2667 alerta

ALERTA: deriva significativa -> abrir tarea de reentrenamiento y avisar a Marta y Diego

El primer lote se parece al entrenamiento y todo queda en verde. En el segundo (simulamos una campaña de Navidad: pedidos un 40 % más caros y un 45 % de clientes nuevos), el importe medio ha subido a 175 €, el PSI está en la zona de vigilancia y el modelo marca al 51 % de los pedidos: 1.530 pedidos en las colas de revisar y marcar en un solo día. La alerta salta antes de conocer ninguna devolución real; eso es deriva de datos. Si dentro de un mes, con las etiquetas ya maduras, el AUC sobre esos pedidos hubiera caído, sería además deriva de concepto, y la respuesta sería reentrenar con los datos de la campaña incluidos (python -m novamarket.entrenar con el histórico ampliado, versión 1.1.0, nueva model card, comparar en sombra, sustituir).

Los tests del proyecto se amplían con dos para el lote (tests/test_predecir_lote.py: las fronteras de asignar_cola con 0,19/0,20/0,49/0,50 y que puntuar_lote devuelve una fila por pedido con probabilidades entre 0 y 1) y pytest pasa 7 tests.

11.6 Calendario de monitorización

Frecuencia Qué se mira Quién Umbral de acción
Cada noche El lote se ejecutó; n de pedidos, tiempo, errores; recuento por cola en logs/decisiones.jsonl Sistemas (alerta automática) Lote ausente o n fuera de ±30 % del habitual → aviso inmediato
Cada semana deriva.py: PSI y media de importe, tasa de clientes nuevos, tasa de marcado Marta Aviso → vigilar; alerta → tarea de reentrenamiento
Cada semana Cola "revisar": pedidos revisados, decisión de la revisora, tiempo medio Diego Cola > 300/día durante 3 días → revisar umbral inferior
Cada mes Con etiquetas maduras (pedidos de hace ≥ 30 días): AUC, coste real por umbral, coste frente a línea base; paridad de tasas entre zonas (02-04) Marta + Diego AUC < 0,80 o coste ≥ línea base → reentrenar o parar; ratio de impacto < 0,8 → revisar equidad
Cada trimestre Revisión de la model card, del documento de definición y de la evaluación de impacto; reentrenamiento programado aunque no haya alerta Marta, Diego, legal Siempre (mantenimiento preventivo)

Con esto el proyecto ha cerrado el ciclo: definición, datos, prototipo, evaluación, despliegue por lotes con banda de revisión y reversión, monitorización con alertas y documentación. En 09-04 recorrerás tú este mismo camino con un caso propio.

Errores Comunes y Consejos

  • Empezar por el modelo. Escribe el documento de definición (una tabla) antes de abrir el notebook. Si no puedes rellenar "decisión que cambia" y "línea base", el proyecto aún no existe.
  • Métrica técnica sin traducción. Un AUC solo no convence a nadie; acompáñalo siempre del coste en euros y del número de casos que llegan a las personas.
  • Datos que no existirán o cambiarán al servir. Comprueba columna a columna que existe en el momento de predecir y que se calcula igual (dias_desde_inicio era un ejemplo silencioso). Usa la misma consulta para entrenar y servir.
  • División aleatoria en problemas temporales. Test posterior al entrenamiento; si no, métricas optimistas.
  • Desplegar de golpe. Sombra → piloto en un almacén → todo, siempre con interruptor de reversión.
  • Automatizar por completo decisiones sobre personas. Banda de revisión humana proporcional al impacto (02-04) y registro de lo que decide la persona.
  • No monitorizar porque "el modelo ya funciona". Programa la tasa de marcado y un PSI desde el primer día; son quince líneas.
  • Umbrales de alerta sacados de un manual. 0,1/0,25 para el PSI y ±5 puntos de tasa de marcado son puntos de partida; ajústalos con la variabilidad real de tus semanas y anótalos.
  • Documentar al final. La model card que se genera desde el código no cuesta nada mantener; la que se escribe a mano al final no se escribe.
  • Consejo: guarda cada versión del modelo con su JSON y su ficha, y nunca sobrescribas la anterior hasta que la nueva haya pasado por sombra.

Ejercicios

Ejercicio 1: capacidad de revisión

Diego solo puede revisar 300 pedidos al día. Con el fichero colas_2025-12-01.csv (o generándolo con simular_dia), calcula cuántos pedidos irían a "revisar" si el umbral inferior fuera 0,25 y 0,30 (manteniendo 0,5 como superior). Propón una decisión razonada. Pista: puntuar_lote ya devuelve prob_devolucion; basta con contar ((p >= u) & (p < 0.5)).sum().

Ejercicio 2: deriva de concepto sin deriva de datos

Describe (sin código) un cambio en NovaMarket que haría que deriva.py no diera ninguna alerta y, sin embargo, el modelo empeorase de verdad. ¿Qué indicador del calendario de monitorización lo detectaría, y con cuánto retraso? ¿Qué añadirías al chequeo semanal para reducir ese retraso?

Ejercicio 3: documento de definición del recomendador

Rellena la tabla de definición de la sección 11.1 para el recomendador de productos (caso de uso 1) de NovaMarket. Presta especial atención a "decisión que cambia", "métrica de negocio", "línea base" y "despliegue" (¿lote o tiempo real?, ¿modo sombra o A/B?).

Soluciones

Solución 1. Ejecutado sobre colas_2025-12-01.csv (2.999 pedidos), contando ((p >= u) & (p < 0.5)).sum() para cada u: con umbral inferior 0,2 hay 641 pedidos en "revisar"; con 0,25 quedan 484; con 0,3 quedan 355 (la cola "marcar" no cambia: 308). Ninguno de los tres cabe en 300, pero con 0,3 el exceso es pequeño (55 pedidos). El precio se ve en el test de 04-05 (750 pedidos, revisión a 3 € y revisora que acierta): con banda 0,2-0,5 el coste era 1.555 € y se escapaban 35 devoluciones; con 0,25-0,5 sube a 1.795 € (47 escapan) y con 0,3-0,5 a 1.966 € (56 escapan). Es decir, subir el umbral inferior a 0,3 ahorra unas 290 revisiones diarias pero cuesta unos 400 € más por cada 750 pedidos: capacidad frente a coste, la clase de intercambio que decide Diego, no el modelo. Decisión razonable: umbral inferior 0,3 en el piloto, cola ordenada por riesgo (los 55 que no se llegan a revisar son los de menor probabilidad, entre 0,30 y 0,32, y se aprueban), medir el coste real al mes con las etiquetas maduras y, si compensa, pedir capacidad de revisión adicional para volver a 0,2. Alternativa igual de válida: mantener 0,2, revisar los 300 más arriesgados y aprobar el resto; como la cola está ordenada, el resultado es prácticamente el mismo que subir el umbral, pero deja registrado cuántos pedidos "de riesgo medio" quedan sin revisar cada día. Lo importante no es el número sino el proceso: la banda es una decisión operativa que se toma con quien la sufre, no un parámetro del modelo.

Solución 2. Ejemplo: NovaMarket lanza "devoluciones gratuitas y sin preguntas durante 60 días". Los pedidos que entran son iguales (mismos importes, mismos clientes, misma tasa de marcado del modelo), pero ahora se devuelven más y por motivos distintos: la relación entre entradas y salida ha cambiado (deriva de concepto). deriva.py no vería nada. Lo detectaría la revisión mensual con etiquetas maduras (AUC y coste real por umbral), con un retraso de entre 30 y 60 días (lo que tardan las devoluciones en registrarse). Para acortarlo: (a) añadir al chequeo semanal la tasa de devolución real de los pedidos de hace 3-4 semanas comparada con la del entrenamiento (16,4 %), aunque sea con etiquetas parciales; (b) usar la cola de revisión como sensor: la proporción de pedidos que la revisora confirma como riesgo cambia antes que las cifras globales; (c) suscribir el proyecto a los cambios de política comercial (una fila en el registro de decisiones: "cambio de política el día X"), porque las derivas de concepto casi siempre tienen una causa conocida por negocio.

Solución 3. Una propuesta: Pregunta: qué productos tienen más probabilidad de interesar a este cliente ahora. Decisión que cambia: qué 6 productos se muestran en el bloque "También te puede interesar" de la ficha y del correo semanal (hoy: los más vendidos de la categoría). Métrica de negocio: ingresos adicionales por sesión y tasa de clic/compra del bloque; técnica: precisión en el top-6 (proporción de recomendaciones que acaban en clic o compra) medida sobre sesiones pasadas. Línea base: "más vendidos de la categoría" (lo que hay hoy) y "los mismos que compró el cliente la última vez". Criterio de éxito: +X % de ingresos del bloque en un A/B de 4 semanas sin subir la tasa de devolución. Restricciones: RGPD (perfilado: información y posibilidad de oponerse), no recomendar por código postal, no empujar productos con devolución alta (conexión con el caso 3), riesgo mínimo en el AI Act. Datos: pedidos.csv, clientes.csv, productos.csv, navegación web; fuga típica: usar compras posteriores a la sesión. Despliegue: la lista de recomendados por cliente puede calcularse en lote cada noche (basta para el correo y para la mayoría de fichas) y servirse desde una tabla; la web la lee por API; A/B obligatorio porque el efecto solo se mide causalmente. Monitorización: cobertura (¿a cuántos clientes se recomienda algo?), diversidad, clics, y que no aparezcan siempre los mismos cinco productos. Propietarios: marketing (negocio), Marta, sistemas.

Conclusión

Esta lección ha convertido el mapa de 04-01 en un método. El ciclo de vida de un proyecto de IA (definición del problema y del éxito, datos, exploración, prototipo, evaluación, despliegue, monitorización y documentación) es la forma que han tomado CRISP-DM y CRISP-ML(Q), con las flechas de vuelta como norma y la calidad como hilo transversal. Hemos visto qué preguntas responde cada fase (la decisión que cambia, la métrica en euros frente a la técnica, la línea base, la disponibilidad de cada dato en el momento de predecir, la división temporal, el presupuesto del prototipo, la evaluación con las personas afectadas, lote frente a tiempo real, sombra y A/B, reversión, deriva de datos y de concepto, model card y ficha de datos), quién participa (negocio, datos, ingeniería, legal) y qué errores hunden proyectos (empezar por el modelo, no tener línea base, métrica equivocada, datos que no existirán en producción, no cerrar el ciclo). Y lo hemos hecho de verdad con el predictor de devoluciones dentro de novamarket_ia/: un documento de definición de una página, la corrección de un training-serving skew en dias_desde_inicio, una model card en JSON y Markdown generada desde entrenar.py con las cifras de siempre (AUC 0,844; 2.485 € → 1.610 €), un predecir_lote.py que reparte 2.999 pedidos de una noche en 2.050 aprobados, 641 a revisar y 308 marcados y deja constancia en un registro, un deriva.py que da verde en un día normal y alerta en un día de campaña (PSI 0,224, tasa de marcado del 51 %), y un calendario de monitorización con responsables y umbrales.

Marta y Diego tienen ahora un método y un proyecto que lo sigue. Pero un método se aprende también mirando cómo lo han aplicado, bien o mal, otros: qué hicieron las plataformas de recomendación, los sistemas de diagnóstico por imagen, AlphaGo y AlphaFold, las herramientas de selección de personal que discriminaban, los asistentes que inventaron políticas y los proyectos célebres que fracasaron. Es la siguiente lección, 08-02, Casos de Estudio en IA, donde cada caso termina con la misma pregunta: qué aprende NovaMarket de esto.

Fundamentos de Inteligencia Artificial (IA)

Módulo 1: Introducción a la Inteligencia Artificial

Módulo 2: Principios Básicos de la IA

Módulo 3: Algoritmos en IA

Módulo 4: Aprendizaje Automático (Machine Learning)

Módulo 5: Redes Neuronales y Deep Learning

Módulo 6: Lógica y Sistemas Expertos

Módulo 7: Herramientas y Lenguajes de Programación en IA

Módulo 8: Proyectos y Casos de Estudio

Módulo 9: Ejercicios y Prácticas

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados