fast.ai · Practical Deep Learning for Coders · Lección 6
Random forests: 20 años de vigencia sin necesitar un gradiente
Ningún .backward(), ningún learning rate, ningún epoch en esta lección — random forests
construyen un modelo entero sin gradient descent, y siguen siendo, dos décadas después de su invención, un
baseline casi imbatible para datos tabulares.
Antes de leerlección 5 §2, datos tabulares
🎯 Al terminar vas a poder
- Explicar con tus palabras cómo un árbol elige un split, y por qué un árbol sin límite de hojas memoriza el training set.
- Preparar un DataFrame para árboles con
TabularPandas(categorías ordinales, fechas descompuestas, split por tiempo) y entrenar unRandomForestRegressormidiendo RMSE en escala log. - Leer el OOB error, una tabla de feature importance y un gráfico de partial dependence sin confundir "importante" con "causal".
- Diagnosticar cuándo un random forest no sirve (tendencia temporal, extrapolación, alta cardinalidad) y qué alternativa usar: red tabular, embeddings, gradient boosting o un ensamble.
1. El dataset: subasta de bulldozers
El caso de estudio es la competencia Kaggle Blue Book for Bulldozers: predecir
SalePrice de maquinaria pesada usada, a partir de uso, tipo de equipo y configuración. Como
Kaggle evalúa con RMSLE (root mean squared log error), se trabaja directamente con el logaritmo del precio
— así el RMSE normal sobre esa columna transformada ya da la métrica de la competencia.
df = pd.read_csv(path/'TrainAndValid.csv', low_memory=False)
dep_var = 'SalePrice'
df[dep_var] = np.log(df[dep_var]) # de acá en más, RMSE sobre esta columna == RMSLE
def r_mse(pred, y): return round(math.sqrt(((pred - y)**2).mean()), 6)
def m_rmse(m, xs, y): return r_mse(m.predict(xs), y)
Leé la fórmula en palabras: diferencia entre predicción y valor real (ya en log), al cuadrado, promedio,
raíz. Como está en escala logarítmica, un RMSE de 0,23 significa que una predicción típica se desvía del
precio real en un factor de e^0.23 ≈ 1.26 — un 26 % arriba o abajo. Eso es lo que mide la
competencia: error relativo, no en dólares (un error de US$5.000 pesa muy distinto sobre una
máquina de US$10.000 que sobre una de US$200.000). El detalle fino: la RMSLE estricta de Kaggle usa
log(1 + x); con precios de miles de dólares la diferencia con log(x) es
despreciable, y el libro la ignora.
Dos detalles de preprocesamiento específicos de datos tabulares que no aparecen con imágenes o texto:
- Categorías ordinales: una columna como
ProductSize(Large / Medium / Small / Mini / Compact) tiene un orden natural que Pandas no conoce por defecto — hay que declararlo explícitamente (df['ProductSize'] = df['ProductSize'].astype('category'); df['ProductSize'].cat.set_categories(sizes, ordered=True)) para que el árbol pueda usar ese orden en sus splits en vez de un orden arbitrario de aparición. Con el orden declarado, un split "ProductSize ≤ Medium" separa grande de chico; sin él, separa según la letra inicial. - Fechas: una fecha no es solo un número ordinal — contiene señal cualitativa (día de la semana, si es feriado, mes).
add_datepart(df, 'saledate')descompone una columna de fecha ensaleYear,saleMonth,saleWeek,saleDayofweek,saleIs_month_end, etc., mássaleElapsed(segundos desde 1970, la versión continua), dándole al árbol variables nuevas sobre las que hacer preguntas binarias sensatas.
El resto del preprocesamiento lo hace TabularPandas con dos procs:
Categorify reemplaza cada string categórico por un entero (y guarda el mapa), y
FillMissing rellena los continuos faltantes con la mediana y agrega una columna booleana
_na por cada uno — porque "faltaba" es en sí mismo información que el árbol puede usar.
cont_cat_split decide qué columnas son continuas y cuáles categóricas según su cardinalidad
(acá, con max_card=1, todo lo que no sea numérico puro va a categórico):
procs = [Categorify, FillMissing]
cond = (df.saleYear < 2011) | (df.saleMonth < 10) # entrena hasta sept. 2011, valida después
train_idx = np.where(cond)[0]; valid_idx = np.where(~cond)[0]
splits = (list(train_idx), list(valid_idx))
cont, cat = cont_cat_split(df, 1, dep_var=dep_var)
to = TabularPandas(df, procs, cat, cont, y_names=dep_var, splits=splits)
xs, y = to.train.xs, to.train.y
valid_xs, valid_y = to.valid.xs, to.valid.y
La validación se define por tiempo, no al azar: entrenamiento hasta cierta fecha, validación después — porque es una serie temporal, y una validación aleatoria filtraría información del futuro al entrenamiento de forma artificial. El test set de Kaggle son ventas posteriores a todo el training set; si tu validación no reproduce esa estructura, tu score de validación va a ser optimista y te vas a enterar recién al enviar.
2. Cómo construye un split un árbol de decisión
Un árbol de decisión hace preguntas binarias sucesivas sobre una variable. El algoritmo de construcción es completamente greedy: en cada nodo, recorre cada columna, prueba cada punto de corte posible, y para cada uno calcula qué tan bien el promedio del target en los dos grupos resultantes predice los valores reales. Elige el split que más reduce el error entre todas las columnas y valores probados, y repite recursivamente en cada subgrupo.
Antes de entrenar un bosque, el libro entrena un árbol diminuto para poder mirarlo:
from sklearn.tree import DecisionTreeRegressor
m = DecisionTreeRegressor(max_leaf_nodes=4)
m.fit(xs, y)
draw_tree(m, xs, size=10, leaves_parallel=True, precision=2) # o dtreeviz para ver la distribución en cada hoja
Con cuatro hojas, el árbol solo puede hacer tres preguntas. La primera que elige es sobre
Coupler_System, la siguiente sobre YearMade (≤ 1991 o no) y la tercera sobre
ProductSize. Cada hoja muestra cuántas filas le cayeron (samples), el promedio
del log-precio en ese grupo (value, que es la predicción para cualquier fila que termine ahí)
y el error cuadrático medio dentro del grupo (squared_error). Es el modelo más interpretable
que vas a ver en todo el curso, y ya captura la estructura gruesa: máquinas viejas y chicas valen menos.
max_leaf_nodes=4 que el libro entrena para poder mirarlo. La predicción para
una fila nueva es el promedio de la hoja donde cae; el algoritmo es greedy y no usa ningún gradiente.
Sin min_samples_leaf sigue partiendo hasta memorizar cada fila del training set.
📐 Criterio matemático de split por reducción de impureza (Varianza / MSE)
En cada nodo con conjunto de muestras de cardinalidad , el árbol predice la media de la variable objetivo: . La impureza en regresión es la varianza muestral interna (MSE local):
Para cada variable y posible punto de corte , el split divide en dos nodos hijos: izquierdo y derecho . La reducción de impureza es:
El algoritmo greedy busca el par óptimo . En clasificación, se sustituye la varianza por la impureza de Gini o la Entropía de Shannon .
Sin restricciones, sklearn sigue partiendo hasta que cada hoja tiene un solo dato — memoriza el training
set en vez de generalizar, el equivalente tabular de overfitting (análogo a jugar "20 preguntas" con más
de 20 objetos posibles: al final cada pregunta identifica un único objeto, sin haber aprendido ningún
patrón general). En el notebook, el árbol sin límite da RMSE 0,0 en entrenamiento y ~0,34 en validación;
el remedio simple es min_samples_leaf=25: exigir un mínimo de registros por hoja, con lo cual
el error de entrenamiento sube a ~0,25 y el de validación baja a ~0,32. Ese cruce —entrenamiento peor,
validación mejor— es la firma de que estabas sobreajustando.
3. Random forest: bagging de árboles no correlacionados
Un random forest entrena muchos árboles, cada uno sobre (a) un subconjunto aleatorio de filas (bootstrap, con reemplazo) y (b) en cada split, un subconjunto aleatorio de columnas candidatas. La predicción final es simplemente el promedio de todos los árboles.
def rf(xs, y, n_estimators=40, max_samples=200_000,
max_features=0.5, min_samples_leaf=5, **kwargs):
return RandomForestRegressor(n_jobs=-1, n_estimators=n_estimators,
max_samples=max_samples, max_features=max_features,
min_samples_leaf=min_samples_leaf, oob_score=True).fit(xs, y)
m = rf(xs, y)
m_rmse(m, xs, y), m_rmse(m, valid_xs, valid_y)
(0.17, 0.23) aproximadamente — contra 0.25 / 0.32 del árbol solo
Cada parámetro tiene una lectura concreta: n_estimators es cuántos árboles;
max_samples cuántas filas ve cada árbol; max_features=0.5 que en cada split se
consideren la mitad de las columnas; min_samples_leaf=5 el mínimo por hoja;
n_jobs=-1 usa todos los cores (los árboles son independientes, así que paralelizan
perfecto).
📐 Deducción de la reducción de varianza en ensambles (Teorema de Breiman)
Sea un ensamble de árboles , cada uno con varianza individual y una correlación par a par para todo . La predicción del bosque es el promedio simple . La varianza total del ensamble es:
Dado que hay términos de varianza y términos de covarianza :
La función crucial de max_features: Al aumentar los estimadores , el segundo término , pero el primer término representa un piso irreducible de varianza.
Si solo se hace bootstrap sobre filas, los árboles eligen siempre las variables dominantes en la raíz, manteniendo alto (~0.7). Al restringir cada split a un subconjunto aleatorio de variables (max_features=0.5 o ), Breiman fuerza a los árboles a explorar caminos ortogonales, reduciendo drásticamente la correlación y colapsando el piso de varianza del bosque completo.
La razón por la que esto funciona no es intuición vaga sino una idea estadística precisa: cada árbol
individual comete errores, pero esos errores no están correlacionados entre árboles
distintos, porque cada uno vio un subconjunto distinto de filas y columnas. El promedio de errores no
correlacionados tiende a cero a medida que se agregan más árboles, mientras que el bias de cada árbol
individual no cambia — por eso promediar reduce varianza sin aumentar bias. Random forests son
notablemente insensibles a sus hiperparámetros: n_estimators más alto casi nunca perjudica
(a diferencia de más epochs en una red, que sí puede sobreajustar). Podés verlo directamente
prediciendo con cada árbol por separado y promediando los primeros i:
preds = np.stack([t.predict(valid_xs) for t in m.estimators_]) # [40, n_valid]
plt.plot([r_mse(preds[:i+1].mean(0), valid_y) for i in range(40)])
La curva baja rápido con los primeros 10-15 árboles y después se aplana: agregar el árbol 40 casi no cambia nada, pero tampoco empeora.
🟢 Gradient boosting: la alternativa que hoy gana la mayoría de las competencias tabulares
El libro nombra, al final del capítulo, la otra gran familia de ensambles de árboles: gradient
boosting (XGBoost, LightGBM, CatBoost, o HistGradientBoostingRegressor en
sklearn). La diferencia conceptual es una sola: en un random forest los árboles se entrenan en
paralelo, cada uno sobre el target original, y se promedian; en boosting se entrenan
en secuencia, y cada árbol nuevo aprende a predecir el residuo (lo que los anteriores
todavía no explican), y se suman. Eso lo hace más preciso en la mayoría de los benchmarks
tabulares, pero también cambia la regla de oro: con boosting, más árboles sí sobreajustan,
porque cada uno sigue corrigiendo el error de entrenamiento hasta memorizarlo — hay que usar early
stopping sobre validación y un learning rate (el peso con el que se suma cada árbol). Todo lo demás de
esta lección (split por tiempo, feature importance, partial dependence, el problema de extrapolación)
aplica igual a boosting, porque siguen siendo árboles.
4. Out-of-bag error: validación gratis
Como cada árbol se entrena solo con un subconjunto bootstrap de filas, las filas que un árbol dado no vio son, para ese árbol específico, un conjunto de validación gratuito. El out-of-bag (OOB) error calcula, para cada fila del training set, la predicción promediando únicamente los árboles que no la vieron durante su entrenamiento — permite detectar overfitting sin sacrificar datos reales para un validation set separado, especialmente valioso con datasets chicos.
📐 Demostración matemática del límite Out-Of-Bag (1/e ≈ 36.8 %)
En un dataset con registros, el muestreo bootstrap extrae muestras con reemplazo. En cada extracción simple, la probabilidad de que una fila particular no sea seleccionada es:
Como las extracciones son independientes e idénticamente distribuidas, la probabilidad de que la fila quede completamente fuera de la muestra bootstrap del árbol es:
Aplicando el límite fundamental del cálculo con :
Por lo tanto, cada árbol deja afuera exactamente el 36.8 % de los datos. El estimador insesgado OOB para la muestra agrega únicamente los árboles donde :
r_mse(m.oob_prediction_, y)
0.21 aproximadamente
Fijate qué atributo se usa: oob_prediction_ (una predicción por fila de entrenamiento), no
oob_score_. Este último existe, pero en un regresor de sklearn es un R², no un RMSE — si lo
imprimís esperando ver algo comparable con tu métrica, vas a leer un número sin sentido. Y prestá
atención a la comparación: OOB ~0,21 contra validación ~0,23. Que el OOB sea mejor que la
validación es la señal de que hay algo más que overfitting: el OOB mide error sobre filas del mismo
período que el entrenamiento, la validación mide error sobre el futuro. La brecha entre ambos es,
exactamente, el costo de la tendencia temporal — el tema de §6.
5. Interpretar el bosque: feature importance y partial dependence
Acá está la ventaja real de random forests frente a redes neuronales en datos tabulares: son mucho más fáciles de interpretar, con dos herramientas concretas.
Feature importance (m.feature_importances_): para cada split de cada árbol
se registra qué variable se usó y cuánto mejoró el error, ponderado por cuántas filas afectó; se suma todo
por variable y se normaliza (las importancias suman 1).
📐 MDI (Mean Decrease Impurity) vs. Importancia por Permutación
El cálculo nativo de scikit-learn es el Mean Decrease in Impurity (MDI). Para una variable , acumula la reducción ponderada de impureza sobre todos los nodos de todos los árboles donde fue elegida para el split:
Sesgo crítico del MDI: El MDI favorece artificialmente variables continuas con muchos valores únicos o categóricas de altísima cardinalidad (como IDs o timestamps), porque al ofrecer millones de puntos de corte posibles tienen mayor probabilidad de lograr una división espuria en el training set.
Importancia por Permutación (Permutation Feature Importance): Evalúa directamente sobre el conjunto de validación. Para la variable , se permutan aleatoriamente sus valores rompiendo su correlación con :
Si la métrica empeora drásticamente ( alto), la red dependía fuertemente de . Si el error no cambia o mejora, la variable es ruido o redundante.
La función del libro envuelve el cálculo de importancia en un DataFrame ordenado:
def rf_feat_importance(m, df):
return pd.DataFrame({'cols': df.columns, 'imp': m.feature_importances_}
).sort_values('imp', ascending=False)
fi = rf_feat_importance(m, xs)
fi[:10] # YearMade, ProductSize, Coupler_System, fiProductClassDesc...
to_keep = fi[fi.imp > 0.005].cols # de 78 columnas quedan ~21
m = rf(xs[to_keep], y)
m_rmse(m, valid_xs[to_keep], valid_y) # casi igual que con las 78
Permite eliminar variables poco importantes sin perder precisión — un modelo más simple e igual de
bueno. El libro va un paso más: con cluster_columns agrupa variables que dicen lo mismo
(saleYear y saleElapsed, ProductGroup y
ProductGroupDesc) y las elimina de a una comprobando que el OOB no empeore. Lo que la
importancia no dice: si una variable importa porque causa el precio o porque está correlacionada
con algo que lo causa. Y entre dos columnas redundantes, la importancia se reparte de forma arbitraria —
por eso conviene eliminar redundantes antes de leer el ranking.
Partial dependence responde una pregunta distinta: "¿cómo cambiaría la predicción si solo variara esta columna, dejando todo lo demás constante?" No alcanza con promediar por valor real de la variable (eso mezcla el efecto de otras variables correlacionadas); en cambio, se reemplaza artificialmente esa columna con cada valor posible para todas las filas y se promedia la predicción resultante. En el dataset de bulldozers esto reveló una relación casi exponencial entre año de fabricación y precio, y en un caso alertó sobre una fuga de datos (data leakage) al mostrar que el grupo "dato faltante" de una columna tenía un precio anómalamente bajo — una señal de que esa "ausencia" en realidad codificaba información del futuro.
🟢 La API de sklearn cambió
El libro usa plot_partial_dependence, que sklearn eliminó en versiones recientes. La forma
actual es from sklearn.inspection import PartialDependenceDisplay y
PartialDependenceDisplay.from_estimator(m, valid_xs, ['YearMade', 'ProductSize']).
El cálculo es el mismo; solo cambió el nombre.
📜 Una tercera herramienta: tree interpreter
Para explicar una predicción individual (no el modelo en general), el libro usa un
tree interpreter: descompone esa predicción puntual en un bias (la
predicción del nodo raíz, la media global) más la contribution de cada variable a lo largo
del camino recorrido en cada árbol, sumado sobre todo el bosque — visualizado como un waterfall chart
que muestra cómo cada variable empuja la predicción hacia arriba o abajo desde el bias hasta el valor
final. Se cumple siempre que bias + contributions.sum() == prediction, por
construcción.
6. El problema de extrapolación
Un árbol (y por lo tanto un random forest) predice siempre el promedio de las hojas que vio en entrenamiento — nunca puede predecir un valor fuera del rango del target que observó. El libro lo demuestra con 40 puntos sobre una recta con ruido, entrenando con los primeros 30: a la derecha del último punto de entrenamiento, la predicción se vuelve una línea horizontal, clavada en el promedio de la última hoja. Con una relación creciente en el tiempo (inflación, tendencia de precios), al testear fuera del rango de entrenamiento las predicciones quedan sistemáticamente por debajo de la tendencia real. Esto es crítico en datos con tendencia temporal fuerte como precios de subasta.
Para detectar este tipo de domain shift entre entrenamiento y validación, una técnica simple: se entrena un random forest cuyo target es "¿esta fila es de validación o de entrenamiento?" — las variables con mayor importancia en ese modelo auxiliar son las que difieren sistemáticamente entre ambos conjuntos, y conviene tratarlas con cuidado (o directamente eliminarlas) en el modelo real.
df_dom = pd.concat([xs_final, valid_xs_final])
is_valid = np.array([0]*len(xs_final) + [1]*len(valid_xs_final))
m = rf(df_dom, is_valid)
rf_feat_importance(m, df_dom)[:6] # saleElapsed, SalesID, MachineID encabezan
Que saleElapsed sea la más discriminante es obvio (es la fecha). Pero SalesID y
MachineID son ids que crecen con el tiempo: no dicen nada del precio en sí, y sin embargo el
modelo puede usarlas como reloj disfrazado. Al sacarlas, el error de validación mejora un poco y el
modelo deja de depender de algo que no va a tener sentido sobre datos nuevos. El libro cierra con otra
idea simple: quitar los datos más viejos (antes de 2004) también ayuda, porque el mercado cambió y los
patrones antiguos ya no aplican.
7. Random forests vs. redes neuronales
| Random forest | Red neuronal | |
|---|---|---|
| Preprocesamiento | Mínimo — tolera categóricas y outliers sin normalizar | Requiere normalización cuidadosa |
| Tuning | Muy poco, insensible a hiperparámetros | Sensible — learning rate, arquitectura, regularización |
| Interpretabilidad | Alta: feature importance, partial dependence, tree interpreter | Baja sin herramientas adicionales |
| Categóricas de alta cardinalidad | Débil | Fuerte, vía entity embeddings |
| Extrapolación (tendencias) | Mala — predice siempre dentro del rango visto | Puede generalizar mejor con la variable temporal como continua |
| Tiempo de entrenamiento | Segundos a minutos en CPU | Minutos, y conviene GPU |
La red tabular del libro se arma con tabular_learner(dls, y_range=(8, 12), layers=[500, 250],
n_out=1, loss_func=F.mse_loss) — y_range acota la salida al rango del log-precio,
igual que en collaborative filtering (Lección 7) — y con
fit_one_cycle(5, 1e-2) llega a un RMSE de validación levemente mejor que el bosque. El
"mejor de ambos mundos" que sugiere el libro: entrenar embeddings categóricos con una red neuronal y
usarlos como input numérico —en vez de las columnas categóricas crudas— para un random forest u otro
método clásico. El paper de referencia (Guo & Berkhahn, 2016) mostró mejoras dramáticas en KNN, random
forest y GBM al reemplazar categorías crudas por embeddings aprendidos, sin necesitar la red neuronal en
el momento de inferencia. Ensamblar (promediar) las predicciones de un random forest y una red neuronal
entrenados por separado también suele superar a cualquiera de los dos solo — en el notebook, el promedio
simple de ambas predicciones da el mejor RMSE de todo el capítulo, del orden de 0,22.
8. Errores comunes
Casi todos los errores con árboles no son del modelo sino de cómo se preparó y se leyó el dataset.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Validación excelente, score de Kaggle (o producción) mucho peor | Split aleatorio sobre una serie temporal: el modelo vio el futuro de cada fila de validación en filas vecinas del training set | Split por fecha, con validación estrictamente posterior al entrenamiento; comprobá con el truco de "¿es validación?" de §6 |
| Una columna rara domina la feature importance y el error es "demasiado bueno" | Fuga de datos: la columna se completa después de conocer el target (ids que crecen con el tiempo, un flag que se marca al cerrar la venta, un "faltante" que solo aparece en cierto período) | Preguntate para cada columna top si existía en el momento de predecir; mirá partial dependence del valor faltante; eliminá y reentrená |
| Splits sobre una categoría ordinal no tienen sentido (separan "Large" de "Mini" pero mezclan "Medium") | La columna es categórica sin orden declarado: sklearn ve enteros asignados por orden alfabético o de aparición | astype('category') + cat.set_categories(sizes, ordered=True) antes de TabularPandas |
Subís n_estimators de 40 a 400 esperando una mejora grande, y no cambia nada | Los árboles se promedian: el error de varianza ya bajó con los primeros; n_estimators no es "epochs" | Aceptá la meseta; para mejorar tocá min_samples_leaf, las columnas, o cambiá a boosting (donde sí importa, y sí sobreajusta) |
| "El año de fabricación causa el precio, importancia 0,18" | Feature importance mide cuánto usó el modelo la variable, no causalidad; una variable correlacionada con la causa real puede rankear igual de alto | Usá la importancia para simplificar el modelo y para orientar preguntas; la causalidad requiere diseño experimental o conocimiento del dominio |
m.oob_score_ da 0,89 y no sabés cómo compararlo con tu RMSE | En regresión, oob_score_ es R², no la métrica de la competencia | r_mse(m.oob_prediction_, y) |
| Las predicciones en datos nuevos quedan todas por debajo de los precios reales | Tendencia temporal: el bosque no extrapola fuera del rango que vio | Eliminá variables que actúan como reloj, recortá datos viejos, o pasá la tendencia a un modelo que sí extrapole (red con la fecha como continua, o modelo lineal sobre el residuo) |
9. Resumen
- Un árbol de decisión elige, en cada nodo, el split que más reduce el error entre todas las variables y valores posibles — sin gradiente de por medio. Sin límite de hojas memoriza;
min_samples_leaflo frena. - Random forest = bagging: muchos árboles sobre subconjuntos aleatorios de filas y columnas, promediados. Funciona porque sus errores individuales no están correlacionados. Más árboles nunca empeora; en gradient boosting (árboles secuenciales sobre el residuo) sí.
- OOB error da una validación casi gratis, usando las filas que cada árbol no vio — y la brecha entre OOB y validación temporal te dice cuánto pesa la tendencia.
- Feature importance y partial dependence son las dos herramientas de interpretación que hacen que random forests sigan siendo preferidos cuando explicar el modelo importa tanto como predecir bien. Ninguna de las dos habla de causalidad.
- Random forests no pueden extrapolar fuera del rango de valores vistos — un problema real con datos que tienen tendencia temporal. El clasificador "¿es validación?" detecta qué columnas cambian entre períodos.
- Elegir entre random forest y red neuronal no es una decisión ideológica: depende de cardinalidad de categóricas, necesidad de extrapolar, y cuánto importa poder explicar cada predicción. Y el ensamble de ambos suele ganarle a los dos.
10. Ponete a prueba
Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.
1. ¿Por qué el libro aplica np.log al precio antes de entrenar, en vez de usar la columna cruda?
Porque la competencia evalúa con RMSLE, que mide error relativo: tomar el log primero convierte esa métrica en un RMSE común sobre la columna transformada, y hace que un error de US$5.000 pese más en una máquina barata que en una cara. (§1)
2. Un árbol sin restricciones da RMSE 0,0 en entrenamiento. ¿Es buena noticia?
No: significa que cada hoja tiene un único registro y el árbol memorizó el training set. Se detecta porque la validación queda mucho peor (~0,34). min_samples_leaf=25 sube el error de entrenamiento y baja el de validación. (§2)
3. ¿Qué pasa con el error de validación si pasás de 40 a 400 árboles? ¿Y si fuera gradient boosting?
Con random forest casi nada: los árboles se promedian y la varianza ya cayó con los primeros 10-15; nunca empeora. Con boosting, cada árbol nuevo corrige el residuo de los anteriores, así que más árboles sí pueden sobreajustar — se necesita early stopping. (§3)
4. El OOB RMSE da 0,21 y el de validación 0,23. ¿Qué te dice esa diferencia?
Que hay algo además de overfitting: el OOB se calcula sobre filas del mismo período que el entrenamiento, mientras la validación es posterior en el tiempo. La brecha mide el costo de la tendencia temporal / domain shift. (§4, §6)
5. ¿Por qué partial dependence no se calcula simplemente promediando el precio real por cada valor de YearMade?
Porque ese promedio mezcla el efecto de otras variables correlacionadas con el año (por ejemplo, máquinas nuevas también son de modelos más caros). Partial dependence reemplaza la columna por cada valor en todas las filas y promedia la predicción, aislando el efecto de esa variable sola. (§5)
6. Entrenás un bosque para distinguir filas de entrenamiento vs. validación y MachineID queda entre las más importantes. ¿Qué hacés?
Sospechar que es un id que crece con el tiempo y el modelo lo usa como reloj disfrazado. Sacarlo del modelo real y comprobar que el error de validación no empeora (en el libro, mejora un poco). (§6)
11. Para practicar
✏️ Ejercicio 1 — Ver la extrapolación fallar con tus propios ojos
Reproducí el experimento del libro con datos sintéticos: una recta con ruido, entrenada con los primeros 30 puntos, evaluada sobre los 40. Solo necesitás numpy, scikit-learn y matplotlib; corre en CPU en un segundo.
import numpy as np, matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(0)
x = np.linspace(0, 20, 40); y = x + rng.normal(0, 0.2, 40)
X = x[:, None]
rf = RandomForestRegressor(n_estimators=100, random_state=0).fit(X[:30], y[:30])
lin = LinearRegression().fit(X[:30], y[:30])
plt.scatter(x, y, label='real', s=12)
plt.plot(x, rf.predict(X), label='random forest')
plt.plot(x, lin.predict(X), label='lineal', ls='--')
plt.axvline(x[29], color='gray', lw=0.8); plt.legend(); plt.show()
print('máximo predicho por el bosque:', rf.predict(X).max().round(2), '| máximo real:', y.max().round(2))
Pista / qué esperar
A la derecha de la línea gris, la predicción del bosque se aplana en el valor de la última hoja (~15), mientras la recta sigue subiendo hasta ~20. El máximo predicho nunca supera el máximo visto en entrenamiento. Probá después con y = x**2: el bosque interpola bien dentro del rango y falla igual afuera.
✏️ Ejercicio 2 — Detectar una fuga de datos con feature importance y OOB
Armá un dataset sintético con una columna "filtrada" que solo existe porque ya se conoce el target, y observá cómo el bosque la adopta. Después sacala y compará.
import numpy as np, pandas as pd
from sklearn.ensemble import RandomForestRegressor
rng = np.random.default_rng(1); n = 5000
df = pd.DataFrame({'year': rng.integers(1990, 2012, n),
'hours': rng.exponential(3000, n),
'size': rng.integers(0, 5, n)})
y = 9 + 0.05*(df.year-1990) - 0.0001*df.hours + 0.2*df.size + rng.normal(0, 0.3, n)
df['leak'] = (y > y.median()).astype(int) # se calcula DESPUÉS de conocer y
def fit(cols):
m = RandomForestRegressor(n_estimators=60, min_samples_leaf=5, oob_score=True,
n_jobs=-1, random_state=0).fit(df[cols], y)
oob = np.sqrt(((m.oob_prediction_ - y)**2).mean())
fi = pd.DataFrame({'cols': cols, 'imp': m.feature_importances_}).sort_values('imp', ascending=False)
print(f'OOB RMSE {oob:.3f}'); print(fi.to_string(index=False))
fit(['year', 'hours', 'size', 'leak'])
fit(['year', 'hours', 'size'])
Pista / qué esperar
Con leak, esa columna se lleva la mayor parte de la importancia y el OOB RMSE queda "sospechosamente" bajo. Sin ella, la importancia se reparte entre year, size y hours en ese orden aproximado, y el RMSE sube hacia el ruido real (~0,3). En un dataset de verdad no tenés la etiqueta "leak": la pista es una columna dominante cuyo valor no podría conocerse en el momento de predecir.
12. Fuentes y para seguir
- fastbook — 09_tabular.ipynb — el capítulo completo con todo el código (TabularPandas, rf, rf_feat_importance, tree interpreter, red tabular y ensamble). Leelo con el dataset descargado de Kaggle a mano.
- course.fast.ai — Lesson 6 — el video de la lección, con la construcción del árbol chico y la explicación en vivo de bagging.
- scikit-learn — Ensembles: gradient boosting, random forests, bagging — documentación oficial de
RandomForestRegressor,HistGradientBoostingRegressory sus parámetros; la referencia para cuando quieras pasar de bosque a boosting. - Guo & Berkhahn, "Entity Embeddings of Categorical Variables" (2016) — el paper detrás de la tabla de §7: embeddings aprendidos por una red mejoran KNN, random forest y GBM.
- Breiman, "Random Forests" (Machine Learning, 2001) — el paper original; la sección sobre correlación entre árboles y el bound del error de generalización es la versión formal de §3.
- Kaggle — Blue Book for Bulldozers — el dataset y la definición exacta de la métrica RMSLE.