Cerramos la lección anterior con una tabla en la que la regresión logística acertaba el 87,1 % de los pedidos y la línea base "nunca se devuelve" el 83,6 %. Diego lo resumió así: "tres puntos y medio; no me parece que compense montar todo esto". Tenía razón en desconfiar de la cifra, pero por el motivo contrario al que pensaba: la exactitud es una medida tosca que, con clases desequilibradas, esconde lo que de verdad importa. En esta lección aprenderemos a evaluar modelos como profesionales: la matriz de confusión y los cuatro tipos de resultado, cada uno con su coste en euros; precisión, sensibilidad, especificidad y F1; las curvas ROC y precisión-recall y el AUC; la elección del umbral según el coste (recuperando la banda de revisión humana de 02-04); las métricas de regresión (MAE, RMSE, R²) para la previsión de demanda; y las estrategias de validación (hold-out, validación cruzada, estratificada y temporal) que evitan que una única división afortunada nos engañe. Es importante porque la evaluación es lo que convierte un experimento en una decisión de negocio: sin métricas adecuadas y validación honesta no se puede saber si un modelo merece desplegarse.
Contenido
- Por qué la exactitud engaña
- La matriz de confusión y el coste de cada error
- Precisión, sensibilidad, especificidad y F1
- Curva ROC, AUC y curva precisión-recall
- Elegir el umbral según el coste
- Métricas de regresión: MAE, RMSE y R²
- Validación: hold-out, validación cruzada, estratificada y temporal
- Línea base obligatoria y los tres conjuntos: entrenamiento, validación y test
- Comparación correcta de los modelos de 04-04
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Por qué la exactitud engaña
La exactitud (accuracy) es la proporción de aciertos. Es lo que devuelve score en clasificación y lo que hemos usado hasta ahora. Su problema aparece en cuanto las clases están desequilibradas: en NovaMarket se devuelve el 16,4 % de los pedidos, así que un "modelo" que diga siempre "no se devuelve" acierta el 83,6 %. Si la tasa fuera del 8 %, acertaría el 92 %; en detección de fraude con tarjeta, donde el fraude es el 0,1 %, un modelo inútil tendría un 99,9 % de exactitud. La cifra es alta y el modelo no sirve para nada, porque no detecta ni un solo caso de lo que se busca.
La lección de fondo: la exactitud trata igual todos los errores, pero para NovaMarket no es lo mismo no anticipar una devolución que revisar un pedido que iba bien. Para razonar hay que separar los tipos de acierto y de error.
- La matriz de confusión y el coste de cada error
La matriz de confusión cruza lo que predijo el modelo con lo que ocurrió realmente. Para el problema de devoluciones (clase positiva = "devuelto"):
| Predicho: no devuelto | Predicho: devuelto | |
|---|---|---|
| Real: no devuelto | Verdadero negativo (VN): pedido normal tratado como normal | Falso positivo (FP): falsa alarma; se revisa o retiene un pedido correcto |
| Real: devuelto | Falso negativo (FN): devolución no anticipada | Verdadero positivo (VP): devolución detectada a tiempo |
Cada casilla tiene un coste de negocio distinto, y ponerle números es la conversación más importante que Marta y Diego tienen en todo el proyecto:
- Un FN (devolución que no se anticipó) cuesta a NovaMarket unos 30 €: transporte de vuelta, reacondicionamiento, gestión, y el descuento con el que se revende el producto abierto.
- Un FP (falsa alarma) cuesta unos 5 €: el tiempo del equipo de Diego en revisar el pedido y la pequeña fricción con un cliente que no había hecho nada raro (recuerda 02-04: si esas fricciones se concentran en un grupo, el coste es también ético y legal).
- Un VP permite actuar (confirmar el pedido con el cliente, reforzar el embalaje, no enviar hasta verificar) y evita buena parte de esos 30 €; un VN no cuesta nada.
Con scikit-learn, sobre el pipeline de 04-03 y la regresión logística de 04-04:
from novamarket_ml import generar_pedidos_ml, ensuciar_pedidos, preparar_pedidos, crear_preparacion
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
X, y = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(3000, 42), 42))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
log = Pipeline([("prep", crear_preparacion()),
("modelo", LogisticRegression(max_iter=1000))]).fit(Xtr, ytr)
pred = log.predict(Xte) # clases con el umbral por defecto (0,5)
prob = log.predict_proba(Xte)[:, 1] # probabilidad de devolución
matriz = confusion_matrix(yte, pred) # filas: real; columnas: predicho
print(matriz)
vn, fp, fn, vp = matriz.ravel()
print(f"VN={vn} FP={fp} FN={fn} VP={vp}")
def coste_negocio(matriz, coste_fp=5, coste_fn=30):
"""Traduce la matriz de confusión a euros: cada FP y cada FN tienen su coste."""
vn, fp, fn, vp = matriz.ravel()
return fp * coste_fp + fn * coste_fn
print("Coste del modelo:", coste_negocio(matriz), "€")Salida:
Lectura: de los 750 pedidos de test, 123 se devolvieron. El modelo detectó 43 (VP), se le escaparon 80 (FN) y dio 17 falsas alarmas (FP). En euros: 17 × 5 + 80 × 30 = 2.485 €. La misma función con la línea base "siempre no" (VP = 0, FN = 123) da 3.690 €, y con la regla de Diego (importe > 300: 19 VP, 104 FN, 15 FP) da 3.195 €. Ahora la comparación tiene un lenguaje que Diego entiende: el modelo ahorra 1.205 € por cada 750 pedidos respecto a no hacer nada, y 710 € respecto a su regla; con 3.000 pedidos diarios, unos 4.800 € al día frente a la línea base. Y aún no hemos ajustado el umbral (sección 5).
- Precisión, sensibilidad, especificidad y F1
De la matriz de confusión se derivan las métricas estándar. Con nuestros números (VN 610, FP 17, FN 80, VP 43):
| Métrica | Fórmula | Pregunta que responde | Valor |
|---|---|---|---|
| Exactitud (accuracy) | (VP + VN) / total | ¿Qué proporción de pedidos clasifica bien? | 653/750 = 0,871 |
| Precisión (precision) | VP / (VP + FP) | De los que marca como devolución, ¿cuántos lo son? (calidad de la alarma) | 43/60 = 0,717 |
| Sensibilidad / recall / exhaustividad | VP / (VP + FN) | De las devoluciones reales, ¿cuántas detecta? (cobertura) | 43/123 = 0,350 |
| Especificidad | VN / (VN + FP) | De los pedidos normales, ¿cuántos deja en paz? | 610/627 = 0,973 |
| F1 | 2 · precisión · recall / (precisión + recall) | Media armónica de precisión y recall: alta solo si ambas lo son | 0,470 |
Ahora la historia real del modelo es visible: cuando marca un pedido acierta el 72 % de las veces (buena precisión), pero solo detecta el 35 % de las devoluciones (recall bajo). Con la exactitud no había forma de saberlo. Precisión y recall están en tensión: para detectar más devoluciones hay que marcar más pedidos, y entre ellos habrá más falsas alarmas; el F1 resume el compromiso en un número y es la métrica más habitual para comparar clasificadores con clases desequilibradas cuando no se dispone de costes.
classification_report imprime todo esto por clase:
from sklearn.metrics import classification_report
print(classification_report(yte, pred, target_names=["no devuelto", "devuelto"], digits=3)) precision recall f1-score support
no devuelto 0.884 0.973 0.926 627
devuelto 0.717 0.350 0.470 123
accuracy 0.871 750
macro avg 0.800 0.661 0.698 750
weighted avg 0.857 0.871 0.851 750Fíjate en que la clase mayoritaria tiene un F1 de 0,93 y la minoritaria de 0,47: los promedios "weighted" (ponderados por tamaño) esconden lo mal que va la clase que importa; el "macro" (media simple) lo refleja mejor.
- Curva ROC, AUC y curva precisión-recall
Todas las métricas anteriores dependen del umbral con el que convertimos la probabilidad en clase (0,5 por defecto). Las curvas evalúan el modelo para todos los umbrales a la vez:
- La curva ROC dibuja, para cada umbral, la tasa de verdaderos positivos (recall) frente a la tasa de falsos positivos (1 − especificidad). Un modelo aleatorio da la diagonal; uno perfecto sube por la izquierda hasta la esquina superior. El AUC (área bajo la curva) resume la curva en un número entre 0,5 (azar) y 1 (perfecto), y tiene una interpretación intuitiva: es la probabilidad de que el modelo dé más puntuación a una devolución real que a un pedido normal elegidos al azar. No depende del umbral ni del desequilibrio de clases, lo que la hace ideal para comparar modelos.
- La curva precisión-recall dibuja la precisión frente al recall para cada umbral. Es más informativa que la ROC cuando la clase positiva es rara, porque se centra en lo que pasa con esa clase; su resumen es la precisión media (average precision, AP). La línea base de un modelo aleatorio no es 0,5, sino la tasa de positivos (0,164 aquí).
from sklearn.metrics import roc_auc_score, roc_curve, precision_recall_curve, average_precision_score
import numpy as np
print("AUC ROC:", round(roc_auc_score(yte, prob), 3))
print("Precisión media (AP):", round(average_precision_score(yte, prob), 3))
fpr, tpr, umbrales = roc_curve(yte, prob) # puntos de la curva ROC
for u in (0.2, 0.3, 0.5):
i = np.argmin(np.abs(umbrales - u))
print(f"umbral {u}: tasa FP {fpr[i]:.3f} recall {tpr[i]:.3f}")
precision, recall, umbrales_pr = precision_recall_curve(yte, prob)
# Para dibujar: plt.plot(fpr, tpr) y plt.plot(recall, precision) con MatplotlibSalida:
AUC ROC: 0.844 Precisión media (AP): 0.598 umbral 0.2: tasa FP 0.175 recall 0.715 umbral 0.3: tasa FP 0.099 recall 0.545 umbral 0.5: tasa FP 0.030 recall 0.350
Un AUC de 0,84 es un modelo claramente útil (en el 84 % de las parejas devolución/no devolución ordena bien). Los tres puntos muestran el intercambio: bajando el umbral de 0,5 a 0,2, el recall sube del 35 % al 72 % a cambio de molestar al 17,5 % de los pedidos normales. Cuál conviene no lo dice la curva: lo dicen los costes.
- Elegir el umbral según el coste
Con la función coste_negocio podemos recorrer umbrales y elegir el que minimiza los euros:
print("umbral precisión recall FP FN coste € marcados")
for u in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7):
pred_u = (prob >= u).astype(int)
m = confusion_matrix(yte, pred_u)
prec = m[1, 1] / max(m[1, 1] + m[0, 1], 1)
rec = m[1, 1] / (m[1, 1] + m[1, 0])
print(f"{u:5.1f} {prec:8.3f} {rec:6.3f} {m[0,1]:3d} {m[1,0]:3d} {coste_negocio(m):6d} {pred_u.sum():4d}")Salida:
umbral precisión recall FP FN coste € marcados 0.1 0.325 0.854 218 18 1630 323 0.2 0.440 0.715 112 35 1610 200 0.3 0.528 0.545 60 56 1980 127 0.4 0.596 0.455 38 67 2200 94 0.5 0.717 0.350 17 80 2485 60 0.6 0.886 0.252 4 92 2780 35 0.7 0.842 0.130 3 107 3225 19
Con costes 5 €/30 €, el umbral óptimo es 0,2: coste 1.610 € frente a los 2.485 € del umbral por defecto, un 35 % menos. El precio es marcar 200 pedidos de 750 (el 27 %), algo que a Diego le parece excesivo operativamente. Aquí es donde encaja la banda de revisión humana de 02-04: en lugar de una única frontera, tres zonas. Con probabilidad ≥ 0,5 se actúa automáticamente (60 pedidos, 43 devoluciones reales); entre 0,2 y 0,5 se pasa a revisión humana (140 pedidos, de los que 45 se devolvieron: la revisora encuentra casi una devolución de cada tres); por debajo de 0,2 se deja pasar (550 pedidos, 35 devoluciones que se escapan). Si una revisión cuesta 3 € y la revisora acierta, el coste total ronda los 1.555 €, y además el sistema genera datos etiquetados a mano para mejorar el modelo. Fíjate en el orden lógico: primero se mide el modelo con AUC y curvas, después se decide el umbral con costes y capacidad operativa; nunca se ajusta el umbral para "que la exactitud quede bonita".
- Métricas de regresión: MAE, RMSE y R²
Para la previsión de demanda no hay clases: el error es una distancia entre lo previsto y lo real. Tres métricas:
| Métrica | Fórmula | Interpretación | Cuándo |
|---|---|---|---|
| MAE (error absoluto medio) | media de |real − previsto| | Error típico en las unidades del problema (unidades vendidas) | Comunicar a negocio; robusta a valores extremos |
| RMSE (raíz del error cuadrático medio) | √(media de (real − previsto)²) | Como el MAE pero penaliza más los errores grandes; siempre ≥ MAE | Cuando un error grande es mucho peor que varios pequeños |
| R² (coeficiente de determinación) | 1 − (error cuadrático del modelo / error cuadrático de predecir la media) | Fracción de la variabilidad explicada: 1 perfecto, 0 igual que la media, negativo peor que la media | Comparar modelos entre sí; no dice el error en unidades |
Sobre el modelo lineal con estacionalidad de 04-04 (entrenado con las semanas 1-78, evaluado en 79-104):
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.linear_model import LinearRegression
from novamarket_ml import generar_demanda_semanal
d = generar_demanda_semanal(104, 42)
d["sen"] = np.sin(2 * np.pi * d["semana"] / 52); d["cos"] = np.cos(2 * np.pi * d["semana"] / 52)
d["black_friday"] = ((d["semana"] - 1) % 52 == 47).astype(int)
feats = ["semana", "sen", "cos", "black_friday"]
train, test = d[d["semana"] <= 78], d[d["semana"] > 78]
reg = LinearRegression().fit(train[feats], train["unidades"])
prev = reg.predict(test[feats])
print("MAE :", round(mean_absolute_error(test["unidades"], prev), 1))
print("RMSE:", round(np.sqrt(mean_squared_error(test["unidades"], prev)), 1))
print("R² :", round(r2_score(test["unidades"], prev), 3))
base = np.full(len(test), train["unidades"].iloc[-1]) # línea base: repetir la última semana
print("Línea base MAE:", round(mean_absolute_error(test["unidades"], base), 1),
" R²:", round(r2_score(test["unidades"], base), 3))Salida:
El modelo se equivoca en unas 17 unidades por semana de media (sobre ventas de 550-670), el RMSE algo mayor (20) indica que hay alguna semana con error grande (la máxima desviación es de 43 unidades, en la semana 95), y explica el 83 % de la variabilidad. La línea base ingenua ("la semana que viene venderemos lo mismo que esta") tiene un MAE de 39 y un R² negativo: peor que predecir la media del periodo. La recta simple de 04-02 tenía MAE 47 y R² −0,44. Sin línea base, un MAE de 17 no significa nada; con ella, sabemos que el modelo reduce el error a menos de la mitad.
- Validación: hold-out, validación cruzada, estratificada y temporal
Hasta ahora hemos usado una única división entrenamiento/test (hold-out). Es rápida, pero el resultado depende de qué pedidos cayeron en cada lado: repitiendo la división con diez semillas distintas, el AUC de la logística oscila entre 0,80 y 0,86. Con un solo test podríamos haber tenido suerte (o mala suerte) y sacar conclusiones equivocadas al comparar dos modelos que se diferencian en 0,02.
La validación cruzada k-fold resuelve el problema: se divide el conjunto en k trozos (folds), se entrena k veces usando cada trozo una vez como test y los k−1 restantes como entrenamiento, y se promedian las k medidas. Todos los datos se usan para evaluar y para entrenar, y la desviación entre folds indica la incertidumbre. Variantes:
- Estratificada (
StratifiedKFold): cada fold conserva la proporción de clases; imprescindible con clases desequilibradas. - Temporal (
TimeSeriesSplit): para series, los folds respetan el orden: se entrena con el pasado y se evalúa con el bloque siguiente, nunca al revés. - Leave-one-out, grupos (
GroupKFold, para que los pedidos de un mismo cliente no se repartan entre entrenamiento y test): otras variantes para casos concretos.
from sklearn.model_selection import cross_val_score, StratifiedKFold, TimeSeriesSplit
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
pipe = Pipeline([("prep", crear_preparacion()), ("modelo", LogisticRegression(max_iter=1000))])
aucs = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc") # ajusta el pipeline en cada fold
print("AUC por fold:", aucs.round(3), " media:", round(aucs.mean(), 3), "±", round(aucs.std(), 3))
tscv = TimeSeriesSplit(n_splits=4, test_size=13) # bloques de 13 semanas (un trimestre)
for i, (tr_idx, te_idx) in enumerate(tscv.split(d)):
print(f"fold {i}: entrena semanas 1-{d['semana'].iloc[tr_idx[-1]]}, "
f"evalúa {d['semana'].iloc[te_idx[0]]}-{d['semana'].iloc[te_idx[-1]]}")
maes = -cross_val_score(LinearRegression(), d[feats], d["unidades"], cv=tscv,
scoring="neg_mean_absolute_error")
print("MAE por trimestre:", maes.round(1), " media:", round(maes.mean(), 1))Salida:
AUC por fold: [0.842 0.815 0.848 0.821 0.857] media: 0.836 ± 0.016 fold 0: entrena semanas 1-52, evalúa 53-65 fold 1: entrena semanas 1-65, evalúa 66-78 fold 2: entrena semanas 1-78, evalúa 79-91 fold 3: entrena semanas 1-91, evalúa 92-104 MAE por trimestre: [25.2 16.4 13. 20. ] media: 18.6
Explicación:
cross_val_scorerecibe el pipeline completo, y en cada fold vuelve a ajustar la imputación, el escalado y el modelo solo con la parte de entrenamiento de ese fold: la validación cruzada hereda la protección contra fugas de 04-03.scoring="roc_auc"elige la métrica; hay decenas ("f1","recall","neg_mean_absolute_error"...; scikit-learn usa el criterio "más alto es mejor", de ahí el signo negativo en los errores).- El AUC medio de la logística es 0,836 ± 0,016. Cuando comparemos dos modelos, una diferencia menor que esa desviación no es concluyente.
- En la serie, cada fold entrena con más historia y evalúa el trimestre siguiente, exactamente como se usará el modelo en producción. El primer fold, entrenado con un solo año, es el peor (25 unidades): con un año no se puede estimar bien la estacionalidad anual. Si en lugar de
TimeSeriesSplitusáramosKFoldbarajado, el MAE medio bajaría a unas 16,7 unidades, una estimación optimista que en producción no se cumpliría.
- Línea base obligatoria y los tres conjuntos: entrenamiento, validación y test
Dos reglas de disciplina cierran la lección:
Siempre una línea base. Antes de celebrar cualquier métrica, calcula la misma métrica para el modelo más tonto razonable: DummyClassifier(strategy="most_frequent") en clasificación (siempre la clase mayoritaria), la media o el último valor en regresión, y la regla manual vigente (la de Diego). Un modelo se justifica por la distancia a la línea base, en la métrica y en euros.
Tres conjuntos, no dos. Si usamos el test para elegir entre modelos, umbrales e hiperparámetros, dejamos de tener una medida honesta: cada decisión que tomamos mirándolo "filtra" información del test al modelo, y el resultado final será optimista. Por eso el flujo profesional separa tres conjuntos:
flowchart LR
D[Datos históricos] --> T[Entrenamiento<br/>~60-70 %]
D --> V[Validación<br/>~15-20 %]
D --> S[Test<br/>~15-20 %]
T -->|fit| M[Modelos candidatos]
V -->|comparar modelos,<br/>umbrales, hiperparámetros| M
M -->|el elegido| F[Modelo final]
S -->|UNA sola vez:<br/>estimación honesta| F
- Entrenamiento: para ajustar parámetros (
fit). - Validación: para tomar decisiones (qué algoritmo, qué umbral, qué hiperparámetros). En la práctica, la validación cruzada sobre el conjunto de entrenamiento hace este papel sin necesidad de un trozo aparte.
- Test: se toca una sola vez, al final, para estimar el rendimiento del modelo ya elegido. En esta lección hemos usado el test para explorar umbrales con fines didácticos; en un proyecto real esa exploración se haría con validación cruzada. La siguiente lección (04-06) desarrolla el ajuste de hiperparámetros con este esquema.
- Comparación correcta de los modelos de 04-04
Repetimos la comparación de 04-04, esta vez con las métricas adecuadas y el coste de negocio:
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import pandas as pd
modelos = {
"Regresión logística": LogisticRegression(max_iter=1000),
"k vecinos (k=15)": KNeighborsClassifier(n_neighbors=15),
"Árbol (prof. 4)": DecisionTreeClassifier(max_depth=4, random_state=42),
"Bosque aleatorio": RandomForestClassifier(n_estimators=200, min_samples_leaf=5, random_state=42),
"Línea base (siempre no)": DummyClassifier(strategy="most_frequent"),
}
filas = []
for nombre, m in modelos.items():
p = Pipeline([("prep", crear_preparacion()), ("modelo", m)]).fit(Xtr, ytr)
pr, pb = p.predict(Xte), p.predict_proba(Xte)[:, 1]
filas.append([nombre, accuracy_score(yte, pr), precision_score(yte, pr, zero_division=0),
recall_score(yte, pr), f1_score(yte, pr), roc_auc_score(yte, pb),
coste_negocio(confusion_matrix(yte, pr))])
tabla = pd.DataFrame(filas, columns=["modelo", "exactitud", "precisión", "recall", "F1", "AUC", "coste €"])
print(tabla.set_index("modelo").round(3))Salida:
exactitud precisión recall F1 AUC coste € modelo Regresión logística 0.871 0.717 0.350 0.470 0.844 2485 k vecinos (k=15) 0.841 0.667 0.065 0.119 0.737 3470 Árbol (prof. 4) 0.855 0.592 0.366 0.452 0.792 2495 Bosque aleatorio 0.859 0.689 0.252 0.369 0.825 2830 Línea base (siempre no) 0.836 0.000 0.000 0.000 0.500 3690
La tabla cuenta una historia mucho más rica que la exactitud sola:
- k vecinos parecía "solo" 3 puntos peor que la logística en exactitud; en realidad detecta el 6,5 % de las devoluciones (recall 0,065) y su AUC de 0,74 lo deja lejos: es casi la línea base con maquillaje.
- El árbol de profundidad 4 tiene menos exactitud que el bosque, pero mejor recall y F1 y menor coste (2.495 € frente a 2.830 €) con el umbral por defecto. Con la exactitud habríamos elegido el bosque.
- La logística gana en todo (AUC 0,844, coste 2.485 €) y, tras ajustar el umbral (sección 5), baja a 1.610 €: un 56 % menos que la línea base. En validación cruzada, la ventaja de la logística sobre el bosque (0,836 frente a 0,822 de AUC medio) es del orden de la desviación entre folds, así que ambos son candidatos razonables y en 04-06 veremos si el bosque mejora ajustando sus hiperparámetros.
- La línea base tiene AUC 0,5 y coste 3.690 €: es la referencia contra la que se justifica todo lo demás.
Marta ya puede responder a Diego con su lenguaje: "el modelo, con el umbral bien puesto, ahorra unos 2.000 € por cada 750 pedidos respecto a no hacer nada, y con la banda de revisión no os llegan más de 140 pedidos a revisar por cada 750".
Errores Comunes y Consejos
- Reportar solo la exactitud. Con clases desequilibradas es casi siempre engañosa. Muestra la matriz de confusión, precisión, recall, F1 y AUC, y si puedes, euros.
- Confundir precisión y recall. Precisión: de lo que marco, cuánto acierto. Recall: de lo que existe, cuánto encuentro. Cuál importa más depende del coste de FP frente a FN.
- Dejar el umbral en 0,5 por defecto. El umbral es una decisión de negocio; recórrelo con la función de coste y considera una banda de revisión humana.
- Usar el test para decidir. Cada mirada al test para elegir algo lo contamina. Decide con validación cruzada; el test, una vez.
- Barajar series temporales. Usa
TimeSeriesSplit; elKFoldbarajado da un optimismo que en producción se paga. - Comparar modelos por una única división. Diferencias pequeñas pueden ser ruido; usa validación cruzada y mira la desviación entre folds.
- Olvidar la línea base. Un MAE de 17 o un AUC de 0,84 no significan nada sin saber qué hace el modelo tonto y la regla actual.
- Ignorar los grupos. Si un mismo cliente tiene pedidos en entrenamiento y en test, el modelo puede "reconocerlo";
GroupKFoldporid_clientelo evita.
Ejercicios
Ejercicio 1. Diego revisa los costes: tras hablar con logística, una devolución no anticipada cuesta 20 € (no 30) y una falsa alarma 8 € (hay que llamar al cliente). Recalcula la tabla de umbrales con coste_negocio(m, coste_fp=8, coste_fn=20). ¿Cambia el umbral óptimo? ¿En qué dirección y por qué?
Ejercicio 2. Calcula con validación cruzada estratificada de 5 folds el F1 y el recall de la logística y del árbol de profundidad 4 (scoring="f1" y scoring="recall"). ¿Cuál de los dos tiene mejor recall medio? ¿La diferencia supera la desviación entre folds? Comenta si cambiaría tu decisión respecto a la tabla de la sección 9.
Ejercicio 3. En la previsión de demanda, sustituye la línea base "última semana" por la media de las 4 últimas semanas de entrenamiento y por "la misma semana del año anterior" (unidades de la semana −52). Calcula el MAE de cada una en las semanas 79-104. ¿Cuál es mejor? ¿Por qué la del año anterior sale tan mal en esta serie, y qué habría que corregir para que fuera una línea base razonable?
Soluciones
Solución 1. Con FP a 8 € y FN a 20 €, el coste de dejar escapar una devolución baja y el de una falsa alarma sube, así que compensa marcar menos. Los costes quedan: 0,1 → 2.104 €; 0,2 → 1.596 €; 0,3 → 1.600 €; 0,4 → 1.644 €; 0,5 → 1.736 €. El mínimo numérico sigue en 0,2, pero ahora 0,3 está a solo 4 € y 0,4 a 48 € (con los costes originales, 0,3 costaba 370 € más que 0,2): la curva se ha aplanado y desplazado hacia umbrales más altos. En general, cuanto más caro es el FP en relación con el FN, más alto conviene poner el umbral (más precisión, menos recall), y viceversa. Cuando la curva de coste es plana alrededor del óptimo, decide el criterio operativo: 0,3 marca 127 pedidos frente a los 200 de 0,2 por prácticamente el mismo coste, así que Diego elegiría 0,3.
Solución 2. Con StratifiedKFold(5, shuffle=True, random_state=42), la logística obtiene un F1 medio de 0,50 (± 0,04) y un recall medio de 0,39 (± 0,04); el árbol de profundidad 4, F1 0,44 (± 0,07) y recall 0,33 (± 0,07) (los valores pueden variar unas centésimas según la versión). La logística es mejor en media en ambas métricas, pero la diferencia de recall (0,05) es del orden de la desviación entre folds, y el árbol es además mucho más variable de un fold a otro (la inestabilidad de los árboles que comentamos en 04-04). La decisión no cambia respecto a la sección 9: la logística sigue siendo preferible por AUC, F1 y estabilidad, pero la validación cruzada nos obliga a decirlo con precisión: "detecta algo más de devoluciones y de forma más estable; con una sola división esa ventaja podría no verse".
Solución 3. La media de las 4 últimas semanas de entrenamiento (unas 649 unidades) da un MAE de unas 35 unidades, algo mejor que "última semana" (39) porque promedia el ruido semanal. La "misma semana del año anterior" da un MAE de unas 118 unidades: sale muy mal porque la serie tiene una tendencia de 2,5 unidades por semana, es decir, unas 130 unidades más que un año antes, y la línea base ignora ese crecimiento. Para hacerla razonable habría que sumar la tendencia (por ejemplo, el crecimiento medio observado entre los dos años) o expresarla como "año anterior × factor de crecimiento"; así se convierte en una línea base estacional muy difícil de batir, y es la que Marta debería usar para justificar el modelo ante Diego. La lección: la línea base también debe ser sensata; una línea base absurda hace que cualquier modelo parezca bueno.
Conclusión
En esta lección hemos aprendido a evaluar con honestidad. La exactitud engaña con clases desequilibradas (83,6 % sin detectar ninguna devolución); la matriz de confusión separa VP, FP, FN y VN, y la función coste_negocio los traduce a euros (30 € por devolución no anticipada, 5 € por falsa alarma), el idioma de Diego. De ahí salen la precisión, la sensibilidad/recall, la especificidad y el F1, y las curvas ROC (con el AUC, 0,84 para la logística) y precisión-recall, que evalúan todos los umbrales a la vez. Después hemos elegido el umbral por coste (0,2 en lugar de 0,5, un 35 % menos de coste) y lo hemos combinado con la banda de revisión humana de 02-04. Para la demanda hemos usado MAE, RMSE y R² frente a una línea base. Y hemos sustituido la división única por validación cruzada estratificada y temporal (TimeSeriesSplit), hemos exigido una línea base y hemos separado entrenamiento, validación y test, con el test reservado para una única mirada final. La comparación de los modelos de 04-04 con estas herramientas ha cambiado la clasificación: la logística sigue primera, el árbol adelanta al bosque en coste y k vecinos queda desenmascarado.
Queda una pregunta abierta desde 04-04: el bosque aleatorio y el árbol sin límite acertaban mucho más en entrenamiento que en test, y k vecinos con k = 1 acertaba el 100 % en entrenamiento y el 81 % en test. En la última lección del módulo, Sobreajuste, Regularización y Ajuste de Hiperparámetros, pondremos nombre a ese fenómeno, veremos cómo detectarlo con curvas de aprendizaje y validación, qué técnicas lo controlan (regularización, poda, ensamblados) y cómo elegir los hiperparámetros con GridSearchCV sin tocar el test, cerrando el flujo completo que Marta ya domina.
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
