Antes de empezar
Varios ejercicios usan las variables de la teoría: X_ent, X_prueba, y_ent, y_prueba (los tumores), proba (los puntajes de la regresión logística en la prueba), y3_prueba y p3 (el problema de tres clases), además de las funciones de sklearn.metrics que importamos allí. Abre un cuaderno nuevo en Colab, pega y ejecuta en orden los bloques de la ficha Código y después trabaja debajo. Intenta cada ejercicio antes de abrir la respuesta.
Ejercicios
1. Métricas a mano
Un filtro de correo basura se probó con 1000 correos (cifras inventadas para el ejercicio). La clase positiva es «basura». Esta es su matriz de confusión:
| Dice «normal» | Dice «basura» | |
|---|---|---|
| Real: normal | 850 | 10 |
| Real: basura | 40 | 100 |
a) ¿Cuántos son VP, FP, VN y FN?
b) Calcula sin computadora la exactitud, la precisión, la exhaustividad y F1.
c) Explica con una frase, sin usar los nombres de las métricas, qué quiere decir la precisión en este problema, y otra para la exhaustividad.
Respuesta
a) VN = 850 (correos normales que llegan a la bandeja), FP = 10 (correos normales mandados a la basura), FN = 40 (basura que se coló) y VP = 100 (basura atrapada).
b)
- Exactitud = (100 + 850) / 1000 = 0,95
- Precisión = 100 / (100 + 10) = 100 / 110 ≈ 0,909
- Exhaustividad = 100 / (100 + 40) = 100 / 140 ≈ 0,714
- F1 = 2 · 0,909 · 0,714 / (0,909 + 0,714) ≈ 0,8
c) Precisión: de cada 11 correos que el filtro manda a la basura, 10 eran basura de verdad. Exhaustividad: de cada 7 correos basura que llegan, el filtro atrapa 5 y deja pasar 2.
Para comprobarlo con código, armamos dos arreglos que tengan esa matriz:
# 860 correos normales y 140 de basura
y_r = np.array([0] * 860 + [1] * 140)
y_p = np.array([0] * 850 + [1] * 10 + [0] * 40 + [1] * 100)
print(confusion_matrix(y_r, y_p))
print(f"exactitud {accuracy_score(y_r, y_p):.3f}"
f" precisión {precision_score(y_r, y_p):.3f}"
f" exhaustividad {recall_score(y_r, y_p):.3f}"
f" F1 {f1_score(y_r, y_p):.3f}")
[[850 10]
[ 40 100]]
exactitud 0.950 precisión 0.909 exhaustividad 0.714 F1 0.800
2. ¿Qué métrica importa?
Para cada caso, di qué error es más grave (falso positivo o falso negativo) y qué métrica mirarías primero. Todos los casos son imaginarios.
- Un análisis de sangre para detectar temprano una enfermedad grave. Si sale positivo, se repite con una prueba más cara que no se equivoca.
- Un filtro de correo basura.
- Un programa que, entre 5000 casas, elige 20 para que un inspector las revise por posibles fallas eléctricas. El inspector solo tiene tiempo para esas 20.
- Un clasificador de fotos de gatos y perros, con la mitad de cada uno, donde los dos errores dan igual.
- Comparar dos modelos de detección de fraude con tarjetas antes de decidir con qué umbral se va a usar el elegido.
Respuesta
- Es peor un falso negativo: un enfermo que no se detecta pierde el tratamiento temprano, mientras que un falso positivo solo cuesta repetir el análisis. Mira la exhaustividad y baja el umbral.
- Es peor un falso positivo: un correo importante que termina en la basura y nadie ve. Un correo basura que se cuela solo molesta. Mira la precisión.
- Lo que importa es que, de las 20 casas elegidas, la mayor cantidad tenga fallas de verdad: es la precisión entre los 20 primeros. Que se escapen otras casas es inevitable con un solo inspector.
- Clases equilibradas y errores del mismo costo: la exactitud sirve bien.
- Todavía no hay umbral: el AUC compara qué tan bien ordena cada modelo. Como el fraude es muy raro, conviene mirar también la curva precisión–exhaustividad.
3. La matriz de confusión para varias clases
Escribe una función matriz_confusion(y_real, y_pred, k) que devuelva la matriz de confusión de un problema con k clases (numeradas de 0 a k − 1) usando solo NumPy. Pruébala con y3_prueba y p3 del problema de tres clases de la teoría y compárala con la de scikit-learn.
Pista: empieza con una matriz de ceros de k × k y suma 1 en la fila de la clase real y la columna de la clase predicha de cada caso.
Respuesta
def matriz_confusion(y_real, y_pred, k):
m = np.zeros((k, k), dtype=int)
for r, p in zip(y_real, y_pred):
m[r, p] += 1 # fila = real, columna = predicho
return m
mia = matriz_confusion(y3_prueba, p3, 3)
print(mia)
print("Igual a scikit-learn:",
np.array_equal(mia, confusion_matrix(y3_prueba, p3)))
# La exhaustividad de cada clase: la diagonal entre el total de su fila
print("Exhaustividad por clase:",
np.round(np.diag(mia) / mia.sum(axis=1), 2))
[[338 18 2]
[ 34 30 5]
[ 10 5 8]]
Igual a scikit-learn: True
Exhaustividad por clase: [0.94 0.43 0.35]
La diagonal son los aciertos. La exhaustividad de cada clase se lee por filas (de los casos reales de esa clase, cuántos acertó) y la precisión, por columnas. Sin el bucle, np.add.at(m, (y_real, y_pred), 1) hace lo mismo de una vez.
4. ¿Le gana al tonto?
Un modelo para detectar una falla rara en piezas de una fábrica (2 de cada 100 piezas la tienen; datos inventados) se probó con 1000 piezas y dio esta matriz: VN = 955, FP = 25, FN = 5, VP = 15.
a) ¿Qué exactitud tiene este modelo? ¿Y un modelo tonto que siempre dice «sin falla»?
b) ¿Cuál de los dos es mejor? Justifica con otra métrica.
Respuesta
vn, fp, fn, vp = 955, 25, 5, 15
total = vn + fp + fn + vp
print(f"Exactitud del modelo: {(vp + vn) / total:.3f}")
print(f"Exactitud del tonto: {(vn + fp) / total:.3f}")
print(f"Exhaustividad del modelo: {vp / (vp + fn):.2f}"
f" precisión: {vp / (vp + fp):.3f}")
Exactitud del modelo: 0.970
Exactitud del tonto: 0.980
Exhaustividad del modelo: 0.75 precisión: 0.375
a) El modelo tiene una exactitud de 0,97 y el tonto, de 0,98: el tonto «gana» en exactitud.
b) El tonto no encuentra ninguna pieza fallada (exhaustividad 0). El modelo encuentra 15 de las 20 (exhaustividad 0,75), a cambio de 25 falsas alarmas: de cada 8 piezas que marca, 3 tienen la falla. Si una pieza fallada que sale de la fábrica es cara, el modelo es mucho mejor, aunque su exactitud sea más baja. La exactitud sola habría elegido al modelo inútil.
5. Lo que el AUC no ve
Predice primero, sin correr nada, qué AUC dará cada línea, y después compruébalo:
print(f"Original: {roc_auc_score(y_prueba, proba):.4f}")
print(f"Puntajes / 2: {roc_auc_score(y_prueba, proba / 2):.4f}")
print(f"Puntajes al cubo: {roc_auc_score(y_prueba, proba ** 3):.4f}")
print(f"1 − puntajes: {roc_auc_score(y_prueba, 1 - proba):.4f}")
Respuesta
Original: 0.9917
Puntajes / 2: 0.9917
Puntajes al cubo: 0.9917
1 − puntajes: 0.0083
Dividir entre 2 o elevar al cubo cambia los números, pero no cambia el orden de los casos: el que tenía más puntaje sigue teniendo más. Como el AUC solo mira el orden (la probabilidad de que un positivo tenga más puntaje que un negativo), queda igual. En cambio, la exactitud con umbral 0,5 sí cambiaría, porque con los puntajes divididos entre 2 casi ninguno pasaría de 0,5.
Con 1 − puntajes el orden se invierte y el AUC pasa a 1 − 0,9917 = 0,0083: un modelo que se equivoca casi siempre en el orden. Un AUC muy por debajo de 0,5 no es un modelo inútil, es uno al revés: invirtiendo su respuesta sería excelente. Lo verdaderamente inútil es un AUC cerca de 0,5.
Mini-proyecto: un detector que no deja pasar casi nada
Un hospital quiere usar el modelo de los tumores como primer filtro: los casos que el modelo marque como malignos irán a una revisión más detallada. La condición es que no se escape más del 5 % de los tumores malignos, es decir, una exhaustividad de al menos 0,95. Entre los umbrales que la cumplen, se quiere el que dé menos falsas alarmas.
La trampa es elegir el umbral mirando los datos de prueba: entonces la prueba ya no medirá lo que pasa con casos nuevos (tema 7). Hazlo así:
- Usa solo
X_entyy_ent. Concross_val_predict(..., method="predict_proba")y 5 pliegues, consigue para cada tumor de entrenamiento el puntaje de un modelo que no lo vio. Usamake_pipeline(StandardScaler(), LogisticRegression())para que en cada pliegue el escalado se ajuste solo con su parte de entrenamiento (las pipelines las verás a fondo en el tema 10). - Con
precision_recall_curvesobre esos puntajes, busca el umbral más alto cuya exhaustividad sea de al menos 0,95. ¿Por qué el más alto? Busca también un segundo umbral con un margen de seguridad: el más alto con exhaustividad de al menos 0,98. - Entrena la pipeline con todo el entrenamiento y aplica a
X_prueba, una sola vez, los dos umbrales y el 0,5 de siempre. Para cada uno, muestra los cuatro conteos de la matriz de confusión, la exhaustividad y la precisión. - ¿Con cuál de los tres umbrales se cumple la condición en la prueba? ¿Cuántas falsas alarmas más cuesta?
- ¿Por qué el umbral elegido justo en 0,95 puede fallar en la prueba?
Resultados de referencia, para comprobar los tuyos
from sklearn.model_selection import cross_val_predict
from sklearn.pipeline import make_pipeline
tuberia = make_pipeline(StandardScaler(), LogisticRegression())
# Paso 1: puntajes «fuera de pliegue» del entrenamiento
proba_cv = cross_val_predict(tuberia, X_ent, y_ent, cv=5,
method="predict_proba")[:, 1]
# Paso 2: el umbral más alto que cumple cada objetivo
prec_cv, exh_cv, umb_cv = precision_recall_curve(y_ent, proba_cv)
elegidos = []
for objetivo in [0.95, 0.98]:
cumple = exh_cv[:-1] >= objetivo
u = umb_cv[cumple].max()
i = list(umb_cv).index(u)
elegidos.append(u)
# Métricas medidas en la validación cruzada
print(f"Objetivo {objetivo}: umbral {u:.3f} exhaustividad"
f" {exh_cv[i]:.3f} precisión {prec_cv[i]:.3f}")
# Pasos 3 y 4: una sola vez sobre la prueba
tuberia.fit(X_ent, y_ent)
proba_t = tuberia.predict_proba(X_prueba)[:, 1]
print("En la prueba:")
for u in elegidos + [0.5]:
p = (proba_t >= u).astype(int)
vn, fp, fn, vp = confusion_matrix(y_prueba, p).ravel()
print(f"umbral {u:.3f}: VP={vp} FN={fn} FP={fp} VN={vn}"
f" exhaustividad={recall_score(y_prueba, p):.3f}"
f" precisión={precision_score(y_prueba, p):.3f}")
Objetivo 0.95: umbral 0.565 exhaustividad 0.953 precisión 1.000
Objetivo 0.98: umbral 0.179 exhaustividad 0.986 precisión 0.912
En la prueba:
umbral 0.565: VP=59 FN=5 FP=2 VN=105 exhaustividad=0.922 precisión=0.967
umbral 0.179: VP=61 FN=3 FP=13 VN=94 exhaustividad=0.953 precisión=0.824
umbral 0.500: VP=60 FN=4 FP=4 VN=103 exhaustividad=0.938 precisión=0.938
- Por qué el umbral más alto: todos los umbrales más bajos también cumplen la condición (bajar el umbral nunca baja la exhaustividad), pero dan más falsas alarmas. El más alto que todavía la cumple es el de mayor precisión.
- El umbral justo en 0,95 no alcanza en la prueba: en la validación cruzada llegaba a 0,953, pero en la prueba se escapan 5 de 64 malignos (exhaustividad 0,922, un 7,8 % de malignos perdidos). Ni siquiera el 0,5 cumple: pierde 4.
- Con el margen sí se cumple: el umbral de 0,179 deja escapar 3 de 64 (exhaustividad 0,953, un 4,7 % perdido). El precio son 13 falsas alarmas en lugar de 4: de cada 74 tumores que el modelo manda a revisión, 61 son malignos. Para un primer filtro médico, es un buen negocio.
- Por qué hace falta el margen: las métricas se miden con pocos casos. Con 64 malignos en la prueba, cada tumor que se escapa mueve la exhaustividad en más de 0,015, así que un umbral elegido justo en el límite puede fallar fácilmente con casos nuevos. El margen se decide antes de mirar la prueba; lo que no se debe hacer es volver a elegir el umbral después de ver cómo le fue en ella.
Errores comunes
- Juzgar un modelo solo por su exactitud. Con clases desbalanceadas, un modelo tonto saca una exactitud altísima. Compárate siempre con él y mira la precisión y la exhaustividad de la clase que importa.
- Confundir precisión con exactitud. La exactitud mira todas las predicciones; la precisión, solo las que dijeron «positivo».
- Leer la matriz al revés. En scikit-learn las filas son lo real y las columnas lo predicho, con la clase 0 primero. Otros libros y programas usan otro orden: revisa los ejes.
- No fijarse cuál es la clase positiva. En
load_breast_cancerla clase 1 es «benigno». Si no la inviertes, la «exhaustividad» que calcules será la de los benignos. - Creer que 0,5 es el umbral correcto. Es solo el que usa
predict. El umbral se elige según el costo de cada error. - Elegir el umbral (o el modelo) mirando la prueba. Se elige con validación cruzada o con un conjunto de validación, y la prueba se usa una sola vez al final.
- Calcular el AUC con las predicciones 0 y 1.
roc_auc_scorenecesita los puntajes depredict_proba; con las predicciones finales la curva tiene un solo punto y el AUC sale peor de lo que es. - Dividir sin estratificar con una clase rara. La prueba puede quedar con muy pocos positivos y las métricas cambian mucho de una semilla a otra. Usa
stratify=y. - Fiarse de una curva ROC bonita con clases muy desbalanceadas. La tasa de falsos positivos se divide entre muchos negativos y parece pequeña aunque la precisión sea baja; mira también la curva precisión–exhaustividad.
¿Lo lograste?
Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.
- Construir e interpretar una matriz de confusión (ejercicios 1 y 3).
- Calcular la exactitud, la precisión, la exhaustividad y F1, y elegir la métrica según el costo de cada error (ejercicios 1 y 2).
- Trazar una curva ROC, calcular el AUC y elegir un umbral (ejercicio 5 y el mini-proyecto).
- Detectar el problema de la exactitud con clases desbalanceadas (ejercicio 4).