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:
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:
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:
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:
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:
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:
4.1 Pseudoresiduos en regresión
Los pseudoresiduos son simplemente los residuos reales:
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
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:
| 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:
Con \(F_0 = 300\):
Con \(F_1\):
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:
| 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\)?
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í:
- Empezamos con \(F_0 = 300\) (la media del entrenamiento)
- Pasamos por el Árbol 1: 100 m² > 90 → \(h_1(x) = +66.67\)
- Pasamos por el Árbol 2: 100 m² > 90 → \(h_2(x) = +60\)
- Sumamos todo:
El modelo predice que la casa de 100 m² cuesta 312.670 €.
La fórmula general es:
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):
Donde \(p = \sigma(F(x)) = \frac{1}{1 + e^{-F(x)}}\) es la probabilidad predicha.
5.2 Pseudoresiduos para clasificación
Los pseudoresiduos son:
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:
Convertimos a probabilidad con la sigmoidea:
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:
| 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\)
| 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:
- \(F_0 = 0.405\)
- Árbol 1: 3.5 > 2.5 → \(h_1(x) = +0.4\) → \(F_1 = 0.405 + 0.04 = 0.445\)
- Árbol 2: 3.5 > 2.5 → \(h_2(x) = +0.39\) → \(F_2 = 0.445 + 0.039 = 0.484\)
- 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):
Para obtener la probabilidad, aplicamos la función sigmoidea:
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:
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:
Donde:
¿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:
- 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.