En la lección 04-02 descubrimos que la regresión logística no predice clases, sino probabilidades (predict_proba), y que el umbral de 0.5 con el que se convierten en "churn / no churn" es una decisión nuestra — una palanca de negocio. En la lección anterior comparamos modelos con ese umbral clavado en 0.5; en esta lo liberamos. La curva ROC evalúa un clasificador en todos los umbrales a la vez, y el AUC resume esa curva en un solo número que permite comparar modelos independientemente del umbral. Después haremos el camino inverso: elegir el umbral operativo concreto que conviene a la campaña de retención de MercaFresh según sus costes y su presupuesto. Cerraremos con la curva precision-recall, la alternativa preferible bajo desbalanceo extremo, y con las limitaciones del AUC.

Contenido

  1. El umbral de decisión como parámetro libre
  2. TPR y FPR: las dos coordenadas de la ROC
  3. Construcción de la curva ROC punto a punto (a mano)
  4. AUC: interpretación probabilística
  5. ROC y AUC en scikit-learn: comparación de modelos
  6. Elegir el umbral operativo según costes de negocio
  7. La curva precision-recall como alternativa
  8. Limitaciones del AUC

El umbral de decisión como parámetro libre

Recordemos el mecanismo de 04-02:

probs = modelo.predict_proba(X_test)[:, 1]   # P(churn) de cada cliente
y_pred_05 = (probs >= 0.5).astype(int)        # umbral por defecto
y_pred_03 = (probs >= 0.3).astype(int)        # umbral más "paranoico"

Bajar el umbral a 0.3 hace que el modelo señale como churn a más clientes: caza más fugas reales (sube el recall) a costa de más falsas alarmas (baja la precision). Subirlo a 0.7 hace lo contrario. El mismo modelo entrenado genera infinitos clasificadores distintos, uno por umbral. Evaluar solo el de 0.5 es mirar un fotograma de una película; la curva ROC muestra la película entera.

TPR y FPR: las dos coordenadas de la ROC

Para cada umbral se calculan dos tasas a partir de la matriz de confusión de 06-02:

  • TPR (tasa de verdaderos positivos) = TP / (TP + FN). Es exactamente el recall: de los clientes que se van, ¿qué fracción detectamos? Queremos que sea alta.
  • FPR (tasa de falsos positivos) = FP / (FP + TN). De los clientes fieles, ¿a qué fracción molestamos con una falsa alarma? Queremos que sea baja.

Nótese la elegancia: cada tasa se calcula dentro de su propia clase (TPR entre los positivos reales, FPR entre los negativos reales), por lo que ninguna depende de la proporción de churn del dataset — un detalle que será relevante al final de la lección.

Construcción de la curva ROC punto a punto (a mano)

Nada fija mejor la idea que calcularla a mano. Tomemos 10 clientes de test de MercaFresh con sus probabilidades de churn según el modelo, ordenados de mayor a menor probabilidad (4 churn reales, 6 fieles):

Cliente P(churn) Real
C1 0.92 Churn
C2 0.81 Churn
C3 0.74 Fiel
C4 0.66 Churn
C5 0.55 Fiel
C6 0.47 Fiel
C7 0.39 Churn
C8 0.30 Fiel
C9 0.18 Fiel
C10 0.05 Fiel

Ahora deslizamos el umbral desde arriba: para cada valor, todo cliente con probabilidad ≥ umbral se predice como churn, y contamos TP, FP → TPR = TP/4, FPR = FP/6:

Umbral Predichos churn TP FP TPR FPR
> 0.92 (nadie) 0 0 0.00 0.00
0.92 C1 1 0 0.25 0.00
0.81 C1–C2 2 0 0.50 0.00
0.74 C1–C3 2 1 0.50 0.17
0.66 C1–C4 3 1 0.75 0.17
0.55 C1–C5 3 2 0.75 0.33
0.47 C1–C6 3 3 0.75 0.50
0.39 C1–C7 4 3 1.00 0.50
0.30 C1–C8 4 4 1.00 0.67
0.18 C1–C9 4 5 1.00 0.83
0.05 todos 4 6 1.00 1.00

La curva ROC es el dibujo de estos puntos (FPR en el eje X, TPR en el eje Y), desde (0,0) — umbral imposible de superar: nadie es señalado — hasta (1,1) — umbral cero: todos señalados. Cada vez que el deslizamiento "traga" un churn real, la curva sube; cada vez que traga un fiel, avanza a la derecha. Un modelo bueno ordena a los churn por delante de los fieles, así que su curva sube pronto y mucho antes de avanzar: se pega a la esquina superior izquierda.

Dos referencias en el gráfico:

  • La diagonal de (0,0) a (1,1) es el azar: un "modelo" que asignara probabilidades aleatorias sube y avanza al mismo ritmo. Toda curva útil debe quedar por encima; una curva por debajo de la diagonal indica un modelo que ordena al revés (¡invertir sus predicciones lo volvería útil!).
  • El punto ideal es (0, 1): 100 % de fugas detectadas, 0 % de fieles molestados. Ningún modelo real lo alcanza, pero la distancia a esa esquina resume la calidad de cada umbral.

AUC: interpretación probabilística

El AUC (Area Under the Curve) es el área bajo la curva ROC: un número entre 0 y 1 que condensa el rendimiento en todos los umbrales.

AUC Lectura
0.5 Azar puro (la diagonal)
0.6–0.7 Discriminación débil
0.7–0.8 Aceptable
0.8–0.9 Buena
> 0.9 Excelente (¡verifica que no haya fuga de datos, 06-01!)

Su interpretación más útil es probabilística: el AUC es la probabilidad de que, tomando al azar un cliente que se fue y uno fiel, el modelo asigne mayor probabilidad de churn al que se fue. Es decir, mide la calidad del ranking, no de la clasificación. Comprobémoslo con nuestra tabla: hay 4 × 6 = 24 pares (churn, fiel) posibles; contando cuántos ordena bien el modelo (el churn con mayor probabilidad que el fiel): C1 y C2 superan a los 6 fieles (12 pares), C4 supera a 5 (todos menos C3), C7 supera a 3 (C8, C9, C10). Total: 12 + 5 + 3 = 20 pares bien ordenados de 24 → AUC = 20/24 ≈ 0.83, que coincide exactamente con el área bajo la curva escalonada anterior.

Esta lectura explica por qué el AUC es ideal para comparar modelos antes de decidir el umbral: un modelo con mejor AUC ordena mejor a los clientes por riesgo, y esa ordenación es la materia prima de cualquier campaña ("llamar primero a los de más riesgo").

ROC y AUC en scikit-learn: comparación de modelos

from sklearn.metrics import roc_curve, roc_auc_score
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt

modelos = {
    "Regresión logística": Pipeline([("sc", StandardScaler()),
                                     ("clf", LogisticRegression(max_iter=1000))]),
    "Árbol (max_depth=5)": DecisionTreeClassifier(max_depth=5, random_state=42),
}

plt.figure(figsize=(6, 6))
for nombre, m in modelos.items():
    m.fit(X_train, y_train)
    probs = m.predict_proba(X_test)[:, 1]        # necesitamos probabilidades, no clases
    fpr, tpr, umbrales = roc_curve(y_test, probs)
    auc = roc_auc_score(y_test, probs)
    plt.plot(fpr, tpr, label=f"{nombre} (AUC = {auc:.3f})")

plt.plot([0, 1], [0, 1], "k--", label="Azar (AUC = 0.5)")   # la diagonal
plt.xlabel("FPR (fieles molestados)")
plt.ylabel("TPR / Recall (fugas detectadas)")
plt.title("Curvas ROC — churn MercaFresh")
plt.legend()
plt.show()

Detalles importantes:

  • roc_curve devuelve tres arrays: fpr, tpr y los umbrales correspondientes a cada punto — los usaremos ahora para elegir el umbral operativo.
  • A roc_auc_score se le pasan las probabilidades, nunca predict() (con clases 0/1 la "curva" tendría un solo punto útil y el AUC saldría distorsionado).
  • Para modelos sin predict_proba bien calibrado (como SVC), se usa decision_function, que también ordena.
  • Si dos curvas se cruzan, un modelo puede ser mejor en la zona de FPR bajo y el otro en la de FPR alto: el AUC global puede empatar mientras que, para tu rango operativo, uno de los dos sea claramente preferible. Mirar la curva, no solo el número.

Elegir el umbral operativo según costes de negocio

El AUC compara modelos; para usar el modelo elegido hay que fijar un umbral. Escenario MercaFresh: el equipo de retención puede llamar a 600 clientes al mes (presupuesto limitado) de una cartera de 10.000, con churn esperado del 20 %. Dos enfoques complementarios:

Enfoque 1 — capacidad: si solo puedes llamar a 600, el umbral es simplemente el que señala a los 600 clientes de mayor probabilidad:

import numpy as np
probs_todos = modelo.predict_proba(X_cartera)[:, 1]
umbral_capacidad = np.sort(probs_todos)[-600]        # prob. del cliente nº 600 por riesgo
print(f"Umbral operativo: {umbral_capacidad:.2f}")

Enfoque 2 — costes: con costes explícitos (llamada + vale ≈ 15 €; cliente perdido ≈ 300 € de valor anual; la retención funciona, digamos, en el 30 % de los contactados a tiempo), se puede calcular el beneficio esperado de cada umbral y quedarse con el máximo:

fpr, tpr, umbrales = roc_curve(y_test, probs)
n_pos, n_neg = y_test.sum(), (1 - y_test).sum()

coste_contacto, valor_cliente, efic = 15, 300, 0.30
beneficio = (tpr * n_pos * efic * valor_cliente          # fugas salvadas
             - (tpr * n_pos + fpr * n_neg) * coste_contacto)  # coste de contactar
mejor = np.argmax(beneficio)
print(f"Umbral óptimo: {umbrales[mejor]:.2f}  (TPR={tpr[mejor]:.2f}, FPR={fpr[mejor]:.2f})")

La conclusión habitual en churn: el umbral óptimo queda muy por debajo de 0.5 (a menudo 0.2–0.35), porque un FN cuesta 20 veces más que un FP. El 0.5 por defecto no es neutral: presupone costes simétricos que casi nunca son los del negocio. Y una nota metodológica: el umbral se elige con datos de validación, no de test (regla de 06-01: el test no participa en decisiones).

La curva precision-recall como alternativa

Con desbalanceo fuerte (positivos al 1–5 %: fraude, enfermedades raras... o un churn mensual muy bajo), la ROC puede pintar un panorama demasiado optimista: como el FPR se calcula sobre la enorme clase negativa, un FPR "bajo" del 5 % puede significar miles de falsas alarmas que sepultan a los verdaderos positivos. La curva precision-recall (precision en Y, recall en X, un punto por umbral) usa la precision en lugar del FPR, y la precision siente cada falso positivo directamente.

from sklearn.metrics import precision_recall_curve, average_precision_score
prec, rec, umb = precision_recall_curve(y_test, probs)
ap = average_precision_score(y_test, probs)      # análogo al AUC para esta curva
plt.plot(rec, prec, label=f"AP = {ap:.3f}")
plt.xlabel("Recall"); plt.ylabel("Precision"); plt.legend(); plt.show()

Regla práctica: churn al 20 % → la ROC funciona bien; positivos por debajo del ~5 % o interés centrado exclusivamente en la clase minoritaria → añade (o prioriza) la curva precision-recall. Aquí su línea base no es la diagonal, sino la horizontal en la prevalencia (0.20 en nuestro caso): esa es la precision de señalar al azar.

Limitaciones del AUC

  • Es ciego a los costes: resume todos los umbrales por igual, incluidos los que nunca usarías. Dos modelos con el mismo AUC pueden rendir muy distinto en tu zona operativa (FPR bajo, si el presupuesto es corto).
  • No mide calibración: evalúa el orden, no si "0.7" significa realmente un 70 % de probabilidad. Un modelo puede tener AUC 0.85 con probabilidades sistemáticamente infladas; si vas a usar las probabilidades como tales (cálculos de beneficio esperado), la calibración importa.
  • Optimista bajo desbalanceo extremo, como acabamos de ver: complementa con precision-recall.
  • Un número no sustituye la curva: con curvas que se cruzan, el resumen escalar oculta la diferencia que te afecta.
  • Nada de esto lo invalida: como métrica de comparación de modelos independiente del umbral, sigue siendo el estándar (y por eso scoring="roc_auc" encaja de forma natural en la validación cruzada de 06-03).

Errores Comunes y Consejos

  • Pasar predict() en vez de predict_proba() a roc_auc_score. Con clases duras el AUC se calcula sobre un ranking de solo dos valores y sale artificialmente bajo. Probabilidades siempre.
  • Quedarse con el umbral 0.5 "porque es el defecto". En problemas con costes asimétricos (casi todos los reales), 0.5 es una elección arbitraria heredada, no una decisión.
  • Elegir el umbral mirando el test. El umbral es una decisión más del modelado: se toma con validación (o CV) y se verifica una única vez en test.
  • Comparar AUCs con tres decimales. La lección 06-03 aplica también aquí: un AUC de 0.842 no es mejor que uno de 0.838 sin mirar la variabilidad entre folds.
  • Usar ROC como única lente con desbalanceo extremo. Acompáñala de la curva precision-recall.
  • Consejo: entrega al negocio la curva con dos o tres puntos operativos anotados ("con umbral 0.3: detectamos el 78 % de las fugas llamando a 1.900 clientes") — es infinitamente más accionable que "AUC = 0.84".

Ejercicios

Ejercicio 1

Con esta mini-tabla (3 churn reales, 3 fieles), calcula los puntos (FPR, TPR) para los umbrales 0.9, 0.6 y 0.3, y el AUC por el método de contar pares bien ordenados.

Cliente P(churn) Real
A 0.90 Churn
B 0.70 Fiel
C 0.60 Churn
D 0.40 Churn
E 0.20 Fiel
F 0.10 Fiel

Ejercicio 2

El modelo de churn de MercaFresh tiene AUC = 0.84. Explica a un directivo, sin tecnicismos, qué significa ese 0.84, y por qué ese número por sí solo no dice cuántos clientes hay que llamar cada mes.

Ejercicio 3

Escribe el código que, dado probs (probabilidades sobre validación) e y_val, encuentra el umbral más bajo cuyo recall es ≥ 0.80 y reporta cuántos clientes habría que contactar con él (sobre una cartera de 10.000 con la misma distribución). Pista: roc_curve te da tpr y umbrales alineados.

Soluciones

Solución 1. Positivos = 3 (A, C, D), negativos = 3 (B, E, F).

  • Umbral 0.9 → señalados {A}: TP = 1, FP = 0 → (FPR, TPR) = (0, 0.33).
  • Umbral 0.6 → señalados {A, B, C}: TP = 2, FP = 1 → (0.33, 0.67).
  • Umbral 0.3 → señalados {A, B, C, D}: TP = 3, FP = 1 → (0.33, 1.00).

Pares (churn, fiel) = 9. Bien ordenados: A supera a B, E, F (3); C supera a E, F (2); D supera a E, F (2). Total 7/9 → AUC ≈ 0.78.

Solución 2. "Si cogemos al azar un cliente que acabó dándose de baja y otro que se quedó, el modelo le asigna más riesgo al que se dio de baja el 84 % de las veces: ordena bien a los clientes por riesgo de fuga." No dice a cuántos llamar porque el AUC evalúa la ordenación completa, sin fijar ningún punto de corte: cuántos llamamos depende de dónde cortemos esa lista ordenada, y esa es una decisión de negocio (presupuesto de la campaña, coste por contacto, valor del cliente), no una propiedad del modelo.

Solución 3.

import numpy as np
from sklearn.metrics import roc_curve

fpr, tpr, umbrales = roc_curve(y_val, probs)

# Primer punto (recorriendo umbrales de mayor a menor) con recall >= 0.80:
idx = np.argmax(tpr >= 0.80)          # primer índice que cumple la condición
umbral_op = umbrales[idx]
print(f"Umbral operativo: {umbral_op:.3f}  (TPR={tpr[idx]:.2f}, FPR={fpr[idx]:.2f})")

# Fracción de la cartera señalada con ese umbral:
frac_contactada = (probs >= umbral_op).mean()
print(f"Contactos sobre 10.000 clientes: {frac_contactada * 10000:.0f}")

Como roc_curve ordena los umbrales de mayor a menor y tpr crece a lo largo del array, el primer índice con tpr >= 0.80 corresponde al umbral más alto (y por tanto con menos contactos) que alcanza ese recall; es el punto más barato que cumple el objetivo de detección.

Conclusión

Hemos liberado el umbral de decisión: TPR y FPR describen cada posible corte, la curva ROC los dibuja todos (construida a mano, punto a punto, sobre diez clientes de MercaFresh), y el AUC la resume con una interpretación memorable — la probabilidad de rankear bien un par churn/fiel. Con roc_curve y roc_auc_score comparamos modelos más allá del 0.5 por defecto, elegimos el umbral operativo con criterios de negocio (presupuesto de la campaña, coste de cada tipo de error) y aprendimos cuándo la curva precision-recall es mejor lente y qué no cuenta el AUC. Ya tenemos el arsenal completo de evaluación; queda la pregunta de diagnóstico que lo atraviesa todo: cuando un modelo rinde mal — o sospechosamente bien — ¿es porque memoriza o porque no aprende lo suficiente? Overfitting y underfitting, el diagnóstico central de todo el Machine Learning, cierran el módulo en la próxima lección.

Curso de Machine Learning

Módulo 1: Introducción al Machine Learning

Módulo 2: Fundamentos de Estadística y Probabilidad

Módulo 3: Preprocesamiento de Datos

Módulo 4: Algoritmos de Machine Learning Supervisado

Módulo 5: Algoritmos de Machine Learning No Supervisado

Módulo 6: Evaluación y Validación de Modelos

Módulo 7: Técnicas Avanzadas y Optimización

Módulo 8: Implementación y Despliegue de Modelos

Módulo 9: Proyectos Prácticos

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados