La regresión es una técnica de aprendizaje supervisado que permite predecir valores numéricos continuos a partir de datos. A diferencia de la clasificación que asigna categorías, la regresión estima magnitudes específicas como precios, temperaturas, edades o cualquier variable cuantitativa.
Los modelos de regresión son fundamentales en ciencia de datos y se utilizan ampliamente para:
En este capítulo exploraremos varios tipos de modelos de regresión, cada uno con sus propias características y casos de uso:
La regresión lineal simple modela la relación entre una variable independiente (X) y una variable dependiente (y) mediante una línea recta. La ecuación que describe este modelo es:
y = β₀ + β₁X + ε
Este modelo es intuitivo, fácil de interpretar y sirve como base para entender modelos más complejos.
La regularización ayuda a controlar el sobreajuste, especialmente útil cuando hay muchas características:
El SVR extiende los principios de las máquinas de vectores de soporte al dominio de la regresión. Busca encontrar un hiperplano que incluya la mayor cantidad de puntos dentro de un margen ε definido, mientras penaliza los puntos fuera de este margen. Es particularmente efectivo en espacios de alta dimensionalidad y cuando la relación entre las variables no es estrictamente lineal.
La regresión KNN predice el valor de un punto basándose en el promedio de los valores de sus k vecinos más cercanos. Es un modelo no paramétrico que no hace suposiciones sobre la distribución de los datos, haciéndolo flexible pero computacionalmente costoso para grandes conjuntos de datos. Es especialmente útil cuando la relación entre variables es muy compleja o desconocida.
Los árboles de decisión para regresión dividen recursivamente el espacio de características en regiones, asignando a cada región un valor de predicción que minimiza el error. Son muy interpretables y pueden capturar relaciones no lineales, pero tienden a sobreajustar si no se controla su complejidad (por ejemplo, limitando la profundidad como hicimos en nuestro análisis).
Implementar un modelo de regresión efectivo requiere seguir un proceso estructurado que va desde la exploración de datos hasta la interpretación y despliegue del modelo. En esta sección, desarrollaremos un flujo de trabajo completo para problemas de regresión siguiendo estas etapas fundamentales:
Utilizaremos un conjunto de datos de automóviles para predecir precios en base a diversas características, aplicando las técnicas más relevantes de regresión con scikit-learn y otras herramientas de Python para ciencia de datos.
Utilizaremos un dataset de automóviles que contiene información sobre características técnicas y precios. Nuestro objetivo será construir un modelo que prediga el precio de un automóvil basado en sus características.
Primero importamos las bibliotecas principales para análisis de datos y visualización:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
print(pd.__version__)
2.2.2
El conjunto de datos que utilizaremos contiene tres tipos de características:
Los valores faltantes están identificados con un "?" en el dataset original.
# Definición de las columnas del dataset
columnas = ['symboling', 'normalized-losses', 'make',
'fuel-type', 'aspiration', 'num-of-doors',
'body-style', 'drive-wheels', 'engine-location',
'wheel-base', 'length', 'width', 'height',
'curb-weight', 'engine-type', 'num-of-cylinders',
'engine-size', 'fuel-system', 'bore', 'stroke',
'compression-ratio', 'horsepower', 'peak-rpm',
'city-mpg', 'highway-mpg', 'price']
# URL de los datos
url_data = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/auto_imports.csv'
# Carga de datos con manejo de valores faltantes
auto_df = pd.read_csv(url_data,
header=None,
names=columnas,
na_values='?')
# Ver 5 registros aleatorios
auto_df.sample(5)
# Tamaño del dataset
print(f'El tamaño del conjunto de datos es {auto_df.shape} \n')
# Información del dataset
auto_df.info()
El tamaño del conjunto de datos es (205, 26)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 205 entries, 0 to 204
Data columns (total 26 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 symboling 205 non-null int64
1 normalized-losses 164 non-null float64
2 make 205 non-null object
3 fuel-type 205 non-null object
4 aspiration 205 non-null object
5 num-of-doors 203 non-null object
6 body-style 205 non-null object
7 drive-wheels 205 non-null object
8 engine-location 205 non-null object
9 wheel-base 205 non-null float64
10 length 205 non-null float64
11 width 205 non-null float64
12 height 205 non-null float64
13 curb-weight 205 non-null int64
14 engine-type 205 non-null object
15 num-of-cylinders 205 non-null object
16 engine-size 205 non-null int64
17 fuel-system 205 non-null object
18 bore 201 non-null float64
19 stroke 201 non-null float64
20 compression-ratio 205 non-null float64
21 horsepower 203 non-null float64
22 peak-rpm 203 non-null float64
23 city-mpg 205 non-null int64
24 highway-mpg 205 non-null int64
25 price 201 non-null float64
dtypes: float64(11), int64(5), object(10)
memory usage: 41.8+ KB
Es fundamental identificar los valores faltantes en nuestro dataset para decidir la mejor estrategia de manejo. Durante la carga, reemplazamos los símbolos "?" con NaN para facilitar su detección:
# Verificación de valores faltantes por columna
auto_df.isna().sum()
symboling 0
normalized-losses 41
make 0
fuel-type 0
aspiration 0
num-of-doors 2
body-style 0
drive-wheels 0
engine-location 0
wheel-base 0
length 0
width 0
height 0
curb-weight 0
engine-type 0
num-of-cylinders 0
engine-size 0
fuel-system 0
bore 4
stroke 4
compression-ratio 0
horsepower 2
peak-rpm 2
city-mpg 0
highway-mpg 0
price 4
dtype: int64
Podemos ver que algunas columnas tienen valores faltantes, siendo "normalized-losses" la que más presenta (41 valores). Durante el feature engineering, implementaremos estrategias para manejar estos valores.
Vamos a convertir las variables categóricas al tipo de dato adecuado, distinguiendo entre categóricas nominales y ordinales:
# Corregir las variables categóricas
cols_categoricas = ["make", "fuel-type", "aspiration", "num-of-doors",
"body-style", "drive-wheels", "engine-location",
"engine-type", "num-of-cylinders", "fuel-system"]
auto_df[cols_categoricas] = auto_df[cols_categoricas].astype("category")
# Corregir variables categóricas ordinales
auto_df["num-of-doors"] = pd.Categorical(auto_df["num-of-doors"],
categories=["two","four"],
ordered=True)
auto_df["num-of-cylinders"] = pd.Categorical(auto_df["num-of-cylinders"],
categories=["two", "three", "four",
"five", "six", "eight",
"twelve"],
ordered=True)
# Verificamos la información del dataset después de las conversiones
auto_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 205 entries, 0 to 204
Data columns (total 26 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 symboling 205 non-null int64
1 normalized-losses 164 non-null float64
2 make 205 non-null category
3 fuel-type 205 non-null category
4 aspiration 205 non-null category
5 num-of-doors 203 non-null category
6 body-style 205 non-null category
7 drive-wheels 205 non-null category
8 engine-location 205 non-null category
9 wheel-base 205 non-null float64
10 length 205 non-null float64
11 width 205 non-null float64
12 height 205 non-null float64
13 curb-weight 205 non-null int64
14 engine-type 205 non-null category
15 num-of-cylinders 205 non-null category
16 engine-size 205 non-null int64
17 fuel-system 205 non-null category
18 bore 201 non-null float64
19 stroke 201 non-null float64
20 compression-ratio 205 non-null float64
21 horsepower 203 non-null float64
22 peak-rpm 203 non-null float64
23 city-mpg 205 non-null int64
24 highway-mpg 205 non-null int64
25 price 201 non-null float64
dtypes: category(10), float64(11), int64(5)
memory usage: 28.6 KB
Ahora obtenemos algunos estadísticos descriptivos básicos del conjunto de datos:
# Descripción estadística de las variables numéricas
auto_df.describe()
Las estadísticas descriptivas nos dan una idea de las distribuciones y rangos de nuestras variables numéricas. Por ejemplo, vemos que el precio (nuestra variable objetivo) varía desde 5,118 hasta 45,400 dólares, con un promedio de 13,207 dólares.
En el análisis univariable examinamos cada variable individualmente para entender su distribución y detectar posibles problemas. Para las variables categóricas, verificamos su frecuencia:
cols_cate_problemas = ["make", "fuel-system", "num-of-cylinders"]
for col in cols_cate_problemas:
print(auto_df[col].value_counts())
print("")
make
toyota 32
nissan 18
mazda 17
honda 13
mitsubishi 13
subaru 12
volkswagen 12
peugot 11
volvo 11
audi 10
bmw 8
dodge 8
mercedes-benz 8
plymouth 7
saab 6
porsche 5
jaguar 3
chevrolet 3
alfa-romeo 3
isuzu 3
mercury 1
renault 1
Name: count, dtype: int64
fuel-system
mpfi 94
2bbl 66
idi 20
1bbl 11
spdi 3
4bbl 3
mfi 1
spfi 1
Name: count, dtype: int64
num-of-cylinders
four 94
six 40
five 11
eight 5
two 4
three 1
twelve 1
Name: count, dtype: int64
Algunos valores categóricos solo están presentes en un solo registro, lo que podría causar problemas en los encoders:
Este desequilibrio tendrá que ser considerado durante el feature engineering para evitar problemas de generalización.
Ahora analizamos la relación entre las variables numéricas y nuestra variable objetivo (precio):
# Listado de variables numéricas excepto price
cols_numericas = (auto_df
.drop(columns=["price"])
.select_dtypes(include=np.number)
.columns.tolist())
print(cols_numericas)
print(len(cols_numericas))
['symboling', 'normalized-losses', 'wheel-base', 'length', 'width', 'height', 'curb-weight', 'engine-size', 'bore', 'stroke', 'compression-ratio', 'horsepower', 'peak-rpm', 'city-mpg', 'highway-mpg']
15
# Crear scatter plots: 5 filas y 3 columnas
fig, axes = plt.subplots(5, 3, figsize=(15, 15))
axes = axes.flatten()
for i, col in enumerate(cols_numericas):
sns.regplot(data=auto_df,
x=col, y="price",
ax=axes[i])
axes[i].set_title(col)
plt.tight_layout()
plt.show()
Estos gráficos nos permiten identificar visualmente relaciones entre las variables predictoras y el precio. Por ejemplo, podemos observar correlaciones positivas fuertes entre el precio y variables como 'engine-size', 'curb-weight' y 'width', mientras que hay correlaciones negativas con 'city-mpg' y 'highway-mpg'.
# Matriz de correlación
automobile_corr = auto_df.corr(numeric_only=True)
fig, ax = plt.subplots(figsize=(12, 10))
sns.heatmap(automobile_corr, annot=True, fmt=".2f");
La matriz de correlación confirma nuestras observaciones de los scatter plots y proporciona una medida cuantitativa de la fuerza de cada relación. Por ejemplo:
# Boxplots para variables categóricas vs precio
fig, axes = plt.subplots(5, 2, figsize=(15, 15))
axes = axes.flatten()
for i, col in enumerate(cols_categoricas):
sns.boxplot(data=auto_df,
x="price", y=col,
ax=axes[i])
axes[i].set_title(col)
plt.tight_layout()
plt.show()
Hay variables categóricas que permiten distinguir claramente entre grupos de valores de precio. Por ejemplo:
El feature engineering es fundamental para preparar nuestros datos para el modelado. En este caso, implementaremos estrategias para manejar valores faltantes y transformar variables categóricas.
Utilizaremos scikit-learn para crear pipelines de preprocesamiento que aplicarán diferentes transformaciones según el tipo de variable:
# Librerías para el preprocesamiento de datos
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OrdinalEncoder
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
# Definición de columnas por tipo
cols_numericas = ['symboling','normalized-losses',
'wheel-base','length', 'width',
'height', 'curb-weight',
'engine-size', 'bore', 'stroke',
'compression-ratio', 'horsepower',
'peak-rpm','city-mpg',
'highway-mpg']
cols_categoricas = ["make", "fuel-type", "aspiration",
"body-style", "drive-wheels",
"engine-location", "engine-type",
"fuel-system"]
cols_categoricas_ord = ["num-of-doors", "num-of-cylinders"]
A continuación, definimos tres pipelines diferentes:
# Creación de pipelines de transformación
# OneHotEncoder para variables categóricas nominales
# OrdinalEncoder para variables categóricas ordinales
numeric_pipe = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median'))])
categorical_pipe = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))])
categorical_ord_pipe = Pipeline(steps=[
('ordenc', OrdinalEncoder(handle_unknown='use_encoded_value',
unknown_value=np.nan)),
('imputer', SimpleImputer(strategy='most_frequent'))])
# Combinación de pipelines en un ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
('numericas', numeric_pipe, cols_numericas),
('categoricas', categorical_pipe, cols_categoricas),
('categoricas ordinales', categorical_ord_pipe, cols_categoricas_ord)
])
preprocessor
Este enfoque nos proporciona varias ventajas:
En esta etapa, implementaremos y evaluaremos diversos modelos de regresión para predecir el precio de los automóviles. Comenzaremos dividiendo nuestros datos en conjuntos de entrenamiento y prueba, y luego probaremos diferentes algoritmos para identificar el más efectivo.
Para una selección eficiente de modelos, seguiremos esta metodología:
from sklearn.model_selection import train_test_split
X_features = auto_df.drop('price', axis='columns')
y_target = auto_df['price']
X_train, X_test, y_train, y_test = train_test_split(X_features,
y_target,
test_size=0.2,
random_state=42)
print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
(160, 25) (160,)
(41, 25) (41,)
Definimos funciones para entrenar y evaluar modelos de forma consistente:
from sklearn.metrics import mean_absolute_error
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import Ridge
from sklearn.linear_model import ElasticNet
from sklearn.svm import SVR
from sklearn.neighbors import KNeighborsRegressor
from sklearn.tree import DecisionTreeRegressor
import warnings
warnings.filterwarnings("ignore")
# Diccionario para almacenar resultados
result_dict = {}
# Funciones de ayuda para entrenar y evaluar modelos
def entrenar_modelo(modelo,
preprocessor,
x_data,
y_data,
test_frac=0.2,
):
"""
Función para entrenar y evaluar un modelo
Args:
modelo: modelo de ML
preprocessor: preprocesador de datos
x_data: datos de entrada
y_data: datos de salida
test_frac: fracción de datos para el conjunto de prueba
Returns:
dict: diccionario con los puntajes de entrenamiento y prueba
"""
# Dividir el dataset en entrenamiento y prueba
x_train, x_test, y_train, y_test = train_test_split(x_data, y_data,
random_state=42,
test_size=test_frac)
# Crear el pipeline con el preprocesador y el modelo
regressor_pipe = Pipeline(steps=[("preprocessor", preprocessor),
("model", modelo)])
# Entrenar el pipeline de regresión
model = regressor_pipe.fit(x_train, y_train)
y_pred_train = model.predict(x_train)
# Predecir con el pipeline de regresión
y_pred = model.predict(x_test)
train_score = mean_absolute_error(y_train, y_pred_train)
test_score = mean_absolute_error(y_test, y_pred)
print(f"Entrenamiento_score : {train_score}")
print(f"Prueba_score : {test_score}")
return {
'Entrenamiento_score': train_score,
'Prueba_score': test_score
}
# Función para comparar los resultados de los modelos
def compare_results():
for key in result_dict:
print('Regresión: ', key)
print('Entrenamiento score', result_dict[key]['Entrenamiento_score'])
print('Prueba score', result_dict[key]['Prueba_score'])
print()
Ahora implementaremos y evaluaremos varios modelos de regresión, comenzando con un modelo base simple:
# Modelo Dummy (línea base)
result_dict['Dummy Regressor'] = entrenar_modelo(DummyRegressor(strategy='median'), preprocessor, X_train, y_train)
Entrenamiento_score : 4968.09375
Prueba_score : 5037.7439024390245
# Regresión lineal
result_dict['Linear Regressor'] = entrenar_modelo(LinearRegression(), preprocessor, X_train, y_train)
Entrenamiento_score : 876.91871739725
Prueba_score : 2140.5372525530713
# ElasticNet
result_dict['Elasticnet'] = entrenar_modelo(ElasticNet(alpha=1, l1_ratio=0.5, max_iter=100000, warm_start=True),
preprocessor,
X_train,
y_train)
Entrenamiento_score : 1823.047871927852
Prueba_score : 2310.247878703888
# SVR (Support Vector Regression)
result_dict['SVR'] = entrenar_modelo(SVR(kernel='linear', epsilon=0.05, C=0.3),
preprocessor,
X_train,
y_train)
Entrenamiento_score : 1973.1475811870937
Prueba_score : 2428.6209834285455
# KNN (K-Nearest Neighbors)
result_dict['KNN'] = entrenar_modelo(KNeighborsRegressor(n_neighbors=10),
preprocessor,
X_train,
y_train)
Entrenamiento_score : 2319.37421875
Prueba_score : 2659.3841463414633
# Decision Tree
result_dict['Decision Tree'] = entrenar_modelo(DecisionTreeRegressor(max_depth=2),
preprocessor,
X_train,
y_train)
Entrenamiento_score : 2095.9670005416547
Prueba_score : 2509.0853658536585
# Comparar todos los resultados
compare_results()
Regresión: Dummy Regressor
Entrenamiento score 4968.09375
Prueba score 5037.7439024390245
Regresión: Linear Regressor
Entrenamiento score 876.91871739725
Prueba score 2140.5372525530713
Regresión: Elasticnet
Entrenamiento score 1823.047871927852
Prueba score 2310.247878703888
Regresión: SVR
Entrenamiento score 1973.1475811870937
Prueba score 2428.6209834285455
Regresión: KNN
Entrenamiento score 2319.37421875
Prueba score 2659.3841463414633
Regresión: Decision Tree
Entrenamiento score 2095.9670005416547
Prueba score 2509.0853658536585
# Visualización comparativa de resultados
# Crear un diccionario solo con los resultados de prueba de cada modelo
nombre_modelos = result_dict.keys()
resultados_train = {} # crear diccionario vacío
resultados_test = {} # crear diccionario vacío
for nombre in nombre_modelos:
resultados_train[nombre] = result_dict[nombre]['Entrenamiento_score']
resultados_test[nombre] = result_dict[nombre]['Prueba_score']
df_comparacion = pd.DataFrame([resultados_train, resultados_test],
index=['train', 'test'])
# Plot the bar chart
fig, ax = plt.subplots(figsize=(10, 4))
df_comparacion.T.plot(kind='bar', ax=ax)
# Adjust the layout
ax.set_ylabel('MAE score')
ax.set_title('Comparación de Modelos [MAE] ')
# Set the x-tick labels inside the bars and rotate by 90 degrees
ax.set_xticks(range(len(df_comparacion.columns)))
ax.set_xticklabels([])
# Draw the x-tick labels inside the bars rotated by 45 degrees
for i, label in enumerate(df_comparacion.columns):
bar_center = (df_comparacion.loc['train', label] +
df_comparacion.loc['test', label]) / 2
ax.text(i, bar_center, label, ha='center',
va='center_baseline', rotation=45)
# Plotear línea en el resultado de DummyRegressor
ax.axhline(df_comparacion['Dummy Regressor']['test'],
color='red',
linestyle='--',
alpha=0.8)
plt.tight_layout()
plt.show()
La validación cruzada (cross-validation) es una técnica fundamental para estimar de manera más robusta el rendimiento de generalización de nuestros modelos. En esta etapa, evaluaremos los modelos más prometedores utilizando validación cruzada de K-folds.
from sklearn import model_selection
# Lista para almacenar cada uno los modelos seleccionados para el cross validation
models = []
# Almacenando los modelos como una tupla (nombre, modelo)
models.append(('Elastic_net', ElasticNet(alpha=1, l1_ratio=0.5, max_iter=100000, warm_start=True)))
models.append(('Kneighbors', KNeighborsRegressor(n_neighbors=10)))
models.append(('Decision_tree', DecisionTreeRegressor(max_depth=2)))
models.append(('SVR', SVR(kernel='linear', epsilon=0.05, C=0.3)))
# Semilla para obtener los mismos resultados de pruebas
seed = 2
results = []
names = []
scoring = 'neg_mean_absolute_error'
for name, model in models:
# Kfold cross validation
kfold = model_selection.KFold(n_splits=10)
model_pipe = Pipeline(steps=[("preprocessor", preprocessor),
("model", model)])
# X train, y train
cv_results = model_selection.cross_val_score(model_pipe, X_train, y_train, cv=kfold, scoring=scoring)
# la métrica neg_mean_absolute_error se debe convertir en positiva
cv_results = np.abs(cv_results)
results.append(cv_results)
names.append(name)
msg = f"{name}: {cv_results.mean():.2f} (±{cv_results.std():.2f})"
print(msg)
Elastic_net: 2120.58 (±1023.35)
Kneighbors: 2566.21 (±1188.78)
Decision_tree: 2494.75 (±1103.84)
SVR: 2445.58 (±1048.04)
Los resultados muestran el error absoluto medio (MAE) y su desviación estándar a través de 10 folds. ElasticNet tiene el MAE más bajo, pero todos los modelos muestran una alta variabilidad en sus resultados.
# Visualización de resultados de cross validation mediante boxplots
plt.figure(figsize=(8, 4))
result_df = pd.DataFrame(results, index=names).T
sns.boxplot(data=result_df)
plt.title("Resultados de Cross Validation")
plt.show()
# Visualización de resultados de cada fold
plt.figure(figsize=(8, 4))
sns.lineplot(data=result_df)
plt.title("Resultados de cada Kfold")
plt.show()
Realizamos una prueba estadística (ANOVA) para determinar si las diferencias entre los modelos son significativas:
# Comparación estadística de modelos
from scipy.stats import f_oneway
model1 = result_df['Elastic_net']
model2 = result_df['Kneighbors']
model3 = result_df['Decision_tree']
model4 = result_df['SVR']
statistic, p_value = f_oneway(model1, model2, model3, model4)
print(f'Statistic: {statistic}')
print(f'p_value: {p_value}')
alpha = 0.05 # nivel de significancia
if p_value < alpha:
print("Existe una diferencia estadísticamente "
"significativa en los resultados de"
" cross-validation de los modelos.")
else:
print("No existe una diferencia estadísticamente "
"significativa en los resultados de "
"cross-validation de los modelos.")
Statistic: 0.4463637923536907
p_value: 0.721363278798939
No existe una diferencia estadísticamente significativa en los resultados de cross-validation de los modelos.
A pesar de que no hay diferencias estadísticamente significativas, seleccionamos ElasticNet para la optimización de hiperparámetros debido a que:
Ahora que hemos seleccionado ElasticNet como nuestro modelo principal, procederemos a optimizar sus hiperparámetros para mejorar su rendimiento. Los principales hiperparámetros a ajustar son:
from sklearn.model_selection import GridSearchCV
# Optimización de Elastic Net Regression
parameters = {
'model__alpha': [0.2, 0.4, 0.6, 0.8, 1.0],
'model__l1_ratio': [0.1, 0.3, 0.5, 0.7, 0.9]
}
elastic_net_pipe = Pipeline(steps=[("preprocessor", preprocessor),
("model", ElasticNet(max_iter=100000, warm_start=True))])
grid_search = GridSearchCV(elastic_net_pipe, parameters, cv=5,
return_train_score=True,
scoring='neg_mean_absolute_error')
grid_search.fit(X_train, y_train);
# Resultados de la hiperparametrización
# La medida neg_mean_absolute_error se debe convertir en positiva
print(f"Mejor resultado = {abs(grid_search.best_score_)}")
print(f"Mejor std = {grid_search.cv_results_['std_test_score'][grid_search.best_index_]}")
print(f"Mejor parámetros = {grid_search.best_params_}")
Mejor resultado = 1756.596029557147
Mejor std = 426.13874790189107
Mejor parámetros = {'model__alpha': 0.2, 'model__l1_ratio': 0.9}
La búsqueda en cuadrícula (grid search) nos indica que los mejores hiperparámetros para nuestro modelo ElasticNet son:
Evaluamos el rendimiento del modelo ElasticNet con los hiperparámetros optimizados:
model = ElasticNet(
alpha=grid_search.best_params_['model__alpha'],
l1_ratio=grid_search.best_params_['model__l1_ratio'],
max_iter=100000,
warm_start=True
)
# Evaluar el modelo con los mejores hiperparámetros
elastic_net_pipe = Pipeline(steps=[
("preprocessor", preprocessor),
("model", model)
])
elastic_net_model = elastic_net_pipe.fit(X_train, y_train)
y_pred = elastic_net_model.predict(X_test)
y_pred_train = elastic_net_model.predict(X_train)
print('Mean Absolute Error en conjunto de Prueba: ', mean_absolute_error(y_test, y_pred))
Mean Absolute Error en conjunto de Prueba: 2393.867610873727
# Visualización de errores de predicción
from sklearn.metrics import PredictionErrorDisplay
PredictionErrorDisplay.from_predictions(y_true=y_test,
y_pred=y_pred,
kind="actual_vs_predicted");
PredictionErrorDisplay.from_predictions(y_true=y_test,
y_pred=y_pred,
kind="residual_vs_predicted");
Estas visualizaciones nos proporcionan información importante sobre el comportamiento de nuestro modelo:
Una vez optimizado nuestro modelo, es fundamental entender qué características influyen más en las predicciones. Esto proporciona insights valiosos sobre los factores que afectan el precio de los automóviles.
Utilizaremos la técnica de "Permutation Importance" para evaluar la importancia de cada característica en el modelo:
from sklearn.inspection import permutation_importance
# Calculamos importancia por permutación (funciona con el pipeline completo)
imps = permutation_importance(elastic_net_pipe, X_test, y_test,
n_repeats=5,
scoring="neg_mean_absolute_error",
n_jobs=-1, random_state=42)
# Visualizamos los resultados
fig = plt.figure(figsize=(10, 8))
perm_sorted_idx = imps.importances_mean.argsort()
plt.boxplot(imps.importances[perm_sorted_idx].T, vert=False, labels=X_test.columns[perm_sorted_idx])
plt.title("Permutation Importances (test set)");
# Seleccionamos las características más importantes según permutation importance
cols_seleccionadas = X_test.columns[perm_sorted_idx][-8:].tolist() # Top 8 características
print("Características más importantes según permutation importance:")
print(cols_seleccionadas)
Características más importantes según permutation importance:
['city-mpg', 'horsepower', 'drive-wheels', 'width', 'curb-weight', 'wheel-base', 'make', 'engine-size']
Los resultados de la importancia por permutación confirman muchas de nuestras observaciones iniciales del análisis exploratorio. Las características más influyentes en el precio de un automóvil son:
Estos resultados tienen varias implicaciones útiles:
En esta etapa final, guardamos nuestro modelo optimizado para uso futuro y demostramos cómo utilizarlo para realizar nuevas predicciones. El guardado del modelo permite integrar nuestro trabajo en aplicaciones, servicios o flujos de trabajo automatizados.
Utilizamos la biblioteca joblib, que es eficiente para serializar modelos de scikit-learn:
# Guardado del modelo final
from joblib import dump # librería de serialización
# Grabar el modelo en un archivo
dump(elastic_net_model, 'elastic-model-auto.joblib')
['elastic-model-auto.joblib']
Demostramos cómo cargar el modelo guardado y utilizarlo para realizar nuevas predicciones:
import pandas as pd
from joblib import load
# Cargar el modelo
modelo = load('elastic-model-auto.joblib')
print(modelo)
Pipeline(steps=[('preprocessor',
ColumnTransformer(
transformers=[('numericas',
Pipeline(steps=[('imputer',
SimpleImputer(strategy='median'))]),
['symboling', 'normalized-losses',
'wheel-base', 'length', 'width', 'height',
'curb-weight', 'engine-size', 'bore',
'stroke', 'compression-ratio', 'horsepower',
'peak-rpm', 'city-mpg', 'highway-mpg']),
('categoricas',
Pipeline(steps=[('imputer',
SimpleImputer(strategy='most_frequent')),
('onehot',
OneHotEncoder(handle_unknown='ignore'))]),
['make', 'fuel-type', 'aspiration',
'body-style', 'drive-wheels',
'engine-location', 'engine-type',
'fuel-system']),
('categoricas ordinales',
Pipeline(steps=[('ordenc',
OrdinalEncoder(handle_unknown='use_encoded_value',
unknown_value=nan)),
('imputer',
SimpleImputer(strategy='most_frequent'))]),
['num-of-doors', 'num-of-cylinders'])])),
('model',
ElasticNet(alpha=0.2, l1_ratio=0.9, max_iter=100000,
warm_start=True))])
Ahora, tomamos algunos datos de ejemplo para realizar predicciones con el modelo cargado:
# Tomar dos datos de entrada para realizar la predicción
datos_prueba = X_features.sample(2)
datos_prueba
# Resultados de predicción con el modelo
predicciones = modelo.predict(datos_prueba)
print(predicciones)
[16371.67580828 7975.86683396]
Al implementar este modelo en un entorno de producción, debemos considerar:
Para consolidar los conceptos aprendidos en este capítulo, te invitamos a realizar los siguientes ejercicios prácticos:
Para profundizar en los conceptos de regresión y técnicas relacionadas, recomendamos consultar estos recursos: