Saltar a contenido

Capítulo 3: Clasificación — Predecir categorías

Módulo 02 — MACHINE LEARNING · AI Master Academy

"La regresión te dice cuánto. La clasificación te dice cuál. Y en la mayoría de los problemas de negocio, la pregunta es cuál: ¿es fraude o no?, ¿se irá el cliente o no?, ¿apruebo este crédito o no?"


Requisitos previos

Antes de empezar este capítulo deberías dominar:

  • El workflow completo de un proyecto ML: train/test split, fit, predict, evaluate (Capítulo 1).
  • Regresión lineal, sus métricas (MAE, RMSE, R²) y regularización (Capítulo 2).
  • Probabilidad básica, teorema de Bayes y la función softmax (Módulo 01-FUNDAMENTOS).
  • Python, NumPy, pandas y scikit-learn instalados y funcionando.

Índice

  1. Qué es clasificar y por qué le importa al negocio
  2. Regresión logística: EL algoritmo de referencia
  3. Métricas de clasificación: la sección más importante
  4. KNN: dime con quién andas
  5. Árboles de decisión: reglas que puedes leer
  6. Naive Bayes: el clasificador probabilístico
  7. SVM: el margen máximo
  8. Desbalanceo de clases: el problema silencioso
  9. Tabla maestra comparativa
  10. Ejemplo integrador: detección de churn de principio a fin
  11. Caso empresarial: scoring de crédito en una fintech
  12. Buenas prácticas
  13. Malas prácticas
  14. Errores comunes
  15. FAQ — Preguntas frecuentes
  16. Resumen del capítulo
  17. Bibliografía y recursos

1. Qué es clasificar y por qué le importa al negocio

En el capítulo anterior predecías números continuos: el precio de una casa, las ventas del mes que viene. En clasificación predices categorías discretas: la etiqueta a la que pertenece cada observación.

La pregunta cambia de "¿cuánto?" a "¿cuál?":

Problema de negocio Pregunta Clases Coste del error
Spam ¿Este email es spam? spam / no spam Email legítimo perdido (grave) vs spam en bandeja (molesto)
Fraude bancario ¿Esta transacción es fraudulenta? fraude / legítima Fraude no detectado (dinero perdido) vs cliente bloqueado (fricción)
Churn ¿Este cliente se dará de baja? se va / se queda Cliente perdido (LTV completo) vs descuento innecesario
Scoring crediticio ¿Este solicitante devolverá el préstamo? paga / impaga Préstamo impagado vs cliente rentable rechazado
Diagnóstico médico ¿Este paciente tiene la enfermedad? enfermo / sano Enfermedad no detectada (vidas) vs pruebas extra
Moderación de contenido ¿Este post viola las normas? tóxico / aceptable Contenido dañino publicado vs censura injusta

Fíjate en la última columna: en clasificación, los dos tipos de error casi nunca cuestan lo mismo. Esta asimetría es el hilo conductor de todo el capítulo y la razón por la que la sección de métricas es la más importante.

1.1 Binaria vs multiclase vs multilabel

No todos los problemas de clasificación tienen la misma estructura:

Tipo Definición Nº de etiquetas por muestra Ejemplos
Binaria Dos clases mutuamente excluyentes Exactamente 1 de 2 Spam/no spam, fraude/legítimo, churn/no churn
Multiclase Más de dos clases mutuamente excluyentes Exactamente 1 de N Dígito manuscrito (0-9), idioma de un texto, especie de flor
Multilabel Varias etiquetas pueden aplicar a la vez 0 a N de N Etiquetas de un artículo (política + economía), géneros de una película, síntomas presentes

Nota

Multiclase y multilabel se confunden constantemente. La prueba del algodón: ¿puede una muestra pertenecer a dos clases a la vez? Una película puede ser comedia Y romance (multilabel). Un dígito manuscrito no puede ser 3 Y 7 (multiclase). En este capítulo nos centramos en binaria y multiclase; sklearn maneja multilabel con MultiOutputClassifier o clasificadores nativos multilabel.

1.2 El mapa mental del capítulo

flowchart TD
    A[Problema de clasificación] --> B{¿Cuántas clases?}
    B -->|2|C[Binaria]
    B -->|N, excluyentes|D[Multiclase]
    B -->|N, no excluyentes|E[Multilabel]
    C --> F{¿Qué priorizas?}
    F -->|Interpretabilidad + baseline|G[Regresión logística]
    F -->|Reglas legibles|H[Árbol de decisión]
    F -->|Texto / conteos|I[Naive Bayes]
    F -->|Pocas filas, muchas columnas|J[SVM]
    F -->|Similitud local, prototipo rápido|K[KNN]
    G --> L[Evaluar con la métrica<br/>que refleje el coste de negocio]
    H --> L
    I --> L
    J --> L
    K --> L
    L --> M[Ajustar umbral de decisión<br/>según coste FP vs FN]

Consejo profesional

El 80% de los proyectos de clasificación tabular en empresa se resuelven bien con regresión logística o con un ensemble de árboles (que verás en el capítulo 5). Los demás algoritmos de este capítulo existen para nichos concretos y, sobre todo, para que entiendas los conceptos que reaparecen en deep learning y en LLMs: sigmoide, softmax, umbrales, márgenes, similitud.


2. Regresión logística: EL algoritmo de referencia

2.1 Qué es y por qué existe

La regresión logística es el algoritmo de clasificación más usado del mundo en producción. Es el primer modelo que se entrena en cualquier proyecto serio, el baseline contra el que se compara todo lo demás, y muchas veces el modelo final que llega a producción por su interpretabilidad y velocidad.

¿Por qué se llama "regresión" si clasifica? Porque históricamente es una extensión de la regresión lineal: en lugar de predecir un número cualquiera, predice la probabilidad de pertenecer a la clase positiva (un número entre 0 y 1). Internamente sigue siendo una combinación lineal de las features; la clasificación aparece al final, cuando comparas esa probabilidad con un umbral.

2.2 Cómo funciona por dentro: la sigmoide

La regresión lineal calcula:

z = w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b

El problema: z puede valer -50 o +300, y una probabilidad debe estar entre 0 y 1. La solución es pasar z por la función sigmoide:

σ(z) = 1 / (1 + e⁻ᶻ)
 P(clase=1)
 1.0 ┤                                    ╭────────────
     │                                ╭───╯
     │                             ╭──╯
 0.5 ┤— — — — — — — — — — — — — ╭─╯— — — — — — — — —
     │                       ╭──╯
     │                   ╭───╯
 0.0 ┤───────────────────╯
     └─────────────────────┬──────────────────────── z
                          z=0

Propiedades clave de la sigmoide:

  • Con z = 0 devuelve exactamente 0.5 (máxima incertidumbre).
  • Con z muy positivo se acerca a 1 (muy seguro de la clase positiva).
  • Con z muy negativo se acerca a 0 (muy seguro de la clase negativa).
  • Es suave y derivable, lo que permite entrenarla con descenso de gradiente (como viste en el módulo 01).

Nota (conexión con el módulo 01)

La sigmoide es el caso particular de softmax con dos clases. Softmax generaliza esta idea a N clases: convierte N puntuaciones en N probabilidades que suman 1. Es exactamente lo que hace la última capa de una red neuronal de clasificación y lo que hace un LLM al elegir el siguiente token: softmax sobre ~100.000 "clases" (el vocabulario). Cuando LogisticRegression de sklearn recibe más de 2 clases, usa softmax internamente (regresión logística multinomial).

Durante el entrenamiento, el modelo ajusta los pesos w minimizando la log-loss (entropía cruzada binaria): penaliza mucho estar muy seguro y equivocarse. Predecir 0.99 de probabilidad de fraude en una transacción legítima cuesta muchísimo más que predecir 0.6.

2.3 Probabilidades y el umbral de decisión (sección importante)

Aquí está la idea más valiosa de toda la regresión logística, y la que más ingenieros junior ignoran:

El modelo NO decide. El modelo estima probabilidades. Quien decide es el UMBRAL, y el umbral lo eliges TÚ según el coste de negocio.

Por defecto, sklearn usa umbral 0.5: si P(clase=1) >= 0.5 predice 1. Pero 0.5 es una convención arbitraria que solo es óptima cuando ambos errores cuestan lo mismo, cosa que casi nunca ocurre.

Escenario ¿Qué error es más caro? Umbral recomendado Efecto
Detección de fraude FN (fraude no detectado) Bajarlo (ej. 0.2) Marcas más transacciones como fraude: cazas más fraudes, más falsas alarmas
Filtro de spam FP (email legítimo a spam) Subirlo (ej. 0.8) Solo marcas spam cuando estás muy seguro: nunca pierdes un email importante
Cribado médico FN (enfermo sin detectar) Bajarlo (ej. 0.15) Más pacientes pasan a pruebas confirmatorias
Campaña de retención cara FP (descuento a quien no se iba) Subirlo (ej. 0.7) Solo gastas presupuesto en clientes con alto riesgo real

Código completo para mover el umbral:

import numpy as np                                          # Álgebra numérica
from sklearn.datasets import make_classification           # Generador de datasets sintéticos
from sklearn.model_selection import train_test_split       # División train/test
from sklearn.linear_model import LogisticRegression        # Nuestro clasificador
from sklearn.metrics import confusion_matrix, recall_score, precision_score

# 1. Creamos un dataset sintético desbalanceado: 5% de fraudes
X, y = make_classification(
    n_samples=5000,        # 5000 transacciones
    n_features=10,         # 10 variables (importe, hora, país, etc. simulados)
    weights=[0.95, 0.05],  # 95% legítimas, 5% fraude -> desbalanceo realista
    random_state=42        # Semilla para reproducibilidad
)

# 2. Split estratificado: mantiene el 5% de fraude en train Y en test
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,     # 20% para test
    stratify=y,        # CLAVE con desbalanceo: preserva proporciones de clase
    random_state=42
)

# 3. Entrenamos la regresión logística
modelo = LogisticRegression(max_iter=1000)  # max_iter alto para asegurar convergencia
modelo.fit(X_train, y_train)                # Aprende los pesos w y el sesgo b

# 4. En lugar de .predict() (que usa umbral 0.5), pedimos PROBABILIDADES
probas = modelo.predict_proba(X_test)[:, 1]  # Columna 1 = P(fraude)
# predict_proba devuelve una matriz (n, 2): [P(clase 0), P(clase 1)]

# 5. Comparamos tres umbrales distintos
for umbral in [0.5, 0.3, 0.15]:
    y_pred = (probas >= umbral).astype(int)  # True/False -> 1/0 según el umbral
    rec = recall_score(y_test, y_pred)       # % de fraudes reales que cazamos
    prec = precision_score(y_test, y_pred)   # % de alertas que eran fraude real
    print(f"Umbral {umbral}: recall={rec:.2f}, precision={prec:.2f}")
    # Al BAJAR el umbral: recall SUBE (cazas más fraudes)
    #                     precision BAJA (más falsas alarmas)

Salida típica:

Umbral 0.5:  recall=0.62, precision=0.86
Umbral 0.3:  recall=0.78, precision=0.71
Umbral 0.15: recall=0.90, precision=0.52

Advertencia

El umbral se elige mirando el conjunto de validación, nunca el test. Si eliges el umbral que maximiza una métrica en test, has contaminado tu estimación final: el test debe ser datos que el proceso completo (modelo + umbral) nunca vio.

2.4 Interpretación de coeficientes y odds (para hablar con negocio)

Cada peso wᵢ de la regresión logística tiene lectura directa. Los coeficientes actúan sobre las log-odds (logaritmo del cociente de probabilidades). La traducción práctica:

  • exp(wᵢ) es el odds ratio: cuánto se multiplican las "apuestas" a favor de la clase positiva cuando xᵢ aumenta en 1 unidad.
  • Si w_importe = 0.7, entonces exp(0.7) ≈ 2.0: cada unidad extra de importe duplica las odds de fraude.
  • Si w_antigüedad = -0.3, entonces exp(-0.3) ≈ 0.74: cada año extra de antigüedad del cliente reduce las odds de fraude un 26%.
import pandas as pd  # Para presentar los coeficientes de forma legible

# Suponiendo columnas con nombre (aquí las inventamos para el ejemplo)
nombres = [f"feature_{i}" for i in range(X.shape[1])]

coefs = pd.DataFrame({
    "feature": nombres,                          # Nombre de cada variable
    "coef": modelo.coef_[0],                     # Peso aprendido (escala log-odds)
    "odds_ratio": np.exp(modelo.coef_[0]),       # exp(coef): interpretable para negocio
}).sort_values("odds_ratio", ascending=False)    # Ordenamos de mayor a menor impacto

print(coefs)
# odds_ratio > 1 -> la variable EMPUJA hacia la clase positiva
# odds_ratio < 1 -> la variable PROTEGE (empuja hacia la clase negativa)
# odds_ratio ≈ 1 -> la variable apenas influye

Consejo profesional

Para que los coeficientes sean comparables entre sí, escala las features antes de entrenar (StandardScaler). Si no, una feature medida en euros (0-100.000) tendrá coeficientes minúsculos frente a otra medida en años (0-30), y la comparación de magnitudes será engañosa.

2.5 Regularización: el hiperparámetro C

Igual que Ridge/Lasso en regresión (capítulo 2), la regresión logística se regulariza para evitar overfitting. En sklearn el control es el parámetro C:

  • C es la inversa de la fuerza de regularización: C pequeño = regularización fuerte (pesos más pequeños, modelo más simple); C grande = regularización débil (modelo más flexible).
  • Por defecto C=1.0 con penalización L2. Con penalty="l1" (y solver liblinear o saga) obtienes selección de features: coeficientes exactamente a cero, como Lasso.
from sklearn.model_selection import GridSearchCV   # Búsqueda de hiperparámetros con CV
from sklearn.pipeline import Pipeline              # Encadena escalado + modelo sin fugas
from sklearn.preprocessing import StandardScaler   # Escalado estándar (media 0, std 1)

pipe = Pipeline([
    ("scaler", StandardScaler()),                  # Paso 1: escalar (se ajusta SOLO en train de cada fold)
    ("clf", LogisticRegression(max_iter=1000)),    # Paso 2: clasificador
])

grid = GridSearchCV(
    pipe,
    param_grid={"clf__C": [0.01, 0.1, 1, 10, 100]},  # Rango logarítmico de C
    scoring="f1",       # Métrica de selección: F1 (adecuada si hay desbalanceo)
    cv=5,               # Validación cruzada de 5 folds
)
grid.fit(X_train, y_train)                # Prueba cada C con CV y se queda con el mejor
print("Mejor C:", grid.best_params_)      # C óptimo encontrado
print("Mejor F1 (CV):", grid.best_score_) # Rendimiento estimado honestamente

2.6 Ventajas, desventajas y cuándo NO usarla

Ventajas Desventajas / limitaciones
Rapidísima de entrenar y de predecir (millones de filas sin despeinarse) Solo captura fronteras lineales (sin ingeniería de features)
Probabilidades bien calibradas de serie Sufre con relaciones muy no lineales o interacciones complejas
Coeficientes interpretables → apta para entornos regulados Sensible a outliers extremos en las features
Baseline perfecto: si algo no la supera claramente, no lo compliques Requiere escalado para interpretar y para converger bien
Regularización integrada (L1/L2) Necesita features numéricas (codificar categóricas antes)

Cuándo NO usarla: cuando la relación entre features y clase es fuertemente no lineal y no puedes/quieres crear features manualmente (usa árboles o ensembles), o en datos no tabulares crudos (imágenes, audio → deep learning).

Ejercicio rápido 1

Tu modelo de fraude da P(fraude) = 0.42 para una transacción. El umbral por defecto es 0.5. El equipo de riesgo te dice: "cada fraude no detectado nos cuesta 40× más que revisar una falsa alarma". ¿Qué haces con esa transacción y con el sistema?

Ver solución Con umbral 0.5, esa transacción se clasificaría como legítima (0.42 < 0.5) y un posible fraude pasaría sin revisión. Pero si un FN cuesta 40 veces más que un FP, el umbral óptimo debe ser **mucho más bajo que 0.5** (con la aproximación coste-sensible, alrededor de 1/(1+40) ≈ 0.024, aunque en la práctica se calibra en validación con la matriz de costes real). Con cualquier umbral razonable para ese ratio de costes (p. ej. 0.1-0.2), la transacción con 0.42 **sí se marcaría para revisión**. Acción: proponer al equipo bajar el umbral, cuantificando en validación cuántas falsas alarmas extra genera y cuánto fraude adicional caza, traducido a euros.

3. Métricas de clasificación: la sección más importante

Si solo puedes dominar una cosa de este capítulo, que sea esta sección. Elegir la métrica equivocada es la forma más común de fracasar en un proyecto de clasificación, porque puedes optimizar durante semanas un número que no significa nada para el negocio.

3.1 La matriz de confusión, célula a célula

Todo empieza aquí. Es una tabla que cruza realidad contra predicción:

                        PREDICCIÓN DEL MODELO
                     ┌───────────────┬───────────────┐
                     │  Positivo (1) │  Negativo (0) │
        ┌────────────┼───────────────┼───────────────┤
        │ Positivo   │      TP       │      FN       │
REALIDAD│ (1)        │ Verdadero     │ Falso         │
        │            │ Positivo ✅   │ Negativo ❌ │
        ├────────────┼───────────────┼───────────────┤
        │ Negativo   │      FP       │      TN       │
        │ (0)        │ Falso         │ Verdadero     │
        │            │ Positivo ❌⚠ │ Negativo ✅   │
        └────────────┴───────────────┴───────────────┘

Las cuatro celdas, con dos ejemplos paralelos:

Celda Significado Ejemplo médico (positivo = enfermo) Ejemplo fraude (positivo = fraude)
TP (True Positive) Predije positivo y ERA positivo Detecté la enfermedad en un enfermo Bloqueé una transacción que era fraude
FN (False Negative) Predije negativo pero ERA positivo Mandé a casa a un enfermo Dejé pasar un fraude: dinero perdido
FP (False Positive) Predije positivo pero ERA negativo Asusté a un sano con más pruebas Bloqueé la tarjeta de un cliente legítimo
TN (True Negative) Predije negativo y ERA negativo Confirmé que un sano está sano Dejé pasar una compra legítima

Nota

"Positivo" no significa "bueno". Positivo es la clase que buscas detectar: la enfermedad, el fraude, el churn. Por convención es la clase 1 y suele ser la minoritaria.

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

y_pred = modelo.predict(X_test)                  # Predicciones con umbral 0.5
cm = confusion_matrix(y_test, y_pred)            # Matriz 2x2: filas=realidad, cols=predicción
print(cm)
# ⚠ En sklearn el orden es: [[TN, FP],
#                             [FN, TP]]
# (clase 0 primero). ¡No lo confundas con otros libros que ponen TP arriba!

ConfusionMatrixDisplay(cm, display_labels=["Legítima", "Fraude"]).plot()
plt.show()                                       # Visualización con colores

3.2 Accuracy: la métrica que MIENTE con clases desbalanceadas

Accuracy = (TP + TN) / (TP + TN + FP + FN)  →  "% de aciertos totales"

Parece razonable. Ahora el ejemplo numérico que debes recordar toda tu carrera:

Dataset de fraude: 1000 transacciones, 990 legítimas (99%) y 10 fraudes (1%).

Modelo "tonto" que SIEMPRE predice "legítima": - Acierta las 990 legítimas, falla los 10 fraudes. - Accuracy = 990/1000 = 99%. - Fraudes detectados: 0 de 10 = 0%.

Un modelo con 99% de accuracy que no detecta ni un solo fraude. Es completamente inútil, y la accuracy lo presenta como excelente.

Advertencia

Si alguien te presenta un clasificador solo con su accuracy, tu primera pregunta debe ser: "¿cuál es la distribución de clases?". Con clases desbalanceadas (fraude, churn, enfermedad rara, fallo de máquina... es decir, casi todos los problemas interesantes), la accuracy es engañosa por diseño: la domina la clase mayoritaria.

3.3 Precision, Recall y F1: el trío que importa

Estas métricas miran la matriz de confusión desde dos ángulos distintos:

Precision = TP / (TP + FP)   →  "De todo lo que MARQUÉ como positivo, ¿cuánto lo era?"
                                 (calidad de las alarmas)

Recall    = TP / (TP + FN)   →  "De todos los positivos REALES, ¿cuántos cacé?"
                                 (cobertura de los casos)

Intuición memorizable:

  • Precision = "cuando el modelo grita, ¿tiene razón?" — la pagan los FP.
  • Recall = "¿cuántos se le escapan?" — la pagan los FN.

Y el tradeoff fundamental: subir una suele bajar la otra. Si marcas TODO como fraude, tu recall es 100% (no se te escapa nada) pero tu precision es ridícula (casi todas las alarmas son falsas). Si solo marcas cuando estás segurísimo, tu precision es altísima pero se te escapan la mayoría (recall bajo). El umbral de decisión de la sección 2.3 es exactamente el mando que mueve este tradeoff.

¿Cuál prima en cada problema? Depende de qué error es más caro:

Problema Error más caro Métrica prioritaria Razonamiento
Fraude bancario FN (fraude sin detectar) Recall Prefieres revisar de más a perder dinero
Filtro de spam FP (email legítimo a spam) Precision Perder un contrato en la carpeta de spam es inaceptable
Cribado de cáncer FN (enfermo sin detectar) Recall Un FP genera pruebas extra; un FN puede costar una vida
Recomendador de contenido FP (recomendar basura) Precision Mejor recomendar poco y bien que saturar al usuario
Churn con retención barata (un email) FN (cliente que se va sin aviso) Recall Contactar de más apenas cuesta
Churn con retención cara (descuento 30%) FP (descuento regalado) Precision Cada falsa alarma quema margen
Moderación de contenido ilegal FN (contenido dañino publicado) Recall El coste reputacional/legal del FN domina

F1 combina ambas en un solo número usando la media armónica:

F1 = 2 · (Precision · Recall) / (Precision + Recall)

¿Por qué armónica y no aritmética? Porque la armónica castiga los desequilibrios: con precision=1.0 y recall=0.02, la media aritmética daría un engañoso 0.51, pero F1 da ≈0.04, delatando que el modelo es inútil. F1 solo es alta si ambas son altas.

Nota (F-beta)

F1 pesa precision y recall por igual. Si tu negocio pesa distinto, usa F-beta: beta > 1 da más peso al recall (F2 es común en fraude/medicina), beta < 1 da más peso a la precision (F0.5 para spam). En sklearn: fbeta_score(y_test, y_pred, beta=2). Es la forma "de libro" de codificar la asimetría de costes en una sola métrica.

3.4 Curva ROC y AUC

Las métricas anteriores evalúan al modelo con un umbral concreto. La curva ROC evalúa al modelo en todos los umbrales a la vez: para cada umbral posible, dibuja el ratio de verdaderos positivos (recall) contra el ratio de falsos positivos.

  • AUC (Area Under the Curve) resume la curva en un número entre 0 y 1.
  • Interpretación probabilística elegante: AUC = probabilidad de que el modelo asigne mayor puntuación a un positivo aleatorio que a un negativo aleatorio.
  • AUC = 0.5 → el modelo no distingue nada (moneda al aire). AUC = 1.0 → separación perfecta. AUC < 0.5 → el modelo distingue... al revés (revisa tus etiquetas).
from sklearn.metrics import roc_curve, roc_auc_score, RocCurveDisplay
from sklearn.metrics import precision_recall_curve, average_precision_score

# ROC se calcula sobre PROBABILIDADES, no sobre predicciones binarias
probas = modelo.predict_proba(X_test)[:, 1]      # P(clase positiva) por muestra

auc = roc_auc_score(y_test, probas)              # Área bajo la curva ROC
print(f"ROC-AUC: {auc:.3f}")                     # 0.5=azar, 1.0=perfecto

fpr, tpr, umbrales = roc_curve(y_test, probas)   # Puntos de la curva para cada umbral
RocCurveDisplay(fpr=fpr, tpr=tpr).plot()         # Dibuja la curva
plt.plot([0, 1], [0, 1], "k--", label="Azar")    # Diagonal de referencia (AUC=0.5)
plt.legend(); plt.show()

# --- Curva Precision-Recall: la alternativa para desbalanceo fuerte ---
prec, rec, _ = precision_recall_curve(y_test, probas)  # Precision y recall por umbral
ap = average_precision_score(y_test, probas)           # Área bajo la curva PR
print(f"Average Precision (PR-AUC): {ap:.3f}")
# Referencia de la PR-curve: NO es 0.5, es la proporción de positivos
# (con 5% de fraude, un modelo aleatorio tiene AP ≈ 0.05)

Advertencia (cuándo NO fiarse de ROC-AUC)

Con desbalanceo fuerte (1% de positivos), la ROC puede verse estupenda (AUC 0.95) mientras el modelo genera 50 falsas alarmas por cada acierto. La razón: el eje X de la ROC (ratio de FP) se calcula sobre los negativos, que son tantísimos que absorben miles de FP sin que la curva se inmute. En esos casos usa la curva Precision-Recall, que sí sufre visiblemente con cada falsa alarma. Regla práctica: desbalanceo < 90/10 → ROC vale; desbalanceo fuerte → PR-curve.

3.5 classification_report interpretado línea a línea

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred, target_names=["Legítima", "Fraude"]))

Salida típica:

              precision    recall  f1-score   support

    Legítima       0.98      0.99      0.99       950
      Fraude       0.86      0.62      0.72        50

    accuracy                           0.97      1000
   macro avg       0.92      0.81      0.85      1000
weighted avg       0.97      0.97      0.97      1000

Lectura línea a línea:

  • Fila "Legítima": precision 0.98 = de todo lo que marcó como legítimo, el 98% lo era. recall 0.99 = detectó el 99% de las legítimas reales. Fácil: es la clase mayoritaria.
  • Fila "Fraude" (la que importa): precision 0.86 = cuando alerta de fraude, acierta el 86% de las veces (14% de falsas alarmas). recall 0.62 = solo caza el 62% de los fraudes reales; el 38% se le escapa. Aquí está el problema del modelo.
  • support: nº de muestras reales de cada clase en el test (950 vs 50: desbalanceo 95/5).
  • accuracy 0.97: alta, pero ya sabes que con este desbalanceo casi no informa.
  • macro avg: media simple de las métricas de ambas clases, tratándolas por igual. Al no ponderar por tamaño, delata el mal rendimiento en la clase minoritaria (recall macro 0.81 frente al 0.97 de accuracy).
  • weighted avg: media ponderada por support. Con desbalanceo se parece a la accuracy y hereda su ceguera. Útil solo como resumen global.

Consejo profesional

En problemas desbalanceados, entrena tu ojo para ir directo a la fila de la clase minoritaria y a la macro avg. Son las dos líneas que no se dejan maquillar por el desbalanceo.

3.6 Cómo traducir métricas a dinero

Caso empresarial

Un banco procesa 1.000.000 de transacciones/mes con un 0.5% de fraude (5.000 fraudes). Costes medidos por el negocio:

  • Cada FN (fraude no detectado): pérdida media de 200 €.
  • Cada FP (cliente legítimo bloqueado): coste operativo de revisión + fricción ≈ 5 €.

Comparamos dos modelos con la matriz de costes:

# Coste total = FN * coste_FN + FP * coste_FP
COSTE_FN = 200   # euros por fraude no detectado
COSTE_FP = 5     # euros por falsa alarma

def coste_mensual(recall, precision, n_fraudes=5000):
    """Traduce recall/precision a euros/mes."""
    tp = recall * n_fraudes                    # Fraudes detectados
    fn = n_fraudes - tp                        # Fraudes que se escapan
    fp = tp / precision - tp                   # De precision = TP/(TP+FP) -> FP = TP/prec - TP
    return fn * COSTE_FN + fp * COSTE_FP       # Coste total en euros

# Modelo A: alta precision, recall modesto
print(f"Modelo A (prec=0.90, rec=0.60): {coste_mensual(0.60, 0.90):,.0f} €/mes")
# Modelo B: precision más baja, recall alto
print(f"Modelo B (prec=0.50, rec=0.90): {coste_mensual(0.90, 0.50):,.0f} €/mes")

Resultado:

Modelo Fraudes cazados Fraudes escapados (FN) Falsas alarmas (FP) Coste FN Coste FP Coste total/mes
A (prec 0.90, rec 0.60) 3.000 2.000 333 400.000 € 1.667 € 401.667 €
B (prec 0.50, rec 0.90) 4.500 500 4.500 100.000 € 22.500 € 122.500 €

El modelo B tiene "peor pinta" en precision (¡la mitad de sus alarmas son falsas!) pero ahorra ~280.000 €/mes porque en este negocio el FN cuesta 40 veces más que el FP. Esta tabla, presentada a dirección, vale más que cualquier AUC. Esta es la habilidad que separa a un ingeniero de ML de alguien que ejecuta notebooks.

Ejercicio rápido 2

Un modelo de cribado de una enfermedad rara (prevalencia 2%) reporta accuracy 98%. ¿Es un buen modelo? ¿Qué dos números pedirías antes de opinar?

Ver solución No se puede saber: un modelo que prediga "sano" para todo el mundo también tiene accuracy 98% con prevalencia del 2%, y no detecta ni un solo enfermo. Pedirías (1) el **recall de la clase enferma** (¿qué % de enfermos reales detecta?) y (2) la **precision de la clase enferma** o directamente la matriz de confusión completa. Con esos números, y los costes de FP (pruebas extra) y FN (enfermedad sin tratar), podrías evaluar de verdad.

4. KNN: dime con quién andas

4.1 Qué es y su intuición

K-Nearest Neighbors es probablemente el algoritmo de clasificación más intuitivo que existe: "para clasificar un punto nuevo, mira los k puntos más parecidos que ya conoces y vota por mayoría". Dime con quién andas y te diré quién eres.

Existe porque materializa una idea profunda: la similitud predice la clase. Los clientes parecidos se comportan parecido; las transacciones parecidas a fraudes conocidos probablemente son fraude.

Cómo funciona por dentro:

  1. Entrenamiento: no hay. KNN simplemente memoriza el dataset completo (por eso se le llama lazy learner).
  2. Predicción: para cada punto nuevo, calcula la distancia (normalmente euclídea) a TODOS los puntos de entrenamiento, se queda con los k más cercanos y predice la clase mayoritaria entre ellos.

Esto invierte el coste habitual: entrenar es gratis, predecir es caro (buscar vecinos entre millones de puntos por cada predicción).

4.2 El hiperparámetro k

k Comportamiento Riesgo
k = 1 Copia la clase del vecino más cercano Overfitting: un solo outlier etiquetado mal contamina su vecindario
k pequeño (3-7) Frontera flexible, sensible al ruido local Varianza alta
k grande (50+) Frontera muy suave, vota "medio barrio" Underfitting: la clase mayoritaria global lo invade todo
k = N (todo el dataset) Siempre predice la clase mayoritaria Modelo inútil

Consejo profesional

Usa k impar en clasificación binaria (evita empates) y elígelo con validación cruzada. Valores típicos de partida: 5-15.

4.3 Por qué escalar es OBLIGATORIO

KNN vive de las distancias, y las distancias son ciegas a las unidades:

Cliente A: (edad=30, salario=30.000 €). Cliente B: (edad=60, salario=30.500 €). Distancia euclídea ≈ √(30² + 500²) ≈ 500.9 — el salario domina el 99.8% de la distancia. La edad, aunque sea la variable más predictiva, se vuelve invisible solo por estar medida en una escala menor.

Sin escalar, KNN clasifica usando básicamente la feature con números más grandes. Con StandardScaler, todas las features hablan con el mismo volumen.

from sklearn.neighbors import KNeighborsClassifier   # El clasificador KNN
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

# Pipeline: escalado + KNN. El escalado NO es opcional con KNN.
knn = Pipeline([
    ("scaler", StandardScaler()),                    # Media 0, desviación 1 por feature
    ("knn", KNeighborsClassifier(
        n_neighbors=7,        # k: número de vecinos que votan
        weights="distance",   # Vecinos más cercanos pesan más en el voto
        metric="euclidean",   # Distancia euclídea (la estándar para numéricas)
    )),
])
knn.fit(X_train, y_train)                # "Entrenar" = memorizar X_train escalado
print("Accuracy KNN:", knn.score(X_test, y_test))  # Evaluación rápida

4.4 La maldición de la dimensionalidad

En dimensiones altas (cientos o miles de features) ocurre algo antiintuitivo: todas las distancias tienden a parecerse. El volumen del espacio crece exponencialmente con las dimensiones, los puntos quedan esparcidos y el "vecino más cercano" está casi tan lejos como el más lejano. El concepto mismo de "cercanía" pierde significado, y KNN con él.

Consecuencia práctica: KNN funciona bien con pocas features informativas (menos de ~20-30) y muchas filas; se degrada rápido con features abundantes y ruidosas.

4.5 Cuándo sirve, cuándo no, y el puente hacia RAG

Úsalo cuando Evítalo cuando
Prototipo rápido / baseline sin supuestos Dataset con millones de filas (predicción lenta)
Pocas dimensiones, frontera de decisión irregular Alta dimensionalidad (maldición)
Los datos nuevos se parecen genuinamente a los históricos Necesitas interpretabilidad ("¿por qué?" → "porque tus vecinos...")
Sistemas de recomendación sencillos Producción con latencia estricta sin índices aproximados

Nota (puente al módulo 06-RAG)

La idea central de KNN —buscar los k elementos más similares en un espacio vectorial— es literalmente el motor de la búsqueda semántica moderna. En RAG, cada documento se convierte en un embedding (un vector), y recuperar contexto para el LLM es un KNN sobre millones de vectores, acelerado con índices aproximados (HNSW, FAISS) y usando similitud coseno en lugar de distancia euclídea. Cuando llegues al módulo 06, reconocerás este algoritmo con otro traje: KNN es el abuelo conceptual de toda base de datos vectorial.


5. Árboles de decisión: reglas que puedes leer

5.1 Qué es y por qué existe

Un árbol de decisión clasifica haciendo una secuencia de preguntas de sí/no sobre las features, como un diagrama de flujo aprendido automáticamente de los datos:

flowchart TD
    A["¿antigüedad < 6 meses?"] -->|Sí|B["¿nº quejas > 2?"]
    A -->|No|C["¿uso mensual < 3 h?"]
    B -->|Sí|D["CHURN <br/>(87% de los casos de esta hoja)"]
    B -->|No|E["NO CHURN <br/>(71%)"]
    C -->|Sí|F["CHURN <br/>(64%)"]
    C -->|No|G["NO CHURN <br/>(93%)"]

Existe porque resuelve dos carencias de los modelos lineales: captura no linealidades e interacciones sin ingeniería de features, y produce reglas legibles por humanos, oro puro en entornos donde hay que justificar cada decisión.

5.2 Cómo se construye: gini y entropía con ejemplo a mano

El algoritmo (CART) es codicioso: en cada nodo prueba todas las divisiones posibles (cada feature, cada punto de corte) y elige la que deja los grupos resultantes más "puros" (más homogéneos en clase). La pureza se mide con la impureza de Gini o la entropía.

Gini = 1 - Σ pᵢ²        (0 = nodo puro, 0.5 = mezcla 50/50 en binario)

Ejemplo numérico a mano. Nodo con 10 clientes: 6 churn (C) y 4 no-churn (N):

Gini inicial = 1 - (0.6² + 0.4²) = 1 - (0.36 + 0.16) = 0.48   (bastante mezclado)

Candidato A: dividir por "antigüedad < 6 meses"

  Izquierda (antigüedad < 6): 5 clientes → 5 C, 0 N
  Gini_izq = 1 - (1.0² + 0.0²) = 0.0        ← ¡nodo puro!

  Derecha (antigüedad ≥ 6): 5 clientes → 1 C, 4 N
  Gini_der = 1 - (0.2² + 0.8²) = 1 - 0.68 = 0.32

  Gini ponderado = (5/10)·0.0 + (5/10)·0.32 = 0.16
  Ganancia = 0.48 - 0.16 = 0.32  ✅

Candidato B: dividir por "es_premium"

  Izquierda (premium): 6 clientes → 3 C, 3 N → Gini = 1 - (0.5²+0.5²) = 0.50
  Derecha (no premium): 4 clientes → 3 C, 1 N → Gini = 1 - (0.75²+0.25²) = 0.375

  Gini ponderado = (6/10)·0.50 + (4/10)·0.375 = 0.45
  Ganancia = 0.48 - 0.45 = 0.03  ❌ (casi no separa)

El árbol elige el candidato A (ganancia 0.32 ≫ 0.03) y repite el proceso recursivamente en cada hijo hasta que los nodos son puros o se alcanza un límite. La entropía (criterion="entropy") es una medida alternativa basada en teoría de la información (la misma entropía que viste en el módulo 01, corazón de la cross-entropy); en la práctica ambas producen árboles casi idénticos y Gini es marginalmente más rápida.

5.3 Por qué solos hacen overfitting (y cómo frenarlos)

Sin límites, un árbol sigue dividiendo hasta que cada hoja tiene una sola muestra: memoriza el train al 100% (accuracy de entrenamiento perfecta) y aprende ruido en lugar de patrones. Es el modelo con más varianza de este capítulo. Los frenos:

Parámetro Qué limita Valor típico de partida
max_depth Profundidad máxima del árbol 3-8
min_samples_split Muestras mínimas para dividir un nodo 20-50
min_samples_leaf Muestras mínimas en cada hoja 10-30
max_leaf_nodes Nº máximo de hojas 10-50
ccp_alpha Poda por complejidad de coste (post-poda) Buscar con CV
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text

arbol = DecisionTreeClassifier(
    max_depth=4,           # Como mucho 4 preguntas encadenadas: legible y sin memorizar
    min_samples_leaf=20,   # Ninguna regla se apoya en menos de 20 clientes
    class_weight="balanced",  # Compensa el desbalanceo (ver sección 8)
    random_state=42,       # Los empates de división se resuelven igual siempre
)
arbol.fit(X_train, y_train)                  # NOTA: los árboles NO necesitan escalado
# (las divisiones son "feature < valor": invariantes a la escala)

# Visualización gráfica del árbol completo
plt.figure(figsize=(16, 8))
plot_tree(
    arbol,
    feature_names=nombres,        # Nombres reales de las columnas
    class_names=["No", "Sí"],     # Nombres de las clases
    filled=True,                  # Colorea por clase mayoritaria e intensidad por pureza
    rounded=True,
)
plt.show()

# Reglas en texto plano: exportables a un PDF para negocio o a un motor de reglas
print(export_text(arbol, feature_names=nombres))
# |--- antiguedad <= 5.50
# |   |--- num_quejas >  2.50
# |   |   |--- class: 1   ← regla extraíble: "nuevo + quejoso = riesgo de churn"

Consejo profesional

export_text es un arma secreta con stakeholders: conviertes el modelo en 10 reglas de negocio que un director sin formación técnica entiende en dos minutos. Ningún otro algoritmo de este capítulo permite eso con tanta claridad. A veces el "modelo" final que se despliega es literalmente esas reglas en un if/else.

5.4 Ventajas, desventajas, cuándo NO

Ventajas Desventajas
Interpretabilidad máxima: reglas legibles Overfitting severo sin poda
No necesita escalado ni supone linealidad Inestables: cambiar 5 filas puede cambiar todo el árbol
Maneja numéricas y categóricas (codificadas) Fronteras "escalonadas" (cortes perpendiculares a los ejes)
Captura interacciones automáticamente Un árbol solo rara vez es el mejor en precisión
Base de los mejores modelos tabulares (RF, XGBoost → cap. 5) Sesgo hacia features con muchos valores distintos

Cuándo NO: cuando buscas la máxima precisión predictiva con un solo modelo (un árbol solo casi siempre pierde contra la logística bien afinada o contra un ensemble) o cuando necesitas fronteras suaves y estables.

Ejercicio rápido 3

Calcula a mano la impureza de Gini de un nodo con 8 muestras: 2 positivas y 6 negativas. ¿Está más puro o menos puro que un nodo 50/50?

Ver solución `p_pos = 2/8 = 0.25`, `p_neg = 6/8 = 0.75`. `Gini = 1 - (0.25² + 0.75²) = 1 - (0.0625 + 0.5625) = 1 - 0.625 = 0.375`. Un nodo 50/50 tiene Gini = 1 - (0.25 + 0.25) = **0.5**, el máximo en binario. Como 0.375 < 0.5, el nodo 2/6 está **más puro** que el 50/50 (más cerca de una decisión clara).

6. Naive Bayes: el clasificador probabilístico

6.1 Intuición bayesiana

Naive Bayes aplica directamente el teorema de Bayes que estudiaste en el módulo 01: actualizar creencias con evidencia.

P(spam | palabras del email) ∝ P(palabras | spam) · P(spam)

En cristiano: "¿cómo de probable es que este email sea spam, dado que contiene estas palabras?" se calcula al revés, con cosas fáciles de contar en los datos: "¿con qué frecuencia aparecen estas palabras en los spams conocidos?" (verosimilitud) por "¿qué proporción de emails son spam en general?" (prior). El modelo calcula esto para cada clase y se queda con la más probable.

6.2 Por qué "naive" (ingenuo)

La suposición ingenua: todas las features son independientes entre sí dada la clase. Es decir, asume que ver la palabra "gratis" no te dice nada sobre si aparecerá "oferta" en el mismo email... lo cual es obviamente falso (van juntas constantemente).

¿Por qué funciona a pesar de una suposición tan falsa? Porque para clasificar no necesitas probabilidades exactas, solo necesitas que la clase correcta quede por encima de las demás. La independencia falsa distorsiona las probabilidades (suelen salir demasiado extremas, cercanas a 0 o 1) pero suele conservar el ranking entre clases. Clasifica bien aunque "crea" cosas falsas.

6.3 Su nicho clásico: texto y spam

Naive Bayes fue el algoritmo de los filtros de spam durante dos décadas, y sigue siendo un baseline excelente para clasificación de texto:

  • Con texto vectorizado por conteos, hay decenas de miles de features (una por palabra): la independencia ingenua encaja "menos mal" y el modelo escala sin esfuerzo.
  • Entrena casi instantáneamente (contar frecuencias, sin optimización iterativa) y funciona bien con pocos datos.

Variantes en sklearn: MultinomialNB (conteos de palabras — texto), BernoulliNB (features binarias — presencia/ausencia), GaussianNB (features continuas con supuesto gaussiano — tabular).

from sklearn.feature_extraction.text import CountVectorizer  # Texto -> matriz de conteos
from sklearn.naive_bayes import MultinomialNB                # NB para conteos
from sklearn.pipeline import Pipeline

# Mini-dataset de ejemplo (en la vida real: miles de emails etiquetados)
emails = [
    "gana dinero gratis ahora oferta limitada",   # spam
    "oferta exclusiva haz click gratis premio",   # spam
    "reunión del proyecto mañana a las 10",       # legítimo
    "te adjunto el informe trimestral revisado",  # legítimo
    "premio gratis click aquí urgente",           # spam
    "confirmo asistencia a la reunión del viernes",  # legítimo
]
etiquetas = [1, 1, 0, 0, 1, 0]   # 1 = spam, 0 = legítimo

pipe_nb = Pipeline([
    ("vectorizador", CountVectorizer()),  # Convierte cada email en un vector de conteos:
                                          # una columna por palabra del vocabulario
    ("nb", MultinomialNB(alpha=1.0)),     # alpha = suavizado de Laplace: evita probabilidad
                                          # CERO para palabras nunca vistas en una clase
                                          # (sin él, una sola palabra nueva vetaría la clase)
])
pipe_nb.fit(emails, etiquetas)            # "Entrenar" = contar frecuencias por clase

nuevo = ["click gratis para ganar tu premio"]
print(pipe_nb.predict(nuevo))             # [1] -> spam
print(pipe_nb.predict_proba(nuevo))       # Probabilidades (ojo: suelen ser extremas)

6.4 Ventajas, desventajas, cuándo NO

Ventajas Desventajas
Entrenamiento casi instantáneo, escala a millones de docs La suposición de independencia limita el techo de precisión
Excelente con pocas muestras de entrenamiento Probabilidades mal calibradas (demasiado extremas)
Baseline canónico para texto Flojo en tabular con features correlacionadas
Maneja altísima dimensionalidad sin inmutarse GaussianNB asume normalidad que rara vez se cumple

Cuándo NO: en datos tabulares con features fuertemente correlacionadas (usa logística o árboles), o cuando necesitas probabilidades fiables para decisiones de coste (recalibra con CalibratedClassifierCV o usa logística). Para clasificación de texto de máxima calidad en 2026, los embeddings + un clasificador ligero, o directamente un LLM, lo superan — pero NB sigue ganando en coste: gratis y en microsegundos.


7. SVM: el margen máximo

7.1 Intuición: no cualquier frontera, la MEJOR frontera

Muchas fronteras separan dos clases. La Support Vector Machine elige la que deja el pasillo más ancho posible entre ambas:

        Clase A                    Clase B
      x                                  o
        x        │←── margen ──→│      o
      x   x      ┆       │      ┆    o   o
        x        ┆       │      ┆      o
      x     [x]══┆═══════│══════┆══[o]     o
        x        ┆       │      ┆    o
      x          ┆       │      ┆  o    o
                 ┆   frontera   ┆
                 ┆   óptima     ┆
              vector         vector
              soporte        soporte

Los puntos [x] y [o] pegados al pasillo son los vectores de soporte: son los únicos que determinan la frontera. Podrías borrar todos los demás puntos y la frontera no se movería. Intuición: una frontera con margen ancho es la más "prudente" — un punto nuevo ligeramente desplazado sigue cayendo en el lado correcto. Maximizar el margen es una forma geométrica de regularizar.

El parámetro C (como en la logística) regula la tolerancia: C grande = margen estricto, intenta clasificar todo bien (riesgo de overfitting); C pequeño = margen blando, tolera errores a cambio de un pasillo más ancho (más generalización).

7.2 El kernel trick sin matemática pesada

¿Y si las clases no son separables por una recta? Ejemplo clásico: clase A dentro de un círculo, clase B fuera. Ninguna línea recta las separa en 2D... pero si añades una tercera dimensión z = x² + y² (distancia al centro), la clase A queda "abajo" y la B "arriba": un plano las separa perfectamente en 3D.

Esa es la idea del kernel trick: proyectar los datos a un espacio de más dimensiones donde SÍ son separables linealmente, pero —aquí la magia— sin calcular nunca las coordenadas en ese espacio. El kernel es una función que devuelve directamente la similitud entre pares de puntos "como si" vivieran en el espacio grande, a coste del espacio pequeño. Un atajo matemático, no fuerza bruta.

Kernel Frontera que produce Cuándo usarlo
linear Recta/hiperplano Alta dimensión (texto), muchos datos, baseline
rbf (radial) Curvas suaves arbitrarias El comodín por defecto para tabular no lineal
poly Polinómica Interacciones de orden conocido (poco común)
from sklearn.svm import SVC                      # Support Vector Classifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

svm = Pipeline([
    ("scaler", StandardScaler()),   # OBLIGATORIO: la SVM trabaja con distancias,
                                    # igual que KNN — sin escalar, la feature con
                                    # números grandes secuestra el margen
    ("svc", SVC(
        kernel="rbf",           # Kernel radial: fronteras no lineales suaves
        C=1.0,                  # Tolerancia margen/errores (buscar con CV: 0.1-100)
        gamma="scale",          # Alcance de influencia de cada punto
                                # gamma alto -> fronteras muy locales (overfitting)
                                # gamma bajo -> fronteras muy suaves (underfitting)
        class_weight="balanced",  # Compensa desbalanceo
        probability=True,       # Habilita predict_proba (coste extra: usa CV interna)
    )),
])
svm.fit(X_train, y_train)
print("Accuracy SVM:", svm.score(X_test, y_test))

7.3 Cuándo elegir SVM en 2026 (y cuándo no)

Honestidad de profesor: la SVM ha perdido protagonismo frente a los gradient boosting (capítulo 5) en tabular y frente a las redes en no estructurado. Pero conserva nichos reales:

Elegirla cuando Evitarla cuando
Dataset mediano (cientos a decenas de miles de filas) Millones de filas: el entrenamiento escala ~cuadráticamente. Inviable
Más features que muestras (bioinformática, genómica) Necesitas probabilidades directas y fiables
Frontera compleja con pocos datos (rbf brilla ahí) Necesitas interpretabilidad para negocio/regulador
Texto vectorizado con kernel lineal (LinearSVC, mucho más rápido) Quieres el mejor tabular sin pensar (gradient boosting suele ganar)

Nota

El concepto de margen máximo trasciende a la SVM: la idea de que separar con holgura generaliza mejor reaparece en las funciones de pérdida con margen del deep learning (triplet loss, contrastive loss) que se usan para entrenar los embeddings que verás en el módulo 06.


8. Desbalanceo de clases: el problema silencioso

Casi todos los problemas de clasificación valiosos son desbalanceados: fraude (~0.1-1%), churn mensual (~2-10%), impago (~2-5%), fallo de maquinaria (~0.5%). Si no lo tratas explícitamente, el modelo aprende la lección equivocada: "predecir siempre la clase mayoritaria sale rentable".

8.1 El arsenal, de lo simple a lo elaborado

1. stratify — siempre, sin excepción. Al hacer el split, preserva las proporciones de clase en train y test. Sin él, con 3% de positivos podrías acabar con un test casi sin positivos y métricas de lotería.

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2,
    stratify=y,          # Mismo % de cada clase en train y en test. SIEMPRE.
    random_state=42,
)

2. class_weight — la primera línea de defensa. Le dice al modelo que equivocarse en la clase minoritaria cuesta más durante el entrenamiento. Sin datos sintéticos, sin duplicados, una línea:

modelo = LogisticRegression(
    class_weight="balanced",  # Peso de cada clase inversamente proporcional a su frecuencia:
                              # con 95/5, cada error en la clase 5% pesa ~19x más
    max_iter=1000,
)
# También puedes pasar costes de negocio reales:
# class_weight={0: 1, 1: 40}   -> un FN cuesta 40 veces un FP (¡tu matriz de costes!)

3. Oversampling / Undersampling. Modificar el TRAIN para equilibrarlo:

  • Oversampling: duplicar (o generar) muestras de la minoritaria. No pierdes información; riesgo de overfitting sobre las repetidas.
  • Undersampling: descartar muestras de la mayoritaria. Rápido; tiras datos (aceptable solo si te sobran millones).

4. SMOTE (la idea). En vez de duplicar, genera minoritarios sintéticos: toma un punto minoritario, busca sus vecinos minoritarios (¡KNN otra vez!) e inventa puntos nuevos interpolando entre ellos. Puebla la región de la clase minoritaria con variedad en lugar de fotocopias.

# pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as ImbPipeline  # Pipeline especial de imblearn:
                                                       # aplica SMOTE SOLO al entrenar,
                                                       # NUNCA al predecir/evaluar

pipe_smote = ImbPipeline([
    ("scaler", StandardScaler()),                 # 1. Escalar
    ("smote", SMOTE(random_state=42)),            # 2. Sintetizar minoritarios (solo en train)
    ("clf", LogisticRegression(max_iter=1000)),   # 3. Entrenar sobre datos balanceados
])
pipe_smote.fit(X_train, y_train)   # SMOTE actúa aquí dentro, en el train
y_pred = pipe_smote.predict(X_test)  # El test llega INTACTO, con su desbalanceo real

Advertencia (la regla de oro del desbalanceo)

NUNCA balancees el test set. El test debe reflejar la realidad de producción, donde el fraude sigue siendo el 1%. Si aplicas SMOTE antes del split, o balanceas el test "para que las métricas se vean mejor", estás midiendo el modelo en un mundo de fantasía: las métricas serán espectaculares en el notebook y desastrosas en producción. SMOTE se aplica después del split y solo al train (el pipeline de imblearn lo garantiza incluso dentro de la validación cruzada).

8.2 Qué probar y en qué orden

flowchart TD
    A[Problema desbalanceado] --> B["1. stratify=y en el split<br/>(no negociable)"]
    B --> C["2. Métrica correcta:<br/>recall/precision/F-beta/PR-AUC<br/>(no accuracy)"]
    C --> D["3. class_weight='balanced'<br/>(gratis, casi siempre suficiente)"]
    D --> E{¿Sigue flojo en<br/>la minoritaria?}
    E -->|Sí|F["4. Ajustar umbral de decisión<br/>según coste FP/FN"]
    F --> G{¿Aún flojo?}
    G -->|Sí|H["5. SMOTE / over-under sampling<br/>(solo en train)"]
    H --> I["6. Más datos de la minoritaria<br/>o modelos más potentes (cap. 5)"]
    E -->|No|J[Listo ]
    G -->|No|J

Consejo profesional

El 90% de las veces, stratify + class_weight="balanced" + métrica adecuada + umbral ajustado resuelven el problema sin necesidad de SMOTE. Empieza simple; SMOTE es la herramienta de la etapa 5, no de la 1.


9. Tabla maestra comparativa

La tabla para elegir clasificador que querrás tener a mano toda tu carrera:

Criterio Reg. logística KNN Árbol de decisión Naive Bayes SVM (rbf)
Interpretabilidad (coeficientes/odds) ("tus vecinos") (reglas legibles) (probabilidades por feature) (caja casi negra)
Velocidad de entrenamiento (no entrena)
Velocidad de predicción (busca en todo el train)
Escalabilidad (millones de filas) (inviable)
¿Necesita escalado? Sí (recomendado) Sí (obligatorio) No No Sí (obligatorio)
¿Maneja no linealidad? No (sin ing. de features) Parcial Sí (kernel)
Probabilidades fiables (extremas) (requiere calibración)
Robustez a features irrelevantes (con L1) (maldición dim.)
Caso de uso típico Baseline universal, scoring regulado Prototipos, similitud, recomendación Reglas de negocio, segmentación explicable Texto/spam, baselines NLP Datasets medianos alta dimensión, bioinformática

Consejo profesional (receta de decisión 2026)

(1) Empieza SIEMPRE con regresión logística como baseline. (2) Si necesitas reglas explicables, añade un árbol podado. (3) Si necesitas más precisión en tabular, salta a Random Forest / Gradient Boosting (capítulo 5) — no a SVM ni KNN. (4) NB para texto rápido y barato; KNN cuando el problema sea genuinamente de similitud. Cualquier modelo complejo debe ganarse su complejidad superando claramente al baseline.


10. Ejemplo integrador: detección de churn de principio a fin

Proyecto completo y realista: una empresa de telecomunicaciones quiere predecir qué clientes se darán de baja (churn) para lanzar una campaña de retención. Presupuesto limitado: la retención (llamada + descuento) cuesta 20 € por cliente contactado; perder un cliente cuesta de media 300 € de LTV.

10.1 Generar un dataset sintético realista

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)     # Generador de aleatorios reproducible
n = 600                              # 600 clientes: dataset pequeño pero realista

# --- Simulamos las features con distribuciones plausibles ---
antiguedad = rng.integers(1, 72, n)              # Meses como cliente (1 a 72)
factura_mensual = rng.normal(55, 20, n).clip(15, 120)  # Euros/mes, entre 15 y 120
num_quejas = rng.poisson(0.8, n)                 # Nº de quejas (Poisson: muchas con 0)
uso_datos_gb = rng.exponential(8, n).clip(0, 60) # GB/mes (exponencial: mayoría bajo)
tiene_permanencia = rng.integers(0, 2, n)        # 0/1: contrato con permanencia

# --- La probabilidad REAL de churn depende de las features (patrón que el modelo debe hallar) ---
logit = (
    -2.2                          # Intercepto: churn base bajo
    - 0.04 * antiguedad           # Más antigüedad -> menos churn (fidelidad)
    + 0.02 * factura_mensual      # Factura alta -> más churn (sensibilidad al precio)
    + 0.85 * num_quejas           # Cada queja aumenta MUCHO el riesgo
    - 0.03 * uso_datos_gb         # Quien usa mucho el servicio, se queda
    - 1.1 * tiene_permanencia     # La permanencia retiene (contractualmente)
)
p_churn = 1 / (1 + np.exp(-logit))               # Sigmoide: de logit a probabilidad
y = rng.binomial(1, p_churn)                     # Etiqueta real: 1 = se dio de baja

df = pd.DataFrame({
    "antiguedad": antiguedad, "factura_mensual": factura_mensual,
    "num_quejas": num_quejas, "uso_datos_gb": uso_datos_gb,
    "tiene_permanencia": tiene_permanencia, "churn": y,
})
print(df["churn"].value_counts(normalize=True))
# ≈ 0: 0.83 / 1: 0.17  -> desbalanceo realista: ~17% de churn

10.2 Split estratificado y baseline

from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyClassifier
from sklearn.metrics import classification_report, f1_score

X = df.drop(columns="churn")     # Features: todo menos la etiqueta
y = df["churn"]                  # Etiqueta a predecir

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25,        # 150 clientes reservados para la evaluación final
    stratify=y,                  # Preserva el ~17% de churn en ambos conjuntos
    random_state=42,
)

# BASELINE: el modelo más tonto posible. Todo lo que hagamos debe superarlo.
dummy = DummyClassifier(strategy="most_frequent")   # Predice SIEMPRE "no churn"
dummy.fit(X_train, y_train)
print("Accuracy dummy:", dummy.score(X_test, y_test))   # ≈ 0.83 ¡sin aprender nada!
print("F1 dummy:", f1_score(y_test, dummy.predict(X_test)))  # 0.0: no caza ni un churn
# Lección: 83% de accuracy es el SUELO aquí, no un logro. Por eso F1, no accuracy.

10.3 Regresión logística: el candidato principal

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

pipe_log = Pipeline([
    ("scaler", StandardScaler()),                 # Escala para comparar coeficientes
    ("clf", LogisticRegression(
        class_weight="balanced",                  # Compensa el 83/17
        max_iter=1000,
        random_state=42,
    )),
])
pipe_log.fit(X_train, y_train)                    # Entrena el pipeline completo

print(classification_report(y_test, pipe_log.predict(X_test),
                            target_names=["Se queda", "Churn"]))
# Miramos SOBRE TODO la fila "Churn": su recall y precision

Salida típica:

              precision    recall  f1-score   support
    Se queda       0.94      0.82      0.88       125
       Churn       0.48      0.76      0.59        25

Lectura: caza el 76% de los churners (recall alto, bien: gracias a class_weight), pero la mitad de sus alertas son falsas (precision 0.48). ¿Es aceptable? Depende del coste — lo calculamos en 10.6.

10.4 Árbol de decisión: el candidato explicable

from sklearn.tree import DecisionTreeClassifier, export_text

arbol = DecisionTreeClassifier(
    max_depth=4, min_samples_leaf=15,    # Frenos anti-overfitting
    class_weight="balanced", random_state=42,
)
arbol.fit(X_train, y_train)              # Sin escalado: los árboles no lo necesitan

print(classification_report(y_test, arbol.predict(X_test),
                            target_names=["Se queda", "Churn"]))

# El regalo del árbol: reglas para el equipo de retención
print(export_text(arbol, feature_names=list(X.columns)))
# Ejemplo de regla emergente:
# |--- num_quejas > 1.5
# |   |--- antiguedad <= 12.5  -> churn (cliente nuevo Y quejoso = máximo riesgo)

10.5 Random Forest (mención) y comparación honesta

from sklearn.ensemble import RandomForestClassifier   # Adelanto del capítulo 5:
                                                      # ¡muchos árboles votando juntos!
rf = RandomForestClassifier(
    n_estimators=300,            # 300 árboles, cada uno con datos/features distintos
    class_weight="balanced",
    random_state=42,
)
rf.fit(X_train, y_train)

# Comparamos los tres candidatos con la MISMA métrica (F1 de la clase churn)
for nombre, m in [("Logística", pipe_log), ("Árbol", arbol), ("Random Forest", rf)]:
    print(f"{nombre}: F1={f1_score(y_test, m.predict(X_test)):.3f}")

En este dataset (relación subyacente ~lineal y solo 600 filas), la logística suele empatar o ganar al Random Forest. Moraleja importante: más complejidad no garantiza más rendimiento; con pocos datos y patrones simples, el modelo simple e interpretable gana el desempate siempre.

10.6 Elegir métrica y umbral SEGÚN EL NEGOCIO

# Costes reales del negocio:
COSTE_CONTACTO = 20    # € por cliente contactado (FP y TP los pagan)
LTV_PERDIDO = 300      # € por churner no detectado (FN); asumimos retención efectiva
                       # (simplificación didáctica; en la práctica multiplicarías
                       #  por la tasa de éxito de la campaña, p. ej. 40%)

from sklearn.metrics import confusion_matrix
probas = pipe_log.predict_proba(X_test)[:, 1]    # P(churn) por cliente

print(f"{'Umbral':>7} {'TP':>4} {'FP':>4} {'FN':>4} {'Coste total':>12}")
mejor = (None, float("inf"))
for umbral in [0.3, 0.4, 0.5, 0.6, 0.7]:
    y_u = (probas >= umbral).astype(int)              # Decisión con este umbral
    tn, fp, fn, tp = confusion_matrix(y_test, y_u).ravel()  # Desempaquetar la matriz
    coste = (tp + fp) * COSTE_CONTACTO + fn * LTV_PERDIDO   # Contactos + clientes perdidos
    print(f"{umbral:>7} {tp:>4} {fp:>4} {fn:>4} {coste:>10} €")
    if coste < mejor[1]:
        mejor = (umbral, coste)                       # Guardamos el umbral más barato
print(f"\nUmbral óptimo por coste: {mejor[0]} ({mejor[1]} €)")
# Con FN (300€) 15 veces más caro que un contacto (20€), el umbral óptimo
# cae muy por debajo de 0.5: compensa contactar de más antes que perder clientes.

Advertencia

En un proyecto real este barrido de umbral se hace sobre un conjunto de validación separado (o con CV), y el test se toca una sola vez al final. Aquí lo simplificamos por espacio didáctico — no repliques este atajo en producción.

10.7 Comunicar resultados (la parte que te hace valioso)

Lo que NO se presenta a dirección: "el modelo alcanza F1 0.59 con AUC 0.84". Lo que SÍ:

Propuesta: Con el modelo de churn contactando a los clientes con riesgo > 30%: - Detectamos ~8 de cada 10 clientes que se habrían ido. - La campaña contacta ~40 clientes/mes por cada 150, con un coste de ~800 €. - Frente a no hacer nada, estimamos retener ~5.700 € de LTV/mes netos. - Los tres factores de riesgo dominantes son: nº de quejas, baja antigüedad y ausencia de permanencia → recomendamos además atacar la causa raíz de las quejas.

Métricas → decisiones → euros. Ese es el formato.

Ejercicio rápido 4

En el barrido de umbral de 10.6, ¿por qué el umbral óptimo por coste es más bajo que 0.5? ¿Qué pasaría con el umbral óptimo si el contacto costase 200 € en vez de 20 €?

Ver solución Es más bajo porque el FN (300 €) cuesta 15 veces más que un contacto (20 €): compensa "disparar" ante riesgos moderados, aceptando falsos positivos baratos con tal de no perder clientes caros. Si el contacto subiera a 200 € (casi el coste del FN), los FP dejarían de ser baratos: el umbral óptimo **subiría** notablemente (hacia 0.5-0.6), contactando solo a clientes con riesgo alto y asumiendo más fugas. El umbral óptimo es, siempre, una función del ratio de costes FP/FN.

11. Caso empresarial: scoring de crédito en una fintech

Caso empresarial

FinCredia (nombre ficticio), fintech europea de préstamos al consumo, procesa 40.000 solicitudes/mes. Su equipo de datos se enfrenta a la decisión de arquitectura del modelo de scoring: ¿regresión logística o un gradient boosting que en validación mejora el AUC de 0.79 a 0.82?

El contexto regulatorio decide. En la UE, la normativa de crédito y el AI Act (los sistemas de scoring crediticio están clasificados como alto riesgo) exigen: explicar cada denegación al solicitante, demostrar ausencia de discriminación por variables protegidas, y auditorías del modelo. El GDPR añade el derecho a explicación de decisiones automatizadas.

La decisión: logística + reglas, no la caja opaca.

  1. Modelo champion: regresión logística sobre ~25 features cuidadosamente construidas (ratios de endeudamiento, historial, estabilidad de ingresos). Cada coeficiente tiene signo y magnitud justificables ante el regulador. Una denegación se explica en lenguaje natural: "ratio deuda/ingresos por encima del umbral (odds ×2.1) y antigüedad laboral insuficiente (odds ×1.6)".
  2. Capa de reglas duras delante del modelo (extraídas en parte de árboles de decisión analizados por riesgo): límites regulatorios y de política de crédito que ningún modelo puede saltarse (edad mínima, importe máximo por perfil, exclusiones legales).
  3. El gradient boosting queda como challenger en shadow mode: puntúa en paralelo sin decidir, para cuantificar durante meses cuánto negocio "cuesta" la interpretabilidad. El comité decidió que +0.03 de AUC no compensaba el riesgo regulatorio ni la pérdida de explicabilidad. (En 2026 existen técnicas de explicabilidad post-hoc como SHAP —las verás más adelante— pero para el regulador de FinCredia una explicación aproximada de un modelo opaco no equivale a un modelo transparente.)
  4. Umbral con matriz de costes: aprobar a un moroso cuesta ~8× más que rechazar a un buen pagador (pérdida esperada vs margen no ganado). El umbral se fija en el punto que maximiza el beneficio esperado en validación, y se revisa trimestralmente porque la economía (tipos de interés, desempleo) mueve los costes.
  5. Monitorización en producción: dashboard con (a) distribución de probabilidades predichas semana a semana — si se desplaza, hay drift en la población de solicitantes; (b) tasa real de impago por decil de score — si el decil "seguro" empieza a impagar, el modelo se está quedando obsoleto; (c) métricas de equidad entre grupos demográficos, exigidas en auditoría; (d) recalibración/reentrenamiento programado cada 6 meses con datos frescos.

Moraleja del caso: la elección de modelo casi nunca es "el de mejor métrica". Es la intersección de métrica × interpretabilidad × regulación × coste operativo × mantenibilidad. La regresión logística lleva 60 años en producción bancaria precisamente porque optimiza esa intersección, no una sola dimensión.


12. Buenas prácticas

  1. Baseline primero, siempre: DummyClassifier + regresión logística antes que cualquier cosa sofisticada. Todo modelo posterior debe ganarse su complejidad.
  2. Elige la métrica ANTES de entrenar, derivada de los costes de negocio de FP y FN. La métrica es una decisión de producto, no un tecnicismo.
  3. stratify=y en todos los splits de clasificación, y StratifiedKFold en validación cruzada (sklearn lo usa por defecto en CV de clasificadores).
  4. Trabaja con predict_proba y ajusta el umbral en validación según la matriz de costes; documenta el umbral elegido y su justificación.
  5. Pipelines para todo (Pipeline de sklearn): escalado, sampling y modelo encapsulados juntos. Elimina fugas de datos y errores de "olvidé escalar el test".
  6. Mira la matriz de confusión completa, no un solo número resumen. Los números resumen esconden; la matriz enseña.
  7. Escala obligatoriamente para KNN y SVM, recomendablemente para logística; los árboles no lo necesitan.
  8. Con desbalanceo: métrica adecuada → class_weight → umbral → SMOTE, en ese orden de escalada.
  9. Reporta intervalos, no puntos: con test sets pequeños (como 150 filas), un F1 de 0.59 tiene una incertidumbre enorme; usa validación cruzada para estimaciones estables.
  10. Deja un rastro reproducible: random_state fijado, versiones de librerías anotadas, y el porqué de cada decisión (métrica, umbral, modelo) escrito donde tu yo futuro lo encuentre.

13. Malas prácticas

  1. Reportar accuracy en problemas desbalanceados sin acompañarla de la matriz de confusión. Es el pecado capital de este capítulo.
  2. Usar el umbral 0.5 por inercia sin preguntarse jamás por los costes relativos de FP y FN.
  3. Aplicar SMOTE (o cualquier sampling) antes del split o al test set: métricas de fantasía, producción de pesadilla.
  4. Escalar con estadísticas calculadas sobre todo el dataset (fuga de datos): el scaler se ajusta solo con train.
  5. Elegir hiperparámetros o umbral mirando el test: convierte el test en un segundo train y destruye tu estimación de generalización.
  6. Entrenar árboles sin límites de profundidad y presumir del 100% de accuracy en train.
  7. Fiarse de las probabilidades de Naive Bayes o SVM sin calibrar para decisiones sensibles al coste.
  8. Elegir el modelo por moda ("usemos deep learning") en un tabular de 600 filas donde una logística gana.
  9. Presentar a negocio AUCs y F1s crudos sin traducción a decisiones y euros.
  10. Ignorar la monitorización post-despliegue: un clasificador es una foto de los datos de ayer; el mundo se mueve.

14. Errores comunes

# Error Síntoma Solución
1 Accuracy alta que "miente" con desbalanceo 97% de accuracy, 0 positivos detectados Matriz de confusión + recall/precision/F1 por clase
2 Olvidar escalar antes de KNN/SVM Rendimiento misteriosamente malo; una feature domina Pipeline con StandardScaler
3 SMOTE antes del split Métricas de validación irrealmente altas Sampling solo en train, con imblearn.pipeline
4 Split sin stratify Métricas que bailan entre ejecuciones; test casi sin positivos stratify=y siempre en clasificación
5 Confundir el orden de la matriz de confusión de sklearn TP y TN intercambiados en el informe Recordar: [[TN, FP], [FN, TP]]; verificar con .ravel()
6 Usar ROC-AUC con desbalanceo extremo AUC 0.95 pero precision inutilizable Curva Precision-Recall y average precision
7 Árbol sin podar Train 100%, test mediocre (gap enorme) max_depth, min_samples_leaf, o CV sobre ccp_alpha
8 LogisticRegression no converge Warning ConvergenceWarning Escalar las features y/o subir max_iter
9 Interpretar coeficientes sin escalar "La feature X es la más importante" (falso: solo tiene otra escala) Escalar antes, comparar en exp(coef)
10 Ajustar el umbral en el test set Métricas finales optimistas, producción decepcionante Umbral en validación; test solo para la foto final
11 Tratar multilabel como multiclase "Necesito una clase comedia-romance-acción..." MultiOutputClassifier o clasificadores multilabel
12 Evaluar una sola vez con test pequeño F1 que cambia ±0.15 al cambiar la semilla Validación cruzada estratificada, reportar media ± std

15. FAQ — Preguntas frecuentes

1. ¿Por qué la regresión logística se llama "regresión" si es un clasificador? Porque técnicamente hace una regresión: predice un valor continuo (la probabilidad de la clase positiva, vía la sigmoide sobre una combinación lineal). La clasificación es un paso posterior y externo al modelo: comparar esa probabilidad con un umbral. El nombre es herencia estadística de los años 40-50 y ya nadie lo va a cambiar.

2. ¿Qué umbral de decisión debo usar? El que minimice el coste (o maximice el beneficio) esperado de negocio, calculado en un conjunto de validación con tu matriz de costes FP/FN. El 0.5 por defecto solo es óptimo si ambos errores cuestan lo mismo, lo cual casi nunca es cierto. Si no tienes costes, maximiza F1 (o F-beta con el beta que refleje tu prioridad) en validación.

3. ¿Accuracy nunca sirve para nada? Sí sirve: con clases razonablemente balanceadas (60/40 o mejor) y costes de error simétricos, es una métrica legítima y fácil de comunicar. El problema es usarla como métrica única en problemas desbalanceados, que son la mayoría de los valiosos.

4. ¿Cuándo ROC-AUC y cuándo la curva Precision-Recall? ROC-AUC para comparar la capacidad discriminativa general de modelos con desbalanceo moderado. PR-curve (y average precision) cuando la clase positiva es rara (<10%) y lo que te importa es la calidad de las alertas: la PR-curve castiga los falsos positivos que la ROC apenas nota.

5. ¿class_weight="balanced" o SMOTE? Empieza por class_weight: es gratis, no inventa datos, funciona dentro de cualquier CV sin trampas y suele bastar. SMOTE es útil cuando la minoritaria es tan escasa que el modelo apenas tiene ejemplos de los que aprender la región, pero añade complejidad y riesgo de sintetizar puntos irreales. Nunca ambos a ciegas: prueba, mide, compara.

6. ¿Por qué mi regresión logística lanza ConvergenceWarning? Casi siempre: features sin escalar con rangos muy dispares, que hacen la optimización lenta y mal condicionada. Solución: StandardScaler en un pipeline y, si persiste, max_iter=1000 o superior. Ignorar el warning significa usar un modelo a medio entrenar.

7. ¿Un árbol de decisión puede darme probabilidades? Sí: predict_proba devuelve la proporción de cada clase en la hoja donde cae la muestra. Pero son probabilidades toscas (pocas hojas = pocos valores posibles) y suelen estar mal calibradas. Para decisiones sensibles a la probabilidad, usa logística o calibra con CalibratedClassifierCV.

8. Con LLMs que clasifican texto con un prompt, ¿siguen teniendo sentido estos algoritmos en 2026? Totalmente. Para tabular (fraude, churn, scoring), los LLMs no son competitivos: estos modelos (y los ensembles del capítulo 5) son el estándar. Para texto, un LLM clasifica bien sin entrenamiento, pero a un coste por predicción órdenes de magnitud mayor: con volumen alto, una logística sobre embeddings o un NB baratísimo ganan el caso de negocio. Y las métricas de este capítulo (precision, recall, matriz de confusión) son exactamente las que usarás para evaluar clasificadores basados en LLM.

9. ¿Cómo manejo un problema multiclase con estos algoritmos? Casi todo es automático en sklearn: logística usa softmax (multinomial), árboles, KNN y NB son multiclase nativos, y SVM usa one-vs-one internamente. Lo que cambia es la evaluación: la matriz de confusión pasa a ser N×N y debes decidir cómo promediar las métricas (average="macro" trata las clases por igual; "weighted" pondera por frecuencia).

10. ¿Qué hago si negocio no sabe decirme los costes de FP y FN? Ayúdales a estimarlos aunque sea a grosso modo ("¿qué es peor, X o Y? ¿cuántas veces peor?"): incluso un ratio aproximado (1:10) es infinitamente mejor que el 1:1 implícito del umbral 0.5. Presenta 2-3 escenarios de umbral con sus consecuencias operativas (nº de alertas/día, % capturado) y deja que elijan: la elección del umbral es SU decisión con TU información.

16. Resumen del capítulo

  • Clasificar es predecir categorías, y el corazón del problema es que los dos errores (FP y FN) casi nunca cuestan lo mismo. Todo lo demás —métricas, umbrales, elección de modelo— se deriva de esa asimetría.
  • La regresión logística es el algoritmo de referencia: combinación lineal + sigmoide = probabilidad. Interpreta con odds ratios, regula con C, y recuerda que el umbral lo decides tú, no el modelo.
  • Las métricas son la sección más importante: la accuracy miente con desbalanceo; precision (calidad de alarmas) y recall (cobertura) capturan el tradeoff; F1/F-beta los combinan; ROC-AUC para desbalanceo moderado, PR-curve para el fuerte; y el classification_report se lee empezando por la clase minoritaria.
  • KNN clasifica por similitud (escala siempre, teme a la alta dimensionalidad) y es el antecesor conceptual de la búsqueda vectorial de RAG. Los árboles dan reglas legibles pero necesitan poda. Naive Bayes domina el nicho texto/spam con velocidad imbatible. SVM maximiza el margen y brilla en datasets medianos de alta dimensión, no en millones de filas.
  • Desbalanceo: stratify siempre, class_weight como primera arma, umbral ajustado por costes, SMOTE solo en train y solo si hace falta. Nunca balancees el test.
  • La habilidad diferencial: traducir métricas a dinero y comunicar el modelo como decisiones de negocio. Un F1 no convence a un comité; una tabla de costes evitados, sí.
  • Próximo capítulo: aprendizaje no supervisado — qué hacer cuando no tienes etiquetas.

17. Bibliografía y recursos

  • Scikit-learn — Supervised learning (guía oficial): https://scikit-learn.org/stable/supervised_learning.html
  • Scikit-learn — Model evaluation (métricas): https://scikit-learn.org/stable/modules/model_evaluation.html
  • Aurélien Géron — Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow (3ª ed., O'Reilly, 2022), capítulos 3-5: https://www.oreilly.com/library/view/hands-on-machine-learning/9781098125967/
  • James, Witten, Hastie, Tibshirani — An Introduction to Statistical Learning (gratuito en PDF), capítulo 4 (clasificación): https://www.statlearning.com/
  • Google Machine Learning Crash Course — Classification: https://developers.google.com/machine-learning/crash-course/classification
  • imbalanced-learn — documentación oficial (SMOTE y sampling): https://imbalanced-learn.org/stable/
  • StatQuest (Josh Starmer) — Logistic Regression, ROC/AUC, Decision Trees (vídeos): https://www.youtube.com/@statquest
  • Chawla et al. (2002) — SMOTE: Synthetic Minority Over-sampling Technique (paper original): https://arxiv.org/abs/1106.1813
  • Davis & Goadrich (2006) — The Relationship Between Precision-Recall and ROC Curves: https://www.biostat.wisc.edu/~page/rocpr.pdf
  • Scikit-learn — Probability calibration: https://scikit-learn.org/stable/modules/calibration.html

Anterior Módulo Siguiente
Capítulo 2: Regresión 02-MACHINE-LEARNING Capítulo 4: Aprendizaje no supervisado