A veces en listas falta información que se puede inferir con cierta certeza a partir de otras columnas. En este trabajo la idea es poder acercarse a completarlas de una forma práctica.

Librerías (las de siempre más OneR)

library(tidyverse)
library(ggplot2)
library(OneR)
library(knitr)

¿Qué es OneR? Es una librería de clasificación simple para machine learning que implementa un árbol de decisión de un nivel. Si suena raro no se preocupen, van a ver que hace y sale solo (espero).

Vamos a lo concreto. Tengo una lista de autos usados en venta a través de un sitio de internet. Veamos:

datosautos <- read.csv(file = "datosml.csv", stringsAsFactors = FALSE)
kable(head(datosautos))
X id title price currency_id listing_type_id ENGINE_DISPLACEMENT DOORS FUEL_TYPE KILOMETERS MODEL TRIM VEHICLE_YEAR cilindrada
1 MLA850265335 Chevrolet Prisma Ls 2011 365000 ARS gold_premium 1389 4 Nafta 155000 Prisma 1.4 Ls 92cv 2011 1.4
2 MLA843044678 Chevrolet Prisma 1.4 Ls 92cv 2011 358000 ARS gold 1389 4 Nafta 98000 Prisma 1.4 Ls 92cv 2011 1.4
3 MLA840209496 Chevrolet Prisma Ls 1.4 Anticipo $ 145 Contado $ 275 450000 ARS gold 1389 4 Nafta 130000 Prisma 1.4 Ls 92cv 2011 1.4
4 MLA840017595 Chevrolet Prisma 1.4 Ls 92cv 2011 390000 ARS silver 1389 4 Nafta 108000 Prisma 1.4 Ls 92cv 2011 1.4
5 MLA851712209 Chevrolet Prisma 1.4 Ls 92cv 2011 345000 ARS free 1389 4 Nafta 90000 Prisma 1.4 Ls 92cv 2011 1.4
6 MLA845309211 Chevrolet Prisma Lt Imperdible 379000 ARS gold 1389 4 Nafta 1111111 Prisma 1.4 Lt 92cv 2011 1.4

Hasta acá todo bien salvo que por alguna razón quiero usar el dato de la cilindrada y no me gusta perder tantos registros simplemente sacando a los que no tienen el valor.

kable(datosautos %>% group_by(cilindrada) %>% summarize(casos= n())%>%filter(is.na(cilindrada)==TRUE))
cilindrada casos
NA 3140

Que falten o estén mal datos es común en listas estructuradas con muchos inputs manuales. Pero por otra parte sabemos que las configuraciones básicas de los autos tal cual salen de fábrica son dentro de todo regulares así que voy a considerar que el dato de la cilindrada es recuperable en cierta forma.

Acomodo un poco los datos. Si bien ocupan categorías distintas no hay diferencias mecánicas significativas entre los motores a nafta y gas para lo que estoy observando entonces podemos fusionar ambas categorías (“Diesel” y “Nafta y/o GNC”). Para probar un par de cosas voy a crear unas columnas en base a las demás, más abajo está explicado.

datosautos <- datosautos %>% mutate(
  combustible = case_when(
    FUEL_TYPE == "Diésel" ~ "Diesel",
    TRUE ~ "Nafta y/o GNC"
  )
)
datosautos$cilindrada <- paste0(datosautos$cilindrada, ' litros')
datosautos$mod_anio_comb <- paste0(datosautos$MODEL, ' ', datosautos$VEHICLE_YEAR ,' ',datosautos$combustible)
datosautos$mod_comb <- paste0(datosautos$MODEL, ' ', datosautos$combustible)
datosautos$mod_comb_ptas <- paste0(datosautos$MODEL, ' ',datosautos$combustible, ' ', datosautos$DOORS, ' puertas')

OneR trabaja sobre una sola variable para detectar la que mejor se ajusta. Como regla general trata de predecir el valor de la última columna en base a alguna de las otras, por eso el select con cilindrada al final.

datosautosmodel <- datosautos %>% select(DOORS, FUEL_TYPE, MODEL, VEHICLE_YEAR, cilindrada)%>%filter(cilindrada != "NA litros")
modelo <- OneR(datosautosmodel, verbose = TRUE)
## 
##     Attribute    Accuracy
## 1 * MODEL        76.01%  
## 2   FUEL_TYPE    45.14%  
## 3   DOORS        43.47%  
## 4   VEHICLE_YEAR 42.06%  
## ---
## Chosen attribute due to accuracy
## and ties method (if applicable): '*'
print(modelo)
## 
## Call:
## OneR.data.frame(x = datosautosmodel, verbose = TRUE)
## 
## Rules:
## If MODEL = 207 Compact           then cilindrada = 1.4 litros
## If MODEL = Classic               then cilindrada = 1.4 litros
## If MODEL = Clio                  then cilindrada = 1.1 litros
## If MODEL = Corolla               then cilindrada = 1.8 litros
## If MODEL = Corsa Classic         then cilindrada = 1.6 litros
## If MODEL = Duster                then cilindrada = 1.6 litros
## If MODEL = Ecosport              then cilindrada = 1.6 litros
## If MODEL = Etios                 then cilindrada = 1.5 litros
## If MODEL = Fiesta Kinetic Design then cilindrada = 1.6 litros
## If MODEL = Fluence               then cilindrada = 2 litros
## If MODEL = Focus III             then cilindrada = 2 litros
## If MODEL = Fox                   then cilindrada = 1.6 litros
## If MODEL = Gol                   then cilindrada = 1.6 litros
## If MODEL = Hilux                 then cilindrada = 2.5 litros
## If MODEL = Ka                    then cilindrada = 1.5 litros
## If MODEL = Kangoo                then cilindrada = 1.6 litros
## If MODEL = Logan                 then cilindrada = 1.6 litros
## If MODEL = Meriva                then cilindrada = 1.8 litros
## If MODEL = Palio                 then cilindrada = 1.4 litros
## If MODEL = Partner               then cilindrada = 1.6 litros
## If MODEL = Prisma                then cilindrada = 1.4 litros
## If MODEL = Ranger                then cilindrada = 3.2 litros
## If MODEL = Sandero               then cilindrada = 1.6 litros
## If MODEL = Siena                 then cilindrada = 1.4 litros
## If MODEL = Spin                  then cilindrada = 1.8 litros
## If MODEL = Suran                 then cilindrada = 1.6 litros
## If MODEL = Tiida                 then cilindrada = 1.8 litros
## If MODEL = Voyage                then cilindrada = 1.6 litros
## 
## Accuracy:
## 12179 of 16023 instances classified correctly (76.01%)

Por lejos el modelo (MODEL) es el que tiene mejor poder de predicción con el 76% pero los otros tres también pueden aportar. Sería lógico que si los modelos diesel generalmente se ofrecen en otra cilindrada que los nafteros esto se pudiera ver en la combinación de ambas características. Para esto voy a usar un “truco” que es considerar como modelos separados a las versiones diesel y nafteras/a gas de cada auto. Por ejemplo en vez de “Corsa” tengo “Corsa Diesel” y “Corsa Nafta y/o GNC”. La implementación del mismo está a cargo de la variable mod_comb que une ambas características. Pero eso más tarde, por ahora sigamos solamente con el modelo.

No me enorgullece sacar así (directamente) la info del modelo. Quiero creer que hay una forma mejor de hacer esto así que si quieren darme una mano más que bienvenidos como siempre. Pero funciona, eso si.

reglas <- stack(modelo$rules)
reglas$MODEL <- reglas$ind
reglas <- reglas %>% select(-c(ind))
datosautosmodel <- left_join(datosautosmodel, reglas)

Armo una variable sobre si los datos coinciden o no, acumulo los porcentajes por modelo y los muestro en un gráfico.

Voy a dejar tres líneas verticales en los gráficos para referencia

Mientras más se acerque al 100% de casos mejor es la predicción.

datosautosmodel <- datosautosmodel %>% mutate(
  coincide = ifelse(cilindrada==values,1,0)
)
porcentajepormodelos <- datosautosmodel %>% filter(cilindrada != "NA Litros") %>%
    group_by(MODEL) %>% 
    summarise(
        porcaciertos = round(mean(coincide)*100,2),
)
porcentajepormodelos <- porcentajepormodelos[order(-porcentajepormodelos$porcaciertos),]
graficoporcentaje <- ggplot(porcentajepormodelos, aes(y=MODEL, x=porcaciertos))+
  geom_point() + 
  scale_y_discrete(limits = porcentajepormodelos$MODEL)+
  labs(title = "Porcentaje de aciertos por modelo", x = "Porcentaje de aciertos", y = "Modelo de vehículo") +
  geom_vline(xintercept = 50, linetype="dashed", 
                color = "red", size=0.8)+
  geom_vline(xintercept = 80, linetype="dashed", 
                color = "blue", size=0.8)+
  geom_vline(xintercept = 95, linetype="dashed", 
                color = "green", size=0.8)
graficoporcentaje

Considerando que más de la mitad de los modelos están arriba del 50% podría pasar así como está y agregarle precisión a la muestra. Aunque la verdad no me gusta que modelos muy presentes en la lista estén más cerca del 50% que del 100%. Veamos si se puede mejorar el modelo inicial.

Probamos entonces lo mismo pero seleccionamos también mod_comb que incluye el modelo y el tipo de combustible.

datosautosmodel <- datosautos %>% select(DOORS, FUEL_TYPE, MODEL, VEHICLE_YEAR, mod_comb, cilindrada)%>%filter(cilindrada != "NA litros")
modelo <- OneR(datosautosmodel, verbose = TRUE)
## 
##     Attribute    Accuracy
## 1 * mod_comb     79.09%  
## 2   MODEL        76.01%  
## 3   FUEL_TYPE    45.14%  
## 4   DOORS        43.47%  
## 5   VEHICLE_YEAR 42.06%  
## ---
## Chosen attribute due to accuracy
## and ties method (if applicable): '*'

No sólo la detecta si no que incrementa el nivel de precisión.

Graficamos para ver mejor la comparación

datosautoscomp <- datosautos %>% select(mod_comb, cilindrada)
datosautosna <- datosautoscomp 
datosautoscomp <- datosautoscomp %>% filter(cilindrada != "NA litros")
reglas <- stack(modelo$rules)
reglas$mod_comb <- reglas$ind
reglas <- reglas %>% select(-c(ind))
datosautosmodel2 <- left_join(datosautoscomp, reglas)
datosautosmodel2 <- datosautosmodel2 %>% mutate(
  coincide = ifelse(cilindrada==values,1,0)
)
porcentajepormodelos <- datosautosmodel2 %>% filter(cilindrada != "NA Litros") %>%
    group_by(mod_comb) %>% 
    summarise(
        porcaciertos = round(mean(coincide)*100,2),
)
porcentajepormodelos <- porcentajepormodelos[order(-porcentajepormodelos$porcaciertos),]
graficoporcentaje <- ggplot(porcentajepormodelos, aes(y=mod_comb, x=porcaciertos))+
  geom_point() + 
  scale_y_discrete(limits = porcentajepormodelos$mod_comb)+
  labs(title = "Porcentaje de aciertos por modelo y combustible", x = "Porcentaje de aciertos", y = "Modelo y combustible de vehículo")+
  geom_vline(xintercept = 50, linetype="dashed", 
                color = "red", size=0.8)+
  geom_vline(xintercept = 80, linetype="dashed", 
                color = "blue", size=0.8)+
  geom_vline(xintercept = 95, linetype="dashed", 
                color = "green", size=0.8)
graficoporcentaje

Si bien se complica la lectura por la cantidad de modelos y combustible hay un avance en la cobertura de casos. Me preocupan los casos con muy pocos aciertos. Los últimos en la tabla de porcentaje de aciertos:

kable(tail(porcentajepormodelos))
mod_comb porcaciertos
Ecosport Diesel 57.78
Hilux Diesel 54.27
207 Compact Nafta y/o GNC 52.12
Ranger Diesel 51.59
207 Compact Diesel 48.15
Ka Nafta y/o GNC 41.14

Personalmente no sabía que había 207 Compact gasolero, me estoy enterando a medida que escribo. Y si me preocupa que tres autos nafteros con buen volumen de ventas como el Compact, el Corsa Classic y el Ka estén ahí. De los que están en NA debe ser por poca cantidad de casos. Todo eso lo compruebo ahora mismo.

Ka: Mucha variación y encima bien distribuida entre las diferentes motorizaciones.

kable(datosautos %>%filter(mod_comb == "Ka Nafta y/o GNC") %>% group_by(mod_comb, cilindrada)  %>% summarize(casos = n()))
mod_comb cilindrada casos
Ka Nafta y/o GNC 1 litros 237
Ka Nafta y/o GNC 1.3 litros 1
Ka Nafta y/o GNC 1.5 litros 325
Ka Nafta y/o GNC 1.6 litros 227
Ka Nafta y/o GNC NA litros 36

207 Compact Diesel: Algo parecido, variación pareja entre las versiones 1,4 y 1,9.

kable(datosautos %>%filter(mod_comb == "207 Compact Diesel") %>% group_by(mod_comb, cilindrada)  %>% summarize(casos = n()))
mod_comb cilindrada casos
207 Compact Diesel 1.4 litros 13
207 Compact Diesel 1.9 litros 9
207 Compact Diesel 2 litros 5
207 Compact Diesel NA litros 85

Ranger Diesel: Otra vez lo mismo. Por como fue planteado el algoritmo mientras mayor sea el rango de motorizaciones dentro de un modelo para un determinado combustible más le va a costar hacer la aproximación.

kable(datosautos %>%filter(mod_comb == "Ranger Diesel") %>% group_by(mod_comb, cilindrada)  %>% summarize(casos = n()))
mod_comb cilindrada casos
Ranger Diesel 2.2 litros 206
Ranger Diesel 2.8 litros 48
Ranger Diesel 3 litros 249
Ranger Diesel 3.2 litros 536
Ranger Diesel NA litros 190

Para ilustrar como se ve del otro lado voy a tomar tres que dieron alto. Todos tienen una sola cilindrada, el resto sólo habría que llenar los NA.

head(porcentajepormodelos)
## # A tibble: 6 x 2
##   mod_comb                            porcaciertos
##   <chr>                                      <dbl>
## 1 Corolla Diesel                               100
## 2 Corsa Classic Diesel                         100
## 3 Fiesta Kinetic Design Nafta y/o GNC          100
## 4 Fox Diesel                                   100
## 5 Fox Nafta y/o GNC                            100
## 6 Hilux Nafta y/o GNC                          100
kable(datosautos %>%filter(mod_comb == "Corolla Diesel") %>% group_by(mod_comb, cilindrada)  %>% summarize(casos = n()))
mod_comb cilindrada casos
Corolla Diesel 2 litros 6
Corolla Diesel NA litros 8
kable(datosautos %>%filter(mod_comb == "Fiesta Kinetic Design Nafta y/o GNC") %>% group_by(mod_comb, cilindrada)  %>% summarize(casos = n()))
mod_comb cilindrada casos
Fiesta Kinetic Design Nafta y/o GNC 1.6 litros 914
Fiesta Kinetic Design Nafta y/o GNC NA litros 19
kable(datosautos %>%filter(mod_comb == "Fox Nafta y/o GNC") %>% group_by(mod_comb, cilindrada)  %>% summarize(casos = n()))
mod_comb cilindrada casos
Fox Nafta y/o GNC 1.6 litros 411
Fox Nafta y/o GNC NA litros 97

Seguimos. Vieron que teníamos las variables para cruzar, ya probamos modelo, combustible y podríamos agregar puertas. Al igual que la anterior tenemos la columna mod_comb_ptas que es algo como “Corsa Diesel 4 puertas”.

datosautoscomp <- datosautos %>% select(mod_comb_ptas, cilindrada)
datosautosna <- datosautoscomp 
datosautoscomp <- datosautoscomp %>% filter(cilindrada != "NA litros")
modelo <- OneR(datosautoscomp, verbose = TRUE)
## 
##     Attribute     Accuracy
## 1 * mod_comb_ptas 82.92%  
## ---
## Chosen attribute due to accuracy
## and ties method (if applicable): '*'
reglas <- stack(modelo$rules)
reglas$mod_comb_ptas <- reglas$ind
reglas <- reglas %>% select(-c(ind))
datosautosmodel2 <- left_join(datosautoscomp, reglas)
datosautosmodel2 <- datosautosmodel2 %>% mutate(
  coincide = ifelse(cilindrada==values,1,0)
)
porcentajepormodelos <- datosautosmodel2 %>% filter(cilindrada != "NA Litros") %>%
    group_by(mod_comb_ptas) %>% 
    summarise(
        porcaciertos = round(mean(coincide)*100,2),
)
porcentajepormodelos <- porcentajepormodelos[order(-porcentajepormodelos$porcaciertos),]
graficoporcentaje <- ggplot(porcentajepormodelos, aes(y=mod_comb_ptas, x=porcaciertos))+
  geom_point() + 
  scale_y_discrete(limits = porcentajepormodelos$mod_comb_ptas)+
  labs(title = "Porcentaje de aciertos por modelo, combustible y cantidad de puertas", x = "Porcentaje de aciertos", y = "Modelo, combustible y puertas de vehículo")+
  geom_vline(xintercept = 50, linetype="dashed", 
                color = "red", size=0.8)+
  geom_vline(xintercept = 80, linetype="dashed", 
                color = "blue", size=0.8)+
  geom_vline(xintercept = 95, linetype="dashed", 
                color = "green", size=0.8)
graficoporcentaje

Ahora si tenemos un piso del 50%: cada dato que agreguemos de esta forma va a ser mejor estadísticamente que tirar una moneda al aire. Esto sin contar de que modelo hablamos, no es lo mismo una precisión del 60% (por decir algo) para un modelo con pocos o con muchos casos faltantes.

Veamos justamente que modelos están más complicados con los casos de NA (o sea sin registro de cilindrada).

autosstats <- datosautos %>% filter(cilindrada == "NA litros")%>% group_by(mod_comb_ptas)%>% summarize(casos = n())
autosstats <- autosstats[order(-autosstats$casos),]
head(autosstats)
## # A tibble: 6 x 2
##   mod_comb_ptas                         casos
##   <chr>                                 <int>
## 1 207 Compact Nafta y/o GNC 5 puertas     235
## 2 Hilux Diesel 4 puertas                  231
## 3 Corsa Classic Nafta y/o GNC 4 puertas   162
## 4 Ranger Diesel 4 puertas                 139
## 5 Suran Nafta y/o GNC 5 puertas           119
## 6 Sandero Nafta y/o GNC 5 puertas         107

Recordemos la lista de las predicciones.

tail(porcentajepormodelos)
## # A tibble: 6 x 2
##   mod_comb_ptas                       porcaciertos
##   <chr>                                      <dbl>
## 1 Ranger Diesel 2 puertas                     52.2
## 2 Ka Nafta y/o GNC 3 puertas                  51.1
## 3 207 Compact Diesel 4 puertas                50  
## 4 Clio Diesel 3 puertas                       50  
## 5 Hilux Diesel 2 puertas                      50  
## 6 207 Compact Nafta y/o GNC 5 puertas         49.2

Es una mala combinación que algo aparezca en estas dos listas al mismo tiempo y el 207 Compact Naftero de 5 puertas aparece en ambas. Sacando ese (y por menos de un punto) del resto como mínimo podemos decir que la estimación arranca en 50% y sube rápidamente a lo largo del rango de modelos a evaluar.

Por último queda implementar todo esto. Dividimos el dataframe original en dos partes, el que tiene los datos y los que están en NA.

datosautosna <- datosautos %>% filter(cilindrada == "NA litros")
datosautoscil <- datosautos %>% filter(cilindrada != "NA litros")

Por conveniencia al que tiene NA le vamos a sacar la columna de cilindrada y las demás, esto último también para el otro data frame. Recuerden que vamos a usar el mod_comb_ptas para trabajar por eso no lo sacamos.

datosautoscil <- datosautoscil %>% select(-c( mod_anio_comb, mod_comb))
datosautosna <- datosautosna %>% select(-c(cilindrada, mod_anio_comb, mod_comb))

Unimos los datos del modelo con los del data frame que está en NA

datosautosna <- left_join(datosautosna, reglas, by = "mod_comb_ptas")

Movemos algunas cosas (no me anduvo el rename directo, fue)

datosautosna$cilindrada <- datosautosna$values
datosautosna$values <- NULL

Y finalmente unimos los frames de nuevo

datosautosrec <- bind_rows(datosautosna, datosautoscil)

Todavía quedan algunos NA pero bajaron de 3100 a 81.

kable(datosautosrec %>% group_by(cilindrada) %>% summarize(casos= n())%>%filter(is.na(cilindrada)==TRUE))
cilindrada casos
NA 81

Si uno revisa estos casos hay algunos que están mal redactados (nunca vi un Palio de 6 puertas) pero esas son cuestiones para otro día. Por ahora completamos 3059 casos con un 82,9% de precisión, nada mal para extrapolar info a través de tres variables multiplicadas en una y asistido por OneR. Una forma más elegante de hacer el análisis en etapas sería con un árbol de decisiones que hace algo parecido a OneR pero en múltiples instancias. Eso nos evitaría por ejemplo probar todos los autos con cada combinación de combustible y carrocería como hicimos acá si no que detectaría por ejemplo modelos con un único motor independientemente de las otras características y los podría agregar automáticamente en una sola regla. Ejemplo, si todos los modelos Gol (por decir algo) tienen motor 1.6 litros no hace falta entrar en la distinción si son nafta, diesel o el número de puertas. Mientras menos reglas para alcanzar la misma o mayor precisión mejor.

Notar que dejé pasar el elefante en el ascensor que es la extracción de características a través de los campos descriptivos, sería la próxima fase del análisis. Ya que estamos esto es algo que surgió en el marco de otro proyecto en el que estoy trabajando sobre análisis de uso de automóviles, me sirve para ir teniendo más información y chequear la existente.

Cualquier idea o comentario es bienvenido, a darle duro que es la única forma de aprender :)