Evaluación de Clasificadores: Métricas Esenciales y su Interpretación

Autor/a

Diego Villalba

Fecha de publicación

19 de mayo de 2026

Introducción

La evaluación de modelos de clasificación constituye uno de los problemas más estratégicamente complejos en la Minería de Datos. No es suficiente construir un modelo que “funcione”: es necesario cuantificar de manera precisa y contextualmente apropiada qué tan bien funciona, en qué escenarios falla y qué tipo de errores comete, ya que como veremos mas adelante la calidad de un clasificador esta completamente determiada por el objetivo del modelo y el contexto de aplicación (Han, Kamber, y Pei 2011).

Esta distinción tiene consecuencias directas en entornos reales: un modelo de diagnóstico médico que confunde “exactitud estadística” con “utilidad clínica” puede dejar pacientes enfermos sin tratamiento; un sistema de detección de fraude que optimiza la métrica equivocada puede bloquear transacciones legítimas y dañar la relación con el cliente.

De esta manera la literatura especializada ha producido un conjunto robusto de métricas y herramientas de evaluación que van más allá del intuitivo porcentaje de aciertos. Con lo cual comprender estas métricas es competencia central del científico de datos (Provost y Fawcett 2013).

Este capítulo desarrolla sistemáticamente las métricas más importantes para la evaluación de clasificadores binarios y multiclase, integrándolas con el proceso KDD (Knowledge Discovery in Databases) y con la lógica de la toma de decisiones en contextos de alto impacto. Cada sección combina rigor matemático con ejemplos de código ejecutable en Python, siguiendo el principio de que la comprensión profunda de una métrica requiere tanto su derivación formal como su implementación y visualización sobre datos reales.

Errores

En este capitulo definiremos un error como la situación en la que el modelo de clasificación asigna una etiqueta incorrecta a una instancia.

Mientras que definiremos el ratio de error como la proporción de instancias clasificadas incorrectamente sobre el total de instancias evaluadas.

Definición. El ratio de error de un clasificador con \(k\) errores en un conjunto de prueba de \(n\) instancias se define como:

\[ \epsilon = \frac{k}{n} \tag{1}\]

En primera instnacia esta definición puede parecer suficiente para los propositos de cualquier clasificador, ya que en la mayoria de los casos el objetivo e minimizar el numero de errores que comete el modelo. Sin embargo como veremos a lo largo del capitulo, no todos los errores son iguales, ya que dependiendo del contexto una equivocación puede tener diferentes consecuencias que exploraremos a continuación.

Tipo de errores y su costo

La matriz de confusión: la base de toda evaluación de clasificadores

En terminos generales decimos que una una clase es positiva (1) cuando es la clase de interés, y negativa (0) cuando es la clase complementaria. Por ejemplo, en un sistema de detección de fraude, la clase positiva sería “fraude” y la negativa “transacción legítima”. En un sistema de diagnóstico médico, la clase positiva sería “enfermo” y la negativa “sano”.

De esta manera podemos clasificar los errores de un clasificador binario por medio de la matriz de confusión, que es la representación tabular que sintetiza el desempeño de un clasificador binario (Han, Kamber, y Pei 2011). Formalmente se define como:

Definición. Para un clasificador con clases positiva (1) y negativa (0), la matriz de confusión se define como:

\[ M = \begin{pmatrix} TN & FP \\ FN & TP \end{pmatrix} \tag{2}\]

donde la fila representa la clase real y la columna la clase predicha. Los cuatro elementos son:

  • TP (Verdadero Positivo): El modelo predijo positivo y el caso real es positivo. Clasificación correcta de la clase de interés.
  • TN (Verdadero Negativo): El modelo predijo negativo y el caso real es negativo. Clasificación correcta de la clase mayoritaria.
  • FP (Falso Positivo): El modelo predijo positivo pero el caso real es negativo. También llamado Error Tipo I.
  • FN (Falso Negativo): El modelo predijo negativo pero el caso real es positivo. También llamado Error Tipo II.

Visualmente podemos visualizar los tipos de errores en la siguiente figura:

Representación visual de una matriz de confusión y métricas de clasificación binaria. El diagrama ilustra la relación entre la realidad (columnas Positivo/Negativo) y las predicciones del modelo (círculo central). Se identifican los Verdaderos Positivos (TP) y Falsos Positivos (FP) dentro del área de predicción, mientras que los Falsos Negativos (FN) y Verdaderos Negativos (TN) se ubican en el área de exclusión.

Representación visual de una matriz de confusión y métricas de clasificación binaria. El diagrama ilustra la relación entre la realidad (columnas Positivo/Negativo) y las predicciones del modelo (círculo central). Se identifican los Verdaderos Positivos (TP) y Falsos Positivos (FP) dentro del área de predicción, mientras que los Falsos Negativos (FN) y Verdaderos Negativos (TN) se ubican en el área de exclusión.

Como veremos mas adelante, la matriz de confusión es la base de toda evaluación de clasificadores, ya que a partir de ella se derivan todas las métricas de desempeño. Además, la interpretación de cada elemento de la matriz tiene implicaciones estratégicas directas en la toma de decisiones basada en el modelo.

Generación de los Datasets Sintéticos

A lo largo de este capítulo se utilizan dos datasets sintéticos que replican las características estadísticas de problemas reales.

  • El Dataset Médico simula la detección de una condición grave con prevalencia del ~8 %, reflejando el desbalance típico en estudios de screening.

  • El Dataset Financiero simula transacciones con una tasa de fraude del ~3 %, consistente con benchmarks de la industria (Pedregosa et al., 2011).

Ambos datasets se generan con sklearn.datasets.make_classification, que permite controlar el número de características informativas, el nivel de ruido en las etiquetas y el desbalance de clases.

Mostrar código
# ── Configuración global e importaciones ───────────────────────────────────
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.figure_factory as ff
from plotly.subplots import make_subplots
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
    confusion_matrix, accuracy_score, precision_score, recall_score,
    f1_score, fbeta_score, roc_curve, auc, precision_recall_curve,
    average_precision_score, classification_report,
)

SEED = 42

# ── Dataset médico: 92 % sanos, 8 % enfermos ───────────────────────────────
X_med, y_med = make_classification(
    n_samples=1000, n_features=10, n_informative=5,
    weights=[0.92, 0.08],   # fuerte desbalance
    flip_y=0.02,            # 2 % de ruido en las etiquetas
    random_state=SEED,
)
X_tr_m, X_te_m, y_tr_m, y_te_m = train_test_split(
    X_med, y_med, test_size=0.3, stratify=y_med, random_state=SEED
)

# ── Dataset financiero: 97 % legítimas, 3 % fraude ─────────────────────────
X_fin, y_fin = make_classification(
    n_samples=5000, n_features=15, n_informative=7,
    weights=[0.97, 0.03],
    flip_y=0.01,
    random_state=SEED,
)
X_tr_f, X_te_f, y_tr_f, y_te_f = train_test_split(
    X_fin, y_fin, test_size=0.3, stratify=y_fin, random_state=SEED
)

# ── Modelo 1: Regresión Logística — dataset médico ─────────────────────────
modelo_lr = LogisticRegression(random_state=SEED, max_iter=500)
modelo_lr.fit(X_tr_m, y_tr_m)
y_pred_lr = modelo_lr.predict(X_te_m)
y_prob_lr = modelo_lr.predict_proba(X_te_m)[:, 1]

# ── Modelo 2: Random Forest — dataset médico ───────────────────────────────
modelo_rf = RandomForestClassifier(n_estimators=100, random_state=SEED)
modelo_rf.fit(X_tr_m, y_tr_m)
y_pred_rf = modelo_rf.predict(X_te_m)
y_prob_rf = modelo_rf.predict_proba(X_te_m)[:, 1]

# ── Modelo 3: Regresión Logística — dataset financiero ─────────────────────
modelo_fin = LogisticRegression(
    random_state=SEED, max_iter=500, class_weight="balanced"
)
modelo_fin.fit(X_tr_f, y_tr_f)
y_pred_fin = modelo_fin.predict(X_te_f)
y_prob_fin = modelo_fin.predict_proba(X_te_f)[:, 1]

print("Distribución de clases — test médico    :",
      dict(zip(*np.unique(y_te_m, return_counts=True))))
print("Distribución de clases — test financiero:",
      dict(zip(*np.unique(y_te_f, return_counts=True))))
Distribución de clases — test médico    : {np.int64(0): np.int64(275), np.int64(1): np.int64(25)}
Distribución de clases — test financiero: {np.int64(0): np.int64(1449), np.int64(1): np.int64(51)}

Para evaluar a lo largo del capitulo diferentes modelos empleamos tres modelos de clasificación binaria: una regresión logística y un random forest para el dataset médico, y una regresión logística con ajuste de clase para el dataset financiero.

Así como primer ejemplo mostramos la matriz de confusión del modelo de regresión logística sobre el dataset médico:

Mostrar código
# ── Función reutilizable: matriz de confusión interactiva ───────────────────
def plot_confusion_matrix(cm,
titulo=" ",
                          etiquetas=None):
    """
    Visualiza una matriz de confusión 2×2 con Plotly.

    Muestra conteos absolutos y porcentajes por fila
    (tasa de acierto/error por clase real).

    Parámetros
    ----------
    cm        : np.ndarray  — salida de sklearn confusion_matrix
    titulo    : str         — título del gráfico
    etiquetas : list[str]   — nombres de las clases [neg, pos]
    """
    if etiquetas is None:
        etiquetas = ["Negativo (0)", "Positivo (1)"]

    cm_pct = cm.astype(float) / cm.sum(axis=1, keepdims=True) * 100
    texto = [
        [f"{cm[i,j]}<br>({cm_pct[i,j]:.1f} %)" for j in range(2)]
        for i in range(2)
    ]
    # Invertir filas para que TP quede arriba-derecha (convención visual habitual)
    z_plot     = cm[::-1, :]
    texto_plot = texto[::-1]
    y_labels   = etiquetas[::-1]

    fig = ff.create_annotated_heatmap(
        z=z_plot,
        x=etiquetas,
        y=y_labels,
        annotation_text=texto_plot,
        colorscale=[[0, "#f8f9fa"], [0.5, "#4a90d9"], [1, "#e74c3c"]],
        showscale=True,
    )
    fig.update_layout(
        title=titulo,
        xaxis_title="Predicción del modelo",
        yaxis_title="Valor real",
        height=430, width=530,
        font=dict(size=13),
    )
    # Etiquetas de cuadrante
    etq = {(0, 0): ("TN", "gray"),    (0, 1): ("FP", "#e67e22"),
           (1, 0): ("FN", "#e74c3c"), (1, 1): ("TP", "#27ae60")}
    for (ri, ci), (label, color) in etq.items():
        ri_plot = 1 - ri
        fig.add_annotation(
            x=etiquetas[ci], y=y_labels[ri_plot],
            text=f"<b>{label}</b>",
            showarrow=False,
            font=dict(size=11, color=color),
            yshift=-22,
        )
    return fig


# ── Aplicar al clasificador médico ──────────────────────────────────────────
cm_lr = confusion_matrix(y_te_m, y_pred_lr)
TP = int(cm_lr[1, 1]); TN = int(cm_lr[0, 0])
FP = int(cm_lr[0, 1]); FN = int(cm_lr[1, 0])

print("Matriz de confusión — Regresión Logística (médico):")
print(cm_lr)
print(f"\nTP={TP}, TN={TN}, FP={FP}, FN={FN}")

fig_cm = plot_confusion_matrix(
    cm_lr,
    # titulo="Matriz de Confusión — Clasificador Médico (Reg. Logística)",
    etiquetas=["Sano", "Enfermo"],
)
fig_cm.show()
Matriz de confusión — Regresión Logística (médico):
[[273   2]
 [ 19   6]]

TP=6, TN=273, FP=2, FN=19

A partir de esta matriz de confusión se derivan todas las métricas de desempeño que analizaremos a lo largo del capítulo. Además, la interpretación de cada elemento de la matriz tiene implicaciones estratégicas directas en la toma de decisiones basada en el modelo.

El Costo Asimétrico del Error: Contextos Reales

La característica más importante de los errores en clasificación no es su frecuencia absoluta, sino su costo diferencial. En la mayoría de los problemas reales de minería de datos, FP y FN no tienen el mismo costo, y optimizar hacia uno implica sacrificar el otro (James et al., 2021).

Medicina: El Costo Devastador del Falso Negativo

En un sistema de detección temprana de cáncer de pulmón, un Falso Negativo significa que el sistema predice “benigno” para un nódulo que en realidad es canceroso. El paciente no recibe tratamiento. Esta asimetría implica que en medicina diagnóstica se prefieren clasificadores con alto Recall (baja tasa de FN), incluso a expensas de menor Precision. Un Falso Positivo significa que el sistema predice “maligno” para un nódulo benigno: el paciente es referido para biopsia innecesaria —procedimiento invasivo y costoso—, pero comparado con el costo del FN, el FP es el error tolerable.

Finanzas: El Costo Operativo del Falso Positivo

En un sistema de detección de fraude, un Falso Positivo bloquea una transacción legítima: el cliente no puede completar su compra, con el riesgo de perder la relación comercial. Un Falso Negativo deja pasar un fraude real, pero la pérdida suele ser cubierta por seguro o chargeback. Esta asimetría inversa implica que en detección de fraude se prefieren clasificadores con alta Precision, aceptando que algunos fraudes pasen sin detectarse (Provost & Fawcett, 2013).

Mostrar código
# ── Visualización del costo asimétrico por contexto ────────────────────────
contextos = ["Medicina\n(screening)", "Finanzas\n(fraude)"]
costo_fn  = [10, 3]   # escala relativa normalizada
costo_fp  = [2,  8]

fig = go.Figure(data=[
    go.Bar(name="Costo FN (Falso Negativo)", x=contextos, y=costo_fn,
           marker_color="#e74c3c",
           text=["Crítico: paciente\nsin tratamiento",
                 "Moderado: pérdida\ncubierta por seguro"],
           textposition="auto"),
    go.Bar(name="Costo FP (Falso Positivo)", x=contextos, y=costo_fp,
           marker_color="#e67e22",
           text=["Moderado: biopsia\ninnecesaria",
                 "Alto: cliente\nabandonado"],
           textposition="auto"),
])
fig.update_layout(
    barmode="group",
    title=(
        "Costo Asimétrico de los Errores por Contexto<br>"
        "<sup>Escala relativa normalizada — Provost & Fawcett (2013)</sup>"
    ),
    yaxis_title="Severidad relativa del error",
    height=420,
    # legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
fig.show()

Métricas de desempeño: más allá del error rate

Como mencionamos anteriormente, siq ueremos evaluar de manera adecuada un clasificador es necesario considerar no solo el número de errores, sino también su tipo y costo. Para esto se han desarrollado un conjunto de métricas que se derivan de la matriz de confusión y que permiten cuantificar diferentes aspectos del desempeño del modelo. Estas métricas no son mutuamente excluyentes, sino complementarias, y su interpretación debe hacerse en conjunto para obtener una visión completa del modelo. A continuación se presentan las métricas más importantes para la evaluación de clasificadores binarios, con su definición formal, cálculo detallado e interpretación contextualizada.

Accuracy: Definición, Utilidad y Limitaciones

Definición Formal

\[ \text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN} \]

La accuracy mide la proporción de instancias correctamente clasificadas sobre el total (Han et al., 2011).

Cálculo e Interpretación Básica

Mostrar código
# ── Accuracy del modelo real vs. clasificador trivial ──────────────────────
acc_lr      = accuracy_score(y_te_m, y_pred_lr)
y_trivial   = np.zeros_like(y_te_m)          # siempre predice "sano"
acc_trivial = accuracy_score(y_te_m, y_trivial)

print(f"Accuracy — Regresión Logística : {acc_lr:.4f}  ({acc_lr*100:.2f} %)")
print(f"Accuracy — Modelo trivial      : {acc_trivial:.4f}  ({acc_trivial*100:.2f} %)")
print()
print("¿El modelo trivial (siempre 'sano') detecta algún enfermo?")
print(f"  Enfermos detectados: {((y_trivial==1) & (y_te_m==1)).sum()} / {y_te_m.sum()}")
Accuracy — Regresión Logística : 0.9300  (93.00 %)
Accuracy — Modelo trivial      : 0.9167  (91.67 %)

¿El modelo trivial (siempre 'sano') detecta algún enfermo?
  Enfermos detectados: 0 / 25

La Paradoja de la Exactitud (Accuracy Paradox)

La limitación más crítica de la accuracy emerge cuando el dataset presenta desbalance de clases, situación extremadamente común en aplicaciones reales de minería de datos (Saito & Rehmsmeier, 2015). En nuestro dataset financiero con 97 % de transacciones legítimas, un clasificador trivial alcanza ~97 % de accuracy sin detectar ningún fraude:

Mostrar código
# ── Paradoja de la exactitud — dataset financiero ──────────────────────────
y_trivial_fin   = np.zeros_like(y_te_f)
acc_trivial_fin = accuracy_score(y_te_f, y_trivial_fin)
acc_modelo_fin  = accuracy_score(y_te_f, y_pred_fin)
recall_trivial  = recall_score(y_te_f, y_trivial_fin, zero_division=0)
recall_modelo   = recall_score(y_te_f, y_pred_fin)

print("Dataset financiero (97 % legítimas, 3 % fraude)")
print("─" * 52)
print(f"Accuracy — modelo trivial : {acc_trivial_fin:.4f}  ({acc_trivial_fin*100:.1f} %)")
print(f"Accuracy — modelo real    : {acc_modelo_fin:.4f}  ({acc_modelo_fin*100:.1f} %)")
print()
print(f"Recall  — modelo trivial  : {recall_trivial:.4f}  → 0 fraudes detectados")
print(f"Recall  — modelo real     : {recall_modelo:.4f}  → detecta fraudes reales")
print()
print("Conclusión: mayor accuracy ≠ mejor modelo en datos desbalanceados.")
print("(Saito & Rehmsmeier, 2015)")
Dataset financiero (97 % legítimas, 3 % fraude)
────────────────────────────────────────────────────
Accuracy — modelo trivial : 0.9660  (96.6 %)
Accuracy — modelo real    : 0.7820  (78.2 %)

Recall  — modelo trivial  : 0.0000  → 0 fraudes detectados
Recall  — modelo real     : 0.7255  → detecta fraudes reales

Conclusión: mayor accuracy ≠ mejor modelo en datos desbalanceados.
(Saito & Rehmsmeier, 2015)
Mostrar código
# ── Visualización: Accuracy vs. Recall ─────────────────────────────────────
modelos    = ["Modelo Real\n(LR + class_weight)", "Modelo Trivial\n(siempre negativo)"]
accuracies = [acc_modelo_fin, acc_trivial_fin]
recalls    = [recall_modelo, recall_trivial]

fig = go.Figure(data=[
    go.Bar(name="Accuracy", x=modelos, y=accuracies,
           marker_color=["#4a90d9", "#e74c3c"],
           text=[f"{v:.2%}" for v in accuracies], textposition="auto"),
    go.Bar(name="Recall (clase fraude)", x=modelos, y=recalls,
           marker_color=["#27ae60", "#c0392b"],
           text=[f"{v:.2%}" for v in recalls], textposition="auto"),
])
fig.update_layout(
    barmode="group",
    title=(
        "Paradoja de la Exactitud en el Dataset Financiero<br>"
        "<sup>Saito & Rehmsmeier (2015): un modelo con mayor accuracy detecta 0 fraudes</sup>"
    ),
    yaxis=dict(title="Valor", range=[0, 1.1], tickformat="%"),
    height=420,
    # legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
fig.show()

Precision: Confiabilidad de las Predicciones Positivas

Definición Formal

\[ \text{Precision} = \frac{TP}{TP + FP} \]

La Precision mide qué proporción de las instancias predichas como positivas son genuinamente positivas (James et al., 2021).

Cálculo Detallado

Mostrar código
# ── Precision: cálculo manual y con sklearn ─────────────────────────────────
precision_manual = TP / (TP + FP) if (TP + FP) > 0 else 0
precision_sk     = precision_score(y_te_m, y_pred_lr, zero_division=0)

print(f"Cálculo manual  — Precision = {TP} / ({TP}+{FP}) = {precision_manual:.4f}")
print(f"sklearn         — Precision = {precision_sk:.4f}")
print()
print(f"Interpretación: de cada 10 alarmas de 'enfermo',")
print(f"  aproximadamente {precision_sk*10:.1f} corresponden a enfermos reales.")
Cálculo manual  — Precision = 6 / (6+2) = 0.7500
sklearn         — Precision = 0.7500

Interpretación: de cada 10 alarmas de 'enfermo',
  aproximadamente 7.5 corresponden a enfermos reales.

Limitación: Alta Precision ≠ Buen Clasificador

Para entenderlo mejor, imagina que la Precisión es la “puntería” o calidad de lo que el sistema detecta.

Si un sistema es extremadamente tímido y solo lanza una alerta cuando tiene una certeza absoluta, su Precisión será perfecta (1.0): cada vez que hable, tendrá razón. Sin embargo, el costo de esa perfección es el silencio; el sistema ignorará muchísimos casos reales por miedo a equivocarse. En la práctica, un sistema que casi nunca se atreve a alertar no es útil, aunque nunca mienta.

Por el contrario, si el sistema es demasiado “sensible” y alerta por cualquier cosa, su precisión cae. Fawcett (2006) identifica que cuando la precisión es muy baja, ocurre la fatiga de alarmas: el usuario recibe tantos avisos falsos que deja de confiar en el sistema y termina ignorándolo por completo.

Ejemplo: El filtro de correos “Spam”

  • Alta Precisión (Certeza absoluta): Configuras tu correo para que solo marque como Spam los mensajes que digan explícitamente “ESTO ES UNA ESTAFA”. Tendrás una precisión de 1.0 (nunca se equivocará con un correo importante), pero tu bandeja de entrada principal se llenará de basura que el filtro no se atrevió a marcar.
  • Baja Precisión (Fatiga de alarmas): Configuras el filtro para que marque como Spam cualquier correo que tenga un signo de exclamación o una palabra en mayúsculas. El filtro atrapará todo el spam, pero también enviará a la basura facturas importantes y mensajes de tus amigos. Al final, tendrás que revisar la carpeta de Spam todos los días, anulando la utilidad del filtro.
Mostrar código
# ── Modelo "conservador": alta precision, bajo recall ──────────────────────
umbral_alto = 0.90
y_conserv   = (y_prob_lr >= umbral_alto).astype(int)

p_c  = precision_score(y_te_m, y_conserv, zero_division=0)
r_c  = recall_score(y_te_m, y_conserv, zero_division=0)
f1_c = f1_score(y_te_m, y_conserv, zero_division=0)

print(f"Umbral = {umbral_alto}  (modelo muy conservador)")
print(f"  Precision : {p_c:.4f}  (casi nunca se equivoca cuando alerta)")
print(f"  Recall    : {r_c:.4f}  (pero deja de detectar muchos enfermos)")
print(f"  F1-Score  : {f1_c:.4f}")
print(f"  Alertas emitidas: {y_conserv.sum()} de {y_te_m.sum()} enfermos reales")
Umbral = 0.9  (modelo muy conservador)
  Precision : 1.0000  (casi nunca se equivoca cuando alerta)
  Recall    : 0.0400  (pero deja de detectar muchos enfermos)
  F1-Score  : 0.0769
  Alertas emitidas: 1 de 25 enfermos reales

Recall (Sensibilidad): Cobertura de los Positivos Reales

Definición Formal

\[ \text{Recall} = \text{Sensibilidad} = \text{TPR} = \frac{TP}{TP + FN} \]

El Recall mide qué proporción de los casos verdaderamente positivos fue correctamente identificada por el clasificador (Fawcett, 2006).

Cálculo y Relación con la Tasa de Falsos Negativos

Mostrar código
# ── Recall: cálculo y relación con FNR ──────────────────────────────────────
recall_manual = TP / (TP + FN) if (TP + FN) > 0 else 0
recall_sk     = recall_score(y_te_m, y_pred_lr)
fnr           = FN / (TP + FN)   # Tasa de Falsos Negativos = 1 − Recall

print(f"Cálculo manual  — Recall = {TP} / ({TP}+{FN}) = {recall_manual:.4f}")
print(f"sklearn         — Recall = {recall_sk:.4f}")
print(f"FNR (1 − Recall)          = {fnr:.4f}")
print()
print(f"De {y_te_m.sum()} enfermos reales en el conjunto de prueba,")
print(f"  el modelo detecta {TP} y deja sin detectar {FN}.")
Cálculo manual  — Recall = 6 / (6+19) = 0.2400
sklearn         — Recall = 0.2400
FNR (1 − Recall)          = 0.7600

De 25 enfermos reales en el conjunto de prueba,
  el modelo detecta 6 y deja sin detectar 19.

Limitación: Alto Recall ≠ Buen Clasificador

Si la Precisión se enfocaba en la “calidad” de las alertas (no mentir), el Recall se enfoca en la “cantidad” o cobertura: qué tan buena es la red del sistema para atrapar todos los casos reales.

Un sistema con Recall = 1.0 es como una red de pesca sin agujeros: atrapa absolutamente todos los peces. Sin embargo, el problema es que, para lograrlo, esa red suele ser tan pesada o grande que termina arrastrando también basura, piedras y algas. En términos operativos, buscar un Recall perfecto suele obligar al sistema a ser menos selectivo, lo que termina dañando la Precisión.

Ejemplo: El detector de incendios

  • Recall Alto (Sensibilidad total): Imagina un detector de humo tan sensible que se activa incluso con el vapor de una ducha caliente o el humo de un cigarrillo a diez metros. Su Recall es de 1.0 porque es imposible que ocurra un incendio real sin que la alarma suene. El problema es que vivirás evacuando el edificio por errores (baja precisión).
  • Recall Bajo (Filtro exigente): Ahora imagina que el detector solo se activa cuando las llamas tocan directamente el sensor. Su precisión será alta (si suena, es que hay fuego de verdad), pero su Recall será muy bajo: para cuando el sistema avise, el incendio ya habrá consumido media habitación. El sistema fue “preciso” pero llegó demasiado tarde porque no tuvo la capacidad de detectar el problema a tiempo.

Trade-off Precision–Recall: Tabla por Umbral

Como mencionaba Fawcett (2006), ambos extremos son fallas: un sistema con Recall bajo es “ciego” ante la realidad, mientras que uno con Recall alto pero baja precisión genera la ya mencionada fatiga de alarmas. En la práctica, el científico de datos debe encontrar un equilibrio entre ambos, lo que se conoce como el trade-off Precision–Recall. Este equilibrio se controla principalmente a través del umbral de decisión: el punto de corte sobre la probabilidad predicha que determina cuándo el modelo clasifica una instancia como positiva.

La siguiente tabla es la evidencia empírica del trade-off fundamental entre Recall y Precision que toda decisión de umbral debe considerar (Provost & Fawcett, 2013):

Mostrar código
# ── Trade-off P-R según umbral — dataset médico ─────────────────────────────
rows = []
for u in [0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80]:
    y_u  = (y_prob_lr >= u).astype(int)
    tp_u = int(((y_u==1) & (y_te_m==1)).sum())
    fp_u = int(((y_u==1) & (y_te_m==0)).sum())
    fn_u = int(((y_u==0) & (y_te_m==1)).sum())
    tn_u = int(((y_u==0) & (y_te_m==0)).sum())
    rows.append({
        "Umbral θ":   u,
        "TP": tp_u, "FP": fp_u, "FN": fn_u, "TN": tn_u,
        "Recall":    round(tp_u/(tp_u+fn_u), 3) if (tp_u+fn_u)>0 else 0,
        "Precision": round(tp_u/(tp_u+fp_u), 3) if (tp_u+fp_u)>0 else 0,
        "F1":        round(f1_score(y_te_m, y_u, zero_division=0), 3),
    })

df_tradeoff = pd.DataFrame(rows)
print("Trade-off Precision–Recall según el umbral de decisión")
print("Dataset Médico — Regresión Logística")
print("(Provost & Fawcett, 2013)\n")
print(df_tradeoff.to_string(index=False))
Trade-off Precision–Recall según el umbral de decisión
Dataset Médico — Regresión Logística
(Provost & Fawcett, 2013)

 Umbral θ  TP  FP  FN  TN  Recall  Precision    F1
      0.2  11  20  14 255    0.44      0.355 0.393
      0.3  10  12  15 263    0.40      0.455 0.426
      0.4   9   3  16 272    0.36      0.750 0.486
      0.5   6   2  19 273    0.24      0.750 0.364
      0.6   6   0  19 275    0.24      1.000 0.387
      0.7   5   0  20 275    0.20      1.000 0.333
      0.8   3   0  22 275    0.12      1.000 0.214

Visualización del Trade-off

Mostrar código
# ── Curvas Precision y Recall en función del umbral ─────────────────────────
umbrales_range = np.arange(0.05, 0.95, 0.01)
prec_vals, rec_vals = [], []

for u in umbrales_range:
    y_u = (y_prob_lr >= u).astype(int)
    prec_vals.append(precision_score(y_te_m, y_u, zero_division=0))
    rec_vals.append(recall_score(y_te_m, y_u, zero_division=0))

fig = go.Figure()
fig.add_trace(go.Scatter(x=umbrales_range, y=prec_vals, name="Precision",
                          line=dict(color="#4a90d9", width=2.5)))
fig.add_trace(go.Scatter(x=umbrales_range, y=rec_vals, name="Recall",
                          line=dict(color="#e74c3c", width=2.5)))
fig.add_vline(x=0.5, line_dash="dash", line_color="gray",
              annotation_text="θ = 0.5 (default)")
fig.update_layout(
    title=(
        "Trade-off Precision vs. Recall según el Umbral de Decisión<br>"
        "<sup>Al subir θ: ↑ Precision, ↓ Recall — Fawcett (2006)</sup>"
    ),
    xaxis_title="Umbral de Decisión (θ)",
    yaxis_title="Valor de la Métrica",
    yaxis=dict(range=[0, 1.05]),
    height=400,
    # legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
fig.show()

Specificity (Especificidad): Protección de los Negativos

Definición Formal

\[ \text{Specificity} = \text{TNR} = \frac{TN}{TN + FP} \]

La Especificidad mide qué proporción de los casos verdaderamente negativos fue correctamente identificada como negativa por el clasificador (Han et al., 2011).

Cálculo y Relación con FPR

Mostrar código
# ── Specificity: cálculo y relación con la Tasa de Falsos Positivos ─────────
specificity = TN / (TN + FP) if (TN + FP) > 0 else 0
fpr_val     = FP / (TN + FP)   # 1 − Specificity = FPR

print(f"Specificity = {TN} / ({TN}+{FP}) = {specificity:.4f}")
print(f"FPR (1 − Specificity) = {fpr_val:.4f}")
print()
print("Tabla resumen de las tres métricas de relevancia:")
print(f"  Precision   = {precision_score(y_te_m, y_pred_lr):.4f}  "
      f"— fiabilidad al predecir positivo")
print(f"  Recall      = {recall_score(y_te_m, y_pred_lr):.4f}  "
      f"— cobertura de positivos reales")
print(f"  Specificity = {specificity:.4f}  "
      f"— protección de los negativos reales")
Specificity = 273 / (273+2) = 0.9927
FPR (1 − Specificity) = 0.0073

Tabla resumen de las tres métricas de relevancia:
  Precision   = 0.7500  — fiabilidad al predecir positivo
  Recall      = 0.2400  — cobertura de positivos reales
  Specificity = 0.9927  — protección de los negativos reales

Valor Predictivo Negativo (VPN): La fiabilidad del silencio

Mientras que la Precisión se enfoca en la confianza de una alerta (“¡Positivo!”), el Valor Predictivo Negativo (VPN) mide qué tanto podemos confiar en el “silencio” del sistema. Responde a la pregunta crucial: Si el modelo dice que el caso es negativo, ¿qué tan seguro es que realmente lo sea?

Definición Formal

\[ \text{NPV} = \frac{TN}{TN + FN} \]

Un VPN alto permite que el usuario tome una decisión basada en el resultado negativo con total tranquilidad. Si el VPN es bajo, un resultado negativo no es confiable, pues existe un riesgo alto de que se trate de un positivo que el sistema no logró identificar.

Cálculo en Código

Mostrar código
# ── Valor Predictivo Negativo (VPN) ─────────
vpn = TN / (TN + FN) if (TN + FN) > 0 else 0

print(f"VPN = {TN} / ({TN}+{FN}) = {vpn:.4f}")
print("Indica la probabilidad de que un caso sea realmente negativo tras una predicción negativa.")
VPN = 273 / (273+19) = 0.9349
Indica la probabilidad de que un caso sea realmente negativo tras una predicción negativa.

Resumen de las “Cuatro Clásicas” de Confianza

Para que no te confundas, piensa en esto como un espejo:

Realidad del Modelo Nombre de la Métrica Lo que el usuario siente
El modelo dice “¡Positivo!” Precisión (VPP) “¿Será verdad o es una falsa alarma?”
El modelo dice “Negativo” VPN “¿Puedo estar tranquilo o me estaré confiando?”
El dato es Positivo Sensibilidad (Recall) “¿Tendrá el modelo la capacidad de verme?”
El dato es Negativo Especificidad “¿Sabrá el modelo dejarme en paz si no pasa nada?”

La Regla SnNout / SpPin en Medicina

James et al. (2021) introducen la regla mnemotécnica clínica:

  • SnNout (alta Snsensibilidad → resultado Negativo descarta la enfermedad): las pruebas de screening deben tener alta Sensibilidad para no dejar pasar enfermos.
  • SpPin (alta Specificidad → resultado Positivo confirma la enfermedad): las pruebas confirmatorias deben tener alta Especificidad para no confirmar falsos positivos.

F1-Score y sus Variantes Multiclase

La Necesidad de una Métrica Compuesta

El dilema entre Precision y Recall genera la necesidad de una métrica que capture ambas dimensiones en un solo número. El F1-Score resuelve esto mediante la media armónica (Han et al., 2011).

Definición Formal

\[ F_1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}} = \frac{2 \cdot TP}{2 \cdot TP + FP + FN} \]

Por Qué Media Armónica y No Aritmética

La media aritmética de Precision y Recall puede ocultar valores extremos. La media armónica penaliza asimétricamente a los clasificadores que sacrifican completamente una de las dos métricas:

Mostrar código
# ── Demostración: media aritmética vs. armónica ─────────────────────────────
def f1_manual(p, r):
    return 2 * p * r / (p + r) if (p + r) > 0 else 0

def media_aritmetica(p, r):
    return (p + r) / 2

casos = [
    ("Modelo A — balanceado",           0.90, 0.90),
    ("Modelo B — solo Precision",        0.99, 0.01),
    ("Modelo C — solo Recall",           0.01, 0.99),
    ("Modelo D — umbral alto moderado",  0.85, 0.60),
]

print(f"{'Modelo':<35}  {'P':>5}  {'R':>5}  {'Aritmética':>11}  {'F1 (armónica)':>14}")
print("─" * 78)
for nombre, p, r in casos:
    print(f"{nombre:<35}  {p:>5.2f}  {r:>5.2f}  "
          f"{media_aritmetica(p,r):>11.4f}  {f1_manual(p,r):>14.4f}")

print()
print("La media armónica domina hacia el valor más bajo: castiga los extremos.")
print("(Han et al., 2011)")
Modelo                                   P      R   Aritmética   F1 (armónica)
──────────────────────────────────────────────────────────────────────────────
Modelo A — balanceado                 0.90   0.90       0.9000          0.9000
Modelo B — solo Precision             0.99   0.01       0.5000          0.0198
Modelo C — solo Recall                0.01   0.99       0.5000          0.0198
Modelo D — umbral alto moderado       0.85   0.60       0.7250          0.7034

La media armónica domina hacia el valor más bajo: castiga los extremos.
(Han et al., 2011)

La Generalización: F-Beta Score

\[ F_\beta = (1 + \beta^2) \cdot \frac{\text{Precision} \cdot \text{Recall}}{\beta^2 \cdot \text{Precision} + \text{Recall}} \]

La elección de \(\beta\) es una declaración explícita sobre la preferencia relativa entre minimizar FP o FN (James et al., 2021):

Mostrar código
# ── F-beta: el parámetro β como declaración de prioridad ───────────────────
p_lr = precision_score(y_te_m, y_pred_lr, zero_division=0)
r_lr = recall_score(y_te_m, y_pred_lr)

print(f"Modelo LR médico — P = {p_lr:.3f}, R = {r_lr:.3f}\n")
print(f"{'β':>5}  {'Énfasis':<40}  {'F-beta':>8}")
print("─" * 58)
for beta, desc in [
    (0.5, "Precision pesa 2× más  (FP es más costoso)"),
    (1.0, "Precision = Recall     (F1 estándar)"),
    (2.0, "Recall pesa 2× más     (FN es más costoso)"),
    (5.0, "Recall domina          (contexto crítico: FN catastrófico)"),
]:
    fb = fbeta_score(y_te_m, y_pred_lr, beta=beta, zero_division=0)
    print(f"{beta:>5}  {desc:<40}  {fb:>8.4f}")
Modelo LR médico — P = 0.750, R = 0.240

    β  Énfasis                                     F-beta
──────────────────────────────────────────────────────────
  0.5  Precision pesa 2× más  (FP es más costoso)    0.5263
  1.0  Precision = Recall     (F1 estándar)        0.3636
  2.0  Recall pesa 2× más     (FN es más costoso)    0.2778
  5.0  Recall domina          (contexto crítico: FN catastrófico)    0.2464

Variantes para Clasificación Multiclase

\[F1_{\text{macro}} = \frac{1}{C}\sum_{c=1}^{C} F1_c \qquad F1_{\text{micro}} = \frac{2\sum TP_c}{2\sum TP_c + \sum FP_c + \sum FN_c} \qquad F1_{\text{weighted}} = \sum_{c=1}^{C} w_c \cdot F1_c\]

Mostrar código
# ── F1 macro / micro / weighted / binario ───────────────────────────────────
print("Variantes del F1-Score:")
for avg in ["macro", "micro", "weighted", "binary"]:
    val = f1_score(y_te_m, y_pred_lr, average=avg, zero_division=0)
    print(f"  F1 {avg:<10}: {val:.4f}")

print()
print("Reporte completo por clase:")
print(classification_report(
    y_te_m, y_pred_lr,
    target_names=["Sano (0)", "Enfermo (1)"],
))
Variantes del F1-Score:
  F1 macro     : 0.6633
  F1 micro     : 0.9300
  F1 weighted  : 0.9130
  F1 binary    : 0.3636

Reporte completo por clase:
              precision    recall  f1-score   support

    Sano (0)       0.93      0.99      0.96       275
 Enfermo (1)       0.75      0.24      0.36        25

    accuracy                           0.93       300
   macro avg       0.84      0.62      0.66       300
weighted avg       0.92      0.93      0.91       300

Visualización Comparativa de Variantes

Mostrar código
# ── Comparación de variantes del F1 ─────────────────────────────────────────
variantes     = ["macro", "micro", "weighted", "binary\n(clase positiva)"]
valores_f1    = [
    f1_score(y_te_m, y_pred_lr, average="macro"),
    f1_score(y_te_m, y_pred_lr, average="micro"),
    f1_score(y_te_m, y_pred_lr, average="weighted"),
    f1_score(y_te_m, y_pred_lr, average="binary"),
]
descripciones = [
    "Todas las clases\npeso igual",
    "Todas las instancias\npeso igual",
    "Ponderado por\nfrecuencia de clase",
    "Solo clase\npositiva (enfermos)",
]

fig = go.Figure(go.Bar(
    x=variantes, y=valores_f1,
    marker_color=["#4a90d9", "#27ae60", "#e67e22", "#e74c3c"],
    text=[f"{v:.3f}<br><i>{d}</i>" for v, d in zip(valores_f1, descripciones)],
    textposition="auto",
))
fig.update_layout(
    title=(
        "Variantes del F1-Score — ¿Qué mide cada una?<br>"
        "<sup>Han et al. (2011); Pedregosa et al. (2011)</sup>"
    ),
    yaxis=dict(title="F1", range=[0, 1.1]),
    height=420,
    xaxis_title="Tipo de promedio",
)
fig.show()

Curva ROC y el Área Bajo la Curva (AUC)

Motivación

Muchas métricas de clasificación binaria dependen del umbral de decisión \(\theta\). Por ejemplo, un mismo modelo puede tener alta sensibilidad con un umbral bajo, pero también producir más falsos positivos. La curva ROC (Receiver Operating Characteristic) permite analizar este intercambio sin fijar un único umbral, evaluando el comportamiento del clasificador a lo largo de todos los umbrales posibles (Fawcett, 2006).

Definición formal

Sea \(\hat{p}(x)\) el score o probabilidad estimada de que la observación \(x\) pertenezca a la clase positiva. Para cada umbral \(\theta\), el modelo predice:

  • clase positiva si \(\hat{p}(x) \ge \theta\),
  • clase negativa si \(\hat{p}(x) < \theta\).

A partir de ello se definen:

\[ \text{TPR}(\theta) = \frac{TP(\theta)}{TP(\theta) + FN(\theta)}, \qquad \text{FPR}(\theta) = \frac{FP(\theta)}{FP(\theta) + TN(\theta)} = 1 - \text{Specificity}(\theta) \]

donde:

  • \(\text{TPR}(\theta)\) es la tasa de verdaderos positivos,
  • \(\text{FPR}(\theta)\) es la tasa de falsos positivos.

La curva ROC es el conjunto de puntos

\[ \big(\text{FPR}(\theta), \text{TPR}(\theta)\big) \]

obtenidos al variar \(\theta\) desde los valores más estrictos hasta los más flexibles.

Interpretación de sus ejes

1. Tasa de Verdaderos Positivos (TPR)

La TPR coincide con el Recall o la Sensibilidad. Indica qué proporción de los casos realmente positivos es identificada correctamente por el modelo.

  • Nombre común: Sensibilidad o Exhaustividad.
  • En lenguaje sencillo: Si un caso realmente es positivo, ¿qué tan probable es que el modelo lo detecte?

2. Tasa de Falsos Positivos (FPR)

La FPR es el complemento de la Especificidad. Indica qué proporción de los casos realmente negativos es clasificada incorrectamente como positiva.

  • Nombre común: Tasa de falsos positivos o fall-out.
  • En lenguaje sencillo: Si un caso realmente es negativo, ¿qué tan probable es que el modelo genere una falsa alarma?

¿Qué representa la curva ROC?

La curva ROC muestra el compromiso entre:

  • detectar más positivos (aumentar TPR),
  • evitar falsas alarmas (mantener baja FPR).

Un clasificador deseable es aquel cuya curva se acerca a la esquina superior izquierda del plano ROC, pues allí se combina una alta sensibilidad con una baja tasa de falsos positivos.

Como referencia:

  • la diagonal \(\text{TPR} = \text{FPR}\) representa un clasificador sin capacidad discriminativa, equivalente a adivinar al azar;
  • cuanto más por encima de esa diagonal se encuentre la curva, mejor será la capacidad del modelo para separar ambas clases.

Área Bajo la Curva (AUC)

El AUC (Area Under the Curve) resume la curva ROC en un solo número:

\[ \text{AUC} = \int_0^1 \text{TPR}(u)\,du \quad \text{con } u = \text{FPR} \]

En términos prácticos, el AUC mide la capacidad global del modelo para ordenar correctamente a los positivos por encima de los negativos, sin depender de un umbral específico.

Sus valores suelen interpretarse así:

  • \(\text{AUC} \approx 0.5\): discriminación aleatoria,
  • \(\text{AUC} > 0.5\): mejor que el azar,
  • \(\text{AUC} \to 1\): excelente capacidad de discriminación.

Interpretación probabilística del AUC

Bamber (1975) mostró que el AUC tiene una interpretación probabilística muy natural:

\[ \text{AUC} = P\!\left(\hat{p}(x^+) > \hat{p}(x^-)\right) \]

donde \(x^+\) es una instancia positiva elegida al azar y \(x^-\) una negativa, también elegida al azar.

Esto significa que el AUC puede entenderse como la probabilidad de que el modelo asigne un score mayor a un caso positivo que a uno negativo. Por ejemplo, un valor de \(\text{AUC} = 0.85\) indica que, en promedio, existe un 85 % de probabilidad de que el modelo puntúe más alto a un positivo que a un negativo seleccionados aleatoriamente.

Ventajas de la curva ROC y del AUC

  • No dependen de un único umbral de decisión.
  • Permiten comparar clasificadores de forma global.
  • Son especialmente útiles cuando interesa evaluar la capacidad de discriminación del modelo más allá de una configuración específica.

Limitación importante

Aunque la curva ROC y el AUC son muy útiles, no siempre reflejan adecuadamente el desempeño en problemas con clases muy desbalanceadas. En esos casos, puede ser más informativa la curva Precision–Recall, porque penaliza de forma más directa los falsos positivos en contextos donde la clase positiva es rara.

Mostrar código
# ── Curva ROC: Regresión Logística vs. Random Forest ───────────────────────
fpr_lr, tpr_lr, thresholds_roc = roc_curve(y_te_m, y_prob_lr)
fpr_rf, tpr_rf, _              = roc_curve(y_te_m, y_prob_rf)

auc_lr = auc(fpr_lr, tpr_lr)
auc_rf = auc(fpr_rf, tpr_rf)

print(f"AUC — Regresión Logística : {auc_lr:.4f}")
print(f"AUC — Random Forest       : {auc_rf:.4f}")
print()
print(f"Interpretación LR: P(score(enfermo) > score(sano)) = {auc_lr:.1%}")
print(f"(Bamber, 1975; Fawcett, 2006)")
AUC — Regresión Logística : 0.7219
AUC — Random Forest       : 0.9201

Interpretación LR: P(score(enfermo) > score(sano)) = 72.2%
(Bamber, 1975; Fawcett, 2006)

Visualización Interactiva: Curva ROC Comparativa

Mostrar código
# ── Curva ROC interactiva con área sombreada ────────────────────────────────
fig = go.Figure()

fig.add_trace(go.Scatter(
    x=[0, 1], y=[0, 1], mode="lines",
    name="Aleatorio (AUC = 0.50)",
    line=dict(color="#aaa", width=1.5, dash="dash"),
))
fig.add_trace(go.Scatter(
    x=[0, 0, 1], y=[0, 1, 1], mode="lines",
    name="Perfecto (AUC = 1.00)",
    line=dict(color="#27ae60", width=1.5, dash="dot"),
))
fig.add_trace(go.Scatter(
    x=fpr_lr, y=tpr_lr, mode="lines",
    name=f"Reg. Logística (AUC = {auc_lr:.3f})",
    line=dict(color="#4a90d9", width=2.5),
    fill="tozeroy", fillcolor="rgba(74,144,217,0.10)",
))
fig.add_trace(go.Scatter(
    x=fpr_rf, y=tpr_rf, mode="lines",
    name=f"Random Forest (AUC = {auc_rf:.3f})",
    line=dict(color="#e74c3c", width=2.5),
    fill="tozeroy", fillcolor="rgba(231,76,60,0.08)",
))

fig.update_layout(
    title=(
        "Curva ROC — Comparación de Modelos (Dataset Médico)<br>"
        "<sup>Fawcett (2006); Bamber (1975)</sup>"
    ),
    xaxis_title="Tasa de Falsos Positivos (FPR = 1 − Specificity)",
    yaxis_title="Tasa de Verdaderos Positivos (Recall)",
    xaxis=dict(range=[0, 1]), yaxis=dict(range=[0, 1.02]),
    height=460,
    legend=dict(x=0.55, y=0.15),
)
fig.show()

Limitaciones del AUC-ROC en Datos Desbalanceados

Saito y Rehmsmeier (2015) demostraron que en datasets con desbalance severo, el AUC-ROC puede sobreestimar el desempeño del clasificador porque incluye los TN en el denominador de la FPR: cuando los negativos son muy numerosos, incluso muchos FP producen una FPR aparentemente pequeña, haciendo que la curva parezca optimista.

Curva Precision–Recall y Average Precision

Motivación

En muchos problemas de clasificación, especialmente cuando la clase positiva es poco frecuente, no basta con saber cuántos aciertos globales tiene el modelo. En estos contextos suele ser más importante responder dos preguntas:

  • cuando el modelo predice un positivo, ¿qué tan confiable es esa predicción?;
  • de todos los positivos reales, ¿cuántos está logrando recuperar?

La curva Precision–Recall (PR) se construye precisamente para estudiar este compromiso. A diferencia de la curva ROC, aquí los verdaderos negativos (TN) no intervienen de forma explícita. Por ello, esta curva se enfoca en el desempeño del modelo sobre la clase positiva, lo cual resulta particularmente útil en escenarios con clases desbalanceadas (Saito & Rehmsmeier, 2015).

Definición formal

Sea \(\hat{p}(x)\) el score o probabilidad estimada para la clase positiva, y sea \(\theta\) un umbral de decisión. Para cada valor de \(\theta\), se definen:

\[ \text{Precision}(\theta) = \frac{TP(\theta)}{TP(\theta) + FP(\theta)}, \qquad \text{Recall}(\theta) = \frac{TP(\theta)}{TP(\theta) + FN(\theta)} \]

donde:

  • Precision mide la proporción de predicciones positivas que realmente son correctas;
  • Recall mide la proporción de positivos reales que el modelo logra recuperar.

La curva Precision–Recall es el conjunto de puntos

\[ \big(\text{Recall}(\theta), \text{Precision}(\theta)\big) \]

obtenidos al variar el umbral \(\theta\).

Interpretación de sus ejes

1. Precision

La Precision evalúa la pureza de las predicciones positivas. Es alta cuando, entre los casos señalados como positivos por el modelo, predominan los verdaderos positivos.

  • Nombre común: Precisión.
  • En lenguaje sencillo: Si el modelo dice “positivo”, ¿qué tan probable es que tenga razón?

2. Recall

El Recall o Sensibilidad mide la capacidad del modelo para no dejar escapar positivos reales.

  • Nombre común: Sensibilidad o Exhaustividad.
  • En lenguaje sencillo: De todos los positivos reales, ¿cuántos logra detectar el modelo?

¿Qué representa la curva Precision–Recall?

La curva PR muestra el compromiso entre:

  • recuperar más positivos (aumentar Recall),
  • mantener confiables las alertas positivas (conservar alta Precision).

En general, al hacer el umbral más flexible, el modelo detecta más positivos y el Recall aumenta, pero también tiende a cometer más falsos positivos, por lo que la Precision puede disminuir. La curva permite visualizar este intercambio de manera completa.

Un modelo deseable es aquel cuya curva se mantiene lo más arriba posible, pues eso indica que conserva una alta precisión incluso cuando incrementa su cobertura de positivos.

Baseline de la curva PR

A diferencia de la curva ROC, cuya referencia natural es la diagonal aleatoria, en la curva PR la línea de referencia depende de la prevalencia de la clase positiva en el conjunto de datos:

\[ \text{Baseline PR} = P(\text{positivo}) = \frac{TP + FN}{N} \]

Esto significa que, si la clase positiva representa por ejemplo el 10 % de los datos, entonces un clasificador sin capacidad real de discriminación tenderá a tener una Precision cercana a 0.10.

Por ello, en problemas desbalanceados, una curva PR y un valor de AP por encima de la prevalencia indican que el modelo está haciendo algo mejor que una asignación aleatoria proporcional.

Average Precision (AP)

El Average Precision (AP) resume la curva Precision–Recall en un solo número. Puede entenderse como un promedio ponderado de la precisión a lo largo de distintos niveles de recall:

\[ \text{AP} = \sum_k (R_k - R_{k-1})\, P_k \]

donde \(R_k\) representa el Recall en el punto \(k\) y \(P_k\) la Precision correspondiente.

En términos prácticos, el AP asigna mayor valor a los modelos que no solo recuperan positivos, sino que lo hacen manteniendo una buena precisión a medida que aumenta la cobertura.

Interpretación del AP

  • Un valor de AP cercano a 1 indica que el modelo logra identificar positivos con alta precisión a lo largo de casi todo el rango de recall.
  • Un valor de AP cercano a la prevalencia sugiere un comportamiento cercano al azar bajo esta métrica.
  • Cuanto mayor sea el AP respecto a la prevalencia, mejor será la capacidad del modelo para priorizar correctamente la clase positiva.

¿Por qué es útil en clases desbalanceadas?

Cuando la clase positiva es rara, puede ocurrir que un modelo tenga una curva ROC aparentemente buena y, sin embargo, produzca demasiados falsos positivos para que sus predicciones sean útiles en la práctica. La curva PR evita que los TN dominen la evaluación y se concentra en lo que muchas veces realmente importa: la capacidad de encontrar positivos sin generar demasiadas falsas alarmas.

Por esta razón, en aplicaciones médicas, detección de fraude, mantenimiento predictivo o búsqueda de eventos raros, la curva PR y el AP suelen ser más informativos que la curva ROC.

Mostrar código
# ── Curva Precision-Recall: LR vs. RF ──────────────────────────────────────
prec_lr_c, rec_lr_c, _ = precision_recall_curve(y_te_m, y_prob_lr)
prec_rf_c, rec_rf_c, _ = precision_recall_curve(y_te_m, y_prob_rf)

ap_lr = average_precision_score(y_te_m, y_prob_lr)
ap_rf = average_precision_score(y_te_m, y_prob_rf)
prevalencia = y_te_m.mean()

print(f"Average Precision — Reg. Logística: {ap_lr:.4f}")
print(f"Average Precision — Random Forest  : {ap_rf:.4f}")
print(f"Baseline (prevalencia clase +)     : {prevalencia:.4f}")
print()
print("Un AP mayor que la prevalencia indica que el modelo supera")
print("una predicción aleatoria proporcional. (Saito & Rehmsmeier, 2015)")
Average Precision — Reg. Logística: 0.4637
Average Precision — Random Forest  : 0.7544
Baseline (prevalencia clase +)     : 0.0833

Un AP mayor que la prevalencia indica que el modelo supera
una predicción aleatoria proporcional. (Saito & Rehmsmeier, 2015)

Visualización Interactiva: Curva PR Comparativa

Mostrar código
# ── Curva Precision-Recall interactiva ─────────────────────────────────────
fig = go.Figure()

fig.add_hline(
    y=prevalencia, line_dash="dash", line_color="#e74c3c",
    annotation_text=f"Baseline aleatoria (prevalencia = {prevalencia:.2f})",
    annotation_position="top right",
)
fig.add_trace(go.Scatter(
    x=rec_lr_c, y=prec_lr_c, mode="lines",
    name=f"Reg. Logística (AP = {ap_lr:.3f})",
    line=dict(color="#4a90d9", width=2.5),
    fill="tozeroy", fillcolor="rgba(74,144,217,0.12)",
))
fig.add_trace(go.Scatter(
    x=rec_rf_c, y=prec_rf_c, mode="lines",
    name=f"Random Forest (AP = {ap_rf:.3f})",
    line=dict(color="#e67e22", width=2.5),
    fill="tozeroy", fillcolor="rgba(230,126,34,0.10)",
))

fig.update_layout(
    title=(
        "Curva Precision-Recall — Comparación de Modelos (Dataset Médico)<br>"
        "<sup>Preferible a ROC en clases desbalanceadas — Saito & Rehmsmeier (2015)</sup>"
    ),
    xaxis_title="Recall",
    yaxis_title="Precision",
    xaxis=dict(range=[0, 1]), yaxis=dict(range=[0, 1.05]),
    height=460,
    legend=dict(x=0.45, y=0.95),
)
fig.show()

Panel Comparativo ROC vs. Precision-Recall

Mostrar código
# ── Panel 1×2: ROC vs. PR para el mismo modelo ─────────────────────────────
fig = make_subplots(
    rows=1, cols=2,
    subplot_titles=[
        f"Curva ROC (AUC = {auc_lr:.3f})",
        f"Curva Precision-Recall (AP = {ap_lr:.3f})",
    ],
)
# ROC
fig.add_trace(go.Scatter(x=[0,1], y=[0,1], mode="lines",
    line=dict(color="#aaa", dash="dash"), showlegend=False),
    row=1, col=1)
fig.add_trace(go.Scatter(x=fpr_lr, y=tpr_lr, mode="lines",
    line=dict(color="#4a90d9", width=2.2), name="LR (ROC)", showlegend=True),
    row=1, col=1)
# PR
fig.add_hline(y=prevalencia, line_dash="dash", line_color="#e74c3c",
              row=1, col=2)
fig.add_trace(go.Scatter(x=rec_lr_c, y=prec_lr_c, mode="lines",
    line=dict(color="#4a90d9", width=2.2), name="LR (PR)", showlegend=True),
    row=1, col=2)

fig.update_xaxes(range=[0, 1], row=1, col=1, title_text="FPR")
fig.update_yaxes(range=[0, 1.02], row=1, col=1, title_text="TPR (Recall)")
fig.update_xaxes(range=[0, 1], row=1, col=2, title_text="Recall")
fig.update_yaxes(range=[0, 1.05], row=1, col=2, title_text="Precision")
fig.update_layout(
    height=500,
    title=(
        "ROC vs. Precision-Recall: el mismo modelo, perspectivas distintas<br>"
        "<sup>Saito & Rehmsmeier (2015)</sup>"
    ),
)
fig.show()

Optimización del Umbral de Decisión

El Umbral como Parámetro Estratégico

La mayoría de los clasificadores probabilísticos producen \(\hat{p}(x) \in [0, 1]\) antes de asignar una clase. El umbral por defecto \(\theta = 0.5\) es una convención, no una elección óptima (Provost & Fawcett, 2013). Si se conocen los costos de cada tipo de error, el umbral óptimo puede derivarse analíticamente (James et al., 2021):

\[\theta^* = \frac{C_{FP}}{C_{FP} + C_{FN}}\]

  • Medicina con \(C_{FN} = 100 \cdot C_{FP}\): \(\theta^* = 1/101 \approx 0.01\) (casi cualquier señal positiva debe investigarse)
  • Finanzas con \(C_{FP} = 10 \cdot C_{FN}\): \(\theta^* = 10/11 \approx 0.91\) (se requiere certeza alta antes de bloquear)

Efecto del Umbral sobre Todas las Métricas

Mostrar código
# ── Efecto del umbral sobre accuracy, precision, recall y F1 ───────────────
umbrales_demo  = np.arange(0.05, 0.95, 0.02)
metricas_umbral = []

for u in umbrales_demo:
    y_u  = (y_prob_lr >= u).astype(int)
    tp_u = int(((y_u==1) & (y_te_m==1)).sum())
    tn_u = int(((y_u==0) & (y_te_m==0)).sum())
    fp_u = int(((y_u==1) & (y_te_m==0)).sum())
    fn_u = int(((y_u==0) & (y_te_m==1)).sum())
    n    = tp_u + tn_u + fp_u + fn_u
    metricas_umbral.append({
        "umbral":    u,
        "accuracy":  (tp_u + tn_u) / n if n > 0 else 0,
        "precision": tp_u / (tp_u + fp_u) if (tp_u + fp_u) > 0 else 0,
        "recall":    tp_u / (tp_u + fn_u) if (tp_u + fn_u) > 0 else 0,
        "f1":        2*tp_u / (2*tp_u+fp_u+fn_u) if (2*tp_u+fp_u+fn_u)>0 else 0,
    })

df_umb = pd.DataFrame(metricas_umbral)

fig = go.Figure()
for col, color, name in [
    ("accuracy",  "#4a90d9", "Accuracy"),
    ("precision", "#27ae60", "Precision"),
    ("recall",    "#e74c3c", "Recall"),
    ("f1",        "#e67e22", "F1-Score"),
]:
    fig.add_trace(go.Scatter(
        x=df_umb["umbral"], y=df_umb[col],
        name=name, line=dict(color=color, width=2.2), mode="lines",
    ))

idx_best = df_umb["f1"].idxmax()
best_u   = df_umb.loc[idx_best, "umbral"]
fig.add_vline(x=0.5, line_dash="dash", line_color="gray",
              annotation_text="θ=0.5 (default)")
fig.add_vline(x=best_u, line_dash="dot", line_color="#e67e22",
              annotation_text=f"θ*_F1 = {best_u:.2f}")

fig.update_layout(
    title=(
        "Efecto del Umbral de Decisión sobre las Métricas<br>"
        "<sup>Provost & Fawcett (2013)</sup>"
    ),
    xaxis_title="Umbral de Decisión (θ)",
    yaxis_title="Valor",
    yaxis=dict(range=[0, 1.05]),
    height=430,
    legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
fig.show()

Búsqueda del Umbral Óptimo según el Criterio

Mostrar código
# ── Tres estrategias de selección del umbral ───────────────────────────────
print("Estrategia 1: Maximizar F1-Score")
f1_arr = [f1_score(y_te_m, (y_prob_lr >= u).astype(int), zero_division=0)
          for u in np.arange(0.01, 0.99, 0.01)]
u_best_f1  = np.arange(0.01, 0.99, 0.01)[np.argmax(f1_arr)]
y_best_f1  = (y_prob_lr >= u_best_f1).astype(int)
print(f"  θ* = {u_best_f1:.2f}  →  F1={max(f1_arr):.4f}, "
      f"P={precision_score(y_te_m, y_best_f1, zero_division=0):.4f}, "
      f"R={recall_score(y_te_m, y_best_f1):.4f}\n")

print("Estrategia 2 (médica): mayor θ con Recall ≥ 0.90")
best_med_u = None
for u in np.arange(0.01, 0.99, 0.01):
    if recall_score(y_te_m, (y_prob_lr >= u).astype(int), zero_division=0) >= 0.90:
        best_med_u = u
if best_med_u is not None:
    y_med_u = (y_prob_lr >= best_med_u).astype(int)
    print(f"  θ* = {best_med_u:.2f}  →  "
          f"P={precision_score(y_te_m, y_med_u, zero_division=0):.4f}, "
          f"R={recall_score(y_te_m, y_med_u):.4f}\n")

print("Estrategia 3: Punto de Youden (max TPR − FPR)")
J        = tpr_lr - fpr_lr
idx_yo   = np.argmax(J)
u_youden = thresholds_roc[idx_yo]
y_yo     = (y_prob_lr >= u_youden).astype(int)
print(f"  θ* = {u_youden:.2f}  →  J={J[idx_yo]:.4f}, "
      f"P={precision_score(y_te_m, y_yo, zero_division=0):.4f}, "
      f"R={recall_score(y_te_m, y_yo):.4f}")
print("  (James et al., 2021)")
Estrategia 1: Maximizar F1-Score
  θ* = 0.34  →  F1=0.5000, P=0.6667, R=0.4000

Estrategia 2 (médica): mayor θ con Recall ≥ 0.90
  θ* = 0.01  →  P=0.0924, R=0.9200

Estrategia 3: Punto de Youden (max TPR − FPR)
  θ* = 0.27  →  J=0.3927, P=0.4583, R=0.4400
  (James et al., 2021)

Comparación Final de Modelos

Esta sección demuestra que un modelo con menor accuracy puede ser estratégicamente superior cuando el contexto así lo demanda (Provost & Fawcett, 2013). Se comparan tres configuraciones del clasificador médico:

Mostrar código
# ── Tres configuraciones del clasificador médico ───────────────────────────
y_base   = y_pred_lr                                      # LR θ=0.50
y_medico = (y_prob_lr >= 0.25).astype(int)                # LR θ=0.25 (médico)
y_rf_b   = y_pred_rf                                      # RF θ=0.50

modelos_cmp = {
    "LR θ=0.50\n(base)":   y_base,
    "LR θ=0.25\n(médico)": y_medico,
    "RF θ=0.50\n(base)":   y_rf_b,
}

resultados = []
for nombre, y_c in modelos_cmp.items():
    resultados.append({
        "Modelo":    nombre,
        "Accuracy":  round(accuracy_score(y_te_m, y_c), 4),
        "Precision": round(precision_score(y_te_m, y_c, zero_division=0), 4),
        "Recall":    round(recall_score(y_te_m, y_c, zero_division=0), 4),
        "F1":        round(f1_score(y_te_m, y_c, zero_division=0), 4),
    })

df_cmp = pd.DataFrame(resultados)
print("Comparación de modelos — Dataset Médico")
print("(Provost & Fawcett, 2013)\n")
print(df_cmp.to_string(index=False))
print()
print("⚠  El modelo 'LR θ=0.25' tiene menor Accuracy que el base,")
print("   pero detecta más enfermos reales (mayor Recall).")
print("   Para un médico, ese es el modelo correcto.")
Comparación de modelos — Dataset Médico
(Provost & Fawcett, 2013)

             Modelo  Accuracy  Precision  Recall     F1
  LR θ=0.50\n(base)    0.9300     0.7500    0.24 0.3636
LR θ=0.25\n(médico)    0.8967     0.3929    0.44 0.4151
  RF θ=0.50\n(base)    0.9500     1.0000    0.40 0.5714

⚠  El modelo 'LR θ=0.25' tiene menor Accuracy que el base,
   pero detecta más enfermos reales (mayor Recall).
   Para un médico, ese es el modelo correcto.
Mostrar código
# ── Visualización comparativa ────────────────────────────────────────────────
metricas_plot = ["Accuracy", "Precision", "Recall", "F1"]
colores_plot  = ["#4a90d9", "#27ae60", "#e74c3c", "#e67e22"]

fig = go.Figure()
for metrica, color in zip(metricas_plot, colores_plot):
    fig.add_trace(go.Bar(
        name=metrica,
        x=df_cmp["Modelo"],
        y=df_cmp[metrica],
        marker_color=color,
        text=[f"{v:.3f}" for v in df_cmp[metrica]],
        textposition="auto",
    ))

fig.update_layout(
    barmode="group",
    title=(
        "Comparación de Modelos: ¿Más Accuracy = Mejor Modelo?<br>"
        "<sup>La métrica correcta depende del costo del error — Provost & Fawcett (2013)</sup>"
    ),
    yaxis=dict(title="Valor", range=[0, 1.1]),
    height=450,
    legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
fig.show()

Tabla-Resumen: ¿Qué Métrica Usar y Cuándo?

Situación Métrica recomendada Referencia clave
Dataset balanceado Accuracy, F1 macro Han et al. (2011)
Clase positiva rara (< 10 %) F1 clase positiva, PR-AUC Saito & Rehmsmeier (2015)
FN muy costoso (medicina, seguridad) Recall / Sensibilidad James et al. (2021)
FP muy costoso (spam, fraude leve) Precision Fawcett (2006)
FP y FN con costo similar F1-Score Han et al. (2011)
Comparar modelos sin fijar umbral ROC-AUC Fawcett (2006); Bamber (1975)
Clase escasa + interés en ranking Precision-Recall AUC Saito & Rehmsmeier (2015)
Multiclase, todas las clases importan igual F1 macro Pedregosa et al. (2011)

Errores Comunes en la Interpretación de Métricas

Reportar Solo Accuracy en Datos Desbalanceados

El error más frecuente y más dañino (Saito & Rehmsmeier, 2015). La paradoja de la exactitud demostrada en la Sección 4.3 hace que la accuracy sea engañosamente alta en datos desbalanceados. Regla práctica: siempre reportar Precision, Recall y F1 cuando la proporción entre clases supera 80/20.

Confundir Precision con Recall

Precision mide la fiabilidad de las predicciones positivas; Recall mide la cobertura de los positivos reales. Son métricas ortogonales y complementarias, no intercambiables. Confundirlas lleva a interpretar que un modelo con alta Precision “no falla con los enfermos” cuando en realidad puede estar dejando sin detectar a la mayoría (Fawcett, 2006).

Asumir que Mayor AUC = Mejor Modelo en Producción

Mostrar código
# ── AUC alto pero F1 bajo en el umbral operativo ───────────────────────────
# Modelo bien rankeado pero con probabilidades mal calibradas
np.random.seed(99)
y_fake_prob = np.where(
    y_te_m == 1,
    np.random.beta(3, 1, size=y_te_m.shape),
    np.random.beta(1, 2, size=y_te_m.shape),
)
y_fake_pred = (y_fake_prob >= 0.50).astype(int)

fpr_fk, tpr_fk, _ = roc_curve(y_te_m, y_fake_prob)
auc_fk = auc(fpr_fk, tpr_fk)

print("Modelo con AUC alto pero mal calibrado (F1 bajo al umbral default):")
print(f"  AUC             : {auc_fk:.4f}")
print(f"  F1  (θ = 0.50)  : {f1_score(y_te_m, y_fake_pred, zero_division=0):.4f}")
print(f"  Recall          : {recall_score(y_te_m, y_fake_pred, zero_division=0):.4f}")
print()
print("Conclusión: el AUC mide capacidad de ranking, no desempeño operativo.")
print("Validar siempre las métricas en el umbral de producción.")
print("(Fawcett, 2006)")
Modelo con AUC alto pero mal calibrado (F1 bajo al umbral default):
  AUC             : 0.9340
  F1  (θ = 0.50)  : 0.3962
  Recall          : 0.8400

Conclusión: el AUC mide capacidad de ranking, no desempeño operativo.
Validar siempre las métricas en el umbral de producción.
(Fawcett, 2006)

Optimizar el Umbral en el Conjunto de Prueba (Data Leakage)

Ajustar el umbral sobre los mismos datos con los que se evalúa constituye data leakage y produce estimaciones optimistas del desempeño real (James et al., 2021). El umbral debe seleccionarse en el conjunto de validación y evaluarse únicamente en el conjunto de prueba.

Usar F1 Macro Ocultando el Desempeño en la Clase Minoritaria

Mostrar código
# ── F1 macro oculta mal desempeño en clase positiva ─────────────────────────
y_casi_trivial       = np.zeros_like(y_te_m)
idx_top2             = np.argsort(y_prob_lr)[-2:]
y_casi_trivial[idx_top2] = 1

f1_macro_mal = f1_score(y_te_m, y_casi_trivial, average="macro")
f1_pos_mal   = f1_score(y_te_m, y_casi_trivial, average="binary")

print("Modelo que casi nunca detecta enfermos (solo 2 predicciones positivas):")
print(f"  F1 macro    = {f1_macro_mal:.4f}  ← parece 'aceptable'")
print(f"  F1 clase +  = {f1_pos_mal:.4f}  ← revela el verdadero problema")
print()
print("Siempre revisar el F1 por clase, no solo el promedio.")
print("(Han et al., 2011)")
Modelo que casi nunca detecta enfermos (solo 2 predicciones positivas):
  F1 macro    = 0.5540  ← parece 'aceptable'
  F1 clase +  = 0.1481  ← revela el verdadero problema

Siempre revisar el F1 por clase, no solo el promedio.
(Han et al., 2011)

Guía de Selección de Métricas

La selección de la métrica adecuada debe seguir un proceso de análisis del contexto antes de calcular cualquier número (Provost & Fawcett, 2013):

Paso 1 — Caracterizar el desbalance: Si la proporción entre clases supera 80/20, descartar la accuracy como métrica principal (Saito & Rehmsmeier, 2015).

Paso 2 — Identificar el costo diferencial del error: FN catastróficos → Recall/Sensibilidad. FP muy costosos → Precision. Ambos similares → F1-Score (James et al., 2021).

Paso 3 — Determinar si se comparan modelos o se elige un umbral: Comparación global → ROC-AUC o PR-AUC. Configuración en producción → métricas en el umbral operativo (Fawcett, 2006).

Paso 4 — Naturaleza multiclase: Todas las clases con igual importancia de negocio → F1 Macro. Volumen total de errores → F1 Micro. Ponderación por prevalencia → F1 Weighted (Han et al., 2011; Pedregosa et al., 2011).

Paso 5 — Traducción a impacto de negocio: Las métricas estadísticas deben conectar con decisiones reales: ¿cuántos pacientes se salvan?, ¿cuánto fraude se evita?, ¿cuántos clientes se retienen? (Provost & Fawcett, 2013).


Conclusiones

La evaluación de clasificadores en Minería de Datos no es un paso mecánico al final del proceso KDD (Fayyad et al., 1996): es una actividad de análisis estratégico que conecta la calidad estadística del modelo con la utilidad práctica del conocimiento descubierto.

Las métricas presentadas en este capítulo forman un sistema complementario (Han et al., 2011). La accuracy provee contexto general pero falla en datos desbalanceados (Saito & Rehmsmeier, 2015). Precision y Recall cuantifican el balance entre los dos tipos de error con implicaciones radicalmente diferentes según el costo asimétrico del dominio (Provost & Fawcett, 2013). El F1-Score sintetiza ese balance mediante la media armónica, penalizando asimétricamente los extremos (James et al., 2021). La curva ROC y el AUC, con su interpretación probabilística directa (Bamber, 1975; Fawcett, 2006), caracterizan la capacidad discriminativa independiente del umbral, pero deben complementarse con la curva Precision-Recall cuando la clase positiva es minoritaria (Saito & Rehmsmeier, 2015). El umbral de decisión, finalmente, es una palanca estratégica derivable del costo diferencial del error (Provost & Fawcett, 2013).

La competencia central del científico de datos en este dominio no es memorizar fórmulas, sino comprender qué declara cada métrica sobre el modelo y el problema, y elegir la que mejor alinea la optimización estadística con el objetivo de negocio.


Referencias

Bamber, D. (1975). The area above the ordinal dominance graph and the area below the receiver operating characteristic graph. Journal of Mathematical Psychology, 12(4), 387–415. https://doi.org/10.1016/0022-2496(75)90001-2

Fawcett, T. (2006). An introduction to ROC analysis. Pattern Recognition Letters, 27(8), 861–874. https://doi.org/10.1016/j.patrec.2005.10.010

Fayyad, U., Piatetsky-Shapiro, G., & Smyth, P. (1996). From data mining to knowledge discovery in databases. AI Magazine, 17(3), 37–54. https://doi.org/10.1609/aimag.v17i3.1230

Han, J., Kamber, M., & Pei, J. (2011). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. https://doi.org/10.1016/B978-0-12-381479-1.00001-0

James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning with Applications in R (2nd ed.). Springer. https://doi.org/10.1007/978-1-0716-1418-1

Pedregosa, F., Varoquaux, G., Gramfort, A., Michel, V., Thirion, B., Grisel, O., Blondel, M., Prettenhofer, P., Weiss, R., Dubourg, V., Vanderplas, J., Passos, A., Cournapeau, D., Brucher, M., Perrot, M., & Duchesnay, É. (2011). Scikit-learn: Machine learning in Python. Journal of Machine Learning Research, 12, 2825–2830.

Provost, F., & Fawcett, T. (2013). Data Science for Business: What You Need to Know about Data Mining and Data-Analytic Thinking. O’Reilly Media.

Saito, T., & Rehmsmeier, M. (2015). The precision-recall plot is more informative than the ROC plot when evaluating binary classifiers on imbalanced datasets. PLOS ONE, 10(3), e0118432. https://doi.org/10.1371/journal.pone.0118432


Fin del Capítulo 7 — Métricas de Clasificación en Minería de Datos