El siguiente informe tiene como objetivo ofrecer una visión general clara y sintetizada del análisis realizado. En él se describen los datos utilizados, la metodología y los resultados obtenidos, destacando los aspectos más relevantes para la comprensión del estudio. A partir del dataset “Used Cars Prices in Spain” (kaggle) se definen los objetivos, tareas de preprocesado y un análisis exploratorio que permite entender relaciones entre variables clave.
El estudio busca predecir el precio de oferta de un coche usado, clasificación binaria para identificar vehículos con baja deprecación, agrupar coches con características similares y sintetizar variables correlacionadas para mejorar la eficiencia del modelado.
Para todo ello, se hace una limpieza del dataset que incluye eliminación de columnas irrelevantes, tratamiento de valores nulos, filtrado de outliers, eliminación de duplicados e ingeniería de caracterísitcas basada en los conceptos de negocio.
Finalmente, se identifican patrones esenciales relacionados con la deprecación, precio y características técnicas del vehículo.
Nuestro primer paso, así como se expresa en el “Caso de estudio” que se nos presenta como ejemplo de ejercicio, debe ser es de establecer un objetivo analítico. Debemos plantear el problema o pregunta que trataremos de resolver con el proyecto.
En este caso, nuestro estudio se centrará en cuantificar y modelar los factores que determinan el precio de venta de un coche usado en España. El contexto de negocio se centra en crear herramientas predictivas de valoración que permitan a concesionarios o plataformas de venta determinar el valor justo de mercado de un vehículo usado, así como identificar vehículos subvalorados o sobrevalorados
El problema analítico que abordaremos será el de cuantificar
y modelar la deprecación del vehículo y los factores que determinan el
precio de venta de un coche usado en España. Buscamos
identificar que características tienen un mayor impacto en el precio
final de oferta: brand, year, KM o CV, entre otros.
Nuestro problema analítico es de naturaleza mixta ya que requiere abordar varios objetivos:
El primer objetivo es la
Regresión (Modelo Supervisado), siendo el objetivo
principal del proyecto el de predecir el precio de oferta del vehículo,
una variable numérica continua.
El segundo objetivo es la
Reducción de Dimensionalidad (Pre-Modelado), debemos
sintetizar algunas variables técnicas altamente correlacionadas en unos
pocos factores de valoración técnica clave, con esto haremos que el
modelado sea robusto y eficiente.
El tercer objetivo es la
Segmentación (Modelado No Supervisado), buscando agrupar
los vehículos en segmentos de mercado homogéneos basándonos en sus
caracterísitcas y precios (Deportivo, Familiar, etc.).
El cuarto objetivo es la
Clasificación Binaria (Modelo Supervisado), donde el
objetivo secundario es clasificar un vehículo segín sus características
como “Baja deprecación” o “Alta deprecación”, a partir de una variable
binaria derivada.
De modo que se trata de un problema de Regresión,
Clasificación y Segmentación seguido de una
etapa crucial de Reducción de Dimensionalidad. Hemos
establecido un conjunto de métricas para evaluar el rendimiento de los
modelos diferenciando entre los objetivos de regresión y los de
clasificación.
Para el objetivo de Regresión, predicción del precio de
oferta del vehículo, utilizaremos el Error Cuadrático Medio de la Raíz.
Nuestro objetivo de cumplimiento será minimizar la desviación promedio
entre el precio predicho y el precio real de la transacción.
Para el objetivo de Clasificación Binaria, determinar si
un vehículo es de “Baja Deprecación”, utilizaremos una métrica
fundamental F1-score, la cual nos indicará la proporción de predicciones
correctas sobre el total de casos clasificados, el objetivo de
cumplimiento se establecerá en alcanzar un valor superior al 85% para la
identificación de vehículos de baja deprecación.
El dataset elegido es “Used Cars Prices in Spain” (kaggle), que se alinea con el problema de “cuantificar y modelar la deprecación del vehículo y los factores que determinan el precio de venta de un coche usado en España”.
Este juego de datos contiene 5980 observaciones, tenemos 24 variables, 19 de ellas numéricas, 3 categóricas y 1 binaria. Los requisitos eran >= 5 numéricas, >= 2 categóricas y >= 1 binaria.
La estructura del juego de datos permite abordar la
regresión sobre Price, la
clasificación sobre una variable binaria y la
segmentación.
Empezaremos por leer el juego de datos y responder a las preguntas clave de ¿El juego de datos contiene errores?, ¿Hay cosas extrañas entre los datos? o ¿Voy a tener que corregir o eliminar parte de los datos?
# asignamos el path
path = 'used_cars_data.csv'
carsData <- read.csv(path, row.names=NULL)
El siguiente paso es visualizar el juego de datos para identificar errores y anomalías siendo muy importante que los gestionemos antes de iniciar el estudio analítico.
Visualizamos en la siguiente tabla, las primeras seis filas y sus
respectivas columnas y observamos que
Brand, Model, Price, KM y Year son las claves y sus nombres
siendo lo suficientemente descriptivos para saber que hace cada una de
ellas:
# mostramos las primeras 10 filas
head(carsData, 10)
## X Brand Name Sticker Year KM Fuel CV
## 1 0 Opel Opel Corsa C 2022 47707 Diésel 102
## 2 1 Peugeot Peugeot Rifter C 2019 57194 Diésel 130
## 3 2 Renault Renault Kadjar C 2017 66428 Diésel 110
## 4 3 Dacia Dacia Sandero C 2016 48430 Gasolina 75
## 5 4 Nissan Nissan QASHQAI C 2020 72209 Gasolina 160
## 6 5 BMW BMW X2 C 2018 123979 Diésel 150
## 7 6 KIA KIA Stonic C 2018 125737 Diésel 110
## 8 7 KIA KIA Stonic C 2019 130012 Gasolina 100
## 9 8 Volkswagen Volkswagen T-Cross C 2020 27769 Gasolina 150
## 10 9 Land Rover Land Rover Range Rover Evoque ECO 2019 75212 Híbrido 200
## Transmission One_owner Location Length Width Height Weight Trunk Tank Vmax
## 1 MANUAL True Almería 4.06 1.77 1.43 1165 NA - 188
## 2 AUTO True Almería 4.40 1.85 1.82 1430 1355 - 179
## 3 MANUAL False Barcelona 4.45 1.84 1.61 1380 1478 - 182
## 4 MANUAL True Madrid 4.06 1.73 1.52 941 1200 - 162
## 5 AUTO False Málaga 4.39 1.81 1.59 1315 1598 - 198
## 6 AUTO False Barcelona 4.36 1.82 1.53 1575 1355 - 207
## 7 MANUAL False Murcia 4.14 1.76 1.52 1255 1155 - 175
## 8 MANUAL True Zaragoza 4.14 1.76 1.52 1180 1155 - 179
## 9 AUTO False Madrid 4.11 1.76 1.58 1330 1281 - 200
## 10 AUTO False Barcelona 4.37 1.90 1.65 1845 1156 - 216
## X0to100 Consumption Emissions Keys_num Extras_num Price
## 1 10.2 4.1 107 1 5 15700
## 2 4.3 114.0 NA 2 5 24900
## 3 11.9 3.8 99 2 5 17800
## 4 14.5 5.8 130 2 3 9300
## 5 9.9 6.9 156 2 5 21500
## 6 9.3 4.5 119 2 5 20900
## 7 11.3 4.2 109 2 5 15690
## 8 10.8 6.1 139 2 5 13500
## 9 8.5 6.6 119 1 5 23800
## 10 8.5 7.7 176 2 5 37900
Con la función skim podemos observar que hay varios
valores que tienen NA’s.
# vemos valores con NA
skim(carsData)
| Name | carsData |
| Number of rows | 5980 |
| Number of columns | 24 |
| _______________________ | |
| Column type frequency: | |
| character | 8 |
| numeric | 16 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| Brand | 0 | 1 | 0 | 10 | 7 | 45 | 0 |
| Name | 0 | 1 | 0 | 29 | 7 | 269 | 0 |
| Sticker | 0 | 1 | 0 | 11 | 32 | 5 | 0 |
| Fuel | 0 | 1 | 0 | 18 | 7 | 8 | 0 |
| Transmission | 0 | 1 | 0 | 6 | 7 | 3 | 0 |
| One_owner | 0 | 1 | 0 | 5 | 7 | 3 | 0 |
| Location | 0 | 1 | 0 | 10 | 1139 | 29 | 0 |
| Tank | 0 | 1 | 0 | 1 | 7 | 2 | 0 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| X | 0 | 1.00 | 2989.50 | 1726.42 | 0.0 | 1494.75 | 2989.50 | 4484.25 | 5979.00 | ▇▇▇▇▇ |
| Year | 7 | 1.00 | 2018.01 | 3.67 | 2000.0 | 2016.00 | 2019.00 | 2021.00 | 2024.00 | ▁▁▂▇▆ |
| KM | 7 | 1.00 | 81691.98 | 57054.50 | 5.0 | 46201.00 | 74836.00 | 108827.00 | 1116416.00 | ▇▁▁▁▁ |
| CV | 7 | 1.00 | 133.58 | 48.99 | 20.0 | 102.00 | 125.00 | 150.00 | 498.00 | ▆▇▁▁▁ |
| Length | 7 | 1.00 | 4.35 | 0.40 | 0.0 | 4.14 | 4.36 | 4.52 | 6.84 | ▁▁▂▇▁ |
| Width | 16 | 1.00 | 1.80 | 0.11 | 0.0 | 1.77 | 1.80 | 1.84 | 2.08 | ▁▁▁▁▇ |
| Height | 11 | 1.00 | 1.57 | 0.18 | 0.0 | 1.45 | 1.52 | 1.65 | 3.08 | ▁▁▇▁▁ |
| Weight | 27 | 1.00 | 1387.06 | 240.58 | 790.0 | 1210.00 | 1361.00 | 1513.00 | 2425.00 | ▂▇▅▁▁ |
| Trunk | 1367 | 0.77 | 1334.00 | 375.61 | 406.0 | 1143.00 | 1251.00 | 1503.00 | 3500.00 | ▂▇▁▁▁ |
| Vmax | 165 | 0.97 | 191.22 | 20.54 | 5.0 | 178.00 | 190.00 | 202.00 | 270.00 | ▁▁▁▇▁ |
| X0to100 | 33 | 0.99 | 13.91 | 26.30 | 3.9 | 8.80 | 10.30 | 11.50 | 352.00 | ▇▁▁▁▁ |
| Consumption | 189 | 0.97 | 19.75 | 48.85 | 1.0 | 4.50 | 5.30 | 6.20 | 602.00 | ▇▁▁▁▁ |
| Emissions | 886 | 0.85 | 125.05 | 26.24 | 31.0 | 110.00 | 120.00 | 138.00 | 271.00 | ▁▇▅▁▁ |
| Keys_num | 7 | 1.00 | 1.77 | 0.42 | 1.0 | 2.00 | 2.00 | 2.00 | 2.00 | ▂▁▁▁▇ |
| Extras_num | 7 | 1.00 | 4.90 | 0.50 | 1.0 | 5.00 | 5.00 | 5.00 | 5.00 | ▁▁▁▁▇ |
| Price | 7 | 1.00 | 17720.13 | 8087.51 | 1300.0 | 12490.00 | 16390.00 | 21490.00 | 61990.00 | ▅▇▁▁▁ |
Este tipo de estadísticas nos son útiles para observar la utilidad de
las columnas, especialmente observar la presencia de NA's y
la distribución.
Al examinar los estadísticos de los cuartiles y extremos, detectamos
lo siguiente en la variable CV:
Asimetría superior: Tenemos que el valor en
p75 es 150cv y el valor en p100
es 498cv, lo cual indica un incremento de un
232% en el último tramo. Esta gran diferencia no
significa necesariamente que tengamos un outlier erroneo,
sino que probablemente se trata de vehículos muy potentes, coches de
lujo o deportivos.
Asimetría inferior: Tenemos que el valor de
p0 es 20cv, lo cual podría indicar que se
están incluyendo vehículos de muy baja potencia o
ciclomotores/cuadriciclos. Estos valores deberían filtrarse ya que
podrían sesgar el modelo de coches estándar.
Se decide eliminar las columnas
Trunk, Vmax, Weight, X0to100, Emissions, Consumption, Tank, One_owner
dado que, desde una perspectiva de negocio
Weight, Trunk y Width son variables correlacionadas y
mantenerlas habría distorsionado el PCA.
Vmax, Consumption, Tank y X0to100 son variables que no
suelen determinar de forma directa el precio dentro del mercado de
segunda mano, donde factores como
antiguedad, CV, KM y Brand tienen mucho más peso.
# eliminación de columnas intranscendentes
carsData <- subset(carsData, select=-c(
Trunk,
Vmax,
Weight,
X0to100,
Emissions,
Consumption,
Tank,
One_owner))
# mostramos las columnas de nuevo
head(carsData, 10)
## X Brand Name Sticker Year KM Fuel CV
## 1 0 Opel Opel Corsa C 2022 47707 Diésel 102
## 2 1 Peugeot Peugeot Rifter C 2019 57194 Diésel 130
## 3 2 Renault Renault Kadjar C 2017 66428 Diésel 110
## 4 3 Dacia Dacia Sandero C 2016 48430 Gasolina 75
## 5 4 Nissan Nissan QASHQAI C 2020 72209 Gasolina 160
## 6 5 BMW BMW X2 C 2018 123979 Diésel 150
## 7 6 KIA KIA Stonic C 2018 125737 Diésel 110
## 8 7 KIA KIA Stonic C 2019 130012 Gasolina 100
## 9 8 Volkswagen Volkswagen T-Cross C 2020 27769 Gasolina 150
## 10 9 Land Rover Land Rover Range Rover Evoque ECO 2019 75212 Híbrido 200
## Transmission Location Length Width Height Keys_num Extras_num Price
## 1 MANUAL Almería 4.06 1.77 1.43 1 5 15700
## 2 AUTO Almería 4.40 1.85 1.82 2 5 24900
## 3 MANUAL Barcelona 4.45 1.84 1.61 2 5 17800
## 4 MANUAL Madrid 4.06 1.73 1.52 2 3 9300
## 5 AUTO Málaga 4.39 1.81 1.59 2 5 21500
## 6 AUTO Barcelona 4.36 1.82 1.53 2 5 20900
## 7 MANUAL Murcia 4.14 1.76 1.52 2 5 15690
## 8 MANUAL Zaragoza 4.14 1.76 1.52 2 5 13500
## 9 AUTO Madrid 4.11 1.76 1.58 1 5 23800
## 10 AUTO Barcelona 4.37 1.90 1.65 2 5 37900
Para gestionar los valores nulos (NA), optamos por la eliminación de las filas que contienen algún valor nulo en las columnas restantes. Elegimos esta estrategia en lugar de la imputación para no introducir sesgos sintéticos en variables clave en la primera iteración.
Otras opciones habrían sido:
Crear una media/mediana para variables numéricas como
KM, Price, antiguedad, etc.
Moda para variables categóricas como
Fuel o Brand.
Los valores faltantes no son aleatorios sino que corresponden a anuncios incompletos e imputarlos podría introducir un sesgo sintético, como hemos dicho anteriormente. En porcentaje de NA eliminados es bajo comparado con el tamaño inicial del dataset y para PCA, la imputación simple podría distorsionar la estructura de las variables.
# omitimos los valores que tengan NA y lo guardamos el dataset
carsData <- na.omit(carsData)
Una vez limpiado el dataset, podemos confirmar con
skimr que, efectivamente, ya no queda ningun dato con
NA:
# asegurarnos de que no quedan valores con NA
skim(carsData)
| Name | carsData |
| Number of rows | 5960 |
| Number of columns | 16 |
| _______________________ | |
| Column type frequency: | |
| character | 6 |
| numeric | 10 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| Brand | 0 | 1 | 2 | 10 | 0 | 44 | 0 |
| Name | 0 | 1 | 5 | 29 | 0 | 267 | 0 |
| Sticker | 0 | 1 | 0 | 11 | 25 | 5 | 0 |
| Fuel | 0 | 1 | 3 | 18 | 0 | 7 | 0 |
| Transmission | 0 | 1 | 4 | 6 | 0 | 2 | 0 |
| Location | 0 | 1 | 0 | 10 | 1132 | 29 | 0 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| X | 0 | 1 | 2986.77 | 1724.30 | 0 | 1493.75 | 2986.50 | 4480.25 | 5972.00 | ▇▇▇▇▇ |
| Year | 0 | 1 | 2018.00 | 3.67 | 2000 | 2016.00 | 2019.00 | 2021.00 | 2024.00 | ▁▁▂▇▆ |
| KM | 0 | 1 | 81684.60 | 57079.56 | 5 | 46178.50 | 74871.50 | 108827.00 | 1116416.00 | ▇▁▁▁▁ |
| CV | 0 | 1 | 133.57 | 49.04 | 20 | 102.00 | 125.00 | 150.00 | 498.00 | ▆▇▁▁▁ |
| Length | 0 | 1 | 4.35 | 0.40 | 0 | 4.14 | 4.36 | 4.52 | 6.84 | ▁▁▂▇▁ |
| Width | 0 | 1 | 1.80 | 0.11 | 0 | 1.77 | 1.80 | 1.84 | 2.08 | ▁▁▁▁▇ |
| Height | 0 | 1 | 1.57 | 0.18 | 0 | 1.45 | 1.52 | 1.65 | 3.08 | ▁▁▇▁▁ |
| Keys_num | 0 | 1 | 1.77 | 0.42 | 1 | 2.00 | 2.00 | 2.00 | 2.00 | ▂▁▁▁▇ |
| Extras_num | 0 | 1 | 4.90 | 0.49 | 1 | 5.00 | 5.00 | 5.00 | 5.00 | ▁▁▁▁▇ |
| Price | 0 | 1 | 17713.18 | 8094.88 | 1300 | 12450.00 | 16390.00 | 21490.00 | 61990.00 | ▅▇▁▁▁ |
La eliminación de los NAs reduce el número total de observaciones, pero nos deja un dataset limpio y sin datos faltantes.
Por último, dado el problema detectado en el punto anterior de
posibles ciclomotores en el dataset que podrían sesgar el
modelo de coches estándar, hemos decidido seleccionar los
40cv como potencia mínima aceptable para un coche estándar
y eliminar todas esas filas que estén por debajo de los
40cv.
# eliminar los coches con menos de 40cv y guardarlos en el mismo dataset
carsData <- carsData[carsData$CV > 40, ]
Al aplicar este filtro evitamos que los valores extremos de baja
potencia introduzcan ruido o arrastren la media de la variable
CV y otras variables correlacionadas (como
Price o Engine Size), lo que hubiera
modificado el comportamiento del modelo de clasificación y
regresión.
Debemos eliminar cualquier registro que represente la misma observación repetida, ya que duplica artificialmente la importancia de ciertas configuraciones de vehículos y sesgan los estadísticos de varianza.
Vamos a comprobar si tenemos datos duplicados en columnas clave
usando la librería dplyr y su comando
duplicated:
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
# guardar columnas clave en una lista
columnasClave <- c("Brand", "Name", "Year", "KM", "Price","Location","CV","Transmission")
# contar duplicados en columnas clave
conteoDuplicadosClave <- carsData %>%
select(all_of(columnasClave)) %>%
duplicated() %>%
sum()
#mostramos cuantos duplicados hay en columnas clave
print(paste("Duplicados basados solo en columnas clave:", conteoDuplicadosClave))
## [1] "Duplicados basados solo en columnas clave: 4797"
Podemos observar que las filas/observaciones duplicadas son muy elevadas, un 80% del dataset son copias exactas o casi exactas de otros vehículos.
Para poder continuar, debemos eliminar dichas observaciones y quedarnos únicamente con aquellas que nos sirvan.
# si la cuenta es mayor que 0
if (conteoDuplicadosClave > 0) {
# mantener unicamente las que sean TRUE
carsData <- carsData %>%
distinct(across(all_of(columnasClave)), .keep_all = TRUE)
print("Redundancias eliminadas.")
# si no hay duplicados
} else {
# no se hace nada
print("No se encontró redundancia en las columnas clave.")
}
## [1] "Redundancias eliminadas."
Al ser un dataset creado a partir de un scraper de Python, puede haber varias razones por las que hay tanta redundancia. Una de ellas podría ser que el scraper ha cogido varias publicaciones del mismo vendedor en múltiples plataformas de venta de coches. Otra posibilidad para que haya tantas duplicaciones es que los scrapers suelen hacer barridos repetitivos y frecuentes, lo que podría haber propiciado a recoger un mismo anuncio varias veces registrándolo como anuncios distintos.
Otra opción es que la redundancia es muy alta en coches nuevos/seminuevos por lo que al ser características similares se pueden llegar a posicionar como idénticos sin ser el mismo coche.
A pesar que el dataset original ya cumplía con los mínimos de variables, debemos crear métricas que reflejen conceptos de negocio y asegurar el requisito de la variable binaria.
Lo primero que haremos es crear una métrica que refleja el valor por unidad de kilometraje, un indicador clave pasa saber el estado de conservación. Un valor alto sugiere que el vehículo ha mantenido su precio de forma excepcional, posiblemente porque se debe a una marca de lujo o está en un excelente estado de conservación.
# crear ratio de precio / km
carsData$ratioPrecioKm <- carsData$Price / carsData$KM
Lo siguiente que haremos es, a partir del año 2024, crear una
variable de antiguedad que capture el valor de la
deprecación:
# crear antiguedad a partir del año 2024 - año del vehiculo
carsData$antiguedad <- 2024 - carsData$Year
Con la variable antiguedad podemos calcular una variable
para el objetivo secundario, un ratio en el que calculemos la
BajaDeprecación de los vehículos de marcas premium:
# marcas premium
marcasPremium <- c("Audi", "BMW", "Porsche", "Mercedes-Benz", "Land Rover", "Jaguar", "Lamborghini", "Buggatti")
# si la marca de coches está en la lista de marcas premium y tiene una antiguedad de 3 o menos marcar con 1, sino 0
carsData$BajaDeprecacion <- ifelse(
carsData$Brand %in% marcasPremium & carsData$antiguedad <= 3,
1,
0
)
Las variables categoricas deben transformarse en numéricas para PCA,
utilizaremos el One-Hot Encoding para las variables de baja
y media cardinalildad.
library(caret)
# convertir variables clave a factor
carsData$Brand <- as.factor(carsData$Brand)
carsData$Fuel <- as.factor(carsData$Fuel)
carsData$Transmission <- as.factor(carsData$Transmission)
carsData$Location <- as.factor(carsData$Location)
carsData$Sticker <- as.factor(carsData$Sticker)
# seleccionar las variables categoricas a codificar
vars_categoricas <- carsData %>% select(Brand, Fuel, Transmission, Location, Sticker)
# aplicar One-Hot Encoding
dmy <- dummyVars("~ .", data = vars_categoricas, fullRank = TRUE)
encoded_vars <- data.frame(predict(dmy, newdata = vars_categoricas))
# eliminar las variables categoricas originales del dataframe principal
carsData_num <- carsData %>% select(-Brand, -Name, -Fuel, -Transmission, -Location, -Sticker)
# unir el dataframe principal con las variables codificadas
carsData_final <- bind_cols(carsData_num, encoded_vars)
El resultado del proceso de One-Hot Encoding ha
resultado en una expansión de la matriz de datos en la que cada
categoría única se ha convertido en una nueva columna binaria (por
ejemplo, Brand.Audi = 0 o Brand.Seat = 1, etc.) lo cual
convierte el dataset en una matriz de datos completamente
numérica.
Identificamos todas las variables predictoras.
# la matriz carsData_final contiene las variables originales numéricas, las derivadas y las dummies codificadas.
vars_predictores_final <- carsData_final %>%
select(-Price, -BajaDeprecacion, -ratioPrecioKm, -X) %>%
names()
El escalado es obligatorio antes del PCA porque las variables tienen diferentes magnitudes. Se aplica Z-Score para que la varianza de cada variable contribuya equitativamente al análisis.
El objetivo del grafico de codo es determinar el numero de componentes que retienen la mayor parte de la información del dataset.
library(ggplot2)
str(carsData_final)
## 'data.frame': 1154 obs. of 94 variables:
## $ X : int 0 1 2 3 4 5 6 7 8 9 ...
## $ Year : int 2022 2019 2017 2016 2020 2018 2018 2019 2020 2019 ...
## $ KM : int 47707 57194 66428 48430 72209 123979 125737 130012 27769 75212 ...
## $ CV : int 102 130 110 75 160 150 110 100 150 200 ...
## $ Length : num 4.06 4.4 4.45 4.06 4.39 4.36 4.14 4.14 4.11 4.37 ...
## $ Width : num 1.77 1.85 1.84 1.73 1.81 1.82 1.76 1.76 1.76 1.9 ...
## $ Height : num 1.43 1.82 1.61 1.52 1.59 1.53 1.52 1.52 1.58 1.65 ...
## $ Keys_num : int 1 2 2 2 2 2 2 2 1 2 ...
## $ Extras_num : int 5 5 5 3 5 5 5 5 5 5 ...
## $ Price : int 15700 24900 17800 9300 21500 20900 15690 13500 23800 37900 ...
## $ ratioPrecioKm : num 0.329 0.435 0.268 0.192 0.298 ...
## $ antiguedad : num 2 5 7 8 4 6 6 5 4 5 ...
## $ BajaDeprecacion : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Alfa.Romeo : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Audi : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.BMW : num 0 0 0 0 0 1 0 0 0 0 ...
## $ Brand.BYD : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Chevrolet : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Citroën : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Cupra : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Dacia : num 0 0 0 1 0 0 0 0 0 0 ...
## $ Brand.DFSK : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.DS : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Fiat : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Ford : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Honda : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Hyundai : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Infiniti : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Iveco : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Jaguar : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Jeep : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.KIA : num 0 0 0 0 0 0 1 1 0 0 ...
## $ Brand.Land.Rover : num 0 0 0 0 0 0 0 0 0 1 ...
## $ Brand.Lexus : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Mazda : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Mercedes : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.MG : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Mini : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Mitsubishi : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Nissan : num 0 0 0 0 1 0 0 0 0 0 ...
## $ Brand.Opel : num 1 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Peugeot : num 0 1 0 0 0 0 0 0 0 0 ...
## $ Brand.Porsche : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Renault : num 0 0 1 0 0 0 0 0 0 0 ...
## $ Brand.RIMOR : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Seat : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Skoda : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Smart : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.SsangYong : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Subaru : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Suzuki : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Tesla : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Toyota : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Brand.Volkswagen : num 0 0 0 0 0 0 0 0 1 0 ...
## $ Brand.Volvo : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Fuel.Eléctrico : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Fuel.Gasolina : num 0 0 0 1 1 0 0 1 1 0 ...
## $ Fuel.GLP : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Fuel.GNC : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Fuel.Híbrido : num 0 0 0 0 0 0 0 0 0 1 ...
## $ Fuel.Híbrido.Enchufable: num 0 0 0 0 0 0 0 0 0 0 ...
## $ Transmission.MANUAL : num 1 0 1 1 0 0 1 1 0 0 ...
## $ Location.Albacete : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Alicante : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Almería : num 1 1 0 0 0 0 0 0 0 0 ...
## $ Location.Badajoz : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Barcelona : num 0 0 1 0 0 1 0 0 0 1 ...
## $ Location.Cáceres : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Cádiz : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Castellón : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Ciudad : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Córdoba : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Gipuzkoa : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Girona : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Granada : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Huelva : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Jaén : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.La : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Madrid : num 0 0 0 1 0 0 0 0 1 0 ...
## $ Location.Málaga : num 0 0 0 0 1 0 0 0 0 0 ...
## $ Location.Murcia : num 0 0 0 0 0 0 1 0 0 0 ...
## $ Location.Navarra : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Oviedo : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Pontevedra : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Santander : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Sevilla : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Toledo : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Valencia : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Vizcaya : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Location.Zaragoza : num 0 0 0 0 0 0 0 1 0 0 ...
## $ Sticker.0_EMISIONES : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Sticker.B : num 0 0 0 0 0 0 0 0 0 0 ...
## $ Sticker.C : num 1 1 1 1 1 1 1 1 1 0 ...
## $ Sticker.ECO : num 0 0 0 0 0 0 0 0 0 1 ...
## - attr(*, "na.action")= 'omit' Named int [1:20] 277 417 557 717 1874 2253 2632 3091 3467 4023 ...
## ..- attr(*, "names")= chr [1:20] "277" "417" "557" "717" ...
pca_result <- prcomp(carsData_final %>% select(all_of(vars_predictores_final)),
center = TRUE,
scale. = TRUE)
# calcular la varianza explicada y la varianza acumulada
variancia_explicada <- (pca_result$sdev^2) / sum(pca_result$sdev^2)
variancia_acumulada <- cumsum(variancia_explicada)
# visualizar el Scree Plot
qplot(c(1:length(variancia_explicada)), variancia_explicada) +
geom_line() +
labs(title = "Gráfico de Codo para PCA",
x = "Componente Principal",
y = "Varianza Explicada") +
theme_minimal()
## Warning: `qplot()` was deprecated in ggplot2 3.4.0.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Podemos ver, en el grafico de codo, que tenemos una caida rapida y pronunciada de la varianza explicada en los primeros 5 a 10 componentes. A partir de ese punto, la curva se aplana donde cada componente explica solo una cantidad muy pequeña de la varianza. Acabaremos la aplicación del PCA en el punto 5.
En la fase de análisis exploratorio es en la que comenzamos a profundizar en los datos. Ya tenemos en dataset limpio y acondicionado para obtener insights, validar hipótesis de negocio y justificar la necesidad de la reducción de la dimensionalidad.
Vamos a validar las distribuciones para entender la población de
coches y confirmar si se requieren transformaciones. Comenzaremos por la
distribución del precio, Price.
Queremos confirmar que existe una asimetría positiva del precio ya que justifica la necesidad de una transformación logarítmica si se utiliza un modelo de regresión sensible a la distribución para acercar dichos datos a una distribución normal.
library(ggplot2)
# mostramos grafico de precio x frecuencia
ggplot(carsData_final, aes(x = Price)) +
geom_histogram(binwidth = 5000, fill = "darkblue", color = "white")+
labs(title = "Distribución del precio",
x = "Precio (€)",
y = "Frecuencia") +
theme_minimal()
Podemos observar que hay una leve asimetría a la derecha, pero es mucho más simétrica de lo que se esperaba para datos de precio de mercado.
La mayor frecuencia (pico) se encuentra en los 15/20.000€.
Probablemente, la limpieza de redundancia masiva ha eliminado muchos de
los registros de precios altos, el dataset resultante muestra
una distribución cercana a la normal lo cual reduce la necesidad de
transformar la variable Price mediante logaritmos y
simplifica la interpretación de los modelos de regresión lineal.
Ahora, queremos confirmar un fuerte desequilibrio de clases, muchos ceros y pocos unos.
# grafico de barras distribucion de la variable binaria
ggplot(carsData_final, aes(x = factor(BajaDeprecacion))) +
geom_bar(fill = "darkred") +
geom_text(stat='count', aes(label=after_stat(count)), vjust=-0.5) +
labs(title = "Distribución de Baja Depreciación (0=Alta, 1=Baja)",
x = "Baja Depreciación",
y = "Conteo de Vehículos") +
theme_minimal()
Este desequilibrio valida la elección del F1-Score como
métrica de clasificación principal ya que la precisión simple nos
engañaría en los resultados.
Ahora vamos a utilizar un HeatMap para buscar la
multicolinealidad al observar la correlación entre las variables
predictoras. La existencia de correlaciones fuertes (valores cercanos a
∓1) justifica la aplicación de PCA para reducir la
redundancia y obtener variables ortogonales.
library(dplyr)
library(corrplot)
## corrplot 0.95 loaded
# seleccionar las variables numericas a correlacionar
vars_num_corr <- carsData_final %>%
select(Price, KM, antiguedad, CV, ratioPrecioKm) %>%
names()
# calcular la matriz de correlacion
matriz_cor <- cor(carsData_final %>% select(all_of(vars_num_corr)))
# visualizar la matriz como un Heatmap
corrplot(matriz_cor,
method = "color",
type = "upper",
addCoef.col = "black",
srt = 45,
tl.col = "black")
Podemos observar que CV muestra la correlación junto con
Price con un (0.69), esto confirma que un motor más potente
está directamente asociado a un precio mayor (coches de lujo/potentes).
La antiguedad muestra una correlación negativa fuerte con
Price con un (-0.53) que, valida el concepto de la
deprecación, a mayor antigüedad, menor es el precio. El KM
muestra una correlación negativa moderada con Price con un
(-0.30), lo cual indica que no es tan relevante para el precio que la
potencia o la edad del vehículo.
La asociación entre KM y antiguedad muestra
una correlación positiva moderada con un (0.48) pero simplemente indica
redundancia ya que los coches antiguos suelen tener más kilómetros que
los nuevos. El resto de las correlaciones indicadas son muy bajas y es
interesante porque sugiere que la potencia es una variable relativamente
independiente que aporta información sobre el precio y no va ligada a la
edad o el uso.
Para finalizar, utilizaremos Boxplots para visualizar la
dispersión del Price según las categorías clave esto
validará si la hipótesis de negocio es correcta (por ejemplo, si las
marcas de lujo tienen una mediana de precio mayor), lo cual apoya los
criterios usados para definir BajaDeprecacion.
# seleccionar las 10 marcas mas frecuentes para un grafico mas limpio
top_marcas <- carsData %>%
count(Brand) %>%
arrange(desc(n)) %>%
head(10) %>%
pull(Brand)
# generar boxplots: precio vs marca
carsData %>%
filter(Brand %in% top_marcas) %>%
ggplot(aes(x = reorder(Brand, Price, FUN = median), y = Price, fill = Brand)) +
geom_boxplot() +
scale_y_continuous(labels = scales::comma) +
labs(title = "Dispersión del Precio por Marca (Top 10)",
x = "Marca",
y = "Precio (€)") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "none")
La línea horizontal que divide cada caja representa la mediana. Para medianas bajas tenemos Opel, Ford, Seat y Renault, todas por debajo de los 20.000€ lo cual indica que representan el segmento más economico. Para medianas altas tenemos BMW que destaca claramente y se situa alrededor de los 20.000€ de mediana con la linea superior y confirma que las marcas premium mantienen un valor superior.
Marcas como Volkswagen o BMW tienen unas cajas mucho más largas, lo cual significa que tienen un rango de precios muy amplio y sugiere una gran cantidad de modelos o una mayor variabilidad de la deprecación. En cambio, marcas como Opel o Ford tienen cajas más cortas indicando que sus precios son más consistentes y se centran en un rango más estrecho.
Tenemos unos valores extremos, que son los puntos negros que se observan, estos extremos representan vehículos con precios muy altos para dicha marca. En el caso de Renault tenemos un valor extremo que se coloca por encima de los 60.000€ lo cual puede deberse a modelos deportivos específicos.
Para concluir con este apartado, podemos extraer que las marcas son un predictor fundamental del Precio y que la dispersón y el valor residual varían significativamente entre segmentos.
Para finalizar, calcularemos cuantos componentes debemos retener para tener una varianza acumulada del 90%.
# encontrar el numero de componentes para retener el 90%
componentes_k <- min(which(variancia_acumulada >= 0.90))
print(paste("Se necesitan", componentes_k, "componentes para explicar el 90% de la varianza."))
## [1] "Se necesitan 67 componentes para explicar el 90% de la varianza."
Para tomar la decisión analitica de cuantos componentes retener, se utiliza el criterio de la varianza acumulada, en la que se seleccionan los componentes suficientes para explicar el alto porcentaje de la varianza total. O bien, el criterio de Kaiser, en el que se retienen solo los componentes cuyo valor propio es superior a 1 y, precisamente, en el grafico la mayoría de componentes es inferior al 2%, por lo que es probable que muchos componentes cumplan ese criterio.
Debemos calcular la varianza acumulada para determinar el punto exacto. Asumiendo que el Scree Plot representa una matriz de 94 dimensiones, para explicar el 90% de la varianza se necesitarán, como se explica en la respuesta, 67 componentes para conseguir el 90% de la varianza total.
Hemos reducido en 26 dimensiones conservando el 90% de la información predictiva del dataset.
La primera parte del estudio analítico se ha completado aplicando CRISP-DM para preparar el dataset de precios de coches para el modelado de la PRA2. Las principales conclusiones han sido:
Limpieza robusta, eliminando 4797 registros duplicados, filtrado de coches de baja potencia potencialmente ciclomotores, eliminación de registros NA, tratamiento de Outliers, eliminación de columnas sin caracter predictivo, etc.
Se han creado métricas de negocio clave como
antiguedad o BajaDeprecacion.
Se ha validado que la distribución de Price es
cercana a la normal lo que simplifica la regresión. El mapa de calor de
correlación confirma que la potencia es el factor más relevante para el
precio.
Se ha aplicado el PCA y ha logrado reducir la alta dimensionalidad de 94 variables a solo 67 variables conservando el 90% de la varianza del dataset.