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.

1. Establecer un objetivo analítico

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:

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.


2. Seleccionar el juego de datos y verificarlos

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)
Data summary
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:


3. Preprocesado de los datos

3.1. Eliminación de columnas intranscendentes

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

3.2. Gestión de valores nulos

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)
Data summary
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.

3.3. Tratamiento de Outliers

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.

3.4. Eliminación de registros duplicados

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.

3.5. Ingeniería de características

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
)

3.6. Codificación categorica

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.

3.7. Escalado y preparación de la matriz de predictores

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.


4. Análisis exploratorio

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.


5. Aplicación del Análisis de Componentes Principales

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.


6. Reproducibilidad

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: