En el capítulo anterior vimos que los árboles de decisión son modelos flexibles capaces de capturar relaciones complejas. Sin embargo, tienen una debilidad fundamental: alta varianza. Un árbol entrenado en un conjunto de datos puede verse completamente diferente si le cambias unas pocas observaciones.
Para ilustrarlo, considera lo siguiente:
Código
import numpy as npimport matplotlib.pyplot as pltfrom sklearn.tree import DecisionTreeClassifier, plot_treefrom sklearn.datasets import make_moonsnp.random.seed(42)X, y = make_moons(n_samples=150, noise=0.3, random_state=42)# Muestras bootstrap del mismo tamaño (con reemplazo) — como hace Baggingidx1 = np.random.choice(len(X), size=len(X), replace=True)idx2 = np.random.choice(len(X), size=len(X), replace=True)# Sin límite de profundidad: árboles completos tienen máxima varianzatree1 = DecisionTreeClassifier(random_state=1)tree2 = DecisionTreeClassifier(random_state=2)tree1.fit(X[idx1], y[idx1])tree2.fit(X[idx2], y[idx2])h =0.03x_min, x_max = X[:, 0].min() -0.5, X[:, 0].max() +0.5y_min, y_max = X[:, 1].min() -0.5, X[:, 1].max() +0.5xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))fig, axes = plt.subplots(1, 2, figsize=(13, 5))for ax, tree, title inzip(axes, [tree1, tree2], ['Árbol 1 (muestra A)', 'Árbol 2 (muestra B)']): Z = tree.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape) ax.contourf(xx, yy, Z, alpha=0.35, cmap='RdBu_r') ax.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu_r', edgecolors='black', s=40) ax.set_title(title, fontsize=13) ax.set_xlabel('X1'); ax.set_ylabel('X2') ax.grid(True, alpha=0.3)plt.tight_layout()plt.show()# Diferencia en prediccionespred1 = tree1.predict(X)pred2 = tree2.predict(X)desacuerdo = (pred1 != pred2).mean()print(f"Porcentaje de desacuerdo entre los dos árboles: {desacuerdo:.1%}")
Alta varianza de los árboles: dos muestras ligeramente distintas generan árboles muy diferentes
Porcentaje de desacuerdo entre los dos árboles: 12.7%
Los dos árboles fueron entrenados con muestras casi idénticas y producen fronteras de decisión muy distintas. Esto es la varianza en acción: el modelo es demasiado sensible a las particularidades del conjunto de entrenamiento.
La solución intuitiva es simple: si un árbol tiene mucha varianza, ¿qué tal si entrenamos muchos árboles y promediamos sus predicciones? Pero para hacerlo necesitamos generar múltiples versiones del conjunto de entrenamiento a partir de los datos que ya tenemos. Ahí entra el bootstrap.
Bootstrap
El bootstrap es una técnica de remuestreo que permite estimar la variabilidad de un estadístico sin asumir una distribución paramétrica, y sin necesidad de recolectar más datos.
La idea es: dado un conjunto de \(n\) observaciones, genera muestras artificiales del mismo tamaño \(n\) tomando observaciones con reemplazo. Cada muestra resultante se llama muestra bootstrap.
Dataset original: [A, B, C, D, E] (n = 5)
Muestra bootstrap 1: [B, B, A, D, A] ← B y A aparecen dos veces; C y E ausentes
Muestra bootstrap 2: [C, E, E, A, D] ← E aparece dos veces; B ausente
Muestra bootstrap 3: [A, C, B, B, C] ← B y C aparecen dos veces; D y E ausentes
La propiedad del 63.2%
¿Qué fracción de las observaciones originales aparece en una muestra bootstrap? La probabilidad de que una observación no sea seleccionada en ninguno de los \(n\) sorteos es:
Por lo tanto, cada muestra bootstrap contiene en promedio \(\approx 63.2\%\) de las observaciones originales (distintas), y el restante \(\approx 36.8\%\) queda fuera de la muestra (out-of-bag, OOB).
Código
import numpy as npimport matplotlib.pyplot as pltrng = np.random.default_rng(42)n =200B =500fracciones_unicas = []conteo_apariciones = np.zeros(n, dtype=int)for _ inrange(B): muestra = rng.integers(0, n, size=n) fracciones_unicas.append(len(np.unique(muestra)) / n) conteo_apariciones += np.bincount(muestra, minlength=n)fig, axes = plt.subplots(1, 2, figsize=(13, 5))# Panel 1: distribución de fracción de observaciones únicasaxes[0].hist(fracciones_unicas, bins=25, color='steelblue', alpha=0.8, edgecolor='white')axes[0].axvline(x=np.mean(fracciones_unicas), color='red', linewidth=2, label=f'Media = {np.mean(fracciones_unicas):.3f}')axes[0].axvline(x=1-1/np.e, color='black', linewidth=2, linestyle='--', label=f'Límite teórico = {1-1/np.e:.3f}')axes[0].set_xlabel('Fracción de observaciones únicas por muestra bootstrap', fontsize=11)axes[0].set_ylabel('Frecuencia (sobre 500 muestras)', fontsize=11)axes[0].set_title('¿Qué fracción de datos incluye cada bootstrap?', fontsize=12)axes[0].legend(fontsize=10)axes[0].grid(True, alpha=0.3)# Panel 2: número de veces que aparece cada observaciónaxes[1].bar(range(n), np.sort(conteo_apariciones), color='tomato', alpha=0.8, width=1.0)axes[1].axhline(y=B, color='black', linewidth=1.5, linestyle='--', label=f'Aparece en todos los bootstraps (B={B})')axes[1].axhline(y=0, color='gray', linewidth=1)oob_count = (conteo_apariciones ==0).sum()axes[1].set_xlabel('Observaciones (ordenadas por frecuencia)', fontsize=11)axes[1].set_ylabel(f'Veces que aparece (sobre {B} muestras)', fontsize=11)axes[1].set_title(f'Frecuencia de aparición de cada observación\n'f'({oob_count} observaciones nunca aparecieron en ningún bootstrap)', fontsize=11)axes[1].legend(fontsize=10)axes[1].grid(True, alpha=0.3, axis='y')plt.tight_layout()plt.show()print(f"Fracción media de obs. únicas por bootstrap: {np.mean(fracciones_unicas):.4f}")print(f"Límite teórico (1 - 1/e): {1-1/np.e:.4f}")
Propiedades del muestreo bootstrap: fracción única y distribución OOB
Fracción media de obs. únicas por bootstrap: 0.6319
Límite teórico (1 - 1/e): 0.6321
Ejemplo: estimación de la media con bootstrap
Un uso clásico del bootstrap es estimar la distribución muestral de un estadístico cuando no conocemos su fórmula analítica (o no queremos asumir normalidad).
El procedimiento es siempre el mismo:
Calcula el estadístico de interés en los datos originales → \(\hat{\theta}\)
Genera \(B\) muestras bootstrap y calcula el estadístico en cada una → \(\hat{\theta}_1^*, \ldots, \hat{\theta}_B^*\)
La distribución de \(\{\hat{\theta}_b^*\}\) aproxima la distribución muestral de \(\hat{\theta}\)
Distribución bootstrap de la media: converge a la distribución muestral teórica
Media muestral: 16,313.4
Error estándar teórico: 1,857.1
Error estándar bootstrap: 1,861.6
IC 95% bootstrap: [12,952, 20,134]
Con \(B \approx 500\)–\(1000\) muestras el error estándar bootstrap converge al teórico. Más importante: cuando el estadístico es complejo (una mediana, un coeficiente de correlación, un percentil), el bootstrap sigue funcionando igual aunque no exista fórmula analítica.
¿Para qué sirve el bootstrap más allá de Bagging?
El bootstrap es una herramienta general de inferencia estadística. Se usa para:
Estimar intervalos de confianza de cualquier estadístico (media, mediana, correlación) sin asumir normalidad
Estimar el error estándar de un estimador complejo
Validar modelos (bootstrap .632 es una alternativa a la validación cruzada)
En este capítulo lo usamos específicamente para generar diversidad entre árboles, pero su utilidad es mucho más amplia.
Bagging (Bootstrap Aggregating)
La idea central
Bagging (Breiman, 1996) es la primera gran técnica de ensemble para árboles. La idea es:
Genera \(B\) muestras bootstrap del conjunto de entrenamiento (muestreo con reemplazo)
Entrena un árbol completo (sin podar) en cada muestra
Para predecir un punto nuevo, promedia (regresión) o toma la moda (clasificación) de las \(B\) predicciones
Para b = 1 hasta B:
1. Samplear n observaciones con reemplazo → D*ᵦ (~63.2% únicas)
2. Entrenar árbol completo T*ᵦ sobre D*ᵦ
Predicción para x nuevo:
- Regresión: ŷ(x) = (1/B) Σ T*ᵦ(x)
- Clasificación: ŷ(x) = moda{ T*ᵦ(x) : b = 1...B }
¿Por qué funciona? La matemática del promedio
Si tenemos \(B\) predictores independientes, cada uno con varianza \(\sigma^2(x)\), su promedio tiene varianza:
La varianza cae con \(B\). En el límite, desaparece.
Pero los árboles entrenados con muestras bootstrap del mismo dataset no son independientes: están correlacionados con coeficiente \(\rho(x)\). La varianza real del promedio es:
Cuando \(B \to \infty\), el segundo término desaparece, pero el primero permanece. En otras palabras:
Aumentar \(B\) más allá de cierto punto no ayuda si los árboles están muy correlacionados. El techo de la mejora es \(\rho(x)\,\sigma^2(x)\).
Esto motivará directamente la idea de Random Forest.
Sesgo vs. Varianza en Bagging
Bagging no cambia el sesgo de los árboles individuales. Un árbol completo tiene sesgo bajo y varianza alta; bagging mantiene el sesgo bajo y reduce la varianza. Por eso se usan árboles sin podar: queremos el sesgo mínimo y dejamos que el promedio controle la varianza.
Muestras Bootstrap y la propiedad OOB
Cada muestra bootstrap incluye en promedio el \(1 - (1 - 1/n)^n \approx 1 - 1/e \approx 63.2\%\) de las observaciones originales. El restante \(\approx 36.8\%\) queda fuera de la bolsa (Out-of-Bag, OOB).
Esto es una propiedad muy útil:
Para cada observación \(i\), solo los árboles que no la usaron en entrenamiento emiten una predicción. El error OOB es el error promedio de esas predicciones “honestas”, y es una estimación casi libre de sesgo del error de generalización.
El error OOB equivale aproximadamente a la validación cruzada leave-one-out, pero mucho más barato de calcular.
Código
from sklearn.ensemble import BaggingClassifierfrom sklearn.model_selection import train_test_splitnp.random.seed(42)X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42)B_values = [5, 10, 15, 20, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]train_errors, test_errors, oob_errors = [], [], []for B in B_values: bag = BaggingClassifier( estimator=DecisionTreeClassifier(), n_estimators=B, bootstrap=True, oob_score=(B >1), random_state=42, n_jobs=-1 ) bag.fit(X_tr, y_tr) train_errors.append(1- bag.score(X_tr, y_tr)) test_errors.append(1- bag.score(X_te, y_te)) oob_errors.append(1- bag.oob_score_ if B >1elseNone)fig, ax = plt.subplots(figsize=(8, 5))ax.plot(B_values, train_errors, marker='o', label='Error entrenamiento', linewidth=2)ax.plot(B_values, test_errors, marker='s', label='Error prueba', linewidth=2)oob_valid = [(b, e) for b, e inzip(B_values[1:], oob_errors[1:]) if e isnotNone]if oob_valid: bs, es =zip(*oob_valid) ax.plot(bs, es, marker='^', label='Error OOB', linewidth=2, linestyle='--')ax.axhline(y=1- DecisionTreeClassifier().fit(X_tr, y_tr).score(X_te, y_te), color='gray', linestyle=':', linewidth=1.5, label='Árbol simple (test)')ax.set_xlabel('Número de árboles (B)', fontsize=11)ax.set_ylabel('Tasa de error', fontsize=11)ax.set_title('Bagging: efecto de B en el error', fontsize=12)ax.legend(fontsize=9)ax.grid(True, alpha=0.3)plt.tight_layout()plt.show()
Bagging reduce la varianza: comparación de errores a medida que crece B
Observaciones:
A partir de ~50 árboles el error se estabiliza: más árboles no degradan el desempeño, solo lo estabilizan
El error OOB es una buena aproximación del error en test sin necesidad de un conjunto de validación separado
¿Cuántos árboles usar?
Una propiedad clave de Bagging (y de Random Forest) es que más árboles nunca empeoran el modelo: el error es monótonamente no creciente con \(B\). La pregunta práctica es cuándo deja de valer la pena agregar más.
La ganancia marginal de pasar de \(B\) a \(B+1\) árboles cae muy rápido. En la mayoría de los problemas, con \(B \approx 100\)–\(200\) se captura el 95% de la reducción de varianza posible. Lo importante es que el error OOB permite monitorear esa convergencia sin reservar datos de validación: cuando la curva OOB se aplana, ya tienes suficientes árboles.
Esto produce una estimación mucho más estable que la de un árbol individual: al promediar sobre cientos de árboles entrenados con distintas muestras, el ruido de estimación se cancela.
Código
import numpy as npimport matplotlib.pyplot as pltfrom sklearn.ensemble import BaggingClassifierfrom sklearn.tree import DecisionTreeClassifierfrom sklearn.datasets import make_classificationfrom sklearn.model_selection import train_test_splitnp.random.seed(42)X_fi, y_fi = make_classification( n_samples=800, n_features=10, n_informative=5, n_redundant=2, random_state=42)feat_names = [f'var_{i+1:02d}'for i inrange(10)]X_tr_fi, X_te_fi, y_tr_fi, y_te_fi = train_test_split( X_fi, y_fi, test_size=0.25, random_state=42)# Árbol individualsingle_tree = DecisionTreeClassifier(random_state=42)single_tree.fit(X_tr_fi, y_tr_fi)imp_tree = single_tree.feature_importances_# Bagging: promediar feature_importances_ de cada estimadorbag = BaggingClassifier( estimator=DecisionTreeClassifier(), n_estimators=200, bootstrap=True, random_state=42, n_jobs=-1)bag.fit(X_tr_fi, y_tr_fi)imp_bag = np.mean([t.feature_importances_ for t in bag.estimators_], axis=0)std_bag = np.std( [t.feature_importances_ for t in bag.estimators_], axis=0)# Orden por importancia en Baggingord_bag = np.argsort(imp_bag)[::-1]fig, axes = plt.subplots(1, 2, figsize=(13, 5))x = np.arange(len(feat_names))axes[0].bar(x, imp_tree[ord_bag], color='steelblue', alpha=0.8)axes[0].set_xticks(x)axes[0].set_xticklabels([feat_names[i] for i in ord_bag], fontsize=10)axes[0].set_ylabel('Importancia MDI', fontsize=11)axes[0].set_title(f'Árbol individual\n(acc. test: {single_tree.score(X_te_fi, y_te_fi):.3f})', fontsize=12)axes[0].grid(True, alpha=0.3, axis='y')axes[1].bar(x, imp_bag[ord_bag], yerr=std_bag[ord_bag], color='tomato', alpha=0.8, capsize=4)axes[1].set_xticks(x)axes[1].set_xticklabels([feat_names[i] for i in ord_bag], fontsize=10)axes[1].set_ylabel('Importancia MDI (promedio ± std)', fontsize=11)axes[1].set_title(f'Bagging (200 árboles)\n(acc. test: {bag.score(X_te_fi, y_te_fi):.3f})', fontsize=12)axes[1].grid(True, alpha=0.3, axis='y')plt.suptitle('Importancia de variables: árbol individual vs. Bagging', fontsize=13)plt.tight_layout()plt.show()print("Variable | Árbol individual | Bagging (media ± std)")print("-"*52)for i in ord_bag:print(f"{feat_names[i]:9s} | {imp_tree[i]:.4f} | {imp_bag[i]:.4f} ± {std_bag[i]:.4f}")
Importancia de variables: árbol individual vs. Bagging (promedio de 200 árboles)
El árbol individual asigna importancias volátiles: si lo reentrenamos con una semilla diferente, el ranking puede cambiar notablemente. Bagging promedia sobre 200 árboles, reduciendo la varianza de estimación por un factor de \(\approx 1/\sqrt{200}\). Las barras de error en el panel derecho muestran la dispersión entre árboles — las variables realmente informativas tienen error estándar bajo relativo a su importancia.
Comparación de fronteras de decisión: árbol individual vs. Bagging
La reducción de varianza de Bagging es más intuitiva visualmente: promediar muchas fronteras angulares e irregulares produce una frontera más suave y estable.
Fronteras de decisión: un árbol completo vs. Bagging con distintos B
¿Qué observar en estas fronteras?
Árbol individual: frontera muy irregular y angulosa — captura el ruido de entrenamiento
Bagging B=10: ya se suaviza notablemente, pero aún hay irregularidades
Bagging B=200: frontera suave y estable — el promedio de muchos árboles ruidosos converge a una frontera limpia
La mejora de test de árbol→Bagging viene exclusivamente de reducir la varianza; el sesgo prácticamente no cambia.
Random Forest
La limitación de Bagging y su solución
El análisis de la varianza mostró que el límite de Bagging es \(\rho(x)\,\sigma^2(x)\), donde \(\rho\) es la correlación entre árboles. Si todos los árboles se parecen mucho, la ganancia es pequeña.
¿Por qué se parecen? Porque si hay una variable muy predictiva, todos los árboles la usarán como primera división. Los árboles comparten estructura.
La solución de Random Forest (Breiman, 2001) es simple y brillante:
En cada división de cada árbol, en lugar de considerar todas las \(p\) variables, selecciona aleatoriamente solo \(m < p\) variables candidatas.
Esto decorrelaciona los árboles: si la variable dominante queda fuera del sorteo en un nodo, otro predictor tendrá que tomar su lugar, forzando diversidad en la estructura de los árboles.
Algoritmo
Para b = 1 hasta B:
1. Generar muestra bootstrap D*ᵦ de tamaño n
2. Construir árbol T*ᵦ con la siguiente modificación:
En cada nodo de decisión:
a) Sortear m variables de las p totales (sin reemplazo)
b) Encontrar la mejor división usando solo esas m variables
c) Dividir el nodo
3. Crecer el árbol sin podar
Predicción para x nuevo:
- Regresión: ŷ(x) = (1/B) Σ T*ᵦ(x)
- Clasificación: ŷ(x) = moda{ T*ᵦ(x) : b = 1...B }
El parámetro m: el control de correlación
El parámetro más importante de Random Forest es \(m\), el número de variables candidatas por nodo:
Valor de m
Efecto
\(m = p\)
Equivale a Bagging (máxima correlación entre árboles)
\(m = \sqrt{p}\)
Default para clasificación — buen balance
\(m = p/3\)
Default para regresión
\(m = 1\)
Máxima decorrelación, pero árboles muy débiles (alto sesgo)
Reducir \(m\) disminuye la correlación entre árboles (baja varianza del ensemble) pero hace que cada árbol individual sea peor (aumenta el sesgo). El óptimo está en el balance.
Regla práctica
Para la mayoría de los problemas, los valores por defecto de scikit-learn funcionan bien:
Clasificación: max_features='sqrt'
Regresión: max_features=1/3
El número de árboles (n_estimators) rara vez es el hiperparámetro más importante; 100-200 suele ser suficiente.
Bagging vs. Random Forest: la correlación en acción
Código
from sklearn.ensemble import RandomForestClassifier, BaggingClassifierfrom sklearn.datasets import make_classificationnp.random.seed(42)X_cl, y_cl = make_classification( n_samples=500, n_features=20, n_informative=10, n_redundant=5, random_state=42)X_tr, X_te, y_tr, y_te = train_test_split(X_cl, y_cl, test_size=0.3, random_state=42)B_values = [5, 10, 15, 20, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]bag_test, rf_test = [], []for B in B_values: bag = BaggingClassifier( estimator=DecisionTreeClassifier(), n_estimators=B, bootstrap=True, random_state=42, n_jobs=-1 ) rf = RandomForestClassifier(n_estimators=B, random_state=42, n_jobs=-1) bag.fit(X_tr, y_tr); rf.fit(X_tr, y_tr) bag_test.append(1- bag.score(X_te, y_te)) rf_test.append(1- rf.score(X_te, y_te))fig, axes = plt.subplots(1, 2, figsize=(13, 5))axes[0].plot(B_values, bag_test, marker='o', label='Bagging', linewidth=2, color='steelblue')axes[0].plot(B_values, rf_test, marker='s', label='Random Forest', linewidth=2, color='tomato')axes[0].set_xlabel('Número de árboles (B)', fontsize=11)axes[0].set_ylabel('Tasa de error en prueba', fontsize=11)axes[0].set_title('Bagging vs. Random Forest', fontsize=12)axes[0].legend(fontsize=10)axes[0].grid(True, alpha=0.3)# Panel 2: correlación entre predicciones de árboles (200 árboles)rf_200 = RandomForestClassifier(n_estimators=200, random_state=42, n_jobs=-1)bag_200 = BaggingClassifier( estimator=DecisionTreeClassifier(), n_estimators=200, bootstrap=True, random_state=42, n_jobs=-1)rf_200.fit(X_tr, y_tr); bag_200.fit(X_tr, y_tr)rf_preds = np.array([t.predict(X_te) for t in rf_200.estimators_])bag_preds = np.array([t.predict(X_te) for t in bag_200.estimators_])corr_rf = np.corrcoef(rf_preds)[np.triu_indices(200, k=1)].mean()corr_bag = np.corrcoef(bag_preds)[np.triu_indices(200, k=1)].mean()methods = ['Bagging', 'Random Forest']corrs = [corr_bag, corr_rf]colors = ['steelblue', 'tomato']bars = axes[1].bar(methods, corrs, color=colors, alpha=0.8, width=0.4)for bar, val inzip(bars, corrs): axes[1].text(bar.get_x() + bar.get_width()/2, val +0.005,f'ρ = {val:.3f}', ha='center', fontsize=12, fontweight='bold')axes[1].set_ylabel('Correlación media entre árboles (ρ)', fontsize=11)axes[1].set_title('Decorrelación entre árboles', fontsize=12)axes[1].set_ylim(0, max(corrs) *1.2)axes[1].grid(True, alpha=0.3, axis='y')plt.tight_layout()plt.show()print(f"Correlación media entre árboles — Bagging: {corr_bag:.3f} | Random Forest: {corr_rf:.3f}")print(f"Error en test — Bagging: {bag_test[-1]:.3f} | Random Forest: {rf_test[-1]:.3f}")
Random Forest vs. Bagging: comparación de errores y correlación entre árboles
Correlación media entre árboles — Bagging: 0.503 | Random Forest: 0.368
Error en test — Bagging: 0.100 | Random Forest: 0.087
La diferencia es clara: Random Forest produce árboles menos correlacionados entre sí, y esa decorrelación se traduce directamente en mejor desempeño.
Efecto de max_features sobre la correlación y el error
Bagging y Random Forest son dos extremos del mismo dial: max_features=p (todas las variables) es Bagging, y valores menores van decorrelacionando los árboles. ¿Qué pasa en los puntos intermedios?
Código
from sklearn.ensemble import RandomForestClassifierimport numpy as npimport matplotlib.pyplot as pltp =20m_values = [1, 2, 3, 4, 5, 7, 10, 14, 20]labels = [str(m) for m in m_values]labels[m_values.index(4)] ='4 ≈ √p\n(default clf)'labels[m_values.index(7)] ='7 ≈ p/3\n(default reg)'labels[m_values.index(20)] ='20 = p\n(Bagging)'correlaciones, test_errors_mf = [], []for m in m_values: rf_m = RandomForestClassifier( n_estimators=200, max_features=m, random_state=42, n_jobs=-1 ) rf_m.fit(X_tr, y_tr) preds = np.array([t.predict(X_te) for t in rf_m.estimators_]) correlaciones.append(np.corrcoef(preds)[np.triu_indices(200, k=1)].mean()) test_errors_mf.append(1- rf_m.score(X_te, y_te))x = np.arange(len(m_values))fig, axes = plt.subplots(1, 2, figsize=(13, 5))# Panel 1: correlación entre árbolescolor_corr ='steelblue'axes[0].plot(x, correlaciones, marker='o', linewidth=2.5, color=color_corr)axes[0].fill_between(x, correlaciones, alpha=0.15, color=color_corr)for xi, (corr, label) inenumerate(zip(correlaciones, labels)): axes[0].annotate(f'{corr:.3f}', (xi, corr), textcoords='offset points', xytext=(0, 8), ha='center', fontsize=9)axes[0].set_xticks(x)axes[0].set_xticklabels(labels, fontsize=9)axes[0].set_xlabel('max_features (m)', fontsize=11)axes[0].set_ylabel('Correlación media entre árboles (ρ)', fontsize=11)axes[0].set_title('Decorrelación al reducir m', fontsize=12)axes[0].grid(True, alpha=0.3)# Panel 2: error de testcolor_err ='tomato'axes[1].plot(x, test_errors_mf, marker='s', linewidth=2.5, color=color_err)axes[1].fill_between(x, test_errors_mf, alpha=0.15, color=color_err)for xi, (err, label) inenumerate(zip(test_errors_mf, labels)): axes[1].annotate(f'{err:.3f}', (xi, err), textcoords='offset points', xytext=(0, 8), ha='center', fontsize=9)axes[1].set_xticks(x)axes[1].set_xticklabels(labels, fontsize=9)axes[1].set_xlabel('max_features (m)', fontsize=11)axes[1].set_ylabel('Error en test', fontsize=11)axes[1].set_title('Error de test al reducir m', fontsize=12)axes[1].grid(True, alpha=0.3)plt.suptitle('Efecto de max_features: menos variables → menos correlación, pero árboles más débiles', fontsize=12)plt.tight_layout()plt.show()
Correlación entre árboles y error de test en función de max_features (p=20)
El trade-off de max_features
m grande (→ Bagging): cada árbol es fuerte individualmente, pero todos se parecen mucho → correlación alta → el promedio no gana tanto
m pequeño: árboles muy distintos entre sí (correlación baja), pero cada uno es un predictor más débil → sesgo crece
El mínimo de error está en el punto intermedio, típicamente cerca de \(\sqrt{p}\) para clasificación
Este trade-off sesgo–varianza entre árboles es el que max_features controla directamente.
Importancia de Variables en Random Forest
Los árboles individuales ya tenían importancia de variables (MDI). En Random Forest se extiende naturalmente: se promedia la importancia MDI sobre todos los árboles del ensemble.
Al promediar sobre cientos de árboles, la estimación se vuelve mucho más estable que la de un árbol individual.
Permutation Importance (variante OOB)
Random Forest habilita una variante especialmente conveniente de la permutation importance: en lugar de usar un conjunto de test separado, se usan las muestras OOB de cada árbol. Para medir la importancia de la variable \(X_j\):
Para cada árbol \(b\), toma sus muestras OOB y calcula el error OOB normal
Permuta (baraja) los valores de \(X_j\) en esas muestras OOB y recalcula el error
La importancia es el incremento promedio en error al permutar
Esto no requiere reservar datos adicionales y es computacionalmente eficiente.
Código
from sklearn.inspection import permutation_importancenp.random.seed(42)X_fi, y_fi = make_classification( n_samples=800, n_features=10, n_informative=5, n_redundant=2, random_state=42)feat_names = [f'var_{i+1:02d}'for i inrange(10)]X_tr_fi, X_te_fi, y_tr_fi, y_te_fi = train_test_split( X_fi, y_fi, test_size=0.25, random_state=42)rf = RandomForestClassifier(n_estimators=200, random_state=42, n_jobs=-1, oob_score=True)rf.fit(X_tr_fi, y_tr_fi)# MDImdi = rf.feature_importances_mdi_ord = np.argsort(mdi)[::-1]# Permutation importance (sobre test)perm = permutation_importance(rf, X_te_fi, y_te_fi, n_repeats=20, random_state=42, n_jobs=-1)perm_mean = perm.importances_meanperm_ord = np.argsort(perm_mean)[::-1]fig, axes = plt.subplots(1, 2, figsize=(13, 5))n_feat =len(feat_names)axes[0].barh(range(n_feat), mdi[mdi_ord], color='steelblue', alpha=0.8)axes[0].set_yticks(range(n_feat))axes[0].set_yticklabels([feat_names[i] for i in mdi_ord], fontsize=10)axes[0].set_xlabel('Importancia MDI (promedio entre árboles)', fontsize=10)axes[0].set_title('Importancia MDI\n(Random Forest)', fontsize=11)axes[0].grid(True, alpha=0.3, axis='x')for i, v inenumerate(mdi[mdi_ord]): axes[0].text(v +0.002, i, f'{v:.3f}', va='center', fontsize=9)axes[1].barh(range(n_feat), perm_mean[perm_ord], xerr=perm.importances_std[perm_ord], color='tomato', alpha=0.8, capsize=4)axes[1].set_yticks(range(n_feat))axes[1].set_yticklabels([feat_names[i] for i in perm_ord], fontsize=10)axes[1].set_xlabel('Caída en accuracy al permutar', fontsize=10)axes[1].set_title('Permutation Importance\n(sobre test)', fontsize=11)axes[1].axvline(x=0, color='black', linewidth=0.8)axes[1].grid(True, alpha=0.3, axis='x')plt.suptitle(f'Random Forest (200 árboles) — Accuracy test: {rf.score(X_te_fi, y_te_fi):.3f} | OOB: {rf.oob_score_:.3f}', fontsize=12)plt.tight_layout()plt.show()
Importancia de variables en Random Forest: MDI vs. Permutation Importance
Hiperparámetros de Random Forest
Los principales hiperparámetros a considerar son:
Parámetro
Descripción
Default
Impacto
n_estimators
Número de árboles \(B\)
100
Más → más estable, jamás empeora
max_features
Variables por nodo \(m\)
'sqrt'
Controla la decorrelación
max_depth
Profundidad máxima
None
None = árboles completos (recomendado)
min_samples_leaf
Min. muestras en hoja
1 (clf) / 5 (reg)
Regulariza los árboles individuales
bootstrap
Usar muestras bootstrap
True
False = Pasting (sin reemplazo)
Comparación de desempeño: árbol simple vs. Bagging vs. Random Forest
Bagging: frontera más suave, promedio de muchas fronteras angulares
Random Forest: frontera más suave aún, gracias a la decorrelación entre árboles
La mejora de Bagging a Random Forest suele ser notable cuando hay muchas variables y algunas dominantes.
Ventajas y Limitaciones
Ventajas
Robusto al sobreajuste: agregar más árboles nunca empeora el desempeño
Sin ajuste fino requerido: los defaults suelen funcionar bien directamente
OOB estima la generalización sin reservar datos de validación
Paralelizable: cada árbol es independiente, se puede entrenar en paralelo
Importancia de variables robusta y estable (promediada sobre cientos de árboles)
Maneja variables mixtas y datos con ruido
Limitaciones
No extrapola más allá del rango de entrenamiento
Menos interpretable que un árbol individual (no hay una regla IF-THEN legible)
Costoso en predicción si \(B\) es grande y la latencia importa
Subóptimo con relaciones lineales puras — la regresión lineal las captura mejor con menos datos
Importancia MDI sesgada hacia variables de alta cardinalidad (aplica el mismo sesgo que en árboles simples)
¿Cuándo usar cada método?
Situación
Recomendación
Interpretabilidad crítica
Árbol de decisión (podado)
Máximo desempeño, baja latencia
Random Forest
Muchas variables irrelevantes
Random Forest (m pequeño)
Pocas variables, todas relevantes
Bagging puede ser suficiente
Relaciones principalmente lineales
Regresión regularizada (Ridge/Lasso)
Discusión: ¿por qué funciona el promedio?
La siguiente secuencia de preguntas está diseñada para que redescubras la lógica detrás de Bagging y Random Forest en lugar de solo recibirla. Cada pregunta lleva a la siguiente.
Pregunta 1 — El caso ideal
Un clasificador se equivoca el 30% de las veces de forma independiente a los demás. Si promedias 2 clasificadores así, ¿cuál es la probabilidad de que el ensemble se equivoque? ¿Y con 10? ¿Y con 100?
Pista: el ensemble comete error solo si la mayoría vota mal. ¿Qué distribución describe eso?
Para discutir: ¿Qué condición es necesaria para que el promedio funcione tan bien? ¿Es realista en la práctica?
Pregunta 2 — El problema de la dependencia
En Bagging, todos los árboles se entrenan con datos del mismo dataset original. Si existe una variable muy predictiva (digamos, la más importante con diferencia), ¿qué harán casi todos los árboles en su primera división?
Para discutir: Si 100 árboles hacen casi la misma primera división, ¿cuánto se parece esto a tener un solo árbol? ¿Qué ganamos realmente al promediarlos?
Pregunta 3 — El techo de Bagging
La varianza del promedio de \(B\) árboles con correlación \(\rho\) entre sí es: \[\text{Var}[\bar{T}] = \rho\,\sigma^2 + \frac{1-\rho}{B}\,\sigma^2\] Cuando \(B \to \infty\), ¿qué término desaparece y cuál queda?
Para discutir: ¿Qué implica eso sobre agregar árboles indefinidamente? ¿Cuál es la única forma de seguir reduciendo la varianza más allá de ese límite?
Pregunta 4 — Reinventando Random Forest
Quieres reducir \(\rho\) — la correlación entre árboles — sin cambiar el dataset ni el tipo de modelo. ¿Qué modificación le harías al proceso de entrenamiento de cada árbol?
No hay una única respuesta correcta. Piensa qué es lo que hace que dos árboles se parezcan.
Para discutir: Compara tu propuesta con la de tus compañeros. ¿Alguna de las ideas coincide con lo que hace Random Forest? ¿Hay propuestas distintas que también podrían funcionar?
Pregunta 5 — El costo de decorrelacionar
Random Forest logra reducir \(\rho\) limitando las variables disponibles en cada nodo. Pero si en un nodo solo puedes usar \(m = 1\) variable (elegida al azar), ¿qué le pasa a la calidad de cada árbol individual?
Para discutir: ¿Qué trade-off estás aceptando al reducir \(m\)? ¿Por qué \(m = \sqrt{p}\) suele ser un buen balance y no \(m = 1\)?
Pregunta de cierre — Conectando todo
Imagina que tienes un dataset con 100 variables pero solo 3 son verdaderamente informativas y las 97 restantes son ruido puro. ¿Qué le pasaría a Bagging? ¿Y a Random Forest con \(m = \sqrt{100} = 10\)? ¿Qué valor de \(m\) usarías tú y por qué?
Esta pregunta no tiene una respuesta única — el objetivo es que justifiques tu razonamiento conectando varianza, correlación y sesgo.
Tarea: ensemble sobre datos reales
En esta tarea aplicarás Bagging y Random Forest al dataset de cáncer de mama (breast_cancer de scikit-learn), que ya conoces del capítulo anterior. El reto: tomar todas tus decisiones de modelado usando únicamente el error OOB, sin mirar el conjunto de test hasta el final.
El dataset tiene 569 observaciones, 30 variables numéricas (características del núcleo celular) y dos clases: tumor maligno (1) o benigno (0).
Entrena un BaggingClassifier y un RandomForestClassifier variando n_estimators entre 2 y 200. Para cada valor y cada modelo registra el error OOB. Grafica ambas curvas en la misma figura y determina a partir de qué \(B\) cada una se estabiliza. Usa ese valor en el resto de la tarea.
Código
# Tu código aquí
Preguntas:
¿A partir de qué \(B\) el error OOB de cada modelo ya no mejora más de 0.5 puntos porcentuales?
¿Convergen al mismo valor de error o uno de los dos es consistentemente mejor? ¿Por qué?
¿Coincide ese umbral con lo que viste en los ejemplos del capítulo, o este dataset requiere más/menos árboles?
Parte 2 — Ajustando max_features (solo Random Forest)
Con el \(B\) fijado en la parte anterior, barre max_features desde 1 hasta \(p = 30\) en el Random Forest. Para cada valor registra el error OOB. Grafica la curva.
Bagging no tiene este dial — siempre usa las \(p = 30\) variables en cada nodo, lo que equivale a max_features=p en la gráfica.
Código
# Tu código aquí
Preguntas:
¿Dónde está el mínimo OOB? ¿Coincide con el default \(\sqrt{30} \approx 5\) o el óptimo está en otro valor?
¿Qué dice eso sobre la estructura de este dataset — hay pocas variables muy dominantes o la información está distribuida?
Marca en la gráfica el punto que corresponde a Bagging (\(m = 30\)). ¿Cuánto mejora Random Forest sobre Bagging en este dataset?
Elige tu max_features óptimo y justifícalo.
Parte 3 — ¿En qué variables confiar?
Obtén el ranking de importancia MDI para Bagging (promediando feature_importances_ sobre los estimadores manualmente) y para Random Forest (directamente de feature_importances_). Grafícalos lado a lado junto con la Permutation Importance del Random Forest.
Código
# Pista para la importancia MDI de Bagging:# import numpy as np# imp_bagging = np.mean([t.feature_importances_ for t in bag.estimators_], axis=0)
Preguntas:
¿Coinciden los rankings de Bagging y Random Forest? Si difieren, ¿en qué variables y por qué crees que ocurre?
Compara el ranking MDI de Random Forest contra su Permutation Importance. ¿Hay variables donde difieran notablemente?
Si tuvieras que reducir el modelo a las 5 variables más importantes para una aplicación clínica, ¿qué ranking usarías y por qué?
Parte 4 — El veredicto
Ahora sí, evalúa los tres modelos sobre el conjunto de test: árbol individual, Bagging y Random Forest (con los hiperparámetros elegidos en las partes anteriores). Reporta accuracy, precisión, recall y F1 para cada uno en una tabla comparativa.
Código
# Tu código aquí
Preguntas:
¿Qué tan bien aproximó el error OOB de cada modelo al error real en test? ¿Fue conservador o optimista?
¿Cuánto mejora Bagging sobre el árbol individual? ¿Y Random Forest sobre Bagging? ¿En cuál salto hay mayor ganancia?
Considerando que en oncología un falso negativo (predecir benigno cuando es maligno) es mucho más costoso que un falso positivo, ¿cuál de los tres modelos elegirías y por qué? ¿Cambiaría tu respuesta si el criterio fuera minimizar intervenciones innecesarias?
¿Cambiarías algo de tus decisiones de las partes 1 y 2 si hubieras podido mirar el test desde el principio?