Los modelos de Random Forest son una poderosa técnica en el campo del aprendizaje automático (machine learning). Se basan en la construcción de múltiples árboles de decisión y combinan sus predicciones para obtener un resultado final más robusto y preciso. Este enfoque de conjunto permite a los Random Forest superar muchas de las limitaciones asociadas con un solo árbol de decisión, como el sobreajuste y la sensibilidad a pequeñas variaciones en los datos de entrenamiento. Gracias a su versatilidad y capacidad para manejar una amplia variedad de problemas de clasificación y regresión, Random Forest se han convertido en una herramienta indispensable para los científicos de datos.
En el ecosistema Python, una de las implementaciones de Random Forest más utilizadas es la disponible en Scikit-learn (RandomForestRegressor, RandomForestClassifier). Aunque esta implementación satisface con éxito la mayoría de los casos de uso, tiene dos limitaciones: no acepta datos ausentes (missing) y carece de la capacidad de manejar de forma nativa variables categóricas. Esto lleva a la necesidad de aplicar estrategias de preprocesamiento (como one hot encoding, target encoding, etc.) para superar estas limitaciones.
XGBoost y LightGBM, conocidos sobre todo por su eficaz aplicación de modelos gradient boosting, también permiten crear modelos Random Forest. Estas implementaciones tienen varias ventajas sobre la de Scikit-learn:
Manejo nativo de datos ausentes: tienen la capacidad de manejar eficazmente los valores que ausentes. Evitando así la necesidad de eliminarlos o imputarlos.
Manejo nativo de variables categóricas: A diferencia de la implementación de Scikit-learn, pueden manejar variables categóricas de forma nativa.
Regularización integrada: incorporan una función de regularización que ayuda a controlar el sobreajuste.
Velocidad de entrenamiento: gracias a las optimizaciones del algoritmo, pueden entrenar modelos Random Forest más rápido que la implementación Scikit-learn.
Aceleración GPU: XGBoost y LightGBM tienen una versión compatible con GPU que puede acelerar significativamente el entrenamiento y la inferencia del modelo.
Es importante señalar que, a pesar de estas ventajas, la elección entre la implementación dependerá del conjunto de datos específico, los objetivos del proyecto y las limitaciones de recursos computacionales. Además, es fundamental tener en cuenta las posibles diferencias en los resultados debidas a las características específicas de cada implementación.
✎ Note
El principal beneficio de utilizar la librería XGBoost o LightGBM para entrenar modelos random forest frente a la implementación nativa de scikit-learn es la velocidad junto con su capacidad para manejar variables categóricas.1.4
de Scikit-learn, la implementación de Random Forest ha añadido la capacidad de manejar valores ausentes.
⚠ Warning
La implementación de XGBoost tiene algunas diferencias con respecto a la implementación de scikit-learn que pueden llevar a resultados diferentes.Las librerías utilizadas en este documento son:
# Tratamiento de datos
# ==============================================================================
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing
# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
# Modelado
# ==============================================================================
import xgboost
from xgboost import XGBRFRegressor
import lightgbm
from lightgbm import LGBMRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import TargetEncoder
from sklearn.compose import ColumnTransformer
import sklearn
import optuna
import time
# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=FutureWarning)
optuna.logging.set_verbosity(optuna.logging.WARNING)
print(f"XGBoost version: {xgboost.__version__}")
print(f"LightGBM version: {lightgbm.__version__}")
print(f"Optuna version: {optuna.__version__}")
print(f"Scikit-learn version: {sklearn.__version__}")
El conjunto de datos utilizado en este documento contiene información sobre la vivienda en California. Es accesible a través de la librería sklearn
y se puede cargar con la función fetch_california_housing
.
El conjunto de datos original (20.640 observaciones y 9 variables) ha sido modificado para incorporar nuevas variables:
Dirección de la vivienda (county y postcode): esta información se ha extraído utilizando el paquete geopy
y la API de Nomatim de OpenStreetMap.
Ciudad más cercana y distancia a la misma: los datos tienen licencia CC0: Public Domain y se pueden obtener en kaggle.
# Descarga de datos california housing extended
# ==============================================================================
datos = pd.read_csv(
'https://raw.githubusercontent.com/JoaquinAmatRodrigo/'
'Estadistica-machine-learning-python/master/data/california_housing_extended.csv'
)
datos = datos.drop(columns=['Latitude', 'Longitude'])
datos['postcode'] = "postcode_" + datos['postcode'].astype(str)
datos['postcode'] = datos['postcode'].replace('postcode_nan', np.nan)
print("Dimensiones de los datos:", datos.shape)
datos.head()
# Porcentaje de valores nuelos por columna
# ==============================================================================
datos.isnull().mean()
# Categorias que aparecen menos de 10 veces se agrupan en una categoria "otro"
# ==============================================================================
limit = 10
for col in ['county', 'ciudad_mas_cercana']:
counts = datos[col].value_counts()
under_limit = counts[counts < limit]
index_under_limit = under_limit.index.tolist()
datos[col] = datos[col].replace(index_under_limit, 'otro')
# Numero de categorias por variable
# ==============================================================================
print("Numero de categorias por variable")
print("---------------------------------")
print("county:", datos['county'].nunique())
print("postcode:", datos['postcode'].nunique())
print("ciudad_mas_cercana:", datos['ciudad_mas_cercana'].nunique())
# Las variables de tipo categorico se almacenan como dtype category
# ==============================================================================
datos['county'] = datos['county'].astype('category')
datos['postcode'] = datos['postcode'].astype('category')
datos['ciudad_mas_cercana'] = datos['ciudad_mas_cercana'].astype('category')
datos.dtypes
# División de los datos en train, validation y test
# ==============================================================================
target = 'MedHouseVal'
X_train, X_test, y_train, y_test = train_test_split(
datos.drop(columns=target),
datos[target],
train_size = 0.8,
random_state = 1234,
shuffle = True
)
X_train, X_val, y_train, y_val = train_test_split(
X_train,
y_train,
train_size = 0.8,
random_state = 1234,
shuffle = True
)
print("Observaciones en train:", X_train.shape)
print("Observaciones en validation:", X_val.shape)
print("Observaciones en test:", X_test.shape)
A continuación, se compararán los modelos Random Forest de las librerías scikit-learn
, XGBoost
y LightGBM
en términos de velocidad de entrenamiento, velocidad de inferencia y capacidad predictiva.
Para todos los modelos, se realiza una búsqueda de hiperparámetros utilizado la librería Optuna
. La selección de hiperparámetros se realiza con los datos de validación. En el caso de scikit-learn, dado que no tiene una implementación nativa para manejar variables categóricas, se utiliza un preprocesamiento con OneHotEncoder
para codificar las variables categóricas.
RandomForestRegressor de scikit-learn no acepta variables categóricas. Para poder utilizar este modelo, es necesario codificar las variables categóricas en formato numérico.
✎ Note
En este caso se realiza una codificación de las variables categóricas conTargetEncoder
para evitar aumentar la dimensionalidad de los datos. Pero también existen alternativas para codificar variables categóricas en scikit-learn como OneHotEncoder
o OrdinalEncoder
.
# Target encoding de las variables categóricas
# ==============================================================================
col_categoricas = datos.select_dtypes(exclude=np.number).columns.to_list()
encoder = ColumnTransformer(
transformers=[
('target', TargetEncoder(target_type="continuous"), col_categoricas)
],
remainder='passthrough'
).set_output(transform='pandas')
encoder.fit(X_train, y_train)
X_train_encoded = encoder.transform(X_train)
X_val_encoded = encoder.transform(X_val)
X_test_encoded = encoder.transform(X_test)
print("Observaciones en train encoded:", X_train_encoded.shape)
print("Observaciones en validation encoded:", X_val_encoded.shape)
print("Observaciones en test encoded:", X_test_encoded.shape)
# Búsqueda bayesiana de hiperparámetros con optuna
# ==============================================================================
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 1000, step=100),
'max_depth': trial.suggest_int('max_depth', 3, 30),
'min_samples_split': trial.suggest_int('min_samples_split', 2, 100),
'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 100),
'max_features': trial.suggest_float('max_features', 0.3, 1),
'ccp_alpha': trial.suggest_float('ccp_alpha', 0.0, 1),
'min_impurity_decrease': trial.suggest_float('min_impurity_decrease', 0.0, 1),
}
model = RandomForestRegressor(
n_jobs = -1,
random_state = 4576688,
criterion = 'squared_error',
**params
)
model.fit(X_train_encoded, y_train)
predictions = model.predict(X_val_encoded)
score = mean_squared_error(y_val, predictions, squared=False)
return score
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=25, show_progress_bar=True, timeout=60*5)
print('Mejores hiperparámetros:', study.best_params)
print('Mejor score:', study.best_value)
# Random Forest scikit-learn con los mejores hiperparámetros encontrados
# ==============================================================================
rf_sklearn = RandomForestRegressor(
n_jobs = -1,
random_state = 4576688,
criterion = 'squared_error',
**study.best_params
)
# Entrenamiento del modelo
start = time.time()
rf_sklearn.fit(
X = pd.concat([X_train_encoded, X_val_encoded]),
y = pd.concat([y_train, y_val])
)
end = time.time()
tiempo_entrenamiento_sklearn = end - start
# Predicciones test
start = time.time()
predicciones = rf_sklearn.predict(X=X_test_encoded)
end = time.time()
tiempo_prediccion_sklearn = end - start
# Error de test del modelo
rmse_rf_sklearn = mean_squared_error(
y_true = y_test,
y_pred = predicciones,
squared = False
)
print(f"Tiempo entrenamiento: {tiempo_entrenamiento_sklearn:.2f} segundos")
print(f"Tiempo predicción: {tiempo_prediccion_sklearn:.2f} segundos")
print(f"RMSE: {rmse_rf_sklearn:.2f}")
# Búsqueda bayesiana de hiperparámetros con optuna
# ==============================================================================
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 1000, step=100),
'max_depth': trial.suggest_int('max_depth', 3, 30),
'reg_lambda': trial.suggest_float('reg_lambda', 1e-5, 1e+3, log=True),
'reg_alpha': trial.suggest_float('reg_alpha', 1e-5, 1e+3, log=True),
'gamma': trial.suggest_float('gamma', 1e-5, 1e+3, log=True),
'subsample': trial.suggest_float('subsample', 0.5, 1),
'colsample_bynode': trial.suggest_float('colsample_bynode', 0.5, 1),
}
model = XGBRFRegressor(
tree_method = 'hist',
grow_policy = 'depthwise',
learning_rate = 1.0,
n_jobs = -1,
random_state = 4576,
enable_categorical = True,
**params
)
model.fit(X_train, y_train)
predictions = model.predict(X_val)
score = mean_squared_error(y_val, predictions, squared=False)
return score
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=25, show_progress_bar=True, timeout=60*10)
print('Mejores hiperparámetros:', study.best_params)
print('Mejor score:', study.best_value)
# Random Forest XGBoost con los mejores hiperparámetros encontrados
# ==============================================================================
rf_xgboost = XGBRFRegressor(
tree_method = 'hist',
grow_policy = 'depthwise',
learning_rate = 1.0,
n_jobs = -1,
random_state = 4576,
enable_categorical = True,
**study.best_params
)
# Entrenamiento del modelo
start = time.time()
rf_xgboost.fit(
X = pd.concat([X_train, X_val]),
y = pd.concat([y_train, y_val])
)
end = time.time()
tiempo_entrenamiento_xgboost = end - start
# Predicciones test
start = time.time()
predicciones = rf_xgboost.predict(X=X_test)
end = time.time()
tiempo_prediccion_xgboost = end - start
# Error de test del modelo
rmse_rf_xgboost = mean_squared_error(
y_true = y_test,
y_pred = predicciones,
squared = False
)
print(f"Tiempo entrenamiento: {tiempo_entrenamiento_xgboost:.2f} segundos")
print(f"Tiempo predicción: {tiempo_prediccion_xgboost:.2f} segundos")
print(f"RMSE: {rmse_rf_xgboost:.2f}")
# Búsqueda bayesiana de hiperparámetros con optuna
# ==============================================================================
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 1000, step=100),
'num_leaves': trial.suggest_int('num_leaves', 5, 256),
'reg_lambda': trial.suggest_float('reg_lambda', 1e-5, 1e+3, log=True),
'reg_alpha': trial.suggest_float('reg_alpha', 1e-5, 1e+3, log=True),
'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 100),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.3, 1),
'colsample_bynode': trial.suggest_float('colsample_bynode', 0.3, 1),
'subsample': trial.suggest_float('subsample', 0.4, 1),
}
model = LGBMRegressor(
boosting_type = 'rf',
learning_rate = 1.0,
subsample_freq = 1,
n_jobs = -1,
random_state = 4576688,
verbose = -1,
**params
)
model.fit(X_train, y_train, categorical_feature='auto')
predictions = model.predict(X_val)
score = mean_squared_error(y_val, predictions, squared=False)
return score
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=25, show_progress_bar=True, timeout=60*5)
print('Mejores hiperparámetros:', study.best_params)
print('Mejor score:', study.best_value)
# Random Forest LightGBM con los mejores hiperparámetros encontrados
# ==============================================================================
rf_lgbm = LGBMRegressor(
boosting_type = 'rf',
learning_rate = 1.0,
subsample_freq = 1,
n_jobs = -1,
random_state = 4576688,
verbose = -1,
**study.best_params
)
# Entrenamiento del modelo
start = time.time()
rf_lgbm.fit(X_train, y_train, categorical_feature='auto')
end = time.time()
tiempo_entrenamiento_lgbm = end - start
# Predicciones test
start = time.time()
predicciones = rf_lgbm.predict(X=X_test)
end = time.time()
tiempo_prediccion_lgbm = end - start
# Error de test del modelo
rmse_rf_lgbm = mean_squared_error(
y_true = y_test,
y_pred = predicciones,
squared = False
)
print(f"Tiempo entrenamiento: {tiempo_entrenamiento_lgbm:.2f} segundos")
print(f"Tiempo predicción: {tiempo_prediccion_lgbm:.2f} segundos")
print(f"RMSE: {rmse_rf_lgbm:.2f}")
# Comparativa de modelos
# ==============================================================================
resultados = pd.DataFrame({
'rmse': [rmse_rf_sklearn, rmse_rf_xgboost, rmse_rf_lgbm],
'tiempo_entrenamiento': [
tiempo_entrenamiento_sklearn,
tiempo_entrenamiento_xgboost,
tiempo_entrenamiento_lgbm
],
'tiempo_prediccion': [
tiempo_prediccion_sklearn,
tiempo_prediccion_xgboost,
tiempo_prediccion_lgbm
]
},
index = [
'Random Forest sklearn',
'Random Forest XGBoost',
'Random Forest LightGBM'
]
)
resultados.style.highlight_min(axis=0, color='green').format(precision=2)
✎ Note
Esta tabla muestra los resultados en cuento a capacidad predictiva, velocidad de entrenamiento y velocidad de inferencia de los modelos Random Forest de las librerías scikit-learn, XGBoost y LightGBM, cada una con la mejor configuración de hiperparámetros encontrada con Optuna. Por lo tanto, los tiempos de entrenamiento y predicción no son comparables entre sí. Para comparar los tiempos de entrenamiento se debería utilizar los mismos hiparparámetros para cada modelo.import session_info
session_info.show(html=False)
¿Cómo citar este documento?
Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!
Random forest con valores nulos y variables categoricas por Joaquín Amat Rodrigo, disponible bajo una licencia Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) en https://www.cienciadedatos.net/documentos/py54-random-forest-valores-nulos-variable-categoricas.html
¿Te ha gustado el artículo? Tu ayuda es importante
Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊
Este documento creado por Joaquín Amat Rodrigo tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.
Se permite:
Compartir: copiar y redistribuir el material en cualquier medio o formato.
Adaptar: remezclar, transformar y crear a partir del material.
Bajo los siguientes términos:
Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.
NoComercial: No puedes utilizar el material para fines comerciales.
CompartirIgual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.