Autores: Mariano Albaladejo González, José Antonio Ruipérez Valiente y Manuel Jesús Gómez Moratilla
Esta práctica consiste en una introducción al machine learning supervisado utilizando de ejemplo un problema de regresión. En los problemas de regresión nuestra variable a predecir será una variable numérica continua mientras que en los problemas de clasificación será una variable que represente categorías discretas.
Antes de comenzar revisa la metodología clásica de machine learning:
Este primer paso consiste en la extracción de datos para obtener el conjunto de datos con el que vamos a trabajar. Esto puede suponer desde cargar un fichero que tengamos almacenado a acceder a múltiples bases de datos para extraer nuestro conjunto de datos. En nuestro caso consistirá en descargar el fichero housing.csv con el conjunto de datos del aula virtual, guárdarlo en tu drive personal y acceder al conjunto de datos mediante la función read_csv de Pandas.
1.1. Descarga el fichero housing.csv del aula virtual y súbelo a tu cuenta de Google Drive
1.2. Monta el acceso a tus archivos de Google Drive a tu entorno de ejecución actual. Para ello, ejecuta la siguiente línea de código.
from google.colab import drive
drive.mount('/content/drive')
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).
Carga el dataframe utilizando la función read_csv de la librería Pandas. Debes introducir la ruta al fichero csv. Esta ruta tendrá el formato /content/drive/MyDrive/RUTA_DE_CARPETAS_DE_TU_DRIVE/housing.csv. También puedes guiarte con el explorador de archivos situado en la barra izquierda. Si tienes dudas consulta la práctica inicial de repaso.
import pandas as pd
df = pd.read_csv("/content/drive/MyDrive/df_MD/housing.csv")
El siguiente paso consiste en analizar y entender qué datos tenemos y qué información representa cada una de las columnas de nuestro conjunto de datos.
Primero vamos a mostrar el conjunto de datos. Para ello basta con introducir la variable que contiene nuestro conjunto de datos en una celda de código y ejecutarla.
df
| longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | ocean_proximity | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | -122.23 | 37.88 | 41.0 | 880.0 | 129.0 | 322.0 | 126.0 | 8.3252 | 452600.0 | NEAR BAY |
| 1 | -122.22 | 37.86 | 21.0 | 7099.0 | 1106.0 | 2401.0 | 1138.0 | 8.3014 | 358500.0 | NEAR BAY |
| 2 | -122.24 | 37.85 | 52.0 | 1467.0 | 190.0 | 496.0 | 177.0 | 7.2574 | 352100.0 | NEAR BAY |
| 3 | -122.25 | 37.85 | 52.0 | 1274.0 | 235.0 | 558.0 | 219.0 | 5.6431 | 341300.0 | NEAR BAY |
| 4 | -122.25 | 37.85 | 52.0 | 1627.0 | 280.0 | 565.0 | 259.0 | 3.8462 | 342200.0 | NEAR BAY |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 20635 | -121.09 | 39.48 | 25.0 | 1665.0 | 374.0 | 845.0 | 330.0 | 1.5603 | 78100.0 | INLAND |
| 20636 | -121.21 | 39.49 | 18.0 | 697.0 | 150.0 | 356.0 | 114.0 | 2.5568 | 77100.0 | INLAND |
| 20637 | -121.22 | 39.43 | 17.0 | 2254.0 | 485.0 | 1007.0 | 433.0 | 1.7000 | 92300.0 | INLAND |
| 20638 | -121.32 | 39.43 | 18.0 | 1860.0 | 409.0 | 741.0 | 349.0 | 1.8672 | 84700.0 | INLAND |
| 20639 | -121.24 | 39.37 | 16.0 | 2785.0 | 616.0 | 1387.0 | 530.0 | 2.3886 | 89400.0 | INLAND |
20640 rows × 10 columns
En nuestro caso tenemos 20.640 entradas y 10 columnas.
Para entender más información sobre qué contiene nuestro conjunto de datos es necesario leer la documentación disponible. En nuestro caso, esta documentación se encuentra en el siguiente enlace: https://www.kaggle.com/datasets/camnugent/california-housing-prices/.
Aquí tienes una descripción de las variables:
Podemos listar el nombre de las columnas mediante el atributo variable_con_dataframe.columns
df.columns
Index(['longitude', 'latitude', 'housing_median_age', 'total_rooms',
'total_bedrooms', 'population', 'households', 'median_income',
'median_house_value', 'ocean_proximity'],
dtype='object')
También podemos ver los tipos asociados a cada una de las columnas mediante el atributo variable_con_dataframe.dtypes
df.dtypes
| 0 | |
|---|---|
| longitude | float64 |
| latitude | float64 |
| housing_median_age | float64 |
| total_rooms | float64 |
| total_bedrooms | float64 |
| population | float64 |
| households | float64 |
| median_income | float64 |
| median_house_value | float64 |
| ocean_proximity | object |
Podemos obtener estadísticas básicas de todas las variables numéricas mediante la función variable_con_dataframe.describe()
df.describe()
| longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | |
|---|---|---|---|---|---|---|---|---|---|
| count | 20640.000000 | 20640.000000 | 20640.000000 | 20640.000000 | 20433.000000 | 20640.000000 | 20640.000000 | 20640.000000 | 20640.000000 |
| mean | -119.569704 | 35.631861 | 28.639486 | 2635.763081 | 537.870553 | 1425.476744 | 499.539680 | 3.870671 | 206855.816909 |
| std | 2.003532 | 2.135952 | 12.585558 | 2181.615252 | 421.385070 | 1132.462122 | 382.329753 | 1.899822 | 115395.615874 |
| min | -124.350000 | 32.540000 | 1.000000 | 2.000000 | 1.000000 | 3.000000 | 1.000000 | 0.499900 | 14999.000000 |
| 25% | -121.800000 | 33.930000 | 18.000000 | 1447.750000 | 296.000000 | 787.000000 | 280.000000 | 2.563400 | 119600.000000 |
| 50% | -118.490000 | 34.260000 | 29.000000 | 2127.000000 | 435.000000 | 1166.000000 | 409.000000 | 3.534800 | 179700.000000 |
| 75% | -118.010000 | 37.710000 | 37.000000 | 3148.000000 | 647.000000 | 1725.000000 | 605.000000 | 4.743250 | 264725.000000 |
| max | -114.310000 | 41.950000 | 52.000000 | 39320.000000 | 6445.000000 | 35682.000000 | 6082.000000 | 15.000100 | 500001.000000 |
La variable ocean_proximity aparece del tipo "object" y en el describe no parece haberse mostrado. Esto puede indicar que no es una variable numérica. Vamos a mostrar esa columna para ver algunos ejemplos de sus valores posibles:
df["ocean_proximity"]
| ocean_proximity | |
|---|---|
| 0 | NEAR BAY |
| 1 | NEAR BAY |
| 2 | NEAR BAY |
| 3 | NEAR BAY |
| 4 | NEAR BAY |
| ... | ... |
| 20635 | INLAND |
| 20636 | INLAND |
| 20637 | INLAND |
| 20638 | INLAND |
| 20639 | INLAND |
20640 rows × 1 columns
Podemos observar que esta variable contiene cadenas de caracteres. Para ver los posibles valores de las cadenas de caracteres y sus frecuencias podemos utilizar la función variable_con_dataframe["nombre_variable"].value_counts()
df["ocean_proximity"].value_counts()
| count | |
|---|---|
| ocean_proximity | |
| <1H OCEAN | 9136 |
| INLAND | 6551 |
| NEAR OCEAN | 2658 |
| NEAR BAY | 2290 |
| ISLAND | 5 |
Por último, vamos a detectar la presencia de valores faltantes, es decir, valores no rellenados en nuestro conjunto de datos. Es necesario solucionar de alguna forma estos datos faltantes ya que los modelos de IA no podrán ser ejecutados si en nuestro conjunto de datos tenemos valores faltantes.
Mediante la función df.isna() podemos conocer si un valor en una fila y columna es faltante o no.
df.isna()
| longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | ocean_proximity | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | False | False | False | False | False | False | False | False | False | False |
| 1 | False | False | False | False | False | False | False | False | False | False |
| 2 | False | False | False | False | False | False | False | False | False | False |
| 3 | False | False | False | False | False | False | False | False | False | False |
| 4 | False | False | False | False | False | False | False | False | False | False |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 20635 | False | False | False | False | False | False | False | False | False | False |
| 20636 | False | False | False | False | False | False | False | False | False | False |
| 20637 | False | False | False | False | False | False | False | False | False | False |
| 20638 | False | False | False | False | False | False | False | False | False | False |
| 20639 | False | False | False | False | False | False | False | False | False | False |
20640 rows × 10 columns
Con el código que tienes a continuación podemos automatizar la detección de las columnas con valores faltantes y su recuento:
# Calculamos el número de valores faltantes por columna
na_por_columna = df.isna().sum()
# Filtramos las columnas con valores faltantes
columnas_con_na = na_por_columna[na_por_columna > 0]
# Mostramos las columnas con valores faltantes y el número de NA
for columna, na_count in columnas_con_na.items():
print("Columna: "+str(columna)+ " Número de NA: "+str(na_count))
Columna: total_bedrooms Número de NA: 207
La fase de preprocesamiento contiene todos los pasos necesarios para preparar nuestros datos para que puedan ser utilizados por nuestros modelos de machine learning.
Antes de comenzar con el preprocesamiento vamos a establecer una semilla de aleatoriedad para que cuando ejecutemos procesos en los que haya aleatoriedad siempre se generen los mismos números aleatorios. Lo correcto es almacenar la semilla de aleatoriedad en una variable, establecer la semilla de aleatoriedad al inicio del notebook y cada vez que vayamos a usar alguna función o método que tenga aleatoriedad debemos volver a establecer la semilla de aleatoriedad o introducir la semilla como un argumento más a la función (esto no siempre es posible).
SEMILLA_ALEATORIEDAD = 123
import numpy as np
np.random.seed(SEMILLA_ALEATORIEDAD)
Este primer paso consiste en eliminar datos erróneos, columnas que no necesitemos, eliminar audios corruptos... Si es necesario aplicar alguna técnica de limpieza de datos debemos darnos cuenta en el análisis exploratorio.
En esta práctica no es necesario aplicar ninguna limpieza de datos.
En este paso debemos dividir nuestro conjunto de datos de forma aleatoria en entrenamiento y test, para ello utilizaremos la función train_test_split de sklearn. Esta función recibe como primer argumento el conjunto de datos a dividir, en test_size la proporción de datos a dejar en test y en random_state la semilla de aleatoriedad a utilizar para que siempre que ejecutemos esta línea de código se realice la misma división. Esta función nos devuelve primero el conjunto de entrenamiento y el conjunto de test.
from sklearn.model_selection import train_test_split
dataset_train, dataset_test = train_test_split(df, test_size=0.3, random_state=SEMILLA_ALEATORIEDAD)
Esta fase se centra en extraer más variables (columnas) para añadirlas a nuestro conjunto de datos.
En esta práctica no necesitamos extraer características adicionales.
Durante el preprocesamiento vimos que teníamos valores faltantes en la columna "total_bedrooms". Esto no siempre es así, pero en este conjunto de datos será necesario que solucionemos la presencia de valores faltantes.
La librería de sklearn nos ofrece diferentes formas de imputar los valores faltantes. Dentro de los imputadores, utilizaremos el algoritmo de imputación más sencillo llamado SimpleImputer que según el campo strategy podemos:
Existen otras opciones externas a esta librería como por ejemplo eliminar todas las filas con valores faltantes.
Referencias:
https://scikit-learn.org/stable/modules/classes.html#module-sklearn.impute
Lo importante a la hora de aplicar un imputador de valores faltantes es que se entrene o utilice de referencia datos del conjunto de entrenamiento y luego se aplique tanto al conjunto de entrenamiento y de test. A continuación, tienes un ejemplo sustituyendo los valores faltantes por la media. Esta media de cada columna es extraída de solo del conjunto de entrenamiento.
import numpy as np
from sklearn.impute import SimpleImputer
# Creamos el imputador a utilizar
imp_media = SimpleImputer(missing_values=np.nan, strategy='mean')
# Entrenamos el imputador con el conjunto de entrenamiento y con únicamente la columna/s a imputar valores "total_bedrooms"
imp_media.fit(dataset_train[["total_bedrooms"]])
# Imputamos los valores faltantes tanto en el conjunto de entrenamiento como en el de test.
dataset_train[["total_bedrooms"]] = imp_media.transform(dataset_train[["total_bedrooms"]])
dataset_test[["total_bedrooms"]] = imp_media.transform(dataset_test[["total_bedrooms"]])
Durante el análisis exploratorio hemos visto que la variable ocean_proximity era categórica o cualitativa. Por ello, necesitamos transformar esta variable antes de poder introducirla en nuestros modelos. Existen dos tipos de variables categóricas:
Variables categóricas ordinales: existe un orden entre las distintas categorías, por ejemplo, una variable categórica que representa evaluaciones y tome los valores "excelente", "bueno" y "malo". En este caso puede aplicarse un OneHotEncoding o asignarle directamente un valor numérico a cada categoría puesto que existe un orden entre estas.
Variables categóricas nominales: no existe un orden entre las distintas categorías, por ejemplo, una variable que contenga marcas de coches, tipos de rotuladores o los colores de las flores. A estas variables solo puede aplicarse un OneHotEncoding.
La estrategia consiste en crear una columna para cada valor distinto que exista en la característica que estamos codificando y, para cada entrada de la base de datos, marcar con un 1 la columna a la que pertenezca dicha entrada y dejar las demás con 0. Aquí tienes un ejemplo:

A continuación tienes el código que realiza esta transformación a la variable "ocean_proximity". La transformación utiliza un OneHotEncoder de la librería de sklearn. De nuevo debemos utilizar de referencia los datos del conjunto de entrenamiento y aplicarlo tanto al conjunto de entrenamiento como al de test.
Referencias: https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html
from sklearn.preprocessing import OneHotEncoder
# Creamos una instancia del OneHotEncoder
encoder = OneHotEncoder(sparse_output=False)
# Entrenamos el encoder con el conjunto de entrenamiento y la variable a codificar
encoder.fit(dataset_train[['ocean_proximity']])
# Transformos el conjunto de entrenamiento utilizando el encoder entrenado en el conjunto de entrenamiento
train_encoded = encoder.transform(dataset_train[['ocean_proximity']])
train_encoded_df = pd.DataFrame(train_encoded, columns=encoder.get_feature_names_out(['ocean_proximity']))
# Ajustmos y transformamos el conjunto de test utilizando el encoder entrenado en el conjunto de entrenamiento
test_encoded = encoder.transform(dataset_test[['ocean_proximity']])
test_encoded_df = pd.DataFrame(test_encoded, columns=encoder.get_feature_names_out(['ocean_proximity']))
# Combinamos los DataFrames codificados con los conjuntos de datos originales
dataset_train_encoded = pd.concat([dataset_train.reset_index(drop=True), train_encoded_df], axis=1)
dataset_test_encoded = pd.concat([dataset_test.reset_index(drop=True), test_encoded_df], axis=1)
# Eliminamos la columna original 'ocean_proximity'
dataset_train_encoded.drop(['ocean_proximity'], axis=1, inplace=True)
dataset_test_encoded.drop(['ocean_proximity'], axis=1, inplace=True)
A continuación puedes ver el resultado final una vez aplicada la codificación.
dataset_train_encoded
| longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | ocean_proximity_<1H OCEAN | ocean_proximity_INLAND | ocean_proximity_ISLAND | ocean_proximity_NEAR BAY | ocean_proximity_NEAR OCEAN | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | -116.46 | 33.82 | 6.0 | 4863.0 | 920.000000 | 3010.0 | 828.0 | 3.9508 | 104200.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
| 1 | -117.04 | 34.00 | 21.0 | 4624.0 | 852.000000 | 2174.0 | 812.0 | 3.5255 | 132100.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
| 2 | -121.03 | 37.55 | 32.0 | 946.0 | 198.000000 | 624.0 | 173.0 | 1.9728 | 97900.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
| 3 | -117.80 | 33.68 | 8.0 | 2032.0 | 349.000000 | 862.0 | 340.0 | 6.9133 | 274100.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 4 | -122.26 | 37.83 | 52.0 | 1656.0 | 420.000000 | 718.0 | 382.0 | 2.6768 | 182300.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 14443 | -118.10 | 33.91 | 36.0 | 726.0 | 541.574843 | 490.0 | 130.0 | 3.6389 | 167600.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 14444 | -117.24 | 33.37 | 14.0 | 4687.0 | 793.000000 | 2436.0 | 779.0 | 4.5391 | 180900.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 14445 | -121.76 | 37.33 | 5.0 | 4153.0 | 719.000000 | 2435.0 | 697.0 | 5.6306 | 286200.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 14446 | -122.44 | 37.78 | 44.0 | 1545.0 | 334.000000 | 561.0 | 326.0 | 3.8750 | 412500.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| 14447 | -119.08 | 36.21 | 20.0 | 1911.0 | 389.000000 | 1241.0 | 348.0 | 2.5156 | 59300.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 |
14448 rows × 14 columns
Además de las técnicas de preprocesamiento que hemos visto, con algunos conjuntos de datos o con algunos modelos es posible que nos veamos obligados a aplicar más técnicas de preprocesamiento. De nuevo, independientemente de la técnica y lo que haga recuerda siempre que solo utilice de referencia los datos del conjunto de entrenamiento y se aplique tanto al conjunto de entrenamiento como al de test. Un ejemplo es el escalado de variables.
Este paso no es un paso como tal de una metodología de machine learning, sin embargo, es habitual que los modelos necesiten que les introduzcas de forma separada las variables predictores y la variable a predecir.
Lo habitual es utilizar la nomenclatura x para las variables predictores o variables independientes y la nomenclatura y para la variable dependiente o variable a predecir. A continuación, tienes un ejemplo de cómo hacer esta división.
train_x = dataset_train_encoded.loc[:, dataset_train_encoded.columns != "median_house_value"]
train_y = dataset_train_encoded.loc[:, "median_house_value"]
test_x = dataset_test_encoded.loc[:, dataset_test_encoded.columns != "median_house_value"]
test_y = dataset_test_encoded.loc[:, "median_house_value"]
En nuestro caso, la variable a predecir es el valor medio de una vivienda (median_house_value). Al ser una variable cuantitativa (es numérica) continua tenemos un problema de regresión. Sin embargo, si la variable fuese cualitativa que tomase valores discretos sería un problema de clasificación.
Este paso se centra en probar diferentes modelos y configuraciones (hiperparámetros) de cada modelo. El objetivo es que al final de esta fase se seleccione el mejor modelo con su mejor combinación de hiperparámetros. Una de las técnicas más comunes es realizar una búsqueda grid search (búsqueda en cuadrícula) que consiste en entrenar y evaluar todas las posibles combinaciones de hiperparámetros. Sin embargo, si queremos entrenar y evaluar nuestro modelo utilizando sólo el conjunto de entrenamiento será necesario realizar una nueva división del conjunto de entrenamiento en entrenamiento y evaluación, en este caso lo llamaremos entrenamiento y validación.
Sin embargo, como vamos a estar ejecutando un montón de combinaciones de hiperparámetros es habitual caer en unos hiperparámetros que funcionen bien "casi por casualidad" con un conjunto de validación específico. Por ello, en machine learning clásico es muy habitual de en lugar de dividir el conjunto de entrenamiento en entrenamiento y validación una vez, lo que hacemos es repetir esta división n veces, es decir, aplicaremos una validación cruzada.
La validación cruzada a N pliegues implica dividir el conjunto de datos en N particiones (pliegues) del mismo tamaño.
El proceso de validación cruzada a N pliegues suele seguir estos pasos:
Se divide el conjunto de datos en N particiones o pliegues.
Se utilizan N-1 de los pliegues para entrenar el modelo y se reserva un pliegue para evaluar el modelo. Este paso se repite N veces, haciendo que cada pliegue se utilice una vez como conjunto de validación.
Una vez realizada la validación cruzada tenemos N rendimientos en validación y lo que hacemos es calcular el rendimiento promedio en los pliegues.
¿Cómo evaluamos el rendimiento de los modelos? Para ello utilizaremos métricas de error. Las métricas de error nos indicarán cuánto se acerca o aleja los valores predichos por un modelo de los valores reales o esperados. A continuación, tienes tres métricas de error muy comunes para problemas de regresión (problemas en los que predecimos una variable cuantitativa continua), si la variable fuera cualitativa utilizaríamos métricas de clasificación.
yi es el valor real.
ŷi es la predicción del modelo para el valor yi.
n es el número total de observaciones en el conjunto de datos de evaluación.
Error Cuadrático Medio (Mean Squared Error, MSE): Se calcula como la media de las diferencias al cuadrado entre las predicciones y los valores reales. MSE = ( Σ(yi - ŷi)^2 )/n
Raíz del Error Cuadrático Medio (Root Mean Squared Error, RMSE): El RMSE es simplemente la raíz cuadrada del MSE. Proporciona una medida del error en la misma unidad que los datos originales. RMSE = √MSE
Error Absoluto Medio (Mean Absolute Error, MAE): El MAE mide la magnitud promedio de los errores sin tener en cuenta su dirección (positiva o negativa). MAE = ( Σ|yi - ŷi| )/n
A continuación, tienes un ejemplo para aplicar una búsqueda de hiperparámetros a un modelo llamado K-Vecinos. Este modelo se basa fundamentalmente en la utilización de ejemplos “vecinos” al dato que hay que procesar (por lo tanto, la distancia entre cada ejemplo). En el código se configuran dos hiperparámetros:
En la búsqueda de hiperparámetros se realiza una validación cruzada a 5 pliegues calculando MSE, RMSE y el MAE, siendo el MAE la métrica a utilizar para elegir el mejor modelo. Para la búsqueda de hiperparámetros utilizaremos la función GridSearchCV de sklearn.
Referencias:
https://scikit-learn.org/1.5/modules/generated/sklearn.neighbors.KNeighborsRegressor.html
https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html
from sklearn.neighbors import KNeighborsRegressor
from sklearn.model_selection import GridSearchCV
# Definimos la métrica de error a optimizar
OPT = "MAE"
# Crear un modelo KNN Regressor
model = KNeighborsRegressor()
# Definimos la grid search con los hiperparámetros que deseamos explorar
param_grid = {
'n_neighbors': [3, 6, 9, 12, 15, 18, 21], # Número de vecinos
'weights': ['uniform', 'distance'], # Peso aplicado a los vecinos
}
# Realizamos la búsqueda de hiperparámetros utilizando validación cruzada
grid_search = GridSearchCV(model, param_grid, cv=5, refit=OPT, n_jobs=-1,
scoring={"MAE": "neg_mean_absolute_error",
"RMSE": "neg_root_mean_squared_error",
"MSE": "neg_mean_squared_error"
}
)
np.random.seed(SEMILLA_ALEATORIEDAD)
grid_search.fit(train_x, train_y)
# Imprimimos los mejores hiperparámetros y su rendimiento
print("Mejores hiperparámetros:", grid_search.best_params_)
print("Mejor puntuación (" + OPT + "):", -grid_search.best_score_)
Mejores hiperparámetros: {'n_neighbors': 21, 'weights': 'distance'}
Mejor puntuación (MAE): 74609.01120088436
También podemos mostrar estos rendimientos en un dataframe:
model_gscv_df = pd.DataFrame(grid_search.cv_results_).sort_values(by="mean_test_"+OPT, ignore_index=True, ascending=False)
model_gscv_df["mean_test_MAE"] = -model_gscv_df["mean_test_MAE"]
model_gscv_df["mean_test_MSE"] = -model_gscv_df["mean_test_MSE"]
model_gscv_df["mean_test_RMSE"] = -model_gscv_df["mean_test_RMSE"]
model_gscv_df[["params", "mean_test_MAE", "mean_test_MSE", "mean_test_RMSE"]]
| params | mean_test_MAE | mean_test_MSE | mean_test_RMSE | |
|---|---|---|---|---|
| 0 | {'n_neighbors': 21, 'weights': 'distance'} | 74609.011201 | 9.128833e+09 | 95542.878640 |
| 1 | {'n_neighbors': 18, 'weights': 'distance'} | 74618.354100 | 9.147443e+09 | 95639.961277 |
| 2 | {'n_neighbors': 15, 'weights': 'distance'} | 74703.142120 | 9.185759e+09 | 95840.315772 |
| 3 | {'n_neighbors': 12, 'weights': 'distance'} | 74898.459655 | 9.251505e+09 | 96183.688441 |
| 4 | {'n_neighbors': 18, 'weights': 'uniform'} | 74995.347436 | 9.244599e+09 | 96146.405522 |
| 5 | {'n_neighbors': 15, 'weights': 'uniform'} | 75023.050070 | 9.268683e+09 | 96271.744559 |
| 6 | {'n_neighbors': 21, 'weights': 'uniform'} | 75032.109009 | 9.236173e+09 | 96102.896106 |
| 7 | {'n_neighbors': 12, 'weights': 'uniform'} | 75186.513461 | 9.317245e+09 | 96524.830009 |
| 8 | {'n_neighbors': 9, 'weights': 'distance'} | 75278.879264 | 9.399795e+09 | 96951.102199 |
| 9 | {'n_neighbors': 9, 'weights': 'uniform'} | 75481.077188 | 9.441201e+09 | 97164.224885 |
| 10 | {'n_neighbors': 6, 'weights': 'distance'} | 76152.655340 | 9.722395e+09 | 98600.864450 |
| 11 | {'n_neighbors': 6, 'weights': 'uniform'} | 76223.768337 | 9.742037e+09 | 98700.163055 |
| 12 | {'n_neighbors': 3, 'weights': 'uniform'} | 79732.168473 | 1.086179e+10 | 104217.512474 |
| 13 | {'n_neighbors': 3, 'weights': 'distance'} | 79793.525861 | 1.090922e+10 | 104444.336031 |
model_gscv_df.columns
Index(['mean_fit_time', 'std_fit_time', 'mean_score_time', 'std_score_time',
'param_n_neighbors', 'param_weights', 'params', 'split0_test_MAE',
'split1_test_MAE', 'split2_test_MAE', 'split3_test_MAE',
'split4_test_MAE', 'mean_test_MAE', 'std_test_MAE', 'rank_test_MAE',
'split0_test_RMSE', 'split1_test_RMSE', 'split2_test_RMSE',
'split3_test_RMSE', 'split4_test_RMSE', 'mean_test_RMSE',
'std_test_RMSE', 'rank_test_RMSE', 'split0_test_MSE', 'split1_test_MSE',
'split2_test_MSE', 'split3_test_MSE', 'split4_test_MSE',
'mean_test_MSE', 'std_test_MSE', 'rank_test_MSE'],
dtype='object')
grid_search.best_params_
{'n_neighbors': 21, 'weights': 'distance'}
Es posible que hayas observado que a los resultados les ha cambiado el signo, esto es porque sklearn optimiza los errores maximizando y nuestro objetivo es minimiza los errores, por ello, sklearn calcula una versión negativa del RMSE, MSE y MSE a la que debemos invertir el signo. Un ejemplo es la línea de código: -grid_search.best_score_
A continuación entrenamos la configuración y modelo que obtuvo mejor rendimiento en la búsqueda de hiperparámetros con todo el conjunto de entrenamiento, sin aplicar validaciones cruzadas. Para ello es necesario que obtengamos los hiperparámetros del mejor modelo de la búsqueda de hiperparámetros mediante grid_search.best_params_
# Creamos un modelo con los hiperparámetros de la mejor configuración de la búsqueda de hiperparámetros
best_model = KNeighborsRegressor(**grid_search.best_params_)
# Entrenamos el modelo con todo el conjunto de datos
np.random.seed(SEMILLA_ALEATORIEDAD)
best_model.fit(train_x, train_y)
KNeighborsRegressor(n_neighbors=21, weights='distance')In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
KNeighborsRegressor(n_neighbors=21, weights='distance')
Para calcular el error en test con el modelo ya entrenado utilizamos la función mi_modelo.predict() introduciéndole de los datos de test sólo las variables predictoras. Una vez obtenidas las predicciones podemos utilizar las funciones que ofrece sklearn para calcular las métricas de error. A estas funciones debemos introducirlos los valores reales y los valores predichos.
Referencias:
https://scikit-learn.org/stable/modules/classes.html#module-sklearn.metrics
from sklearn.metrics import mean_squared_error, mean_absolute_error
from math import sqrt
# Obtenemos las predicciones del modelo en test
predictions = best_model.predict(test_x)
# Calculamos el MSE, RMSE y MAE en el conjunto de test a partir de las predicciones de nuestro modelo y los datos reales
mse = mean_squared_error(test_y, predictions)
rmse = sqrt(mse)
mae = mean_absolute_error(test_y, predictions)
print("MSE en test: "+str(mse))
print("RMSE en test: "+str(rmse))
print("MAE en test: "+str(mae))
MSE en test: 9077236205.1214 RMSE en test: 95274.53072632477 MAE en test: 74794.27019410505
Podemos reducir el código necesario para entrenar el mejor modelo de la búsqueda de hiperparámetros con todos los datos de entrenamiento y predecir su rendimiento en test, es decir, los pasos 4 y 5. Al haber puesto GridSearchCV() el atributo refit=OPT además de servir para que la función conozca cómo ordenar los resultados, también deja entrenado al mejor modelo con el 100% de los datos de entrenamiento y solo hace falta obtenerlo del atributo mi_grid_search.best_estimator_. Aquí tienes un ejemplo:
best_model = grid_search.best_estimator_
# Calculamos MSE, RMSE y MAE en el conjunto de test con el mejor modelo ya entrenado
predictions = best_model.predict(test_x)
mse = mean_squared_error(test_y, predictions)
rmse = sqrt(mse)
mae = mean_absolute_error(test_y, predictions)
print("MSE en test: "+str(mse))
print("RMSE en test: "+str(rmse))
print("MAE en test: "+str(mae))
MSE en test: 9077236205.1214 RMSE en test: 95274.53072632477 MAE en test: 74794.27019410505
En ocasiones también es habitual calcular el error en el 100% del conjunto de entrenamiento. Es decir, ver el rendimiento con los datos que ha visto durante su entrenamiento. Aquí tienes un ejemplo, la diferencia es que en lugar de predecir el conjunto de test predice el conjunto de entrenamiento.
best_model = grid_search.best_estimator_
# Calculamos MSE, RMSE y MAE en el conjunto de entrenamiento utilizando el mejor modelo
predictions = best_model.predict(train_x)
mse = mean_squared_error(train_y, predictions)
rmse = sqrt(mse)
mae = mean_absolute_error(train_y, predictions)
print("MSE en entrenamiento: "+str(mse))
print("RMSE en entrenamiento: "+str(rmse))
print("MAE en entrenamiento: "+str(mae))
MSE en entrenamiento: 0.0 RMSE en entrenamiento: 0.0 MAE en entrenamiento: 0.0
¡Cuidado! No confundas el error de entrenamiento del modelo del paso 5 con los errores en la búsqueda de hiperparámetros del paso 4. Piensa que en el paso 4 hemos aplicado una validación cruzada, por ello, no se ha entrenado el modelo con el 100% de los datos de entrenamiento. Además, el error que reportamos en el paso 4 es el error promedio de las validaciones cruzadas (porque repetíamos el proceso n veces, siendo n el número de pliegues).
1.1 Modifica los hiperparámetros de K-Vecinos añadiendo más opciones en el hiperparámetro n_neighbors.
1.2 Realiza la búsqueda de hiperparámetros de otro modelo, concretamente de un regresor lineal que en sklearn recibe el nombre LinearRegression. Este regresor lineal sólo tiene un único hiperparámetro que es fit_intercept. Este hiperaprámetro toma los valores True o False. Compara los resultados con K-Vecinos. En el caso en el que el regresor lineal haya obtenido un mejor rendimiento en las validaciones cruzadas de la búsqueda de hiperparámetros cambia de mejor modelo y predice con este modelo el conjunto de test.
1.3 Calcula en test la métrica de error r2_score.
Referencias:
https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html