En esta sección, realizaremos un análisis exploratorio inicial de los datos filtrados para Portugal (wishlist_pt, damage_pt, customer_vehicle_pt) y del dataset global de vehículos (vehicle_data). El objetivo es entender la estructura, el contenido, la calidad de los datos y detectar posibles problemas o patrones interesantes a primera vista.
2.1wishlist_pt
En esta sección, exploraremos en detalle el conjunto de datos wishlist_pt, que contiene información sobre los vehículos deseados por los clientes en Portugal.
2.1.1 Estructura, distribución y nulos
# Función auxiliar para resumir NAsresumen_nulos <-function(dataframe) { # Calcular numero y % de NAs na_counts <-colSums(is.na(dataframe)) na_perc <-round(100* na_counts /nrow(dataframe), 2) na_summary <-data.frame(NA_number = na_counts,NA_percentage = na_perc,stringsAsFactors =FALSE )return(na_summary)}# Estructuraglimpse(wishlist_pt)
No hay nulos y los datos parecen de buena calidad. Los tipos son adecuados y las distribuciones tienen sentido, salvo el máximo de baumuster que es extrañamente alto. Tiene pinta de que se ha metido un dígito de más al introducir ese baumuster concretamente.
# A tibble: 22 × 8
customer_id country_code vehicle_name baumuster baumuster_4 wish_create_date
<chr> <chr> <chr> <dbl> <dbl> <date>
1 5ef5e2871ef… PT G 450 d 4653101 4653 2024-12-27
2 6cae494bf65… PT G 450 d 4653101 4653 2025-02-18
3 adbd3055d67… PT G 450 d 4653101 4653 2024-09-11
4 7c1b1bae677… PT G 450 d 4653101 4653 2024-12-06
5 ad8a2b7b096… PT G 450 d 4653101 4653 2024-10-29
6 93a63b80054… PT G 450 d 4653101 4653 2024-12-08
7 93a63b80054… PT G 450 d 4653101 4653 2024-11-17
8 93a63b80054… PT G 450 d 4653101 4653 2024-12-29
9 2560a86d9f2… PT G 450 d 4653101 4653 2024-08-26
10 344034feec4… PT G 450 d 4653101 4653 2024-09-11
# ℹ 12 more rows
# ℹ 2 more variables: wish_update_date <date>, wish_type <chr>
# Se cumple que, generalmente, el mismo coche comparte el mismo baumuster independientemente del wish type# Asumo que puede haber más errores como este. Miraré directamente todos los baumuster de más de 7 caractereswishlist_pt |>filter(baumuster >=10000000)
Parece que no era un outlier único, sino que hay varios valores así. Todos los que tienen más de 7 caracteres, parece que cumplen con el patrón de que su último carácter es un 0 y que todos son del tipo stock. Pero no todos los de tipo stock tienen más de 7 caracteres… Diría que es un error de digitación. Esto puede dar problemas en el futuro al hacer análisis de frecuencias o al hacer joins entre tablas.
# Verificar si los de 8 dígitos terminan en 0wishlist_pt |>filter(baumuster_len ==8) |>mutate(ends_in_zero =str_ends(baumuster, "0")) |>count(ends_in_zero)
# A tibble: 1 × 2
ends_in_zero n
<lgl> <int>
1 TRUE 65
# Comprobar si hay más de un vehicle_name por baumusterbaumuster_mas_1_vehicle <- wishlist_pt |>group_by(baumuster) |>mutate(num_vehiculos_unicos =n_distinct(vehicle_name)) |>filter(num_vehiculos_unicos >1) |>group_by(baumuster, vehicle_name) |>summarise(count =n(), .groups ="drop") |>arrange(baumuster, desc(count))# Separar por baumuster y mostrar cada grupo individualmenteresultados_split <- baumuster_mas_1_vehicle |>group_split(baumuster)resultados_split; length(resultados_split)
<list_of<
tbl_df<
baumuster : double
vehicle_name: character
count : integer
>
>[23]>
[[1]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 1183851 CLA 250 e Coupé com tecnologia EQ 11
2 1183851 CLA 250e Coupé com tecnologia híbrida EQ 8
[[2]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 1186851 CLA 250e Shooting Brake com tecnologia híbrida EQ 5
2 1186851 CLA 250 e Shooting Brake with EQ hybrid technology 3
[[3]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 1673061 GLE 350 de 4MATIC Coupé com Tecnologia Híbrida EQ 13
2 1673061 GLE 350 de 4MATIC Coupé 1
[[4]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2140541 E 300 e Limousine com tecnologia híbrida EQ 18
2 2140541 E 300 e com tecnologia híbrida EQ Limousine 1
[[5]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2142081 E 300 de Station com tecnologia híbrida EQ 27
2 2142081 E 300 de com tecnologia híbrida EQ Station 4
[[6]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2142191 E 300 de 4MATIC All-Terrain com tecnologia híbrida EQ 34
2 2142191 E 300 de 4MATIC com tecnologia híbrida EQ All-Terrain 1
[[7]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2142541 E 300 e Station com tecnologia híbrida EQ 18
2 2142541 E 300 e com tecnologia híbrida EQ Station 2
[[8]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2363541 CLE 300 e Coupé com tecnologia híbrida EQ 19
2 2363541 CLE 300 e com tecnologia híbrida EQ Coupé 1
[[9]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2436021 EQB 250+ 29
2 2436021 EQB 250+ Edition 10
[[10]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2436131 EQB 350 4MATIC 4
2 2436131 EQB 350 4MATIC Edition 1
[[11]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2437021 EQA 250+ Edition 24
2 2437021 EQA 250+ 21
[[12]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2437131 EQA 350 4MATIC 2
2 2437131 EQA 350 4MATIC Edition 1
[[13]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2946111 EQE 300 SUV 20
2 2946111 EQE 300 SUV Edition 4
[[14]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2946211 EQE 350+ SUV 33
2 2946211 EQE 350+ SUV Edition 7
[[15]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2946221 EQE 500 4MATIC SUV 6
2 2946221 EQE 500 4MATIC SUV Edition 5
[[16]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2951111 EQE 300 Edition 7
2 2951111 EQE 300 Limousine 5
[[17]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2951211 EQE 350+ Limousine 13
2 2951211 EQE 350+ Edition 5
[[18]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2951321 Mercedes-AMG EQE 43 4MATIC Edition 3
2 2951321 Mercedes-AMG EQE 43 4MATIC Limousine 3
[[19]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2966231 EQS 450+ SUV Edition 6
2 2966231 EQS 450+ SUV 5
[[20]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 2971231 EQS 450+ Edition 4
2 2971231 EQS 450+ Limousine 1
[[21]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 11838510 CLA 250 e Coupé com tecnologia EQ 1
2 11838510 CLA 250e Coupé com tecnologia híbrida EQ 1
[[22]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 24370210 EQA 250+ 2
2 24370210 EQA 250+ Edition 1
[[23]]
# A tibble: 2 × 3
baumuster vehicle_name count
<dbl> <chr> <int>
1 29462110 EQE 350+ SUV 2
2 29462110 EQE 350+ SUV Edition 1
[1] 23
Debería haber el mismo número de baumuster que de vehicle_name, pues un baumuster hace referencia a un vehicle_name. Aparte de los fallos de digitación, parece que la forma de llamar a los vehicle_name para un mismo baumuster NO es consistente. Hay 23 baumuster con vehicle_name distintos —aunque muy parecidos—.
Curiosamente hay más wish_create_date únicos que wish_update_date únicos. Podría ocurrir, pero me hace pensar que puede haber wish_update_date anteriores a wish_create_date (lo cual no tiene sentido).
Por suerte, no es el caso. Estos datos parecen tener sentido.
2.1.3 Vehículos más deseados
# Define los puntos de quiebre y límites deseados para el eje Xmis_breaks <-seq(from =1000000, to =5000000, by =1000000)mis_limits <-c(1000000, 5000000) # Establece el rango visible del eje X# Gráfico 1: distribución de baumusterp1 <-ggplot(wishlist_pt, aes(x = baumuster)) +geom_histogram(bins =50, fill ="lightblue", color ="black") +labs(title ="Distribución de baumuster (PT)", x ="Baumuster", y ="Frecuencia") +theme_minimal() +scale_x_continuous(breaks = mis_breaks, limits = mis_limits, labels = scales::label_comma(), # Formatea las etiquetas con separadores (ej: 1,000,000)oob = scales::oob_keep # Decide qué hacer con datos fuera de los límites# oob_keep: los mantiene para el cálculo del histograma )p1
# Gráfico 2: distribución de baumuster_4 p2 <-ggplot(wishlist_pt, aes(x = baumuster_4)) +geom_histogram(bins =30, fill ="lightgreen", color ="black") +labs(title ="Distribución de baumuster (4 dígitos)", x ="Baumuster (4 dígitos)", y ="Frecuencia") +theme_minimal() +scale_x_continuous(labels = scales::label_number(accuracy =1)) # Formatear el eje X sin notación científicap2
La distribución de baumuster y baumuster_4 muestra varios picos similares, indicando ciertos modelos o familias de modelos son más populares en las wishlists. Eso tiene sentido, ya que los clientes suelen tener preferencias por ciertos modelos o configuraciones, y como un modelo pertenece a un baumuster específico, es normal que haya agrupaciones en la wishlists.
# Tipos de Wishlist (wish_type)p_wish_type <- wishlist_pt |>count(wish_type, sort =TRUE) |>ggplot(aes(x =reorder(wish_type, n), y = n)) +geom_col(fill ="skyblue") +coord_flip() +labs(title ="Frecuencia de tipos de wishlist (PT)", x ="Tipo", y ="Cantidad") +theme_minimal() +geom_text(aes(label = n), hjust =-0.1) # Añadir etiquetas de conteop_wish_type
Los vehículos híbridos o eléctricos (a menudo con “EQ” en el nombre) dominan la lista de los más deseados, aunque también aparecen otros modelos. Los vehículos eléctricos los que parecen captar más interés. Esto tiene sentido, pues refleja la tendencia creciente hacia la sostenibilidad y la electrificación en el mercado automotriz actual.
# Top N Vehículos más deseados (vehicle_name)top_n_vehicles <-15p_vehicle_name <- wishlist_pt |>count(vehicle_name, sort =TRUE) |>slice_head(n = top_n_vehicles) |>ggplot(aes(x =reorder(vehicle_name, n), y = n)) +geom_col(fill ="salmon") +coord_flip() +labs(title =paste("Top", top_n_vehicles, "Vehículos en wishlist (PT)"),x ="Nombre del vehículo", y ="Cantidad") +theme_minimal()p_vehicle_name
La mayoría de los vehículos deseados son configurables (es decir, wish_type = "CONFIGURABLE"). Esto es un buen indicativo de que los clientes están interesados en personalizar su vehículo. Sin embargo, también hay un número significativo de vehículos de stock deseados (wish_type = “STOCK”). Esto puede indicar que algunos clientes están buscando opciones más rápidas o disponibles inmediatamente.
2.1.4 Fechas de creación y actualización. Tiempo entre ellas
# Distribución de fechas de creaciónp_create <-ggplot(wishlist_pt, aes(x = wish_create_date)) +geom_histogram(bins =50, fill ="cornflowerblue", color ="black") +labs(title ="Distribución de fechas de creación de wishlist (PT)", x ="Fecha creación", y ="Frecuencia") +theme_minimal() +scale_x_date(date_labels ="%d-%m-%Y", date_breaks ="1 month") # Cambia el intervalo de las etiquetas# Distribución de fechas de actualizaciónp_update <-ggplot(wishlist_pt, aes(x = wish_update_date)) +geom_histogram(bins =50, fill ="lightcoral", color ="black") +labs(title ="Distribución de fechas de actualización de wishlist (PT)", x ="Fecha actualización", y ="Frecuencia") +theme_minimal() +scale_x_date(date_labels ="%d-%m-%Y", date_breaks ="1 month") # Cambia el intervalo de las etiquetas# Calcular diferencia de tiempo entre creación y actualizaciónwishlist_pt <- wishlist_pt |>mutate(update_lag_days =as.numeric(difftime(wish_update_date, wish_create_date, units ="days")))# Distribución de la diferencia de tiempop_lag <-ggplot(wishlist_pt |>filter(update_lag_days >=0), aes(x = update_lag_days)) +geom_histogram(bins =50, fill ="mediumpurple", color ="black") +labs(title ="Distribución del tiempo entre creación y actualización (días)",x ="Días entre creación y actualización", y ="Frecuencia") +theme_minimal()# Mostrar gráficosp_create; p_update; p_lag
Las fechas de creación y actualización muestran patrones temporales, posiblemente picos de actividad o periodos con menos datos. La mayoría de las wishlists no son actualizadas nunca, pues tienen la misma fecha en ambos valores (diferencia 0). La mayoría de los clientes que actualizan su wishlist lo hacen en un plazo de unos 30 días tras su creación.
2.1.5 Clientes con múltiples entradas
Es posible que algunos clientes utilicen la wishlist de forma extensiva, añadiendo muchos vehículos diferentes. Si bien esto puede indicar un interés genuino y una fase de exploración amplia, un número excesivamente alto de vehículos podría también señalar a usuarios atípicos (ej. empleados probando la funcionalidad, bots, o usuarios sin una intención de compra clara). Identificar estos patrones puede ser útil para refinar la segmentación de clientes o para decidir si excluir ciertos perfiles extremos del modelado predictivo.
Contaremos cuántos vehículos únicos (basado en vehicle_name) ha añadido cada cliente a su wishlist.
Min. 1st Qu. Median Mean 3rd Qu. Max.
1.000 1.000 2.000 3.172 4.000 48.000
# Visualizar la distribución del número de vehículos distintos por clientep_customer_wishlist_activity <-ggplot(customer_wishlist_counts, aes(x = n_distinct_vehicles_in_wishlist)) +geom_histogram(binwidth =1, fill ="steelblue", color ="black") +labs(title ="Distribución del nº de vehículos únicos en wishlist por cliente (PT)",x ="Nº de vehículos únicos en wishlist",y ="Frecuencia de clientes") +theme_minimal() +scale_x_continuous(breaks = scales::pretty_breaks(n=10)) # Mejorar legibilidad de breaksp_customer_wishlist_activity
# Clientes con un número elevado de vehículos en wishlist (ej. > 5)customer_wishlist_counts |>filter(n_distinct_vehicles_in_wishlist >5) |>arrange(desc(n_distinct_vehicles_in_wishlist)) |>print(n=10) # Mostrar los 10 primeros
La mayoría de los clientes tienen entre 1 y 2 vehículos únicos en su wishlist. Sin embargo, observamos un pequeño número de clientes con una cantidad significativamente mayor (ej., más de 5 vehículos distintos). Estos podrían ser casos a investigar más a fondo o a tratar con cuidado en fases posteriores, ya que podrían no representar a un comprador típico enfocado. La diferencia entre n_distinct_vehicles_in_wishlist y n_total_wishlist_entries nos indica si los clientes añaden el mismo vehículo múltiples veces o si actualizan sus configuraciones.
Los tipos de datos parecen tener sentido, aunque creo que damage_loc_id y service_type_code podrían ser perfectamente variables numéricas. repair_cost_category podría ser discretizado a variables numéricas también, ya que tiene pinta que solo habrá unos cuantos posibles valores. Si no se discretiza a variables numéricas, se podría convertir a factor para evitar problemas de interpretación en algunos gráficos.
La distribución es buena. Puede que haya algún outlier en damage_mileage pero nada extremadamente preocupante. Ahora miraremos más profundamente.
Hay más descripciones de servicios que códigos. En el documento readme.pdf se explica claramente que esta discrepancia puede ocurrir con damage_loc_id y damage_type_description, pues “se clasifican en categorías para agrupar motivos similares”. Esto explica que un mismo damage_type_description pueda estar asociado a distintos damage_loc_id. Pero no se dice nada sobre service.
Efectivamente, hay 9 servicios con descripciones muy similares pero distintas. Lo mismo que pasaba con los baumuster y los vehicle_name en el dataset anterior.
2.2.2.1 Relación coste-código de servicio
Adicionalmente a las múltiples descripciones para un mismo service_type_code, es interesante ver cómo estos códigos de servicio (que representan el tipo de servicio ofrecido) se relacionan con la categoría de coste de la reparación.
# Usaremos service_type_code directamente, ya que la estandarización final de las descripciones se hará en limpieza# Visualizar las más frecuentes (ej. Top 10 service_type_code)top_service_codes <- damage_pt |>count(service_type_code, sort =TRUE) |>slice_head(n =10) |>pull(service_type_code)# Estandarización temporal de service_type_description# Para cada service_type_code, tomar la descripción más frecuenteservice_description_standardized <- damage_pt |>filter(service_type_code %in% top_service_codes) |>count(service_type_code, service_type_description, sort =TRUE) |>group_by(service_type_code) |>slice_head(n =1) |># Tomar la descripción más frecuenteungroup() |>mutate(service_code_and_desc =paste0(service_type_code, " - ", service_type_description)) |>select(service_type_code, service_code_and_desc)# Unir la descripción estandarizada y crear el factor ordenado para el eje Xdamage_pt_plot_data <- damage_pt |>filter(service_type_code %in% top_service_codes) |>left_join(service_description_standardized, by ="service_type_code") |>mutate(repair_cost_category =factor(repair_cost_category,levels =c("High cost", "Medium-high cost", "Medium-low cost", "Low cost")))# Crear niveles ordenados para el eje X basados en la frecuencia original de service_type_code# y usando la nueva etiqueta combinadaordered_x_labels <- damage_pt_plot_data |>distinct(service_type_code, service_code_and_desc) |>mutate(service_type_code_factor =factor(service_type_code, levels = top_service_codes)) |>arrange(service_type_code_factor) |>pull(service_code_and_desc)# Calcular recuentos totales para las anotaciones de textoplot_text_data <- damage_pt_plot_data |>group_by(service_code_and_desc) |>summarise(total_n =n(), .groups ='drop') |>mutate(service_code_and_desc =factor(service_code_and_desc, levels = ordered_x_labels))p_service_cost_relation <- damage_pt_plot_data |>ggplot(aes(x =factor(service_code_and_desc, levels = ordered_x_labels), fill = repair_cost_category)) +geom_bar(position ="fill") +geom_text(data = plot_text_data,aes(x = service_code_and_desc, y =1.01, label = total_n, fill =NULL), # y ligeramente por encima del 100%vjust =0, size =3, show.legend =FALSE) +# vjust=0 para alinear la base del texto en yscale_y_continuous(labels = scales::percent, limits =c(0, 1.1)) +# Ajustar límites para dejar espacio al textolabs(title ="Relación coste-reparación vs Top 10 tipos de servicio (PT)",x ="Código y descripción del tipo de servicio",y ="Proporción de categorías de coste",fill ="Categoría coste") +theme_minimal() +theme(axis.text.x =element_text(angle =45, hjust =1))# Imprimir el gráficop_service_cost_relation
Echando un ojo al gráfico que relaciona los tipos de servicio con el coste de reparación, podemos sacar varias cosas en claro:
Los servicios que más se repiten, como "01 - mobility" o "02 - KDM Campaign Claims", tiran más hacia costes bajos y medio-bajos. Lógico, suelen ser intervenciones más comunes.
Por otro lado, ojo con "11 - KULA - Goodwill Claims" y "04 - parts warranty". Estos tienen una porción más grande de “High cost”. "08 - POW", las partes fuera de garantía, también se lleva un buen pellizco de la categoría "Medium-high cost".
Los que tienen menos volumen, como "13 - other services" o el ya mencionado "14 - Fleetsite", parece que se inclinan más a costes altos, pero hay que cogerlo con pinzas porque son muy pocos casos y la proporción puede engañar.
2.2.3damage_mileage y damage_repair_date
Estas variables nos dan información sobre cuándo y con qué uso ocurren los daños, lo cual podría ser relevante para entender el ciclo de vida del vehículo y el comportamiento del cliente.
# Distribución de damage_mileagep_damage_mileage <-ggplot(damage_pt, aes(x = damage_mileage)) +geom_histogram(bins =50, fill ="firebrick", color ="black") +scale_x_continuous(labels = scales::comma,breaks =seq(0, max(damage_pt$damage_mileage, na.rm =TRUE), by =200000) ) +coord_cartesian(xlim =c(0, min(max(damage_pt$damage_mileage, na.rm =TRUE), 600000))) +# Limitar eje X para mejor visualizaciónlabs(title ="Distribución del kilometraje en visitas al taller (PT)",x ="Kilometraje (damage_mileage)",y ="Frecuencia") +theme_minimal()p_damage_mileage
# Distribución de damage_repair_datep_damage_repair_date <-ggplot(damage_pt, aes(x = damage_repair_date)) +geom_histogram(bins =50, fill ="darkorchid", color ="black") +scale_x_date(date_labels ="%m-%Y", date_breaks ="6 months") +labs(title ="Distribución temporal de fechas de reparación (PT)",x ="Fecha de reparación",y ="Frecuencia") +theme_minimal() +theme(axis.text.x =element_text(angle =45, hjust =1))p_damage_repair_date
El histograma de damage_mileage muestra que la mayoría de las visitas al taller ocurren con un kilometraje relativamente bajo (por debajo de 100.000 km), con una cola larga hacia kilometrajes más altos. Esto es esperable. Hay que prestar atención a valores extremadamente altos que podrían ser outliers o errores.
La distribución de damage_repair_date muestra la actividad de reparaciones a lo largo del tiempo. Podríamos observar picos de actividad, posibles efectos de estacionalidad, o periodos donde la recolección de datos fue más intensa. Esta información temporal puede ser útil para contextualizar el comportamiento del cliente. Ahora mismo no parece haber un patrón claro, pero es algo a tener en cuenta.
2.2.4 Relaciones de coste-tipo de daño
top_n_damage_types <-15# Ajusta N según sea necesario# Calcular frecuencias de damage_type_descriptiondamage_type_freq <- damage_pt |>count(damage_type_description, sort =TRUE, name ="total_count")# Filtrar damage_pt para incluir solo los top N tipos y añadir el factor ordenadodamage_pt_top_types <- damage_pt |>inner_join(damage_type_freq |>slice_head(n = top_n_damage_types), by ="damage_type_description") |># Convertir repair_cost_category a factor con el orden deseadomutate(repair_cost_category =factor(repair_cost_category,levels =c("High cost", "Medium-high cost", "Medium-low cost", "Low cost")))# Crear data para las anotaciones: número total por tipo de dañotext_data_damage <- damage_pt_top_types |>distinct(damage_type_description, total_count)# Gráfico de barras apiladas (proporciones) con anotación de recuento totalp_damage_cost_relation <-ggplot(damage_pt_top_types,aes(x =reorder(damage_type_description, -total_count),fill = repair_cost_category)) +geom_bar(position ="fill") +# 'fill' muestra proporciones (total = 1)coord_flip() +# Voltear ejes para mejor legibilidad con muchos tipos de dañoscale_y_continuous(labels = scales::percent) +# Eje Y como porcentajelabs(title =paste("Relación coste reparación vs Top", top_n_damage_types, "tipos de daño (PT)"),x ="Tipo de daño (más frecuentes arriba)",y ="Proporción de categorías de coste",fill ="Categoría coste") +geom_text(data = text_data_damage,inherit.aes =FALSE,aes(x =reorder(damage_type_description, -total_count),y =1.01, label = total_count),vjust =0, size =3, show.legend =FALSE) +# Posicionar el texto ligeramente fuera del 100%theme_minimal() +theme(axis.text.y =element_text(size =8))p_damage_cost_relation
En resumen, este gráfico nos da una idea de qué tipos de daños suelen salir más caros. Es útil para entender dónde se va el dinero en las reparaciones. Por ejemplo, si vemos muchos Electrical fault, ya sabemos que probablemente estemos hablando de facturas más elevadas. Su utilidad actual es limitada, pero puede ser interesante para el futuro.
customer_id vehicle_id valid_from
Length:43574 Length:43574 Min. :2016-08-31
Class :character Class :character 1st Qu.:2020-10-01
Mode :character Mode :character Median :2022-09-20
Mean :2022-01-05
3rd Qu.:2023-09-07
Max. :2024-12-31
valid_to role country_code
Min. :2016-09-05 Length:43574 Length:43574
1st Qu.:2019-06-07 Class :character Class :character
Median :2022-03-30 Mode :character Mode :character
Mean :2021-09-16
3rd Qu.:2023-12-15
Max. :2025-02-27
NA's :17018
Estructura y distribución adecuadas. valid_to tiene bastantes nulos, pero es normal porque en este caso los nulos sí tienen significado: si el valor es nulo, significa que el cliente aún está vinculado al vehículo.
La unicidad de los datos es la esperada. La columna de country_code se podría eliminar, ya que al estar filtrado por Portugal esta ya no aporta nada (solo hay 1 dato distinto). Algo similar ocurrirá con los roles, pues en nuestro caso de uso solo nos interesan los clientes ya existentes, es decir, aquellos que tienen el rol de OWNER (solo habrá 1 dato distinto, y tras filtrar se podrá eliminar la columna).
2.3.3 Rol, vinculación y posesión
Entender el rol del cliente con el vehículo y cuánto tiempo lo posee es crucial para un modelo de recompra.
# Distribución de 'role'p_role_dist <- customer_vehicle_pt |>count(role, sort =TRUE) |>ggplot(aes(x =reorder(role, n), y = n)) +geom_col(fill ="olivedrab") +coord_flip() +labs(title ="Distribución de roles de vinculación cliente-vehículo (PT)",x ="Rol", y ="Frecuencia") +geom_text(aes(label = n), hjust =-0.1, size =3) +theme_minimal()p_role_dist
La gran mayoría de las vinculaciones son de tipo OWNER, lo cual es consistente con nuestro enfoque B2C.
# Distribución de 'valid_from'p_valid_from <-ggplot(customer_vehicle_pt, aes(x = valid_from)) +geom_histogram(bins =50, fill ="gold", color ="black") +scale_x_date(date_labels ="%Y-%m", date_breaks ="1 year") +labs(title ="Distribución de Fechas de Inicio de Vinculación (valid_from)",x ="Fecha", y ="Frecuencia") +theme_minimal()p_valid_from
La distribución de valid_from muestra cuándo los clientes inician su vinculación con los vehículos. Se observa una actividad de vinculación constante a lo largo de los años, con un incremento paulatino y un pico muy notable a finales de 2023 y principios de 2024. Este aumento significativo podría deberse a factores como campañas de ventas exitosas, el lanzamiento de modelos populares, o incluso cambios en los procesos de registro de datos. Identificar la causa de este pico podría ofrecer insights valiosos sobre los impulsores de la adquisición de vehículos, pero es complicado sin contexto adicional.
# Calcular duración de posesión para vehículos ya desvinculados# Asumiremos que la fecha máxima en el dataset es un buen proxy para "hoy" en el contexto de los datos.current_analysis_date <-max(c(max(customer_vehicle_pt$valid_from, na.rm=T), max(customer_vehicle_pt$valid_to, na.rm=T),max(wishlist_pt$wish_create_date, na.rm=T), # Incluir otras fechas relevantesmax(damage_pt$damage_repair_date, na.rm=T)), na.rm=T)customer_vehicle_pt_duration <- customer_vehicle_pt |>mutate(effective_valid_to =if_else(is.na(valid_to), current_analysis_date, valid_to),ownership_duration_days =as.numeric(difftime(effective_valid_to, valid_from, units ="days")),still_owner =is.na(valid_to) )p_ownership_duration <-ggplot(customer_vehicle_pt_duration, aes(x = ownership_duration_days)) +geom_histogram(aes(fill = still_owner), bins =50, color ="black", alpha=0.7, position="identity") +scale_x_continuous(labels = scales::comma, breaks =seq(0, max(customer_vehicle_pt_duration$ownership_duration_days, na.rm=T), by=365)) +labs(title ="Distribución de duración de vinculación con vehículo (días)",subtitle =paste0("Para 'still_owner=TRUE', duración hasta ", format(current_analysis_date, "%Y-%m-%d")),x ="Duración en Días (1 año = 365 días)", y ="Frecuencia",fill ="Aún vinculado") +theme_minimal()p_ownership_duration
La distribución de ownership_duration_days nos muestra cuánto tiempo los clientes mantienen la vinculación con sus vehículos. Es una variable crucial para entender los ciclos de vida de los productos y los posibles momentos de recompra. El gráfico diferencia entre:
Clientes que ya no están vinculados al vehículo (still_owner = FALSE, en naranja): se observa una frecuencia muy alta de vinculaciones de duración extremadamente corta, con un pico masivo justo después de los 0 días. Esto podría deberse a múltiples factores como devoluciones tempranas, contratos de muy corta duración, vehículos de demostración o incluso particularidades en el registro de datos que necesitarían más contexto para ser interpretados correctamente. Después de este pico inicial, la frecuencia de desvinculaciones disminuye a medida que aumenta el tiempo de posesión.
Clientes que aún están vinculados al vehículo (still_owner = TRUE, en cian): se aprecia una concentración importante de clientes cuya vinculación actual se sitúa alrededor de los 500-600 días (aproximadamente 1.5 años). Esto indica que un segmento considerable de la base de clientes activos ha tenido su vehículo por este lapso. La distribución también muestra clientes con vinculaciones más largas y más cortas, reflejando la diversidad en la antigüedad de la flota activa.
Identificar las causas detrás del pico de desvinculaciones tempranas y entender el comportamiento del grupo que actualmente lleva alrededor de 1.5 años con su vehículo podría ofrecer insights valiosos para estrategias de retención y recompra.
Es importante notar que las columnas relacionadas con las dimensiones físicas del vehículo (height_range, height_unit, width_range, width_unit, length_range, length_unit) presentan un porcentaje considerable de valores nulos (entre el 16.66% y el 16.97%). También date_of_first_registration tiene un 6.97% de nulos. Estas ausencias deberán ser gestionadas en la fase de limpieza de datos si decidimos utilizar estas variables para la predicción. La estrategia de imputación o manejo de estos NAs dependerá de la importancia predictiva que se les atribuya.
Hay prácticamente un vehicle_id por fila, como se espera. Si bien un vehicle_id debería ser único, esta pequeña cantidad de duplicados podría deberse a errores de entrada o a una lógica de negocio específica que no conocemos, o quizá la presencia de vehículos de segunda mano vendidos por el concesionario oficial de Mercedes. Las demás columnas muestran una unicidad esperada: alta para fechas y baja para categorías como class_bodytype o las unidades de medida.
2.4.3class_bodytypes más comunes y distribución de fechas
# Visualización de class_bodytype (Top N)top_n_bodytypes <-15p_bodytype_dist <- vehicle_data |>count(class_bodytype, sort =TRUE) |>slice_head(n = top_n_bodytypes) |>ggplot(aes(x =reorder(class_bodytype, n), y = n)) +geom_col(fill ="cyan4") +coord_flip() +labs(title =paste("Top", top_n_bodytypes, "Class Bodytypes en vehicle_data"),x ="Class Bodytype", y ="Frecuencia") +theme_minimal()p_bodytype_dist
El gráfico muestra la distribución de los 15 class_bodytype más frecuentes en el conjunto de datos vehicle_data.
Se observa que A_class_hatchback es el tipo de carrocería más común, seguido de cerca por V_class_van y C_class_estate. La lista también incluye una variedad de otros tipos como SUVs (por ejemplo, GLC_class_suv, GLA_class_suv), sedanes (C_class_sedan, E_class_sedan) y coupés. Esto indica una diversidad en los tipos de vehículos registrados en este dataset, con una predominancia de hatchbacks, vans y station wagons.
# Pueden ser útiles para calcular la edad del vehículo.# Distribución de date_of_technical_approvalp_date_tech <-ggplot(vehicle_data, aes(x = date_technical_approval)) +geom_histogram(bins =50, fill ="darkgoldenrod1", color ="black") +labs(title ="Distribución de fecha aprobación técnica", x ="Fecha", y ="Frecuencia") +theme_minimal()p_date_tech
# Distribución de date_of_first_registrationp_date_reg <-ggplot(vehicle_data, aes(x = date_of_first_registration)) +geom_histogram(bins =50, fill ="darkseagreen", color ="black") +labs(title ="Distribución de fecha primer registro", x ="Fecha", y ="Frecuencia") +scale_y_continuous(labels = scales::comma) +theme_minimal()p_date_reg
Los histogramas muestran la distribución temporal de date_technical_approval y date_of_first_registration.
date_technical_approval (aprobación técnica, en naranja): la mayoría de las aprobaciones técnicas se concentran en años recientes, con un incremento notable a partir de aproximadamente 2010 y un pico en el periodo 2020-2023. Hay muy pocas aprobaciones registradas antes del año 2000.
date_of_first_registration (primer registro, en verde): sigue un patrón muy similar al de la aprobación técnica. La mayoría de los vehículos fueron registrados por primera vez en años recientes, también con un fuerte aumento desde 2010 y alcanzando su máximo alrededor de 2020-2023.
Ambas distribuciones sugieren que el conjunto de datos vehicle_data está compuesto predominantemente por vehículos relativamente nuevos. Estas fechas son fundamentales para calcular la antigüedad de los vehículos, una variable que podría ser importante. La tendencia creciente en ambas fechas podría reflejar un aumento en las ventas de vehículos, una mayor digitalización de los registros o una combinación de factores.
En general, se trata de un dataset principalmente descriptivo. Clave para obtener class_bodytype y potencialmente la edad del vehículo o las dimensiones. Los NAs en dimensiones y en date_of_first_registration son el principal punto a tratar, ya que hay bastantes (16-17%).
2.5 Conclusiones
Este análisis exploratorio ha revelado varios aspectos clave sobre los datos que serán cruciales para las fases de preprocesamiento y modelado. Las siguientes conclusiones y decisiones guiarán los próximos pasos:
Enfoque del proyecto y relevancia de los datos:
Objetivo principal: el proyecto se centra en predecir la próxima fecha de recompra (next_repurchase_date) y el modelo de vehículo (next_class_bodytype) para clientes particulares ya existentes (B2C).
Filtrado de roles: consecuentemente, en customer_vehicle_pt, solo se considerarán las vinculaciones donde el rol del cliente es OWNER. Otros roles (e.g., USER, KEEPER) se excluirán, ya que no representan la compra directa por un particular, que es el foco del análisis.
Simplificación de baumuster: para la predicción del next_class_bodytype, la granularidad del baumuster completo es excesiva y propensa a errores. Se utilizará el baumuster_4 (los primeros 4 dígitos del baumuster), que define la clase y el tipo de carrocería del vehículo. Esta generalización también ayuda a mitigar el impacto de los errores de digitación observados.
Calidad de datos e inconsistencias identificadas:
wishlist_pt:
Errores de digitación en baumuster: se detectaron baumuster de 8 dígitos que parecen ser versiones de 7 dígitos con un cero adicional al final (principalmente en wish_type = "STOCK"). La decisión de usar baumuster_4 hace que estos errores específicos sean menos problemáticos para el objetivo de predicción de class_bodytype.
Inconsistencia baumuster vs. vehicle_name: existen 23 baumuster asociados a múltiples vehicle_name (aunque semánticamente muy similares). Esto subraya la necesidad de estandarizar la información del vehículo, y el uso de baumuster_4 es un paso en esta dirección.
damage_pt:
Inconsistencia service_type_code vs. service_type_description: se encontraron 9 service_type_code con más de una service_type_description asociada. Se requerirá una estandarización, probablemente eligiendo la descripción más frecuente o una versión generalizada para cada código durante la limpieza de datos.
Patrones observados:
customer_vehicle_pt (datos de vinculación cliente-vehículo):
Significado de NAs en valid_to: los valores nulos en valid_to indican que el cliente aún posee el vehículo, lo cual es fundamental para calcular la duración actual de la posesión y para identificar la población de clientes activos.
Duración de la posesión (ownership_duration_days):
Se observa un pico muy alto de vinculaciones de corta duración para clientes que ya no poseen el vehículo (still_owner = FALSE). Esto podría deberse a ventas rápidas, vehículos de demostración, o errores en los datos, y necesitará una consideración especial, ya que estos casos podrían no ser representativos para un modelo de recompra a largo plazo.
Para los propietarios actuales (still_owner = TRUE), existe una concentración notable alrededor de los 500-600 días (aprox. 1.5 años) de posesión, sugiriendo un ciclo de vida común o un grupo importante de clientes en esta etapa.
Fecha de inicio de vinculación (valid_from): se detectó un incremento significativo en las vinculaciones a finales de 2023 y principios de 2024.
vehicle_data (datos descriptivos de vehículos):
Nulos en dimensiones y fechas: las columnas de dimensiones (height_range, width_range, length_range) y date_of_first_registration presentan un porcentaje considerable de NAs (6.97% para la fecha, ~16-17% para dimensiones). Se deberá abordar mediante imputación o exclusión estratégica si estas variables resultan importantes. La date_of_first_registration es clave para calcular la antigüedad del vehículo.
Predominio de vehículos recientes: las distribuciones de date_technical_approval y date_of_first_registration indican que el dataset está compuesto mayoritariamente por vehículos más nuevos (posteriores a 2010, con picos en 2020-2023).
wishlist_pt (lista de deseos):
La mayoría de los clientes tienen 1-2 vehículos únicos en su wishlist. Un pequeño grupo presenta un número mucho mayor, lo que podría indicar perfiles de usuario atípicos.
Los vehículos eléctricos/híbridos (“EQ”) y los configurables son populares, reflejando tendencias del mercado.
La mayoría de las wishlists no se actualizan o se actualizan poco después de su creación.
Próximos pasos en preprocesamiento y Feature Engineering:
Estandarización: aplicar las correcciones y estandarizaciones mencionadas para baumuster, vehicle_name, y service_type_description.
Manejo de NAs: definir estrategias para los NAs en vehicle_data (especialmente date_of_first_registration).
Creación de variables:
Antigüedad del vehículo.
Duración de posesión (actual y pasadas).
Frecuencia de visitas al taller, tipos de daño/servicio.
Historial de vehículos por cliente.
Agregación de datos: consolidar la información a nivel de cliente para el modelado.
Segmentación/exclusión: considerar si los clientes con patrones de posesión extremadamente cortos o actividad atípica en la wishlist deben ser tratados de forma diferente o excluidos.
---title: "Análisis exploratorio de los datos"author: "Rubén Oliva Zamora"editor: visual---```{r}#| label: setup#| include: false# 1. Cargar las librerías comunessource("scripts/load_packages.R")# 2. Cargar los datos procesadosprocessed_data_path <-"data/processed"wishlist_pt <-readRDS(file.path(processed_data_path,"wishlist_pt.rds"))damage_pt <-readRDS(file.path(processed_data_path,"damage_pt.rds"))customer_vehicle_pt <-readRDS(file.path(processed_data_path,"customer_vehicle_pt.rds"))vehicle_data <-readRDS(file.path(processed_data_path,"vehicle_data.rds"))```En esta sección, realizaremos un análisis exploratorio inicial de los datos filtrados para Portugal (`wishlist_pt`, `damage_pt`, `customer_vehicle_pt`) y del dataset global de vehículos (`vehicle_data`). El objetivo es entender la **estructura**, el **contenido**, la **calidad de los datos** y detectar posibles problemas o patrones interesantes a primera vista.## `wishlist_pt`En esta sección, exploraremos en detalle el conjunto de datos `wishlist_pt`, que contiene información sobre los **vehículos deseados por los clientes en Portugal**.### Estructura, distribución y nulos```{r}# Función auxiliar para resumir NAsresumen_nulos <-function(dataframe) { # Calcular numero y % de NAs na_counts <-colSums(is.na(dataframe)) na_perc <-round(100* na_counts /nrow(dataframe), 2) na_summary <-data.frame(NA_number = na_counts,NA_percentage = na_perc,stringsAsFactors =FALSE )return(na_summary)}# Estructuraglimpse(wishlist_pt)# Distribuciónsummary(wishlist_pt)# Nulosresumen_nulos(wishlist_pt)```No hay nulos y los datos parecen de buena calidad. Los **tipos son adecuados** y las **distribuciones tienen sentido**, salvo el máximo de baumuster que es extrañamente alto. Tiene pinta de que se ha metido un dígito de más al introducir ese baumuster concretamente.#### Investigación del máximo baumuster```{r}max_baum <- wishlist_pt |>filter(baumuster ==max(baumuster)); max_baumwishlist_pt |>filter(vehicle_name == max_baum$vehicle_name)# Se cumple que, generalmente, el mismo coche comparte el mismo baumuster independientemente del wish type# Asumo que puede haber más errores como este. Miraré directamente todos los baumuster de más de 7 caractereswishlist_pt |>filter(baumuster >=10000000)wishlist_pt |>filter(baumuster <=1000000)```Parece que no era un outlier único, sino que **hay varios valores así**. Todos los que tienen más de 7 caracteres, parece que cumplen con el patrón de que su último carácter es un 0 y que todos son del tipo stock. Pero no todos los de tipo stock tienen más de 7 caracteres... Diría que es un **error de digitación**. Esto puede dar problemas en el futuro al hacer análisis de frecuencias o al hacer joins entre tablas.#### Análisis de longitud de baumuster```{r}wishlist_pt <- wishlist_pt |>mutate(baumuster_len =str_length(baumuster)) # Añadir columna temporal para análisiswishlist_pt |>count(baumuster_len, sort =TRUE)# Relación entre longitud de baumuster y wish typewishlist_pt |>count(wish_type, baumuster_len)# Verificar si los de 8 dígitos terminan en 0wishlist_pt |>filter(baumuster_len ==8) |>mutate(ends_in_zero =str_ends(baumuster, "0")) |>count(ends_in_zero)wishlist_pt <- wishlist_pt |>select(-baumuster_len)```Patrón confirmado. Todos acaban en 0. Asumo que es un **error de digitación**, al ser una muestra pequeña la que tiene esta característica.### Unicidad```{r}# Valores únicos por columnasapply(wishlist_pt, function(x) length(unique(x)))# Comprobar si hay más de un vehicle_name por baumusterbaumuster_mas_1_vehicle <- wishlist_pt |>group_by(baumuster) |>mutate(num_vehiculos_unicos =n_distinct(vehicle_name)) |>filter(num_vehiculos_unicos >1) |>group_by(baumuster, vehicle_name) |>summarise(count =n(), .groups ="drop") |>arrange(baumuster, desc(count))# Separar por baumuster y mostrar cada grupo individualmenteresultados_split <- baumuster_mas_1_vehicle |>group_split(baumuster)resultados_split; length(resultados_split)```Debería haber el mismo número de `baumuster` que de `vehicle_name`, pues un `baumuster` hace referencia a un `vehicle_name`. Aparte de los fallos de digitación, parece que **la forma de llamar a los `vehicle_name` para un mismo `baumuster` NO es consistente**. Hay 23 baumuster con `vehicle_name` distintos —aunque muy parecidos—.Curiosamente hay más `wish_create_date` únicos que `wish_update_date` únicos. Podría ocurrir, pero me hace pensar que puede haber `wish_update_date` anteriores a `wish_create_date` (lo cual no tiene sentido).```{r}wishlist_pt |>filter(wish_create_date > wish_update_date)```Por suerte, no es el caso. Estos datos parecen tener sentido.### Vehículos más deseados```{r}# Define los puntos de quiebre y límites deseados para el eje Xmis_breaks <-seq(from =1000000, to =5000000, by =1000000)mis_limits <-c(1000000, 5000000) # Establece el rango visible del eje X# Gráfico 1: distribución de baumusterp1 <-ggplot(wishlist_pt, aes(x = baumuster)) +geom_histogram(bins =50, fill ="lightblue", color ="black") +labs(title ="Distribución de baumuster (PT)", x ="Baumuster", y ="Frecuencia") +theme_minimal() +scale_x_continuous(breaks = mis_breaks, limits = mis_limits, labels = scales::label_comma(), # Formatea las etiquetas con separadores (ej: 1,000,000)oob = scales::oob_keep # Decide qué hacer con datos fuera de los límites# oob_keep: los mantiene para el cálculo del histograma )p1# Gráfico 2: distribución de baumuster_4 p2 <-ggplot(wishlist_pt, aes(x = baumuster_4)) +geom_histogram(bins =30, fill ="lightgreen", color ="black") +labs(title ="Distribución de baumuster (4 dígitos)", x ="Baumuster (4 dígitos)", y ="Frecuencia") +theme_minimal() +scale_x_continuous(labels = scales::label_number(accuracy =1)) # Formatear el eje X sin notación científicap2```La distribución de `baumuster` y `baumuster_4` muestra varios picos similares, indicando ciertos modelos o familias de modelos son más populares en las wishlists. Eso tiene sentido, ya que los clientes suelen tener preferencias por ciertos modelos o configuraciones, y como un **modelo pertenece a un baumuster específico**, es normal que haya agrupaciones en la wishlists.```{r}# Tipos de Wishlist (wish_type)p_wish_type <- wishlist_pt |>count(wish_type, sort =TRUE) |>ggplot(aes(x =reorder(wish_type, n), y = n)) +geom_col(fill ="skyblue") +coord_flip() +labs(title ="Frecuencia de tipos de wishlist (PT)", x ="Tipo", y ="Cantidad") +theme_minimal() +geom_text(aes(label = n), hjust =-0.1) # Añadir etiquetas de conteop_wish_type```Los **vehículos híbridos o eléctricos** (a menudo con "EQ" en el nombre) dominan la lista de los más deseados, aunque también aparecen otros modelos. Los vehículos eléctricos los que parecen captar más interés. Esto tiene sentido, pues refleja la tendencia creciente hacia la sostenibilidad y la electrificación en el mercado automotriz actual.```{r}# Top N Vehículos más deseados (vehicle_name)top_n_vehicles <-15p_vehicle_name <- wishlist_pt |>count(vehicle_name, sort =TRUE) |>slice_head(n = top_n_vehicles) |>ggplot(aes(x =reorder(vehicle_name, n), y = n)) +geom_col(fill ="salmon") +coord_flip() +labs(title =paste("Top", top_n_vehicles, "Vehículos en wishlist (PT)"),x ="Nombre del vehículo", y ="Cantidad") +theme_minimal()p_vehicle_name```La mayoría de los vehículos deseados son **configurables** (es decir, `wish_type = "CONFIGURABLE"`). Esto es un buen indicativo de que los clientes están interesados en personalizar su vehículo. Sin embargo, también hay un número significativo de vehículos de stock deseados (`wish_type` = "STOCK"). Esto puede indicar que algunos clientes están buscando opciones más rápidas o disponibles inmediatamente.### Fechas de creación y actualización. Tiempo entre ellas```{r}# Distribución de fechas de creaciónp_create <-ggplot(wishlist_pt, aes(x = wish_create_date)) +geom_histogram(bins =50, fill ="cornflowerblue", color ="black") +labs(title ="Distribución de fechas de creación de wishlist (PT)", x ="Fecha creación", y ="Frecuencia") +theme_minimal() +scale_x_date(date_labels ="%d-%m-%Y", date_breaks ="1 month") # Cambia el intervalo de las etiquetas# Distribución de fechas de actualizaciónp_update <-ggplot(wishlist_pt, aes(x = wish_update_date)) +geom_histogram(bins =50, fill ="lightcoral", color ="black") +labs(title ="Distribución de fechas de actualización de wishlist (PT)", x ="Fecha actualización", y ="Frecuencia") +theme_minimal() +scale_x_date(date_labels ="%d-%m-%Y", date_breaks ="1 month") # Cambia el intervalo de las etiquetas# Calcular diferencia de tiempo entre creación y actualizaciónwishlist_pt <- wishlist_pt |>mutate(update_lag_days =as.numeric(difftime(wish_update_date, wish_create_date, units ="days")))# Distribución de la diferencia de tiempop_lag <-ggplot(wishlist_pt |>filter(update_lag_days >=0), aes(x = update_lag_days)) +geom_histogram(bins =50, fill ="mediumpurple", color ="black") +labs(title ="Distribución del tiempo entre creación y actualización (días)",x ="Días entre creación y actualización", y ="Frecuencia") +theme_minimal()# Mostrar gráficosp_create; p_update; p_lag```Las fechas de creación y actualización muestran patrones temporales, posiblemente picos de actividad o periodos con menos datos. La **mayoría de las wishlists no son actualizadas nunca**, pues tienen la misma fecha en ambos valores (diferencia 0). La mayoría de los clientes que actualizan su wishlist lo hacen en un plazo de unos 30 días tras su creación.### Clientes con múltiples entradasEs posible que algunos clientes utilicen la wishlist de forma extensiva, añadiendo muchos vehículos diferentes. Si bien esto puede indicar un interés genuino y una fase de exploración amplia, un **número excesivamente alto de vehículos podría también señalar a usuarios atípicos** (ej. empleados probando la funcionalidad, bots, o usuarios sin una intención de compra clara). Identificar estos patrones puede ser útil para refinar la segmentación de clientes o para decidir si excluir ciertos perfiles extremos del modelado predictivo.Contaremos cuántos vehículos únicos (basado en `vehicle_name`) ha añadido cada cliente a su wishlist.```{r}customer_wishlist_counts <- wishlist_pt |>group_by(customer_id) |>summarise(n_distinct_vehicles_in_wishlist =n_distinct(vehicle_name),n_total_wishlist_entries =n(),.groups ="drop")summary(customer_wishlist_counts$n_distinct_vehicles_in_wishlist)summary(customer_wishlist_counts$n_total_wishlist_entries)# Visualizar la distribución del número de vehículos distintos por clientep_customer_wishlist_activity <-ggplot(customer_wishlist_counts, aes(x = n_distinct_vehicles_in_wishlist)) +geom_histogram(binwidth =1, fill ="steelblue", color ="black") +labs(title ="Distribución del nº de vehículos únicos en wishlist por cliente (PT)",x ="Nº de vehículos únicos en wishlist",y ="Frecuencia de clientes") +theme_minimal() +scale_x_continuous(breaks = scales::pretty_breaks(n=10)) # Mejorar legibilidad de breaksp_customer_wishlist_activity# Clientes con un número elevado de vehículos en wishlist (ej. > 5)customer_wishlist_counts |>filter(n_distinct_vehicles_in_wishlist >5) |>arrange(desc(n_distinct_vehicles_in_wishlist)) |>print(n=10) # Mostrar los 10 primeros```La **mayoría de los clientes tienen entre 1 y 2 vehículos únicos en su wishlist**. Sin embargo, observamos un **pequeño número de clientes con una cantidad significativamente mayor** (ej., más de 5 vehículos distintos). Estos podrían ser casos a investigar más a fondo o a tratar con cuidado en fases posteriores, ya que podrían no representar a un comprador típico enfocado. La diferencia entre `n_distinct_vehicles_in_wishlist` y `n_total_wishlist_entries` nos indica si los clientes añaden el mismo vehículo múltiples veces o si actualizan sus configuraciones.## `damage_pt`### Estructura, distribución y nulos```{r}glimpse(damage_pt)summary(damage_pt)resumen_nulos(damage_pt)```Los tipos de datos parecen tener sentido, aunque creo que **`damage_loc_id` y `service_type_code` podrían ser perfectamente variables numéricas**. `repair_cost_category` podría ser discretizado a variables numéricas también, ya que tiene pinta que solo habrá unos cuantos posibles valores. Si no se discretiza a variables numéricas, se podría **convertir a factor** para evitar problemas de interpretación en algunos gráficos.La distribución es buena. Puede que haya algún outlier en `damage_mileage` pero nada extremadamente preocupante. Ahora miraremos más profundamente.**No hay NAs**. Un dato esperanzador.### Unicidad e inconsistencias```{r}sapply(damage_pt, function(x) length(unique(x)))```**Hay más descripciones de servicios que códigos**. En el documento `readme.pdf` se explica claramente que esta discrepancia puede ocurrir con `damage_loc_id` y `damage_type_description`, pues "se clasifican en categorías para agrupar motivos similares". Esto explica que un mismo `damage_type_description` pueda estar asociado a distintos `damage_loc_id`. Pero no se dice nada sobre service.```{r}damage_pt |>group_by(service_type_code) |>mutate(num_service_description_unico =n_distinct(service_type_description)) |>filter(num_service_description_unico >1) |>group_by(service_type_code, service_type_description) |>summarise(count =n(), .groups ="drop") |>arrange(service_type_code, desc(count)) |>group_split(service_type_code)```Efectivamente, **hay 9 servicios con descripciones muy similares pero distintas**. Lo mismo que pasaba con los `baumuster` y los `vehicle_name` en el dataset anterior.#### Relación coste-código de servicioAdicionalmente a las múltiples descripciones para un mismo `service_type_code`, es interesante ver cómo estos códigos de servicio (que representan el tipo de servicio ofrecido) se relacionan con la categoría de coste de la reparación.```{r}# Usaremos service_type_code directamente, ya que la estandarización final de las descripciones se hará en limpieza# Visualizar las más frecuentes (ej. Top 10 service_type_code)top_service_codes <- damage_pt |>count(service_type_code, sort =TRUE) |>slice_head(n =10) |>pull(service_type_code)# Estandarización temporal de service_type_description# Para cada service_type_code, tomar la descripción más frecuenteservice_description_standardized <- damage_pt |>filter(service_type_code %in% top_service_codes) |>count(service_type_code, service_type_description, sort =TRUE) |>group_by(service_type_code) |>slice_head(n =1) |># Tomar la descripción más frecuenteungroup() |>mutate(service_code_and_desc =paste0(service_type_code, " - ", service_type_description)) |>select(service_type_code, service_code_and_desc)# Unir la descripción estandarizada y crear el factor ordenado para el eje Xdamage_pt_plot_data <- damage_pt |>filter(service_type_code %in% top_service_codes) |>left_join(service_description_standardized, by ="service_type_code") |>mutate(repair_cost_category =factor(repair_cost_category,levels =c("High cost", "Medium-high cost", "Medium-low cost", "Low cost")))# Crear niveles ordenados para el eje X basados en la frecuencia original de service_type_code# y usando la nueva etiqueta combinadaordered_x_labels <- damage_pt_plot_data |>distinct(service_type_code, service_code_and_desc) |>mutate(service_type_code_factor =factor(service_type_code, levels = top_service_codes)) |>arrange(service_type_code_factor) |>pull(service_code_and_desc)# Calcular recuentos totales para las anotaciones de textoplot_text_data <- damage_pt_plot_data |>group_by(service_code_and_desc) |>summarise(total_n =n(), .groups ='drop') |>mutate(service_code_and_desc =factor(service_code_and_desc, levels = ordered_x_labels))p_service_cost_relation <- damage_pt_plot_data |>ggplot(aes(x =factor(service_code_and_desc, levels = ordered_x_labels), fill = repair_cost_category)) +geom_bar(position ="fill") +geom_text(data = plot_text_data,aes(x = service_code_and_desc, y =1.01, label = total_n, fill =NULL), # y ligeramente por encima del 100%vjust =0, size =3, show.legend =FALSE) +# vjust=0 para alinear la base del texto en yscale_y_continuous(labels = scales::percent, limits =c(0, 1.1)) +# Ajustar límites para dejar espacio al textolabs(title ="Relación coste-reparación vs Top 10 tipos de servicio (PT)",x ="Código y descripción del tipo de servicio",y ="Proporción de categorías de coste",fill ="Categoría coste") +theme_minimal() +theme(axis.text.x =element_text(angle =45, hjust =1))# Imprimir el gráficop_service_cost_relation```Echando un ojo al gráfico que relaciona los **tipos de servicio con el coste de reparación**, podemos sacar varias cosas en claro:- Los servicios que más se repiten, como `"01 - mobility"` o `"02 - KDM Campaign Claims"`, tiran más hacia **costes bajos y medio-bajos**. Lógico, suelen ser intervenciones más comunes.- Por otro lado, ojo con `"11 - KULA - Goodwill Claims"` y `"04 - parts warranty"`. Estos tienen una **porción más grande de "High cost"**. `"08 - POW"`, las partes fuera de garantía, también se lleva un buen pellizco de la categoría `"Medium-high cost"`.- Los que tienen menos volumen, como `"13 - other services"` o el ya mencionado `"14 - Fleetsite"`, parece que se inclinan más a costes altos, pero hay que **cogerlo con pinzas** porque son muy pocos casos y la proporción puede engañar.### `damage_mileage` y `damage_repair_date`Estas variables nos dan información sobre **cuándo y con qué uso ocurren los daños**, lo cual podría ser relevante para entender el ciclo de vida del vehículo y el comportamiento del cliente.```{r}# Distribución de damage_mileagep_damage_mileage <-ggplot(damage_pt, aes(x = damage_mileage)) +geom_histogram(bins =50, fill ="firebrick", color ="black") +scale_x_continuous(labels = scales::comma,breaks =seq(0, max(damage_pt$damage_mileage, na.rm =TRUE), by =200000) ) +coord_cartesian(xlim =c(0, min(max(damage_pt$damage_mileage, na.rm =TRUE), 600000))) +# Limitar eje X para mejor visualizaciónlabs(title ="Distribución del kilometraje en visitas al taller (PT)",x ="Kilometraje (damage_mileage)",y ="Frecuencia") +theme_minimal()p_damage_mileage# Distribución de damage_repair_datep_damage_repair_date <-ggplot(damage_pt, aes(x = damage_repair_date)) +geom_histogram(bins =50, fill ="darkorchid", color ="black") +scale_x_date(date_labels ="%m-%Y", date_breaks ="6 months") +labs(title ="Distribución temporal de fechas de reparación (PT)",x ="Fecha de reparación",y ="Frecuencia") +theme_minimal() +theme(axis.text.x =element_text(angle =45, hjust =1))p_damage_repair_date```El histograma de `damage_mileage` muestra que **la mayoría de las visitas al taller ocurren con un kilometraje relativamente bajo (por debajo de 100.000 km)**, con una cola larga hacia kilometrajes más altos. Esto es esperable. Hay que prestar atención a valores extremadamente altos que podrían ser outliers o errores.La distribución de `damage_repair_date` muestra la actividad de reparaciones a lo largo del tiempo. Podríamos observar picos de actividad, posibles efectos de estacionalidad, o periodos donde la recolección de datos fue más intensa. Esta información temporal puede ser útil para contextualizar el comportamiento del cliente. Ahora mismo no parece haber un patrón claro, pero es algo **a tener en cuenta**.### Relaciones de coste-tipo de daño```{r}top_n_damage_types <-15# Ajusta N según sea necesario# Calcular frecuencias de damage_type_descriptiondamage_type_freq <- damage_pt |>count(damage_type_description, sort =TRUE, name ="total_count")# Filtrar damage_pt para incluir solo los top N tipos y añadir el factor ordenadodamage_pt_top_types <- damage_pt |>inner_join(damage_type_freq |>slice_head(n = top_n_damage_types), by ="damage_type_description") |># Convertir repair_cost_category a factor con el orden deseadomutate(repair_cost_category =factor(repair_cost_category,levels =c("High cost", "Medium-high cost", "Medium-low cost", "Low cost")))# Crear data para las anotaciones: número total por tipo de dañotext_data_damage <- damage_pt_top_types |>distinct(damage_type_description, total_count)# Gráfico de barras apiladas (proporciones) con anotación de recuento totalp_damage_cost_relation <-ggplot(damage_pt_top_types,aes(x =reorder(damage_type_description, -total_count),fill = repair_cost_category)) +geom_bar(position ="fill") +# 'fill' muestra proporciones (total = 1)coord_flip() +# Voltear ejes para mejor legibilidad con muchos tipos de dañoscale_y_continuous(labels = scales::percent) +# Eje Y como porcentajelabs(title =paste("Relación coste reparación vs Top", top_n_damage_types, "tipos de daño (PT)"),x ="Tipo de daño (más frecuentes arriba)",y ="Proporción de categorías de coste",fill ="Categoría coste") +geom_text(data = text_data_damage,inherit.aes =FALSE,aes(x =reorder(damage_type_description, -total_count),y =1.01, label = total_count),vjust =0, size =3, show.legend =FALSE) +# Posicionar el texto ligeramente fuera del 100%theme_minimal() +theme(axis.text.y =element_text(size =8))p_damage_cost_relation```En resumen, este gráfico nos da una idea de **qué tipos de daños suelen salir más caros**. Es útil para entender dónde se va el dinero en las reparaciones. Por ejemplo, si vemos muchos `Electrical fault`, ya sabemos que probablemente estemos hablando de facturas más elevadas. Su utilidad actual es limitada, pero puede ser interesante para el futuro.## `customer_vehicle_pt`### Estructura, distribución y nulos```{r}glimpse(customer_vehicle_pt)summary(customer_vehicle_pt)resumen_nulos(customer_vehicle_pt)```Estructura y distribución adecuadas. `valid_to` tiene bastantes nulos, pero es normal porque en este caso los nulos sí tienen significado: **si el valor es nulo, significa que el cliente aún está vinculado al vehículo**.### Unicidad```{r}sapply(customer_vehicle_pt, function(x) length(unique(x)))```La unicidad de los datos es la esperada. La columna de `country_code` se podría eliminar, ya que al estar filtrado por Portugal esta ya no aporta nada (solo hay 1 dato distinto). Algo similar ocurrirá con los roles, pues en nuestro caso de uso **solo nos interesan los clientes ya existentes**, es decir, aquellos que tienen el **rol de `OWNER`** (solo habrá 1 dato distinto, y tras filtrar se podrá eliminar la columna).### Rol, vinculación y posesiónEntender el rol del cliente con el vehículo y cuánto tiempo lo posee es crucial para un modelo de recompra.```{r}# Distribución de 'role'p_role_dist <- customer_vehicle_pt |>count(role, sort =TRUE) |>ggplot(aes(x =reorder(role, n), y = n)) +geom_col(fill ="olivedrab") +coord_flip() +labs(title ="Distribución de roles de vinculación cliente-vehículo (PT)",x ="Rol", y ="Frecuencia") +geom_text(aes(label = n), hjust =-0.1, size =3) +theme_minimal()p_role_dist```La gran mayoría de las vinculaciones son de tipo `OWNER`, lo cual es consistente con nuestro enfoque B2C.```{r}# Distribución de 'valid_from'p_valid_from <-ggplot(customer_vehicle_pt, aes(x = valid_from)) +geom_histogram(bins =50, fill ="gold", color ="black") +scale_x_date(date_labels ="%Y-%m", date_breaks ="1 year") +labs(title ="Distribución de Fechas de Inicio de Vinculación (valid_from)",x ="Fecha", y ="Frecuencia") +theme_minimal()p_valid_from```La distribución de `valid_from` muestra **cuándo los clientes inician su vinculación con los vehículos**. Se observa una **actividad de vinculación constante a lo largo de los años**, con un **incremento paulatino y un pico muy notable a finales de 2023 y principios de 2024**. Este aumento significativo podría deberse a factores como campañas de ventas exitosas, el lanzamiento de modelos populares, o incluso cambios en los procesos de registro de datos. Identificar la causa de este pico podría ofrecer insights valiosos sobre los impulsores de la adquisición de vehículos, pero es complicado sin contexto adicional.```{r}# Calcular duración de posesión para vehículos ya desvinculados# Asumiremos que la fecha máxima en el dataset es un buen proxy para "hoy" en el contexto de los datos.current_analysis_date <-max(c(max(customer_vehicle_pt$valid_from, na.rm=T), max(customer_vehicle_pt$valid_to, na.rm=T),max(wishlist_pt$wish_create_date, na.rm=T), # Incluir otras fechas relevantesmax(damage_pt$damage_repair_date, na.rm=T)), na.rm=T)customer_vehicle_pt_duration <- customer_vehicle_pt |>mutate(effective_valid_to =if_else(is.na(valid_to), current_analysis_date, valid_to),ownership_duration_days =as.numeric(difftime(effective_valid_to, valid_from, units ="days")),still_owner =is.na(valid_to) )p_ownership_duration <-ggplot(customer_vehicle_pt_duration, aes(x = ownership_duration_days)) +geom_histogram(aes(fill = still_owner), bins =50, color ="black", alpha=0.7, position="identity") +scale_x_continuous(labels = scales::comma, breaks =seq(0, max(customer_vehicle_pt_duration$ownership_duration_days, na.rm=T), by=365)) +labs(title ="Distribución de duración de vinculación con vehículo (días)",subtitle =paste0("Para 'still_owner=TRUE', duración hasta ", format(current_analysis_date, "%Y-%m-%d")),x ="Duración en Días (1 año = 365 días)", y ="Frecuencia",fill ="Aún vinculado") +theme_minimal()p_ownership_duration```La distribución de `ownership_duration_days` nos muestra **cuánto tiempo los clientes mantienen la vinculación con sus vehículos**. Es una variable crucial para entender los ciclos de vida de los productos y los posibles momentos de recompra. El gráfico diferencia entre:- **Clientes que ya no están vinculados al vehículo (`still_owner = FALSE`, en naranja)**: se observa una **frecuencia muy alta de vinculaciones de duración extremadamente corta**, con un pico masivo justo después de los 0 días. Esto podría deberse a múltiples factores como devoluciones tempranas, contratos de muy corta duración, vehículos de demostración o incluso particularidades en el registro de datos que necesitarían más contexto para ser interpretados correctamente. Después de este pico inicial, la frecuencia de desvinculaciones disminuye a medida que aumenta el tiempo de posesión.- **Clientes que aún están vinculados al vehículo (`still_owner = TRUE`, en cian)**: se aprecia una **concentración importante de clientes cuya vinculación actual se sitúa alrededor de los 500-600 días** (aproximadamente 1.5 años). Esto indica que un segmento considerable de la base de clientes activos ha tenido su vehículo por este lapso. La distribución también muestra clientes con vinculaciones más largas y más cortas, reflejando la diversidad en la antigüedad de la flota activa.Identificar las causas detrás del pico de desvinculaciones tempranas y entender el comportamiento del grupo que actualmente lleva alrededor de 1.5 años con su vehículo podría ofrecer insights valiosos para estrategias de retención y recompra.## `vehicle_data`### Estructura, distribución y nulos```{r}glimpse(vehicle_data) summary(vehicle_data) resumen_nulos(vehicle_data)```Es importante notar que las columnas relacionadas con las dimensiones físicas del vehículo (`height_range`, `height_unit`, `width_range`, `width_unit`, `length_range`, `length_unit`) presentan un **porcentaje considerable de valores nulos** (entre el 16.66% y el 16.97%). También `date_of_first_registration` tiene un 6.97% de nulos. Estas ausencias **deberán ser gestionadas** en la fase de limpieza de datos si decidimos utilizar estas variables para la predicción. La estrategia de imputación o manejo de estos NAs dependerá de la importancia predictiva que se les atribuya.### Unicidad```{r}sapply(vehicle_data, function(x) length(unique(x)))```Hay prácticamente un `vehicle_id` por fila, como se espera. Si bien **un `vehicle_id` debería ser único**, esta pequeña cantidad de duplicados podría deberse a errores de entrada o a una lógica de negocio específica que no conocemos, o quizá la presencia de vehículos de segunda mano vendidos por el concesionario oficial de Mercedes. Las demás columnas muestran una unicidad esperada: alta para fechas y baja para categorías como `class_bodytype` o las unidades de medida.### `class_bodytypes` más comunes y distribución de fechas```{r}#| warning: false# Visualización de class_bodytype (Top N)top_n_bodytypes <-15p_bodytype_dist <- vehicle_data |>count(class_bodytype, sort =TRUE) |>slice_head(n = top_n_bodytypes) |>ggplot(aes(x =reorder(class_bodytype, n), y = n)) +geom_col(fill ="cyan4") +coord_flip() +labs(title =paste("Top", top_n_bodytypes, "Class Bodytypes en vehicle_data"),x ="Class Bodytype", y ="Frecuencia") +theme_minimal()p_bodytype_dist```El gráfico muestra la **distribución de los 15 `class_bodytype` más frecuentes** en el conjunto de datos `vehicle_data`.Se observa que `A_class_hatchback` es el tipo de carrocería más común, seguido de cerca por `V_class_van` y `C_class_estate`. La lista también incluye una variedad de otros tipos como SUVs (por ejemplo, `GLC_class_suv`, `GLA_class_suv`), sedanes (`C_class_sedan`, `E_class_sedan`) y coupés. Esto indica una diversidad en los tipos de vehículos registrados en este dataset, con una predominancia de hatchbacks, vans y station wagons.```{r}#| warning: false# Pueden ser útiles para calcular la edad del vehículo.# Distribución de date_of_technical_approvalp_date_tech <-ggplot(vehicle_data, aes(x = date_technical_approval)) +geom_histogram(bins =50, fill ="darkgoldenrod1", color ="black") +labs(title ="Distribución de fecha aprobación técnica", x ="Fecha", y ="Frecuencia") +theme_minimal()p_date_tech# Distribución de date_of_first_registrationp_date_reg <-ggplot(vehicle_data, aes(x = date_of_first_registration)) +geom_histogram(bins =50, fill ="darkseagreen", color ="black") +labs(title ="Distribución de fecha primer registro", x ="Fecha", y ="Frecuencia") +scale_y_continuous(labels = scales::comma) +theme_minimal()p_date_reg```Los histogramas muestran la distribución temporal de `date_technical_approval` y `date_of_first_registration`.- **`date_technical_approval` (aprobación técnica, en naranja):** la mayoría de las aprobaciones técnicas se concentran en años recientes, con un **incremento notable a partir de aproximadamente 2010 y un pico en el periodo 2020-2023**. Hay muy pocas aprobaciones registradas antes del año 2000.- **`date_of_first_registration` (primer registro, en verde):** sigue un patrón muy similar al de la aprobación técnica. La **mayoría de los vehículos fueron registrados por primera vez en años recientes**, también con un **fuerte aumento desde 2010 y alcanzando su máximo alrededor de 2020-2023**.Ambas distribuciones sugieren que el conjunto de datos `vehicle_data` está compuesto predominantemente por **vehículos relativamente nuevos**. Estas fechas son fundamentales para calcular la antigüedad de los vehículos, una variable que podría ser importante. La tendencia creciente en ambas fechas podría reflejar un aumento en las ventas de vehículos, una mayor digitalización de los registros o una combinación de factores.En general, se trata de un dataset principalmente descriptivo. Clave para obtener **`class_bodytype`** y potencialmente la **edad del vehículo** o las dimensiones. Los NAs en dimensiones y en **`date_of_first_registration` son el principal punto a tratar**, ya que hay bastantes (16-17%).## ConclusionesEste análisis exploratorio ha revelado varios aspectos clave sobre los datos que serán cruciales para las fases de preprocesamiento y modelado. Las siguientes conclusiones y decisiones guiarán los próximos pasos:1. **Enfoque del proyecto y relevancia de los datos:** - **Objetivo principal:** el proyecto se centra en predecir la **próxima fecha de recompra** (`next_repurchase_date`) y el **modelo de vehículo** (`next_class_bodytype`) para **clientes particulares ya existentes (B2C)**. - **Filtrado de roles:** consecuentemente, en `customer_vehicle_pt`, solo se considerarán las vinculaciones donde el rol del cliente es `OWNER`. Otros roles (e.g., `USER`, `KEEPER`) se excluirán, ya que no representan la compra directa por un particular, que es el foco del análisis. - **Simplificación de `baumuster`:** para la predicción del `next_class_bodytype`, la granularidad del `baumuster` completo es excesiva y propensa a errores. Se utilizará el `baumuster_4` (los primeros 4 dígitos del `baumuster`), que define la clase y el tipo de carrocería del vehículo. Esta generalización también ayuda a mitigar el impacto de los errores de digitación observados.2. **Calidad de datos e inconsistencias identificadas:** - **`wishlist_pt`:** - **Errores de digitación en `baumuster`:** se detectaron `baumuster` de 8 dígitos que parecen ser versiones de 7 dígitos con un cero adicional al final (principalmente en `wish_type = "STOCK"`). La decisión de usar `baumuster_4` hace que estos errores específicos sean menos problemáticos para el objetivo de predicción de `class_bodytype`. - **Inconsistencia `baumuster` vs. `vehicle_name`:** existen 23 `baumuster` asociados a múltiples `vehicle_name` (aunque semánticamente muy similares). Esto subraya la necesidad de estandarizar la información del vehículo, y el uso de `baumuster_4` es un paso en esta dirección. - **`damage_pt`:** - **Inconsistencia `service_type_code` vs. `service_type_description`:** se encontraron 9 `service_type_code` con más de una `service_type_description` asociada. Se requerirá una estandarización, probablemente eligiendo la descripción más frecuente o una versión generalizada para cada código durante la limpieza de datos.3. **Patrones observados:** - **`customer_vehicle_pt` (datos de vinculación cliente-vehículo):** - **Significado de NAs en `valid_to`:** los valores nulos en `valid_to` indican que el cliente **aún posee el vehículo**, lo cual es fundamental para calcular la duración actual de la posesión y para identificar la población de clientes activos. - **Duración de la posesión (`ownership_duration_days`):** - Se observa un **pico muy alto de vinculaciones de corta duración** para clientes que ya no poseen el vehículo (`still_owner = FALSE`). Esto podría deberse a ventas rápidas, vehículos de demostración, o errores en los datos, y necesitará una consideración especial, ya que estos casos podrían no ser representativos para un modelo de recompra a largo plazo. - Para los propietarios actuales (`still_owner = TRUE`), existe una **concentración notable alrededor de los 500-600 días (aprox. 1.5 años) de posesión**, sugiriendo un ciclo de vida común o un grupo importante de clientes en esta etapa. - **Fecha de inicio de vinculación (`valid_from`):** se detectó un **incremento significativo en las vinculaciones a finales de 2023 y principios de 2024**. - **`vehicle_data` (datos descriptivos de vehículos):** - **Nulos en dimensiones y fechas:** las columnas de dimensiones (`height_range`, `width_range`, `length_range`) y `date_of_first_registration` presentan un porcentaje considerable de NAs (6.97% para la fecha, \~16-17% para dimensiones). Se deberá abordar mediante imputación o exclusión estratégica si estas variables resultan importantes. La `date_of_first_registration` es clave para calcular la antigüedad del vehículo. - **Predominio de vehículos recientes:** las distribuciones de `date_technical_approval` y `date_of_first_registration` indican que el dataset está compuesto mayoritariamente por vehículos más nuevos (posteriores a 2010, con picos en 2020-2023). - **`wishlist_pt` (lista de deseos):** - La mayoría de los clientes tienen 1-2 vehículos únicos en su wishlist. Un pequeño grupo presenta un número mucho mayor, lo que podría indicar perfiles de usuario atípicos. - Los vehículos eléctricos/híbridos ("EQ") y los configurables son populares, reflejando tendencias del mercado. - La mayoría de las wishlists no se actualizan o se actualizan poco después de su creación.4. **Próximos pasos en preprocesamiento y Feature Engineering:** - **Estandarización:** aplicar las correcciones y estandarizaciones mencionadas para `baumuster`, `vehicle_name`, y `service_type_description`. - **Manejo de NAs:** definir estrategias para los NAs en `vehicle_data` (especialmente `date_of_first_registration`). - **Creación de variables:** - Antigüedad del vehículo. - Duración de posesión (actual y pasadas). - Frecuencia de visitas al taller, tipos de daño/servicio. - Historial de vehículos por cliente. - **Agregación de datos:** consolidar la información a nivel de cliente para el modelado. - **Segmentación/exclusión:** considerar si los clientes con patrones de posesión extremadamente cortos o actividad atípica en la wishlist deben ser tratados de forma diferente o excluidos.