Curso / fast.ai / Lección 6
fast.ai · 6 de 14

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 un RandomForestRegressor midiendo 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 en saleYear, saleMonth, saleWeek, saleDayofweek, saleIs_month_end, etc., más saleElapsed (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.

Un árbol de decisión con cuatro hojas: tres preguntas binarias y un promedio en cada hoja El nodo raíz pregunta por Coupler System; una rama pregunta si YearMade es menor o igual a 1991; otra pregunta por ProductSize. Cada hoja guarda cuántas filas cayeron, el promedio del log-precio, que es la predicción para cualquier fila que llegue ahí, y el error cuadrático dentro del grupo. Para elegir cada split el algoritmo prueba todas las columnas y todos los puntos de corte y se queda con el que más reduce el error. raíz · todas las filas Coupler_System ≤ 0,5 ? sí no nodo YearMade ≤ 1991 ? nodo ProductSize ≤ … ? sí no sí no hoja samples: n filas value: promedio del log-precio hoja samples · value squared_error del grupo hoja samples · value squared_error del grupo hoja samples · value squared_error del grupo cada split: probar cada columna y cada corte, elegir el que más reduce el error, repetir en cada rama · sin límite de hojas, sigue hasta una fila por hoja
El árbol de 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 t con conjunto de muestras Dt de cardinalidad Nt, el árbol predice la media de la variable objetivo: y¯t=1Nt∑i∈Dtyi. La impureza I(t) en regresión es la varianza muestral interna (MSE local):

I(t)=1Nt∑i∈Dt(yi−y¯t)2

Para cada variable j y posible punto de corte s, el split divide Dt en dos nodos hijos: izquierdo DL={i∈Dt:xi,j≤s} y derecho DR={i∈Dt:xi,j>s}. La reducción de impureza ΔI(s,t) es:

ΔI(s,t)=I(t)−(NLNtI(tL)+NRNtI(tR))

El algoritmo greedy busca el par óptimo (j*,s*)=argmaxj,sΔI(s,t). En clasificación, se sustituye la varianza por la impureza de Gini IG(t)=1−∑kptk2 o la Entropía de Shannon H(t)=−∑kptklog2ptk.

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 B árboles T1(x),…,TB(x), cada uno con varianza individual Var(Tb(x))=σ2 y una correlación par a par ρ=Corr(Tb(x),Tc(x)) para todo b≠c. La predicción del bosque es el promedio simple T¯(x)=1B∑b=1BTb(x). La varianza total del ensamble es:

Var(T¯(x))=Var(1B∑b=1BTb(x))=1B2[∑b=1BVar(Tb)+∑b≠cCov(Tb,Tc)]

Dado que hay B términos de varianza y B(B−1) términos de covarianza Cov(Tb,Tc)=ρσ2:

Var(T¯(x))=1B2[Bσ2+B(B−1)ρσ2]=ρσ2+1−ρBσ2

La función crucial de max_features: Al aumentar los estimadores B→∞, el segundo término 1−ρBσ2→0, pero el primer término ρσ2 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 D), Breiman fuerza a los árboles a explorar caminos ortogonales, reduciendo drásticamente la correlación ρ→0 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.

Random forest: bagging de árboles que ven filas y columnas distintas, y el error out-of-bag Del dataset se sacan muestras bootstrap con reemplazo, una por árbol, y en cada split cada árbol considera solo la mitad de las columnas. La predicción es el promedio de los árboles. Las filas que un árbol no vio son su conjunto de validación gratis: la predicción out-of-bag de cada fila promedia solo los árboles que no la usaron. dataset n filas 78 columnas bootstrap muestra por árbol · con reemplazo · mitad de las columnas por split punteado: filas que ese árbol no vio (out-of-bag) árbol 1 árbol 2 árbol 3 … 40 promedio de los 40 árboles predicción RMSE 0,23 en validación (0,32 con un árbol solo) por qué funciona: cada árbol se equivoca, pero sus errores no están correlacionados porque vio filas y columnas distintas; el promedio los cancela sin subir el bias out-of-bag: para cada fila, promediar solo los árboles que no la usaron → validación gratis sobre el training set · r_mse(m.oob_prediction_, y) ≈ 0,21 la curva del error baja rápido con 10–15 árboles y después se aplana: más árboles nunca empeora (en gradient boosting, secuencial sobre el residuo, sí)
Bagging en una imagen: muchos árboles mediocres y descorrelacionados promedian a un modelo bueno. Las filas que cada árbol no vio dan, de regalo, el error out-of-bag; que sea mejor que el de validación temporal es la medida de cuánto pesa la tendencia en el tiempo.

🟢 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 N registros, el muestreo bootstrap extrae N muestras con reemplazo. En cada extracción simple, la probabilidad de que una fila particular i no sea seleccionada es:

P(fila i no elegida en 1 intento)=1−1N

Como las N extracciones son independientes e idénticamente distribuidas, la probabilidad de que la fila i quede completamente fuera de la muestra bootstrap del árbol es:

P(i∉bootstrap)=(1−1N)N

Aplicando el límite fundamental del cálculo limN→∞(1+x/N)N=ex con x=−1:

limN→∞(1−1N)N=e−1=1e≈0.367879…≈36.8%

Por lo tanto, cada árbol deja afuera exactamente el 36.8 % de los datos. El estimador insesgado OOB para la muestra i agrega únicamente los árboles b donde i∉Sb:

y^iOOB=1∑b=1BI[i∉Sb]∑b:i∉SbTb(xi)
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 Xj, acumula la reducción ponderada de impureza sobre todos los nodos t de todos los árboles B donde Xj fue elegida para el split:

MDI(Xj)=1B∑b=1B∑t∈Tb:v(t)=jNtNΔI(st,t)

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 Xj, se permutan aleatoriamente sus valores rompiendo su correlación con y:

PFI(Xj)=L(Xπj,y)−L(X,y)

Si la métrica empeora drásticamente (PFI>0 alto), la red dependía fuertemente de Xj. 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.

Un random forest no extrapola: fuera del rango de entrenamiento predice el promedio de la última hoja Cuarenta puntos sobre una recta con ruido. El bosque se entrena con los primeros treinta y sigue bien los datos dentro de ese rango, en escalones; a la derecha del último punto de entrenamiento su predicción se vuelve una línea horizontal, mientras que un modelo lineal continúa la recta. entrenó con estos 30 puntos nunca vio esto el bosque se aplana en la última hoja lineal: sigue la recta x → un árbol solo puede devolver promedios de valores que vio: con precios que crecen en el tiempo, todas las predicciones nuevas quedan por debajo
El experimento del libro con datos sintéticos. Dentro del rango de entrenamiento el bosque interpola bien; afuera devuelve el promedio de la última hoja, porque no tiene otra cosa que devolver. Con una tendencia temporal eso es sistemáticamente malo, y por eso conviene sacar las columnas que actúan de reloj.

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 forestRed neuronal
PreprocesamientoMínimo — tolera categóricas y outliers sin normalizarRequiere normalización cuidadosa
TuningMuy poco, insensible a hiperparámetrosSensible — learning rate, arquitectura, regularización
InterpretabilidadAlta: feature importance, partial dependence, tree interpreterBaja sin herramientas adicionales
Categóricas de alta cardinalidadDébilFuerte, vía entity embeddings
Extrapolación (tendencias)Mala — predice siempre dentro del rango vistoPuede generalizar mejor con la variable temporal como continua
Tiempo de entrenamientoSegundos a minutos en CPUMinutos, 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íntomaCausa probableQué hacer
Validación excelente, score de Kaggle (o producción) mucho peorSplit aleatorio sobre una serie temporal: el modelo vio el futuro de cada fila de validación en filas vecinas del training setSplit 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ónastype('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 nadaLos á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 altoUsá 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 RMSEEn regresión, oob_score_ es R², no la métrica de la competenciar_mse(m.oob_prediction_, y)
Las predicciones en datos nuevos quedan todas por debajo de los precios realesTendencia temporal: el bosque no extrapola fuera del rango que vioEliminá 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

  1. 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_leaf lo frena.
  2. 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í.
  3. 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.
  4. 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.
  5. 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.
  6. 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