3  Limpieza de los datos

Author

Rubén Oliva Zamora

Esta fase se centra en las primeras etapas de limpieza y transformación de los datos. Incluye la estandarización de identificadores clave, la eliminación de columnas redundantes o no informativas y la preparación general de los datasets para análisis más profundos y la ingeniería de características.

3.1 Eliminación de datos

3.1.1 Eliminación de la columna country_code

Dado que los conjuntos de datos con el sufijo _pt (por ejemplo, wishlist_pt, customer_vehicle_pt) se refieren exclusivamente a datos de Portugal, la columna country_code es redundante en estos contextos. Por lo tanto, se eliminará para simplificar los dataframes.

wishlist_pt_processed$country_code <- NULL
customer_vehicle_pt_processed$country_code <- NULL

3.1.2 Eliminación de clientes con actividad anómala en wishlist

En el EDA se observó que algunos clientes tienen un número muy elevado de entradas en la wishlist. Estos podrían ser casos atípicos (por ejemplo, empleados realizando pruebas, bots, o usuarios sin una intención de compra clara) que no representan a un comprador estándar. Para mitigar el impacto de estos outliers, se procederá a eliminar todas las entradas de la wishlist correspondientes a clientes que superen un umbral de actividad definido. En este caso, ese umbral será de 15 entradas.

# Contar el número de entradas en la wishlist por cada cliente
customer_wishlist_activity <- wishlist_pt_processed |>
  group_by(customer_id) |>
  summarise(n_wishlist_entries = n(), .groups = 'drop')

# Definir el umbral para considerar actividad anómala
umbral_anomalia_wishlist <- 15

# Identificar los customer_id que superan el umbral
clientes_a_excluir_ids <- customer_wishlist_activity |>
  filter(n_wishlist_entries > umbral_anomalia_wishlist) |>
  pull(customer_id) # Extraer solo los IDs de los clientes
  
# Eliminar todas las entradas de la wishlist pertenecientes a estos clientes anómalos
wishlist_pt_processed <- wishlist_pt_processed |>
  filter(!(customer_id %in% clientes_a_excluir_ids))

3.1.3 Enfoque en class_bodytype y clientes OWNER

Para alinear los datos con los objetivos del análisis, se realizarán dos ajustes principales:

  1. Eliminación de vehicle_type: la columna vehicle_type en vehicle_data_processed se eliminará. El análisis se centrará en class_bodytype, que proporciona una categorización más relevante para los objetivos de predicción de modelo.
  2. Filtrado de clientes por rol: en customer_vehicle_pt_processed, se conservarán únicamente los registros donde el role del cliente es OWNER. Esto enfoca el análisis en los propietarios directos de vehículos, que son el grupo de interés para el caso de uso de recompra. Posteriormente, la columna role se eliminará, ya que solo contendrá un único valor (OWNER).
vehicle_data_processed$vehicle_type <- NULL
customer_vehicle_pt_processed <- customer_vehicle_pt_processed |> filter(role == "OWNER")

# Como ya solo tengo Owner, esa columna no me aporta nada
customer_vehicle_pt_processed$role <- NULL

3.2 Traslación de dimensiones de vehículos a otro dataset: bodytype_dimensions

Las dimensiones físicas de los vehículos (alto, ancho, largo) podrían ofrecer información relevante para el análisis. En caso de ser utilizadas, su valor es mayor si se consideran de forma agregada por class_bodytype, ya que vehículos de la misma clase y tipo de carrocería tienden a tener dimensiones similares. Por ello, se crea un dataset bodytype_dimensions que consolida esta información.

Es importante destacar que la utilidad final de bodytype_dimensions está por determinarse. Existe la posibilidad de que este conjunto de datos no se utilice en las fases posteriores del modelado. Consecuentemente, los valores nulos (NAs) presentes en estas dimensiones (aproximadamente un 23% después de la agregación inicial) podrían no llegar a ser imputados si las variables no se consideran críticas.

bodytype_dimensions <- vehicle_data_processed |>
  select(
    class_bodytype,
    height_range,
    height_unit,
    width_range,
    width_unit,
    length_range,
    length_unit   
  )

vehicle_data_processed <- vehicle_data_processed |> select(-height_range, -height_unit, -width_range, -width_unit, -length_range, -length_unit)

Los rangos de dimensiones originales se transforman en valores numéricos mínimos y máximos para cada class_bodytype. Esta representación permite capturar la variabilidad dimensional dentro de cada categoría. Las unidades de medida se omiten, puesto que todas las dimensiones están expresadas en milímetros (mm), el estándar para estos datos, eliminando la necesidad de conversión o de mantener la columna de unidad.

bodytype_dimensions <- bodytype_dimensions |>
  # 1. Separar height_range
  separate(
    col = height_range,                       
    into = c("height_min", "height_max"),     
    sep = "-",                                
    remove = TRUE,                            # TRUE para eliminar la columna original (height_range)
    convert = TRUE                            # TRUE para intentar convertir las nuevas columnas a tipos adecuados (numérico en este caso)
  ) |>
  separate(
    col = width_range,
    into = c("width_min", "width_max"),
    sep = "-",
    remove = TRUE,
    convert = TRUE
  ) |>
  # 3. Separar length_range
  separate(
    col = length_range,
    into = c("length_min", "length_max"),
    sep = "-",
    remove = TRUE,
    convert = TRUE
  )

unique(c(bodytype_dimensions$height_unit, bodytype_dimensions$width_unit, bodytype_dimensions$length_unit))
[1] NA   "mm"
# Todos son mm, no hay por qué tener estos datos
bodytype_dimensions <- bodytype_dimensions |> select(-height_unit, -width_unit, -length_unit)

head(bodytype_dimensions)
# A tibble: 6 × 7
  class_bodytype height_min height_max width_min width_max length_min length_max
  <chr>               <int>      <int>     <int>     <int>      <int>      <int>
1 GLK_class_suv          NA         NA        NA        NA         NA         NA
2 GLC_class_suv        1620       1664      2096      2096       4656       4656
3 A_class_hatch…       1452       1452      1992      1992       4419       4419
4 B_class_hatch…       1550       1567      2020      2020       4419       4419
5 CLA_class_cou…       1450       1450      1999      1999       4688       4688
6 GLE_class_cou…       1730       1730      2157      2157       4939       4939
bodytype_dimensions <- bodytype_dimensions |>
  group_by(class_bodytype) |>  # Group the data by the body type
  summarise(
    height_min = min(height_min, na.rm = TRUE),
    height_max = max(height_max, na.rm = TRUE),
    width_min = min(width_min, na.rm = TRUE),
    width_max = max(width_max, na.rm = TRUE),
    length_min = min(length_min, na.rm = TRUE),
    length_max = max(length_max, na.rm = TRUE)
  )
Warning: There were 80 warnings in `summarise()`.
The first warning was:
ℹ In argument: `height_min = min(height_min, na.rm = TRUE)`.
ℹ In group 6: `class_bodytype = "CLC_class_coupe"`.
Caused by warning in `min()`:
! no non-missing arguments to min; returning Inf
ℹ Run `dplyr::last_dplyr_warnings()` to see the 79 remaining warnings.
cols_to_clean <- c("height_min", "height_max", "width_min",
                   "width_max", "length_min", "length_max")

# Cambios infinitos a NA
bodytype_dimensions[cols_to_clean] <- lapply(bodytype_dimensions[cols_to_clean], function(column) {
  column[is.infinite(column)] <- NA
  return(column)
})

head(bodytype_dimensions)
# A tibble: 6 × 7
  class_bodytype height_min height_max width_min width_max length_min length_max
  <chr>               <dbl>      <dbl>     <dbl>     <dbl>      <dbl>      <dbl>
1 A_class_hatch…       1405       1452      1780      2022       4292       4453
2 A_class_sedan        1411       1458      1992      1992       4549       4570
3 B_class_hatch…       1545       1574      1786      2020       4359       4419
4 CLA_class_cou…       1402       1450      1999      2032       4630       4701
5 CLA_class_sho…       1405       1453      1999      2032       4630       4701
6 CLC_class_cou…         NA         NA        NA        NA         NA         NA

3.3 Estandarización de datos

3.3.1 Estandarización del identificador de vehículo (baumuster)

Para asegurar la consistencia y facilitar el análisis entre los diferentes conjuntos de datos, se adoptará el baumuster_4 como el identificador estándar para los modelos de vehículos. Este corresponde a los primeros cuatro dígitos del baumuster original (presente como baumuster_6 en la mayoría de los datasets o como baumuster en wishlist_pt) y representa la clase y el tipo de carrocería. Esta simplificación también ayuda a mitigar el impacto de las inconsistencias y errores de digitación identificados durante el EDA. Las columnas baumuster originales más largas serán procesadas para extraer el baumuster_4 y luego eliminadas.

wishlist_pt_processed$baumuster <- NULL

# Obtengo los 4 primeros caracteres de baumuster_6
vehicle_data_processed <- vehicle_data_processed |> 
  mutate(baumuster_4 = as.numeric(substr(baumuster_6, 1, 4)), .after=baumuster_6)

vehicle_data_processed$baumuster_6 <- NULL

3.3.2 Estandarización de service_type_code y service_type_description

Durante el Análisis Exploratorio de Datos (EDA) en el conjunto damage_pt, se identificó una inconsistencia donde algunos service_type_code estaban asociados a múltiples service_type_description, aunque estas descripciones eran semánticamente muy similares. Para asegurar la consistencia y facilitar análisis futuros, se procederá a estandarizar service_type_description. La estrategia adoptada es asignar a cada service_type_code la service_type_description más frecuente observada en los datos. Esto unificará las descripciones para un mismo código de servicio.

# Creamos el mapeo de service_type_code a la descripción más frecuente
service_desc_mapping <- damage_pt |>
  count(service_type_code, service_type_description, sort = TRUE, name = "freq") |>
  group_by(service_type_code) |>
  # Seleccionamos la descripción con la mayor frecuencia para cada código
  slice_max(order_by = freq, n = 1, with_ties = FALSE) |>
  ungroup() |>
  # Renombramos la columna de descripción para evitar conflicto durante el join inicial
  select(service_type_code, new_service_description = service_type_description)

# Unimos el mapeo a damage_pt_processed
damage_pt_processed <- damage_pt_processed |>
  left_join(service_desc_mapping, by = "service_type_code") |>
  # Eliminamos la descripción original inconsistente
  select(-service_type_description) |>
  # Renombramos la nueva columna estandarizada para que tenga el nombre original
  rename(service_type_description = new_service_description)

3.3.3 Unificación de la identificación de vehículos mediante class_bodytype

El EDA reveló que la columna vehicle_name presentaba variaciones e inconsistencias para un mismo baumuster_4, lo que dificulta su uso como identificador de modelo. Para garantizar una categorización uniforme y reducir la redundancia, se utilizará class_bodytype (derivado de los primeros cuatro dígitos de baumuster). De esta forma, se mejora la calidad y coherencia de los datos, además de que se simplifica el análisis posterior al considerar solo aquello que cae dentro del caso de uso.

# Paso 1: Seleccionar las columnas necesarias de vehicle_data y asegurar unicidad
vehicle_info <- vehicle_data_processed |>
  select(baumuster_4, class_bodytype) |>
  distinct(baumuster_4, .keep_all = TRUE) # Mantiene la primera aparición si hay duplicados de baumuster_4 con diferentes class_bodytype

# Paso 2: Unir wishlist_pt con vehicle_info para añadir class_bodytype
wishlist_pt_processed <- wishlist_pt_processed |>
  left_join(vehicle_info, by = "baumuster_4")

# Paso 3: Eliminar la columna original vehicle_name
wishlist_pt_processed <- wishlist_pt_processed |>
  select(-vehicle_name)

3.4 Ajustes de tipos de datos

Para que los análisis y modelos posteriores manejen correctamente las categorías, se convierten en factores algunas variables clave:

  • repair_cost_category: se transforma en un factor ordenado, de menor a mayor coste de reparación, para preservar la jerarquía semántica.
  • wish_type: se convierte en un factor nominal, facilitando su uso como variable categórica en descripciones y modelos.
# 1. Convertir repair_cost_category a factor ordenado en damage_pt_processed
cost_levels <- c("Low cost", "Medium-low cost", "Medium-high cost", "High cost")
damage_pt_processed <- damage_pt_processed |>
  mutate(repair_cost_category = factor(repair_cost_category, levels = cost_levels, ordered = TRUE))

# 2. Convertir wish_type a factor en wishlist_pt_processed
wishlist_pt_processed <- wishlist_pt_processed |>
  mutate(wish_type = factor(wish_type))

3.5 Manejo de NAs

  1. NAs en customer_vehicle_pt$valid_to (~39%):
    • Como se discutió en el EDA, estos NAs son informativos (indican posesión actual). No deben ser eliminados ni imputados.
  2. NAs en dimensiones de bodytype_dimensions (~23%):
    • Potencialmente no se acaben usando los datos de las dimensiones de los vehículos, pero si sea caban usando habría que aplicar una estrategia de imputación aquí. Una buena idea para obtener datos reales es, por ejemplo, buscar referencias de ellos en internet y, a partir de esa referencia, sacar alguna variable estadística que represente a groso modo las dimensiones de ese bodytype. Aunque, como he mencionado, es posible que no se utilicen en el modelado final, por lo que la imputación podría no ser necesaria.
  3. NAs en vehicle_data$date_of_first_registration (~7%):
    • Estos NAs son problemáticos si queremos calcular la edad exacta del vehículo. Dado que la date_of_first_registration suele ocurrir poco después de la date_technical_approval, se ha decidido imputar los valores faltantes.

3.5.1 Imputación de date_of_first_registration basada en date_technical_approval

Para ello, primero se calcula la diferencia en días entre date_of_first_registration y date_technical_approval para todos los casos donde ambas fechas están presentes.

# 1) Calculamos para los casos no NA la diferencia en días
lag_days <- as.numeric(
  difftime(
    vehicle_data_processed$date_of_first_registration,
    vehicle_data_processed$date_technical_approval,
    units = "days"
  )
)

Luego, se utiliza la mediana de estas diferencias para estimar los NA en date_of_first_registration. Se prefiere la mediana sobre la media porque es más robusta a valores atípicos (outliers) que podrían existir en las diferencias de tiempo, asegurando una imputación más estable y representativa del patrón general.

median_lag <- median(lag_days, na.rm = TRUE)

# 2) Imputamos los NA como date_technical_approval + mediana de días
vehicle_data_processed <- vehicle_data_processed |>
  mutate(
    date_of_first_registration = if_else(
      is.na(date_of_first_registration),
      date_technical_approval + days(median_lag),
      date_of_first_registration
    )
  )

Finalmente, comprobamos que no queden NAs en date_of_first_registration:

summary(vehicle_data_processed)
  vehicle_id         baumuster_4   class_bodytype     date_technical_approval
 Length:1308497     Min.   :1070   Length:1308497     Min.   :1970-07-09     
 Class :character   1st Qu.:1770   Class :character   1st Qu.:2017-10-12     
 Mode  :character   Median :2132   Mode  :character   Median :2020-02-16     
                    Mean   :2563                      Mean   :2019-07-19     
                    3rd Qu.:2539                      3rd Qu.:2022-06-01     
                    Max.   :9106                      Max.   :2024-12-17     
                                                      NA's   :5              
 date_of_first_registration
 Min.   :1970-08-07        
 1st Qu.:2017-12-06        
 Median :2020-06-16        
 Mean   :2019-09-20        
 3rd Qu.:2022-08-16        
 Max.   :2025-12-02        
 NA's   :4                 

Quedan 4-5 NAs en date_of_first_registration, que son los que ya teníamos en date_technical_approval. Estos, al ser tan pocos, serán eliminados.

# Eliminamos las filas donde date_of_first_registration sigue siendo NA
vehicle_data_processed <- vehicle_data_processed |>
  filter(!is.na(date_of_first_registration)) |> filter(!is.na(date_technical_approval))

summary(vehicle_data_processed)
  vehicle_id         baumuster_4   class_bodytype     date_technical_approval
 Length:1308492     Min.   :1070   Length:1308492     Min.   :1970-07-09     
 Class :character   1st Qu.:1770   Class :character   1st Qu.:2017-10-12     
 Mode  :character   Median :2132   Mode  :character   Median :2020-02-16     
                    Mean   :2563                      Mean   :2019-07-19     
                    3rd Qu.:2539                      3rd Qu.:2022-06-01     
                    Max.   :9106                      Max.   :2024-12-17     
 date_of_first_registration
 Min.   :1970-08-07        
 1st Qu.:2017-12-06        
 Median :2020-06-16        
 Mean   :2019-09-20        
 3rd Qu.:2022-08-16        
 Max.   :2025-12-02