<jupyter_start><jupyter_text>**Autores**<NAME> <NAME> **Dataset**Vehicles: https://www.kaggle.com/austinreese/craigslist-carstrucks-data<jupyter_code>from google.colab import drive
drive.mount('/content/drive')<jupyter_output>Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).<jupyter_text>Librerias<jupyter_code>!pip install dython
import numpy as np
from numpy.random import normal

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from pandas import read_csv


from time import time

pd.set_option('precision', 3)
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"


# Extra imports
from sklearn.model_selection import train_test_split

from sklearn.linear_model import LinearRegression, RidgeCV, LassoCV
from sklearn.neighbors import KNeighborsRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.svm import SVR
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor,VotingRegressor,GradientBoostingRegressor,StackingRegressor,ExtraTreesRegressor
from sklearn.decomposition import PCA

from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from sklearn import preprocessing
from sklearn.metrics import r2_score, mean_squared_error, median_absolute_error, mean_absolute_error

from dython.nominal import associations

import warnings
warnings.filterwarnings('ignore')<jupyter_output><empty_output><jupyter_text>Leer DatasetLeemos el Dataset y mostramos las primeras filas para coger una idea del dataset<jupyter_code># Leemos el Dataset
data = pd.read_csv('/content/drive/MyDrive/APA/APA Practica/vehicles.csv')
data.head()
# Mostramos una breve descripción de los datos
data.describe(include='all')
# Función que nos servirá para mostrar el porcentage de nulls que tiene cada 
# columna de la tabla
def percentatge_nulls(data):
  null_values = data.isnull().sum()
  null_values = pd.DataFrame(null_values,columns=['null'])
  j = 1
  sum_tot = len(data)
  null_values['percent'] = null_values['null']/sum_tot
  return round(null_values * 100, 3).sort_values('percent', ascending=False)
percentatge_nulls(data)<jupyter_output><empty_output><jupyter_text>Nos quedamos con todas aquellas columnas que creemos que son relevantes para solucionar el problema o que tienen demasiados nulls Para este proyecto disponemos de un dataset con variables tanto numéricas como categóricas y es por ello que tendremos que ir con cuidado a la hora del preprocesado de datos. Solo al ver el dataset ya nos damos cuenta de que hay atributos que no son necesarios para predecir el precio y otros que tienen demasiados valores nulos como para que tenerlos en cuenta. Estos atributos son los siguientes: id, url, region_url, title_status, vin, size, paint_color, image_url, description, county, state, lat, long y region.<jupyter_code># Nos quedamos solo con las columnas útiles
# Borramos todas las columnas que no son importantes, como la descripción... Y también todas aquellas que tiene muchos nulls (size).
columnas_borrar = ['id', 'url', 'region_url', 'title_status', 'vin', 'size', 'paint_color', 'image_url', 'description', 'county', 'state', 'lat', 'long', 'region']

data = data.drop(columns = columnas_borrar)
data.describe(include='all')
percentatge_nulls(data)<jupyter_output><empty_output><jupyter_text>PreprocesadoEn este apartado iremos viendo columna a columna como tenemos los datos del dataset para ver si tenemos que preprocesarlos de algún modo.  Columna: PriceEl atributo precio representa el precio que tiene el coche en cuestión. Este atributo será el target que intentaremos predecir. Su distribución antes de aplicar el preprocesado es el siguiente:<jupyter_code># Mostramos un boxpolot con los datos originales
data.boxplot(column='price')
# Mostramos cuantas muestras hay con un precio de 0 €
print("Coches con precios de 0€:", (data.price == 0).sum())<jupyter_output>Coches con precios de 0€: 35025<jupyter_text>Podemos observar como por un lado tenemos demasiados outliers, por lo tanto intentaremos aplicar un IQR para ver si podemos solucionar el problema.Por el otro lado vemos que hay 35.025 coches que tienen un precio de 0€. Estos coches hemos decidido eliminarlos ya que consideramos que son valores sin sentido.<jupyter_code># Borramos los 0
data = data[data.price != 0]

# Aplicamos el IQR para ver si podemos quitar los outliers
Q1 = data.price.quantile(0.25)
Q3 = data.price.quantile(0.75)

IQR = Q3 - Q1

minimo_outliers = (Q1 - 1.5 * IQR)
maximo_outliers = (Q3 + 1.5 * IQR) 

dataAux = data[(data.price >= minimo_outliers) & (data.price <= maximo_outliers)]

dataAux.boxplot(column='price')<jupyter_output><empty_output><jupyter_text>Después de eliminar los coches que tenían como precio 0€, vemos como el IQR no ha acabado de solucionarnos el problema de los outliers. Por esta razón hemos decidido aplicar los percentiles 90-10 para quedarnos con los datos que hemos considerado que son más relevantes.<jupyter_code># Aplicamos el percentil para lidiar con los outliers
price_ordenado = sorted(data.price)
quantile1, quantile3= np.percentile(price_ordenado,[10,90])
print(quantile1, quantile3)

data = data[(data.price < quantile3) & (data.price >= quantile1 )]

data.boxplot(column='price')<jupyter_output>2800.0 32898.0<jupyter_text>Columna: OdometerOdometer, representa los kilómetros recorridos por el coche. Para esta columna hemos dibujado un kdeplot que nos muestra la distribución de los datos.<jupyter_code># Mostramos los datos originales
sns.kdeplot(data=data.odometer)<jupyter_output><empty_output><jupyter_text>Como podemos observar tenemos bastantes outliers en este atributo. Viendo la gráfica, nos damos cuenta de que la mayor parte de ellos son coches que han recorrido menos de unos 325.000 km. Es por eso que hemos decidido cortar, y quedarnos con todos los menores a 325.000. Además, valores tan elevados de odometer no son reales.<jupyter_code># Basandonos en el scatterplot hemos decidido coger los datos que estan por debajo de 325.000

data = data[(data.odometer <= 325000)]
sns.kdeplot(data=data.odometer)<jupyter_output><empty_output><jupyter_text>Columna: YearYear, representa el año de fabricación del automóvil. La distribución de este atributo es la siguiente:<jupyter_code># Mostramos los datos originales de year.
plt.figure(figsize=(15,9))
ax = sns.countplot(x='year',data=data);
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right",fontsize=10);

data = data[data.year > 2000]<jupyter_output><empty_output><jupyter_text>Viendo las distribución de los datos, podemos ver como la gran mayoría de los datos los podemos encontrar a partir del año 2000 hacia adelante. Por este motivo hemos decidido cortar, y eliminar los anteriores a esta fecha.<jupyter_code># Mostramos los datos preprocesados de year.
plt.figure(figsize=(15,9))
ax = sns.countplot(x='year',data=data);
ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha="right",fontsize=10);<jupyter_output><empty_output><jupyter_text>Columna: CylindersEste atributo representa el número de cilindros que tiene el coche. Es un atributo categórico, por eso vamos a mostrar qué valores puede tomar.<jupyter_code># Mostramos que valores puede tomar cylinders
data.cylinders.value_counts()<jupyter_output><empty_output><jupyter_text>Lo primero que hemos hecho ha sido eliminar la palabra cylinder de nuestros valores, es decir, hemos pasado de tener ‘6 cylinders’ a tener ‘6’. Seguidamente nos hemos quedado con las muestras que tienen un valor definido de cilindros y, por lo tanto, hemos decidido desechar todas las que tenían el valor ‘other’. Hemos decidido tomar esta decisión debido a que ‘other’ puede ser cualquier valor e intentar imputarlo sería inventarnos posibles valores.Finalmente, debido a que intentamos imputar los valores nulos mediante ffill y nos dio unos resultados nefastos, decidimos hacer un drop de estos valores y así conseguimos un valor de predicción bastante mejor.<jupyter_code># Eliminamos el string cylinders del valor
data.cylinders = data.cylinders.str.replace(' cylinders','')


# Eliminamos los other ya que puede ser cualquier valor
data = data[data.cylinders != 'other']

# Vemos que imputando no sale bien
data = data.dropna(subset=['cylinders'])

data.cylinders.value_counts()<jupyter_output><empty_output><jupyter_text>Columna: ConditionEste atributo se refiere a en qué condiciones se encuentra el coche. Su valor es categórico.<jupyter_code># Mostramos que valores puede tomar originalmente
data.condition.value_counts()<jupyter_output><empty_output><jupyter_text>En el momento de tratar la variable condition nos surgió un problema. Al principio de todo, cuando calculamos el porcentaje de nulos de cada clase, obtuvimos que condition tenía casi un 60% de valores nulos y nos planteamos la opción de descartarlo. Por otro lado, pensamos que no debíamos hacer eso ya que la condición de un coche es una característica importante a la hora de ponerle un precio.Finalmente nos dimos cuenta que la condición de un coche guarda una gran relación con la columna odometer, ya que contra mas kilometros recorre un coche más fácil es que un coche se encuentre en peor estado. Es por ello que hemos utilizado la variable odometer para imputar los valores nulos de condition.<jupyter_code># Para imputar los valores nulos de condition nos basaremos en la variable odometer ya que parecen tener una buena relacion.
# Calculamos la media de los valores de odometer para cada valor de condition
new_odo_mean = data[data['condition'] == 'new']['odometer'].mean()
excellent_odo_mean = data[data['condition'] == 'excellent']['odometer'].mean()
good_odo_mean = data[data['condition'] == 'good']['odometer'].mean()
like_new_odo_mean = data[data['condition'] == 'like new']['odometer'].mean()
salvage_odo_mean = data[data['condition'] == 'salvage']['odometer'].mean()
fair_odo_mean = data[data['condition'] == 'fair']['odometer'].mean()

# NEW
data.loc[data['odometer'] >= new_odo_mean, 'condition'] = data.loc[data['odometer'] >= new_odo_mean, 'condition'].fillna('new')

# LIKE NEW
data.loc[((data['odometer'] < excellent_odo_mean) & 
       (data['odometer'] >= like_new_odo_mean)), 'condition'] = data.loc[((data['odometer'] < excellent_odo_mean) & 
       (data['odometer'] >= like_new_odo_mean)), 'condition'].fillna('like new')

# EXCELLENT
data.loc[((data['odometer'] < good_odo_mean) & 
       (data['odometer'] >= excellent_odo_mean)), 'condition'] = data.loc[((data['odometer'] < good_odo_mean) & 
       (data['odometer'] >= excellent_odo_mean)), 'condition'].fillna('excellent')

# GOOD
data.loc[((data['odometer'] < like_new_odo_mean) & 
       (data['odometer'] >= good_odo_mean)), 'condition'] = data.loc[((data['odometer'] < like_new_odo_mean) & 
       (data['odometer'] >= good_odo_mean)), 'condition'].fillna('good')

# SALVAGE
data.loc[((data['odometer'] < good_odo_mean) & 
       (data['odometer'] >= fair_odo_mean)), 'condition'] = data.loc[((data['odometer'] < good_odo_mean) & 
       (data['odometer'] >= fair_odo_mean)), 'condition'].fillna('salvage')

# FAIR
data.loc[data['odometer'] <= fair_odo_mean, 'condition'] = data.loc[data['odometer'] <= fair_odo_mean, 'condition'].fillna('fair')

# Mostramos que valores puede tomar despues de preprocesar
data.condition.value_counts()<jupyter_output><empty_output><jupyter_text>Columnas RestantesA estas alturas ya hemos preprocesado todas aquellas columnas que tenían más missings. Por tanto, dado el tamaño de nuestro dataset, nos podemos permitir eliminar todos los nulos restantes debido a que las columnas que nos quedan por procesar tienen un porcentaje muy pequeño de estos. Entonces eliminaremos los nulos de las columnas siguientes:- Fuel- Transmission- Model- Manufacturer- Type- Drive<jupyter_code># Mostramos los nulos de los atributos restantes
percentatge_nulls(data)
# Eliminamos todos los nulls ya que hay muy pocos
data = data.dropna(subset=['fuel','transmission','model','manufacturer', 'type', 'drive'])
percentatge_nulls(data)<jupyter_output><empty_output><jupyter_text>Relación de las variablesEn este apartado estudiaremos como de relacionadas están nuestras variables respecto al precio. Lo dividiremos en dos partes: variables categóricas y numéricas.  CategóricasPara poder ver como de relacionadas están las variables categóricas respecto al precio, utilizaremos una tabla de asociaciones. La manera de interpretar esta tabla es mirando la primera columna, y contra más cercano esté el valor a 1  o a -1, más relacionada está esa variable con el precio del coche.<jupyter_code># Mostramos la tabla de asociaciones para ver comomo de importantes son las 
# distintas columnas respecto el precio
output = associations(data, nan_strategy='drop_samples', figsize=(10,10))

correlation = output['corr']
ax = output['ax']

correlation<jupyter_output><empty_output><jupyter_text>Por lo tanto, una vez analizada la tabla, podemos decir que las variables más relevantes a la hora de predecir el precio de un coche son:- Model- Condition- Drive- Type- Manufacturer  NuméricasEn el caso de las variables numéricas podemos observar como de relacionadas están respecto al precio utilizando un heatmap. La forma de interpretar esta tabla es la misma que en el caso de las categóricas, contra más cerca de 1 o -1 esté el valor, más relacionadas están las variables.<jupyter_code># Muy relacionado con year, e inversamente relacionado con odometer
plt.figure(figsize=(5,5))
cor = data.corr()
sns.heatmap(cor, annot=True, cmap=plt.cm.Reds)
plt.show()<jupyter_output><empty_output><jupyter_text>Por lo tanto, como podemos observar, en este caso todas las variables numéricas están altamente relacionadas con la variable precio.  Codificación Variables CategóricasPara transformar las variables categóricas en numéricas hemos decidido aplicar One Hot Encoding a aquellas variables que tengan pocos valores distintos y para las columnas que tienen muchos más valores posibles hemos aplicado un Label Encoding.En nuestro caso, debido a que por ejemplo el modelo del coche (model) tiene más de 27 mil valores diferentes, aplicar one hot encoding a esta característica nos generaría más de 27 mil columnas y haría que nuestro dataset tuviera un tamaño demasiado grande.Es por esto que una vez consultados cuantos valores puede tomar de cada variable categórica, hemos decidido aplicar Label Enconding a manufacturer, model y type. Por otro lado, hemos decidido aplicar one hot encoding a condition y drive.<jupyter_code>le_categorical_columns = ['manufacturer', 'model', 'type']
ohe_categorical_columns = ['condition', 'drive']

def LabelEncoding(df, categorical_columns):
  le = LabelEncoder()
  df[categorical_columns] = df[categorical_columns].apply(le.fit_transform) 
  return df 

def OneHotEncoding(df, categorical_columns):
  for column in categorical_columns:
    x_one_hot = pd.get_dummies(df[column], prefix=column)
    df = df.merge(x_one_hot,left_index=True,right_index=True)
    df = df.drop(columns=[column])
  return df

data = OneHotEncoding(data, ohe_categorical_columns)
data = LabelEncoding(data, le_categorical_columns)
data<jupyter_output><empty_output><jupyter_text>NormalizaciónDado que tenemos rangos de datos muy distintos, por ejemplo entre el modelo del coche (model) y el año de fabricación (year), es necesario aplicar algún tipo de normalización/estandarización de los datos si queremos usar modelos que usen distancias euclídeas como por ejemplo el KNN.<jupyter_code># Probamos diferentes maneras de normalizar/estandarizar nuestros datos para comprobar
# cual nos da mejores resultados
dataAux = data.copy()
min_max_scaler =  preprocessing.MinMaxScaler()
dataAux[['price_minMax', 'year_minMax', 'odometer_minMax']] = min_max_scaler.fit_transform(data[['price', 'year', 'odometer']])

std_scaler = preprocessing.StandardScaler()
dataAux[['price_std', 'year_std', 'odometer_std']]= preprocessing.scale(data[['price', 'year', 'odometer']])
# Comparativa entre price, aplicando MinMaxScaler y aplicando StandardScaler
dataAux[['price','price_minMax', 'price_std']].hist(figsize=(14,4), layout=(1,4));
# Comparativa entre year, aplicando MinMaxScaler y aplicando StandardScaler
dataAux[['year','year_minMax', 'year_std']].hist(figsize=(14,4), layout=(1,4));
# Comparativa entre odometer, aplicando MinMaxScaler y aplicando StandardScaler
dataAux[['odometer','odometer_minMax', 'odometer_std']].hist(figsize=(14,4), layout=(1,4));<jupyter_output><empty_output><jupyter_text>Como podemos observar, cualquiera de las dos transformaciones mantiene la distribución de los datos y por lo tanto, decidimos usar MinMaxScaler junto a los atributos model y manufacturer.<jupyter_code># Aplicamos MinMaxScaler a los datos numéricos
min_max_scaler =  preprocessing.MinMaxScaler()
data[['price', 'year', 'odometer', 'model', 'manufacturer']] = min_max_scaler.fit_transform(data[['price', 'year', 'odometer', 'model', 'manufacturer']])<jupyter_output><empty_output><jupyter_text>Features Finalmente después del preprocesado y de mirar como de relevantes son cada una de las variables, obtenemos las columnas que usaremos para predecir el precio:- Year- Manufacturer- Model- Odometer- Type- Condition- Drive<jupyter_code>data = data[['price', 'year', 'manufacturer', 'model',
       'odometer', 'type', 'condition_excellent',
       'condition_fair', 'condition_good', 'condition_like new',
       'condition_new', 'condition_salvage', 'drive_4wd', 'drive_fwd',
       'drive_rwd']]

# Features seran los atributos que utilizaremos para predecir      
features = data[['year', 'manufacturer', 'model',
       'odometer', 'type', 'condition_excellent',
       'condition_fair', 'condition_good', 'condition_like new',
       'condition_new', 'condition_salvage', 'drive_4wd', 'drive_fwd',
       'drive_rwd']]
       
# Target será nuestra variable a predecir
target = data[["price"]]<jupyter_output><empty_output><jupyter_text>Resumen de los DatosMostramos el dataset preprocesado<jupyter_code>data.describe(include='all')<jupyter_output><empty_output><jupyter_text>Train / Val / TestYa que nuestro dataset es muy grande, nos podemos permitir tener una partición de train para entrenar los modelos, una de validación para poder comparar los modelos, y una de test para poder mostrar los resultados finales.<jupyter_code># Ya que tenemos muchas muestras nos podemos permitir tener particiones para train, val y test
x_train, x_test, y_train, y_test = train_test_split(features, target, test_size=0.2, random_state=42)
x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.2, random_state=42)<jupyter_output><empty_output><jupyter_text>Métodos de resolución Para resolver el problema primero probaremos modelos de distintas familias para ver si hay alguno de ellos que parezca ajustarse más a nuestro problema de entrada. Una vez tengamos unos resultados base, cogeremos los mejores modelos e intentaremos tunearlos para ver si podemos conseguir mejores resultados. En nuestro caso probaremos modelos lineales y no lineales.Para poder llevar a cabo la práctica, dividiremos nuestro dataset en una partición de Train (60%) para poder entrenar nuestros modelos, una partición de Validación (20%) para poder comparar los resultados entre modelos y una partición de Test (20%) para poder mostrar el resultado final con el mejor modelo. Para comparar los modelos, principalmente nos fijaremos en la métrica del R2. De todos modos calcularemos el Mean Squared Error, el Mean Absolute Error y el Median Absolute Error para poder tener más información.<jupyter_code># Funcion que nos permite calcular la puntuacion de un modelo
def compute_metrics(y_pred, y_real):
    r2 = r2_score(y_pred,y_real)
    mse = mean_squared_error(y_pred, y_real)
    median_abs_e = median_absolute_error(y_pred, y_real)
    mean_abs_e = mean_absolute_error(y_pred, y_real)
    return [r2, mse, median_abs_e, mean_abs_e]

# Tabla donde iremos guardando los resultados de cada modelo, para poderlos 
# comparar más tarde
results = pd.DataFrame(columns=['R2', 'Mean_Square_Error', 'Median_Absolute_Error', 'Mean_Absolute_Error', 'Time'])<jupyter_output><empty_output><jupyter_text>Linear Regression Primero de todo probamos con modelos muy sencillos para ver si pueden ser una posible solución. Instanciamos los 3 modelos con los parámetros por defecto. En el caso del Ridge le hemos pasado un conjunto de alphas.<jupyter_code># Entrenamos el modelo
init_time = time()

lr = LinearRegression()
lr.fit(x_train,y_train)

training_time = time() - init_time


# Predecimos con los valores de validación para comparar entre modelos
y_pred = lr.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['Linear Regression', :] = compute_metrics(y_pred,y_val) + [training_time]
results
# Entrenamos el modelo
init_time = time()

alphas = 10**np.arange(-6,2,0.1)
ridge_cv = RidgeCV(cv=5,alphas=alphas)
ridge_cv.fit(x_train,y_train)

training_time = time() - init_time


# Predecimos con los valores de validacion para comparar entre modelos
y_pred = ridge_cv.predict(x_val)

# Añadimos la puntuacion del modelo para poder compararlo con los demas
results.loc['Ridge Regression', :] = compute_metrics(y_pred,y_val) + [training_time]
results
# Entrenamos el modelo
init_time = time()

lasso_cv = LassoCV(max_iter=5000)
lasso_cv.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = lasso_cv.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['Lasso Regression', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Podemos ver como las regresiones lineales no parecen ofrecernos grandes resultados así que seguiremos buscando  KNN El segundo modelo a probar será un KNN. Lo instanciamos con una k de 5 vecinos para empezar.<jupyter_code># Entrenamos el modelo
init_time = time()

knn = KNeighborsRegressor(n_neighbors=5)
knn.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = knn.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['KNN-5', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Podemos ver como este modelo ya nos da resultados bastante buenos, así nos lo guardaremos como posible candidato a resolver el problema.  MLP En este caso probaremos un par de MLP con distintas arquitecturas para ver si nos puede ofrecer resultados decentes. En primer lugar probaremos una arquitectura de [1] y posteriormente probaremos una arquitectura de [8,8,8], sin regularización y con una función de activación logística.<jupyter_code># Entrenamos el modelo
init_time = time()

xarxa = MLPRegressor(hidden_layer_sizes=[1],
                     alpha=0.00,
                     activation='logistic',
                     max_iter=10000,
                     solver='lbfgs',
                     random_state=42)
xarxa.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = xarxa.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['MLP-1', :] = compute_metrics(y_pred,y_val) + [training_time]
results
# Entrenamos el modelo
init_time = time()

xarxa = MLPRegressor(hidden_layer_sizes=[8, 8, 8],
                     alpha=0.00,
                     activation='logistic',
                     max_iter=1000,
                     solver='lbfgs',
                     random_state=42)
xarxa.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = xarxa.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['MLP-8-8-8', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Podemos ver como el resultado de las MLP es muy malo. El modelo KNN es un modelo muy sencillo de calcular y nos da resultados mucho mejores, es por eso que no perderemos mucho el tiempo intentando optimizar una MLP. Así que seguiremos probando modelos nuevos que puedan competir con el KNN.   SVM Ahora probaremos con las SVM, unos modelos muy potentes que podrían ofrecernos resultados muy buenos. Instanciamos un modelos SVM por defecto para ver que resultados puede ofrecernos.<jupyter_code># Entrenamos el modelo
init_time = time()

svm = SVR()
svm.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validacion para comparar entre modelos
y_pred = svm.predict(x_val)

# Añadimos la puntuacion del modelo para poder compararlo con los demas
results.loc['SVM-default', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Como podemos ver el resultado no puede competir contra el KNN. Además podemos ver como ha tardado 157 segundos en entrenar la SVM por defecto frente a los 1,44 segundos del KNN. Por este motivo no nos entretendremos demasiado en intentar tunear una SVM ya que muy rápidamente se nos disparará el tiempo de entrenamiento.  Decision Tree, Random Forest y Extra Trees Finalmente probaremos de instanciar estos 3 modelos con los parámetros por defecto para ver si podemos por lo menos plantarle cara al KNN. En el caso del Decision Tree, lo configuraremos con un max depth de 2, ya que vemos que nos ofrece "buenos" resultados.  Decision Tree<jupyter_code># Entrenamos el modelo
init_time = time()

decisionTree = DecisionTreeRegressor(max_depth=2)
decisionTree.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = decisionTree.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['DecisionTree-default', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Random Forest<jupyter_code># Entrenamos el modelo
init_time = time()

randomForest = RandomForestRegressor()
randomForest.fit(x_train, y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = randomForest.predict(x_val)


# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['RandomForest-default', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Extra Trees<jupyter_code># Entrenamos el modelo
init_time = time()

extra_trees = ExtraTreesRegressor()
extra_trees.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = extra_trees.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['Extra Trees-default', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Como podemos ver el Decision Tree nos ha dado resultados nefastos pero por el otro lado, el Random Forest y el Extra Trees han superado ampliamente la puntuación del KNN. Por este motivo estos dos modelos se convertirán en los principales candidatos a resolver nuestro problema.Finalmente podemos decir que los principales modelos a estudiar serán el Random Forest, Extra Trees y el KNN.  Reducción de Dimensionalidad (PCA)Dado que nuestro dataset es muy grande, probaremos si podemos reducir su dimensionalidad mediante un PCA.<jupyter_code>pca = PCA().fit(x_train)

n_components = (pca.explained_variance_ratio_.cumsum() < 0.9).sum()

print(f'Original Columns: {len(data.columns)}')
print(f'After PCA Transformation: {n_components}')
pca = PCA(n_components = n_components).fit(x_train)

x_train_pca = pca.transform(x_train)
x_val_pca = pca.transform(x_val)
x_test_pca = pca.transform(x_test)<jupyter_output><empty_output><jupyter_text>Lamentablemente, después de probarlo, nos hemos dado cuenta cuenta de que no es posible aplicar PCA a nuestros datos.  Optimización de ModelosUna vez encontrados los modelos que aparentemente parecen funcionar mejor para nuestros datos, probaremos de tunear sus hyper parámetros para ver si podemos mejorar su puntuación.  KNN Haremos un GridSearch para encontrar el número óptimo de vecinos.<jupyter_code># Vecinos
vecinos = [5*i for i in range(1,51)]

check_parameters = { 'n_neighbors': vecinos }

init_time = time()

gridsearchcv = GridSearchCV(knn, check_parameters, scoring = 'r2', n_jobs=-1, cv=3)
gridsearchcv.fit(x_train, x_train)

training_time = time() - init_time

print('Temps d\'entrenament: {} segons'.format(round(training_time, 3)))
print('Best parameters found:\n', gridsearchcv.best_params_)
print('Best score found:\n', gridsearchcv.best_score_)
best_n_neighbors = gridsearchcv.best_params_['n_neighbors']
# Entrenamos el modelo
init_time = time()

knn = KNeighborsRegressor(n_neighbors = best_n_neighbors)
knn.fit(x_train,y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = knn.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc[f'KNN-Opt-{best_n_neighbors}', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Como podemos observar, nuestro número óptimo de vecinos es 5 que era el valor que habiamos probado al principio y, por lo tanto, obtenemos los mismos resultados.  Random Forest Probaremos diferentes valores para el número de árboles que genera y la profundidad de estos.<jupyter_code>ntrees = [300, 200, 100]
max_depth = [100, 50, None]


trc = GridSearchCV(estimator = randomForest,
                   scoring=['r2'],
                   param_grid={
                       'n_estimators': ntrees,
                       'max_depth':max_depth
                   },
                   cv=2,
                   return_train_score=True,
                   refit='r2',
                   n_jobs=-1)

gridsearchcv = trc.fit(x_train, y_train)
print('Best parameters found:\n', gridsearchcv.best_params_)
print('Best score found:\n', gridsearchcv.best_score_)
best_n = gridsearchcv.best_params_['n_estimators']
best_depth = gridsearchcv.best_params_['max_depth']
# Entrenamos el modelo
init_time = time()

randomForest = RandomForestRegressor(n_estimators = best_n,
                        max_depth = best_depth)

randomForest.fit(x_train, y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = randomForest.predict(x_val)


# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['RandomForest-Opt', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Observamos que los mejores parámetros son:



1.   max_depth será el que viene por defecto
2.   n_estimators serán 300

Además, vemos que esta ooptimización nos da mejores resultados.

  Extra Trees Probaremos diferentes valores para el número de árboles que genera y la profundidad de estos.<jupyter_code>ntrees = [300, 200, 100]
max_depth = [100, 50, None]


trc = GridSearchCV(estimator = extra_trees,
                   scoring=['r2'],
                   param_grid={
                       'n_estimators': ntrees,
                       'max_depth':max_depth
                   },
                   cv=2,
                   return_train_score=True,
                   refit='r2')

gridsearchcv = trc.fit(x_train, y_train)
print('Best parameters found:\n', gridsearchcv.best_params_)
print('Best score found:\n', gridsearchcv.best_score_)
best_n = gridsearchcv.best_params_['n_estimators']
best_depth = gridsearchcv.best_params_['max_depth']
# Entrenamos el modelo
init_time = time()

extra_trees = ExtraTreesRegressor(n_estimators = best_n,
                        max_depth = best_depth)

extra_trees.fit(x_train, y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = extra_trees.predict(x_val)


# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['Extra Trees Opt', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Observamos que los mejores parámetros son:



1.   max_depth será el que viene por defecto
2.   n_estimators serán 300

Como podemos observar, esta optmización nos da unos buenos resultados, pero apenas ha habido una mejora respecto al Extra Trees por defecto  ComparativaPodemos ver como apenas hemos conseguido mejorar sus puntuaciones. Por este motivo consideramos que el Random Forest se convierte en el favorito para solucionar nuestro problema.<jupyter_code>results.sort_values(by=['R2'], ascending=False)<jupyter_output><empty_output><jupyter_text>EnsamblesAun habiendo obtenido un muy buen resultado con el Random Forest, no nos quedaremos ahí, e intentaremos exprimir al máximo los modelos que hemos obtenido. En este apartado ensamblaremos los 3 modelos que mejor resultado nos han dado (RF, ET, KNN) para ver si podemos conseguir mejorar su puntuación.En nuestro caso hemos probado un Voting Regressor, y un Stacking Regressor. En el caso del Stacking, lo hemos combinado con un Gradient Boosting Regressor.<jupyter_code># Escogemos los 3 modelos que nos han dado mejores resultados
best_models = [('et', extra_trees), ('rf', randomForest), ('knn', knn)]
# Entrenamos el modelo
init_time = time()

voting_hard = VotingRegressor(best_models)
voting_hard.fit(x_train, y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = voting_hard.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results.loc['Voting Hard', :] = compute_metrics(y_pred,y_val) + [training_time]
results
# Entrenamos el modelo
init_time = time()

stacky = StackingRegressor(
    estimators = best_models,
    final_estimator = GradientBoostingRegressor())

stacky.fit(x_train, y_train)

training_time = time() - init_time

# Predecimos con los valores de validación para comparar entre modelos
y_pred = stacky.predict(x_val)

# Añadimos la puntuación del modelo para poder compararlo con los demças
results.loc['Stackyn', :] = compute_metrics(y_pred,y_val) + [training_time]
results<jupyter_output><empty_output><jupyter_text>Como podemos ver, apenas hemos podido mejorar el resultado con el Staking Regressor.<jupyter_code># Imprimimos la tabla en orden descendente dde R2
results.sort_values(by=['R2'], ascending=False)<jupyter_output><empty_output><jupyter_text>ResultadosFinalmente, el modelo que hemos escogido para resolver nuestro problema ha sido el Random Forest con una precisión del 87,6%. Si bien es cierto que el Stacking Regressor ofrece un resultado ligeramente mejor, el Random Forest nos ofrece prácticamente el mismo resultado pero con mucho menos tiempo de entrenamiento. Por este motivo creemos que si este problema tuviera que resolverse en un escenario real lo mejor sería escoger un modelo con el que pudiéramos conseguir un equilibrio entre precisión y velocidad.<jupyter_code># Predecimos con los valores de test para comparar entre modelos
y_pred = randomForest.predict(x_test)

# Añadimos la puntuación del modelo para poder compararlo con los demás
results_final = pd.DataFrame(columns=['R2', 'Mean_Square_Error', 'Median_Absolute_Error', 'Mean_Absolute_Error', 'Time'])
results_final.loc['RandomForest', :] = compute_metrics(y_pred,y_test) + [training_time]
results_final<jupyter_output><empty_output><jupyter_text>Finalmente, una vez aplicado RandomForest() a nuestra partición de test, podemos observar que obtenemos una R2 del 87,2%.

Por lo tanto, podemos decir que nuestro modelo predice bastante bien.

Ademas, mostramos un scatterplot para comparar los datos predichos respecto los datos originales y vemos que la distribución de los datos predichos siguen la linea formada por los datos originales.<jupyter_code># Scatterplot
plt.scatter(y_pred, y_test, label='Predicted')
plt.plot(y_test, y_test, label='Original', c = 'r')
plt.xlabel("Predicted")
plt.ylabel("Original")
plt.legend()

plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0826989429870201, 'fraction_numerical': 0.007941289227230407, 'mean_word_length': 5.622959738846572, 'pattern_counts': {'":': 0, '<': 178, '<?xml version=': 0, '>': 177, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '41', 'dir': 'jupyter-structured-clean-dedup', 'id': '659484', 'license': 'MIT', 'n_tokens_mistral': 12264, 'n_tokens_neox': 11843, 'n_words': 3774, 'path': '[APA] Practica Final.ipynb', 'repo_name': 'MarcDV1999/Vehicles-Price-Predictions', 'size': '36529'}