El siguiente informe tiene cómo 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 cuál 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?

path <- "used_cars.csv"

# Cargamos el archivo
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 Reducción de la dimensionalidad aplicada al Modelo

Al finalizar el preprocesado y la codificación One-Hot, hemos obtenido una matriz de 94 variables. Para garantizar que los modelos sean robustos y no sufran desajustes por exceso de dimensiones aplicamos PCA.

5.1 Selección de componentes para modelos supervisados

Calcularemos cuantos componentes debemos retener para tener una varianza acumulada del 90%, asegurando que la pérdida de información sea minima mientras simplificamos el modelo.

# 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.

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.


6 Resumen de la fase de preparación y transición

Con la aplicación del PCA, se ha completado la fase de preparación de datos aplicando CRISP-DM. Las principales conclusiones han sido:

Este dataset acondicionado es el que utilizaremos a continuación par alos algoritmos de segmentación no supervisada y los modelos de predicción supervisada.


7 Modelado analítico K-Means

Tras haber limpiado y preparado los datos en la fase anterior nuestro objetivo pasa a ser doble, primero segmentar el mercado de forma automática para entender los perfiles de vehículos existentes y el segundo construir modelos predictivos que nos permitan tasar un coche nuevo.

7.1 Obtención de agrupaciones

Para la segmentación, seleccionamos las variables numéricas clave de Price, KM, antiguedad y CV. Para ello usamos el algoritmo K-Means, este agrupa los datos calculando la distancia entre dos puntos y el centro de cada grupo. Aqui queremos hacer una comparativa para demostrar la importancia del escalado de datos.

Compararemos el algoritmo usando datos en “bruto” contra los datos “normalizados”.

library(cluster)
library(factoextra)
## Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
# selección de variables y escalado
vars_cluster <- carsData_final %>% select(Price, KM, antiguedad, CV)
vars_cluster_scaled <- scale(vars_cluster)

# K-means sobre datos originales
set.seed(123)
km_orig <- kmeans(vars_cluster, centers = 3, nstart = 25)

# K-means sobre datos normalizados
km_norm <- kmeans(vars_cluster_scaled, centers = 3, nstart = 25)

7.2 Medidas de calidad

Hemos seleccionado las variables y el escalado y hemos sacado dos algoritmos K-Means, el “bruto” y el “normalizado”. Ahora, utilizamos el método de la silueta para encontrar el número de clústeres donde los coches están más cerca de los suyos y más lejos de los otros grupos.

# K óptimo con datos normalizados
fviz_nbclust(vars_cluster_scaled, kmeans, method = "silhouette") +
  labs(title = "K óptimo según Silueta")

sil_norm <- silhouette(km_norm$cluster, dist(vars_cluster_scaled))
fviz_silhouette(sil_norm)
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## ℹ The deprecated feature was likely used in the factoextra package.
##   Please report the issue at <https://github.com/kassambara/factoextra/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
##   cluster size ave.sil.width
## 1       1  137          0.18
## 2       2  725          0.43
## 3       3  292          0.21

Una vez obtenidos los resultados, usamos el grafico de la Silueta donde un valor cercano a 1 indica que el coche está bien clasificado y un valor cercano a 0 está en la frontera entre ambos grupos.

7.3 Análisis de los resultados obtenidos

Con un tamaño de 725 coches, el clúster 2 tiene la silueta más alta (0.43), este grupo representa un modelo exitoso. Cuando la silueta es cercana a 0.5 indica que los objetos están bien clasificados y aquí es donde se encuentran los vehículos con una relación precio-antigüedad-potencia más estandarizada.

Con 292 vehículos, el clúster 3 tiene una silueta de 0.21, lo cual sugiere que, aunque comparten rasgos, hay solapamiento entre grupos y podrían ser coches que están sufriendo una deprecación acelerada o vehículos con kilometrajes elevados para su edad.

Por último, con 137 vehículos tenemos el clúsuter 1 con una silueta de 0.18. En mineria de datos, suele indicar que el algoritmo sufre para agrupar estos datos porque son muy heterogeneos. Podrian encontrarse aqui coches deportivos, de lujo o electricos ya que sus características extremas pueden no encajar bien con una media.

La baja silueta del clúster 1 justifica el uso del algorimto PAM , que al seleccionar un coche real como centro, deberia mejorar la representación de ese segmento de 137 vehículos que ahora está fragmentado.


8 Algoritmo K-medians (PAM)

8.1 Justificación del método

Tras los resultados de K-Means, observamos que aunque es efectivo para el segmento estándar, presenta difultades para agrupar vehículos del clúster 1.

En este apartado aplicamos el algoritmo PAM. Mientras K-Means utiliza un centroide (media aritmetica imaginaria), PAM utiliza un medoide (un registro real del dataset que representa el centro del grupo). Es mucho más robusto frente a valores extremos/outliers, como coches de gran potencia o precios elevados.

8.2 Aplicación de PAM con 3 clústeres

Utilizamos el mismo número de clústeres y los datos normalizados para asegurar una comparativa directa y justa con el ejercicio anterior.

library(cluster)

# PAM con k=3 sobre los datos normalizados
set.seed(123)
pam_res <- pam(vars_cluster_scaled, k = 3)

# identificacion de los medoides
print(pam_res$medoids)
##          Price         KM antiguedad         CV
## 297 -0.2535208 -0.3794199 -0.3275892 -0.3700941
## 812  0.7653299 -0.5661447 -0.5945652  0.5903750
## 955 -0.7715453  0.5928952  0.7403146 -0.3700941
# medida de calidad del agrupamiento
sil_pam <- silhouette(pam_res$cluster, dist(vars_cluster_scaled))
fviz_silhouette(sil_pam)
##   cluster size ave.sil.width
## 1       1  456          0.45
## 2       2  348          0.06
## 3       3  350          0.10

8.3 Comparación de resultados y conclusiones

Al analizar las métricas obtenidas con PAM, observamos que el clúster 1, tiene 456 vehículos, tiene una silueta de 0.45, lo cual indica que este grupo se mantiene como el más estable y coherente. Al usar el mdioide como centro, hemos logrado agrupar casi la mitad de la muestra en un segmento estándar definido.

Por el otro lado, los clústeres 2 y 3, con 348 y 350 vehículos respectivamente, tienen una silueta de 0.06 y 0.1, unas puntuaciones tan bajas que indican que hay un gran solapamiento en estos segmentos. Sugieren que en el mercado estándar, las combinaciones de CV, KM y Price son tan variadas que los coches no forman grupos compactos.


9 K-Means con métrica de distancia Manhattan

9.1 Justificación del cambio de métrica

Tanto en K-Means como en PAM, hemos utilizado por defecto la distancia euclidiana. Sin embargo, en nuestro dataset de coches, tenemos variables como KM o Precio que presentan distribuciones con colas largas o asimetrías.

En este ejercicio usaremos la distancia Manhattan, la cual aporta distintas cosas a la distancai euclediana:

  • A diferencia de la euclediana, la Manhattan no eleva las diferencias al cuadrado, por lo que no penaliza de forma tan extrema a los coches con valores muy altos en una sola variable.

  • En mercados de segunda mano, los factores de valor suelen sumarse de forma lineal. Manhattan mide la distancia como la suma de las diferencias absolutas de sus coordenadas, lo que puede ajustarse mejor a la realidad de como un comprador percine las diferencias entre vehículos.

9.2 Implementación con la librería flexclust

Para poder cambiar la familia de distancias, utilizaremos la librería flexclust, la cual nos permite definir tipos de centroides basados en medianas y distancias. En concreto usaremos kmedians con distancia L1, de esta forma creamos un modelo más resistente a los valores extremos de precio que los modelos basados en medias cuadráticas.

if (!require('flexclust')) install.packages('flexclust')
## Loading required package: flexclust
library(flexclust)

# aplicamos K-means usando la familia 'kmedians'
set.seed(123)
km_manhattan <- kcca(vars_cluster_scaled, k = 3, family = kccaFamily("kmedians"))

summary(km_manhattan)
## kcca object of family 'kmedians' 
## 
## call:
## kcca(x = vars_cluster_scaled, k = 3, family = kccaFamily("kmedians"))
## 
## cluster info:
##   size  av_dist max_dist separation
## 1  395 2.272474 11.18468   1.268590
## 2  293 2.198153 17.05633   1.586498
## 3  466 1.440581 15.80682   1.359565
## 
## convergence after 11 iterations
## sum of within cluster distances: 2212.997
# comparamos la distribución 
table(km_norm$cluster, clusters(km_manhattan))
##    
##       1   2   3
##   1 137   0   0
##   2 255  17 453
##   3   3 276  13

9.3 Comparativas y conclusiones

Tras aplicar el algoritmo con la métrica Manhattan, los estadisticos de los clústeres nos ofrecen una visión detallada de la cohesión interna.

Con 466 vehículos, el clúster 3, es el grupo más compacto. Su distancia media es de 1.44 lo cual indica que la métrica Manhattan ha logrado agrupar de forma muy eficiente al segmento estándar. Sin embargo, su distancia máxima de 15.8 revela que incluso en este grupo bueno hay coches que se desvian mucho del centro.

El segundo grupo, con 293 vehículos, tiene una distancia media de 2.19 con una maxima de 17.05. Esto confirma que hay vehículos con características tan únicas que la distancia los tolera dentro del grupo pero están muy alejados. Lo positivo es que su separación es la más alta, con un 1.59, lo cuál significa que este grupo de coches raros está bien diferenciado de los coches normales.

El clúster 1, con 395 vehículos, representa un gripo con un tamaño considerable. Su distancai media es de 2.27, la más alta de los tres grupos, lo cuál nos indica que los coches de este clúster son los más diferentes entre si. Tiene la separación más baja y signfica que está tocandose con los limites de otros grupos.


10 Algorimtos de densidad (DBSACN y OPTICS)

10.1 Justificación del modelo

Hasta ahora, hemos utilizado algoritmos que fuerzan cada coche a pertenecer a un grupo. Sin embargo, las distancais máximas de hasta 17.05 detectadas en el ejercicio anterior sugieren que hay vehículos que están aislados.

Aplicamos Density-Based Spatial Clustering of Applications with Noise o (DBSCAN) para gestionar, como su nombre indica, el ruido de los outliers y los marca como registros que no pertenecen a ningunn grupo. Además, puede encontrar clústeres con formas arbitrarias.

10.2 Determinación de parametros: eps y minPts

Para DBSCAN, necesitamos definir dos parámetros críticos:

  • minPts: El número mínimo de coches para formar un grupo, para el cuál usaremos 5 por ser un estándar para estos casos.

  • eps: El radio de vecinidad, el cuál determinaremos visualmente con un K-dist plot.

library(dbscan)
## 
## Attaching package: 'dbscan'
## The following object is masked from 'package:stats':
## 
##     as.dendrogram
# determinación de epsilon
kNNdistplot(vars_cluster_scaled, k = 5)
abline(h = 0.7, col = "red", lty = 2) 

# aplicación de DBSCAN
set.seed(123)
db_res <- dbscan(vars_cluster_scaled, eps = 0.7, minPts = 5)

# visualización de resultados
print(db_res)
## DBSCAN clustering for 1154 objects.
## Parameters: eps = 0.7, minPts = 5
## Using euclidean distances and borderpoints = TRUE
## The clustering contains 3 cluster(s) and 74 noise points.
## 
##    0    1    2    3 
##   74 1070    5    5 
## 
## Available fields: cluster, eps, minPts, metric, borderPoints

Se han detectado 74 coches que no tienen vecinos similares en un radio de 0.7. Estos son los responsables de las distancias máximas de 17.05 que hemos visto antes. Al aislarlo como “ruido”, dejamos de formzar su inclusión en grupos donde solo generaban distorsión y bajaban la silueta.

El clúster mayoritario, con 1070 vehículos, nos revela que el mercado de coches usados en España no está tan fragmentado como sugeria K-Means, existe una densidad continua que abarca la gran mayoria de los coches estándar. Por lo que K-Means estaba dividiendo en tres partes artificiales, DBSCAN lo ve como un cuerpo unico.

Hemos encontrado dos grupos minusculos de solo 5 coches cada uno que indican la existencia de configuraciones técnicas extremadamente especificas que son los suficientemente similares entre si como para formar una isla de densidad.

10.3 Aplicación de OPTICS

Como DBSCAN es muy sensible al valor de eps, utilizamos OPTICS, que es una extensión que permite visualizar la estructura de densidad sin fijar un radio estricto, ideal para entender clústeres de diferentes densidades.

# aplicación de OPTICS
opt_res <- optics(vars_cluster_scaled, minPts = 5)

# visualización del diagrama de alcanzabilidad 
plot(opt_res)

10.4 Comparativa y resultados obtenidos

La transición de los modelos de centros a los de densidad han mostrado que el mercado español de vehículos usados no está fragmentado en grupos de un mismo tamaño, sino que tiene un nucleo masivo y denso de 1070 vehículos y una serie de casos atopicos aislados. Esta estructura sugiere que los modelos supervisados de predicción de precio funcionarán muy bien para la mayoría de los casos, pero tendran dificultades con los vehículos identificados como ruido.


11 Selección de muestra de entrenamiento y test

11.1 Procedimiento de partición

Para validar la capacidad predictiva de nuestros modelos, dividimos el dataset en dos subconjuntos independientes. Un 80% para entrenamiento y un 20% para los tests.

set.seed(123)
# creamos el índice de participación basado en la variable Price
trainIndex <- createDataPartition(carsData_final$Price, p = 0.8, list = FALSE)

train_set <- carsData_final[trainIndex, ]
test_set  <- carsData_final[-trainIndex, ]

11.2 Justificación de las proporciones

La elección del 80/20 se fundamenta en el volumen de datos disponible. Contar con un 80% permite que los algoritmos de árboles de decisión tengan suficientes ejemplos para aprender la variabilidad de las marcas y potencias, mientras que el 20% restante es un tamaño muestral estadísticamente sólido para evaluar el error de predicción sin caer en el sobreajuste.


12 Árbol de decision

12.1 Entrenamiento y generación de reglas

Entrenamos un árbol de decisión para predecir la variable continua Price. Elárbol busca particiones que maximicen la homogeneidad del precio en los nodos resultantes.

library(rpart)
library(rpart.plot)

# usamos todas las variables predictoras procesadas
fit_full <- rpart(Price ~ ., data = train_set %>% select(-BajaDeprecacion, -ratioPrecioKm), method = "anova", control = rpart.control(cp = 0, minsplit = 20))

# identificamos el valor de CP 
plotcp(fit_full)

best_cp <- fit_full$cptable[which.min(fit_full$cptable[,"xerror"]), "CP"]

# aplicamos la poda
fit_pruned <- prune(fit_full, cp = best_cp)

# visualización del modelo final optimizado
rpart.plot(fit_pruned, type = 2, extra = 1, under = TRUE, faclen = 0, cex = 0.8, main = "Árbol de Regresión: Predicción de Precio")

12.2 Extracción de reglas y matriz de error

El modelo genera reglas lógicas. Para evaluar la capacidad predictiva en una variable cuantitativa, calculamos MAE (Error Medio Absoluto) y el RMSE (Error Cuadrático Medio de la Raíz).

# predicción sobre el conjunto de test
preds <- predict(fit_pruned, test_set)

# métricas de error
mae <- mean(abs(test_set$Price - preds))
rmse <- sqrt(mean((test_set$Price - preds)^2))

12.3 Comparativa: con y sin poda

Tras aplicar el modelo sobre el conjunto de test obtenemos las métricas de error:

print(paste("MAE:", round(mae, 2), "€"))
## [1] "MAE: 2998.46 €"
print(paste("RMSE:", round(rmse, 2), "€"))
## [1] "RMSE: 4659.09 €"
  • MAE: El error medio absoluto nos indica que, de media, nuestras predicciones se desvian unos 3000€ del precio real de oferta. Considerando que el rango de precios en nuestro dataset es amplio, una desviación tal como esta indica que el modelo captura correctamente la jerarquia de valor del mercado.

  • RMSE: Es significativamente mayor que el MAE. Cuando pasa esto, significa que el modelo comete errores grandes en unos pocos casos especificos. Los 74 coches identificados como outliers son los que disparan el RMSE ya que el árbol no tiene suficientes ejemplos para tasarlos correctamente.

Para un concesionario es un modelo perfecto para un cribado inicial. Permite automatizar el 90% del stock con un error controlado, dejando la revision manual a aquellos vahiculos donde el modelo muestra incertidumbre.


13 Modelo supervisado alternativo

13.1 Entrenamiento del modelo y reducción del error

Dado que el árbol de decisión simple es sensible a los cambios en los datos y se ve afectado por los valores extremos, aplicamos Random Forest. Al promediar las predicciones de 200 árboles diferentes, esperamos reducir ese RMSE de 4659€.

library(randomForest)
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
## 
## Attaching package: 'randomForest'
## The following object is masked from 'package:dplyr':
## 
##     combine
## The following object is masked from 'package:ggplot2':
## 
##     margin
set.seed(123)

# entrenamos el bosque 
rf_model <- randomForest(Price ~ ., 
                         data = train_set %>% select(-BajaDeprecacion, -ratioPrecioKm), 
                         ntree = 200, 
                         importance = TRUE)

# evaluación en Test
pred_rf <- predict(rf_model, test_set)
rmse_rf <- sqrt(mean((test_set$Price - pred_rf)^2))

print(paste("RMSE Random Forest:", round(rmse_rf, 2), "€"))
## [1] "RMSE Random Forest: 3411.07 €"

13.2 Entrenamiento del modelo y reducción del error

Una de las mayores ventajas de este modelo es que nos permite ver que factores pesan más en la formación del precio, incluyendo los nuevos combustibles.

varImpPlot(rf_model, main = "Importancia de las Variables en la Tasación")

El gráfico obtenido mediante varImpPlot nos permite jerarquizar los factores que realmente mueven el mercado. A diferencia del árbol de decisión, el Random Forest evalúa la importancia de forma global:

  • Las variables dominantes son CV, antiguedad y Year aparecen en la parte superior del gráfico. Esto confirma la hipotesis de deprecación temporal. También podemos observar que la Trasmission.MANUAL aparece en el Top 5 de importancia, lo que indica que el tipo de caja de cambios es diferenciador de precio crítico.

  • La gasolina y el hibrido son los combustibles que más aportan a la precisión del modelo. Las etiquetas ambientales Sticker.B y Sticker.ECO aparecen con una importancia muy superior a muchas marcas, lo que refleja que en 2026, la capacidad de circular por zonas de bajas emisiones es un factor de valor directo.

  • Mercedes y Porsche son las marcas que más mueven la aguja del precio, situandose en la parte alta de la tabla, lo que confirma su estatus de activos con dinámicas de precio diferenciadas del resto de marcas generalistas como Seat u Opel.

13.3 Validación visual del modelo

Para finalizar la evaluación del Random Forest, realizamos una validación visual comparando valores reales del conjunto de test frente a las predicciones del algoritmo.

library(ggplot2)

# dataframe con los valores reales y las predicciones del Random Forest
resultados_val <- data.frame(
  Real = test_set$Price,
  Predicho = predict(rf_model, test_set)
)

# generamos la grafica de comparacion
ggplot(resultados_val, aes(x = Real, y = Predicho)) +
  geom_point(alpha = 0.5, color = "darkblue") + # Puntos semi-transparentes para ver densidad
  geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed", size = 1) + # Línea ideal
  labs(title = "Validación del modelo: Precio Real vs. Predicho",
       subtitle = "La línea roja representa la predicción perfecta",
       x = "Precio Real (€)",
       y = "Precio Predicho (€)") +
  theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

En la gráfica de dispersión se observa que la mayoría de los vehículos se agrupan estrechamente a lo largo de la diagonal roja, lo que confirma la alta precisión del Random Forest para el segmento de mercado estándar (entre 10.000€ y 30.000€).

Sin embargo, a medida que el precio aumenta hacia el segmento de lujo (>35.000€), la dispersión es mayor. Esto ratifica nuestras conclusiones previas del análisis no supervisado: la presencia de los puntos de ruido identificados por DBSCAN y la menor densidad de datos en modelos de alta gama dificultan una predicción exacta en los extremos del mercado, donde factores de exclusividad pesan más que las variables técnicas estandarizadas.


14 Limitaciones y conclusiones finales

En este estudio hemos modelado con éxito la estructura de precios de mercado de vehículos de ocasión en España. Tras la aplicación de técnicas de minería de datos supervisadas y no supervisadas.

14.1 Limitaciones del dataset y análisis de riesgos

A pesar de la robustez metodológica, el modelo enfrenta limitaciones como:

  • Omisión de variables cualitativas: no se toman en cuenta factores como el estado de la carrocería, desgaste interior, historial de revisiones, etc. Esto introduce un riesgo de error en la tasación que los algoritmos no son capaces de comprender.

  • Riesgo de sobreajuste en nichos: el uso de estos modelos para tasar vehículos de lujo conlleva un riesgo de infravaloración ya que estos activos no se rigen por el mercado masivo.

  • Sesgo de ofertas: existe el riesgo de que el precio real de cierre sea inferior tras la negociación en la venta del vehiculo.

14.2 Conclusiones finales

La investigación confirma que la potencia (CV) es el factor con mayor peso en la formación del precio, seguido de la antiguedad.

Hemos descubierto que el mercado no es tan caotico como parece y existe un grupo de más de 1000 vehículos estándar altamente predecible. La aplicación de Manhattan y DBSCAN ha sido clave para el entendimiento de que los modelos de centros fragmentan la realidad de forma artificial al hacer una división sesgada de clústeres.

El Random Forest se consolida como la herramienta de producción ideal, logrando reducir el RMSE frente al árbol de decisión simple. Esto demuestra que la interacción entre factores como el tipo de etiqueta que tienen y la transmisión requiere de modelos de ensamble para capturar matices que un modelo lineal o un árbol simple ignoran.

En conclusión, el modelo es altamente fiable para automatizar el 90% de las tasaciones de un concesionario generalista, permitiendo optimizar recursos y centrar la revisión humana experta únicamente en los casos identificados como ruido o incertidumbre.


15 Interacción con la IA

Para la realización de la PR2, se ha utilizado la IA generativa para estas categorías de apoyo: