Author

Dante Conti, Sergi Ramirez, (c) IDEAI

1 Descripción del problema

En este documento se estudia el algoritmo Random Forest aplicado a un problema de clasificación supervisada. El objetivo será predecir el nivel de renta del entorno de una vivienda de Barcelona (RENTA) a partir de características del inmueble, del edificio y de su localización.

La base de datos procede de anuncios de vivienda de Idealista y se ha enriquecido con información de renta media por hogar/persona a nivel de sección censal. A partir de la renta media por hogar se construye una variable categórica de tres niveles:

  • Baja: renta media por hogar inferior a 30.000 €.
  • Media: renta media por hogar entre 30.000 € y 50.000 €.
  • Alta: renta media por hogar superior a 50.000 €.

Por tanto, el problema se puede formular como:

\[Y = f(X_1, X_2, \ldots, X_p) + \varepsilon\]

donde:

  • \(Y\) es la variable objetivo RENTA.
  • \((X_1, X_2, \ldots, X_p)\) son las variables explicativas del inmueble.
  • \(f\) es una función no lineal aprendida mediante un conjunto de árboles de decisión.

2 Introducción teórica a Random Forest

2.1 De un árbol de decisión a un bosque aleatorio

Un árbol de decisión construye reglas del tipo:

Si DISTANCE_TO_CITY_CENTER < 2.5 y HASLIFT = Sí, entonces clasificar como RENTA = Alta.

El problema de un único árbol es que suele tener alta varianza: pequeñas variaciones en los datos de entrenamiento pueden generar árboles bastante diferentes. Esto puede provocar sobreajuste, es decir, un modelo que memoriza demasiado bien el conjunto de entrenamiento pero generaliza peor en datos nuevos.

Random Forest reduce este problema entrenando muchos árboles distintos y combinando sus predicciones. En clasificación, cada árbol vota una clase y el bosque devuelve la clase más votada.

\[\hat{y}_{RF} = \operatorname{mode}\{\hat{y}_1, \hat{y}_2, \ldots, \hat{y}_B\}\]

donde:

  • \(B\) es el número de árboles del bosque.
  • \(\hat{y}_b\) es la predicción del árbol \(b\).
  • \(\operatorname{mode}\) representa la clase más frecuente entre los votos.

2.2 Dos fuentes de aleatoriedad

Random Forest introduce aleatoriedad de dos formas:

  1. Bootstrap de observaciones: cada árbol se entrena con una muestra aleatoria con reemplazo del conjunto de entrenamiento.
  2. Selección aleatoria de variables en cada división: en cada nodo, el árbol no evalúa todas las variables, sino solamente un subconjunto aleatorio de tamaño mtry en R o max_features en Python.

Esto hace que los árboles sean menos correlacionados entre sí. Cuanto menor sea la correlación entre árboles, más beneficiosa será la agregación.

2.3 Ventajas principales

Random Forest es uno de los algoritmos clásicos más utilizados porque:

  • suele ofrecer buen rendimiento predictivo sin requerir demasiada configuración inicial;
  • captura relaciones no lineales;
  • captura interacciones entre variables;
  • funciona bien con variables numéricas y categóricas previamente tratadas;
  • proporciona medidas de importancia de variables;
  • permite extraer árboles individuales para interpretar reglas concretas;
  • puede complementarse con herramientas de interpretabilidad como SHAP values.

2.4 Limitaciones

También tiene limitaciones importantes:

  • es menos interpretable que un único árbol;
  • la importancia basada en impureza puede estar sesgada hacia variables con muchos posibles puntos de corte;
  • puede ser costoso computacionalmente si se usan muchos árboles;
  • no extrapola bien fuera del rango observado en los datos;
  • en clasificación desbalanceada puede favorecer la clase mayoritaria si no se ajustan pesos o métricas adecuadas.

3 Carga de datos

El siguiente bloque reproduce la carga de datos usada en el ejemplo de árboles de decisión. La versión final de la base de datos se carga directamente desde GitHub.

Mostrar código
path <- 'https://raw.githubusercontent.com/ramIA-lab/MLforEducation/refs/heads/main/material/trees_ensambleMethods/idealista18_BCN_conRenta.csv'
BCN <- read.csv2(path)
Mostrar código
library(dplyr)
library(tidyr)
library(ggplot2)
library(caret)
library(randomForest)
library(ranger)
library(vip)
library(pdp)
library(iml)
library(rpart.plot)
library(reshape2)

4 Preprocesamiento de los datos

El preprocesamiento tiene tres objetivos principales:

  1. eliminar columnas identificadoras, geométricas o que no se quieren usar como predictores;
  2. transformar variables binarias de 0/1 a etiquetas interpretables (No/Si);
  3. construir la variable objetivo RENTA a partir de la renta media por hogar.
Mostrar código
BCN <- BCN %>%
  select(-X, -PRICE, -LONGITUDE, -LATITUDE, -geometry, -CONSTRUCTIONYEAR,
         -ASSETID, -PERIOD, -CUSEC, -CSEC, -CMUN, -CPRO, -CCA, -CUDIS, -CLAU2,
         -NPRO, -NCA, -CNUT0, -CNUT1, -CNUT2, -CNUT3, -NMUN, -Shape_Leng,
         -Shape_Area, -geometry, -CUMUN, -CADASTRALQUALITYID) %>%
  mutate(
    across(
      .cols = starts_with(c("HAS", "IS")),
      .fns = ~ case_when(. == 0 ~ "No", . == 1 ~ "Si"),
      .names = "{.col}"),
    AMENITYID = case_when(
      AMENITYID == 1 ~ "SinMuebleSinCocina",
      AMENITYID == 2 ~ "CocinaSinMuebles",
      AMENITYID == 3 ~ "CocinaMuebles"),
    FLATLOCATIONID = case_when(
      FLATLOCATIONID == 1 ~ "exterior",
      FLATLOCATIONID == 2 ~ "interior",
      .default = "noInfo"),
    BUILTTYPEID_1 = case_when(
      BUILTTYPEID_1 == 0 ~ "noObraNueva",
      BUILTTYPEID_1 == 1 ~ "obraNueva"),
    BUILTTYPEID_2 = case_when(
      BUILTTYPEID_2 == 0 ~ "noRestaurar",
      BUILTTYPEID_2 == 1 ~ "Restaurar"),
    BUILTTYPEID_3 = case_when(
      BUILTTYPEID_3 == 0 ~ "noSegundaMano",
      BUILTTYPEID_3 == 1 ~ "SegundaMano"),
    FLOORCLEAN = replace_na(FLOORCLEAN, 0),
    CDIS = case_when(
      CDIS == 1 ~ "Ciutat-Vella",
      CDIS == 2 ~ "Eixample",
      CDIS == 3 ~ "Sants-Montjuic",
      CDIS == 4 ~ "Les Corts",
      CDIS == 5 ~ "Sarrià-Sant Gervasi",
      CDIS == 6 ~ "Gràcia",
      CDIS == 7 ~ "Horta-Guinardó",
      CDIS == 8 ~ "Nou Barris",
      CDIS == 9 ~ "Sant Andreu",
      CDIS == 10 ~ "Sant Martí"),
    RENTA = case_when(
      Renta.media.por.hogar < 30000 ~ "Baja",
      Renta.media.por.hogar >= 30000 & Renta.media.por.hogar <= 50000 ~ "Media",
      Renta.media.por.hogar > 50000 ~ "Alta"
    )
  ) %>%
  select(-Renta.media.por.hogar, -Renta.media.por.persona) %>%
  mutate(RENTA = as.factor(RENTA)) %>%
  na.omit()

5 Análisis descriptivo

Antes de entrenar el modelo conviene revisar la estructura de los datos, el tamaño de las clases y la distribución de algunas variables relevantes.

Mostrar código
dim(BCN)
[1] 23334    36
Mostrar código
str(BCN)
'data.frame':   23334 obs. of  36 variables:
 $ UNITPRICE                    : num  5232 4108 4056 6585 4200 ...
 $ CONSTRUCTEDAREA              : int  56 74 72 65 70 33 82 98 86 94 ...
 $ ROOMNUMBER                   : int  1 2 3 1 3 1 2 3 3 3 ...
 $ BATHNUMBER                   : int  1 1 1 2 1 1 2 1 1 1 ...
 $ HASTERRACE                   : chr  "No" "Si" "No" "Si" ...
 $ HASLIFT                      : chr  "Si" "Si" "Si" "No" ...
 $ HASAIRCONDITIONING           : chr  "No" "No" "No" "Si" ...
 $ AMENITYID                    : chr  "CocinaSinMuebles" "CocinaMuebles" "CocinaMuebles" "CocinaMuebles" ...
 $ HASPARKINGSPACE              : chr  "No" "No" "No" "No" ...
 $ ISPARKINGSPACEINCLUDEDINPRICE: chr  "No" "No" "No" "No" ...
 $ PARKINGSPACEPRICE            : num  1 1 1 1 1 1 1 1 1 1 ...
 $ HASNORTHORIENTATION          : chr  "No" "No" "No" "No" ...
 $ HASSOUTHORIENTATION          : chr  "No" "No" "No" "No" ...
 $ HASEASTORIENTATION           : chr  "No" "No" "No" "No" ...
 $ HASWESTORIENTATION           : chr  "No" "No" "No" "Si" ...
 $ HASBOXROOM                   : chr  "No" "No" "No" "No" ...
 $ HASWARDROBE                  : chr  "No" "No" "No" "No" ...
 $ HASSWIMMINGPOOL              : chr  "No" "No" "No" "No" ...
 $ HASDOORMAN                   : chr  "No" "No" "No" "No" ...
 $ HASGARDEN                    : chr  "No" "No" "No" "No" ...
 $ ISDUPLEX                     : chr  "No" "No" "No" "No" ...
 $ ISSTUDIO                     : chr  "No" "No" "No" "No" ...
 $ ISINTOPFLOOR                 : chr  "No" "No" "No" "No" ...
 $ FLOORCLEAN                   : int  0 4 3 2 0 0 0 6 4 4 ...
 $ FLATLOCATIONID               : chr  "exterior" "noInfo" "exterior" "exterior" ...
 $ CADCONSTRUCTIONYEAR          : int  2018 1959 1959 1936 1950 1900 2018 1936 1993 1993 ...
 $ CADMAXBUILDINGFLOOR          : int  5 5 5 6 3 6 5 11 5 5 ...
 $ CADDWELLINGCOUNT             : int  11 11 11 14 3 7 11 45 44 44 ...
 $ BUILTTYPEID_1                : chr  "noObraNueva" "noObraNueva" "noObraNueva" "noObraNueva" ...
 $ BUILTTYPEID_2                : chr  "noRestaurar" "noRestaurar" "noRestaurar" "noRestaurar" ...
 $ BUILTTYPEID_3                : chr  "SegundaMano" "SegundaMano" "SegundaMano" "SegundaMano" ...
 $ DISTANCE_TO_CITY_CENTER      : num  1.7 1.75 1.75 1.68 1.66 ...
 $ DISTANCE_TO_METRO            : num  0.291 0.336 0.332 0.3 0.252 ...
 $ DISTANCE_TO_DIAGONAL         : num  2.31 2.38 2.44 2.5 2.41 ...
 $ CDIS                         : chr  "Ciutat-Vella" "Ciutat-Vella" "Ciutat-Vella" "Ciutat-Vella" ...
 $ RENTA                        : Factor w/ 3 levels "Alta","Baja",..: 2 2 2 2 2 2 2 2 2 2 ...
Mostrar código
table(BCN$RENTA)

 Alta  Baja Media 
 2408  7749 13177 
Mostrar código
prop.table(table(BCN$RENTA))

     Alta      Baja     Media 
0.1031971 0.3320905 0.5647124 
Mostrar código
ggplot(BCN, aes(x = RENTA, fill = RENTA)) +
  geom_bar(show.legend = FALSE) +
  labs(
    title = "Distribución de la variable objetivo",
    x = "Nivel de renta",
    y = "Número de observaciones"
  ) +
  theme_minimal()

Mostrar código
BCN %>%
  select(where(is.numeric)) %>%
  summary()
   UNITPRICE    CONSTRUCTEDAREA    ROOMNUMBER       BATHNUMBER    
 Min.   :1210   Min.   : 21.00   Min.   : 0.000   Min.   : 0.000  
 1st Qu.:3286   1st Qu.: 65.00   1st Qu.: 2.000   1st Qu.: 1.000  
 Median :4058   Median : 82.00   Median : 3.000   Median : 1.000  
 Mean   :4099   Mean   : 94.91   Mean   : 2.846   Mean   : 1.519  
 3rd Qu.:4873   3rd Qu.:109.00   3rd Qu.: 3.000   3rd Qu.: 2.000  
 Max.   :6994   Max.   :790.00   Max.   :12.000   Max.   :10.000  
 PARKINGSPACEPRICE    FLOORCLEAN     CADCONSTRUCTIONYEAR CADMAXBUILDINGFLOOR
 Min.   :     1.0   Min.   :-1.000   Min.   :1588        Min.   : 0.000     
 1st Qu.:     1.0   1st Qu.: 1.000   1st Qu.:1927        1st Qu.: 5.000     
 Median :     1.0   Median : 2.000   Median :1961        Median : 6.000     
 Mean   :   606.5   Mean   : 2.617   Mean   :1948        Mean   : 6.724     
 3rd Qu.:     1.0   3rd Qu.: 4.000   3rd Qu.:1973        3rd Qu.: 8.000     
 Max.   :150001.0   Max.   :11.000   Max.   :2018        Max.   :34.000     
 CADDWELLINGCOUNT DISTANCE_TO_CITY_CENTER DISTANCE_TO_METRO  
 Min.   :  1.00   Min.   :0.08033         Min.   :0.0007362  
 1st Qu.: 11.00   1st Qu.:1.31146         1st Qu.:0.1541246  
 Median : 16.00   Median :2.28058         Median :0.2418701  
 Mean   : 27.19   Mean   :2.60691         Mean   :0.2616103  
 3rd Qu.: 29.00   3rd Qu.:3.63917         3rd Qu.:0.3389505  
 Max.   :505.00   Max.   :8.61172         Max.   :4.0996294  
 DISTANCE_TO_DIAGONAL
 Min.   :0.005767    
 1st Qu.:0.921281    
 Median :1.661283    
 Mean   :1.748727    
 3rd Qu.:2.319904    
 Max.   :6.811647    

6 Separación entrenamiento/test

Separamos los datos en dos conjuntos:

  • train: datos usados para entrenar el modelo;
  • test: datos reservados para evaluar la capacidad de generalización.

La partición se hace de forma estratificada para conservar aproximadamente la proporción de clases en ambos subconjuntos.

Mostrar código
set.seed(1994)

index <- caret::createDataPartition(BCN$RENTA, p = 0.8, list = FALSE)
rtrain <- BCN %>% slice(index)
rtest  <- BCN %>% slice(-index)

prop.table(table(rtrain$RENTA))

     Alta      Baja     Media 
0.1032192 0.3321013 0.5646794 
Mostrar código
prop.table(table(rtest$RENTA))

     Alta      Baja     Media 
0.1031083 0.3320472 0.5648446 
Mostrar código
import pandas as pd
import numpy as np

# Eliminamos filas con posibles valores perdidos
pyBCN = pyBCN.dropna().copy()

# Variable objetivo
y = pyBCN["RENTA"].astype(str)
X = pyBCN.drop(columns=["RENTA"])

# One-hot encoding para variables categóricas.
# A diferencia de LabelEncoder, one-hot evita imponer un orden artificial entre categorías.
X = pd.get_dummies(X, drop_first=False)

print(X.shape)
(23334, 68)
Mostrar código
print(y.value_counts(normalize=True))
RENTA
Media    0.564712
Baja     0.332091
Alta     0.103197
Name: proportion, dtype: float64
Mostrar código
from sklearn.model_selection import train_test_split

pyX_train, pyX_test, pyy_train, pyy_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=1994,
    stratify=y
)

print(pyX_train.shape, pyX_test.shape)
(18667, 68) (4667, 68)

7 Modelo Random Forest

7.1 Entrenamiento básico

Un primer modelo se puede entrenar con parámetros razonables. En R se usa randomForest; en Python se usa RandomForestClassifier de scikit-learn.

Mostrar código
set.seed(1994)

rf_basico <- randomForest(
  RENTA ~ .,
  data = rtrain,
  ntree = 500,
  importance = TRUE
)

rf_basico

Call:
 randomForest(formula = RENTA ~ ., data = rtrain, ntree = 500,      importance = TRUE) 
               Type of random forest: classification
                     Number of trees: 500
No. of variables tried at each split: 5

        OOB estimate of  error rate: 8.38%
Confusion matrix:
      Alta Baja Media class.error
Alta  1550    2   375  0.19564089
Baja     3 5536   661  0.10709677
Media  183  340 10019  0.04961108
Mostrar código
from sklearn.ensemble import RandomForestClassifier

rf_basico_py = RandomForestClassifier(
    n_estimators=500,
    random_state=1994,
    n_jobs=-1,
    class_weight="balanced"
)

rf_basico_py.fit(pyX_train, pyy_train)
RandomForestClassifier(class_weight='balanced', n_estimators=500, n_jobs=-1,
                       random_state=1994)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Mostrar código
rf_basico_py
RandomForestClassifier(class_weight='balanced', n_estimators=500, n_jobs=-1,
                       random_state=1994)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

7.2 Predicción y matriz de confusión

La evaluación principal se realiza sobre el conjunto de test. La matriz de confusión permite identificar qué clases se predicen correctamente y qué clases se confunden entre sí.

Mostrar código
pred_train_rf <- predict(rf_basico, rtrain)
pred_test_rf  <- predict(rf_basico, rtest)

caret::confusionMatrix(pred_train_rf, rtrain$RENTA)
Confusion Matrix and Statistics

          Reference
Prediction  Alta  Baja Media
     Alta   1927     0     0
     Baja      0  6200     0
     Media     0     0 10542

Overall Statistics
                                     
               Accuracy : 1          
                 95% CI : (0.9998, 1)
    No Information Rate : 0.5647     
    P-Value [Acc > NIR] : < 2.2e-16  
                                     
                  Kappa : 1          
                                     
 Mcnemar's Test P-Value : NA         

Statistics by Class:

                     Class: Alta Class: Baja Class: Media
Sensitivity               1.0000      1.0000       1.0000
Specificity               1.0000      1.0000       1.0000
Pos Pred Value            1.0000      1.0000       1.0000
Neg Pred Value            1.0000      1.0000       1.0000
Prevalence                0.1032      0.3321       0.5647
Detection Rate            0.1032      0.3321       0.5647
Detection Prevalence      0.1032      0.3321       0.5647
Balanced Accuracy         1.0000      1.0000       1.0000
Mostrar código
caret::confusionMatrix(pred_test_rf, rtest$RENTA)
Confusion Matrix and Statistics

          Reference
Prediction Alta Baja Media
     Alta   377    0    35
     Baja     1 1398    86
     Media  103  151  2514

Overall Statistics
                                          
               Accuracy : 0.9194          
                 95% CI : (0.9112, 0.9271)
    No Information Rate : 0.5648          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.8535          
                                          
 Mcnemar's Test P-Value : 2.542e-11       

Statistics by Class:

                     Class: Alta Class: Baja Class: Media
Sensitivity              0.78378      0.9025       0.9541
Specificity              0.99163      0.9721       0.8749
Pos Pred Value           0.91505      0.9414       0.9082
Neg Pred Value           0.97555      0.9525       0.9362
Prevalence               0.10311      0.3320       0.5648
Detection Rate           0.08081      0.2997       0.5389
Detection Prevalence     0.08832      0.3183       0.5934
Balanced Accuracy        0.88771      0.9373       0.9145
Mostrar código
CM <- caret::confusionMatrix(pred_test_rf, rtest$RENTA)
CM_df <- as.data.frame(CM$table)

plot_cm <- ggplot(CM_df, aes(x = Reference, y = Prediction, fill = Freq)) +
  geom_tile() +
  geom_text(aes(label = Freq), size = 5) +
  scale_fill_gradient(low = "white", high = "#0077C8") +
  labs(
    title = "Matriz de confusión - Random Forest",
    x = "Clase real",
    y = "Clase predicha"
  ) +
  theme_minimal()

plot_cm

Mostrar código
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

py_pred_train = rf_basico_py.predict(pyX_train)
py_pred_test = rf_basico_py.predict(pyX_test)

print("Accuracy train:", round(accuracy_score(pyy_train, py_pred_train), 4))
Accuracy train: 1.0
Mostrar código
print("Accuracy test:", round(accuracy_score(pyy_test, py_pred_test), 4))
Accuracy test: 0.9096
Mostrar código
print("\nClassification report test:\n")

Classification report test:
Mostrar código
print(classification_report(pyy_test, py_pred_test))
              precision    recall  f1-score   support

        Alta       0.88      0.81      0.84       482
        Baja       0.94      0.88      0.91      1550
       Media       0.90      0.94      0.92      2635

    accuracy                           0.91      4667
   macro avg       0.91      0.88      0.89      4667
weighted avg       0.91      0.91      0.91      4667
Mostrar código
import matplotlib.pyplot as plt
from sklearn.metrics import ConfusionMatrixDisplay

cm = confusion_matrix(pyy_test, py_pred_test, labels=rf_basico_py.classes_)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=rf_basico_py.classes_)
disp.plot(values_format="d")
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay object at 0x000001FDFFF79090>
Mostrar código
plt.title("Matriz de confusión - Random Forest")
plt.show()

8 Validación cruzada

La validación cruzada permite estimar de forma más estable el rendimiento del modelo. En lugar de depender de una única partición train/test, se divide el entrenamiento en varios folds.

Mostrar código
trControl <- trainControl(
  method = "cv",
  number = 5,
  classProbs = TRUE,
  summaryFunction = multiClassSummary,
  savePredictions = "final"
)

set.seed(1994)
rf_cv <- train(
  RENTA ~ .,
  data = rtrain,
  method = "rf",
  metric = "Accuracy",
  trControl = trControl,
  ntree = 300
)

rf_cv
Random Forest 

18669 samples
   35 predictor
    3 classes: 'Alta', 'Baja', 'Media' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 14935, 14936, 14936, 14935, 14934 
Resampling results across tuning parameters:

  mtry  logLoss    AUC        prAUC      Accuracy   Kappa      Mean_F1  
   2    0.3837728  0.9573015  0.9003123  0.8591246  0.7374143  0.8073345
  23    0.1874353  0.9888396  0.8540817  0.9311686  0.8761303  0.9148925
  45    0.1792408  0.9893154  0.7308102  0.9360437  0.8851472  0.9206214
  Mean_Sensitivity  Mean_Specificity  Mean_Pos_Pred_Value  Mean_Neg_Pred_Value
  0.7713629         0.9034198         0.8703969            0.9221782          
  0.9086395         0.9559249         0.9219028            0.9600771          
  0.9153453         0.9595549         0.9265099            0.9625254          
  Mean_Precision  Mean_Recall  Mean_Detection_Rate  Mean_Balanced_Accuracy
  0.8703969       0.7713629    0.2863749            0.8373914             
  0.9219028       0.9086395    0.3103895            0.9322822             
  0.9265099       0.9153453    0.3120146            0.9374501             

Accuracy was used to select the optimal model using the largest value.
The final value used for the model was mtry = 45.
Mostrar código
ggplot(rf_cv) +
  labs(title = "Validación cruzada de Random Forest") +
  theme_minimal()

Mostrar código
from sklearn.model_selection import StratifiedKFold, cross_val_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1994)

rf_cv_py = RandomForestClassifier(
    n_estimators=300,
    random_state=1994,
    n_jobs=-1,
    class_weight="balanced"
)

scores = cross_val_score(
    rf_cv_py,
    pyX_train,
    pyy_train,
    cv=cv,
    scoring="accuracy",
    n_jobs=-1
)

print("Accuracy por fold:", np.round(scores, 4))
Accuracy por fold: [0.9006 0.9138 0.9215 0.9202 0.9113]
Mostrar código
print("Accuracy medio:", round(scores.mean(), 4))
Accuracy medio: 0.9135
Mostrar código
print("Desviación típica:", round(scores.std(), 4))
Desviación típica: 0.0075

9 Ajuste de hiperparámetros

Los hiperparámetros más importantes en Random Forest son:

  • ntree / n_estimators: número de árboles.
  • mtry / max_features: número de variables candidatas en cada división.
  • min.node.size / min_samples_leaf: tamaño mínimo de los nodos terminales.
  • max.depth / max_depth: profundidad máxima de cada árbol.

Un bosque con muchos árboles reduce la varianza, pero aumenta el coste computacional. El parámetro más crítico suele ser mtry o max_features, porque controla la diversidad entre árboles.

Mostrar código
set.seed(1994)

rf_grid <- expand.grid(
  mtry = c(3, 5, 8, 12)
)

rf_tuned <- train(
  RENTA ~ .,
  data = rtrain,
  method = "rf",
  metric = "Accuracy",
  trControl = trControl,
  tuneGrid = rf_grid,
  ntree = 500
)

rf_tuned
Random Forest 

18669 samples
   35 predictor
    3 classes: 'Alta', 'Baja', 'Media' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 14935, 14936, 14936, 14935, 14934 
Resampling results across tuning parameters:

  mtry  logLoss    AUC        prAUC      Accuracy   Kappa      Mean_F1  
   3    0.3033418  0.9732015  0.9343610  0.8942629  0.8065904  0.8643059
   5    0.2528195  0.9811333  0.9483121  0.9146172  0.8453745  0.8945864
   8    0.2245533  0.9847436  0.9490133  0.9205095  0.8564924  0.9025094
  12    0.2078128  0.9867971  0.9342018  0.9254907  0.8657207  0.9093323
  Mean_Sensitivity  Mean_Specificity  Mean_Pos_Pred_Value  Mean_Neg_Pred_Value
  0.8408619         0.9293819         0.8952869            0.9404042          
  0.8817297         0.9441248         0.9095907            0.9511028          
  0.8929585         0.9485128         0.9133261            0.9540793          
  0.9021088         0.9519407         0.9174107            0.9568204          
  Mean_Precision  Mean_Recall  Mean_Detection_Rate  Mean_Balanced_Accuracy
  0.8952869       0.8408619    0.2980876            0.8851219             
  0.9095907       0.8817297    0.3048724            0.9129272             
  0.9133261       0.8929585    0.3068365            0.9207357             
  0.9174107       0.9021088    0.3084969            0.9270248             

Accuracy was used to select the optimal model using the largest value.
The final value used for the model was mtry = 12.
Mostrar código
rf_tuned$bestTune
Mostrar código
ggplot(rf_tuned) +
  labs(title = "Tuning del parámetro mtry") +
  theme_minimal()

Mostrar código
from sklearn.model_selection import GridSearchCV

param_grid = {
    "n_estimators": [300, 500],
    "max_features": ["sqrt", "log2", None],
    "min_samples_leaf": [1, 5, 10],
    "max_depth": [None, 8, 15]
}

rf_grid_py = GridSearchCV(
    estimator=RandomForestClassifier(
        random_state=1994,
        n_jobs=-1,
        class_weight="balanced"
    ),
    param_grid=param_grid,
    cv=cv,
    scoring="accuracy",
    n_jobs=-1
)

rf_grid_py.fit(pyX_train, pyy_train)
GridSearchCV(cv=StratifiedKFold(n_splits=5, random_state=1994, shuffle=True),
             estimator=RandomForestClassifier(class_weight='balanced',
                                              n_jobs=-1, random_state=1994),
             n_jobs=-1,
             param_grid={'max_depth': [None, 8, 15],
                         'max_features': ['sqrt', 'log2', None],
                         'min_samples_leaf': [1, 5, 10],
                         'n_estimators': [300, 500]},
             scoring='accuracy')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Mostrar código
print("Mejores parámetros:", rf_grid_py.best_params_)
Mejores parámetros: {'max_depth': None, 'max_features': None, 'min_samples_leaf': 1, 'n_estimators': 500}
Mostrar código
print("Mejor accuracy CV:", round(rf_grid_py.best_score_, 4))
Mejor accuracy CV: 0.9361

10 Importancia de variables

La importancia de variables responde a la pregunta:

¿Qué variables han contribuido más al poder predictivo del modelo?

En Random Forest hay varias formas de medir importancia:

  1. Mean Decrease Gini / impureza: mide cuánto reduce una variable la impureza de los nodos en promedio.
  2. Permutation importance: mide cuánto empeora el modelo si se permuta aleatoriamente una variable.
  3. SHAP values: distribuye la predicción de cada observación entre las variables, proporcionando interpretabilidad local y global.

10.1 Matriz de variables importantes en R

En randomForest, al activar importance = TRUE, se obtiene una matriz de importancia. Para clasificación multiclase, pueden aparecer columnas específicas por clase y medidas globales como MeanDecreaseAccuracy y MeanDecreaseGini.

Mostrar código
imp_matrix <- as.data.frame(rf_basico$importance)
imp_matrix$Variable <- rownames(imp_matrix)
head(imp_matrix[order(-imp_matrix$MeanDecreaseGini), ], 15)
Mostrar código
imp_top <- imp_matrix %>%
  arrange(desc(MeanDecreaseGini)) %>%
  slice(1:20) %>%
  mutate(Variable = reorder(Variable, MeanDecreaseGini))

ggplot(imp_top, aes(x = Variable, y = MeanDecreaseGini)) +
  geom_col(fill = "#0077C8") +
  coord_flip() +
  labs(
    title = "Top 20 variables importantes - Mean Decrease Gini",
    x = "Variable",
    y = "Importancia"
  ) +
  theme_minimal()

Mostrar código
varImpPlot(rf_basico, n.var = 20, main = "Importancia de variables - Random Forest")

Mostrar código
importances = rf_basico_py.feature_importances_
importance_df = pd.DataFrame({
    "Variable": pyX_train.columns,
    "Importance": importances
}).sort_values("Importance", ascending=False)

print(importance_df.head(20))
                    Variable  Importance
11      DISTANCE_TO_DIAGONAL    0.145674
9    DISTANCE_TO_CITY_CENTER    0.101951
67  CDIS_Sarrià-Sant Gervasi    0.078778
1            CONSTRUCTEDAREA    0.069157
0                  UNITPRICE    0.068790
58         CDIS_Ciutat-Vella    0.066791
6        CADCONSTRUCTIONYEAR    0.041500
10         DISTANCE_TO_METRO    0.040757
59             CDIS_Eixample    0.039429
8           CADDWELLINGCOUNT    0.035114
3                 BATHNUMBER    0.026456
7        CADMAXBUILDINGFLOOR    0.026411
2                 ROOMNUMBER    0.019426
5                 FLOORCLEAN    0.016596
14                HASLIFT_No    0.016070
60               CDIS_Gràcia    0.014980
15                HASLIFT_Si    0.014639
62            CDIS_Les Corts    0.014250
66       CDIS_Sants-Montjuic    0.010680
65           CDIS_Sant Martí    0.008724
Mostrar código
top_imp = importance_df.head(20).sort_values("Importance")

plt.figure(figsize=(8, 8))
plt.barh(top_imp["Variable"], top_imp["Importance"])
plt.xlabel("Importancia")
plt.title("Top 20 variables importantes - Random Forest")
plt.tight_layout()
plt.show()

10.2 Importancia por permutación

La importancia por permutación es más intuitiva que la importancia por impureza. El procedimiento es:

  1. calcular el rendimiento del modelo original;
  2. permutar aleatoriamente una variable;
  3. volver a calcular el rendimiento;
  4. medir cuánto cae la métrica.

Si al permutar una variable el rendimiento cae mucho, esa variable era importante.

Mostrar código
set.seed(1994)

rf_ranger <- ranger(
  RENTA ~ .,
  data = rtrain,
  num.trees = 500,
  importance = "permutation",
  probability = TRUE,
  seed = 1994
)

vip(rf_ranger, num_features = 20) +
  labs(title = "Importancia por permutación - ranger") +
  theme_minimal()

Mostrar código
from sklearn.inspection import permutation_importance

perm = permutation_importance(
    rf_basico_py,
    pyX_test,
    pyy_test,
    n_repeats=10,
    random_state=1994,
    n_jobs=-1,
    scoring="accuracy"
)

perm_df = pd.DataFrame({
    "Variable": pyX_test.columns,
    "ImportanceMean": perm.importances_mean,
    "ImportanceStd": perm.importances_std
}).sort_values("ImportanceMean", ascending=False)

print(perm_df.head(20))
                    Variable  ImportanceMean  ImportanceStd
11      DISTANCE_TO_DIAGONAL        0.142233       0.004354
58         CDIS_Ciutat-Vella        0.070431       0.002933
9    DISTANCE_TO_CITY_CENTER        0.039940       0.002536
67  CDIS_Sarrià-Sant Gervasi        0.028391       0.001856
59             CDIS_Eixample        0.022370       0.003592
0                  UNITPRICE        0.020934       0.002006
1            CONSTRUCTEDAREA        0.018513       0.001304
10         DISTANCE_TO_METRO        0.011228       0.001167
65           CDIS_Sant Martí        0.008207       0.000678
6        CADCONSTRUCTIONYEAR        0.007114       0.000908
8           CADDWELLINGCOUNT        0.006557       0.000930
7        CADMAXBUILDINGFLOOR        0.005014       0.000895
62            CDIS_Les Corts        0.005014       0.000451
3                 BATHNUMBER        0.004414       0.001412
60               CDIS_Gràcia        0.004393       0.000600
66       CDIS_Sants-Montjuic        0.004178       0.000699
64          CDIS_Sant Andreu        0.002571       0.000583
14                HASLIFT_No        0.001821       0.000940
15                HASLIFT_Si        0.001500       0.000924
2                 ROOMNUMBER        0.001414       0.001089
Mostrar código
perm_top = perm_df.head(20).sort_values("ImportanceMean")

plt.figure(figsize=(8, 8))
plt.barh(perm_top["Variable"], perm_top["ImportanceMean"])
plt.xlabel("Caída media de accuracy")
plt.title("Top 20 variables - Importancia por permutación")
plt.tight_layout()
plt.show()

11 Extracción de uno de los árboles del bosque

Aunque Random Forest es un modelo de conjunto, internamente está formado por muchos árboles individuales. Extraer uno de ellos ayuda a entender la lógica local de algunas reglas, aunque no debe confundirse con la explicación completa del bosque.

11.1 Interpretación conceptual

Un árbol individual del bosque:

  • se ha entrenado con una muestra bootstrap;
  • ha usado subconjuntos aleatorios de variables en cada división;
  • representa solo una parte del comportamiento total del modelo;
  • puede ser visualizado como un árbol de decisión clásico.

Por tanto, extraer un árbol sirve para explicar el mecanismo interno, no para sustituir la interpretación global del Random Forest.

Mostrar código
# Extraemos el árbol número 1 del Random Forest.
# getTree devuelve la estructura interna del árbol.
arbol_1 <- getTree(rf_basico, k = 1, labelVar = TRUE)
head(arbol_1, 20)
Mostrar código
# Para visualizar un árbol de forma más clara, entrenamos un árbol pequeño auxiliar.
# Este árbol NO es el Random Forest completo, sino una representación interpretativa sencilla.
library(rpart)

set.seed(1994)
arbol_auxiliar <- rpart(
  RENTA ~ .,
  data = rtrain,
  control = rpart.control(maxdepth = 3, minbucket = 50, cp = 0.001)
)

rpart.plot(arbol_auxiliar, main = "Árbol auxiliar para interpretación")

Mostrar código
from sklearn.tree import plot_tree, export_text

# Extraemos el primer árbol del bosque
one_tree = rf_basico_py.estimators_[0]

print("Profundidad del árbol:", one_tree.get_depth())
Profundidad del árbol: 32
Mostrar código
print("Número de hojas:", one_tree.get_n_leaves())
Número de hojas: 2473
Mostrar código
# Exportamos solamente los primeros niveles para que sea legible
rules = export_text(
    one_tree,
    feature_names=list(pyX_train.columns),
    max_depth=3
)

print(rules)
|--- HASDOORMAN_Si <= 0.50
|   |--- CDIS_Sant Andreu <= 0.50
|   |   |--- CDIS_Ciutat-Vella <= 0.50
|   |   |   |--- DISTANCE_TO_DIAGONAL <= 2.27
|   |   |   |   |--- truncated branch of depth 29
|   |   |   |--- DISTANCE_TO_DIAGONAL >  2.27
|   |   |   |   |--- truncated branch of depth 27
|   |   |--- CDIS_Ciutat-Vella >  0.50
|   |   |   |--- ROOMNUMBER <= 4.50
|   |   |   |   |--- truncated branch of depth 19
|   |   |   |--- ROOMNUMBER >  4.50
|   |   |   |   |--- truncated branch of depth 6
|   |--- CDIS_Sant Andreu >  0.50
|   |   |--- CADCONSTRUCTIONYEAR <= 1958.50
|   |   |   |--- HASSOUTHORIENTATION_Si <= 0.50
|   |   |   |   |--- truncated branch of depth 4
|   |   |   |--- HASSOUTHORIENTATION_Si >  0.50
|   |   |   |   |--- truncated branch of depth 5
|   |   |--- CADCONSTRUCTIONYEAR >  1958.50
|   |   |   |--- DISTANCE_TO_DIAGONAL <= 3.88
|   |   |   |   |--- truncated branch of depth 17
|   |   |   |--- DISTANCE_TO_DIAGONAL >  3.88
|   |   |   |   |--- truncated branch of depth 15
|--- HASDOORMAN_Si >  0.50
|   |--- CONSTRUCTEDAREA <= 125.50
|   |   |--- DISTANCE_TO_CITY_CENTER <= 1.99
|   |   |   |--- CADCONSTRUCTIONYEAR <= 1978.50
|   |   |   |   |--- truncated branch of depth 8
|   |   |   |--- CADCONSTRUCTIONYEAR >  1978.50
|   |   |   |   |--- truncated branch of depth 12
|   |   |--- DISTANCE_TO_CITY_CENTER >  1.99
|   |   |   |--- DISTANCE_TO_CITY_CENTER <= 5.10
|   |   |   |   |--- truncated branch of depth 19
|   |   |   |--- DISTANCE_TO_CITY_CENTER >  5.10
|   |   |   |   |--- truncated branch of depth 5
|   |--- CONSTRUCTEDAREA >  125.50
|   |   |--- CDIS_Ciutat-Vella <= 0.50
|   |   |   |--- FLOORCLEAN <= 6.50
|   |   |   |   |--- truncated branch of depth 16
|   |   |   |--- FLOORCLEAN >  6.50
|   |   |   |   |--- truncated branch of depth 12
|   |   |--- CDIS_Ciutat-Vella >  0.50
|   |   |   |--- DISTANCE_TO_CITY_CENTER <= 0.46
|   |   |   |   |--- truncated branch of depth 3
|   |   |   |--- DISTANCE_TO_CITY_CENTER >  0.46
|   |   |   |   |--- truncated branch of depth 3
Mostrar código
plt.figure(figsize=(18, 10))
plot_tree(
    one_tree,
    feature_names=list(pyX_train.columns),
    class_names=list(rf_basico_py.classes_),
    max_depth=3,
    filled=True,
    rounded=True,
    fontsize=8
)
plt.title("Primer árbol extraído del Random Forest. Se muestran solo 3 niveles")
plt.show()

12 Interpretación mediante Partial Dependence Plots

Los Partial Dependence Plots muestran cómo cambia la predicción media del modelo cuando modificamos una variable, manteniendo el resto de variables según su distribución observada.

Sirven para responder preguntas como:

¿Cómo cambia la probabilidad de renta alta cuando aumenta la distancia al centro?

Mostrar código
# Seleccionamos una variable numérica importante.
# Si DISTANCE_TO_CITY_CENTER está disponible, suele ser una buena candidata.
partial_rf <- partial(
  rf_basico,
  pred.var = "DISTANCE_TO_CITY_CENTER",
  train = rtrain,
  which.class = "Alta",
  prob = TRUE
)

autoplot(partial_rf) +
  labs(
    title = "Partial Dependence Plot",
    subtitle = "Efecto medio de DISTANCE_TO_CITY_CENTER sobre la probabilidad de RENTA Alta",
    x = "Distancia al centro",
    y = "Probabilidad media predicha"
  ) +
  theme_minimal()

Mostrar código
from sklearn.inspection import PartialDependenceDisplay

# Elegimos una variable si existe tras el one-hot encoding
candidate_var = "DISTANCE_TO_CITY_CENTER"

if candidate_var in pyX_train.columns:
    PartialDependenceDisplay.from_estimator(
        rf_basico_py,
        pyX_train,
        features=[candidate_var],
        target="Alta"
    )
    plt.title("Partial Dependence Plot - DISTANCE_TO_CITY_CENTER")
    plt.show()
else:
    print(f"La variable {candidate_var} no está disponible en pyX_train.")

13 Interpretación mediante SHAP values

13.1 ¿Qué son los SHAP values?

Los SHAP values son una técnica de interpretabilidad basada en teoría de juegos. La idea es repartir la predicción de un modelo entre las variables explicativas.

Para una observación concreta, la predicción puede expresarse de forma aproximada como:

\[\hat{f}(x) = \phi_0 + \phi_1 + \phi_2 + \cdots + \phi_p\]

donde:

  • \(\phi_0\) es el valor base del modelo;
  • \(\phi_j\) es la contribución de la variable \(j\);
  • si \(\phi_j > 0\), esa variable empuja la predicción hacia arriba para una clase concreta;
  • si \(\phi_j < 0\) , esa variable empuja la predicción hacia abajo para esa clase concreta.

En clasificación multiclase, los SHAP values se calculan por clase. Por ejemplo, podemos interpretar qué variables empujan la predicción hacia RENTA = Alta.

13.2 SHAP global

La interpretación global resume qué variables tienen mayor impacto medio sobre las predicciones del modelo.

Mostrar código
library(iml)
library(future)

future::plan(future::sequential)
options(future.globals.maxSize = 2 * 1024^3)

# Para SHAP en R usamos una muestra pequeña.
# Así evitamos que iml copie objetos enormes internamente.
set.seed(1994)

X_train_r <- rtrain %>% select(-RENTA)
y_train_r <- rtrain$RENTA

idx_shap_r <- sample(
  seq_len(nrow(X_train_r)),
  size = min(300, nrow(X_train_r))
)

X_shap_r <- X_train_r[idx_shap_r, , drop = FALSE]
y_shap_r <- y_train_r[idx_shap_r]

predict_fun_alta <- function(model, newdata) {
  predict(model, newdata, type = "prob")[, "Alta"]
}

predictor_alta <- Predictor$new(
  model = rf_basico,
  data = X_shap_r,
  y = as.numeric(y_shap_r == "Alta"),
  predict.function = predict_fun_alta,
  type = "prob"
)
Mostrar código
# FeatureImp no es SHAP estrictamente, pero da una lectura global complementaria
# basada en pérdida por permutación para la clase Alta.
set.seed(1994)

effect_global <- FeatureImp$new(
  predictor_alta,
  loss = "ce",
  n.repetitions = 3
)

plot(effect_global) +
  labs(title = "Importancia global por permutación para la clase Alta")

Mostrar código
# SHAP local para una observación concreta.
# Elegimos la primera observación del conjunto de test.
set.seed(1994)
shap_obs_1 <- Shapley$new(
  predictor_alta,
  x.interest = rtest[1, setdiff(names(rtest), "RENTA"), drop = FALSE],
  sample.size = 100
)

plot(shap_obs_1) +
  labs(title = "SHAP local para una vivienda - Clase Alta")

Mostrar código
# Si no tienes shap instalado, ejecuta previamente:
# pip install shap

import shap

# Para acelerar el cálculo, usamos una muestra del conjunto de test.
X_shap = pyX_test.sample(min(300, len(pyX_test)), random_state=1994)

explainer = shap.TreeExplainer(rf_basico_py)
shap_values = explainer.shap_values(X_shap)

# En clasificación multiclase, shap_values suele ser una lista:
# una matriz de SHAP values por cada clase.
print(rf_basico_py.classes_)
Mostrar código
# Seleccionamos la clase Alta
class_index = list(rf_basico_py.classes_).index("Alta")

shap.summary_plot(
    shap_values[class_index],
    X_shap,
    plot_type="dot",
    show=True
)
Mostrar código
shap.summary_plot(
    shap_values[class_index],
    X_shap,
    plot_type="bar",
    show=True
)
Mostrar código
# Interpretación local de una observación concreta.
obs_id = 0

shap.force_plot(
    explainer.expected_value[class_index],
    shap_values[class_index][obs_id, :],
    X_shap.iloc[obs_id, :],
    matplotlib=True
)

13.3 Cómo leer un gráfico SHAP summary plot

En el gráfico SHAP de puntos:

  • cada punto representa una observación;
  • el eje X representa el impacto SHAP sobre la predicción;
  • valores positivos empujan hacia la clase analizada;
  • valores negativos alejan de la clase analizada;
  • el color representa el valor de la variable: valores altos frente a valores bajos;
  • las variables aparecen ordenadas por importancia global media.

Ejemplo de interpretación para la clase Alta:

Si DISTANCE_TO_CITY_CENTER aparece con puntos rojos a la izquierda, significaría que valores altos de distancia al centro reducen la probabilidad de renta alta. Si aparecen a la derecha, significaría que la aumentan.

14 Comparación entre árbol individual y Random Forest

Criterio Árbol de decisión Random Forest
Interpretabilidad Muy alta Media
Rendimiento predictivo Medio Alto
Riesgo de sobreajuste Alto Menor
Estabilidad Baja Alta
Coste computacional Bajo Medio/alto
Importancia de variables Sí, más robusta
Extracción de reglas Directa Parcial, árbol por árbol

La idea fundamental es que Random Forest sacrifica parte de la interpretabilidad directa de un único árbol a cambio de mayor estabilidad y mejor rendimiento predictivo.

15 Buenas prácticas

Al usar Random Forest conviene:

  1. reservar siempre un conjunto de test;
  2. usar validación cruzada para comparar configuraciones;
  3. revisar matriz de confusión, no solo accuracy;
  4. analizar si hay clases desbalanceadas;
  5. comparar importancia por impureza y por permutación;
  6. usar SHAP para interpretación local y global;
  7. no interpretar un solo árbol como si fuera todo el bosque;
  8. documentar los hiperparámetros finales.

16 Conclusiones

En este documento se ha construido un flujo completo de modelización con Random Forest en R y Python. Se han cubierto las fases principales:

  • carga y preprocesamiento de datos;
  • análisis descriptivo inicial;
  • partición train/test;
  • entrenamiento de Random Forest;
  • evaluación mediante matriz de confusión y métricas de clasificación;
  • validación cruzada;
  • ajuste de hiperparámetros;
  • matriz y gráficos de importancia de variables;
  • extracción de un árbol individual del bosque;
  • interpretación mediante Partial Dependence Plots;
  • interpretación local y global mediante SHAP values.

Random Forest es especialmente útil cuando se busca un modelo robusto y potente, pero sigue siendo necesario acompañarlo de técnicas de interpretación para justificar sus predicciones y entender qué variables están guiando las decisiones del modelo.

17 Sesión

Mostrar código
sessionInfo()
R version 4.5.1 (2025-06-13 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26200)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=Spanish_Spain.utf8  LC_CTYPE=Spanish_Spain.utf8   
[3] LC_MONETARY=Spanish_Spain.utf8 LC_NUMERIC=C                  
[5] LC_TIME=Spanish_Spain.utf8    

time zone: Europe/Madrid
tzcode source: internal

attached base packages:
 [1] parallel  splines   stats4    grid      tools     compiler  stats    
 [8] graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] pkgconfig_2.0.3      ModelMetrics_1.2.2.2 xfun_0.56           
 [4] checkmate_2.3.4      prodlim_2025.04.28   nlme_3.1-168        
 [7] Rcpp_1.1.1           class_7.3-23         backports_1.5.0     
[10] png_0.1-8            evaluate_1.0.5       R6_2.6.1            
[13] lava_1.8.2           ipred_0.9-15         htmltools_0.5.9     
[16] pillar_1.11.1        tibble_3.3.1         gtable_0.3.6        
[19] stringi_1.8.7        lubridate_1.9.5      codetools_0.2-20    
[22] parallelly_1.46.1    glue_1.8.0           recipes_1.3.2       
[25] gower_1.0.2          foreach_1.5.2        listenv_0.10.1      
[28] jsonlite_2.0.0       Matrix_1.7-4         hardhat_1.4.3       
[31] vctrs_0.7.1          stringr_1.6.0        proxy_0.4-29        
[34] future.apply_1.20.2  rstudioapi_0.18.0    otel_0.2.0          
[37] generics_0.1.4       rmarkdown_2.30       cli_3.6.6           
[40] MASS_7.3-65          iterators_1.0.14     scales_1.4.0        
[43] globals_0.19.1       e1071_1.7-17         nnet_7.3-20         
[46] Metrics_0.1.4        MLmetrics_1.1.3      purrr_1.2.2         
[49] withr_3.0.2          RColorBrewer_1.1-3   plyr_1.8.9          
[52] reticulate_1.45.0    htmlwidgets_1.6.4    labeling_0.4.3      
[55] knitr_1.51           data.table_1.18.2.1  yaml_2.3.12         
[58] rlang_1.2.0          magrittr_2.0.4       ROCR_1.0-12         
[61] survival_3.8-6       lifecycle_1.0.5      timechange_0.4.0    
[64] digest_0.6.39        pROC_1.19.0.1        fastmap_1.2.0       
[67] S7_0.2.1             farver_2.1.2         timeDate_4052.112   
[70] tidyselect_1.2.1     future_1.70.0        reshape2_1.4.5      
[73] rpart.plot_3.1.4     rpart_4.1.24         iml_0.11.4          
[76] pdp_0.8.3            vip_0.4.6            ranger_0.18.0       
[79] randomForest_4.7-1.2 caret_7.0-1          lattice_0.22-9      
[82] ggplot2_4.0.3        tidyr_1.3.2          dplyr_1.2.1         

Aquesta web està creada por Dante Conti y Sergi Ramírez, (c) 2026