Saltar a contenido

Capítulo 6: Ensembles y boosting — lo que realmente gana en datos tabulares

Módulo 02 — MACHINE LEARNING · AI Master Academy

"Ninguno de nosotros es tan inteligente como todos nosotros juntos." — Ken Blanchard

En el capítulo anterior aprendiste a construir pipelines robustos, a validar con rigor y a buscar hiperparámetros sin engañarte a ti mismo. En este capítulo llegamos al territorio donde el machine learning clásico gana dinero de verdad: los ensembles y, en particular, el gradient boosting.

Si miras las competiciones de Kaggle sobre datos tabulares de la última década, o los sistemas de scoring crediticio, detección de fraude, pricing de seguros y predicción de churn que funcionan hoy en producción en miles de empresas, encontrarás una y otra vez los mismos nombres: Random Forest, XGBoost, LightGBM, CatBoost. No es casualidad ni moda: hay razones matemáticas y prácticas profundas, y en este capítulo las vas a entender todas.


Índice

  1. La idea de los ensembles: sabiduría de las multitudes
  2. Bagging: bootstrap y reducción de varianza
  3. Random Forest a fondo
  4. Boosting: aprender de los errores
  5. Las implementaciones modernas: HistGB, XGBoost, LightGBM, CatBoost
  6. Early stopping a fondo
  7. Stacking y Voting
  8. ¿Por qué no deep learning para tablas?
  9. Interpretabilidad de ensembles: SHAP
  10. Ejemplo integrador: predicción de impago
  11. Caso empresarial: de reglas manuales a LightGBM en una aseguradora
  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. La idea de los ensembles: sabiduría de las multitudes

1.1 La analogía del jurado

Imagina un juicio con un solo juez. Por muy competente que sea, tiene sus sesgos, sus días malos, sus puntos ciegos. Ahora imagina un jurado de 12 personas: cada miembro es imperfecto individualmente, pero si cada uno acierta un poco más de la mitad de las veces y sus errores no están correlacionados (cada uno se equivoca por razones distintas), la decisión por mayoría es dramáticamente más fiable que la de cualquier miembro individual.

Este resultado tiene nombre y apellidos: es el teorema del jurado de Condorcet (1785). Hagamos los números para que no quede en metáfora:

  • Supón 11 "jueces" (clasificadores) independientes, cada uno con probabilidad de acierto p = 0.6 (apenas mejor que una moneda).
  • La probabilidad de que la mayoría (6 o más) acierte es la suma binomial:
P(mayoría acierta) = Σ (k=6..11) C(11,k) · 0.6^k · 0.4^(11-k) ≈ 0.753
  • Con 101 clasificadores independientes de p = 0.6, la mayoría acierta con probabilidad ≈ 0.979.

De clasificadores mediocres (60 %) a un sistema casi infalible (98 %), solo combinando. Eso es un ensemble: un comité de modelos cuyas predicciones se agregan (por votación, promedio o un meta-modelo).

1.2 Las dos condiciones para que funcione

El truco de Condorcet tiene letra pequeña. Para que un ensemble mejore a sus miembros se necesitan exactamente dos condiciones:

Condición Qué significa Qué pasa si falla
Mejor que el azar Cada modelo base debe tener accuracy > 50 % (en binario balanceado); debe contener algo de señal Si los miembros son peores que el azar, la mayoría amplifica el error en vez de corregirlo
Diversidad Los errores de los modelos deben estar poco correlacionados: cada uno debe equivocarse en ejemplos distintos Si todos los modelos son clones, la votación es idéntica a un solo modelo: cero ganancia

Nota

la diversidad es el ingrediente escaso. Entrenar 100 veces el mismo árbol sobre los mismos datos produce 100 árboles idénticos. Toda la ingeniería de los ensembles (bootstrap, submuestreo de features, pesos sobre errores, semillas distintas) existe para fabricar diversidad artificialmente sin destruir la señal.

1.3 Sesgo y varianza, revisitados

Recuerda la descomposición del error que vimos en el capítulo 3:

Error esperado = Sesgo² + Varianza + Ruido irreducible

Los ensembles atacan este error por dos caminos distintos, y esa distinción organiza todo el capítulo:

  • Bagging (y Random Forest) → ataca la varianza. Toma modelos de bajo sesgo y alta varianza (árboles profundos, que memorizan) y promedia muchos de ellos entrenados sobre datos ligeramente distintos. El promedio de variables aleatorias poco correlacionadas tiene menor varianza que cada una: si tienes B modelos con varianza σ² y correlación media ρ entre ellos, la varianza del promedio es aproximadamente ρσ² + (1-ρ)σ²/B. Con B grande, el segundo término desaparece; lo único que queda es la parte correlacionada — por eso reducir ρ (diversidad) es tan importante como aumentar B.
  • Boosting → ataca el sesgo (y de paso la varianza). Toma modelos de alto sesgo y baja varianza (árboles diminutos, "stumps" o árboles de profundidad 3-6) y los encadena secuencialmente, cada uno corrigiendo los errores residuales del conjunto anterior. El sesgo baja paso a paso.
flowchart TB
    subgraph BAGGING["BAGGING — paralelo, reduce VARIANZA"]
        D1[Dataset original] --> B1[Muestra bootstrap 1]
        D1 --> B2[Muestra bootstrap 2]
        D1 --> B3[Muestra bootstrap N]
        B1 --> A1[Árbol profundo 1]
        B2 --> A2[Árbol profundo 2]
        B3 --> A3[Árbol profundo N]
        A1 --> V[Votación / Promedio]
        A2 --> V
        A3 --> V
        V --> P1[Predicción final]
    end
    subgraph BOOSTING["BOOSTING — secuencial, reduce SESGO"]
        D2[Dataset original] --> M1[Árbol pequeño 1]
        M1 -->|residuos / errores|M2[Árbol pequeño 2]
        M2 -->|residuos / errores|M3[Árbol pequeño 3]
        M3 -->|residuos / errores|M4[Árbol pequeño M]
        M4 --> S[Suma ponderada por learning rate]
        S --> P2[Predicción final]
    end

Consejo profesional

memoriza esta regla nemotécnica: bagging = democracia en paralelo (todos votan a la vez, nadie escucha a nadie), boosting = relevo secuencial (cada corredor empieza donde el anterior falló). Cuando dudes de qué familia usar: si tu modelo base sobreajusta → bagging; si infraajusta → boosting.

1.4 Ejercicio rápido 1

Tienes 3 clasificadores binarios independientes, cada uno con accuracy del 70 %. ¿Cuál es la accuracy del ensemble por votación mayoritaria (gana la clase con 2 o más votos)?

Ver solución La mayoría acierta si aciertan los 3, o si aciertan exactamente 2: - P(3 aciertos) = 0.7³ = 0.343 - P(exactamente 2 aciertos) = C(3,2) · 0.7² · 0.3 = 3 · 0.49 · 0.3 = 0.441 Total = 0.343 + 0.441 = **0.784 → 78.4 %** De tres modelos del 70 % obtenemos un sistema del 78.4 %, *sin datos nuevos ni modelos mejores*, solo con diversidad e independencia. Nota que si los 3 modelos fueran copias idénticas (correlación 1), el ensemble seguiría en 70 %: la ganancia viene íntegramente de la independencia de los errores.

2. Bagging: bootstrap y reducción de varianza

2.1 El bootstrap explicado con un ejemplo numérico

Bagging = Bootstrap AGGregatING (Breiman, 1996). El ingrediente central es el muestreo bootstrap: dado un dataset de n filas, generamos una nueva "versión" del dataset extrayendo n filas con reemplazo (una misma fila puede salir 0, 1, 2 o más veces).

Ejemplo numérico con un dataset diminuto de 6 filas {A, B, C, D, E, F}:

Muestra bootstrap Filas extraídas (con reemplazo) Filas que NO aparecen (out-of-bag)
Muestra 1 A, A, C, D, D, F B, E
Muestra 2 B, C, C, E, F, F A, D
Muestra 3 A, B, D, E, E, E C, F

Observaciones clave:

  1. Cada muestra tiene el mismo tamaño que el original (6 filas), pero composición distinta.
  2. Algunas filas se repiten (D sale dos veces en la muestra 1; E sale tres veces en la 3).
  3. Algunas filas quedan fuera de cada muestra: son las filas out-of-bag (OOB) de ese modelo.

¿Qué fracción de filas queda fuera en promedio? La probabilidad de que una fila concreta no sea elegida en una extracción es (1 - 1/n). Tras n extracciones:

P(fila fuera) = (1 - 1/n)^n  →  e⁻¹ ≈ 0.368  cuando n es grande

Es decir: cada árbol ve en promedio el 63.2 % de las filas únicas, y deja fuera un 36.8 %. Ese 36.8 % será oro puro dentro de un momento (sección 2.4).

2.2 Por qué el bagging reduce varianza

Un árbol de decisión profundo es el ejemplo canónico de modelo con baja calidad de sesgo pero altísima varianza: cambia 5 filas del training set y el árbol resultante puede ser completamente distinto (splits diferentes desde la raíz). Esa inestabilidad es varianza pura.

El bagging explota esa inestabilidad a su favor:

  1. Cada muestra bootstrap es una perturbación aleatoria del dataset → cada árbol resultante es distinto (¡diversidad fabricada!).
  2. Cada árbol individual sigue siendo bueno (ve el 63 % de los datos, suficiente señal).
  3. Al promediar (regresión) o votar (clasificación), los errores idiosincráticos de cada árbol se cancelan entre sí, y lo que sobrevive es la señal común.

Advertencia

el bagging solo ayuda con modelos inestables (árboles, redes). Hacer bagging de regresión lineal apenas aporta: la regresión lineal es tan estable que todas las muestras bootstrap producen casi el mismo modelo → cero diversidad → cero ganancia. La diversidad no sale gratis: sale de la inestabilidad del modelo base.

2.3 BaggingClassifier en scikit-learn

# ============================================================
# BAGGING con scikit-learn: código comentado línea por línea
# ============================================================
from sklearn.ensemble import BaggingClassifier      # el meta-estimador de bagging
from sklearn.tree import DecisionTreeClassifier     # el modelo base (inestable a propósito)
from sklearn.datasets import make_classification    # generador de datos sintéticos
from sklearn.model_selection import train_test_split, cross_val_score
import numpy as np

# Generamos un problema de clasificación sintético reproducible:
X, y = make_classification(
    n_samples=1000,        # 1000 filas
    n_features=20,         # 20 columnas
    n_informative=8,       # solo 8 llevan señal real
    n_redundant=4,         # 4 son combinaciones lineales de las informativas
    random_state=42,       # semilla para reproducibilidad
)

# Separamos un test set que NO tocaremos hasta el final:
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

# --- Modelo 1: un solo árbol profundo (referencia de alta varianza) ---
arbol = DecisionTreeClassifier(random_state=42)     # sin límite de profundidad: memoriza
arbol.fit(X_train, y_train)                          # entrena sobre el training set
print(f"Árbol solo  — test: {arbol.score(X_test, y_test):.3f}")

# --- Modelo 2: bagging de 200 árboles profundos ---
bagging = BaggingClassifier(
    estimator=DecisionTreeClassifier(random_state=42),  # el mismo árbol inestable
    n_estimators=200,      # 200 árboles: más árboles = promedio más estable
    max_samples=1.0,       # cada bootstrap tiene el 100% del tamaño original
    bootstrap=True,        # muestreo CON reemplazo (esto lo hace "bagging")
    oob_score=True,        # activa la evaluación out-of-bag (¡gratis!)
    n_jobs=-1,             # entrena los 200 árboles en paralelo (son independientes)
    random_state=42,
)
bagging.fit(X_train, y_train)                        # entrena el ensemble completo
print(f"Bagging 200 — test: {bagging.score(X_test, y_test):.3f}")
print(f"Bagging 200 — OOB : {bagging.oob_score_:.3f}")  # estimación sin usar el test set

Salida típica:

Árbol solo  — test: 0.870
Bagging 200 — test: 0.925
Bagging 200 — OOB : 0.921

Cinco puntos y medio de mejora sin cambiar los datos, sin tuning, solo promediando. Y fíjate en algo importante: la OOB score (0.921) casi clava la test score (0.925). No es suerte, es el tema de la siguiente sección.

2.4 Out-of-bag score: evaluación gratis

Recuerda: cada árbol deja fuera ~36.8 % de las filas. Para cada fila del training set, existe por tanto un subconjunto de árboles (en torno a un tercio del bosque) que nunca la vieron durante el entrenamiento. La idea OOB es:

  1. Para cada fila i, reúne solo los árboles que NO la tenían en su bootstrap.
  2. Haz que esos árboles voten sobre la fila i → predicción OOB de i.
  3. Compara predicciones OOB con las etiquetas reales → OOB score.

Es, en esencia, una validación cruzada gratuita: cada predicción se hace con modelos que no vieron ese dato, sin apartar datos ni entrenar de nuevo.

Método de evaluación Coste computacional extra Datos "sacrificados" Cuándo usarlo
Hold-out (train/test) Ninguno 20-30 % del dataset Siempre, como control final
Validación cruzada k-fold k entrenamientos Ninguno (rotación) Selección de modelo/hiperparámetros
OOB score Prácticamente cero Ninguno Chequeo rápido en bagging/RF; datasets pequeños

Consejo profesional

con datasets pequeños (< 2 000 filas) donde apartar un test set duele, la OOB score de un Random Forest es un estimador de generalización sorprendentemente honesto. No sustituye a la CV para comparar familias de modelos distintas, pero para monitorizar un RF es la opción más barata que existe.

2.5 Ejercicio rápido 2

Con un dataset de 10 filas, ¿cuál es la probabilidad exacta de que la fila nº 7 no aparezca en una muestra bootstrap? ¿Y con 1 000 filas (aproximada)?

Ver solución - n = 10: P = (1 − 1/10)¹⁰ = 0.9¹⁰ ≈ **0.349** (34.9 %) - n = 1000: P = (1 − 1/1000)¹⁰⁰⁰ ≈ e⁻¹ ≈ **0.368** (36.8 %) La convergencia a e⁻¹ ≈ 36.8 % es rapidísima: ya con 10 filas estamos cerca. Por eso la regla "cada árbol ve ~63 % de las filas únicas" vale para casi cualquier tamaño de dataset.

3. Random Forest a fondo

3.1 Bagging + aleatoriedad de features: la doble dosis de azar

Random Forest (Breiman, 2001) es bagging de árboles más un segundo truco de diversidad: en cada split de cada árbol, en lugar de considerar todas las features, se sortea un subconjunto aleatorio de features (típicamente sqrt(n_features) en clasificación) y el split solo puede elegir entre esas.

¿Por qué el doble azar ayuda? Piensa en qué falla con bagging puro: si existe una feature dominante (p. ej. "importe de la deuda" en un problema de impago), todos los árboles la elegirán en la raíz, aunque cada uno vea filas distintas. Los árboles acaban pareciéndose demasiado → correlación ρ alta entre árboles → recuerda la fórmula ρσ² + (1-ρ)σ²/B: la parte ρσ² no se reduce por muchos árboles que añadas.

Al sortear las features de cada split:

  • Muchos árboles se ven obligados a empezar por features secundarias.
  • Descubren interacciones y señales que el árbol "codicioso" ignoraría.
  • La correlación entre árboles cae en picado → el promedio elimina mucha más varianza.

Cada árbol individual es peor (tiene las manos atadas), pero el bosque es mejor. Es el ejemplo perfecto de que en un ensemble no optimizas los miembros: optimizas el comité.

flowchart LR
    D[Dataset<br>n filas, p features] --> S1[Bootstrap 1<br>~63% filas únicas]
    D --> S2[Bootstrap 2]
    D --> S3[Bootstrap B]
    S1 --> T1["Árbol 1<br>cada split: sortea √p features"]
    S2 --> T2["Árbol 2<br>cada split: sortea √p features"]
    S3 --> T3["Árbol B<br>cada split: sortea √p features"]
    T1 --> AGG["Clasificación: promedio de probabilidades<br>Regresión: media de predicciones"]
    T2 --> AGG
    T3 --> AGG
    AGG --> OUT[Predicción final]
    style AGG fill:#e8f4e8,stroke:#2e7d32

3.2 Hiperparámetros clave y sus efectos

Hiperparámetro Qué controla Subirlo → Bajarlo → Valor de partida razonable
n_estimators Nº de árboles ↓ varianza, ↑ tiempo; nunca sobreajusta por sí solo (satura) ↑ varianza, más ruido 300–500
max_depth Profundidad máxima de cada árbol ↑ capacidad, ↑ riesgo de overfitting por árbol ↑ sesgo, árboles más "suaves" None (sin límite) y regular con min_samples_leaf
max_features Features sorteadas por split Árboles más parecidos (↑ ρ), menos diversidad Más diversidad, árboles individuales más débiles "sqrt" (clasificación), 1.0 o 0.3 (regresión)
min_samples_leaf Mínimo de muestras por hoja ↑ suavizado, ↓ overfitting, ↑ sesgo Hojas de 1 muestra: memorización 1–5 (subir a 10–50 con datos ruidosos)
class_weight Peso de cada clase "balanced" compensa desbalanceo "balanced" si hay desbalanceo
n_jobs Paralelismo -1 usa todos los núcleos -1 siempre

Nota

a diferencia del boosting, en Random Forest n_estimators no es un hiperparámetro de regularización: más árboles nunca empeoran la generalización (solo cuesta más CPU y memoria). El error de test baja y se aplana. Pon tantos como tu tiempo permita y olvídate.

3.3 Código completo, línea por línea

# ============================================================
# RANDOM FOREST completo: entrenamiento, OOB, importancias
# ============================================================
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score

# 1) Datos sintéticos con nombres de columna para poder hablar de importancias:
X, y = make_classification(
    n_samples=2000, n_features=15, n_informative=6,
    n_redundant=3, weights=[0.8, 0.2],   # clases desbalanceadas 80/20
    random_state=42,
)
columnas = [f"feat_{i:02d}" for i in range(15)]      # nombres feat_00 ... feat_14
X = pd.DataFrame(X, columns=columnas)                # DataFrame para trazabilidad

# 2) Split estratificado (mantiene la proporción 80/20 en train y test):
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, stratify=y, random_state=42
)

# 3) El bosque:
rf = RandomForestClassifier(
    n_estimators=400,          # 400 árboles: suficiente para que el error se aplane
    max_depth=None,            # árboles sin límite de profundidad...
    min_samples_leaf=3,        # ...pero cada hoja necesita >=3 muestras (regularización suave)
    max_features="sqrt",       # sortea sqrt(15)≈4 features por split (diversidad)
    class_weight="balanced",   # compensa el desbalanceo 80/20 reponderando errores
    oob_score=True,            # calcula la score out-of-bag (validación gratis)
    n_jobs=-1,                 # entrena en paralelo: los árboles son independientes
    random_state=42,           # reproducibilidad del sorteo de filas y features
)

# 4) Entrenamiento (una sola línea; los pipelines del cap. 5 aplican igual):
rf.fit(X_train, y_train)

# 5) Evaluación honesta:
print(f"OOB score        : {rf.oob_score_:.3f}")     # estimación interna, sin test set
proba_test = rf.predict_proba(X_test)[:, 1]          # probabilidad de la clase positiva
print(f"ROC-AUC en test  : {roc_auc_score(y_test, proba_test):.3f}")
print(classification_report(y_test, rf.predict(X_test), digits=3))

# 6) Feature importance por impureza (la "de serie"):
imp = pd.Series(rf.feature_importances_, index=columnas).sort_values(ascending=False)
print("\nTop 5 importancias (impureza):")
print(imp.head(5).round(3))

Puntos que debes poder explicar de este código en una entrevista:

  • class_weight="balanced" multiplica el peso de los errores de la clase minoritaria por n / (2 · n_minoritaria), forzando al árbol a no ignorarla. Es la primera línea de defensa contra el desbalanceo, antes de técnicas más agresivas como SMOTE.
  • predict_proba + ROC-AUC en vez de accuracy: con 80/20, un modelo que dice siempre "no" tiene 80 % de accuracy y cero valor. Lección del capítulo 4 que aquí es obligatoria.
  • min_samples_leaf=3 con max_depth=None: es más elegante regular por tamaño de hoja que por profundidad, porque se adapta a la densidad local de los datos.

3.4 Feature importance: cómo funciona y dónde te miente

La importancia "de serie" (feature_importances_) es la importancia por disminución de impureza (MDI, Mean Decrease in Impurity): para cada feature, suma cuánto redujo la impureza (Gini o entropía) en todos los splits donde se usó, ponderado por cuántas muestras pasaron por ese split, y promedia sobre todos los árboles. Se normaliza para que sume 1.

Sirve para:

  • Detectar features candidatas a eliminar (importancia ≈ 0).
  • Contar una primera historia de "qué mira el modelo" a negocio.
  • Detectar fugas de datos: si una feature tiene importancia 0.95, sospecha que codifica el target.

Advertencia

la importancia por impureza tiene dos sesgos graves que debes conocer:

  1. Sesgo hacia alta cardinalidad: las features con muchos valores únicos (IDs, códigos postales, variables continuas de grano fino) ofrecen más puntos de corte posibles, así que por puro azar consiguen splits que reducen impureza en training — aunque no generalicen. Una columna de números aleatorios con 1 000 valores únicos puede aparecer con más importancia que una binaria genuinamente predictiva.
  2. Se calcula sobre training: mide qué usó el modelo para ajustar, no qué le ayuda a generalizar.

3.5 Permutation importance: la alternativa honesta

La importancia por permutación responde a la pregunta correcta: "si destruyo la información de esta feature, ¿cuánto empeora el modelo en datos que no ha visto?". El procedimiento: baraja (permuta) los valores de una columna en el set de validación, mide la caída de la métrica, repite varias veces y promedia.

# ============================================================
# PERMUTATION IMPORTANCE: la medida honesta
# ============================================================
from sklearn.inspection import permutation_importance  # utilidad oficial de sklearn

# Calculamos sobre el TEST set (datos no vistos), no sobre train:
perm = permutation_importance(
    rf,                      # el modelo ya entrenado
    X_test, y_test,          # datos NO vistos: mide utilidad real, no memorización
    scoring="roc_auc",       # la métrica de negocio que nos importa
    n_repeats=20,            # baraja cada columna 20 veces y promedia (reduce ruido)
    n_jobs=-1,               # en paralelo
    random_state=42,
)

# Ordenamos por importancia media descendente:
perm_imp = pd.Series(perm.importances_mean, index=columnas).sort_values(ascending=False)
print("Top 5 importancias (permutación, sobre test):")
print(perm_imp.head(5).round(4))

# Bonus: la desviación estándar nos dice si la importancia es estable o ruido:
for feat in perm_imp.head(5).index:
    i = columnas.index(feat)
    print(f"  {feat}: {perm.importances_mean[i]:.4f} ± {perm.importances_std[i]:.4f}")
Aspecto Importancia por impureza (MDI) Permutation importance
Datos usados Training Validación/test (no vistos)
Sesgo por cardinalidad Sí, grave No
Coste Gratis (subproducto del fit) n_features × n_repeats evaluaciones
Features correlacionadas Reparte importancia arbitrariamente Puede infravalorar ambas (la otra "cubre")
Uso recomendado Vistazo rápido, detección de fugas Informe final, decisiones de negocio

Consejo profesional

cuando dos features están muy correlacionadas, la permutación de una la "cubre" la otra y ambas parecen poco importantes. Solución práctica: agrupa features correlacionadas (clustering jerárquico sobre la matriz de correlación) y permuta grupos enteros. El ejemplo de la documentación de sklearn "Permutation Importance with Multicollinear Features" lo hace paso a paso.

3.6 Cuándo Random Forest es la mejor opción

Random Forest es, probablemente, el mejor primer modelo serio para cualquier problema tabular:

  • Robusto por defecto: funciona razonablemente bien sin tuning (sus defaults son sensatos). El boosting mal tuneado puede ser peor que un RF sin tocar.
  • Difícil de romper: no le afectan la escala de las features (no necesita StandardScaler), tolera outliers y features irrelevantes.
  • Paralelizable al 100 %: los árboles son independientes → escala linealmente con los núcleos. El boosting es secuencial por naturaleza.
  • OOB score gratis para monitorizar.
  • Menos sensible al ruido de etiquetas que el boosting (el boosting se obsesiona con los ejemplos difíciles, y los ejemplos mal etiquetados son "difíciles" para siempre).

Elige RF sobre boosting cuando: tienes poco tiempo de tuning, el dataset es ruidoso, necesitas entrenamientos rápidos en paralelo, o el último 1-2 % de métrica no cambia la decisión de negocio. Elige boosting cuando ese último punto de AUC vale dinero y puedes pagar el coste de tuning y validación cuidadosa.

3.7 Ejercicio rápido 3

Tu Random Forest da importancia por impureza altísima a la columna id_cliente (un entero único por fila). ¿Qué está pasando y qué dos acciones tomarías?

Ver solución **Qué pasa:** `id_cliente` tiene cardinalidad máxima (un valor único por fila), el caso extremo del sesgo de MDI. Los árboles pueden usarla para *memorizar filas individuales* (por ejemplo "si id < 4187.5 → clase 1"), reduciendo impureza en training sin generalizar nada. Peor aún: si los IDs se asignaron por orden temporal y el target cambia con el tiempo, hay una **fuga de datos encubierta**. **Acciones:** 1. **Eliminar `id_cliente` del modelo** — un identificador nunca debe ser feature (salvo que lo transformes deliberadamente en algo con significado, como antigüedad). 2. **Recalcular con permutation importance sobre validación** para ver el ranking honesto de las demás features, y revisar si hay más columnas sospechosas de fuga (fechas, códigos internos posteriores al evento).

4. Boosting: aprender de los errores

4.1 La idea secuencial: el equipo que aprende de los fallos

Cambia la metáfora: en bagging el comité delibera en paralelo, sin comunicarse. En boosting, el equipo trabaja en relevos y con feedback. Imagina un equipo de soporte resolviendo tickets:

  1. El técnico junior 1 resuelve lo que puede. Los tickets que falla se marcan.
  2. El técnico 2 se especializa en los tickets que falló el 1 (los estudia con lupa).
  3. El técnico 3 se especializa en lo que fallaron 1 y 2 juntos.
  4. ... y así M rondas. La respuesta final combina a todos, dando más peso a los que demostraron acertar.

Cada modelo individual es débil (un árbol de profundidad 1-6, apenas mejor que reglas simples), pero la secuencia ataca sistemáticamente el error residual. Por eso el boosting reduce sesgo: cada iteración añade capacidad exactamente donde el conjunto actual se queda corto.

4.2 AdaBoost: la intuición histórica

AdaBoost (Freund & Schapire, 1997; premio Gödel 2003) fue el primer boosting práctico y funciona reponderando ejemplos:

  1. Todos los ejemplos empiezan con el mismo peso.
  2. Entrena un clasificador débil (típicamente un stump: árbol de profundidad 1).
  3. Sube el peso de los ejemplos mal clasificados, baja el de los bien clasificados.
  4. Entrena el siguiente stump sobre los datos reponderados → se ve forzado a atender los casos difíciles.
  5. Repite M veces. La predicción final es una votación ponderada, donde cada stump pesa según su tasa de acierto (α = ½ ln((1-ε)/ε), con ε = error ponderado).

AdaBoost demostró que "muchos aprendices débiles = un aprendiz fuerte" no era solo teoría (la conjetura de Kearns y Valiant), y dominó los 2000. Hoy es sobre todo pieza de museo didáctica: el gradient boosting lo generaliza y lo supera. Pero su intuición — concentra el esfuerzo en lo que fallas — es el alma de todo lo que viene.

4.3 Gradient Boosting paso a paso, con números

Gradient Boosting (Friedman, 2001) reformula la idea: en lugar de reponderar ejemplos, cada árbol nuevo predice los residuos (errores) del conjunto acumulado. Para regresión con error cuadrático, los residuos son literalmente y - predicción_actual; en general, el árbol ajusta el gradiente negativo de la función de pérdida (de ahí el nombre).

Veámoslo con un ejemplo numérico minúsculo. Queremos predecir el precio de 5 pisos (en miles de €) a partir de sus metros cuadrados:

Piso Precio real y
A 40 100
B 55 150
C 70 200
D 85 240
E 100 310

Paso 0 — predicción inicial F₀: la media de y: (100+150+200+240+310)/5 = 200.

Paso 1 — residuos de F₀: r = y − 200:

Piso y F₀ Residuo r₁
A 100 200 −100
B 150 200 −50
C 200 200 0
D 240 200 +40
E 310 200 +110

Paso 2 — árbol 1 sobre los residuos: un árbol pequeño (profundidad 1) encuentra el mejor split, digamos m² ≤ 62.5:

  • Hoja izquierda (A, B): media de residuos = (−100 − 50)/2 = −75
  • Hoja derecha (C, D, E): media = (0 + 40 + 110)/3 = +50

Paso 3 — actualizar con learning rate η = 0.5:

F₁(x) = F₀(x) + η · árbol₁(x)
Piso F₀ árbol₁ F₁ = F₀ + 0.5·árbol₁ Nuevo residuo r₂ = y − F₁
A 200 −75 162.5 −62.5
B 200 −75 162.5 −12.5
C 200 +50 225.0 −25.0
D 200 +50 225.0 +15.0
E 200 +50 225.0 +85.0

Observa: los residuos han encogido (de ±110 a ±85 como máximo). El árbol 2 se entrenará sobre r₂, quizá con el split m² ≤ 92.5 para aislar el piso E que sigue muy infravalorado. Y así sucesivamente: 100, 500, 2 000 árboles, cada uno mordisqueando el error restante.

Predicción final = F₀ + η·árbol₁ + η·árbol₂ + ... + η·árbolₘ

El papel del learning rate η: ¿por qué avanzar solo el 50 % (o el 5 %) de lo que sugiere cada árbol? Porque cada árbol se ajusta a los residuos del training set, que contienen señal + ruido. Dar pasos pequeños hace que el ruido de árboles sucesivos tienda a cancelarse mientras la señal (consistente) se acumula. Es la versión boosting del descenso de gradiente con paso corto: más lento, pero llega a un mínimo mejor. Regla empírica fundamental: η pequeño exige más árboles (η=0.1 con 500 árboles ≈ η=0.05 con 1000).

4.4 Por qué es tan potente y por qué puede sobreajustar

Potencia: el gradient boosting es una máquina de reducir sesgo con control fino de la varianza. Cada árbol añade exactamente la corrección que la pérdida pide (es descenso de gradiente en el espacio de funciones), los árboles pequeños capturan interacciones de orden limitado (profundidad 3 → interacciones de hasta 3 variables), y η + regularización controlan cuánto confías en cada corrección. Sobre datos tabulares con relaciones no lineales, umbrales e interacciones — es decir, casi todos los datos de negocio — es letal.

Peligro: la misma obsesión con los residuos es su talón de Aquiles. Si sigues añadiendo árboles, llegará un punto en que los residuos que quedan son ruido puro (errores de medida, etiquetas incorrectas, azar) — y el boosting los perseguirá igual, con idéntica diligencia. A diferencia del Random Forest, en boosting más árboles SÍ sobreajustan. La curva de error de validación baja, toca fondo y vuelve a subir. Los frenos disponibles:

Freno Mecanismo
learning_rate bajo Pasos cortos: el ruido se cancela antes de acumularse
Early stopping Parar cuando la validación deja de mejorar (sección 6)
max_depth / num_leaves pequeños Árboles simples = correcciones simples
subsample < 1 Cada árbol ve una fracción aleatoria de filas (boosting estocástico)
colsample_bytree < 1 Cada árbol ve una fracción de columnas (idea robada al RF)
Regularización L1/L2 (reg_alpha, reg_lambda) Penaliza pesos grandes en las hojas
flowchart TD
    F0["F₀ = valor inicial (media / log-odds)"] --> R1["Calcular residuos:<br>gradiente negativo de la pérdida"]
    R1 --> T1["Entrenar árbol pequeño<br>sobre los residuos"]
    T1 --> U1["F₁ = F₀ + η · árbol₁"]
    U1 --> R2["Nuevos residuos<br>(más pequeños)"]
    R2 --> T2["Entrenar árbol 2"]
    T2 --> U2["F₂ = F₁ + η · árbol₂"]
    U2 --> C{"¿Validación<br>sigue mejorando?"}
    C -- "Sí"--> R3["Siguiente iteración..."]
    C -- "No (early stopping)"--> FIN["Modelo final:<br>F₀ + η·Σ árboles"]
    R3 --> C
    style FIN fill:#e8f4e8,stroke:#2e7d32
    style C fill:#fff3e0,stroke:#ef6c00

4.5 Ejercicio rápido 4

En el ejemplo numérico de los pisos, calcula la predicción F₁ para un piso nuevo de 60 m². Y responde: si hubiéramos usado η = 1.0 en lugar de 0.5, ¿qué predicción daría F₁ para el piso B, y qué riesgo ilustra?

Ver solución **Piso de 60 m²:** cae en la hoja izquierda del árbol 1 (m² ≤ 62.5), cuyo valor es −75.
F₁(60 m²) = 200 + 0.5 · (−75) = 162.5 mil €
**Piso B con η = 1.0:** F₁(B) = 200 + 1.0·(−75) = **125**, con residuo y − F₁ = 150 − 125 = +25. Con η=0.5 el residuo de B era −12.5; con η=1.0 es +25: el paso completo se ha **pasado de frenada** (B necesitaba −50 de corrección y la hoja le aplicó −75, porque comparte hoja con A que necesitaba −100). η=1.0 confía ciegamente en correcciones calculadas sobre promedios de hoja que mezclan casos distintos; los pasos cortos amortiguan ese error de sobre-corrección iteración a iteración. Es exactamente el mecanismo por el que learning rates altos sobreajustan y oscilan.

5. Las implementaciones modernas: HistGB, XGBoost, LightGBM, CatBoost

El GradientBoostingClassifier clásico de sklearn (2011) es didáctico pero lento: evalúa cada posible punto de corte de cada feature. Las implementaciones modernas cambiaron el juego con una idea común: histogramas (discretizar cada feature en ~255 bins y evaluar cortes solo entre bins → 100× más rápido) más trucos propios de cada biblioteca.

5.1 HistGradientBoosting: el nativo de sklearn

Desde sklearn 0.21 (inspirado en LightGBM), y hoy totalmente maduro:

# ============================================================
# HistGradientBoosting: boosting moderno SIN dependencias extra
# ============================================================
from sklearn.ensemble import HistGradientBoostingClassifier

hgb = HistGradientBoostingClassifier(
    max_iter=500,                 # número máximo de árboles (iteraciones)
    learning_rate=0.08,           # paso corto: más robusto
    max_leaf_nodes=31,            # complejidad por árbol (crecimiento leaf-wise)
    early_stopping=True,          # ¡integrado! aparta validación automáticamente
    validation_fraction=0.15,     # 15% del train para vigilar el overfitting
    n_iter_no_change=30,          # paciencia: 30 rondas sin mejora → stop
    categorical_features="from_dtype",  # trata como categóricas las columnas
                                        # con dtype 'category' del DataFrame
    random_state=42,
)
# Maneja NaN de forma NATIVA: no necesitas SimpleImputer en el pipeline.
# Los NaN se envían, en cada split, al lado que más reduzca la pérdida.
hgb.fit(X_train, y_train)
print(f"Árboles usados realmente: {hgb.n_iter_}")   # cuántos sobrevivieron al early stopping

Sus dos superpoderes prácticos: acepta valores nulos sin imputación (aprende a qué rama enviar los NaN) y acepta categóricas nativas (columnas category de pandas, sin one-hot). Para muchísimos proyectos es todo lo que necesitas, sin instalar nada.

5.2 XGBoost: el estándar industrial

XGBoost (Chen & Guestrin, 2016) dominó Kaggle durante años y sigue siendo el más desplegado en producción. Sus señas de identidad:

  • Regularización de serie: la función objetivo incluye penalizaciones L1 (reg_alpha) y L2 (reg_lambda) sobre los valores de las hojas, y gamma (pérdida mínima para permitir un split). El overfitting se combate dentro de la fórmula, no solo con trucos externos.
  • Manejo nativo de nulos (aprende la dirección por defecto de cada split).
  • scale_pos_weight para desbalanceo, ecosistema gigante, GPU, y soporte en todos los servicios cloud.
# ============================================================
# XGBoost con su API sklearn-compatible + EARLY STOPPING
# ============================================================
# pip install xgboost
from xgboost import XGBClassifier
from sklearn.model_selection import train_test_split

# Apartamos un conjunto de validación EXCLUSIVO para early stopping.
# Nunca uses el test final para esto: sería fuga de información.
X_tr, X_val, y_tr, y_val = train_test_split(
    X_train, y_train, test_size=0.2, stratify=y_train, random_state=42
)

xgb = XGBClassifier(
    n_estimators=2000,            # techo alto A PROPÓSITO: early stopping decidirá
    learning_rate=0.05,           # paso corto
    max_depth=5,                  # árboles poco profundos (interacciones de orden <=5)
    subsample=0.8,                # cada árbol ve el 80% de las filas (boosting estocástico)
    colsample_bytree=0.8,         # y el 80% de las columnas (diversidad estilo RF)
    reg_lambda=1.0,               # regularización L2 sobre los valores de hoja
    scale_pos_weight=4.0,         # peso extra a la clase positiva (ratio 80/20 -> ~4)
    eval_metric="auc",            # métrica que vigila el early stopping
    early_stopping_rounds=50,     # paciencia: 50 rondas sin mejorar AUC de validación
    random_state=42,
)

xgb.fit(
    X_tr, y_tr,
    eval_set=[(X_val, y_val)],    # el conjunto que se vigila en cada iteración
    verbose=False,                # sin log por ronda
)
print(f"Mejor iteración: {xgb.best_iteration}")      # p.ej. 317 de las 2000 posibles
print(f"AUC validación : {xgb.best_score:.4f}")

Estrategia de tuning de XGBoost, en orden (no tunees todo a la vez):

Orden Hiperparámetro(s) Rango típico Por qué en este orden
1 learning_rate + n_estimators (vía early stopping) 0.01–0.1 / techo 2000–5000 Fijan la escala de todo lo demás
2 max_depth, min_child_weight 3–8 / 1–20 Complejidad del árbol: mayor palanca de over/underfitting
3 subsample, colsample_bytree 0.6–1.0 Diversidad estocástica
4 reg_lambda, reg_alpha, gamma 0–10 / 0–5 / 0–5 Ajuste fino de regularización
5 Bajar learning_rate y re-parar con early stopping 0.01–0.03 Exprimir el último punto de métrica

5.3 LightGBM: velocidad y crecimiento leaf-wise

LightGBM (Microsoft, 2017) es habitualmente 5-20× más rápido que XGBoost clásico con igual o mejor métrica. Sus tres ideas:

  1. Crecimiento leaf-wise (por hoja) vs level-wise (por nivel). XGBoost tradicional crece árboles nivel a nivel (todos los nodos de profundidad k antes de pasar a k+1, árboles simétricos). LightGBM expande siempre la hoja con mayor ganancia de pérdida, esté donde esté → árboles asimétricos que gastan su presupuesto de complejidad donde más rinde. Con el mismo número de hojas, leaf-wise reduce más la pérdida... y sobreajusta más fácil en datasets pequeños (por eso num_leaves es SU hiperparámetro crítico).
  2. GOSS y EFB (muestreo sesgado a gradientes grandes y empaquetado de features dispersas): menos cómputo con mínima pérdida de precisión.
  3. Categóricas nativas: pásale columnas con dtype category y encuentra particiones óptimas de categorías sin one-hot (que en árboles es especialmente dañino: fragmenta la señal en decenas de columnas binarias casi vacías).
# ============================================================
# LightGBM con API sklearn + categóricas nativas + early stopping
# ============================================================
# pip install lightgbm
from lightgbm import LGBMClassifier, early_stopping, log_evaluation

lgbm = LGBMClassifier(
    n_estimators=2000,            # techo alto; early stopping decide
    learning_rate=0.05,
    num_leaves=31,                # ¡EL hiperparámetro de LightGBM! (leaf-wise)
                                  # regla: num_leaves < 2^max_depth para no explotar
    max_depth=-1,                 # -1 = sin límite de profundidad; controla via num_leaves
    min_child_samples=20,         # mínimo de filas por hoja (anti-overfitting clave)
    subsample=0.8, colsample_bytree=0.8,
    class_weight="balanced",      # desbalanceo
    random_state=42,
    verbose=-1,                   # silencia warnings informativos
)

lgbm.fit(
    X_tr, y_tr,
    eval_set=[(X_val, y_val)],
    eval_metric="auc",
    callbacks=[
        early_stopping(stopping_rounds=50),   # API de callbacks de LightGBM
        log_evaluation(period=0),             # 0 = sin log
    ],
    # Si X es un DataFrame con dtypes 'category', LightGBM las detecta solo.
    # También puedes forzarlo: categorical_feature=["provincia", "canal"]
)
print(f"Mejor iteración: {lgbm.best_iteration_}")

5.4 CatBoost: categóricas y menos tuning

CatBoost (Yandex, 2017) merece mención por dos virtudes: su tratamiento de categóricas mediante ordered target encoding (codifica cada categoría con estadísticas del target calculadas solo sobre filas anteriores en un orden aleatorio, evitando la fuga que arruina el target encoding ingenuo) y el ordered boosting, que reduce el sobreajuste estructural del boosting clásico. En la práctica: excelente con muchas categóricas de alta cardinalidad y muy buenos resultados con los hiperparámetros por defecto — ideal cuando no hay tiempo de tuning. A cambio, suele entrenar más lento que LightGBM.

5.5 Tabla comparativa completa

Criterio HistGradientBoosting XGBoost LightGBM CatBoost
Instalación Incluido en sklearn pip install xgboost pip install lightgbm pip install catboost
Velocidad de entrenamiento Rápida Media-rápida (hist) La más rápida Media-lenta
Crecimiento del árbol Leaf-wise Level-wise (o leaf-wise con grow_policy) Leaf-wise Simétrico (oblivious)
Nulos nativos
Categóricas nativas (dtype category) Parcial (enable_categorical) excelente la mejor
Necesidad de tuning Baja Alta Media-alta Baja
Riesgo de overfitting sin tuning Medio Medio Alto (leaf-wise) Bajo
Comunidad / despliegue Ecosistema sklearn El más extendido Muy grande Grande
GPU No
Punto fuerte Cero dependencias, integración pipelines Regularización, madurez, soporte universal Velocidad en datasets grandes Categóricas, defaults sólidos

Cuándo elegir cada una:

  • HistGradientBoosting: proyecto 100 % sklearn, prototipos, cuando no quieres dependencias externas ni pelearte con versiones. Rinde a un 95-99 % de las demás en la mayoría de casos.
  • XGBoost: producción corporativa (todo el mundo sabe operarlo), necesidad de regularización fina, compatibilidad con servicios gestionados (SageMaker, Vertex).
  • LightGBM: datasets de cientos de miles a millones de filas, iteración rápida de experimentos, muchas categóricas. La elección por defecto de la mayoría de practicantes en 2026.
  • CatBoost: dominio plagado de categóricas de alta cardinalidad (retail, publicidad), equipos sin tiempo de tuning.

Consejo profesional

las cuatro rinden dentro de un ±1 % entre sí en la mayoría de datasets si se tunean bien. La elección real casi nunca es por métrica: es por velocidad de iteración, manejo de tus tipos de datos y facilidad de despliegue en TU infraestructura. No pierdas una semana comparándolas; elige una, tunéala bien y sigue.

5.6 Ejercicio rápido 5

Un compañero entrena LightGBM con num_leaves=1024 y max_depth=-1 sobre un dataset de 3 000 filas, y obtiene AUC 1.00 en train y 0.71 en validación. Diagnostica y propone tres cambios concretos.

Ver solución **Diagnóstico:** overfitting severo por exceso de capacidad. 1 024 hojas con crecimiento leaf-wise sobre solo 3 000 filas significa ~3 filas por hoja: el modelo está memorizando filas individuales (de ahí el 1.00 en train). Leaf-wise agrava el problema porque concentra la complejidad en las zonas "difíciles", que con pocos datos suelen ser ruido. **Tres cambios:** 1. `num_leaves=15-31` (y/o `max_depth=4-6`): recorta drásticamente la capacidad por árbol. 2. `min_child_samples=30-50`: prohíbe hojas que aíslan un puñado de filas. 3. **Early stopping** con conjunto de validación (`stopping_rounds=50`) y `learning_rate` ≤ 0.05: que la validación decida cuántos árboles. (Bonus: `subsample=0.8`, `colsample_bytree=0.8` y `reg_lambda>0` como refuerzo.)

6. Early stopping a fondo

6.1 Qué es y por qué es LA técnica anti-overfitting del boosting

Ya lo hemos visto de pasada; ahora en serio. En boosting, el número de árboles M es el hiperparámetro más peligroso: pocos árboles = infraajuste, demasiados = perseguir ruido. Podrías buscar M por validación cruzada como cualquier otro hiperparámetro... pero sería un desperdicio absurdo, porque el boosting tiene una propiedad única: un modelo de 2 000 árboles contiene dentro a todos los modelos de 1, 2, ..., 1 999 árboles. Entrenar una vez con techo alto y evaluar la validación en cada iteración te da la curva completa de M gratis.

Early stopping = entrenar con un techo generoso de árboles, evaluar una métrica sobre un conjunto de validación separado tras cada iteración, y parar cuando lleve patience rondas sin mejorar, quedándote con la mejor iteración.

Por qué es la técnica y no una más:

  1. Convierte el hiperparámetro más sensible en automático. Ya no tuneas n_estimators: pones 2 000-5 000 y dejas que la validación decida.
  2. Se adapta a los demás hiperparámetros. Bajas el learning rate → automáticamente usa más árboles. Subes max_depth → para antes. Cada configuración de tu búsqueda de hiperparámetros encuentra su M óptimo sin coste extra.
  3. Es barata: una sola pasada de entrenamiento con evaluación incremental.
  4. Es el freno de emergencia universal: aunque el resto del tuning sea mediocre, el early stopping impide el desastre de las 2 000 iteraciones persiguiendo ruido.

6.2 Código canónico con conjunto de validación

# ============================================================
# EARLY STOPPING canónico: el patrón de tres conjuntos
# ============================================================
from sklearn.model_selection import train_test_split
from lightgbm import LGBMClassifier, early_stopping

# Patrón de TRES conjuntos. Cada uno tiene UN trabajo:
#   train      -> ajustar los árboles
#   validación -> decidir CUÁNDO PARAR (early stopping)
#   test       -> evaluación final, se toca UNA vez
X_temp, X_test, y_temp, y_test = train_test_split(      # 1) apartamos el test final
    X, y, test_size=0.20, stratify=y, random_state=42
)
X_tr, X_val, y_tr, y_val = train_test_split(            # 2) del resto, apartamos validación
    X_temp, y_temp, test_size=0.20, stratify=y_temp, random_state=42
)
# Resultado: 64% train / 16% validación / 20% test

modelo = LGBMClassifier(
    n_estimators=3000,        # techo deliberadamente alto: NO es el M final
    learning_rate=0.03,       # paso corto: la curva de validación baja suave
    num_leaves=31,
    random_state=42,
    verbose=-1,
)

modelo.fit(
    X_tr, y_tr,
    eval_set=[(X_val, y_val)],           # el árbitro que decide cuándo parar
    eval_metric="auc",                   # qué métrica vigila (¡la de negocio!)
    callbacks=[early_stopping(
        stopping_rounds=100,             # paciencia: 100 rondas sin nuevo máximo
        # first_metric_only=True,        # si pasas varias métricas, vigila la 1ª
    )],
)

# best_iteration_ es el M que maximizó el AUC de validación.
# predict() usa automáticamente esa mejor iteración, no la última.
print(f"Techo: 3000 | Usados: {modelo.best_iteration_}")
print(f"AUC test final: {roc_auc_score(y_test, modelo.predict_proba(X_test)[:, 1]):.4f}")

Advertencia (el error nº 1 con early stopping)

usar el test set como eval_set. Si el test decide cuándo parar, tu métrica de test es optimista: el modelo se eligió mirando esos datos. Es la misma fuga que tunear hiperparámetros sobre test. El patrón correcto es siempre tres conjuntos (o CV externa + validación interna para el stopping). Y segundo error frecuente: paciencia demasiado corta (5-10 rondas) con learning rate bajo — la curva mejora a rachas y un bache local te para antes de tiempo. Con η ≤ 0.05, usa paciencia 50-150.

Nota

¿y en el despliegue final? Práctica común: tras decidir hiperparámetros y conocer el M óptimo (digamos 480), reentrenar sobre train+validación juntos con n_estimators=480 fijo (o ~10 % más, porque hay más datos), sin early stopping. Así el modelo de producción aprovecha todos los datos disponibles.


7. Stacking y Voting

7.1 VotingClassifier: el comité heterogéneo

Hasta ahora los ensembles combinaban árboles con el mismo algoritmo. VotingClassifier combina modelos de familias distintas — y la diversidad entre familias (lineal vs árboles vs vecinos) es de la buena, porque sus sesgos inductivos son genuinamente diferentes.

  • Hard voting: cada modelo emite una clase; gana la mayoría. Simple, pero tira la información de confianza.
  • Soft voting: se promedian las probabilidades y gana la clase con mayor promedio. Casi siempre mejor (un modelo seguro al 95 % pesa más que dos dudosos al 51 %), pero exige que todos los modelos tengan predict_proba bien calibrado.
# ============================================================
# VOTING: combinar familias distintas
# ============================================================
from sklearn.ensemble import VotingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from lightgbm import LGBMClassifier

voting = VotingClassifier(
    estimators=[
        # La logística necesita escalado -> va en su propio pipeline (cap. 5):
        ("logreg", make_pipeline(StandardScaler(),
                                 LogisticRegression(max_iter=2000, C=0.5))),
        ("rf",     RandomForestClassifier(n_estimators=300, min_samples_leaf=3,
                                          n_jobs=-1, random_state=42)),
        ("lgbm",   LGBMClassifier(n_estimators=400, learning_rate=0.05,
                                  num_leaves=31, random_state=42, verbose=-1)),
    ],
    voting="soft",         # promedia probabilidades (mejor que "hard" casi siempre)
    weights=[1, 1, 2],     # opcional: doble peso al LightGBM si es claramente mejor
    n_jobs=-1,
)
voting.fit(X_train, y_train)

7.2 StackingClassifier: el meta-modelo

Stacking sustituye la votación fija por un meta-modelo que aprende a combinar: los modelos base generan predicciones, y un segundo nivel (típicamente una regresión logística) aprende qué peso dar a cada uno — incluso condicionado a la situación ("cuando el RF y el boosting discrepan, fíate del boosting").

El punto delicado: el meta-modelo debe entrenarse sobre predicciones out-of-fold (generadas por CV interna), nunca sobre predicciones de los modelos base sobre sus propios datos de entrenamiento — si no, el meta-modelo aprendería a fiarse de modelos que están memorizando. StackingClassifier hace esta CV interna automáticamente (cv=5 por defecto).

# ============================================================
# STACKING: un meta-modelo aprende a combinar
# ============================================================
from sklearn.ensemble import StackingClassifier

stacking = StackingClassifier(
    estimators=[            # nivel 0: los modelos base (mismos de antes)
        ("logreg", make_pipeline(StandardScaler(),
                                 LogisticRegression(max_iter=2000))),
        ("rf",     RandomForestClassifier(n_estimators=300, n_jobs=-1,
                                          random_state=42)),
        ("lgbm",   LGBMClassifier(n_estimators=400, learning_rate=0.05,
                                  random_state=42, verbose=-1)),
    ],
    final_estimator=LogisticRegression(),  # nivel 1: el "juez" que combina
    cv=5,                   # CV interna: predicciones out-of-fold para el meta-modelo
    stack_method="predict_proba",  # el meta-modelo ve probabilidades, no clases
    passthrough=False,      # True añadiría también las features originales al meta-modelo
    n_jobs=-1,
)
stacking.fit(X_train, y_train)

7.3 ¿Cuándo merecen la pena? Coste/beneficio honesto

Aspecto Voting/Stacking Un buen boosting solo
Ganancia típica de métrica +0.2 a +1 punto de AUC/accuracy — (la referencia)
Coste de entrenamiento 3-10× (todos los modelos + CV interna)
Latencia de inferencia Suma de todos los modelos 1 modelo
Complejidad de despliegue y monitorización Alta: N modelos que versionar, reentrenar y vigilar Baja
Interpretabilidad Muy difícil (capas de modelos) Difícil pero tratable (SHAP)
Dónde brilla Competiciones (cada diezmilésima cuenta y nadie mantiene nada) Producción

El veredicto profesional: en competiciones, el stacking (a menudo en varias capas) es obligatorio — las diferencias entre el puesto 1 y el 50 son milésimas. En producción, esa ganancia de +0.3 AUC rara vez compensa duplicar o triplicar el coste de mantenimiento, la latencia y la superficie de fallo. La excepción razonable: un soft voting de 2-3 modelos ya entrenados y estables, en un caso de uso donde cada décima de métrica tiene valor económico directo claro (p. ej. fraude de alto volumen).

Consejo profesional

antes de apilar modelos, comprueba que sus errores son de verdad distintos: mide la correlación entre sus predicciones (o entre sus vectores de error). Si tu RF y tu XGBoost tienen predicciones correlacionadas al 0.98, el stacking te dará casi nada: estás apilando clones con distinto logo.


8. ¿Por qué no deep learning para tablas?

Es la pregunta que todo alumno hace en 2026, con razón: si los transformers escriben código y las redes reconocen tumores, ¿por qué seguimos con árboles para un CSV?

8.1 La evidencia práctica

La literatura y la práctica llevan años alineadas en esto. El estudio de referencia de Grinsztajn, Oyallon y Varoquaux ("Why do tree-based models still outperform deep learning on tabular data?", NeurIPS 2022) comparó sistemáticamente ambos mundos en decenas de datasets tabulares medianos (~10K–50K filas): los métodos de árboles ganaron de forma consistente, con menos tuning y órdenes de magnitud menos cómputo. Los benchmarks posteriores (TabZilla y sucesores) matizaron pero no revirtieron la conclusión para el rango de tamaño típico de los datos de negocio.

Las razones estructurales de fondo:

  1. Los datos tabulares no tienen la estructura que las redes explotan. CNNs explotan localidad espacial; transformers, estructura secuencial. En una tabla, la columna 3 no tiene relación "espacial" con la 4: son features heterogéneas (euros, categorías, contadores, ratios) sin geometría compartida.
  2. Las funciones objetivo tabulares son irregulares: umbrales bruscos ("si edad ≥ 65...", "si moroso previo..."), interacciones no suaves. Los árboles modelan discontinuidades de forma nativa; las redes tienen un sesgo hacia funciones suaves y necesitan muchos datos para aproximar escalones.
  3. Features irrelevantes: las tablas reales llevan decenas de columnas inútiles. Los árboles las ignoran (nunca las eligen para un split); las redes deben aprender a ignorarlas, gastando datos en ello.
  4. Régimen de datos: con 1 000–100 000 filas (el 90 % de los problemas de empresa) no hay suficiente para que una red amortice su flexibilidad. El boosting extrae casi toda la señal disponible con una fracción del coste.

Nota (estado en 2026)

existen modelos fundacionales tabulares tipo TabPFN y sucesores que, en datasets pequeños (miles de filas, decenas de features), dan resultados competitivos sin entrenamiento (in-context learning sobre priors sintéticos). Son una herramienta legítima para prototipos rápidos en datos pequeños y hay que seguirles la pista. Pero para el grueso de los problemas de negocio — decenas o cientos de miles de filas, restricciones de latencia, coste y auditoría — el gradient boosting sigue siendo el campeón práctico en 2026, y ningún benchmark serio dice lo contrario.

8.2 Tabla honesta: boosting vs redes vs LLMs para tabular

Criterio Gradient boosting Red neuronal (MLP/transformer tabular) LLM (prompt con la tabla)
Datos necesarios para rendir Desde ~500 filas Decenas de miles a millones Cero-shot, pero precisión pobre y no escalable
Métrica en tabular mediano La mejor (referencia) Igual o peor, con mucho más esfuerzo Claramente peor; no es su trabajo
Coste de entrenamiento Minutos en CPU Horas, a menudo GPU No se entrena (pero coste por token en inferencia)
Coste de inferencia Microsegundos, CPU barata Milisegundos, a veces GPU Segundos y céntimos por predicción
Tuning necesario Moderado y bien documentado Alto (arquitectura, LR, schedulers, inicialización) Prompting frágil
Interpretabilidad Buena (SHAP, importancias) Baja Explicaciones verbales no fiables (post-hoc)
Mantenimiento en producción Sencillo y maduro (MLOps estándar) Complejo (drift + infra GPU) Dependencia de proveedor, deriva de versiones
Nulos y categóricas Nativo Requiere embeddings/imputación Textual, ad hoc
Cuándo elegirlo Datos tabulares, casi siempre Tablas gigantes + señal compleja, o multimodal (tabla+texto+imagen) Extraer features de texto libre que luego alimentan al boosting

Consejo profesional

la combinación ganadora en 2026 no es "boosting O LLMs", es boosting CON LLMs: usa el LLM para convertir texto no estructurado (comentarios de clientes, notas de agentes, descripciones de siniestros) en features tabulares (sentimiento, temas, flags), y alimenta esas columnas al LightGBM. Cada herramienta en su terreno.


9. Interpretabilidad de ensembles: SHAP

9.1 El problema

Un LightGBM de 500 árboles con 31 hojas cada uno es una suma de ~15 000 reglas. Nadie puede leerlo. Pero el analista de riesgos debe justificar por qué se denegó un crédito, el regulador puede exigirlo (en la UE, el AI Act y la normativa de crédito al consumo lo hacen explícito para casos de alto riesgo), y tú mismo necesitas saber si el modelo aprendió señal o un artefacto. La permutation importance (sección 3.5) da una visión global; nos falta la local: ¿por qué ESTA predicción para ESTE cliente?

9.2 Valores de Shapley: la analogía del reparto justo

Los valores SHAP (Lundberg & Lee, 2017) adaptan un concepto de teoría de juegos cooperativos de 1953 (Lloyd Shapley, Nobel de Economía 2012). La analogía:

Tres consultoras (A, B, C) colaboran en un proyecto que factura 100 000 €. ¿Cómo repartir de forma justa? La idea de Shapley: mide la contribución marginal de cada una — cuánto añade A al unirse a cada posible coalición previa (a nadie, solo a B, solo a C, a B y C juntas) — y promedia sobre todos los órdenes posibles de llegada. El promedio de contribuciones marginales es el pago justo de A. Es el único reparto que cumple simultáneamente eficiencia (los pagos suman el total), simetría, y que quien no aporta nada recibe cero.

Traducción a ML: el "proyecto" es una predicción concreta, los "jugadores" son las features, y el valor SHAP de la feature j para el cliente i es su contribución marginal promedio a mover la predicción desde la base (la predicción media del modelo) hasta el valor final:

predicción(cliente_i) = valor_base + Σⱼ shap_valueⱼ(cliente_i)

Esa aditividad exacta es lo que hace SHAP tan útil: cada predicción se descompone en un "recibo detallado": partimos de un 12 % de probabilidad de impago media; +9 pts por ratio deuda/ingresos = 0.8; +5 pts por 2 impagos previos; −3 pts por 12 años de antigüedad laboral → 23 %.

Calcular Shapley exacto es exponencial (2^p coaliciones), pero TreeSHAP lo computa de forma exacta y en tiempo polinómico para modelos de árboles — otra razón por la que los ensembles de árboles dominan en entornos regulados: son de los pocos modelos potentes con explicaciones exactas y rápidas.

9.3 Código básico y el summary plot

# ============================================================
# SHAP sobre un modelo de boosting
# ============================================================
# pip install shap
import shap

# TreeExplainer: algoritmo exacto y rápido para RF/XGBoost/LightGBM/CatBoost
explainer = shap.TreeExplainer(lgbm)          # el modelo ya entrenado

# Valores SHAP para el test set: matriz (n_filas, n_features)
# shap_values[i, j] = contribución de la feature j a la predicción de la fila i
shap_values = explainer.shap_values(X_test)

# 1) VISIÓN GLOBAL — summary plot (beeswarm):
shap.summary_plot(shap_values, X_test)
# Cómo leerlo:
#  - Cada fila del gráfico = una feature, ordenadas por importancia global
#    (media del |valor SHAP|): las de arriba son las que más mueven predicciones.
#  - Cada punto = un cliente. Posición horizontal = cuánto y hacia dónde empujó
#    esa feature SU predicción (derecha = hacia impago, izquierda = hacia no-impago).
#  - Color = valor de la feature (rojo alto, azul bajo). Un patrón "rojo a la
#    derecha, azul a la izquierda" significa: valores altos aumentan el riesgo.
#  - Las formas asimétricas revelan no linealidades e interacciones que una
#    importancia escalar jamás mostraría.

# 2) VISIÓN LOCAL — explicar UNA predicción (el "recibo" del cliente 7):
shap.plots.waterfall(explainer(X_test)[7])
# Muestra la cascada: valor base -> aportación de cada feature -> predicción final.

9.4 Por qué importa para negocio y regulación

  • Adopción: los equipos de negocio no confían en cajas negras; un summary plot que confirma su intuición ("sí, la morosidad previa es lo que más pesa") y añade matices nuevos convierte escépticos en aliados. Es, en la práctica, la herramienta de venta interna del modelo.
  • Regulación: en crédito, seguros y salud, el derecho a explicación de decisiones automatizadas (GDPR art. 22, AI Act para sistemas de alto riesgo, normativas sectoriales de banca) exige poder justificar decisiones individuales. SHAP local es hoy el estándar de facto para generar esos "motivos principales de denegación".
  • Debugging: SHAP destapa fugas (una feature con SHAP dominante y sospechosamente "perfecta"), sesgos (el código postal actuando como proxy de variables protegidas) y artefactos, antes de que lleguen a producción.

Advertencia

SHAP explica qué hace el modelo, no cómo funciona el mundo. Si el modelo aprendió una correlación espuria, SHAP te la mostrará con total claridad... como si fuera un motivo legítimo. Es una lupa sobre el modelo, no un detector de causalidad. Las afirmaciones causales exigen otras herramientas (y otro capítulo).


10. Ejemplo integrador: predicción de impago

Juntemos todo el capítulo en un caso completo: una financiera quiere predecir el impago de préstamos personales (clase positiva ≈ 15 %, desbalanceada). Compararemos honestamente Random Forest, XGBoost y LightGBM con CV, tuning, early stopping, permutation importance y una decisión final coste/beneficio.

# ================================================================
# EJEMPLO INTEGRADOR — Predicción de impago
# Pipeline completo: datos -> CV honesta -> tuning -> decisión
# ================================================================
import numpy as np
import pandas as pd

from sklearn.model_selection import (train_test_split, StratifiedKFold,
                                     cross_val_score, RandomizedSearchCV)
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.metrics import (roc_auc_score, average_precision_score,
                             confusion_matrix, classification_report)
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier, early_stopping

# ----------------------------------------------------------------
# 1) DATASET SINTÉTICO (~800 filas, desbalanceado 85/15)
#    Simulamos features de crédito con relaciones realistas.
# ----------------------------------------------------------------
rng = np.random.default_rng(42)                  # generador reproducible
n = 800                                          # ~800 solicitudes de préstamo

edad          = rng.integers(21, 70, n)          # edad del solicitante
ingresos      = rng.lognormal(mean=7.9, sigma=0.5, size=n)   # ingresos mensuales (€), asimétricos
deuda         = rng.lognormal(mean=6.5, sigma=0.9, size=n)   # deuda total existente (€)
ratio_deuda   = deuda / ingresos                 # ratio deuda/ingresos: LA señal clásica
antiguedad    = np.clip(rng.exponential(6, n), 0, 40)        # años en el empleo actual
impagos_prev  = rng.poisson(0.3, n)              # nº de impagos anteriores
consultas_6m  = rng.poisson(1.2, n)              # consultas de crédito últimos 6 meses
canal         = rng.choice(["online", "oficina", "broker"], n, p=[0.5, 0.3, 0.2])

# Probabilidad "real" de impago: combinación no lineal + ruido
# (esto es lo que el modelo debe descubrir; en la vida real no la conoces)
logit = (-3.2                                    # base: impago poco frecuente
         + 2.2 * np.tanh(ratio_deuda * 1.5)      # ratio deuda alto -> mucho riesgo (saturante)
         + 0.85 * impagos_prev                   # cada impago previo suma mucho
         + 0.22 * consultas_6m                   # buscar crédito compulsivamente = señal
         - 0.05 * antiguedad                     # estabilidad laboral protege
         - 0.012 * (edad - 21)                   # más edad, algo menos riesgo
         + 0.35 * (canal == "broker").astype(float)   # el canal broker trae peor cartera
         + rng.normal(0, 0.6, n))                # ruido irreducible (el mundo es ruidoso)
y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)   # muestreo Bernoulli

X = pd.DataFrame({
    "edad": edad, "ingresos": ingresos, "deuda": deuda,
    "ratio_deuda": ratio_deuda, "antiguedad_empleo": antiguedad,
    "impagos_previos": impagos_prev, "consultas_6m": consultas_6m,
    "canal": pd.Categorical(canal),              # dtype category: LightGBM la usará nativa
})
print(f"Tasa de impago: {y.mean():.1%}  ({y.sum()} de {n})")   # ~15%

# ----------------------------------------------------------------
# 2) SPLITS: test final intocable + validación para early stopping
# ----------------------------------------------------------------
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42)         # 160 filas de test
X_tr, X_val, y_tr, y_val = train_test_split(
    X_temp, y_temp, test_size=0.2, stratify=y_temp, random_state=42)

# Ratio de desbalanceo para XGBoost (negativos/positivos en train):
ratio = (y_tr == 0).sum() / (y_tr == 1).sum()
print(f"scale_pos_weight = {ratio:.1f}")

# Versión one-hot para los modelos que no comen categóricas nativas (RF, XGB):
X_tr_ohe  = pd.get_dummies(X_tr,  columns=["canal"])
X_val_ohe = pd.get_dummies(X_val, columns=["canal"])
X_test_ohe = pd.get_dummies(X_test, columns=["canal"])

# ----------------------------------------------------------------
# 3) COMPARACIÓN BASE con validación cruzada estratificada 5-fold
#    Misma CV para los tres -> comparación justa (cap. 4)
# ----------------------------------------------------------------
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

candidatos = {
    "RandomForest": RandomForestClassifier(
        n_estimators=400, min_samples_leaf=3, class_weight="balanced",
        n_jobs=-1, random_state=42),
    "XGBoost": XGBClassifier(
        n_estimators=300, learning_rate=0.05, max_depth=4,
        subsample=0.8, colsample_bytree=0.8,
        scale_pos_weight=ratio, eval_metric="auc", random_state=42),
    "LightGBM": LGBMClassifier(
        n_estimators=300, learning_rate=0.05, num_leaves=15,
        min_child_samples=25, class_weight="balanced",
        random_state=42, verbose=-1),
}

X_temp_ohe = pd.get_dummies(X_temp, columns=["canal"])  # CV sobre train+val juntos
for nombre, modelo in candidatos.items():
    # cross_val_score entrena 5 veces, cada vez validando en un fold distinto:
    scores = cross_val_score(modelo, X_temp_ohe, y_temp,
                             cv=cv, scoring="roc_auc", n_jobs=-1)
    print(f"{nombre:13s} AUC = {scores.mean():.3f} ± {scores.std():.3f}")

# Salida típica:
#   RandomForest  AUC = 0.836 ± 0.031
#   XGBoost       AUC = 0.848 ± 0.028
#   LightGBM      AUC = 0.851 ± 0.026
# Lectura honesta: con ±0.03 de desviación, los tres SOLAPAN.
# Con 800 filas nadie puede proclamar un ganador por milésimas (cap. 4).

# ----------------------------------------------------------------
# 4) TUNING del favorito (LightGBM) con RandomizedSearchCV
# ----------------------------------------------------------------
espacio = {
    "num_leaves":        [7, 15, 31],            # complejidad por árbol (leaf-wise)
    "learning_rate":     [0.02, 0.05, 0.1],      # tamaño del paso
    "min_child_samples": [10, 25, 50],           # anti-memorización de hojas
    "subsample":         [0.7, 0.85, 1.0],       # filas por árbol
    "colsample_bytree":  [0.6, 0.8, 1.0],        # columnas por árbol
    "reg_lambda":        [0.0, 1.0, 5.0],        # regularización L2
}

busqueda = RandomizedSearchCV(
    LGBMClassifier(n_estimators=400, class_weight="balanced",
                   random_state=42, verbose=-1),
    param_distributions=espacio,
    n_iter=40,                # 40 combinaciones al azar (de 729 posibles):
                              # explora bien el espacio a un 5% del coste del grid
    cv=cv,                    # la MISMA CV estratificada de antes
    scoring="roc_auc",
    n_jobs=-1, random_state=42,
)
busqueda.fit(X_temp_ohe, y_temp)                 # nota: tuning sin tocar el test
print(f"Mejor AUC (CV): {busqueda.best_score_:.3f}")
print(f"Mejores params: {busqueda.best_params_}")

# ----------------------------------------------------------------
# 5) MODELO FINAL: mejores params + early stopping + categóricas nativas
# ----------------------------------------------------------------
final = LGBMClassifier(
    **busqueda.best_params_,      # los hiperparámetros ganadores de la búsqueda
    n_estimators=2000,            # techo alto: ahora manda el early stopping
    class_weight="balanced",
    random_state=42, verbose=-1,
)
final.fit(
    X_tr, y_tr,                   # DataFrame original: 'canal' entra como categórica nativa
    eval_set=[(X_val, y_val)],
    eval_metric="auc",
    callbacks=[early_stopping(stopping_rounds=100)],
)
print(f"Árboles usados: {final.best_iteration_} de 2000")

# ----------------------------------------------------------------
# 6) EVALUACIÓN FINAL sobre el test intocable (una sola vez)
# ----------------------------------------------------------------
proba = final.predict_proba(X_test)[:, 1]        # probabilidades de impago
print(f"ROC-AUC test : {roc_auc_score(y_test, proba):.3f}")
print(f"PR-AUC  test : {average_precision_score(y_test, proba):.3f}")
# Con 15% de positivos, la PR-AUC es más informativa que la ROC-AUC (cap. 4)

# ----------------------------------------------------------------
# 7) PERMUTATION IMPORTANCE sobre test: la historia honesta
# ----------------------------------------------------------------
perm = permutation_importance(final, X_test, y_test, scoring="roc_auc",
                              n_repeats=30, random_state=42, n_jobs=-1)
ranking = pd.Series(perm.importances_mean, index=X_test.columns).sort_values(ascending=False)
print("\nImportancia por permutación (caída de AUC al barajar):")
print(ranking.round(4))
# Esperado: ratio_deuda e impagos_previos arriba (así generamos los datos);
# si apareciera otra cosa dominando, sería momento de sospechar.

# ----------------------------------------------------------------
# 8) DECISIÓN DE NEGOCIO: elegir umbral por COSTE, no por accuracy
# ----------------------------------------------------------------
# Matriz de costes del negocio (por préstamo medio):
#   - Falso negativo (concedes a un moroso):   pierdes ~2.000 €
#   - Falso positivo (deniegas a un buen cliente): pierdes ~300 € de margen
COSTE_FN, COSTE_FP = 2000, 300

umbrales = np.linspace(0.05, 0.95, 91)           # barrido de umbrales de decisión
costes = []
for u in umbrales:
    pred = (proba >= u).astype(int)              # denegar si proba de impago >= umbral
    tn, fp, fn, tp = confusion_matrix(y_test, pred).ravel()
    costes.append(fn * COSTE_FN + fp * COSTE_FP) # coste total de los errores

mejor_u = umbrales[int(np.argmin(costes))]
print(f"\nUmbral óptimo por coste: {mejor_u:.2f}  "
      f"(coste esperado: {min(costes):,} € en el test)")
print(classification_report(y_test, (proba >= mejor_u).astype(int),
                            target_names=["paga", "impaga"], digits=3))
# El umbral óptimo saldrá BAJO (p.ej. ~0.25-0.35): como un FN cuesta ~7x más
# que un FP, compensa denegar "en caso de duda". Esta asimetría — no la
# accuracy — es la decisión de negocio. El modelo da probabilidades;
# el umbral lo pone la cuenta de resultados.

Lectura de resultados, punto por punto:

  1. La CV dice "empate técnico" entre los tres modelos: con 800 filas, las diferencias de ±0.015 AUC están dentro del ruido (±0.03 de std). Elegir LightGBM aquí es legítimo por criterios operativos (velocidad, categóricas nativas), no porque "ganara".
  2. RandomizedSearch con 40 iteraciones cubre bien un espacio de 729 combinaciones; el grid completo habría costado 18× más para una mejora esperable nula (capítulo 5).
  3. El early stopping paró muy por debajo del techo de 2 000 árboles — con learning rate bajo y dataset pequeño, es lo esperable y es la señal de que el freno funciona.
  4. La permutation importance confirma el mecanismo: ratio_deuda e impagos_previos dominan, que es exactamente la señal que sembramos. En un proyecto real, este paso valida (o destruye) la confianza en el modelo.
  5. La decisión final no es una métrica, es un umbral con euros. El mejor modelo del mundo con el umbral 0.5 por defecto puede perder dinero frente a un modelo mediano con el umbral correcto.

Ejercicio rápido 6

En el paso 8, ¿por qué el umbral óptimo de coste sale muy por debajo de 0.5? ¿Y qué pasaría con el umbral óptimo si el coste del falso positivo subiera de 300 € a 1 800 €?

Ver solución **Por qué sale bajo:** porque los errores son asimétricos: conceder a un moroso (FN) cuesta 2 000 €, denegar a un buen pagador (FP) solo 300 €. El punto de indiferencia teórico está donde `p·COSTE_FN = (1-p)·COSTE_FP` → `p* = 300/(300+2000) ≈ 0.13`. Con calibración imperfecta y efectos de muestra el barrido empírico dará algo entre 0.2 y 0.35, pero la lógica es esa: cuando equivocarte hacia un lado cuesta ~7× más, disparas la alarma con mucha menos evidencia. **Si COSTE_FP sube a 1 800 €:** los errores se vuelven casi simétricos (2 000 vs 1 800), y el punto de indiferencia sube a `1800/(1800+2000) ≈ 0.47`: el umbral óptimo se acercaría a ~0.5. Moraleja: el umbral no es una propiedad del modelo, es una propiedad de la **economía del problema** — y debe revisarse cuando cambian los costes, aunque el modelo no cambie.

11. Caso empresarial: de reglas manuales a LightGBM en una aseguradora

Caso empresarial

Aseguradora de auto, ~900 000 pólizas, mercado español. Nombres alterados; el patrón es real y se repite en media industria.

Situación de partida (2023). El pricing de las pólizas se calculaba con un sistema de reglas manuales acumuladas durante 18 años: una tarifa base por segmento y ~140 recargos y descuentos escritos en un motor de reglas ("+12 % si conductor < 25 años", "−5 % si garaje propio", "+8 % en tal código postal..."). Nadie recordaba el porqué de un tercio de las reglas. Actuarios expertos las retocaban dos veces al año, tras semanas de análisis. Los síntomas clásicos de un sistema así: selección adversa — la competencia, con pricing más fino, les robaba los buenos riesgos con precios mejores, y a la aseguradora le quedaba una cartera cada vez peor: el ratio de siniestralidad subía 1-2 puntos al año sin que ninguna regla individual explicara por qué.

El proyecto (2024). Un equipo de tres personas (una actuaria senior, un data scientist, un ingeniero) propuso un modelo de frecuencia y severidad de siniestros con LightGBM sobre 6 años de historial (~4 M de registros póliza-año, 60 features: vehículo, conductor, zona, historial). Por qué LightGBM y no otra cosa: datos 100 % tabulares con muchas categóricas de alta cardinalidad (modelo de vehículo, código postal), volumen que exigía velocidad de reentrenamiento, y necesidad de explicar cada precio (TreeSHAP exacto). Metodología de manual: validación temporal (entrenar en 2018-2022, validar en 2023 — jamás CV aleatoria con datos temporales), early stopping, tuning por etapas como el de la sección 5.2, y SHAP para cada tarifa propuesta.

La resistencia interna. Fue el 70 % del proyecto, y conviene contarla porque te la encontrarás:

  • Los actuarios: "llevamos décadas afinando esas reglas; una caja negra no puede capturar lo que sabemos". Respuesta del equipo: no confrontar, traducir. Sesión a sesión, mostraron con summary plots de SHAP que el modelo redescubría casi todas las reglas históricas (edad joven → recargo, garaje → descuento)... con dos diferencias: cuantificaba mejor las magnitudes y encontraba interacciones que ninguna regla manual tenía (el recargo por conductor joven debía ser mucho mayor en vehículos potentes y casi nulo en utilitarios urbanos — obvio a posteriori, invisible en reglas aditivas). Cuando los actuarios vieron sus propias intuiciones validadas y refinadas, pasaron de opositores a coautores: ellos definieron las restricciones de monotonía (más caballos no puede bajar la prima, por coherencia comercial) que se impusieron al modelo con monotone_constraints.
  • El área comercial: miedo a saltos de precio que espantaran a la cartera. Se pactó un despliegue con límites de variación (ninguna renovación podía cambiar más de ±X % por año) y un piloto A/B en dos provincias durante 6 meses.
  • Cumplimiento normativo: el supervisor exige justificar las tarifas y prohíbe discriminación por variables protegidas. Aquí SHAP fue decisivo — y también reveló un problema real: el código postal actuaba parcialmente como proxy socioeconómico; se rehízo la feature con variables de riesgo vial objetivas de la zona.

Resultados (piloto 2024, despliegue 2025). En las provincias piloto frente al grupo de control: mejora del ratio combinado de ~2.3 puntos — mezcla de mejor selección de riesgo (precios altos donde había siniestralidad oculta) y recuperación de buenos riesgos que antes pagaban de más. La mejora en el Gini de la tarifa frente al motor de reglas fue de dos dígitos. El reentrenamiento pasó de "dos semanas de comité, dos veces al año" a "pipeline mensual con validación automática y revisión actuarial de un día". Nada de esto salió del modelo en sí: salió del sistema alrededor del modelo.

Y la decisión más interesante: mantuvieron un GLM como fallback. Junto al LightGBM, el equipo mantiene un modelo lineal generalizado (GLM) — el estándar actuarial clásico — entrenado sobre las mismas features. Tres razones, todas de ingeniería adulta:

  1. Regulatoria: ante una inspección o disputa, el GLM ofrece coeficientes directamente auditables ("el recargo por antigüedad del carnet es exactamente β=..."); es el lenguaje que el supervisor y los peritos llevan 40 años hablando. El LightGBM tarifica; el GLM certifica que la estructura tarifaria es defendible.
  2. De monitorización: si el LightGBM y el GLM divergen bruscamente en un segmento, salta una alerta — o hay drift en los datos o el boosting está explotando un artefacto. Un modelo simple es el mejor detector de humo de un modelo complejo.
  3. De continuidad: si el pipeline del modelo principal falla (datos corruptos, bug), el sistema degrada a las tarifas del GLM en lugar de detener la contratación.

Consejo profesional

esta es la lección que debes llevarte por encima de cualquier hiperparámetro: en producción, el modelo campeón casi nunca vive solo. Un modelo complejo que gana dinero + un modelo simple que lo vigila y lo respalda es una arquitectura, no una redundancia. Los equipos que despliegan "solo el mejor modelo" son los que a los dos años no pueden explicar por qué las decisiones cambiaron.


12. Buenas prácticas

  1. Empieza siempre con una baseline en dos escalones: regresión logística (o lineal) → Random Forest con defaults. Solo pasa a boosting si el RF demuestra que hay señal no lineal que exprimir. Sabrás cuánto aporta cada capa de complejidad.
  2. Early stopping siempre en boosting, con conjunto de validación propio (nunca el test) y paciencia acorde al learning rate (η ≤ 0.05 → paciencia ≥ 50).
  3. Tunea por etapas y en orden (learning rate → complejidad del árbol → submuestreos → regularización), con RandomizedSearch, no grid exhaustivo.
  4. Métricas acordes al desbalanceo (ROC-AUC, PR-AUC, coste esperado) y umbral de decisión elegido con la matriz de costes del negocio, no el 0.5 por defecto.
  5. Permutation importance sobre validación/test para el informe; la importancia por impureza solo como vistazo rápido.
  6. SHAP antes de desplegar: valida con negocio que el modelo usa señales legítimas; busca proxies de variables protegidas y fugas.
  7. Aprovecha lo nativo: nulos y categóricas nativas en HistGB/LightGBM/CatBoost simplifican el pipeline y suelen rendir mejor que imputar + one-hot.
  8. Validación temporal si los datos tienen tiempo (scoring, pricing, churn): entrenar en el pasado, validar en el futuro. La CV aleatoria con datos temporales infla las métricas.
  9. Fija semillas y versiona modelo + datos + hiperparámetros + umbral de decisión. La reproducibilidad no es opcional en entornos regulados.
  10. Mantén un modelo simple como sombra del complejo en producción (monitorización, fallback, auditoría), como en el caso de la aseguradora.

13. Malas prácticas

  1. Usar el test set para early stopping o para elegir el umbral — es fuga: tu métrica final queda inflada y lo descubrirás en producción.
  2. Tunear los cuatro frameworks a la vez "para ver cuál gana" — semanas de cómputo para diferencias dentro del ruido; elige por criterios operativos.
  3. One-hot de categóricas de alta cardinalidad para árboles — fragmenta la señal en cientos de columnas casi vacías; usa soporte nativo o target encoding con CV.
  4. Fiarte de feature_importances_ para decisiones de negocio — el sesgo por cardinalidad puede poner arriba features inútiles.
  5. n_estimators gigante sin early stopping en boosting — es pedir overfitting por diseño (en RF es inofensivo; en boosting no).
  6. Stacking de tres capas en producción "porque en Kaggle funciona" — multiplicas coste de mantenimiento y latencia por décimas de métrica que el negocio no notará.
  7. Ignorar el desbalanceo y reportar accuracy — el clásico "93 % de accuracy" en un problema con 93 % de clase mayoritaria.
  8. Interpretar SHAP causalmente — "si bajamos X del cliente, bajará su riesgo" no se sigue de un valor SHAP; el modelo captura correlación.
  9. Reentrenar sin validar contra el modelo anterior — cada reentrenamiento debe superar al campeón vigente en una comparación con la misma vara de medir.
  10. Presentar el modelo a negocio sin una sola explicación individual — sin "recibos" de predicciones concretas, la confianza no llega y la adopción muere.

14. Errores comunes

# Error Síntoma Causa Solución
1 Early stopping sobre el test Métrica de test excelente que se desploma en producción El test decidió cuándo parar → fuga Tres conjuntos: train / validación (stopping) / test intocable
2 num_leaves enorme en LightGBM con pocos datos Train ≈ 1.0, validación mediocre Leaf-wise memoriza filas num_leaves 15–31, min_child_samples ≥ 20, early stopping
3 Comparar modelos con CVs distintas o semillas distintas "XGBoost gana por 0.004" irreproducible Ruido de partición, no señal Misma StratifiedKFold con misma semilla para todos; mirar la std
4 Confiar en importancia por impureza Un ID o feature de alta cardinalidad domina el ranking Sesgo MDI hacia cardinalidad + cálculo en train Permutation importance sobre datos no vistos
5 One-hot masivo antes de un modelo de árboles Entrenamiento lento, métrica peor que con la categórica original Señal fragmentada en columnas dispersas Categóricas nativas (LightGBM/HistGB/CatBoost)
6 Umbral 0.5 por defecto en problema desbalanceado con costes asimétricos Recall ridículo de la clase que importa; pérdidas evitables El 0.5 no sabe nada de tu matriz de costes Barrer umbrales optimizando coste esperado (sección 10, paso 8)
7 Learning rate alto "para entrenar rápido" Curva de validación que mejora y se degrada en dientes de sierra Sobre-correcciones que amplifican ruido η ≤ 0.1 (mejor 0.03–0.05) + más árboles + early stopping
8 CV aleatoria con datos temporales AUC 0.90 en validación, 0.72 en producción Filas del futuro en el train de cada fold Validación temporal (train pasado → test futuro)
9 Bagging de modelos estables (lineales) El ensemble no mejora nada al modelo individual Sin inestabilidad no hay diversidad Bagging solo con modelos inestables; para lineales, cambia de familia
10 Reportar la métrica de la búsqueda de hiperparámetros como métrica final Números finales optimistas El máximo de muchas configuraciones sobreestima (winner's curse) Evaluar la configuración ganadora sobre test nunca usado (o CV anidada)

15. FAQ — Preguntas frecuentes

1. ¿Random Forest o gradient boosting: cuál pruebo primero? Random Forest primero: casi no necesita tuning, es robusto y paraleliza. Si su resultado ya sirve al negocio, quizá no necesites más. Pasa a boosting (con early stopping y tuning por etapas) cuando el último punto de métrica valga el esfuerzo. En datasets muy ruidosos o con etiquetas dudosas, el RF puede incluso ganar.

2. ¿Más árboles pueden sobreajustar? En Random Forest, no: el error de test converge y se aplana; solo pagas CPU. En boosting, sí: cada árbol adicional sigue ajustando residuos y acaba persiguiendo ruido. Por eso en RF pones "muchos y a olvidarse" y en boosting pones un techo alto con early stopping.

3. ¿Tengo que escalar las features para estos modelos? No. Los árboles deciden por umbrales (x > c), que son invariantes a transformaciones monótonas de escala. StandardScaler no aporta nada a RF ni a boosting. Sí lo necesitan los modelos lineales o KNN que combines en un Voting/Stacking (por eso en la sección 7 la logística lleva su propio pipeline con escalado).

4. ¿Cómo manejo el desbalanceo en estos modelos? Primera línea: class_weight="balanced" (RF, LightGBM) o scale_pos_weight (XGBoost), más una métrica adecuada (PR-AUC) y un umbral elegido por costes. El resampling tipo SMOTE rara vez es necesario con árboles bien ponderados, y si lo usas debe ir dentro del pipeline de CV para no contaminar los folds de validación.

5. ¿Puedo usar early stopping dentro de RandomizedSearchCV? Sí, y es lo ideal, con un matiz operativo: necesitas pasar el eval_set en cada fit. Opciones: usar HistGradientBoostingClassifier (early stopping interno automático con validation_fraction, cero fricción), o en XGBoost/LightGBM fijar el eval_set mediante fit_params, aceptando que sea un split fijo. La alternativa pragmática: buscar hiperparámetros con n_estimators moderado y fijo, y aplicar early stopping solo al reentrenar la configuración ganadora.

6. ¿Qué hago con las categóricas: one-hot, target encoding o soporte nativo? Para modelos de árboles, en este orden de preferencia: (1) soporte nativo (LightGBM, CatBoost, HistGB) — simple y eficaz; (2) target encoding con CV para cardinalidades enormes en frameworks sin soporte nativo (cuidado extremo con la fuga: siempre out-of-fold); (3) one-hot solo con cardinalidad baja (< 10-15 categorías). One-hot masivo en árboles es la opción por defecto de mucha gente y la peor de las tres.

7. ¿SHAP no es demasiado lento para producción? TreeSHAP sobre modelos de árboles es exacto y rápido: explicar una predicción cuesta milisegundos. Lo que puede ser costoso es explicar millones de filas de golpe (para dashboards globales, muestrea). El SHAP "model-agnostic" (KernelSHAP) sí es lento — otra razón práctica para preferir árboles en entornos que exigen explicación por decisión.

8. ¿Cuándo debería plantearme una red neuronal para datos tabulares? Señales legítimas: millones de filas con señal compleja y presupuesto de tuning; necesidad de embeddings compartidos con otras modalidades (tabla + texto + imagen en el mismo modelo); aprendizaje online extremo; o transfer learning tabular real para tu dominio. Si tu caso es "tabla de 50 000 filas para predecir churn", la respuesta en 2026 sigue siendo boosting, y quien te diga lo contrario debería traer un benchmark, no un argumento de autoridad.

9. ¿El boosting me sirve para regresión, o solo clasificación? Todo lo del capítulo aplica a regresión cambiando la pérdida (XGBRegressor, LGBMRegressor, HistGradientBoostingRegressor): residuos aún más literales (con pérdida cuadrática, el árbol ajusta exactamente y − F(x)), mismas reglas de early stopping y tuning. Además soportan pérdidas robustas (Huber) y regresión cuantílica para intervalos de predicción — muy valiosa en pricing y demanda.

10. Mi XGBoost da AUC 0.99. ¿Debo alegrarme? Debes asustarte. En problemas reales de negocio, un AUC ≥ 0.97 casi siempre significa fuga de datos: una feature que codifica el target (una columna escrita después del evento, un ID correlacionado con el tiempo, el propio target disfrazado). Revisa la permutation importance: si una feature domina de forma aplastante, investiga su origen y su fecha de disponibilidad antes de celebrar nada.

16. Resumen del capítulo

  • Un ensemble combina modelos imperfectos pero diversos y mejores que el azar; la matemática de Condorcet garantiza que el comité supera al individuo cuando los errores no están correlacionados. Toda la ingeniería de ensembles es fabricación de diversidad.
  • Bagging ataca la varianza: muestras bootstrap (cada árbol ve ~63 % de filas únicas) + promedio. Regalo incluido: la OOB score, validación gratuita con el 37 % restante.
  • Random Forest = bagging + sorteo de features por split. La doble aleatoriedad descorrelaciona los árboles. Robusto, paralelo, casi sin tuning: la mejor primera opción seria. Sus feature_importances_ mienten con alta cardinalidad; usa permutation importance sobre datos no vistos.
  • Boosting ataca el sesgo: árboles pequeños en secuencia, cada uno ajustando los residuos del conjunto anterior, sumados con un learning rate corto. Potentísimo y propenso a sobreajustar: sus frenos son η bajo, árboles simples, submuestreo, regularización y — sobre todo — early stopping con conjunto de validación propio.
  • Implementaciones modernas: HistGB (nativo sklearn, nulos y categóricas), XGBoost (regularización y madurez industrial), LightGBM (leaf-wise, el más rápido, categóricas nativas), CatBoost (categóricas de alta cardinalidad, buenos defaults). Rinden parecido bien tuneadas: elige por criterios operativos.
  • Voting y stacking exprimen décimas adicionales combinando familias distintas: imprescindibles en competición, rara vez rentables en producción.
  • En datos tabulares medianos, el boosting sigue ganando al deep learning en 2026: mejor métrica con menos datos, menos coste y más interpretabilidad. La sinergia real es usar LLMs para fabricar features desde texto y dárselas al boosting.
  • SHAP descompone cada predicción en contribuciones aditivas por feature (reparto justo de Shapley, exacto y rápido en árboles): es la llave de la adopción por negocio y del cumplimiento regulatorio — pero explica el modelo, no la causalidad.
  • Y la lección de sistema: el modelo ganador en producción convive con un modelo simple que lo vigila y lo respalda. La métrica gana benchmarks; la arquitectura gana años de servicio.

En el próximo capítulo cruzamos la frontera que separa el notebook del mundo real: ML en producción — serialización, APIs, monitorización, drift y el ciclo de vida completo de un modelo que tiene que funcionar el día 400, no solo el día 1.

17. Bibliografía y recursos

  • Breiman, L. (1996). Bagging Predictors. Machine Learning, 24. — https://link.springer.com/article/10.1007/BF00058655
  • Breiman, L. (2001). Random Forests. Machine Learning, 45. — https://link.springer.com/article/10.1023/A:1010933404324
  • Freund, Y. & Schapire, R. (1997). A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting (AdaBoost). — https://www.sciencedirect.com/science/article/pii/S002200009791504X
  • Friedman, J. (2001). Greedy Function Approximation: A Gradient Boosting Machine. Annals of Statistics. — https://projecteuclid.org/journals/annals-of-statistics/volume-29/issue-5/Greedy-function-approximation-A-gradient-boosting-machine/10.1214/aos/1013203451.full
  • Chen, T. & Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. KDD 2016. — https://arxiv.org/abs/1603.02754
  • Documentación oficial de XGBoost. — https://xgboost.readthedocs.io/
  • Ke, G. et al. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. NeurIPS 2017. — https://papers.nips.cc/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html
  • Documentación oficial de LightGBM. — https://lightgbm.readthedocs.io/
  • Prokhorenkova, L. et al. (2018). CatBoost: unbiased boosting with categorical features. NeurIPS 2018. — https://arxiv.org/abs/1706.09516
  • Lundberg, S. & Lee, S.-I. (2017). A Unified Approach to Interpreting Model Predictions (SHAP). NeurIPS 2017. — https://arxiv.org/abs/1705.07874
  • Documentación de SHAP. — https://shap.readthedocs.io/
  • Grinsztajn, L., Oyallon, E. & Varoquaux, G. (2022). Why do tree-based models still outperform deep learning on tabular data? NeurIPS 2022. — https://arxiv.org/abs/2207.08815
  • scikit-learn: Ensembles (Bagging, Forests, Voting, Stacking, HistGradientBoosting). — https://scikit-learn.org/stable/modules/ensemble.html
  • scikit-learn: Permutation importance (y su comparación con MDI). — https://scikit-learn.org/stable/modules/permutation_importance.html
  • Hastie, T., Tibshirani, R. & Friedman, J. The Elements of Statistical Learning (caps. 10, 15 y 16). — https://hastie.su.domains/ElemStatLearn/

Anterior Módulo Siguiente
Capítulo 5: Features, pipelines y validación 02-MACHINE-LEARNING Capítulo 7: ML en producción