2  Análisis exploratorio de los datos

Author

Rubén Oliva Zamora

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

2.1.1 Estructura, distribución y nulos

# Función auxiliar para resumir NAs
resumen_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)
}

# Estructura
glimpse(wishlist_pt)
Rows: 1,383
Columns: 8
$ customer_id      <chr> "427c9d2ac87f9131ac19ab995ade04117205f23a23fc06039ee3…
$ country_code     <chr> "PT", "PT", "PT", "PT", "PT", "PT", "PT", "PT", "PT",…
$ vehicle_name     <chr> "GLA 250 e com tecnologia híbrida EQ", "GLA 250 e com…
$ baumuster        <dbl> 2477851, 2477851, 2140041, 2946111, 1186121, 2060421,…
$ baumuster_4      <dbl> 2477, 2477, 2140, 2946, 1186, 2060, 1770, 2546, 1186,…
$ wish_create_date <date> 2025-02-07, 2025-02-03, 2024-08-30, 2025-01-26, 2025…
$ wish_update_date <date> 2025-02-07, 2025-02-03, 2024-08-30, 2025-01-26, 2025…
$ wish_type        <chr> "STOCK", "CONFIGURATION", "STOCK", "STOCK", "STOCK", …
# Distribución
summary(wishlist_pt)
 customer_id        country_code       vehicle_name         baumuster       
 Length:1383        Length:1383        Length:1383        Min.   : 1183101  
 Class :character   Class :character   Class :character   1st Qu.: 2062041  
 Mode  :character   Mode  :character   Mode  :character   Median : 2363621  
                                                          Mean   : 3257251  
                                                          3rd Qu.: 2546091  
                                                          Max.   :46531010  
  baumuster_4   wish_create_date     wish_update_date      wish_type        
 Min.   :1183   Min.   :2024-08-26   Min.   :2024-08-26   Length:1383       
 1st Qu.:2060   1st Qu.:2024-10-20   1st Qu.:2024-11-04   Class :character  
 Median :2363   Median :2024-12-26   Median :2025-01-03   Mode  :character  
 Mean   :2324   Mean   :2024-12-10   Mean   :2024-12-16                     
 3rd Qu.:2543   3rd Qu.:2025-02-02   3rd Qu.:2025-02-06                     
 Max.   :4656   Max.   :2025-02-26   Max.   :2025-02-26                     
# Nulos
resumen_nulos(wishlist_pt)
                 NA_number NA_percentage
customer_id              0             0
country_code             0             0
vehicle_name             0             0
baumuster                0             0
baumuster_4              0             0
wish_create_date         0             0
wish_update_date         0             0
wish_type                0             0

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.

2.1.1.1 Investigación del máximo baumuster

max_baum <- wishlist_pt |> filter(baumuster == max(baumuster)); max_baum
# A tibble: 1 × 8
  customer_id   country_code vehicle_name baumuster baumuster_4 wish_create_date
  <chr>         <chr>        <chr>            <dbl>       <dbl> <date>          
1 1b37480033be… PT           G 450 d       46531010        4653 2024-09-08      
# ℹ 2 more variables: wish_update_date <date>, wish_type <chr>
wishlist_pt |> filter(vehicle_name == max_baum$vehicle_name)
# 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 caracteres
wishlist_pt |> filter(baumuster >= 10000000)
# A tibble: 65 × 8
   customer_id  country_code vehicle_name baumuster baumuster_4 wish_create_date
   <chr>        <chr>        <chr>            <dbl>       <dbl> <date>          
 1 a827fcb7b91… PT           GLE 350 de …  16730610        1673 2024-11-23      
 2 b06f4ef2882… PT           E 300 de St…  21420810        2142 2024-08-31      
 3 0b03b23f2a1… PT           EQE 350+ SU…  29462110        2946 2024-08-27      
 4 54720140877… PT           CLA 200 d C…  11831210        1183 2025-01-23      
 5 54720140877… PT           CLA 250e Co…  11838510        1183 2024-12-16      
 6 04897a77433… PT           CLA 250e Sh…  11868510        1186 2025-01-10      
 7 04897a77433… PT           GLA 250 e c…  24778510        2477 2025-01-10      
 8 9f1b5bc5dfb… PT           A 200         17708710        1770 2025-01-09      
 9 c4bf3f137ed… PT           CLE 200 Cou…  23635010        2363 2024-11-20      
10 3d1c5bdc02c… PT           GLC 220 d 4…  25430510        2543 2024-10-01      
# ℹ 55 more rows
# ℹ 2 more variables: wish_update_date <date>, wish_type <chr>
wishlist_pt |> filter(baumuster <= 1000000)
# A tibble: 0 × 8
# ℹ 8 variables: customer_id <chr>, country_code <chr>, vehicle_name <chr>,
#   baumuster <dbl>, baumuster_4 <dbl>, wish_create_date <date>,
#   wish_update_date <date>, wish_type <chr>

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.

2.1.1.2 Análisis de longitud de baumuster

wishlist_pt <- wishlist_pt |> mutate(baumuster_len = str_length(baumuster)) # Añadir columna temporal para análisis

wishlist_pt |> count(baumuster_len, sort = TRUE)
# A tibble: 2 × 2
  baumuster_len     n
          <int> <int>
1             7  1318
2             8    65
# Relación entre longitud de baumuster y wish type
wishlist_pt |> count(wish_type, baumuster_len)
# A tibble: 3 × 3
  wish_type     baumuster_len     n
  <chr>                 <int> <int>
1 CONFIGURATION             7   750
2 STOCK                     7   568
3 STOCK                     8    65
# Verificar si los de 8 dígitos terminan en 0
wishlist_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
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.

2.1.2 Unicidad

# Valores únicos por columna
sapply(wishlist_pt, function(x) length(unique(x)))
     customer_id     country_code     vehicle_name        baumuster 
             436                1              166              188 
     baumuster_4 wish_create_date wish_update_date        wish_type 
              36              179              175                2 
# Comprobar si hay más de un vehicle_name por baumuster
baumuster_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 individualmente
resultados_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).

wishlist_pt |> filter(wish_create_date > wish_update_date)
# A tibble: 0 × 8
# ℹ 8 variables: customer_id <chr>, country_code <chr>, vehicle_name <chr>,
#   baumuster <dbl>, baumuster_4 <dbl>, wish_create_date <date>,
#   wish_update_date <date>, wish_type <chr>

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 X
mis_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 baumuster
p1 <- 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ífica

p2

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 conteo

p_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 <- 15
p_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ón
p_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ón
p_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ón
wishlist_pt <- wishlist_pt |>
  mutate(update_lag_days = as.numeric(difftime(wish_update_date, wish_create_date, units = "days")))

# Distribución de la diferencia de tiempo
p_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áficos
p_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.

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)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  1.000   1.000   1.000   1.954   2.000  24.000 
summary(customer_wishlist_counts$n_total_wishlist_entries)
   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 cliente
p_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 breaks
p_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
# A tibble: 13 × 3
   customer_id                     n_distinct_vehicles_…¹ n_total_wishlist_ent…²
   <chr>                                            <int>                  <int>
 1 9caa8e8d21e3d94e824828b2e776e4…                     24                     38
 2 0395d24c13a66e0947b086010abb27…                     17                     34
 3 54720140877ee20066cb384ddea405…                     16                     48
 4 852f15bd5d3979f22fc9e8a143c003…                     10                     21
 5 a7f72d1afb4673ee8a01a1a1d2c9fd…                      9                     30
 6 93a63b8005425f7a33f088001553d4…                      8                     11
 7 230132673731878af23486b434e8af…                      7                     12
 8 acbba20167f4c1e2a9b8f2eaad8eb6…                      7                      9
 9 034d7f238a0ae3756ef08223bbc71f…                      6                     12
10 0671656754b2125cd12dad59c077df…                      6                      6
# ℹ 3 more rows
# ℹ abbreviated names: ¹​n_distinct_vehicles_in_wishlist,
#   ²​n_total_wishlist_entries

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.

2.2 damage_pt

2.2.1 Estructura, distribución y nulos

glimpse(damage_pt)
Rows: 80,990
Columns: 9
$ customer_id              <chr> "9920961282effd75cb7e39f8003dd23ceacd84b67252…
$ vehicle_id               <chr> "caad759ba28a7617a76f1d8dd22d96e55f53d76098b8…
$ damage_loc_id            <chr> "54992", "00012", "09911", "00011", "49920", …
$ damage_type_description  <chr> "worn/worn out", "Service contract", "Broken"…
$ damage_mileage           <dbl> 68695, 52781, 68695, 68695, 68695, 18741, 603…
$ damage_repair_date       <date> 2024-10-07, 2023-11-30, 2024-10-07, 2024-10-…
$ service_type_code        <chr> "02", "12", "02", "12", "02", "03", "02", "02…
$ service_type_description <chr> "KDM (service measures/campaigns)", "service …
$ repair_cost_category     <chr> "Low cost", "High cost", "Low cost", "High co…
summary(damage_pt)
 customer_id         vehicle_id        damage_loc_id     
 Length:80990       Length:80990       Length:80990      
 Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character  
                                                         
                                                         
                                                         
 damage_type_description damage_mileage    damage_repair_date  
 Length:80990            Min.   :      1   Min.   :2016-09-13  
 Class :character        1st Qu.:  15445   1st Qu.:2022-11-17  
 Mode  :character        Median :  35674   Median :2023-11-21  
                         Mean   :  56218   Mean   :2023-05-27  
                         3rd Qu.:  73646   3rd Qu.:2024-07-01  
                         Max.   :1726898   Max.   :2025-02-26  
 service_type_code  service_type_description repair_cost_category
 Length:80990       Length:80990             Length:80990        
 Class :character   Class :character         Class :character    
 Mode  :character   Mode  :character         Mode  :character    
                                                                 
                                                                 
                                                                 
resumen_nulos(damage_pt)
                         NA_number NA_percentage
customer_id                      0             0
vehicle_id                       0             0
damage_loc_id                    0             0
damage_type_description          0             0
damage_mileage                   0             0
damage_repair_date               0             0
service_type_code                0             0
service_type_description         0             0
repair_cost_category             0             0

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.

2.2.2 Unicidad e inconsistencias

sapply(damage_pt, function(x) length(unique(x)))
             customer_id               vehicle_id            damage_loc_id 
                   14814                    18462                     3331 
 damage_type_description           damage_mileage       damage_repair_date 
                     272                    33242                     2469 
       service_type_code service_type_description     repair_cost_category 
                      10                       19                        4 

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.

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)
<list_of<
  tbl_df<
    service_type_code       : character
    service_type_description: character
    count                   : integer
  >
>[9]>
[[1]]
# A tibble: 2 × 3
  service_type_code service_type_description count
  <chr>             <chr>                    <int>
1 01                mobilty                  13501
2 01                Mobility                 12435

[[2]]
# A tibble: 2 × 3
  service_type_code service_type_description         count
  <chr>             <chr>                            <int>
1 02                KDM Campaign Claims              10342
2 02                KDM (service measures/campaigns)  9173

[[3]]
# A tibble: 2 × 3
  service_type_code service_type_description count
  <chr>             <chr>                    <int>
1 03                Warranty                  9879
2 03                warranty                  8089

[[4]]
# A tibble: 2 × 3
  service_type_code service_type_description count
  <chr>             <chr>                    <int>
1 04                parts warranty             266
2 04                Parts warranty             167

[[5]]
# A tibble: 2 × 3
  service_type_code service_type_description         count
  <chr>             <chr>                            <int>
1 07                ISP (integrated service package)   124
2 07                ISP                                 95

[[6]]
# A tibble: 2 × 3
  service_type_code service_type_description     count
  <chr>             <chr>                        <int>
1 08                POW                            996
2 08                POW (pre-owned car warranty)   786

[[7]]
# A tibble: 2 × 3
  service_type_code service_type_description count
  <chr>             <chr>                    <int>
1 11                KULA - Goodwill Claims     877
2 11                goodwill                   683

[[8]]
# A tibble: 2 × 3
  service_type_code service_type_description count
  <chr>             <chr>                    <int>
1 12                service contract          8293
2 12                Service contract          5054

[[9]]
# A tibble: 2 × 3
  service_type_code service_type_description count
  <chr>             <chr>                    <int>
1 13                other services             175
2 13                Other services              32

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 frecuente
service_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 frecuente
  ungroup() |>
  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 X
damage_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 combinada
ordered_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 texto
plot_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 y
  scale_y_continuous(labels = scales::percent, limits = c(0, 1.1)) + # Ajustar límites para dejar espacio al texto
  labs(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áfico
p_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.3 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.

# Distribución de damage_mileage
p_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ón
  labs(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_date
p_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_description
damage_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 ordenado
damage_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 deseado
  mutate(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ño
text_data_damage <- damage_pt_top_types |>
  distinct(damage_type_description, total_count)

# Gráfico de barras apiladas (proporciones) con anotación de recuento total
p_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ño
  scale_y_continuous(labels = scales::percent) + # Eje Y como porcentaje
  labs(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.

2.3 customer_vehicle_pt

2.3.1 Estructura, distribución y nulos

glimpse(customer_vehicle_pt)
Rows: 43,574
Columns: 6
$ customer_id  <chr> "34b870907b526c0b3138b76a983ccf5f5546d392fc78518443e6ea77…
$ vehicle_id   <chr> "93e5d41a6afcdc9f5d9c006d8dee671bee3b72bab6a65c14cc044762…
$ valid_from   <date> 2024-12-18, 2018-05-23, 2023-05-25, 2024-04-18, 2022-09-…
$ valid_to     <date> NA, 2018-05-23, 2023-09-26, NA, 2022-09-27, 2022-11-30, …
$ role         <chr> "OWNER", "LEGACY", "OWNER", "OWNER", "OWNER", "OWNER", "O…
$ country_code <chr> "PT", "PT", "PT", "PT", "PT", "PT", "PT", "PT", "PT", "PT…
summary(customer_vehicle_pt)
 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                                             
resumen_nulos(customer_vehicle_pt)
             NA_number NA_percentage
customer_id          0          0.00
vehicle_id           0          0.00
valid_from           0          0.00
valid_to         17018         39.06
role                 0          0.00
country_code         0          0.00

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.

2.3.2 Unicidad

sapply(customer_vehicle_pt, function(x) length(unique(x)))
 customer_id   vehicle_id   valid_from     valid_to         role country_code 
       14961        31024         2916         2859            4            1 

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 relevantes
                               max(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.

2.4 vehicle_data

2.4.1 Estructura, distribución y nulos

glimpse(vehicle_data)  
Rows: 1,308,497
Columns: 12
$ vehicle_id                 <chr> "00003f1ce208afbea3ebba38fe828e571705c36a04…
$ baumuster_6                <dbl> 204984, 253925, 177086, 247047, 118386, 167…
$ vehicle_type               <chr> "GLK 220 CDI 4MATIC", "GLC 350 d 4MATIC", "…
$ class_bodytype             <chr> "GLK_class_suv", "GLC_class_suv", "A_class_…
$ date_technical_approval    <date> 2015-03-16, 2019-06-23, 2021-02-16, 2022-1…
$ date_of_first_registration <date> 2015-03-26, 2019-07-22, 2021-02-23, 2023-0…
$ height_range               <chr> NA, "1620-1664", "1452-1452", "1550-1567", …
$ height_unit                <chr> NA, "mm", "mm", "mm", "mm", "mm", NA, "mm",…
$ width_range                <chr> NA, "2096-2096", "1992-1992", "2020-2020", …
$ width_unit                 <chr> NA, "mm", "mm", "mm", "mm", "mm", NA, "mm",…
$ length_range               <chr> NA, "4656-4656", "4419-4419", "4419-4419", …
$ length_unit                <chr> NA, "mm", "mm", "mm", "mm", "mm", NA, "mm",…
summary(vehicle_data)  
  vehicle_id         baumuster_6     vehicle_type       class_bodytype    
 Length:1308497     Min.   :107024   Length:1308497     Length:1308497    
 Class :character   1st Qu.:177087   Class :character   Class :character  
 Mode  :character   Median :213204   Mode  :character   Mode  :character  
                    Mean   :256329                                        
                    3rd Qu.:253905                                        
                    Max.   :910643                                        
                                                                          
 date_technical_approval date_of_first_registration height_range      
 Min.   :1970-07-09      Min.   :1986-03-06         Length:1308497    
 1st Qu.:2017-10-12      1st Qu.:2018-01-15         Class :character  
 Median :2020-02-16      Median :2020-05-06         Mode  :character  
 Mean   :2019-07-19      Mean   :2019-11-21                           
 3rd Qu.:2022-06-01      3rd Qu.:2022-05-23                           
 Max.   :2024-12-17      Max.   :2025-12-02                           
 NA's   :5               NA's   :91173                                
 height_unit        width_range         width_unit        length_range      
 Length:1308497     Length:1308497     Length:1308497     Length:1308497    
 Class :character   Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character  
                                                                            
                                                                            
                                                                            
                                                                            
 length_unit       
 Length:1308497    
 Class :character  
 Mode  :character  
                   
                   
                   
                   
resumen_nulos(vehicle_data)
                           NA_number NA_percentage
vehicle_id                         0          0.00
baumuster_6                        0          0.00
vehicle_type                    1755          0.13
class_bodytype                     0          0.00
date_technical_approval            5          0.00
date_of_first_registration     91173          6.97
height_range                  221991         16.97
height_unit                   221991         16.97
width_range                   218061         16.66
width_unit                    218061         16.66
length_range                  218061         16.66
length_unit                   218061         16.66

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.

2.4.2 Unicidad

sapply(vehicle_data, function(x) length(unique(x)))
                vehicle_id                baumuster_6 
                   1303588                       1827 
              vehicle_type             class_bodytype 
                      1910                         56 
   date_technical_approval date_of_first_registration 
                     10018                       6303 
              height_range                height_unit 
                       255                          2 
               width_range                 width_unit 
                        51                          2 
              length_range                length_unit 
                       215                          2 

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.3 class_bodytypes más comunes y distribución de fechas

# Visualización de class_bodytype (Top N)
top_n_bodytypes <- 15
p_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_approval
p_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_registration
p_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:

  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.