Autor/a

Diego Villalba

En el capítulo introductorio medimos a los clasificadores con un solo número, la exactitud. Basta un ejemplo para ver que no alcanza: si el 97 % de las transacciones son legítimas, el clasificador que nunca detecta fraude acierta el 97 % de las veces y es completamente inútil. Evaluar bien exige separar los tipos de error, reconocer que tienen costos distintos y distinguir entre la calidad del puntaje que produce un modelo y la calidad de la decisión que se toma al compararlo con un umbral (Provost y Fawcett 2013; Fawcett 2006).

Este capítulo construye todas las métricas a partir de un único ejemplo de veinte pacientes, calculado a mano, y después las generaliza. El siguiente capítulo usa estas métricas para responder otra pregunta: cómo estimarlas de forma honesta y cómo elegir modelos con ellas.

53.1 Intuición: una prueba de detección

Un modelo asigna a cada paciente un puntaje entre 0 y 1: la probabilidad estimada de tener una enfermedad. De veinte pacientes, cinco están enfermos (la clase positiva) y quince sanos (la negativa); es decir, la prevalencia es \(5/20 = 25\%\).

Puntajes
Enfermos (5) 0.95, 0.85, 0.70, 0.55, 0.30
Sanos (15) 0.80, 0.60, 0.45, 0.40, 0.35, 0.25, 0.20, 0.18, 0.15, 0.12, 0.10, 0.08, 0.05, 0.04, 0.02
Tabla 53.1: Puntajes del modelo para los veinte pacientes.

El modelo es bueno pero no perfecto: casi todos los enfermos tienen puntajes altos, pero un sano tiene 0.80 y un enfermo apenas 0.30. Para decidir hay que fijar un umbral \(\theta\): se declara «enfermo» a quien tenga puntaje \(\ge \theta\). Con el umbral usual \(\theta = 0.5\), ¿qué tan bueno es el resultado? La respuesta depende de qué errores nos importen.

53.2 Construcción paso a paso

53.2.1 Paso 1. La matriz de confusión

Con \(\theta = 0.5\) se declaran enfermos los puntajes 0.95, 0.85, 0.70 y 0.55 (cuatro enfermos reales) y 0.80 y 0.60 (dos sanos). El enfermo con 0.30 queda sin detectar. La matriz de confusión cruza la clase real con la predicha:

Predicho enfermo Predicho sano Total
Real enfermo VP = 4 FN = 1 5
Real sano FP = 2 VN = 13 15
Total 6 14 20
Tabla 53.2: Matriz de confusión con θ = 0.5.
  • Verdaderos positivos (VP): enfermos detectados.
  • Falsos negativos (FN): enfermos no detectados, el error de tipo II.
  • Falsos positivos (FP): sanos señalados como enfermos, el error de tipo I o falsa alarma.
  • Verdaderos negativos (VN): sanos correctamente descartados.

Todas las métricas de esta sección son cocientes entre celdas de esta tabla. La clave para no confundirlas es preguntarse qué fila o qué columna se usa como denominador.

53.2.2 Paso 2. Métricas por fila: ¿qué le pasó a cada clase real?

Dividiendo entre el total de cada fila (la realidad):

\[ \text{Sensibilidad} = \frac{VP}{VP + FN} = \frac{4}{5} = 0.80, \qquad \text{Especificidad} = \frac{VN}{VN + FP} = \frac{13}{15} \approx 0.867. \]

La sensibilidad (recall, exhaustividad o tasa de verdaderos positivos, TPR) responde: de los enfermos, ¿cuántos detecta la prueba? La especificidad (tasa de verdaderos negativos) responde: de los sanos, ¿cuántos deja en paz? Sus complementos son la tasa de falsos negativos, \(1 - 0.80 = 0.20\), y la tasa de falsos positivos (FPR), \(2/15 \approx 0.133\).

53.2.3 Paso 3. Métricas por columna: ¿cuánto vale cada predicción?

Dividiendo entre el total de cada columna (lo que dijo el modelo):

\[ \text{Precisión} = \frac{VP}{VP + FP} = \frac{4}{6} \approx 0.667, \qquad \text{VPN} = \frac{VN}{VN + FN} = \frac{13}{14} \approx 0.929. \]

La precisión (valor predictivo positivo, VPP) responde: si la prueba dice «enfermo», ¿qué tan probable es que lo esté? El valor predictivo negativo (VPN) responde lo mismo para el «sano». Son las métricas que le importan a quien recibe el resultado; las de fila, a quien diseña la prueba.

Pregunta Denominador Métrica Valor
De los enfermos, ¿cuántos detecto? fila positiva Sensibilidad (recall, TPR) 0.800
De los sanos, ¿cuántos descarto? fila negativa Especificidad (TNR) 0.867
Si digo «enfermo», ¿acierto? columna positiva Precisión (VPP) 0.667
Si digo «sano», ¿acierto? columna negativa VPN 0.929
Tabla 53.3: Las cuatro métricas básicas y su denominador.

53.2.4 Paso 4. Métricas globales y la paradoja de la exactitud

La exactitud usa toda la tabla: \((VP + VN)/n = 17/20 = 0.85\). Ahora comparemos con el clasificador trivial que declara sanos a todos: no tiene VP ni FP, acierta los 15 sanos y su exactitud es \(15/20 = 0.75\). Solo diez puntos por debajo de nuestro modelo… sin detectar a un solo enfermo. Con una prevalencia del 1 %, el clasificador trivial tendría 0.99 de exactitud. Esta es la paradoja de la exactitud: con clases desbalanceadas, la exactitud premia ignorar a la clase minoritaria (Saito y Rehmsmeier 2015).

Tres métricas globales resisten mejor el desbalance:

  • Exactitud balanceada: el promedio de sensibilidad y especificidad, \((0.80 + 0.867)/2 \approx 0.833\). El clasificador trivial obtiene \((0 + 1)/2 = 0.5\), como el azar.
  • F1: la media armónica1 de precisión y sensibilidad, \[F_1 = \frac{2 \cdot 0.667 \cdot 0.80}{0.667 + 0.80} = \frac{2\,VP}{2\,VP + FP + FN} = \frac{8}{11} \approx 0.727.\] Ignora los VN, así que solo tiene sentido cuando la clase positiva es la de interés. El trivial obtiene 0.
  • Coeficiente de correlación de Matthews (MCC) (Matthews 1975), la correlación entre la clase real y la predicha, que usa las cuatro celdas: \[\text{MCC} = \frac{VP \cdot VN - FP \cdot FN}{\sqrt{(VP+FP)(VP+FN)(VN+FP)(VN+FN)}} = \frac{4 \cdot 13 - 2 \cdot 1}{\sqrt{6 \cdot 5 \cdot 15 \cdot 14}} = \frac{50}{79.4} \approx 0.630.\] Vale 1 para un clasificador perfecto, 0 para uno que no aprende nada (incluido el trivial) y \(-1\) para uno que se equivoca siempre. Es simétrico respecto a qué clase se llama positiva y suele recomendarse como resumen único en problemas binarios desbalanceados (Chicco y Jurman 2020).

1 La media armónica de \(a\) y \(b\) es \(2ab/(a + b)\). Siempre es menor o igual que la media aritmética y se acerca al menor de los dos valores: con precisión 0.99 y sensibilidad 0.01, la media aritmética es 0.5 pero la armónica es 0.02. Así, F1 solo es alto si ambas métricas lo son.

53.2.5 Paso 5. Mover el umbral

Nada obliga a usar \(\theta = 0.5\). Bajar el umbral detecta más enfermos a costa de más falsas alarmas; subirlo, lo contrario:

Umbral \(\theta\) VP FP FN Sensibilidad (TPR) FPR Precisión
0.90 1 0 4 0.20 0.000 1.000
0.75 2 1 3 0.40 0.067 0.667
0.50 4 2 1 0.80 0.133 0.667
0.30 5 5 0 1.00 0.333 0.500
0.10 5 11 0 1.00 0.733 0.313
Tabla 53.4: Métricas para distintos umbrales.

Ningún umbral es «el correcto» en abstracto: la sensibilidad y la precisión se mueven en direcciones opuestas. Elegir el umbral es una decisión de negocio, no un detalle técnico.

53.2.6 Paso 6. La curva ROC y el AUC

Si recorremos todos los umbrales posibles, de mayor a menor, y graficamos cada par (FPR, TPR), obtenemos la curva ROC (receiver operating characteristic). Con veinte pacientes es una escalera: cada vez que el umbral pasa por un enfermo, la curva sube \(1/5\); cada vez que pasa por un sano, avanza \(1/15\) a la derecha. Recorriendo los puntajes de mayor a menor (E = enfermo, S = sano):

\[ \underset{0.95}{E}\;\underset{0.85}{E}\;\underset{0.80}{S}\;\underset{0.70}{E}\;\underset{0.60}{S}\;\underset{0.55}{E}\;\underset{0.45}{S}\;\underset{0.40}{S}\;\underset{0.35}{S}\;\underset{0.30}{E}\;\underset{0.25}{S}\;\cdots\;\underset{0.02}{S} \]

El área bajo la curva (AUC) resume la curva en un número. Tiene una interpretación muy útil: es la probabilidad de que un enfermo elegido al azar tenga un puntaje mayor que un sano elegido al azar (Bamber 1975). Con 5 enfermos y 15 sanos hay \(5 \times 15 = 75\) parejas. Contamos, para cada enfermo, cuántos sanos tienen un puntaje menor:

Enfermo 0.95 0.85 0.70 0.55 0.30 Total
Sanos con puntaje menor 15 15 14 13 10 67
Tabla 53.5: Conteo de parejas bien ordenadas.

\[ \text{AUC} = \frac{67}{75} \approx 0.893. \]

Un AUC de 0.5 es el azar (la diagonal); 1 es una separación perfecta. Lo importante es que el AUC no depende del umbral: mide qué tan bien ordena el modelo a los pacientes, no qué tan buena es una decisión concreta.

53.2.7 Paso 7. La curva Precision–Recall

La curva ROC usa tasas por fila, que no dependen de la prevalencia. Cuando los positivos son pocos, conviene mirar también la curva Precision–Recall (PR), que grafica la precisión contra la sensibilidad. Su resumen habitual es la precisión promedio (AP): el promedio de la precisión en los umbrales donde aparece un nuevo enfermo, ponderado por el aumento en sensibilidad (\(1/5\) cada vez):

Nuevo enfermo en 0.95 0.85 0.70 0.55 0.30
Sensibilidad 0.2 0.4 0.6 0.8 1.0
Precisión 1/1 = 1.000 2/2 = 1.000 3/4 = 0.750 4/6 = 0.667 5/10 = 0.500
Tabla 53.6: Puntos de la curva Precision–Recall donde aumenta la sensibilidad.

\[ \text{AP} = 0.2\,(1 + 1 + 0.75 + 0.667 + 0.5) \approx 0.783. \]

La referencia de la curva PR no es 0.5 sino la prevalencia: un clasificador al azar tiene precisión \(5/20 = 0.25\) en todo el rango. Por eso un AP de 0.78 debe compararse con 0.25.

53.2.8 Paso 8. El umbral según los costos

Supongamos que dejar sin diagnóstico a un enfermo cuesta diez veces más que una falsa alarma: \(C_{FN} = 10\) y \(C_{FP} = 1\). El costo total de cada umbral de la Tabla 53.4 es \(10 \cdot FN + 1 \cdot FP\):

\(\theta\) 0.90 0.75 0.50 0.30 0.10
Costo 40 + 0 = 40 30 + 1 = 31 10 + 2 = 12 0 + 5 = 5 0 + 11 = 11
Tabla 53.7: Costo total según el umbral con C_FN = 10 y C_FP = 1.

El mejor umbral es 0.30, no 0.50. La teoría de la decisión da una regla general: si el puntaje es una probabilidad bien calibrada2, conviene declarar positivo cuando el costo esperado de hacerlo es menor que el de no hacerlo,

2 Un puntaje está calibrado si, entre todos los casos con puntaje 0.3, alrededor del 30 % son positivos. La regresión logística suele estar razonablemente calibrada; los bosques aleatorios, Naive Bayes y las máquinas de soporte vectorial, no. El capítulo siguiente muestra cómo diagnosticarlo y corregirlo.

\[ (1 - p)\,C_{FP} < p\,C_{FN} \iff p > \theta^{*} = \frac{C_{FP}}{C_{FP} + C_{FN}} = \frac{1}{11} \approx 0.09. \tag{53.1}\]

En nuestro ejemplo los puntajes no están calibrados y con solo veinte pacientes la regla teórica (0.09) y el mejor umbral observado (0.30) no coinciden; con muchos datos y probabilidades calibradas, sí lo hacen. En cualquier caso el mensaje es el mismo: cuando un error cuesta más que el otro, el umbral debe moverse hacia donde ese error se vuelve raro.

53.3 Formalización

53.3.1 Matriz de confusión y métricas binarias

Sea \(n_+ = VP + FN\) el número de positivos, \(n_- = VN + FP\) el de negativos y \(\pi = n_+/n\) la prevalencia. Las métricas anteriores se resumen así:

Métrica Fórmula Rango Depende de \(\pi\)
Exactitud \((VP + VN)/n\) [0, 1] sí
Sensibilidad, recall, TPR \(VP/(VP + FN)\) [0, 1] no
Especificidad, TNR \(VN/(VN + FP)\) [0, 1] no
FPR \(FP/(VN + FP) = 1 - \text{especificidad}\) [0, 1] no
Precisión, VPP \(VP/(VP + FP)\) [0, 1] sí
VPN \(VN/(VN + FN)\) [0, 1] sí
Exactitud balanceada \((\text{TPR} + \text{TNR})/2\) [0, 1] no
\(F_\beta\) \((1+\beta^2)\dfrac{\text{prec}\cdot\text{rec}}{\beta^2\,\text{prec} + \text{rec}}\) [0, 1] sí
MCC \(\dfrac{VP\,VN - FP\,FN}{\sqrt{(VP+FP)(VP+FN)(VN+FP)(VN+FN)}}\) [−1, 1] débilmente
Tabla 53.8: Métricas derivadas de la matriz de confusión binaria.

En \(F_\beta\), el parámetro \(\beta\) declara cuánto más importa la sensibilidad que la precisión: \(F_2\) la pondera el doble (detección médica), \(F_{0.5}\) la mitad (filtros donde las falsas alarmas son caras) y \(F_1\) las trata por igual.

53.3.2 Por qué la precisión depende de la prevalencia

La sensibilidad y la especificidad describen a la prueba; la precisión describe a la prueba en una población. Por el teorema de Bayes3,

3 El teorema de Bayes permite «invertir» una probabilidad condicional: \(P(\text{enfermo} \mid +) = \frac{P(+ \mid \text{enfermo})\,P(\text{enfermo})}{P(+)}\). Aquí \(P(+ \mid \text{enfermo})\) es la sensibilidad, \(P(\text{enfermo}) = \pi\), y el denominador suma las dos maneras de obtener un positivo: ser enfermo y detectado, o ser sano y recibir una falsa alarma.

\[ \text{VPP} = \frac{\text{TPR}\cdot\pi}{\text{TPR}\cdot\pi + \text{FPR}\cdot(1 - \pi)}. \tag{53.2}\]

Con la sensibilidad (0.80) y la FPR (0.133) de nuestro ejemplo, la precisión es 0.667 cuando \(\pi = 0.25\), pero baja a \(\frac{0.8 \cdot 0.01}{0.8 \cdot 0.01 + 0.133 \cdot 0.99} \approx 0.057\) cuando \(\pi = 0.01\): en una población con 1 % de enfermos, diecisiete de cada dieciocho alarmas serían falsas, con la misma prueba. Por eso las métricas de columna no se pueden trasladar de una población a otra, y por eso el laboratorio permite mover la prevalencia.

53.3.3 Curvas ROC y PR

Sea \(s(\mathbf{x})\) el puntaje y \(\theta\) el umbral. Al variar \(\theta\), la curva ROC recorre los puntos \((\text{FPR}(\theta), \text{TPR}(\theta))\) desde \((0, 0)\) hasta \((1, 1)\), y la curva PR recorre \((\text{TPR}(\theta), \text{VPP}(\theta))\). El AUC tiene la forma

\[ \text{AUC} = P\big(s(\mathbf{x}^{+}) > s(\mathbf{x}^{-})\big) + \tfrac{1}{2}P\big(s(\mathbf{x}^{+}) = s(\mathbf{x}^{-})\big), \tag{53.3}\]

donde \(\mathbf{x}^{+}\) y \(\mathbf{x}^{-}\) son un positivo y un negativo elegidos al azar; es el estadístico de Mann–Whitney normalizado. Dos propiedades prácticas:

  • La curva ROC y el AUC no cambian si se transforma el puntaje con una función creciente: solo importa el orden. Por eso dos modelos con el mismo AUC pueden tener probabilidades muy distintas.
  • La curva ROC no cambia con la prevalencia, pero la curva PR sí. Con clases muy desbalanceadas, un AUC alto puede convivir con una precisión pésima en el umbral de operación; la curva PR lo revela (Saito y Rehmsmeier 2015).

53.3.4 Más de dos clases

Con \(K\) clases la matriz de confusión es \(K \times K\). Para cada clase \(c\) se calculan precisión, sensibilidad y F1 tratándola como positiva y a las demás como negativas («una contra el resto»). Después se promedian de tres maneras:

  • Macro: promedio simple sobre clases; todas pesan igual, también las raras.
  • Ponderado (weighted): promedio ponderado por el número de casos de cada clase.
  • Micro: se suman VP, FP y FN de todas las clases y se calcula una sola métrica; en clasificación multiclase de una etiqueta, el F1 micro coincide con la exactitud.

Un ejemplo con tres clases y 100 casos:

Predicho A Predicho B Predicho C Sensibilidad
Real A (50) 40 5 5 0.800
Real B (30) 10 15 5 0.500
Real C (20) 2 3 15 0.750
Precisión 40/52 = 0.769 15/23 = 0.652 15/25 = 0.600
Tabla 53.9: Matriz de confusión con tres clases.

Los F1 por clase son 0.784, 0.566 y 0.667. El F1 macro es su promedio, \(0.672\); el ponderado, \(0.5 \cdot 0.784 + 0.3 \cdot 0.566 + 0.2 \cdot 0.667 = 0.695\); el micro es igual a la exactitud, \(70/100 = 0.700\). El macro es el más bajo porque da a la clase B, la peor, el mismo peso que a las demás: es el promedio adecuado cuando todas las clases importan por igual.

53.3.5 Métricas para probabilidades

Las métricas anteriores evalúan decisiones o rankings. Cuando el producto del modelo es la probabilidad misma (un riesgo de impago que se usará para fijar una tasa, por ejemplo), hay que evaluar la probabilidad:

  • Pérdida logarítmica (log-loss, entropía cruzada): \(\;-\frac{1}{n}\sum_i \big[y_i \ln \hat{p}_i + (1 - y_i)\ln(1 - \hat{p}_i)\big]\). Castiga sin piedad la confianza equivocada: predecir 0.99 para un sano cuesta \(-\ln 0.01 \approx 4.6\), cuarenta veces más que predecir 0.9 para un enfermo (\(-\ln 0.9 \approx 0.105\)).
  • Puntaje de Brier (Brier 1950): \(\frac{1}{n}\sum_i (\hat{p}_i - y_i)^2\), el error cuadrático de la probabilidad. Está acotado entre 0 y 1 y es más indulgente con los errores extremos.

Para los veinte pacientes, la log-loss es 0.387 y el Brier 0.124. Ambas son reglas de puntuación propias: se minimizan, en promedio, reportando la probabilidad verdadera, así que premian a los modelos calibrados. Su complemento gráfico, el diagrama de calibración, se estudia en el capítulo siguiente.

53.4 Laboratorio: umbral, prevalencia y curvas

El laboratorio tiene dos modos. Veinte pacientes usa los puntajes de la Tabla 53.1; el estado inicial (\(\theta = 0.5\)) reproduce la Tabla 53.2 y las métricas de los pasos 2 a 4. Modelo binormal supone que el puntaje latente de los sanos sigue una normal estándar y el de los enfermos una normal con media \(d\); ese puntaje se convierte en una probabilidad calibrada según la prevalencia, y todo se calcula para una población de 1000 personas. Ahí se pueden mover la separación entre clases, la prevalencia y el cociente de costos.

Laboratorio · Métricas de clasificación

¿Dónde pongo el umbral?

Mueve el umbral y observa cómo cambian la matriz de confusión, las métricas y el punto sobre las curvas ROC y Precision–Recall.

Datos
Umbral

Puntajes por clase

enfermos (positivos)sanos (negativos)umbral

Matriz de confusión

Curva ROC

curva ROCazarumbral actual

Curva Precision–Recall

curva PRprevalencia (azar)umbral actual

Guía del laboratorio.

  1. Con Veinte pacientes y \(\theta = 0.50\), comprueba la Tabla 53.2 y las métricas de los pasos 2 a 4. ¿Dónde está el punto naranja sobre la curva ROC?
  2. Predice antes de mover: si bajas el umbral a 0.30, ¿qué celda de la matriz se vacía? ¿Qué le pasa a la precisión? Compruébalo con la Tabla 53.4.
  3. Recorre el umbral de 0.99 a 0.01 y observa cómo el punto recorre la escalera de la curva ROC. ¿Por qué el AUC no cambia?
  4. Cambia a Modelo binormal. Con \(d = 1.5\) y \(\theta = 0.5\), baja la prevalencia de 0.10 a 0.01. ¿Qué curva cambia y cuál no? ¿Qué le pasa a la precisión en el umbral actual?
  5. Con prevalencia 0.01, compara la exactitud del modelo con la del clasificador trivial. ¿Qué métrica te avisa del problema?
  6. Pulsa Ir al umbral de mínimo costo con cocientes de costo 1, 10 y 50. Mueve el umbral un poco a cada lado y verifica que el costo sube.
  7. Pon \(d = 0\). ¿Qué forma tienen las curvas? ¿Qué AUC obtienes y por qué?

53.5 Cómo elegir la métrica

La métrica se elige antes de entrenar, a partir del problema y no de los resultados:

Situación Métrica principal Complemento
Clases balanceadas y errores con costo similar Exactitud Matriz de confusión
Positivo raro y lo importante es detectarlo (cribado médico, fraude) Sensibilidad a una precisión mínima, \(F_2\) Curva PR, AP
Las falsas alarmas son caras (bloquear clientes, spam) Precisión a una sensibilidad mínima, \(F_{0.5}\) Curva PR
Desbalance y se quiere un resumen único y simétrico MCC o exactitud balanceada Matriz de confusión
Comparar la capacidad de ordenar, sin fijar umbral AUC-ROC AP si el positivo es raro
Costos conocidos Costo esperado en el umbral \(\theta^{*}\) Curva de costo vs. umbral
La probabilidad es el producto (riesgo, tarifas) Log-loss o Brier Diagrama de calibración
Multiclase con clases igualmente importantes F1 macro Matriz \(K \times K\) y F1 por clase
Tabla 53.10: Guía para elegir métricas.

Errores frecuentes:

  • Reportar solo la exactitud con clases desbalanceadas.
  • Elegir el umbral mirando el conjunto de prueba. El umbral es un hiperparámetro: se elige con validación, como se explica en el capítulo siguiente.
  • Comparar precisiones entre poblaciones con prevalencias distintas (Ecuación 53.2).
  • Confundir buen ranking con buenas probabilidades. Un AUC alto no garantiza que «0.8» signifique 80 %.
  • Promediar sin mirar las clases. Un F1 macro aceptable puede ocultar una clase con F1 cercano a cero; reporta siempre las métricas por clase.

53.6 Implementación en Python

53.6.1 El ejemplo manual con scikit-learn

El módulo sklearn.metrics tiene una función para cada métrica. Reproducimos los números de la sección anterior.

Mostrar código
import numpy as np
import pandas as pd
from sklearn.metrics import (confusion_matrix, accuracy_score, recall_score, precision_score,
                             f1_score, balanced_accuracy_score, matthews_corrcoef, roc_auc_score,
                             average_precision_score, log_loss, brier_score_loss)

puntaje = np.array([0.95, 0.85, 0.70, 0.55, 0.30,                   # enfermos
                    0.80, 0.60, 0.45, 0.40, 0.35, 0.25, 0.20, 0.18,  # sanos
                    0.15, 0.12, 0.10, 0.08, 0.05, 0.04, 0.02])
real = np.array([1] * 5 + [0] * 15)
pred = (puntaje >= 0.5).astype(int)

vn, fp, fn, vp = confusion_matrix(real, pred).ravel()
print(f"VP={vp}  FN={fn}  FP={fp}  VN={vn}")
pd.Series({
    "exactitud": accuracy_score(real, pred),
    "sensibilidad": recall_score(real, pred),
    "especificidad": recall_score(real, pred, pos_label=0),
    "precisión": precision_score(real, pred),
    "VPN": precision_score(real, pred, pos_label=0),
    "F1": f1_score(real, pred),
    "exactitud balanceada": balanced_accuracy_score(real, pred),
    "MCC": matthews_corrcoef(real, pred),
    "AUC": roc_auc_score(real, puntaje),
    "AP": average_precision_score(real, puntaje),
    "log-loss": log_loss(real, puntaje),
    "Brier": brier_score_loss(real, puntaje),
}).round(3)
VP=4  FN=1  FP=2  VN=13
exactitud               0.850
sensibilidad            0.800
especificidad           0.867
precisión               0.667
VPN                     0.929
F1                      0.727
exactitud balanceada    0.833
MCC                     0.630
AUC                     0.893
AP                      0.783
log-loss                0.387
Brier                   0.124
dtype: float64

Observa que la especificidad y el VPN no tienen función propia: son la sensibilidad y la precisión de la clase negativa (pos_label=0). El costo por umbral de la Tabla 53.7:

Mostrar código
umbrales = [0.90, 0.75, 0.50, 0.30, 0.10]
filas = []
for t in umbrales:
    vn_, fp_, fn_, vp_ = confusion_matrix(real, (puntaje >= t).astype(int)).ravel()
    filas.append({"umbral": t, "VP": vp_, "FP": fp_, "FN": fn_, "costo (10·FN + FP)": 10 * fn_ + fp_})
pd.DataFrame(filas)
umbral VP FP FN costo (10·FN + FP)
0 0.90 1 0 4 40
1 0.75 2 1 3 31
2 0.50 4 2 1 12
3 0.30 5 5 0 5
4 0.10 5 11 0 11

53.6.2 Un caso realista: cribado con 8 % de prevalencia

Simulamos un problema de cribado médico con 3000 pacientes y una prevalencia cercana al 8 %, y comparamos una regresión logística con un bosque aleatorio. Separamos tres conjuntos: entrenamiento para ajustar los modelos, validación para elegir el umbral y prueba para la evaluación final.

Mostrar código
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = make_classification(n_samples=3000, n_features=12, n_informative=6, n_redundant=2,
                           weights=[0.92, 0.08], flip_y=0.01, class_sep=1.0, random_state=11)
X_tmp, X_te, y_tmp, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=0)
X_tr, X_va, y_tr, y_va = train_test_split(X_tmp, y_tmp, test_size=0.3, stratify=y_tmp, random_state=0)
print(f"Entrenamiento: {len(y_tr)} (positivos {y_tr.mean():.1%}) · validación: {len(y_va)} · prueba: {len(y_te)}")

modelos = {
    "Regresión logística": make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),
    "Bosque aleatorio": RandomForestClassifier(n_estimators=300, min_samples_leaf=3, random_state=0),
}
proba_va, proba_te = {}, {}
for nombre, m in modelos.items():
    m.fit(X_tr, y_tr)
    proba_va[nombre], proba_te[nombre] = m.predict_proba(X_va)[:, 1], m.predict_proba(X_te)[:, 1]
Entrenamiento: 1470 (positivos 8.5%) · validación: 630 · prueba: 900

Con el umbral por defecto (0.5), ambos modelos tienen exactitud alta y sensibilidad modesta, el síntoma clásico del desbalance:

Mostrar código
from sklearn.metrics import classification_report
for nombre, p in proba_te.items():
    print(f"── {nombre} (θ = 0.5) ──")
    print(classification_report(y_te, (p >= 0.5).astype(int), target_names=["sano", "enfermo"], digits=3))
print(f"Exactitud del clasificador trivial: {1 - y_te.mean():.3f}")
── Regresión logística (θ = 0.5) ──
              precision    recall  f1-score   support

        sano      0.961     0.992     0.976       824
     enfermo      0.860     0.566     0.683        76

    accuracy                          0.956       900
   macro avg      0.911     0.779     0.829       900
weighted avg      0.953     0.956     0.951       900

── Bosque aleatorio (θ = 0.5) ──
              precision    recall  f1-score   support

        sano      0.965     0.999     0.982       824
     enfermo      0.979     0.605     0.748        76

    accuracy                          0.966       900
   macro avg      0.972     0.802     0.865       900
weighted avg      0.966     0.966     0.962       900

Exactitud del clasificador trivial: 0.916
Mostrar código
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from sklearn.metrics import roc_curve, precision_recall_curve

fig = make_subplots(rows=1, cols=2, subplot_titles=("ROC", "Precision–Recall"))
colores = {"Regresión logística": "#2563eb", "Bosque aleatorio": "#16a34a"}
for nombre, p in proba_te.items():
    fpr, tpr, _ = roc_curve(y_te, p)
    pr, rc, _ = precision_recall_curve(y_te, p)
    fig.add_trace(go.Scatter(x=fpr, y=tpr, name=f"{nombre} (AUC {roc_auc_score(y_te, p):.3f})",
                             line=dict(color=colores[nombre])), row=1, col=1)
    fig.add_trace(go.Scatter(x=rc, y=pr, name=f"{nombre} (AP {average_precision_score(y_te, p):.3f})",
                             line=dict(color=colores[nombre], dash="dot")), row=1, col=2)
fig.add_trace(go.Scatter(x=[0, 1], y=[0, 1], line=dict(color="gray", dash="dash"), showlegend=False), row=1, col=1)
fig.add_trace(go.Scatter(x=[0, 1], y=[y_te.mean()] * 2, line=dict(color="gray", dash="dash"), showlegend=False), row=1, col=2)
fig.update_xaxes(title_text="FPR", row=1, col=1); fig.update_yaxes(title_text="TPR", row=1, col=1)
fig.update_xaxes(title_text="recall", row=1, col=2); fig.update_yaxes(title_text="precisión", row=1, col=2)
fig.update_layout(height=400, margin=dict(l=10, r=10, t=40, b=40), legend=dict(orientation="h", y=-0.25))
fig.show()
Figura 53.1: Curvas ROC (izquierda) y Precision–Recall (derecha) en el conjunto de prueba. En ROC ambos modelos superan 0.9 de AUC; la curva PR, cuya referencia es la prevalencia (línea discontinua), muestra cuánto cuesta en precisión recuperar a los últimos enfermos.

53.6.3 Elegir el umbral con validación

Supongamos que un enfermo no detectado cuesta diez veces más que una falsa alarma. Elegimos el umbral que minimiza el costo en validación y lo aplicamos, sin tocarlo, en prueba:

Mostrar código
ths = np.linspace(0.01, 0.9, 90)
nombre = "Regresión logística"
costo_va = [10 * ((proba_va[nombre] < t) & (y_va == 1)).sum() + ((proba_va[nombre] >= t) & (y_va == 0)).sum() for t in ths]
t_opt = ths[int(np.argmin(costo_va))]

fig = go.Figure(go.Scatter(x=ths, y=costo_va, line=dict(color="#7c3aed")))
fig.add_vline(x=t_opt, line=dict(color="#ea580c"), annotation_text=f"mínimo en validación: {t_opt:.2f}")
fig.add_vline(x=1 / 11, line=dict(color="gray", dash="dot"))
fig.add_vline(x=0.5, line=dict(color="gray", dash="dash"), annotation_text="0.5", annotation_position="bottom right")
fig.update_layout(xaxis_title="umbral θ", yaxis_title="costo en validación", height=360, margin=dict(l=10, r=10, t=20, b=40))
fig.show()
Figura 53.2: Costo total en el conjunto de validación (10·FN + FP) según el umbral, para la regresión logística. El mínimo está muy por debajo de 0.5; la línea punteada es el umbral teórico 1/11 para probabilidades calibradas.
Mostrar código
def resumen(p, t):
    pr = (p >= t).astype(int)
    vn_, fp_, fn_, vp_ = confusion_matrix(y_te, pr).ravel()
    return {"umbral": round(t, 2), "sensibilidad": recall_score(y_te, pr), "precisión": precision_score(y_te, pr),
            "MCC": matthews_corrcoef(y_te, pr), "costo en prueba": 10 * fn_ + fp_}
pd.DataFrame([resumen(proba_te[nombre], 0.5), resumen(proba_te[nombre], t_opt)]).round(3)
umbral sensibilidad precisión MCC costo en prueba
0 0.50 0.566 0.860 0.676 337
1 0.13 0.803 0.399 0.511 242

El umbral elegido en validación reduce el costo en prueba alrededor de un 30 % respecto a 0.5, a cambio de una precisión menor: es exactamente el compromiso que se decidió al fijar los costos.

53.6.4 Métricas multiclase

classification_report calcula precisión, sensibilidad y F1 por clase con sus promedios macro y ponderado. Reproducimos la Tabla 53.9:

Mostrar código
M = np.array([[40, 5, 5], [10, 15, 5], [2, 3, 15]])
y_real = np.repeat(np.repeat(["A", "B", "C"], 3), M.ravel())
y_pred = np.repeat(np.tile(["A", "B", "C"], 3), M.ravel())
print(classification_report(y_real, y_pred, digits=3))
print("F1 micro:", round(f1_score(y_real, y_pred, average="micro"), 3))
              precision    recall  f1-score   support

           A      0.769     0.800     0.784        50
           B      0.652     0.500     0.566        30
           C      0.600     0.750     0.667        20

    accuracy                          0.700       100
   macro avg      0.674     0.683     0.672       100
weighted avg      0.700     0.700     0.695       100

F1 micro: 0.7

53.7 Resumen

  • La matriz de confusión es la base de todas las métricas; cada métrica es un cociente con un denominador distinto: por fila (sensibilidad, especificidad) o por columna (precisión, VPN).
  • La exactitud engaña con clases desbalanceadas; la exactitud balanceada, F1 y sobre todo el MCC son resúmenes más robustos.
  • La precisión depende de la prevalencia: la misma prueba produce muchas más falsas alarmas en una población donde el positivo es raro.
  • Las curvas ROC y PR recorren todos los umbrales. El AUC mide la capacidad de ordenar y equivale a la probabilidad de que un positivo tenga mayor puntaje que un negativo; la curva PR revela problemas que la ROC oculta cuando el positivo es raro.
  • El umbral es una decisión: con costos \(C_{FP}\) y \(C_{FN}\) y probabilidades calibradas, el óptimo es \(C_{FP}/(C_{FP} + C_{FN})\). Se elige con validación, nunca con la prueba.
  • Con varias clases, las métricas se promedian (macro, ponderado, micro); cuando la probabilidad es el producto, se evalúa con log-loss o Brier.

Referencias