library(tidyverse)
library(tidymodels)
6 Árboles de decisión y Random Forest
6.1 Árboles de decisión
6.1.1 Definición
Un árbol de decisión es un modelo predictivo que representa un conjunto de reglas “si… entonces…” organizadas en forma de árbol. Parte de un nodo raíz (todos los datos) y, en cada nodo interno, divide los datos según el valor de una variable predictora para reducir la impureza (en regresión: varianza/MSE).
El proceso continúa hasta cumplir criterios de parada (profundidad, mínimo de casos, complejidad) y las hojas entregan la predicción final: promedio de la variable objetivo (regresión).
También se puede aplicar a clasificación. Eso lo veremos más adelante!
6.1.2 Ventajas y desventajas
Ventajas:
Interpretables: reglas “si… entonces…” fáciles de explicar.
Capturan no linealidades e interacciones sin ingeniería previa.
Poco preprocesamiento: no requieren normalizar; toleran outliers mejor que modelos lineales.
Mixtos: funcionan con variables numéricas y categóricas.
Rápidos de entrenar y útiles para generar hipótesis/segmentaciones.
Desventajas:
Sobreajustan e inestables: pequeños cambios en los datos alteran mucho el árbol (alta varianza).
Fronteras en “escalera” y predicciones por tramos; no extrapolan bien.
Sesgo en splits hacia variables con muchos niveles; cuidado al interpretar “importancia”.
6.1.3 Ejemplo simple de partición
Imagina que tenemos la siguiente data:
<- tibble::tibble(
df id = 1:10,
horas_estudio = c(1, 2, 2, 3, 3, 5, 4, 6, 2, 5),
ingreso = c(22, 35, 33, 40, 45, 35, 40, 21, 28, 24)
)
Lo visualizamos en un gráfico de dispersión.
|>
df ggplot()+
aes(x=horas_estudio, y=ingreso)+
geom_point()
Generamos una primera partición:
|>
df ggplot()+
aes(x=horas_estudio, y=ingreso)+
geom_point()+
geom_vline(xintercept = 1.5, linetype = "dashed")
Dentro de cada una de estas secciones vamos a calcular un indicador, el cual es el RMSE a la media de cada uno.
Para el caso de la primera partición, como el error es 0, al tener sólo una observación, su RMSE será 0.
Para el caso de la segunda partición, calculamos los errores, los elevamos al cuadrado, los sumamos y sacamos promedio:
|>
df filter(horas_estudio>=1.5) |>
summarise(mean(ingreso))
# A tibble: 1 × 1
`mean(ingreso)`
<dbl>
1 33.4
|>
df filter(horas_estudio>=1.5) |>
mutate(error_ingreso=ingreso-mean(ingreso)) |> # calculamos el error
mutate(error_ingreso_al2=error_ingreso**2) |>
summarise(RMSE=sum(error_ingreso_al2)/n()) |>
mutate(RMSE*0.9)
# A tibble: 1 × 2
RMSE `RMSE * 0.9`
<dbl> <dbl>
1 55.4 49.8
Ahora, conociendo el grado de dispersión de cada una de las agrupaciones, podemos encontrar un puntaje para el umbral seleccionado.
Este umbral tendrá un puntaje que será la suma de la dispersión de cada sección. En este caso: 49.8
Ya tenemos una métrica para evaluar qué tan buena ha sido esta partición tomando como punto de corte 1.5. Y cómo serán las demás particiones posibles?
Una vez tengamos la función de costo de todas las particiones posibles, el modelo escogerá aquella en la cual el costo será el menor. Ok, ya tenemos el primer nodo.
6.1.4 Aplicación con una predictora numérica
Utilizamos nuestra base de datos:
library(tidyverse)
library(readxl)
library(tidymodels)
<- read_xlsx("data/AML_2.xlsx")
data <- data |>
datafilter(!is.na(aml_index))
set.seed(2025)
<- initial_split(data)
index <- training(index)
training_data <- testing(index) testing_data
<- recipe(aml_index ~ pbi_pc, data = training_data) mi_receta_arbol
<- decision_tree(min_n=5) |>
arbol set_engine("rpart") |>
set_mode("regression")
<- workflow() |>
wf_arbol add_recipe(mi_receta_arbol) |>
add_model(arbol)
<- wf_arbol |>
fit_arbol fit(training_data)
fit_arbol
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: decision_tree()
── Preprocessor ────────────────────────────────────────────────────────────────
0 Recipe Steps
── Model ───────────────────────────────────────────────────────────────────────
n= 59
node), split, n, deviance, yval
* denotes terminal node
1) root 59 66.593350 5.266441
2) pbi_pc>=3813.495 39 18.884070 4.723333
4) pbi_pc>=15749.22 15 3.088373 4.074667
8) pbi_pc>=54674.97 3 0.706400 3.620000 *
9) pbi_pc< 54674.97 12 1.606767 4.188333 *
5) pbi_pc< 15749.22 24 5.539462 5.128750 *
3) pbi_pc< 3813.495 20 13.773500 6.325500
6) pbi_pc>=1118.465 11 4.157018 5.922727 *
7) pbi_pc< 1118.465 9 5.650956 6.817778
14) pbi_pc>=553.69 7 4.194143 6.627143
28) pbi_pc< 735.42 2 0.605000 6.090000 *
29) pbi_pc>=735.42 5 2.781280 6.842000
58) pbi_pc>=823.395 3 0.673400 6.530000 *
59) pbi_pc< 823.395 2 1.377800 7.310000 *
15) pbi_pc< 553.69 2 0.312050 7.485000 *
El resultado del modelo lo podemos visualizar con:
library(rpart.plot)
Cargando paquete requerido: rpart
Adjuntando el paquete: 'rpart'
The following object is masked from 'package:dials':
prune
|>
fit_arbol extract_fit_engine() |>
rpart.plot()
Warning: Cannot retrieve the data used to build the model (model.frame: objeto '..y' no encontrado).
To silence this warning:
Call rpart.plot with roundint=FALSE,
or rebuild the rpart model with model=TRUE.
En este caso, el primer número de cada recuadro es el valor promedio de y (aml_index) en cada sección. Luego aparece el promedio del nodo original que se va a tal o cual nodo.
6.1.5 Aplicación con dos predictoras numéricas
<- recipe(aml_index ~ pbi_pc + rule_of_law, data = training_data)
mi_receta_arbol
<- decision_tree() |>
arbol set_engine("rpart") |>
set_mode("regression")
<- workflow() |>
wf_arbol add_recipe(mi_receta_arbol) |>
add_model(arbol)
<- wf_arbol |>
fit_arbol fit(training_data)
fit_arbol
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: decision_tree()
── Preprocessor ────────────────────────────────────────────────────────────────
0 Recipe Steps
── Model ───────────────────────────────────────────────────────────────────────
n= 59
node), split, n, deviance, yval
* denotes terminal node
1) root 59 66.593350 5.266441
2) pbi_pc>=3813.495 39 18.884070 4.723333
4) pbi_pc>=15749.22 15 3.088373 4.074667 *
5) pbi_pc< 15749.22 24 5.539462 5.128750
10) rule_of_law>=0.5721363 7 0.404600 4.780000 *
11) rule_of_law< 0.5721363 17 3.932906 5.272353 *
3) pbi_pc< 3813.495 20 13.773500 6.325500
6) pbi_pc>=1118.465 11 4.157018 5.922727 *
7) pbi_pc< 1118.465 9 5.650956 6.817778 *
El resultado del modelo lo podemos visualizar con:
library(rpart.plot)
|>
fit_arbol extract_fit_engine() |>
rpart.plot()
Warning: Cannot retrieve the data used to build the model (model.frame: objeto '..y' no encontrado).
To silence this warning:
Call rpart.plot with roundint=FALSE,
or rebuild the rpart model with model=TRUE.
6.1.6 Aplicación con dos predictoras numéricas y un factor
<- recipe(aml_index ~ pbi_pc +
mi_receta_arbol +
rule_of_law
continente, data = training_data)
<- decision_tree() |>
arbol set_engine("rpart") |>
set_mode("regression")
<- workflow() |>
wf_arbol add_recipe(mi_receta_arbol) |>
add_model(arbol)
<- wf_arbol |>
fit_arbol fit(training_data)
fit_arbol
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: decision_tree()
── Preprocessor ────────────────────────────────────────────────────────────────
0 Recipe Steps
── Model ───────────────────────────────────────────────────────────────────────
n= 59
node), split, n, deviance, yval
* denotes terminal node
1) root 59 66.5933500 5.266441
2) pbi_pc>=3813.495 39 18.8840700 4.723333
4) pbi_pc>=15749.22 15 3.0883730 4.074667 *
5) pbi_pc< 15749.22 24 5.5394620 5.128750
10) rule_of_law>=0.5721363 7 0.4046000 4.780000 *
11) rule_of_law< 0.5721363 17 3.9329060 5.272353 *
3) pbi_pc< 3813.495 20 13.7735000 6.325500
6) continente=Americas,Asia 7 0.9899714 5.704286 *
7) continente=Africa 13 8.6276000 6.660000 *
Lo visualizamos:
|>
fit_arbol extract_fit_engine() |>
rpart.plot()
Warning: Cannot retrieve the data used to build the model (model.frame: objeto '..y' no encontrado).
To silence this warning:
Call rpart.plot with roundint=FALSE,
or rebuild the rpart model with model=TRUE.
6.1.7 Importancia de las variables
La importancia de variables es un ranking que indica cuánto aporta cada predictor a las mejoras de predicción del árbol.
Para qué sirve: priorizar predictores, simplificar cuestionarios/inputs (coste), detectar variables inútiles o fugas de información, y comunicar qué factores “pesan” más en términos predictivos.
library(vip)
Adjuntando el paquete: 'vip'
The following object is masked from 'package:utils':
vi
|>
fit_arbol extract_fit_engine() |>
vip()
6.1.8 Comparación RL y Árbol de Decisión
Considerando las mismas variables:
<- recipe(aml_index ~ pbi_pc + rule_of_law+ continente,
mi_receta data = training_data)
¿Cuál modelo logrará la mejor predicción, la regresión lineal o el árbol de decisión?
<- decision_tree() |>
arbol set_engine("rpart") |>
set_mode("regression")
<- workflow() |>
wf_arbol add_recipe(mi_receta_arbol) |>
add_model(arbol)
<- wf_arbol |>
fit_arbol fit(training_data)
|>
fit_arbol predict(testing_data) |>
bind_cols(valor_real=testing_data$aml_index) |>
rmse(truth = valor_real,
estimate = .pred)
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 0.602
En este caso, la regresión lineal ha probado seguir siendo la mejor opción (RMSE = 0.56). No obstante, se encuentran muy cerca en cuanto a su rendimiento.
6.1.9 Problema!
Sobreajuste (overfitting): Crecen hasta “memorizar” el ruido si no se limitan.
Alta varianza / inestabilidad: Pequeños cambios en los datos pueden cambiar mucho la estructura del árbol.
SOLUCIÓN: RANDOM FOREST!
6.2 Random Forest
6.2.1 Definición
Random Forest es un modelo de ensamble que combina muchos árboles de decisión para mejorar la capacidad de generalización. Cada árbol se entrena sobre una muestra bootstrap (con reemplazo) del conjunto de datos y, en cada división, considera solo un subconjunto aleatorio de predictores (mtry).
En regresión, la predicción final es el promedio de los árboles. Esta aleatoriedad + agregación reduce la varianza del árbol individual y hace al modelo robusto sin requerir preprocesamientos complejos.
6.2.2 Ventajas y desventajas
Ventajas:
Captura no linealidades e interacciones automáticamente.
Suele rendir bien “out-of-the-box” y es estable frente a ruido.
No requiere normalización de variables; funciona con muchas x.
Proporciona importancia de variables.
Desventajas:
Menor interpretabilidad que un árbol único.
Puede ser pesado en memoria/tiempo con muchos árboles o datos muy grandes.
6.2.3 Aplicación con tidymodels
Vamos con la misma receta del árbol:
<- recipe(aml_index ~ pbi_pc + rule_of_law+ continente,
mi_receta data = training_data)
Aplicamos el workflow utilizando la función rand_forest():
<- rand_forest(trees = 1000, min_n = 5) |>
bosque_model set_engine("ranger", importance = "permutation") |>
set_mode("regression")
Iniciamos el workflow():
<- workflow() |>
bosque_wf add_recipe(mi_receta) |>
add_model(bosque_model)
Ahora fiteamos el modelo y calculamos el RMSE.
<- bosque_wf |>
fit_bosque fit(training_data)
6.2.4 Comparación RL, DT, RF
Excelente!!! Ahora conseguir una mejor performance predictiva!!
|>
fit_bosque predict(testing_data) |>
bind_cols(valor_real=testing_data$aml_index) |>
rmse(truth = valor_real,
estimate = .pred)
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 0.513
También podemos comprobar la importancia de las variables:
|>
fit_bosque extract_fit_engine() |>
vip()