Saltar a contenido

Gradient Boosting: XGBoost, LightGBM y CatBoost

1 Introducción

Los bosques aleatorios (ver Bosques Aleatorios) combinan muchos árboles de decisión de forma paralela e independiente. Cada árbol se entrena con una muestra bootstrap diferente y, al final, las predicciones se promedian (regresión) o se votan (clasificación). Esta estrategia se llama bagging y reduce la varianza sin aumentar demasiado el sesgo.

Pero existe otra forma de combinar árboles: en lugar de entrenarlos en paralelo y promediar, podemos entrenarlos uno tras otro, donde cada nuevo árbol intenta corregir los errores del anterior. Esta estrategia se llama boosting y es la base de los algoritmos más potentes para datos tabulares: Gradient Boosting, XGBoost, LightGBM y CatBoost.

Nota

¿Por qué son importantes? Los métodos de Gradient Boosting dominan las competiciones de Kaggle en datos tabulares y son el estándar en la industria para problemas de clasificación y regresión con estructuras de tabla (clientes, transacciones, mediciones).

2 Del bagging al boosting: una filosofía diferente

2.1 El problema de un solo árbol

Un árbol de decisión profundo se sobreajusta: memoriza el entrenamiento pero falla en test. Un árbol poco profundo (débil) tiene bajo sobreajuste pero también baja precisión.

2.2 Bagging: reducir la varianza

Los bosques aleatorios resuelven este problema entrenando muchos árboles fuertes (profundos) de forma paralela y promediando sus predicciones. La fórmula de la varianza del promedio de \(B\) predictores con correlación \(\rho\) es:

\[ \mathrm{Var}(\bar{h}) = \rho\,\sigma^2 + \frac{1-\rho}{B}\sigma^2 \]

Al aumentar \(B\), el segundo término se anula, pero el primero (\(\rho\,\sigma^2\)) no se puede reducir solo con más árboles. Ahí es donde el boosting toma un enfoque radicalmente diferente.

2.3 Boosting: reducir el sesgo

En lugar de promediar árboles fuertes e independientes, el boosting construye secuencialmente árboles débiles (poco profundos, típicamente de profundidad 3-6), donde cada uno corrige los errores del conjunto anterior.

  Árbol 1 (débil)     Árbol 2 (corrige)     Árbol 3 (corrige)
  ┌─────┐             ┌─────┐               ┌─────┐
  │  A  │  error →    │  B  │  error →      │  C  │  → Predicción Final
  └─────┘             └─────┘               └─────┘

  Pred_1(x)           Pred_2(x) = Pred_1(x) + α₁·T₂(x)
                                        Pred_3(x) = Pred_2(x) + α₂·T₃(x)

La predicción final es una suma ponderada de todos los árboles:

\[ F_M(x) = \sum_{m=1}^{M} \gamma_m \, h_m(x) \]

Donde \(h_m(x)\) es el árbol débil \(m\)-ésimo y \(\gamma_m\) es su peso (tasa de aprendizaje).

3 Gradient Boosting como descenso de gradiente funcional

3.1 El modelo aditivo

Gradient Boosting minimiza una función de pérdida \(L(y, F(x))\) construyendo un modelo aditivo \(F(x)\). La clave es entender que \(F(x)\) es la suma de muchos árboles débiles:

\[ F_0(x) = \arg\min_c \sum_{i=1}^N L(y_i, c) \]
\[ F_m(x) = F_{m-1}(x) + \gamma_m \, h_m(x) \]

Donde \(F_0(x)\) es un valor constante inicial (la predicción más simple, como la media en regresión o el log-odds en clasificación).

3.2 Pseudoresiduos: el corazón del método

Para decidir qué corrige el siguiente árbol \(h_m(x)\), calculamos los pseudoresiduos:

\[ r_{im} = -\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)} \Bigg|_{F = F_{m-1}} \]

Los pseudoresiduos son las direcciones en las que la función de pérdida decrece más rápido. Cada árbol siguiente se entrena para predecir esos pseudoresiduos, no los valores originales \(y_i\).

¿Por qué funciona? Si la función de pérdida es el error cuadrático (\(L = \frac{1}{2}(y - F(x))^2\)), los pseudoresiduos son simplemente los residuos reales:

\[ r_{im} = -\frac{\partial}{\partial F(x_i)} \frac{1}{2}(y_i - F(x_i))^2 = y_i - F_{m-1}(x_i) \]

Pero si usamos otra pérdida (como la entropía cruzada para clasificación), los pseudoresiduos son más complejos y por eso el método se llama Gradient Boosting: usamos el gradiente de la función de pérdida.

3.3 El algoritmo completo

1. Inicializar: F₀(x) = constante (media en regresión, log-odds en clasificación)
2. Para m = 1, 2, ..., M:
   a. Calcular pseudoresiduos: rᵢₘ = -∂L/∂F(xᵢ) evaluados en Fₘ₋₁
   b. Entrenar árbol débil hₘ(x) para predecir rᵢₘ
   c. Calcular el peso óptimo: γₘ = argmin_γ Σ L(yᵢ, Fₘ₋₁(xᵢ) + γ·hₘ(xᵢ))
   d. Actualizar: Fₘ(x) = Fₘ₋₁(x) + γₘ·hₘ(x)
3. Devolver Fₘ(x)

4 Gradient Boosting para Regresión

En regresión, la función de pérdida más común es el error cuadrático medio:

\[ L(y, F(x)) = \frac{1}{2}(y - F(x))^2 \]

4.1 Pseudoresiduos en regresión

Los pseudoresiduos son simplemente los residuos reales:

\[ r_{im} = y_i - F_{m-1}(x_i) \]

Nota

¿Por qué real − predicción y no al revés? Es la convención estándar en estadística: - Si \(r < 0\): sobreestimamos (predijimos más de lo real) - Si \(r > 0\): subestimamos (predijimos menos de lo real) - Si \(r = 0\): acertamos

Esto permite interpretar fácilmente el signo del error.

Es decir, cada nuevo árbol intenta predecir cuánto se equivocó el modelo anterior en cada punto.

4.2 Ejemplo numérico paso a paso

Supongamos que queremos predecir el precio de una casa basándonos en su tamaño (m²). Tenemos 5 ejemplos de entrenamiento:

Casa Tamaño (m²) Precio (miles €)
A 50 150
B 80 250
C 100 300
D 120 350
E 150 450

4.2.1 Paso 0: Inicialización

\[ F_0(x) = \bar{y} = \frac{150 + 250 + 300 + 350 + 450}{5} = 300 \]

4.2.2 Paso 1: Primer árbol débil

Calculamos los residuos (pseudoresiduos):

Casa Precio real \(F_0(x)\) Residuo \(r_{i1}\)
A 150 300 -150
B 250 300 -50
C 300 300 0
D 350 300 +50
E 450 300 +150

Entrenamos un árbol débil (profundidad 2) que prediga los residuos. El árbol divide en tamaño ≤ 90:

  • Si tamaño ≤ 90: predice la media de \(\{-150, -50\} = \frac{-150 + (-50)}{2} = -100\)
  • Si tamaño > 90: predice la media de \(\{0, +50, +150\} = \frac{0 + 50 + 150}{3} = +66.67\)

Con \(\gamma_1 = 0.1\) (tasa de aprendizaje), actualizamos:

\[ F_1(x) = F_0(x) + 0.1 \cdot h_1(x) \]
Casa \(F_0(x)\) \(h_1(x)\) \(F_1(x)\) Precio real Error²
A 300 -100 290 150 19600
B 300 -100 290 250 1600
C 300 +66.67 306.67 300 44.44
D 300 +66.67 306.67 350 1877.78
E 300 +66.67 306.67 450 20544.44

¿Cómo se calcula el error cuadrático?

El error cuadrático es la suma de los errores al cuadrado para cada casa:

\[ \text{Error}^2 = \sum_{i=1}^{N} (y_i - F(x_i))^2 \]

Con \(F_0 = 300\):

\[ \text{Error}^2 = (150-300)^2 + (250-300)^2 + (300-300)^2 + (350-300)^2 + (450-300)^2 \]
\[ = 22500 + 2500 + 0 + 2500 + 22500 = \mathbf{50.000} \]

Con \(F_1\):

\[ \text{Error}^2 = (150-290)^2 + (250-290)^2 + (300-306.67)^2 + (350-306.67)^2 + (450-306.67)^2 \]
\[ = 19600 + 1600 + 44.44 + 1877.78 + 20544.44 = \mathbf{43.667} \]

El error cuadrático total bajó de 50.000 (con \(F_0\)) a 43.667 (con \(F_1\)). Un solo árbol débil ya mejoró el modelo.

4.2.3 Paso 2: Segundo árbol débil

Calculamos los nuevos residuos con \(F_1(x)\):

Casa Precio real \(F_1(x)\) Residuo \(r_{i2}\)
A 150 290 -140
B 250 290 -40
C 300 306.67 -6.67
D 350 306.67 +43.33
E 450 306.67 +143.33

Entrenamos otro árbol débil para predecir estos nuevos residuos. El árbol divide en tamaño ≤ 90:

  • Si tamaño ≤ 90: predice la media de \(\{-140, -40\} = \frac{-140 + (-40)}{2} = -90\)
  • Si tamaño > 90: predice la media de \(\{-6.67, +43.33, +143.33\} = \frac{-6.67 + 43.33 + 143.33}{3} = +60\)

Con \(\gamma_2 = 0.1\), actualizamos:

\[ F_2(x) = F_1(x) + 0.1 \cdot h_2(x) \]
Casa \(F_1(x)\) \(h_2(x)\) \(F_2(x)\) Precio real Error²
A 290 -90 281 150 16641
B 290 -90 281 250 961
C 306.67 +60 312.67 300 160.58
D 306.67 +60 312.67 350 1393.78
E 306.67 +60 312.67 450 18827.11

¿Cómo se calcula el error cuadrático con \(F_2\)?

\[ \text{Error}^2 = (150-281)^2 + (250-281)^2 + (300-312.67)^2 + (350-312.67)^2 + (450-312.67)^2 \]
\[ = 17161 + 961 + 160.58 + 1393.78 + 18827.11 = \mathbf{38.503} \]

Resumen de la mejora:

Iteración Error² Reducción
\(F_0\) (solo media) 50.000 —
\(F_1\) (1 árbol) 43.667 -6.333 (12.7%)
\(F_2\) (2 árboles) 38.503 -5.164 (11.8%)

Cada árbol nuevo reduce el error. Con más árboles, el modelo se acerca más a la verdad.

4.2.4 Predicción con datos nuevos

¿Qué pasa si entra una casa nueva que no estaba en entrenamiento? Supongamos que tenemos una casa de 100 m²:

Casa nueva Tamaño (m²)
F 100

La predicción se hace así:

  1. Empezamos con \(F_0 = 300\) (la media del entrenamiento)
  2. Pasamos por el Árbol 1: 100 m² > 90 → \(h_1(x) = +66.67\)
  3. Pasamos por el Árbol 2: 100 m² > 90 → \(h_2(x) = +60\)
  4. Sumamos todo:
\[ \hat{y} = F_0 + \gamma_1 \cdot h_1(x) + \gamma_2 \cdot h_2(x) = 300 + 0.1 \cdot 66.67 + 0.1 \cdot 60 = 312.67 \]

El modelo predice que la casa de 100 m² cuesta 312.670 €.

La fórmula general es:

\[ \hat{y} = F_0 + \sum_{m=1}^{M} \gamma_m \cdot h_m(x) \]

Cada árbol mira en qué hoja cae el nuevo dato y suma su predicción ponderada.

Tip

Analogía: Imagina que lanzas dardos a un tablero. El primer dardo cae cerca del centro pero no en el bullseye. El segundo dardo apunta exactamente hacia dónde se desvió el primero. Cada dardo corrige el error del anterior, y después de varios lanzamientos, los dardos se agrupan en el centro.

5 Gradient Boosting para Clasificación

5.1 Log-loss (entropía cruzada)

Para clasificación binaria, la función de pérdida es la log-loss (entropía cruzada):

\[ L(y, F(x)) = -\left[ y \log(p) + (1-y) \log(1-p) \right] \]

Donde \(p = \sigma(F(x)) = \frac{1}{1 + e^{-F(x)}}\) es la probabilidad predicha.

5.2 Pseudoresiduos para clasificación

Los pseudoresiduos son:

\[ r_{im} = y_i - \sigma(F_{m-1}(x_i)) \]

Es decir, la diferencia entre la etiqueta real (0 o 1) y la probabilidad predicha hasta el momento.

5.3 Ejemplo numérico paso a paso

Supongamos que queremos predecir si un estudiante aprueba (1) o suspende (0) un examen basándonos en las horas que estudió:

Estudiante Horas estudio Resultado
A 1 0
B 2 0
C 3 1
D 4 1
E 5 1

5.3.1 Paso 0: Inicialización

En clasificación, la predicción inicial \(F_0\) es el log-odds de la clase positiva:

\[ \text{Proporción positivos} = \frac{3}{5} = 0.6 \]
\[ F_0 = \log\left(\frac{0.6}{1-0.6}\right) = \log\left(\frac{0.6}{0.4}\right) = \log(1.5) = 0.405 \]

Convertimos a probabilidad con la sigmoidea:

\[ p_0 = \sigma(F_0) = \frac{1}{1 + e^{-0.405}} = 0.6 \]

5.3.2 Paso 1: Primer árbol débil

Calculamos los pseudoresiduos (diferencia entre etiqueta real y probabilidad predicha):

Estudiante Real (\(y\)) \(p_0\) Pseudoresiduo (\(y - p_0\))
A 0 0.6 \(0 - 0.6 = -0.6\)
B 0 0.6 \(0 - 0.6 = -0.6\)
C 1 0.6 \(1 - 0.6 = +0.4\)
D 1 0.6 \(1 - 0.6 = +0.4\)
E 1 0.6 \(1 - 0.6 = +0.4\)

Entrenamos un árbol débil (profundidad 1) que prediga estos pseudoresiduos. El árbol divide en horas ≤ 2.5: - Si horas ≤ 2.5: predice la media de \(\{-0.6, -0.6\} = -0.6\) - Si horas > 2.5: predice la media de \(\{+0.4, +0.4, +0.4\} = +0.4\)

Con \(\gamma_1 = 0.1\), actualizamos en escala log-odds:

\[ F_1(x) = F_0 + 0.1 \cdot h_1(x) \]
Estudiante \(F_0\) \(h_1(x)\) \(F_1 = F_0 + 0.1 \cdot h_1(x)\) \(p_1 = \sigma(F_1)\)
A 0.405 -0.6 \(0.405 + 0.1 \cdot (-0.6) = 0.345\) 0.585
B 0.405 -0.6 \(0.405 + 0.1 \cdot (-0.6) = 0.345\) 0.585
C 0.405 +0.4 \(0.405 + 0.1 \cdot 0.4 = 0.445\) 0.610
D 0.405 +0.4 \(0.405 + 0.1 \cdot 0.4 = 0.445\) 0.610
E 0.405 +0.4 \(0.405 + 0.1 \cdot 0.4 = 0.445\) 0.610

5.3.3 Paso 2: Segundo árbol débil

Calculamos los nuevos pseudoresiduos con \(p_1\):

Estudiante Real (\(y\)) \(p_1\) Pseudoresiduo (\(y - p_1\))
A 0 0.585 \(0 - 0.585 = -0.585\)
B 0 0.585 \(0 - 0.585 = -0.585\)
C 1 0.610 \(1 - 0.610 = +0.390\)
D 1 0.610 \(1 - 0.610 = +0.390\)
E 1 0.610 \(1 - 0.610 = +0.390\)

Entrenamos otro árbol débil. Divide en horas ≤ 2.5: - Si horas ≤ 2.5: predice \(\{-0.585, -0.585\} = -0.585\) - Si horas > 2.5: predice \(\{+0.390, +0.390, +0.390\} = +0.390\)

\[ F_2(x) = F_1(x) + 0.1 \cdot h_2(x) \]
Estudiante \(F_1\) \(h_2(x)\) \(F_2 = F_1 + 0.1 \cdot h_2(x)\) \(p_2 = \sigma(F_2)\) Real
A 0.345 -0.585 \(0.345 - 0.0585 = 0.287\) 0.571 0
B 0.345 -0.585 \(0.345 - 0.0585 = 0.287\) 0.571 0
C 0.445 +0.390 \(0.445 + 0.039 = 0.484\) 0.619 1
D 0.445 +0.390 \(0.445 + 0.039 = 0.484\) 0.619 1
E 0.445 +0.390 \(0.445 + 0.039 = 0.484\) 0.619 1

5.3.4 Predicción con datos nuevos

Para un estudiante nuevo que estudió 3.5 horas:

  1. \(F_0 = 0.405\)
  2. Árbol 1: 3.5 > 2.5 → \(h_1(x) = +0.4\) → \(F_1 = 0.405 + 0.04 = 0.445\)
  3. Árbol 2: 3.5 > 2.5 → \(h_2(x) = +0.39\) → \(F_2 = 0.445 + 0.039 = 0.484\)
  4. Probabilidad: \(p = \sigma(0.484) = \frac{1}{1 + e^{-0.484}} = 0.619\)

Como \(p = 0.619 > 0.5\), el modelo predice aprueba (1).

Nota

Diferencia con regresión: En regresión, los árboles predicen valores directos. En clasificación, los árboles predicen pseudoresiduos en escala log-odds, y al final se convierten a probabilidades con la sigmoidea.

5.4 Predicción en escala log-odds

Gradient Boosting para clasificación no predice probabilidades directamente. Predice log-odds (logaritmo de la razón de probabilidades):

\[ F(x) = \log\left(\frac{p}{1-p}\right) \]

Para obtener la probabilidad, aplicamos la función sigmoidea:

\[ p = \sigma(F(x)) = \frac{1}{1 + e^{-F(x)}} \]

Importante

Diferencia con Random Forest: Random Forest devuelve directamente la proporción de votos (una probabilidad). Gradient Boosting construye una suma de log-odds que luego se transforma con sigmoidea.

6 XGBoost: eXtreme Gradient Boosting

XGBoost (eXtreme Gradient Boosting) es la implementación más popular y optimizada de Gradient Boosting. Fue creado por Tianqi Chen en 2014 y se convirtió en el algoritmo ganador de múltiples competiciones de Kaggle.

6.1 Función objetivo regularizada

La diferencia clave de XGBoost es que la función de pérdida incluye regularización explícita:

\[ \mathcal{L}(\phi) = \sum_{i=1}^{N} L(y_i, \hat{y}_i) + \sum_{k=1}^{K} \Omega(f_k) \]

Donde:

  • \(L(y_i, \hat{y}_i)\) es la pérdida de entrenamiento (MSE para regresión, log-loss para clasificación)
  • \(\Omega(f_k) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2\) es la regularización del árbol \(k\)
  • \(T\) es el número de hojas
  • \(w_j\) es el peso de la hoja \(j\)
  • \(\gamma\) penaliza el número de hojas ( complejidad del árbol)
  • \(\lambda\) penaliza los pesos grandes (similar a L2)

6.2 Expansión de Taylor de segundo orden

XGBoost usa una expansión de Taylor de segundo orden para aproximar la función de pérdida:

\[ \mathcal{L}^{(t)} \approx \sum_{i=1}^{N} \left[ g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i) \right] + \Omega(f_t) + \text{constante} \]

Donde:

\[ g_i = \frac{\partial L(y_i, \hat{y}_i^{(t-1)})}{\partial \hat{y}_i^{(t-1)}}, \quad h_i = \frac{\partial^2 L(y_i, \hat{y}_i^{(t-1)})}{\partial (\hat{y}_i^{(t-1)})^2} \]

¿Por qué segundo orden? Gradient Boosting estándar usa solo el gradiente (\(g_i\)). XGBoost añade la segunda derivada (\(h_i\)), que proporciona información sobre la curvatura de la función de pérdida, permitiendo pasos más precisos.

6.3 Split finding: cómo decide dónde dividir

Para cada posible división del árbol, XGBoost calcula la ganancia:

\[ \text{Gain} = \frac{1}{2} \left[ \frac{(\sum g_i)^2}{\sum h_i + \lambda} \right]_{\text{izq}} + \frac{1}{2} \left[ \frac{(\sum g_i)^2}{\sum h_i + \lambda} \right]_{\text{der}} - \frac{1}{2} \left[ \frac{(\sum g_i)^2}{\sum h_i + \lambda} \right]_{\text{padre}} - \gamma \]
  • Si \(\text{Gain} < 0\), la división no mejora el modelo y se detiene (podado).
  • \(\gamma\) controla cuánto “cuesta” añadir una división.

6.4 Parámetros importantes

Parámetro Descripción Valor típico
n_estimators Número de árboles (iteraciones de boosting) 100-1000
learning_rate (\(\eta\)) Escala cada árbol (menor = más regularization) 0.01-0.3
max_depth Profundidad máxima de cada árbol 3-10
min_child_weight Peso mínimo en una hoja (regularización) 1-10
subsample Fracción de ejemplos por árbol 0.6-1.0
colsample_bytree Fracción de características por árbol 0.6-1.0
reg_alpha (\(\lambda\)) Regularización L1 en pesos de hojas 0-1
reg_lambda (\(\gamma\)) Regularización L2 en pesos de hojas 0-1

6.5 Código con scikit-learn: aprobación de préstamos

Veamos un ejemplo práctico de aprobación de préstamos bancarios:

#| eval: false
import xgboost as xgb
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
from sklearn.preprocessing import StandardScaler

# Crear dataset sintético de préstamos
np.random.seed(42)
n = 2000

# Características
ingresos = np.random.normal(45000, 15000, n)  # Ingresos anuales
deudas = np.random.normal(15000, 8000, n)  # Deudas actuales
puntuacion_credito = np.random.normal(650, 100, n)  # Score crediticio
antiguedad_trabajo = np.random.exponential(5, n)  # Años en el trabajo
num_productos = np.random.poisson(2, n)  # Número de productos bancarios

# Variable objetivo: mayor ingreso + menor deuda + mejor score = mayor probabilidad de aprobación
ingreso_neto = ingresos - deudas
prob_aprobacion = 1 / (1 + np.exp(-(ingreso_neto/50000 + puntuacion_credito/1000 - 3)))
aprobado = (prob_aprobacion > 0.5).astype(int)

# Crear DataFrame
df = pd.DataFrame({
    'ingresos': ingresos,
    'deudas': deudas,
    'puntuacion_credito': puntuacion_credito,
    'antiguedad_trabajo': antiguedad_trabajo,
    'num_productos': num_productos,
    'aprobado': aprobado
})

print(f"Préstamos aprobados: {aprobado.sum()} ({aprobado.mean()*100:.1f}%)")
print(f"Préstamos rechazados: {(1-aprobado).sum()} ({(1-aprobado).mean()*100:.1f}%)")

# Separar variables
X = df.drop('aprobado', axis=1)
y = df['aprobado']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Crear modelo
modelo = xgb.XGBClassifier(
    n_estimators=200,
    learning_rate=0.1,
    max_depth=4,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    eval_metric='logloss'
)

# Entrenar
modelo.fit(X_train, y_train)

# Predecir
y_pred = modelo.predict(X_test)
y_prob = modelo.predict_proba(X_test)[:, 1]

# Evaluar
print("\nMatriz de confusión:")
print(confusion_matrix(y_test, y_pred))
print("\nReporte de clasificación:")
print(classification_report(y_test, y_pred, target_names=['Rechazado', 'Aprobado']))
print(f"AUC-ROC: {roc_auc_score(y_test, y_prob):.4f}")

# Validación cruzada
cv_scores = cross_val_score(modelo, X, y, cv=5, scoring='roc_auc')
print(f"AUC-ROC CV: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")

# Importancia de variables
xgb.plot_importance(modelo, max_num_features=5)
plt.title("Top 5 Variables más Importantes para Aprobación")
plt.tight_layout()
plt.show()

Salida esperada:

Préstamos aprobados: 952 (47.6%)
Préstamos rechazados: 1048 (52.4%)

Matriz de confusión:
[[385  25]
 [ 30 360]]

Reporte de clasificación:
              precision    recall  f1-score   support

  Rechazado       0.93      0.94      0.93       410
   Aprobado       0.94      0.92      0.93       390

    accuracy                           0.93       800
   macro avg       0.93      0.93      0.93       800
weighted avg       0.93      0.93      0.93       800

AUC-ROC: 0.9756
AUC-ROC CV: 0.9742 ± 0.0089

Nota

Interpretación: El modelo aprueba préstamos con 93% de precisión y detecta el 92% de los que serían aprobados. La variable más importante es la puntuación crediticia, seguida de los ingresos netos (ingresos - deudas).

6.6 Ejemplo completo: XGBoost para regresión

Veamos un ejemplo más completo con el dataset de California Housing (predecir el precio de casas):

#| eval: false
import xgboost as xgb
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import mean_squared_error, r2_score
import matplotlib.pyplot as plt

# Cargar datos
datos = fetch_california_housing()
X_train, X_test, y_train, y_test = train_test_split(
    datos.data, datos.target, test_size=0.2, random_state=42
)

# Crear modelo
modelo = xgb.XGBRegressor(
    n_estimators=200,
    learning_rate=0.1,
    max_depth=6,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42
)

# Entrenar
modelo.fit(X_train, y_train)

# Predecir
y_pred = modelo.predict(X_test)

# Evaluar
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)

print(f"RMSE: {rmse:.4f}")
print(f"R²: {r2:.4f}")

# Validación cruzada
cv_scores = cross_val_score(modelo, datos.data, datos.target, cv=5, scoring='r2')
print(f"R² CV: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")

# Importancia de variables
xgb.plot_importance(modelo, max_num_features=10)
plt.title("Top 10 Variables más Importantes")
plt.tight_layout()
plt.show()

# Comparar con Random Forest
from sklearn.ensemble import RandomForestRegressor

rf = RandomForestRegressor(n_estimators=200, random_state=42)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)

print(f"\nXGBoost RMSE: {rmse:.4f}")
print(f"Random Forest RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_rf)):.4f}")

Salida esperada:

RMSE: 0.4721
R²: 0.8347
R² CV: 0.8298 ± 0.0124

Top 10 Variables más Importantes:
[Gráfico de barras con variables como MedInc, AveOccup, etc.]

XGBoost RMSE: 0.4721
Random Forest RMSE: 0.5289

Nota

Ventaja de XGBoost sobre Random Forest: En este ejemplo, XGBoost logra un RMSE de 0.4721 frente a 0.5289 de Random Forest. XGBoost es más preciso porque cada árbol corrige los errores del anterior, mientras que Random Forest solo promedia árboles independientes.

7 LightGBM: One-side Growth + Histogram

LightGBM (Microsoft, 2017) es una alternativa a XGBoost diseñada para ser más rápida y consumir menos memoria.

7.1 Crecimiento leaf-wise vs level-wise

XGBoost crece los árboles nivel por nivel (level-wise): en cada paso, expande todos los nodos del mismo nivel. LightGBM crece hoja por hoja (leaf-wise): en cada paso, expande solo la hoja que reduce más la pérdida.

  Level-wise (XGBoost)         Leaf-wise (LightGBM)
       ┌───┐                        ┌───┐
      ┌─┴─┐─┐                      ┌─┴─┐─┐
     ┌─┴─┐ ┌─┴─┐                  ┌─┴─┐ ┌─┴─┐
     ●   ● ●   ●                  ●   ● ●   ■ ← solo expande esta hoja
     ▲   ▲ ▲   ▲                        ▲
     Todos expanden                      Solo la mejor

Ventaja: Converge más rápido y requiere menos árboles. Riesgo: Puede sobreajustar si no se controla con num_leaves.

7.2 Histogram-based split finding

En lugar de probar todos los valores posibles como umbral (como hace XGBoost con el modo exact), LightGBM discretiza las características en histogramas con \(B\) bines (por defecto, 255). Esto reduce la complejidad de \(O(N \cdot p)\) a \(O(B \cdot p)\) por nodo.

7.3 Categorías nativas

LightGBM maneja variables categóricas directamente (sin one-hot encoding). Usa una estrategia basada en la frecuencia de las categorías: agrupa las categorías según su impacto en la pérdida.

#| eval: false
import lightgbm as lgb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

datos = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    datos.data, datos.target, test_size=0.2, random_state=42
)

modelo = lgb.LGBMClassifier(
    n_estimators=200,
    learning_rate=0.1,
    num_leaves=31,
    max_depth=-1,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    verbose=-1
)

modelo.fit(X_train, y_train)
y_pred = modelo.predict(X_test)

print(f"Precisión: {accuracy_score(y_test, y_pred):.4f}")

8 CatBoost: Ordered Boosting

CatBoost (Yandex, 2017) se especializa en dos cosas: manejar categorías nativas y evitar el data leakage en el boosting.

8.1 Ordered boosting (anti data-leakage)

El problema del boosting estándar es que, al calcular los pseudoresiduos, usa todos los ejemplos de entrenamiento, incluyendo los que el árbol actual ya ha visto. Esto causa un sesgo (data leakage).

CatBoost resuelve esto con ordered boosting: para cada ejemplo, solo usa ejemplos anteriores en una permutación aleatoria para calcular los pseudoresiduos.

8.2 Categorías nativas con permutaciones

CatBoost convierte cada categoría categórica en un número estadístico basado en la probabilidad condicional de la objetivo dado ese valor de categoría, usando solo ejemplos anteriores en la permutación.

#| eval: false
import catboost as cb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

datos = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    datos.data, datos.target, test_size=0.2, random_state=42
)

modelo = cb.CatBoostClassifier(
    iterations=200,
    learning_rate=0.1,
    depth=6,
    random_state=42,
    verbose=0
)

modelo.fit(X_train, y_train)
y_pred = modelo.predict(X_test)

print(f"Precisión: {accuracy_score(y_test, y_pred):.4f}")

8.3 ¿Cuándo elegir CatBoost?

  • Cuando tienes muchas variables categóricas nativas
  • Cuando quieres un modelo que funcione bien sin mucha sintonización
  • Cuando la velocidad de entrenamiento no es crítica

9 Comparación: Bagging vs Boosting

Aspecto Bagging (Random Forest) Boosting (XGBoost/LightGBM/CatBoost)
Estrategia Paralela: muchos árboles independientes Secuencial: cada árbol corrige el anterior
Árboles Fuertes (profundos, sin poda) Débiles (poca profundidad, 3-6)
Objetivo Reducir varianza Reducir sesgo (y varianza)
Entrenamiento Paralelizable completamente Secuencial (difícil de paralelizar)
Overfitting Resistente (más árboles = mejor o igual) Puede sobreajustar (necesita early stopping)
Hiperparámetros Pocos (n_estimators, max_features) Muchos (learning_rate, max_depth, subsample, reg_alpha, …)
Datos faltantes Manejo nativo en la mayoría Manejo nativo en la mayoría
Velocidad Rápido (paralelo) Más lento (secuencial), pero LightGBM optimiza esto
Cuándo usar Línea base, datos ruidosos, muchas features Datos tabulares limpios, máxima precisión

Tip

Regla práctica: Empieza con Random Forest como línea base. Si necesitas más precisión, pasa a XGBoost o LightGBM. Si tienes muchas categorías, prueba CatBoost.

10 Feature Importance y SHAP

10.1 Importancia de variables

Los métodos de boosting calculan la importancia de variables de varias formas:

  • Gain (ganancia): Cuánto reduce la pérdida total cada variable cuando se usa para dividir. Es la métrica más informativa.
  • Cover (cobertura): Cuántos ejemplos afecta cada variable (ponderado).
  • Frequency (frecuencia): Cuántas veces se usa una variable para dividir.
#| eval: false
import xgboost as xgb
import matplotlib.pyplot as plt

# Entrenar modelo (asumiendo que 'modelo' ya está entrenado)
xgb.plot_importance(modelo, importance_type="gain", max_num_features=10)
plt.title("Importancia de variables (Gain)")
plt.show()

10.2 SHAP values

SHAP (SHapley Additive exPlanations) es el método más robusto para explicar predicciones individuales. Se basa en la teoría de juegos: calcula cuánto contribuye cada variable a la predicción de cada ejemplo.

#| eval: false
import shap

explainer = shap.TreeExplainer(modelo)
shap_values = explainer.shap_values(X_test)

# Resumen global
shap.summary_plot(shap_values, X_test, feature_names=datos.feature_names)

# Explicación individual
shap.force_plot(explainer.expected_value, shap_values[0], X_test[0])

Nota

SHAP vs feature importance clásica: La importancia clásica da un ranking global. SHAP explica por qué el modelo predijo algo concreto para un ejemplo específico, lo cual es invaluable para interpretar modelos en producción.

11 Ejemplo completo: Random Forest vs XGBoost

Comparamos ambos algoritmos en un problema real de clasificación (cáncer de mama, 30 variables, 2 clases):

#| eval: false
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
import xgboost as xgb

# Cargar datos
datos = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    datos.data, datos.target, test_size=0.2, random_state=42, stratify=datos.target
)

# --- Random Forest ---
rf = RandomForestClassifier(n_estimators=200, max_features="sqrt", random_state=42)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)

print("=" * 50)
print("RANDOM FOREST")
print("=" * 50)
print(classification_report(y_test, y_pred_rf, target_names=datos.target_names))

# --- XGBoost ---
xgb_model = xgb.XGBClassifier(
    n_estimators=200,
    learning_rate=0.1,
    max_depth=4,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    eval_metric="logloss"
)
xgb_model.fit(X_train, y_train)
y_pred_xgb = xgb_model.predict(X_test)

print("=" * 50)
print("XGBOOST")
print("=" * 50)
print(classification_report(y_test, y_pred_xgb, target_names=datos.target_names))

# --- Comparación con validación cruzada ---
cv_rf = cross_val_score(rf, datos.data, datos.target, cv=5, scoring="accuracy")
cv_xgb = cross_val_score(xgb_model, datos.data, datos.target, cv=5, scoring="accuracy")

print(f"\nRandom Forest — Precisión CV: {cv_rf.mean():.4f} ± {cv_rf.std():.4f}")
print(f"XGBoost — Precisión CV: {cv_xgb.mean():.4f} ± {cv_xgb.std():.4f}")

Nota

Early stopping: Para evitar sobreajustar con XGBoost, puedes usar early_stopping_rounds en el método fit(), pasando un conjunto de validación. El entrenamiento se detiene cuando la métrica de validación deja de mejorar.

#| eval: false
# Early stopping con XGBoost
xgb_model.fit(
    X_train, y_train,
    eval_set=[(X_test, y_test)],
    verbose=False
)
# El modelo usará automáticamente los mejores n_estimators
Algoritmo Tipo Ventaja principal Cuándo usarlo
Random Forest Bagging Robusto, pocos hiperparámetros Línea base, datos ruidosos
XGBoost Boosting Máxima precisión, regularización Datos tabulares limpios
LightGBM Boosting Velocidad, memoria eficiente Grandes volúmenes de datos
CatBoost Boosting Categorías nativas, ordered boosting Muchas variables categóricas

Los métodos de Gradient Boosting son, en la práctica, los algoritmos más precisos para datos tabulares. La clave de su éxito es combinar la simplicidad de los árboles de decisión con la potencia del descenso de gradiente funcional y la regularización moderna.