Las tres lecciones anteriores han consolidado, ejercicio a ejercicio, los algoritmos (09-01), el aprendizaje automático (09-02) y las redes neuronales (09-03). Falta lo que 08-01 llamó "el método": llevar un caso completo desde la pregunta de negocio hasta un prototipo documentado y presentable, tomando todas las decisiones intermedias sin que la lección las tome por ti. Esta lección es esa práctica. Resolvemos de principio a fin el caso 9 de NovaMarket, el diagnóstico de la causa de las incidencias, que hasta ahora solo tenía la red bayesiana de 06-03 y las reglas de 06-04: generaremos un incidencias.csv sintético coherente con aquellas tablas de probabilidad, mediremos la red como línea base, entrenaremos un clasificador con columnas que la red no ve, los combinaremos con reglas de negocio en colas de trabajo, escribiremos la model card y un chequeo de deriva, y prepararemos la presentación a Diego. Al final encontrarás el guion equivalente para el caso 4 (clasificar reseñas y priorizar la respuesta de atención al cliente), por si prefieres hacer el proyecto con texto.
Cómo trabajar la lección: es un proyecto de 8-12 horas repartidas en varias sesiones. Lee las reglas del juego, haz tú cada paso (documento, código, tablas) y solo después compara con nuestra versión. Al terminar, autoevalúate con la rúbrica. Necesitas el entorno del módulo 7 (pandas, scikit-learn, joblib) y, si haces la alternativa, resenas_nm.py.
Contenido
- Reglas del juego: tiempo, entregables y rúbrica de autoevaluación
- Definición del problema: el documento de una página
- Datos:
incidencias.csvsintético coherente con la red de 06-03 - Línea base: la red bayesiana por enumeración
- Clasificador supervisado con las columnas extra y comparación honesta
- Combinación neurosimbólica: reglas de negocio y colas de trabajo
- Model card y chequeo de deriva
- Presentación de resultados a Diego
- Rúbrica final y guion para el caso 4
- Errores Comunes y Consejos
- Conclusión
- Reglas del juego: tiempo, entregables y rúbrica de autoevaluación
Tiempo sugerido: 8-12 horas en 4-6 sesiones (definición y datos, línea base y clasificador, reglas y colas, documentación y presentación).
Entregables (una carpeta novamarket_ia/incidencias/ con la estructura de 07-04):
DEFINICION.md: el documento de definición de una página (sección 1).datos.pycon el generador reproducible ydata/raw/incidencias.csv(fuera de Git si fuera real).linea_base.py,entrenar.py,decidir.py,deriva.py: código ejecutable con salidas.models/model_card.jsonymodels/referencia_deriva.json.PRESENTACION.md: el guion de 5 diapositivas.- Esta rúbrica rellenada por ti.
| Criterio | 1 (insuficiente) | 2 (aceptable) | 3 (bien) | 4 (excelente) |
|---|---|---|---|---|
| Definición | Solo "predecir la causa" | Pregunta y decisión escritas | + métrica de negocio y técnica, línea base | + restricciones legales/éticas y criterio de éxito verificable |
| Datos | Generador sin semilla ni coherencia | Reproducible | + coherente con las CPT, con etiquetas parciales | + comprobación numérica de la coherencia y fugas identificadas |
| Línea base | No hay | Mayoritaria | Red bayesiana evaluada | + análisis por clase y por confianza |
| Modelo | Un modelo sin validar | Validación cruzada | + comparación con la línea base en las mismas filas | + curva de aprendizaje o análisis de qué columna aporta |
| Combinación | Solo el modelo | Umbral de revisión | + reglas de negocio con traza | + colas medidas (tamaño y acierto) y efecto del umbral |
| Documentación | Nada | Model card mínima | + referencia y chequeo de deriva | + calendario de monitorización y limitaciones honestas |
| Comunicación | Cifras sueltas | Tabla de resultados | Guion de 5 diapositivas | + una decisión pedida a Diego y riesgos |
- Definición del problema: el documento de una página
Recordatorio (08-01, secciones 2 y 11.1). Antes de tocar un dato: qué pregunta responde el sistema, qué decisión cambia, métrica de negocio y técnica, línea base, restricciones, criterio de éxito. Una tabla de una página firmada por negocio (Diego) y datos (Marta).
| Campo | Contenido |
|---|---|
| Pregunta | Dada una incidencia recién abierta (almacén, síntomas declarados, días desde el envío, transportista, valor), ¿cuál es su causa más probable y con qué confianza? |
| Decisión que cambia | Hoy el agente lee el ticket y decide a mano. Con el sistema, cada incidencia entra en una de dos colas: automática (causa aceptada, acciones lanzadas: reenvío, traza con el transportista, aviso a compras) o revisión humana (el agente decide con la propuesta y la traza a la vista). Las acciones automáticas son todas reversibles. |
| Métrica de negocio | Minutos de agente por incidencia (una incidencia automática cuesta ~2 min; una revisada, ~8) y acierto en la cola automática ≥ 85 % (un diagnóstico erróneo automático genera una segunda incidencia). |
| Métrica técnica | Exactitud y F1 macro sobre las cinco causas; log-loss (calibración) porque las reglas usan la probabilidad. |
| Línea base | Mayoritaria ("siempre error de picking", 31 %) y la red bayesiana de 06-03 con las CPT de Diego (sin datos etiquetados). |
| Criterio de éxito | Automatizar ≥ 60 % de las incidencias con ≥ 85 % de acierto en esa cola; ganar a la red bayesiana en exactitud; presentar traza legible por incidencia. |
| Restricciones | La causa no se usa para sancionar a nadie (empleados o transportistas) sin revisión humana; RGPD: sin datos personales del cliente en el modelo; AI Act: riesgo mínimo (soporte interno). Solo el 50 % de las incidencias tiene la causa registrada. |
| Datos | incidencias.csv (2.000 filas, 3 meses), síntomas, almacén, días, transportista, valor; etiqueta = causa registrada por el agente al cerrar. |
| Despliegue | Lote cada hora sobre las incidencias nuevas; salida = cola + causa + acciones + traza; sombra 2 semanas en Zaragoza. |
| Propietario / responsable | Diego (operaciones), Marta (modelo), atención al cliente (usuarios), compras y logística (acciones). |
| Presupuesto | Prototipo: esta práctica; piloto: 4 semanas. |
- Datos:
incidencias.csv sintético coherente con la red de 06-03
incidencias.csv sintético coherente con la red de 06-03Recordatorio (06-03, secciones 6-7; 02-03). La red tiene almacen (zaragoza 0,60 / getafe 0,40) → causa (5 valores, CPT por almacén) → cuatro síntomas binarios con P(síntoma | causa). Los datos reales tienen la causa solo en parte de los casos y columnas que la red no modela; un generador reproducible permite comprobar que el sistema recupera lo que sabemos que hay.
Tu tarea. Escribe generar_incidencias(n=2000, semilla=42, frac_etiquetada=0.5) que muestree almacén, causa y síntomas exactamente de las CPT de 06-03 y añada tres columnas fuera de la red: dias_desde_envio (Poisson con media según la causa: picking 3, transporte 4, dirección 8, stock 9, proveedor 7, más 1), transportista (según el almacén: Zaragoza usa sobre todo TransNova; Getafe, RapidEnvío) y valor_pedido (gamma, independiente). Guarda la causa real aparte y deja causa_registrada solo en la mitad de las filas. Comprueba numéricamente que las tasas coinciden con las CPT.
import numpy as np, pandas as pd, json
from itertools import product
pd.set_option("display.width", 200)
CAUSAS = ["error_picking", "dano_transporte", "direccion_incorrecta", "stock_desactualizado", "fallo_proveedor"]
SINTOMAS = ["paquete_danado", "producto_equivocado", "no_llega", "retraso"]
P_ALMACEN = {"zaragoza": 0.60, "getafe": 0.40}
P_CAUSA = {"zaragoza": [0.35, 0.30, 0.15, 0.08, 0.12], "getafe": [0.25, 0.20, 0.12, 0.33, 0.10]} # CPT de 06-03
P_SINTOMA = {"paquete_danado": [0.10, 0.80, 0.02, 0.02, 0.15], # P(sí | causa)
"producto_equivocado": [0.70, 0.02, 0.01, 0.10, 0.30],
"no_llega": [0.05, 0.10, 0.85, 0.40, 0.30],
"retraso": [0.10, 0.30, 0.60, 0.90, 0.70]}
DIAS_MEDIA = {"error_picking": 3, "dano_transporte": 4, "direccion_incorrecta": 8, "stock_desactualizado": 9, "fallo_proveedor": 7}
P_TRANSPORTISTA = {"zaragoza": {"TransNova": 0.5, "RapidEnvio": 0.3, "LogiSur": 0.2},
"getafe": {"RapidEnvio": 0.5, "LogiSur": 0.3, "TransNova": 0.2}}
def generar_incidencias(n=2000, semilla=42, frac_etiquetada=0.5, p_causa=None):
"""incidencias.csv sintético coherente con la red de 06-03. 'causa' es la verdad; 'causa_registrada' solo en frac_etiquetada."""
rng = np.random.default_rng(semilla); p_causa = p_causa or P_CAUSA # p_causa: para simular un mes distinto (deriva)
almacen = rng.choice(list(P_ALMACEN), size=n, p=list(P_ALMACEN.values()))
causa = np.array([rng.choice(CAUSAS, p=p_causa[a]) for a in almacen])
idx = np.array([CAUSAS.index(c) for c in causa])
df = pd.DataFrame({"id_incidencia": [f"I{7000 + i}" for i in range(n)], "almacen": almacen})
for s in SINTOMAS:
df[s] = (rng.random(n) < np.array(P_SINTOMA[s])[idx]).astype(int)
df["dias_desde_envio"] = np.clip(rng.poisson([DIAS_MEDIA[c] for c in causa]) + 1, 1, 30)
df["transportista"] = [rng.choice(list(P_TRANSPORTISTA[a]), p=list(P_TRANSPORTISTA[a].values())) for a in almacen]
df["valor_pedido"] = np.round(rng.gamma(2.0, 60.0, n) + 5, 2)
df["causa"] = causa
df["causa_registrada"] = np.where(rng.random(n) < frac_etiquetada, causa, None)
return df
inc = generar_incidencias(); print(inc.shape, "| etiquetadas:", inc["causa_registrada"].notna().sum())
print(inc.drop(columns=["causa"]).head(3).to_string())
print(inc.groupby("almacen")["causa"].value_counts(normalize=True).unstack()[CAUSAS].round(2).to_string())
print(inc.groupby("causa")[SINTOMAS].mean().loc[CAUSAS].round(2))
print("días medios por causa:", inc.groupby("causa")["dias_desde_envio"].mean().round(1).to_dict())
etiq = inc[inc["causa_registrada"].notna()].copy(); y = etiq["causa_registrada"](2000, 11) | etiquetadas: 1003
id_incidencia almacen paquete_danado producto_equivocado no_llega retraso dias_desde_envio transportista valor_pedido causa_registrada
0 I7000 getafe 0 0 0 1 9 RapidEnvio 23.26 stock_desactualizado
1 I7001 zaragoza 1 0 0 0 5 TransNova 100.02 dano_transporte
2 I7002 getafe 0 0 1 1 7 RapidEnvio 119.75 NaN
causa error_picking dano_transporte direccion_incorrecta stock_desactualizado fallo_proveedor
almacen
getafe 0.26 0.19 0.12 0.32 0.11
zaragoza 0.37 0.30 0.14 0.08 0.11
paquete_danado producto_equivocado no_llega retraso
causa
error_picking 0.08 0.70 0.06 0.10
dano_transporte 0.83 0.01 0.11 0.32
direccion_incorrecta 0.02 0.02 0.83 0.58
stock_desactualizado 0.03 0.11 0.40 0.90
fallo_proveedor 0.13 0.25 0.30 0.68
días medios por causa: {'dano_transporte': 5.1, 'direccion_incorrecta': 9.0, 'error_picking': 4.0, 'fallo_proveedor': 8.0, 'stock_desactualizado': 10.2}Las frecuencias muestrales reproducen las CPT con ±0,03 (2.000 filas): el generador es coherente. Decisiones de datos que debes dejar escritas: causa (la verdad) no existe en el fichero real y solo se usa aquí para evaluar la cola sin etiqueta al final; el modelo se entrena únicamente con las 1.003 etiquetadas; id_incidencia no es característica; no hay datos personales.
- Línea base: la red bayesiana por enumeración
Recordatorio (06-03, sección 7). inferir(consulta, evidencia) suma la conjunta sobre las variables ocultas y normaliza. Con la evidencia completa (almacén y los cuatro síntomas) no hay ocultas: es Bayes directo. La red no necesita etiquetas: sus 29 cifras las puso Diego.
Tu tarea. Reutiliza RED e inferir de 06-03 con las tablas de la sección 2, escribe diagnostico_bayes(df) que devuelva P(causa | almacén, síntomas) por fila (hay solo 2 × 2⁴ = 32 combinaciones de evidencia: cachea), evalúa exactitud, F1 macro y log-loss sobre las 1.003 etiquetadas frente a la mayoritaria, y analiza el acierto según la confianza máxima.
from sklearn.metrics import accuracy_score, f1_score, log_loss, classification_report
def cpt_binaria(lista): return {(c,): {1: p, 0: round(1 - p, 4)} for c, p in zip(CAUSAS, lista)}
RED = {"almacen": ([], list(P_ALMACEN), {(): P_ALMACEN}),
"causa": (["almacen"], CAUSAS, {(a,): dict(zip(CAUSAS, P_CAUSA[a])) for a in P_ALMACEN})}
for s in SINTOMAS: RED[s] = (["causa"], [1, 0], cpt_binaria(P_SINTOMA[s]))
ORDEN = ["almacen", "causa"] + SINTOMAS
def prob_conjunta(asig):
p = 1.0
for nodo in ORDEN:
padres, _, cpt = RED[nodo]; p *= cpt[tuple(asig[q] for q in padres)][asig[nodo]]
return p
def inferir(consulta, evidencia):
ocultas = [n for n in ORDEN if n != consulta and n not in evidencia]; res = {}
for v in RED[consulta][1]:
res[v] = sum(prob_conjunta(dict(evidencia, **{consulta: v}, **dict(zip(ocultas, comb))))
for comb in product(*[RED[n][1] for n in ocultas]))
z = sum(res.values()); return {v: p / z for v, p in res.items()}
def diagnostico_bayes(df):
cache, filas = {}, []
for _, r in df.iterrows():
clave = (r["almacen"],) + tuple(int(r[s]) for s in SINTOMAS)
if clave not in cache:
cache[clave] = inferir("causa", dict({"almacen": r["almacen"]}, **{s: int(r[s]) for s in SINTOMAS}))
filas.append(cache[clave])
return pd.DataFrame(filas, index=df.index)[CAUSAS]
CLASES = sorted(CAUSAS) # el orden alfabético que usa scikit-learn
P_bayes = diagnostico_bayes(etiq); pred_bayes = P_bayes.idxmax(axis=1)
print("Mayoritaria (error_picking):", round((y == "error_picking").mean(), 3))
print("Red bayesiana: exactitud", round(accuracy_score(y, pred_bayes), 3), "| F1 macro", round(f1_score(y, pred_bayes, average="macro"), 3),
"| log-loss", round(log_loss(y, P_bayes[CLASES].values, labels=CLASES), 3))
print(classification_report(y, pred_bayes, digits=2))
maxp = P_bayes.max(axis=1)
print(f"P máxima media {maxp.mean():.3f} | filas con P ≥ 0,6: {(maxp >= 0.6).mean():.1%}, acierto {accuracy_score(y[maxp >= 0.6], pred_bayes[maxp >= 0.6]):.3f} | P < 0,6: acierto {accuracy_score(y[maxp < 0.6], pred_bayes[maxp < 0.6]):.3f}")Mayoritaria (error_picking): 0.314
Red bayesiana: exactitud 0.719 | F1 macro 0.606 | log-loss 0.789
precision recall f1-score support
dano_transporte 0.88 0.82 0.85 257
direccion_incorrecta 0.62 0.62 0.62 134
error_picking 0.78 0.90 0.83 315
fallo_proveedor 0.50 0.05 0.09 118
stock_desactualizado 0.54 0.77 0.64 179
accuracy 0.72 1003
macro avg 0.66 0.63 0.61 1003
P máxima media 0.723 | filas con P ≥ 0,6: 67.3%, acierto 0.822 | P < 0,6: acierto 0.506La red acierta el 71,9 % (frente al 31,4 % de la mayoritaria) sin haber visto una sola etiqueta: es el valor del conocimiento experto. Sus puntos débiles son claros: fallo_proveedor casi nunca se diagnostica (recall 0,05: sus síntomas se parecen a los de stock y de picking y su prior es el más bajo), y stock/dirección se confunden entre sí. Y la confianza es informativa: cuando P ≥ 0,6 (dos tercios de los casos) acierta el 82 %; por debajo, la mitad. Esa es la semilla de la banda de revisión de la sección 5. Como los datos se generaron con estas mismas tablas, la red es el mejor clasificador posible con esas cinco variables: cualquier mejora tendrá que venir de las columnas que la red no ve.
- Clasificador supervisado con las columnas extra y comparación honesta
Recordatorio (04-04, 04-05, 09-02). Regresión logística multiclase, árbol, bosque y gradient boosting sobre un ColumnTransformer; validación cruzada estratificada; comparar en las mismas filas y por pliegue; mirar la matriz de confusión y qué característica aporta.
Tu tarea. Entrena y compara con StratifiedKFold(5) cuatro modelos sobre almacén, transportista, síntomas, días y valor; muestra que la mejora viene de dias_desde_envio (quita columnas); compara con la red por pliegue; dibuja una curva de aprendizaje con 100, 250, 500 y 1.003 etiquetas.
from sklearn.model_selection import StratifiedKFold, cross_val_predict
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier
CATS, NUMS, BINS = ["almacen", "transportista"], ["dias_desde_envio", "valor_pedido"], SINTOMAS
COLS = CATS + NUMS + BINS
def crear_modelo(clasificador=None, cats=CATS, nums=NUMS, bins=BINS):
partes = ([("cat", OneHotEncoder(handle_unknown="ignore"), cats)] if cats else []) + \
([("num", StandardScaler(), nums)] if nums else []) + ([("bin", "passthrough", bins)] if bins else [])
return Pipeline([("prep", ColumnTransformer(partes)), ("modelo", clasificador if clasificador is not None else LogisticRegression(max_iter=2000))])
cv = StratifiedKFold(5, shuffle=True, random_state=42)
def evaluar_cv(pipe, Xd, nombre):
P = cross_val_predict(pipe, Xd, y, cv=cv, method="predict_proba"); pred = np.array(CLASES)[P.argmax(1)]
print(f"{nombre:34s} exactitud {accuracy_score(y, pred):.3f} F1 macro {f1_score(y, pred, average='macro'):.3f} log-loss {log_loss(y, P, labels=CLASES):.3f}")
return P, pred
modelos = {"Logística multiclase": LogisticRegression(max_iter=2000), "Árbol (prof. 5)": DecisionTreeClassifier(max_depth=5, random_state=42),
"Bosque (300, hoja 5)": RandomForestClassifier(n_estimators=300, min_samples_leaf=5, random_state=42),
"HGB (prof. 3)": HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05, max_iter=200, random_state=42)}
resultados = {n: evaluar_cv(crear_modelo(m), etiq[COLS], n) for n, m in modelos.items()}
evaluar_cv(crear_modelo(cats=["almacen"], nums=[], bins=BINS), etiq[["almacen"] + BINS], "Logística SOLO almacén + síntomas")
evaluar_cv(crear_modelo(cats=[], nums=["dias_desde_envio"], bins=[]), etiq[["dias_desde_envio"]], "Logística SOLO días")
P_log, pred_log = resultados["Logística multiclase"]
print(pd.crosstab(y, pred_log, rownames=["real"], colnames=["logística"]).to_string())
accs = [(accuracy_score(y.iloc[te], pred_bayes.iloc[te]), accuracy_score(y.iloc[te], pred_log[te])) for _, te in cv.split(etiq[COLS], y)]
print("Por pliegue (bayes, logística):", [(round(a, 3), round(b, 3)) for a, b in accs], "| ventaja media", round(np.mean([b - a for a, b in accs]), 3))
for n_lab in (100, 250, 500, 1003):
sub = etiq.sample(n_lab, random_state=1) if n_lab < len(etiq) else etiq
P = cross_val_predict(crear_modelo(), sub[COLS], sub["causa_registrada"], cv=cv, method="predict_proba")
print(f"n etiquetadas={n_lab:4d}: logística {accuracy_score(sub['causa_registrada'], np.array(CLASES)[P.argmax(1)]):.3f} red bayesiana {accuracy_score(sub['causa_registrada'], diagnostico_bayes(sub).idxmax(axis=1)):.3f}")Logística multiclase exactitud 0.738 F1 macro 0.658 log-loss 0.690 Árbol (prof. 5) exactitud 0.690 F1 macro 0.598 log-loss 1.796 Bosque (300, hoja 5) exactitud 0.735 F1 macro 0.646 log-loss 0.759 HGB (prof. 3) exactitud 0.724 F1 macro 0.655 log-loss 0.802 Logística SOLO almacén + síntomas exactitud 0.709 F1 macro 0.612 log-loss 0.804 Logística SOLO días exactitud 0.454 F1 macro 0.292 log-loss 1.206 logística dano_transporte direccion_incorrecta error_picking fallo_proveedor stock_desactualizado real dano_transporte 211 3 26 12 5 direccion_incorrecta 2 85 4 15 28 error_picking 10 2 291 9 3 fallo_proveedor 15 24 20 26 33 stock_desactualizado 3 24 5 20 127 Por pliegue (bayes, logística): [(0.677, 0.716), (0.731, 0.721), (0.751, 0.761), (0.73, 0.77), (0.705, 0.72)] | ventaja media 0.019 n etiquetadas= 100: logística 0.780 red bayesiana 0.790 n etiquetadas= 250: logística 0.752 red bayesiana 0.744 n etiquetadas= 500: logística 0.764 red bayesiana 0.746 n etiquetadas=1003: logística 0.738 red bayesiana 0.719
Lectura honesta:
- La logística multiclase es la mejor (0,738; F1 macro 0,658; log-loss 0,690, la mejor calibrada), seguida del bosque; el árbol solo está mal calibrado (log-loss 1,8) y HGB no aporta con 1.000 filas. La ventaja sobre la red bayesiana es +1,9 puntos, a favor en 4 de 5 pliegues: pequeña pero real.
- De dónde viene: la logística solo con almacén y síntomas da 0,709, un poco por debajo de la red (0,719), como debe ser (la red es el modelo generador exacto de esas variables);
dias_desde_enviosola clasifica el 45 %, y es la que suma al combinarse: fallo_proveedor pasa de recall 0,05 a 0,22 y stock/dirección se separan algo mejor.transportistayvalor_pedidono aportan (por construcción no dependen de la causa: comprobarlo es parte del trabajo). - La curva de aprendizaje cuenta la historia de 06-03: con 100 etiquetas la red del experto gana (0,79 frente a 0,78); a partir de unas 250 el modelo aprendido la iguala y con 1.000 la supera. Con pocas etiquetas, conocimiento; con muchas, datos; y lo ideal (variante) es lo neurosimbólico también aquí: estimar las CPT de la red desde las etiquetas con suavizado de Laplace y añadirle un nodo
diasdiscretizado. - Decisión: la logística multiclase, por métrica, calibración y explicabilidad (sus coeficientes son legibles y sus probabilidades alimentan las reglas de la sección 5); la red se queda como línea base documentada y como respaldo si el etiquetado se degrada.
- Combinación neurosimbólica: reglas de negocio y colas de trabajo
Recordatorio (06-04, secciones 9 y 11; 08-01, 11.4). El modelo es un sensor que devuelve probabilidades; las reglas convierten esas probabilidades en hechos con una banda de revisión humana, aplican la política y dejan traza legible. La salida operativa es una tabla de colas.
Tu tarea. Escribe decidir(fila, probs) con estas reglas y evalúa las colas con las probabilidades de validación cruzada: R0 pedidos de más de 300 € siempre a revisión humana; R1 confianza < 0,6 → revisión humana; R2 confianza ≥ 0,6 → cola automática con la causa aceptada; R3 daño en transporte → abrir traza con el transportista; R4 error de picking → reenvío desde el almacén; R5 "no llega" y más de 10 días → reclamación al transportista sea cual sea la causa; R6 stock desactualizado → aviso a compras. Mide tamaño y acierto de cada cola, el efecto del umbral, y aplica el modelo final a las 997 incidencias sin etiqueta.
UMBRAL_AUTO, VALOR_ALTO, DIAS_MAX = 0.60, 300.0, 10
def decidir(fila, probs):
causa, p = CLASES[int(np.argmax(probs))], float(np.max(probs)); acciones, traza = [], []
if fila["valor_pedido"] > VALOR_ALTO:
cola = "revision_humana"; traza.append(f"R0: valor {fila['valor_pedido']:.0f} € > {VALOR_ALTO:.0f} € → siempre revisa una persona")
elif p < UMBRAL_AUTO:
cola = "revision_humana"; traza.append(f"R1: P({causa})={p:.2f} < {UMBRAL_AUTO} → zona gris")
else:
cola = "automatica"; traza.append(f"R2: P({causa})={p:.2f} ≥ {UMBRAL_AUTO} → causa aceptada como hecho")
if causa == "dano_transporte" and p >= UMBRAL_AUTO:
acciones.append(f"abrir_traza:{fila['transportista']}"); traza.append("R3: daño en transporte → traza con el transportista")
if causa == "error_picking" and p >= UMBRAL_AUTO:
acciones.append(f"reenvio_desde:{fila['almacen']}"); traza.append("R4: error de picking → reenvío inmediato y aviso al almacén")
if fila["no_llega"] == 1 and fila["dias_desde_envio"] > DIAS_MAX:
acciones.append("reclamacion_transportista"); traza.append(f"R5: no llega y {fila['dias_desde_envio']} días > {DIAS_MAX} → reclamación, sea cual sea la causa")
if causa == "stock_desactualizado" and p >= UMBRAL_AUTO:
acciones.append("aviso_compras"); traza.append("R6: stock desactualizado → aviso a compras y alternativa al cliente")
return {"cola": cola, "causa_propuesta": causa, "confianza": round(p, 3), "acciones": acciones, "traza": traza}
decisiones = pd.DataFrame([decidir(f, P_log[i]) for i, (_, f) in enumerate(etiq.iterrows())], index=etiq.index)
etiq2 = pd.concat([etiq, decisiones], axis=1)
for cola, g in etiq2.groupby("cola"):
print(f"{cola:16s} n={len(g):4d} ({len(g) / len(etiq2):.1%}) acierto de la causa propuesta {accuracy_score(g['causa_registrada'], g['causa_propuesta']):.3f}")
print("acciones lanzadas:", pd.Series([a.split(':')[0] for l in etiq2["acciones"] for a in l]).value_counts().to_dict())
r = etiq2.iloc[6]; print(r["id_incidencia"], "| real:", r["causa_registrada"], "→", r["cola"], r["causa_propuesta"], r["confianza"], r["acciones"]); [print(" ", t) for t in r["traza"]]
for u in (0.5, 0.6, 0.7, 0.8):
auto = etiq2[(P_log.max(1) >= u) & (etiq2["valor_pedido"] <= VALOR_ALTO)]
print(f"umbral {u}: automática {len(auto) / len(etiq2):.1%}, acierto {accuracy_score(auto['causa_registrada'], auto['causa_propuesta']):.3f}")
modelo = crear_modelo().fit(etiq[COLS], y) # modelo final con las 1.003 etiquetadas
sin = inc[inc["causa_registrada"].isna()].copy(); P_sin = modelo.predict_proba(sin[COLS])
dec_sin = pd.DataFrame([decidir(f, P_sin[i]) for i, (_, f) in enumerate(sin.iterrows())], index=sin.index)
auto = dec_sin["cola"] == "automatica"
print("Sin etiqueta:", dec_sin["cola"].value_counts().to_dict(), "| acierto real (oculto) automática:", round(accuracy_score(sin.loc[auto, "causa"], dec_sin.loc[auto, "causa_propuesta"]), 3),
"| humana:", round(accuracy_score(sin.loc[~auto, "causa"], dec_sin.loc[~auto, "causa_propuesta"]), 3))automatica n= 649 (64.7%) acierto de la causa propuesta 0.858
revision_humana n= 354 (35.3%) acierto de la causa propuesta 0.517
acciones lanzadas: {'reenvio_desde': 290, 'abrir_traza': 223, 'aviso_compras': 104, 'reclamacion_transportista': 67}
I7010 | real: error_picking → revision_humana error_picking 0.808 ['reenvio_desde:zaragoza']
R0: valor 349 € > 300 € → siempre revisa una persona
R4: error de picking → reenvío inmediato y aviso al almacén
umbral 0.5: automática 76.5%, acierto 0.821
umbral 0.6: automática 64.7%, acierto 0.858
umbral 0.7: automática 53.8%, acierto 0.906
umbral 0.8: automática 45.8%, acierto 0.919
Sin etiqueta: {'automatica': 697, 'revision_humana': 300} | acierto real (oculto) automática: 0.852 | humana: 0.52El criterio de éxito de la sección 1 se cumple en el prototipo: 64,7 % automatizado con 85,8 % de acierto en esa cola (y 85,2 % en las 997 incidencias sin etiqueta, cuya causa real conocemos solo porque los datos son sintéticos: es la comprobación de que la validación cruzada no mentía). La cola humana concentra los casos difíciles (51,7 %): el agente recibe la propuesta, la confianza y la traza, exactamente lo que 06-04 pedía. El umbral es un mando de negocio: 0,7 sube el acierto automático al 90,6 % automatizando el 54 %; Diego decide según la capacidad del equipo. La incidencia I7010 muestra la interacción de reglas: el modelo está seguro (0,81) pero R0 la manda a revisión por valor, y aun así R4 lanza el reenvío, porque reenviar es reversible y barato. En minutos de agente (2 frente a 8): 1.003 incidencias costaban 8.024 min a mano; ahora 649 × 2 + 354 × 8 = 4.130, casi la mitad, sin contar la segunda incidencia por diagnóstico erróneo (14 % de 649 ≈ 92 casos), que hay que restar y vigilar.
- Model card y chequeo de deriva
Recordatorio (08-01, secciones 11.3 y 11.5). La model card documenta uso previsto y no previsto, datos, métricas y limitaciones a partir de lo que el código calcula; el chequeo de deriva compara cada lote con una referencia guardada al entrenar (PSI por tramos, tasas, fracción de cada cola).
Tu tarea. Genera model_card.json y referencia_deriva.json desde el código; escribe chequear_deriva(lote, referencia) con PSI de dias_desde_envio, diferencias de tasa de síntomas, fracción automática y distribución de causas propuestas; pruébalo con un mes normal y con un mes en que el daño en transporte se dispara.
import joblib
card = {"nombre": "Diagnóstico de causa de incidencias NovaMarket (caso 9)", "version": "0.1.0-prototipo", "fecha": "2026-08-18",
"modelo": "Regresión logística multiclase: almacén, transportista, 4 síntomas, días desde envío, valor del pedido",
"datos": {"incidencias": int(len(inc)), "etiquetadas": int(len(etiq)), "clases": CLASES},
"metricas_cv5": {"exactitud": 0.738, "f1_macro": 0.658, "linea_base_red_bayesiana": 0.719, "linea_base_mayoritaria": 0.314},
"colas": {"umbral_automatica": UMBRAL_AUTO, "valor_alto_revision": VALOR_ALTO, "fraccion_automatica": 0.647, "acierto_automatica": 0.858},
"uso_previsto": "Proponer causa y acciones al agente; decidir automáticamente solo en la cola automática y con acciones reversibles",
"uso_no_previsto": "Imputar responsabilidad a un transportista o empleado sin revisión humana; usar la causa para penalizar",
"limitaciones": ["fallo_proveedor se detecta mal (recall 0,22)", "solo 1.003 etiquetas, registradas en la mitad de los casos",
"datos sintéticos coherentes con las CPT de 06-03: validar con datos reales antes del piloto"]}
referencia = {"tasa_sintomas": etiq[SINTOMAS].mean().round(3).to_dict(), "frac_automatica": 0.647,
"dist_causa_propuesta": etiq2["causa_propuesta"].value_counts(normalize=True).round(3).to_dict(),
"bordes_dias": [0, 2, 4, 6, 8, 10, 14, None], "frec_dias": None}
PSI_AVISO, PSI_ALERTA, DIF_TASA = 0.10, 0.25, 0.08
def psi(ref, nuevo, eps=1e-4):
r, n = np.clip(np.asarray(ref, float), eps, None), np.clip(np.asarray(nuevo, float), eps, None)
return float(np.sum((n - r) * np.log(n / r)))
def frecuencias(valores, bordes):
b = [-np.inf if x is None else x for x in bordes]; b[-1] = np.inf
c, _ = np.histogram(valores, bins=b); return (c / c.sum()).tolist()
referencia["frec_dias"] = frecuencias(etiq["dias_desde_envio"], referencia["bordes_dias"])
json.dump(card, open("model_card_incidencias.json", "w"), indent=2, ensure_ascii=False)
json.dump(referencia, open("referencia_deriva_incidencias.json", "w"), indent=2); joblib.dump(modelo, "modelo_incidencias.joblib")
def chequear_deriva(lote, referencia, modelo):
P = modelo.predict_proba(lote[COLS]); dec = pd.DataFrame([decidir(f, P[i]) for i, (_, f) in enumerate(lote.iterrows())])
avisos, v = [], psi(referencia["frec_dias"], frecuencias(lote["dias_desde_envio"], referencia["bordes_dias"]))
if v > PSI_AVISO: avisos.append(f"PSI días = {v:.3f}")
for s, t in referencia["tasa_sintomas"].items():
if abs(lote[s].mean() - t) > DIF_TASA: avisos.append(f"tasa {s}: {lote[s].mean():.3f} frente a {t:.3f}")
fa = (dec["cola"] == "automatica").mean()
if abs(fa - referencia["frac_automatica"]) > DIF_TASA: avisos.append(f"fracción automática {fa:.3f} frente a {referencia['frac_automatica']:.3f}")
dc = dec["causa_propuesta"].value_counts(normalize=True)
for c, t in referencia["dist_causa_propuesta"].items():
if abs(dc.get(c, 0) - t) > DIF_TASA: avisos.append(f"causa propuesta {c}: {dc.get(c, 0):.3f} frente a {t:.3f}")
estado = "ALERTA" if v > PSI_ALERTA or len(avisos) >= 2 else ("AVISO" if avisos else "OK")
return estado, avisos
print("Mes normal:", chequear_deriva(generar_incidencias(400, semilla=7), referencia, modelo))
p_transporte = {"zaragoza": [0.20, 0.55, 0.10, 0.06, 0.09], "getafe": [0.15, 0.45, 0.08, 0.25, 0.07]} # el daño en transporte se dispara
print("Mes con problema de transporte:", chequear_deriva(generar_incidencias(400, semilla=8, p_causa=p_transporte), referencia, modelo))Mes normal: ('OK', [])
Mes con problema de transporte: ('ALERTA', ['tasa paquete_danado: 0.525 frente a 0.259', 'tasa producto_equivocado: 0.150 frente a 0.282', 'tasa no_llega: 0.177 frente a 0.260', 'fracción automática 0.782 frente a 0.647', 'causa propuesta error_picking: 0.233 frente a 0.345', 'causa propuesta dano_transporte: 0.497 frente a 0.240'])El mes normal pasa en verde; el mes con problema de transporte dispara seis avisos coherentes entre sí (más paquetes dañados, más causas de transporte propuestas, más cola automática). Interesante: en ese mes el modelo acierta más (el daño en transporte es la causa más fácil), así que la alerta no significa "reentrenar" sino "algo ha cambiado en el mundo: que logística hable con el transportista". Es la diferencia entre deriva de datos y de concepto de 08-01, y por eso el chequeo lo lee una persona. Calendario mínimo: cada hora, que el lote se ejecutó; cada semana, este chequeo; cada mes, con las causas registradas por los agentes en la cola humana (que ahora se recogen sistemáticamente), exactitud real por cola y reentrenamiento si baja del 80 % en la automática.
- Presentación de resultados a Diego
Recordatorio (08-01, sección 9). Empezar por la decisión y el dinero, una cifra por diapositiva, comparar siempre con la línea base, decir qué no sabe el sistema y qué se pide.
| # | Diapositiva | Contenido (una idea, una cifra) |
|---|---|---|
| 1 | El problema y la propuesta | Cada incidencia la diagnostica un agente a mano (~8 min). Proponemos dos colas: automática (2 min, acciones reversibles) y humana con la causa sugerida y su explicación. |
| 2 | Qué hemos construido | Modelo entrenado con las 1.003 incidencias con causa registrada + reglas de negocio (valor > 300 €, confianza < 0,6, traza con transportista, reenvío, aviso a compras). Sin datos personales. |
| 3 | Resultados frente a la línea base | Acierta el 73,8 % (la red de las tablas de Diego, 71,9 %; "siempre picking", 31,4 %). En la cola automática, 85,8 % de acierto sobre el 64,7 % de las incidencias. Ahorro estimado: de 8.024 a ~4.130 minutos de agente por cada 1.000 incidencias. |
| 4 | Lo que no sabe y los riesgos | Fallo de proveedor se detecta mal (22 %); solo la mitad de las incidencias tienen causa registrada (pedimos que se registre siempre); datos del prototipo sintéticos: hace falta el piloto; ninguna sanción a transportistas o empleados sin revisión. |
| 5 | Decisión que pedimos | Piloto de 4 semanas en Zaragoza en modo sombra, umbral 0,6 (o 0,7 si el equipo prefiere 54 % automatizado con 90,6 % de acierto), chequeo semanal de deriva y revisión mensual con las causas registradas. |
- Rúbrica final y guion para el caso 4
Vuelve a la rúbrica de la sección 0 y puntúate con honestidad; con nuestra versión, la casilla más difícil de justificar es "excelente" en Datos (la coherencia se comprobó, pero las fugas se dieron por supuestas: causa nunca entra en el modelo, y dias_desde_envio se conoce al abrir la incidencia, pero ¿y si en la realidad se calculara al cerrarla?). Ese tipo de pregunta es la que debes hacerte en cada fila.
Guion alternativo, caso 4: clasificar reseñas y priorizar la respuesta de atención al cliente. Misma estructura, otras piezas:
| Paso | Qué hacer |
|---|---|
| 1 Definición | Pregunta: para cada reseña nueva, ¿sentimiento y urgencia (hay que responder hoy)? Decisión: cola "responder hoy" (negativa y urgente: producto roto, no llega, cobro doble), "responder esta semana", "agradecer/automático". Métrica de negocio: reseñas urgentes atendidas en < 24 h; técnica: exactitud del sentimiento y recall de la urgencia. Línea base: bolsa de palabras + logística de 05-05 (0,767) y reglas de palabras clave para la urgencia. |
| 2 Datos | Amplía las plantillas de resenas_nm.py con una etiqueta urgente por plantilla (las negativas de "llegó roto", "nunca llegó", "me cobraron dos veces", "peligroso" son urgentes; "ruidoso" o "caro" no) y genera 400 reseñas con generar_resenas_ampliado de 09-03 devolviendo plantilla; valida por plantilla. |
| 3 Línea base | Reglas de palabras clave para urgencia (lista de 10 términos) + bolsa de palabras para sentimiento; mide ambas por plantilla. |
| 4 Modelo | Dos clasificadores (sentimiento, urgencia) o uno multiclase de 3 colas; compara bolsa de palabras, bigramas y EmbeddingBag de 09-03; F1 de la urgencia por encima de la exactitud. |
| 5 Combinación | Reglas sobre las probabilidades: P(urgente) ≥ 0,7 → "hoy"; 0,4-0,7 → revisión humana; mención de "cobro" o "peligro" → "hoy" siempre (regla dura); respuestas de agradecimiento automáticas solo si P(positiva) ≥ 0,9. |
| 6 Documentación | Model card con la advertencia de que el modelo no entiende negaciones ni ironía; deriva: PSI de la longitud de las reseñas, tasa de negativas, fracción "hoy". |
| 7 Presentación | Reseñas urgentes atendidas en 24 h antes/después; cuántas se escaparían (falsos negativos) y por qué; petición: piloto con revisión humana de la cola "hoy" y, si hay presupuesto, un modelo preentrenado en español (09-03). |
Errores Comunes y Consejos
- Saltarse la definición y empezar por el modelo. Sin la tabla de la sección 1 no sabrías que la métrica que importa es el acierto en la cola automática, no la exactitud global.
- Evaluar la línea base y el modelo en filas distintas o con protocolos distintos; aquí la red no necesita entrenamiento y el modelo sí, pero ambos se miden sobre las mismas 1.003 filas y por pliegue.
- Usar la etiqueta oculta (
causa) para algo que no sea la comprobación final de la cola sin etiqueta: en la realidad no existe. - Reglas que contradicen al modelo sin dejar traza. Cada decisión debe poder explicarse como una lista de reglas disparadas con sus valores; es lo que el agente y el auditor leerán.
- Deriva = reentrenar. El mes con problema de transporte enseña que una alerta puede pedir una acción de negocio, no de datos.
- Documentar al final. La model card se genera desde el código y las cifras salen de las mismas variables que imprimiste; si la escribes a mano al final, se desincroniza.
- Consejo: guarda todo como scripts con
main()y una prueba (assert exactitud > 0.70), como en 07-04, y usa la rúbrica dos veces: al empezar (qué me falta) y al terminar (qué he conseguido).
Conclusión
Con este proyecto has recorrido el método de 08-01 completo sobre un caso nuevo: un documento de definición con la decisión (dos colas), la métrica de negocio (minutos de agente y acierto en la cola automática) y las restricciones; un generador de incidencias.csv coherente con las tablas de 06-03 y con la mitad de las causas sin etiquetar; la red bayesiana como línea base (71,9 %, sin etiquetas, con fallo_proveedor como punto ciego); una logística multiclase que la supera por 1,9 puntos gracias a dias_desde_envio (73,8 %, F1 macro 0,658), con la curva de aprendizaje que muestra cuándo gana el experto y cuándo los datos; las reglas de 06-04 convirtiendo probabilidades en colas (64,7 % automático con 85,8 % de acierto; el umbral como mando de negocio); una model card generada desde el código y un chequeo de deriva que distingue un mes normal de uno con problema de transporte; y cinco diapositivas que terminan pidiendo una decisión. Y tienes el guion para repetirlo con las reseñas.
Aquí termina el módulo 9 y, con él, la parte práctica del curso. Lo que sigue no es más contenido, sino cómo seguir aprendiendo por tu cuenta: el módulo 10 reúne libros y artículos (10-01), cursos y tutoriales (10-02), comunidades y foros donde preguntar y contribuir (10-03) y, en 10-04, itinerarios concretos según hacia dónde quieras ir (ingeniería de datos y ML, deep learning, sistemas basados en conocimiento, gestión de proyectos de IA), con los próximos pasos que Marta y Diego darían en NovaMarket como ejemplo.
Fundamentos de Inteligencia Artificial (IA)
Módulo 1: Introducción a la Inteligencia Artificial
Módulo 2: Principios Básicos de la IA
- Conceptos Fundamentales: Agentes, Entornos y Racionalidad
- Tipos de Inteligencia Artificial
- Los Datos como Materia Prima de la IA
- Ética y Consideraciones en IA
Módulo 3: Algoritmos en IA
- Introducción a los Algoritmos
- Algoritmos de Búsqueda
- Búsqueda con Adversario: Juegos y Minimax
- Algoritmos de Optimización
Módulo 4: Aprendizaje Automático (Machine Learning)
- Conceptos Básicos de Machine Learning
- Tipos de Aprendizaje Automático
- Preparación de Datos y Características
- Algoritmos de Machine Learning
- Evaluación y Validación de Modelos
- Sobreajuste, Regularización y Ajuste de Hiperparámetros
Módulo 5: Redes Neuronales y Deep Learning
- Introducción a las Redes Neuronales
- Arquitectura de Redes Neuronales
- Cómo Aprende una Red: Descenso del Gradiente y Retropropagación
- Deep Learning y sus Aplicaciones
- Transformers, Grandes Modelos de Lenguaje e IA Generativa
Módulo 6: Lógica y Sistemas Expertos
- Lógica en IA
- Sistemas Expertos
- Razonamiento con Incertidumbre: Probabilidad y Redes Bayesianas
- Aplicaciones de Sistemas Expertos
Módulo 7: Herramientas y Lenguajes de Programación en IA
- Lenguajes de Programación para IA
- Python Científico: NumPy, pandas y Matplotlib
- Herramientas y Librerías Populares
- Entornos de Desarrollo
Módulo 8: Proyectos y Casos de Estudio
Módulo 9: Ejercicios y Prácticas
- Ejercicios de Algoritmos
- Prácticas de Machine Learning
- Proyectos de Redes Neuronales
- Proyecto Integrador: de la Idea al Prototipo
