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