6  Modelo predictivo

Author

Rubén Oliva Zamora

6.1 Preparación de datos para el modelado de la regresión de time_until_next_purchase_days

El objetivo es crear un dataframe (model_data) donde cada fila represente un cliente con su última transacción de compra conocida. La variable objetivo será time_until_next_purchase_days. Las características predictoras se derivarán de su historial de wishlist, daños y vehículo actual.

6.1.1 Definición de la fecha de referencia

Este bloque define una fecha de referencia (ref_date) calculada como la fecha máxima observada entre los datos de daños, wishlist y transacciones de vehículos de clientes. Esta fecha es útil para cálculos consistentes de características basadas en el tiempo. La hemos usado anteriormente en otros capítulos, pero aquí vuelve a ser necesaseria.

ref_date <- max(c(
  max(damage_cleaned$damage_repair_date, na.rm = TRUE),
  max(wishlist_cleaned$wish_create_date, na.rm = TRUE),
  max(cv_with_targets$valid_from, na.rm = TRUE),
  max(cv_with_targets$valid_to, na.rm = TRUE)
), na.rm = TRUE)

print(paste("Reference Date:", ref_date))
[1] "Reference Date: 2025-02-27"

6.1.2 Base del conjunto de datos enriquecido para entrenamiento

El primer paso es crear un dataframe base (model_data_base) que contenga las transacciones de recompra conocidas (time_until_next_purchase_days no es NA). No se puede entrenar al modelo que prediga esa variable sin algunos ejemplos de esta misma variable. Por ello, obtengo un subconjunto de cv_with_targets donde time_until_next_purchase_days no es NA. Este dataframe contendrá las columnas customer_id, vehicle_id, valid_from y time_until_next_purchase_days.

Se añadirá también una variable transaction_id para identificar cada transacción unívocamente, ya que es posible que un cliente tenga varias transacciones el mismo día para un mismo vehículo.

cv_with_targets <- cv_with_targets |> mutate(transaction_id = row_number()) 

model_data_base <- cv_with_targets |>
  filter(!is.na(time_until_next_purchase_days)) |>
  # Seleccionamos columnas relevantes para empezar
  select(transaction_id, customer_id, vehicle_id, valid_from, time_until_next_purchase_days)

head(model_data_base)
# A tibble: 6 × 5
  transaction_id customer_id        vehicle_id valid_from time_until_next_purc…¹
           <int> <chr>              <chr>      <date>                      <dbl>
1              2 000b7927a082cf83d… 1ddcf7657… 2023-12-30                      0
2              3 000b7927a082cf83d… 14f34f861… 2023-12-30                      0
3              4 000b7927a082cf83d… 2e89db126… 2023-12-30                      0
4              5 000b7927a082cf83d… 9fa3436f0… 2023-12-30                      0
5              6 000b7927a082cf83d… f500f300b… 2023-12-30                      0
6              7 000b7927a082cf83d… f3c371bef… 2023-12-30                      0
# ℹ abbreviated name: ¹​time_until_next_purchase_days

6.1.3 Enriquecimiento de model_data_base

6.1.3.1 Vehículo actual (vehicle_data_cleaned)

Para cada transacción en model_data_base, necesitamos las características del vehicle_id asociado a esa transacción.

No nos hace falta el vehicle_id per se, sino el baumuster_4, forma numérica de identificar el class_bodytype (mejor para hacer regresión lineal) y la fecha de primera matriculación (date_of_first_registration) para poder calcular la antigüedad del vehículo en el momento de la transacción.

# Unir con vehicle_data_cleaned para obtener baumuster_4 y date_of_first_registration
vehicle_info_for_model <- vehicle_cleaned |>
  select(vehicle_id, baumuster_4, date_of_first_registration) |>
  distinct(vehicle_id, .keep_all = TRUE) # Asegurar unicidad

# Con date_of_first_registration, podemos calcular la antigüedad del vehículo en el momento de la transacción
model_data <- model_data_base |>
  left_join(vehicle_info_for_model, by = "vehicle_id") |>
  mutate(
    # Antigüedad del vehículo en el momento de la transacción
    vehicle_age_at_transaction_days = if_else(
      !is.na(date_of_first_registration),
      as.numeric(difftime(valid_from, date_of_first_registration, units = "days")),
      NA_real_
    )
  ) |>
  select(-date_of_first_registration) # Limpiar columnas intermedias

head(model_data)
# A tibble: 6 × 7
  transaction_id customer_id        vehicle_id valid_from time_until_next_purc…¹
           <int> <chr>              <chr>      <date>                      <dbl>
1              2 000b7927a082cf83d… 1ddcf7657… 2023-12-30                      0
2              3 000b7927a082cf83d… 14f34f861… 2023-12-30                      0
3              4 000b7927a082cf83d… 2e89db126… 2023-12-30                      0
4              5 000b7927a082cf83d… 9fa3436f0… 2023-12-30                      0
5              6 000b7927a082cf83d… f500f300b… 2023-12-30                      0
6              7 000b7927a082cf83d… f3c371bef… 2023-12-30                      0
# ℹ abbreviated name: ¹​time_until_next_purchase_days
# ℹ 2 more variables: baumuster_4 <dbl>, vehicle_age_at_transaction_days <dbl>

6.1.3.2 Historial de compras (customer_vehicle_pt_cleaned)

Para cada transacción, se examina el historial de compras del cliente para determinar cuántos vehículos ha poseído anteriormente y la duración de la posesión de su vehículo inmediatamente precedente. Esta información ayuda a contextualizar el ciclo de vida vehicular del cliente.

Específicamente, para cada transacción de un cliente, calcularemos:

  • num_purchases_before_this_one: el número de vehículos distintos que el cliente ha adquirido antes de la transacción actual. Se calcula contando las transacciones previas del mismo cliente.

  • previous_ownership_duration_days: la duración en días durante la cual el cliente poseyó su último vehículo antes del vehículo de la transacción actual. Este valor se obtiene calculando primero la duración de cada posesión y luego usando la función lag() para asignar la duración de la posesión anterior a la transacción actual. Para la primera compra de un cliente, este valor será NA (ya que no hay un vehículo anterior), que se manejará en pasos posteriores de preprocesamiento.

purchase_history <- cv_with_targets |>
  select(transaction_id, customer_id, vehicle_id, valid_from, valid_to) |>
  arrange(customer_id, valid_from) |> 
  group_by(customer_id) |> # Agrupar por customer_id
  mutate(
    num_purchases_before_this_one = row_number() - 1,
    # Duración de la posesión ANTERIOR
    # Primero, calcula la duración de cada posesión
    current_possession_ends_at = lead(valid_from), # Siguiente compra del mismo cliente
    # Si no hay siguiente compra, usa valid_to si existe, sino ref_date
    current_possession_duration_days = case_when(
      !is.na(current_possession_ends_at) ~ as.numeric(difftime(current_possession_ends_at, valid_from, units = "days")),
      !is.na(valid_to) ~ as.numeric(difftime(valid_to, valid_from, units = "days")), # Usar valid_to si está disponible
      TRUE ~ as.numeric(difftime(ref_date, valid_from, units = "days")) # Fallback a ref_date
    ),
    # Luego, usa lag() para obtener la duración de la posesión ANTERIOR para la fila actual
    previous_ownership_duration_days = lag(current_possession_duration_days)
  ) |>
  ungroup() # Desagrupar al final

purchase_history <- purchase_history |> select(-current_possession_ends_at, -current_possession_duration_days)

# Uno los nuevos datos a model_data para entrenamiento.
model_data <- model_data |>
  left_join(
    purchase_history |> select(transaction_id, num_purchases_before_this_one, previous_ownership_duration_days),
    by = "transaction_id"
  )

6.1.3.3 Lista de deseados (wishlist_cleaned)

Para incorporar la actividad de la lista de deseados en el modelo, se calcula una métrica agregada para cada cliente. Específicamente, se determina el número total de entradas (wl_total_entries_all_time) que cada cliente ha registrado en la wishlist_cleaned a lo largo de todo su historial conocido.

Este valor, que refleja la propensión general del cliente a utilizar la wishlist, se une luego al conjunto de datos model_data a través del customer_id. De esta manera, cada transacción de un cliente se asocia con su recuento total de actividad en la wishlist. Si un cliente no tiene entradas en la wishlist (resultando en NA tras la unión), su wl_total_entries_all_time se establece en 0.

# Calcular métricas de wishlist por cliente
wishlist_summary_per_customer <- wishlist_cleaned |>
  group_by(customer_id) |>
  summarise(
    wl_total_entries_all_time = n(),
    wl_last_update_date_general = if (n() > 0) max(wish_create_date, na.rm = TRUE) else NA_Date_,
    .groups = "drop"
  ) |>
  mutate(
    wl_days_since_last_update_general = if_else(
      !is.na(wl_last_update_date_general),
      as.numeric(difftime(ref_date, wl_last_update_date_general, units = "days")),
      NA_real_ # Será NA si no hay wl_last_update_date_general
    )
  ) |>
  select(-wl_last_update_date_general) # Limpiar columna intermedia

# Unir esta información a model_data
model_data <- model_data |>
  left_join(wishlist_summary_per_customer, by = "customer_id") |>
  mutate(
    wl_total_entries_all_time = coalesce(wl_total_entries_all_time, 0),
    wl_days_since_last_update_general = coalesce(wl_days_since_last_update_general, 99999)
  )

6.1.3.4 Daños y visitas al taller (damage_cleaned)

Para cada transacción en model_data, se resume el historial de daños y visitas al taller del cliente que ocurrieron antes de la fecha de dicha transacción (valid_from). El objetivo es capturar el comportamiento y la experiencia del cliente con el servicio técnico previo a la compra que se está analizando.

Se calculan las siguientes características:

  • dmg_n_total_visits_before_tx: número total de visitas al taller registradas para el cliente antes de la fecha de la transacción actual.

  • dmg_n_low_cost_visits_before_tx: número de visitas al taller clasificadas como de “Low cost” antes de la transacción.

  • dmg_n_medium_cost_visits_before_tx: número de visitas al taller clasificadas como de “Medium-low cost” o “Medium-high cost” antes de la transacción.

  • dmg_n_high_cost_visits_before_tx: número de visitas al taller clasificadas como de “High cost” antes de la transacción.

  • dmg_days_since_last_visit_before_tx: días transcurridos desde la última visita al taller del cliente hasta la fecha de la transacción actual. Si no hay visitas previas, se imputa un valor grande (99999) para indicar una ausencia de historial reciente de visitas.

damage_features_list <- lapply(1:nrow(model_data), function(i) {
  current_customer_id <- model_data$customer_id[i]
  current_transaction_date <- model_data$valid_from[i]
  current_transaction_id <- model_data$transaction_id[i]

  # Filtrar daños para el cliente actual y ANTES de la fecha de transacción actual
  damage_previas <- damage_cleaned |>
    filter(customer_id == current_customer_id, damage_repair_date < current_transaction_date)

  dmg_n_total_visits_before_tx <- nrow(damage_previas)
  
  dmg_days_since_last_visit_before_tx <- if (dmg_n_total_visits_before_tx > 0) {
    as.numeric(difftime(current_transaction_date, max(damage_previas$damage_repair_date, na.rm = TRUE), units = "days"))
  } else {
    99999 # Valor grande para indicar "sin visita previa" o "hace mucho tiempo"
  }
  
  dmg_n_low_cost_visits_before_tx <- damage_previas |> 
    filter(repair_cost_category == "Low cost") |> 
    nrow()
  
  dmg_n_medium_cost_visits_before_tx <- damage_previas |> 
    filter(repair_cost_category %in% c("Medium-low cost", "Medium-high cost")) |> 
    nrow()
    
  dmg_n_high_cost_visits_before_tx <- damage_previas |> 
    filter(repair_cost_category == "High cost") |> 
    nrow()

  return(tibble(
    transaction_id = current_transaction_id,
    dmg_n_total_visits_before_tx = dmg_n_total_visits_before_tx,
    dmg_n_low_cost_visits_before_tx = dmg_n_low_cost_visits_before_tx,
    dmg_n_medium_cost_visits_before_tx = dmg_n_medium_cost_visits_before_tx,
    dmg_n_high_cost_visits_before_tx = dmg_n_high_cost_visits_before_tx,
    dmg_days_since_last_visit_before_tx = dmg_days_since_last_visit_before_tx
  ))
})

damage_features_df <- bind_rows(damage_features_list)

# Unir las características de daños
model_data <- model_data |>
  left_join(damage_features_df, by = "transaction_id") |>
  # Imputar NAs para las nuevas columnas de daños si es necesario
  # (la lógica de arriba debería asignar 0 o 99999, pero por si acaso)
  mutate(
    dmg_n_total_visits_before_tx = ifelse(is.na(dmg_n_total_visits_before_tx), 0, dmg_n_total_visits_before_tx),
    dmg_n_low_cost_visits_before_tx = ifelse(is.na(dmg_n_low_cost_visits_before_tx), 0, dmg_n_low_cost_visits_before_tx),
    dmg_n_medium_cost_visits_before_tx = ifelse(is.na(dmg_n_medium_cost_visits_before_tx), 0, dmg_n_medium_cost_visits_before_tx),
    dmg_n_high_cost_visits_before_tx = ifelse(is.na(dmg_n_high_cost_visits_before_tx), 0, dmg_n_high_cost_visits_before_tx),
    dmg_days_since_last_visit_before_tx = ifelse(is.na(dmg_days_since_last_visit_before_tx), 99999, dmg_days_since_last_visit_before_tx)
  )

6.1.4 Preprocesamiento de datos para el modelo

En esta sección, se realizan los pasos finales de preparación sobre el conjunto de datos model_data antes de proceder al entrenamiento del modelo. Estos pasos incluyen el manejo de valores faltantes, la transformación de la variable objetivo y la selección de las características finales.

  1. Manejo de valores faltantes (NAs) en previous_ownership_duration_days:
    • Se imputan los valores NA en previous_ownership_duration_days. Si un NA corresponde a la primera compra de un cliente (num_purchases_before_this_one == 0), la duración se establece en 0.
    • Los NA restantes en previous_ownership_duration_days (para clientes que no son primerizos pero para los que falta el dato) se imputan utilizando la mediana de esta columna.
  2. Transformación de la variable objetivo:
    • La variable objetivo time_until_next_purchase_days se transforma aplicando el logaritmo natural (log(x + 1)) para crear log_time_until_next_purchase_days. Esta transformación ayuda a normalizar la distribución de la variable objetivo, pues está muy sesgada hacia la derecha, y estabilizar su varianza, lo cual es beneficioso para modelos lineales.
  3. Selección de columnas finales y tipado:
    • Se crea un dataframe final, model_data_final, que contiene la variable objetivo transformada y el conjunto de predictores seleccionados.
    • La variable baumuster_4 (que representa el tipo de carrocería) se convierte explícitamente a un tipo factor para asegurar que el modelo la trate como una variable categórica, pues representa esencialmente al class_bodytype.
# Copia de model_data para preprocesamiento
model_data_processed <- model_data

# 1. Manejo de NAs y corrección de datos
# Asumimos que NA aquí significa que num_purchases_before_this_one es 0
model_data_processed <- model_data_processed |>
  mutate(
    previous_ownership_duration_days = ifelse(
      is.na(previous_ownership_duration_days) & num_purchases_before_this_one == 0, 
      0, 
      previous_ownership_duration_days
    )
  )

# Si aún quedan NAs en previous_ownership_duration_days (por si acaso), imputar con mediana
median_prev_ownership_days <- median(model_data_processed$previous_ownership_duration_days, na.rm = TRUE)
model_data_processed <- model_data_processed |>
  mutate(
    previous_ownership_duration_days = ifelse(
      is.na(previous_ownership_duration_days),
      median_prev_ownership_days,
      previous_ownership_duration_days
    )
  )

# 2. Transformación de la variable objetivo
model_data_processed <- model_data_processed |>
  mutate(
    log_time_until_next_purchase_days = log(time_until_next_purchase_days + 1)
  )

# 3. Selección de columnas finales
model_data_final <- model_data_processed |>
  select(
    # Variable objetivo original
    # time_until_next_purchase_days, 
    # Variable objetivo transformada
    log_time_until_next_purchase_days,
    # Predictores
    baumuster_4,
    vehicle_age_at_transaction_days,
    num_purchases_before_this_one,
    previous_ownership_duration_days,
    wl_total_entries_all_time,
    wl_days_since_last_update_general,
    dmg_n_total_visits_before_tx,
    dmg_n_low_cost_visits_before_tx,
    dmg_n_medium_cost_visits_before_tx,
    dmg_n_high_cost_visits_before_tx,
    dmg_days_since_last_visit_before_tx
  ) |>
  mutate(baumuster_4 = factor(baumuster_4)) # Paso a factor, ya que representa categorías (class_bodytype)

6.1.5 Entrenamiento del modelo y evaluación

Una vez que model_data_final está completamente preprocesado, se procede a construir y evaluar un modelo de regresión lineal.

El proceso es el siguiente:

  1. Definición de la fórmula del modelo: se establece la fórmula para el modelo lm(), donde log_time_until_next_purchase_days es la variable dependiente, y el . indica que todas las demás columnas en model_data_final se utilizarán como variables predictoras. La función lm() maneja automáticamente las variables de tipo factor (como baumuster_4) creando las variables dummy necesarias.

  2. División en conjuntos de entrenamiento y prueba: el conjunto de datos se divide aleatoriamente en un conjunto de entrenamiento (80% de los datos) y un conjunto de prueba (20% restante). Se utiliza set.seed(123) para asegurar que esta división sea reproducible.

  3. Entrenamiento del modelo lineal: se entrena un modelo de regresión lineal (lm_model) utilizando la fórmula definida y únicamente los datos del conjunto de entrenamiento (train_data).

  4. Resumen del modelo: se muestra un resumen estadístico del modelo entrenado (summary(lm_model)).

# Construir el modelo
# lm() manejará las variables factor creando dummies automáticamente.
# Por defecto, lm usará na.omit si hay NAs restantes.
model_formula <- log_time_until_next_purchase_days ~ .
  
# Dividir en entrenamiento y prueba
set.seed(123) # para reproducibilidad
train_indices <- sample(1:nrow(model_data_final), 0.8 * nrow(model_data_final))
train_data <- model_data_final[train_indices, ]
test_data <- model_data_final[-train_indices, ]

lm_model <- lm(model_formula, data = train_data)

# Ver resumen del modelo
summary(lm_model)

Call:
lm(formula = model_formula, data = train_data)

Residuals:
    Min      1Q  Median      3Q     Max 
-6.2487 -1.9120  0.3082  1.7467  5.1647 

Coefficients: (1 not defined because of singularities)
                                      Estimate Std. Error t value Pr(>|t|)    
(Intercept)                          3.113e+00  1.815e-01  17.152  < 2e-16 ***
baumuster_41179                     -1.149e-01  1.891e-01  -0.608 0.543454    
baumuster_41183                     -3.266e-01  1.741e-01  -1.876 0.060616 .  
baumuster_41186                     -2.321e-01  1.692e-01  -1.371 0.170250    
baumuster_41569                     -2.349e-01  1.888e-01  -1.244 0.213406    
baumuster_41641                      1.253e+00  7.950e-01   1.576 0.114963    
baumuster_41660                     -3.319e-01  5.482e-01  -0.605 0.544906    
baumuster_41668                      1.275e+00  6.459e-01   1.974 0.048436 *  
baumuster_41671                      6.865e-02  1.950e-01   0.352 0.724773    
baumuster_41673                      6.248e-01  2.359e-01   2.648 0.008099 ** 
baumuster_41679                     -3.596e-02  3.622e-01  -0.099 0.920909    
baumuster_41690                      9.612e-01  5.052e-01   1.902 0.057123 .  
baumuster_41714                      6.383e-01  6.309e-01   1.012 0.311660    
baumuster_41724                      5.362e-02  4.524e-01   0.119 0.905660    
baumuster_41760                     -1.806e-01  1.652e-01  -1.094 0.274082    
baumuster_41770                     -6.228e-01  1.470e-01  -4.238 2.26e-05 ***
baumuster_41771                     -4.539e-01  1.781e-01  -2.548 0.010832 *  
baumuster_41903                     -2.871e-01  5.639e-01  -0.509 0.610682    
baumuster_41904                      6.320e-01  7.411e-01   0.853 0.393784    
baumuster_41923                     -1.642e+00  7.855e-01  -2.091 0.036571 *  
baumuster_41973                     -3.078e-01  1.552e+00  -0.198 0.842773    
baumuster_41974                      1.703e+00  1.103e+00   1.544 0.122529    
baumuster_42030                      1.187e+00  1.276e+00   0.930 0.352289    
baumuster_42032                      4.452e-01  1.274e+00   0.349 0.726812    
baumuster_42037                     -1.492e+00  2.194e+00  -0.680 0.496526    
baumuster_42040                      9.766e-01  3.210e-01   3.042 0.002351 ** 
baumuster_42042                      1.300e+00  3.180e-01   4.087 4.40e-05 ***
baumuster_42043                     -9.488e-02  5.151e-01  -0.184 0.853850    
baumuster_42049                      1.052e+00  6.772e-01   1.554 0.120317    
baumuster_42050                      8.226e-02  1.706e-01   0.482 0.629778    
baumuster_42052                      6.453e-02  1.595e-01   0.404 0.685879    
baumuster_42053                      5.406e-01  2.093e-01   2.583 0.009816 ** 
baumuster_42054                      3.813e-02  2.791e-01   0.137 0.891333    
baumuster_42060                     -1.464e-01  1.746e-01  -0.838 0.401924    
baumuster_42062                     -1.930e-01  1.705e-01  -1.132 0.257532    
baumuster_42073                      4.271e-01  3.479e-01   1.228 0.219617    
baumuster_42074                      6.290e-01  5.104e-01   1.232 0.217820    
baumuster_42083                      1.586e+00  2.197e+00   0.722 0.470296    
baumuster_42093                     -2.265e+00  2.193e+00  -1.033 0.301726    
baumuster_42110                      8.056e-01  4.154e-01   1.939 0.052499 .  
baumuster_42112                      6.127e-02  6.547e-01   0.094 0.925450    
baumuster_42120                      1.028e+00  2.891e-01   3.555 0.000378 ***
baumuster_42122                      6.177e-01  2.487e-01   2.484 0.013015 *  
baumuster_42130                      1.770e-02  1.647e-01   0.107 0.914398    
baumuster_42132                      3.506e-02  1.605e-01   0.218 0.827161    
baumuster_42140                     -7.292e-01  2.364e-01  -3.085 0.002039 ** 
baumuster_42142                     -5.079e-01  2.363e-01  -2.149 0.031632 *  
baumuster_42173                     -5.424e-01  7.049e-01  -0.769 0.441620    
baumuster_42174                     -5.315e-01  1.551e+00  -0.343 0.731856    
baumuster_42183                      9.532e-01  5.204e-01   1.832 0.067029 .  
baumuster_42189                      4.066e-02  4.436e-01   0.092 0.926956    
baumuster_42193                      3.308e-01  8.456e-01   0.391 0.695629    
baumuster_42210                      4.714e-01  6.288e-01   0.750 0.453432    
baumuster_42220                     -1.702e-01  2.793e-01  -0.609 0.542349    
baumuster_42221                      3.448e-01  4.596e-01   0.750 0.453146    
baumuster_42229                      2.413e+00  2.188e+00   1.103 0.270024    
baumuster_42230                     -1.653e-01  2.351e-01  -0.703 0.481954    
baumuster_42231                      1.724e-01  3.696e-01   0.466 0.640935    
baumuster_42239                     -1.526e+00  8.375e-01  -1.823 0.068369 .  
baumuster_42304                      3.002e+00  2.194e+00   1.368 0.171300    
baumuster_42324                     -3.623e-01  2.941e-01  -1.232 0.217979    
baumuster_42363                     -3.254e-01  3.255e-01  -1.000 0.317437    
baumuster_42364                     -1.139e+00  6.221e-01  -1.831 0.067079 .  
baumuster_42383                      4.179e-01  2.303e-01   1.815 0.069594 .  
baumuster_42384                     -5.102e-02  2.844e-01  -0.179 0.857632    
baumuster_42428                      6.403e-01  9.028e-01   0.709 0.478197    
baumuster_42436                     -1.857e-01  1.940e-01  -0.957 0.338466    
baumuster_42437                     -2.561e-01  1.751e-01  -1.462 0.143635    
baumuster_42452                      8.585e-01  5.573e-01   1.540 0.123467    
baumuster_42462                      1.558e-01  2.536e-01   0.614 0.538938    
baumuster_42470                     -1.756e-01  1.826e-01  -0.962 0.336259    
baumuster_42476                     -2.018e-01  1.864e-01  -1.083 0.278948    
baumuster_42477                     -2.834e-01  1.770e-01  -1.601 0.109388    
baumuster_42510                      4.397e+00  1.554e+00   2.829 0.004669 ** 
baumuster_42511                     -7.545e-01  9.891e-01  -0.763 0.445577    
baumuster_42533                     -7.572e-03  1.682e-01  -0.045 0.964101    
baumuster_42539                      2.358e-02  1.722e-01   0.137 0.891065    
baumuster_42543                     -3.920e-01  2.213e-01  -1.771 0.076574 .  
baumuster_42546                     -1.970e-01  1.909e-01  -1.032 0.301920    
baumuster_42573                      4.514e-02  2.903e-01   0.156 0.876416    
baumuster_42906                      7.530e-02  3.240e-01   0.232 0.816258    
baumuster_42923                     -5.596e-01  5.480e-01  -1.021 0.307178    
baumuster_42938                      2.280e-01  2.168e-01   1.052 0.292994    
baumuster_42946                     -3.804e-01  2.330e-01  -1.633 0.102564    
baumuster_42951                     -3.132e-01  1.875e-01  -1.670 0.094956 .  
baumuster_42966                     -7.217e-01  3.239e-01  -2.228 0.025905 *  
baumuster_42969                     -2.528e+00  1.552e+00  -1.629 0.103353    
baumuster_42971                      1.487e-02  2.376e-01   0.063 0.950099    
baumuster_44156                     -2.023e-02  1.550e+00  -0.013 0.989586    
baumuster_44157                      2.164e-01  2.188e+00   0.099 0.921200    
baumuster_44206                     -1.310e+00  2.249e-01  -5.823 5.88e-09 ***
baumuster_44207                     -1.051e+00  4.590e-01  -2.291 0.021983 *  
baumuster_44208                     -1.052e+00  4.112e-01  -2.557 0.010552 *  
baumuster_44476                     -6.768e-01  3.034e-01  -2.230 0.025741 *  
baumuster_44477                     -1.742e+00  2.678e-01  -6.506 7.92e-11 ***
baumuster_44478                     -6.080e-01  1.950e-01  -3.118 0.001826 ** 
baumuster_44530                     -1.761e-01  2.755e-01  -0.639 0.522754    
baumuster_44533                     -5.195e-01  2.370e-01  -2.192 0.028365 *  
baumuster_44534                     -3.584e-01  3.616e-01  -0.991 0.321537    
baumuster_44632                     -5.861e-01  3.953e-01  -1.483 0.138209    
baumuster_44633                      7.600e-01  5.642e-01   1.347 0.177970    
baumuster_44652                     -2.594e+00  1.550e+00  -1.673 0.094310 .  
baumuster_44653                     -1.471e+00  8.392e-01  -1.753 0.079628 .  
baumuster_44656                     -8.907e-01  1.101e+00  -0.809 0.418712    
baumuster_44702                      1.305e-01  4.436e-01   0.294 0.768554    
baumuster_49061                     -1.432e-01  1.268e+00  -0.113 0.910087    
baumuster_49062                     -2.900e+00  2.188e+00  -1.325 0.185045    
baumuster_49066                     -1.275e+00  1.101e+00  -1.158 0.246730    
baumuster_49071                     -1.560e+00  3.691e-01  -4.225 2.40e-05 ***
baumuster_49072                     -1.743e+00  4.766e-01  -3.657 0.000256 ***
baumuster_49076                     -1.605e+00  2.276e-01  -7.054 1.81e-12 ***
baumuster_49077                     -2.215e-01  6.737e-01  -0.329 0.742318    
baumuster_49101                     -1.616e+00  1.101e+00  -1.468 0.142111    
baumuster_49106                     -8.038e-01  5.335e-01  -1.507 0.131892    
vehicle_age_at_transaction_days     -1.342e-04  2.672e-05  -5.021 5.18e-07 ***
num_purchases_before_this_one       -8.586e-03  4.442e-04 -19.329  < 2e-16 ***
previous_ownership_duration_days     8.508e-04  6.365e-05  13.368  < 2e-16 ***
wl_total_entries_all_time            1.449e-02  3.314e-02   0.437 0.661989    
wl_days_since_last_update_general    1.882e-07  1.135e-06   0.166 0.868295    
dmg_n_total_visits_before_tx         4.556e-02  8.528e-03   5.342 9.32e-08 ***
dmg_n_low_cost_visits_before_tx     -4.693e-02  1.104e-02  -4.251 2.14e-05 ***
dmg_n_medium_cost_visits_before_tx  -5.075e-02  9.413e-03  -5.392 7.06e-08 ***
dmg_n_high_cost_visits_before_tx            NA         NA      NA       NA    
dmg_days_since_last_visit_before_tx  9.902e-06  3.888e-07  25.465  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 2.183 on 17322 degrees of freedom
Multiple R-squared:  0.1679,    Adjusted R-squared:  0.1621 
F-statistic: 28.66 on 122 and 17322 DF,  p-value: < 2.2e-16

El modelo de regresión lineal es estadísticamente significativo en general (F-statistic p-value < 2.2e-16), indicando que los predictores, en conjunto, explican una parte de la varianza de la variable objetivo (log_time_until_next_purchase_days).

La bondad de ajuste es modesta:

  • R-cuadrado múltiple: 0.1679

  • R-cuadrado ajustado: 0.1621

Esto significa que aproximadamente el 16.21% de la variabilidad en el logaritmo del tiempo hasta la próxima compra es explicada por el modelo.

Coeficientes destacados:

  • Varias categorías de baumuster_4 son significativas. Por ejemplo, baumuster_41770, baumuster_44206, baumuster_44477, y baumuster_49076 tienen coeficientes negativos altamente significativos, sugiriendo un menor tiempo hasta la siguiente compra en comparación con la categoría de referencia. Otros como baumuster_42042 y baumuster_42510 tienen coeficientes positivos significativos.

  • vehicle_age_at_transaction_days: coeficiente negativo pequeño pero altamente significativo (***), indicando que vehículos más antiguos en el momento de la transacción tienden a tener un tiempo ligeramente menor hasta la siguiente compra.

  • num_purchases_before_this_one: coeficiente negativo altamente significativo (***). Más compras previas se asocian con un menor tiempo hasta la siguiente.

  • previous_ownership_duration_days: coeficiente positivo altamente significativo (***). Mayor duración de la posesión anterior se asocia con un mayor tiempo hasta la siguiente compra.

  • dmg_n_total_visits_before_tx: positivo y significativo (***). Más visitas totales al taller previas se asocian con un mayor tiempo hasta la siguiente compra.

  • dmg_n_low_cost_visits_before_tx y dmg_n_medium_cost_visits_before_tx: negativos y significativos (***). Más visitas de coste bajo o medio se asocian con un menor tiempo hasta la siguiente compra.

  • dmg_days_since_last_visit_before_tx: positivo y altamente significativo (***). Más días desde la última visita al taller se asocian con un ligero aumento en el tiempo hasta la siguiente compra.

  • Las variables de wishlist (wl_total_entries_all_time, wl_days_since_last_update_general) no son significativas en este modelo. Probablemente esto ocurra porque solo un 3% de la clientela usa la wishlist. Al haber tan pocos datos, estos se convierten en mucho menos significativos.

Puntos a considerar:

  • El coeficiente para dmg_n_high_cost_visits_before_tx es NA debido a singularidades (probablemente multicolinealidad). Esto estará ocurriendo porque esta variable se puede inferir de las otras tres que hay presentes; dmg_n_low_cost_visits_before_tx, dmg_n_medium_cost_visits_before_tx y dmg_n_total_visits_before_tx. Se puede calcular como: High = TOTAL - Low - Medium

  • El R-cuadrado ajustado, aunque el modelo es significativo, es bajo, sugiriendo que gran parte de la varianza no es explicada. Enriqueciéndolo con más características predictoras, haciéndo una división de los datos (parece haber 2 categorías diferenciadas; clientes que compran muchos vehículos de golpe y clientes que tienen un patrón de compra común) o cambiando a un modelo más complejo (XGBoost, Random Forest…) seguro que se obtendrían mejores resultados.

Dado mi conocimiento en la materia, lo único que puedo intentar fehacientemente es enriquecer con más características predictoras al modelo, especialmente con algunas de las obtenidas en el análisis de patrones.

6.1.6 Enriquecimiento con carácterísticas de análisis de patrones

Dados los hallazgos obtenidos en el análisis de patrones, se añaden nuevas características al conjunto de datos model_data_final para mejorar la capacidad predictiva del modelo. Estas características se derivan de las interacciones entre el uso de la wishlist y el historial de daños del cliente, ya que son los principales y más relevantes patrones encontrados en el análisis de patrones.

library(forcats) # facilita tareas a nivel de factor. Para fct_na_value_to_level

model_data_enriched <- model_data_final |>
  mutate(
    # 1. Indicador de uso de wishlist
    wl_has_used_wishlist = ifelse(wl_total_entries_all_time > 0, 1, 0),

    # 2. Indicador de historial de daños
    dmg_has_had_damage = ifelse(dmg_n_total_visits_before_tx > 0, 1, 0),

    # 3. Interacción: Tuvo daño Y NO usó wishlist
    interaction_dmg_and_no_wl = ifelse(dmg_has_had_damage == 1 & wl_has_used_wishlist == 0, 1, 0),

    # 4. Categorización de la antigüedad del vehículo (similar a cv_edad_vehiculo_cat)
    # vehicle_age_at_transaction_days puede tener NAs si date_of_first_registration era NA
    vehicle_age_category = cut(
      vehicle_age_at_transaction_days,
      breaks = c(-Inf, 365*2, 365*5, Inf),
      labels = c("Age_0-2Y", "Age_3-5Y", "Age_6Y+"),
      right = FALSE
    ),
    vehicle_age_category = fct_na_value_to_level(vehicle_age_category),

    # 5. Categorización de la recencia del último daño (similar a dmg_recency)
    # dmg_days_since_last_visit_before_tx está imputado con 99999 si no hay visitas
    dmg_recency_category = cut(
      dmg_days_since_last_visit_before_tx,
      breaks = c(-Inf, 90, 365, 730, Inf),
      labels = c("Rec_0-3M", "Rec_3M-1Y", "Rec_1Y-2Y", "Rec_>2Y_or_None"),
      right = FALSE
    ),

    # 6. Categorización de la frecuencia de daños (similar a dmg_freq)
    # dmg_n_total_visits_before_tx está imputado con 0 si no hay visitas
    dmg_freq_category = cut(
      dmg_n_total_visits_before_tx,
      breaks = c(-Inf, 0, 1, 3, Inf), # El primer break en -Inf asegura que 0 caiga en "Freq_0"
      labels = c("Freq_0", "Freq_1", "Freq_2-3", "Freq_4+"),
      right = TRUE # right = TRUE para que (0, 1] sea Freq_1, etc. 0 estará en (-Inf, 0]
    )
  )

Finalmente, se seleccionan las columnas relevantes para el modelo enriquecido. Se mantiene la variable objetivo transformada (log_time_until_next_purchase_days) y se añaden las nuevas características derivadas del análisis de patrones.

model_data_final_enriched <- model_data_enriched |>
  select(
    # Variable objetivo original
    # time_until_next_purchase_days, 
    # Variable objetivo transformada
    log_time_until_next_purchase_days,
    # Predictores originales
    baumuster_4,
    vehicle_age_at_transaction_days,
    num_purchases_before_this_one,
    previous_ownership_duration_days,
    wl_total_entries_all_time,
    wl_days_since_last_update_general,
    dmg_n_low_cost_visits_before_tx,
    dmg_n_medium_cost_visits_before_tx,
    dmg_n_high_cost_visits_before_tx,
    dmg_days_since_last_visit_before_tx,
    # Nuevas características basadas en patrones
    wl_has_used_wishlist,
    dmg_has_had_damage,
    interaction_dmg_and_no_wl,
    vehicle_age_category,
    dmg_recency_category,
    dmg_freq_category
  ) |>
  mutate(baumuster_4 = factor(baumuster_4)) # Asegurar que baumuster_4 sigue siendo factor


model_formula <- log_time_until_next_purchase_days ~ .

# Dividir en entrenamiento y prueba
set.seed(123) # para reproducibilidad
train_indices_enriched <- sample(1:nrow(model_data_final_enriched), 0.8 * nrow(model_data_final_enriched))
train_data_enriched <- model_data_final_enriched[train_indices_enriched, ]
test_data_enriched <- model_data_final_enriched[-train_indices_enriched, ]

lm_model_enriched <- lm(model_formula, data = train_data_enriched)

# Ver resumen del modelo
summary(lm_model_enriched)

Call:
lm(formula = model_formula, data = train_data_enriched)

Residuals:
    Min      1Q  Median      3Q     Max 
-5.6840 -1.8443  0.2926  1.7355  5.3632 

Coefficients: (1 not defined because of singularities)
                                      Estimate Std. Error t value Pr(>|t|)    
(Intercept)                         -3.128e+02  1.425e+02  -2.194 0.028214 *  
baumuster_41179                     -1.036e-01  1.877e-01  -0.552 0.580901    
baumuster_41183                     -3.711e-01  1.730e-01  -2.145 0.031942 *  
baumuster_41186                     -3.187e-01  1.682e-01  -1.895 0.058144 .  
baumuster_41569                     -2.410e-01  1.874e-01  -1.286 0.198309    
baumuster_41641                      1.892e+00  8.047e-01   2.352 0.018694 *  
baumuster_41660                     -3.492e-01  5.442e-01  -0.642 0.521117    
baumuster_41668                      1.242e+00  6.410e-01   1.937 0.052759 .  
baumuster_41671                     -3.720e-03  1.937e-01  -0.019 0.984675    
baumuster_41673                      5.226e-01  2.347e-01   2.226 0.025996 *  
baumuster_41679                     -1.495e-01  3.611e-01  -0.414 0.678871    
baumuster_41690                      1.309e+00  5.081e-01   2.577 0.009985 ** 
baumuster_41714                      1.279e+00  6.367e-01   2.009 0.044561 *  
baumuster_41724                      1.466e-01  4.492e-01   0.326 0.744179    
baumuster_41760                     -1.719e-01  1.639e-01  -1.049 0.294296    
baumuster_41770                     -6.615e-01  1.460e-01  -4.530 5.92e-06 ***
baumuster_41771                     -4.681e-01  1.769e-01  -2.646 0.008156 ** 
baumuster_41903                     -2.790e-01  5.596e-01  -0.499 0.618080    
baumuster_41904                      4.269e-01  7.357e-01   0.580 0.561773    
baumuster_41923                     -1.506e+00  7.795e-01  -1.932 0.053397 .  
baumuster_41973                      1.179e-01  1.541e+00   0.077 0.939014    
baumuster_41974                      1.915e+00  1.095e+00   1.749 0.080321 .  
baumuster_42030                      1.784e+00  1.278e+00   1.396 0.162721    
baumuster_42032                      1.233e+00  1.271e+00   0.970 0.332055    
baumuster_42037                     -3.187e-01  2.187e+00  -0.146 0.884172    
baumuster_42040                      1.218e+00  3.226e-01   3.774 0.000161 ***
baumuster_42042                      1.397e+00  3.167e-01   4.410 1.04e-05 ***
baumuster_42043                      1.986e-01  5.139e-01   0.387 0.699119    
baumuster_42049                      1.431e+00  6.744e-01   2.122 0.033864 *  
baumuster_42050                      6.215e-02  1.694e-01   0.367 0.713785    
baumuster_42052                      1.765e-02  1.584e-01   0.111 0.911309    
baumuster_42053                      5.101e-01  2.078e-01   2.455 0.014100 *  
baumuster_42054                     -3.908e-03  2.770e-01  -0.014 0.988745    
baumuster_42060                     -1.836e-01  1.736e-01  -1.058 0.290139    
baumuster_42062                     -1.805e-01  1.695e-01  -1.065 0.286937    
baumuster_42073                      5.968e-01  3.465e-01   1.723 0.084976 .  
baumuster_42074                      7.512e-01  5.072e-01   1.481 0.138628    
baumuster_42083                      2.931e+00  2.201e+00   1.331 0.183043    
baumuster_42093                     -2.007e+00  2.187e+00  -0.917 0.358899    
baumuster_42110                      1.491e+00  4.413e-01   3.379 0.000728 ***
baumuster_42112                      5.960e-01  6.597e-01   0.904 0.366261    
baumuster_42120                      1.088e+00  2.872e-01   3.791 0.000151 ***
baumuster_42122                      7.299e-01  2.476e-01   2.948 0.003207 ** 
baumuster_42130                     -8.961e-04  1.635e-01  -0.005 0.995628    
baumuster_42132                     -8.194e-03  1.595e-01  -0.051 0.959030    
baumuster_42140                     -6.951e-01  2.347e-01  -2.962 0.003059 ** 
baumuster_42142                     -5.325e-01  2.347e-01  -2.269 0.023303 *  
baumuster_42173                     -4.664e-01  6.998e-01  -0.666 0.505113    
baumuster_42174                     -5.530e-01  1.540e+00  -0.359 0.719505    
baumuster_42183                      9.639e-01  5.166e-01   1.866 0.062071 .  
baumuster_42189                      8.994e-02  4.403e-01   0.204 0.838143    
baumuster_42193                      9.659e-01  8.504e-01   1.136 0.256079    
baumuster_42210                      1.034e+00  6.304e-01   1.639 0.101136    
baumuster_42220                     -1.736e-01  2.772e-01  -0.626 0.531095    
baumuster_42221                      3.827e-01  4.563e-01   0.839 0.401744    
baumuster_42229                      2.376e+00  2.171e+00   1.094 0.273777    
baumuster_42230                     -1.906e-01  2.335e-01  -0.817 0.414224    
baumuster_42231                      4.850e-02  3.676e-01   0.132 0.895044    
baumuster_42239                     -1.503e+00  8.310e-01  -1.809 0.070480 .  
baumuster_42304                      4.157e+00  2.188e+00   1.899 0.057527 .  
baumuster_42324                     -3.333e-01  2.919e-01  -1.142 0.253420    
baumuster_42363                     -4.029e-01  3.231e-01  -1.247 0.212322    
baumuster_42364                     -1.104e+00  6.174e-01  -1.788 0.073722 .  
baumuster_42383                      3.835e-01  2.286e-01   1.677 0.093500 .  
baumuster_42384                     -5.979e-02  2.823e-01  -0.212 0.832244    
baumuster_42428                      7.475e-01  8.960e-01   0.834 0.404130    
baumuster_42436                     -2.321e-01  1.928e-01  -1.204 0.228652    
baumuster_42437                     -3.054e-01  1.740e-01  -1.755 0.079346 .  
baumuster_42452                      1.330e+00  5.626e-01   2.364 0.018088 *  
baumuster_42462                      1.492e-01  2.517e-01   0.593 0.553268    
baumuster_42470                     -2.701e-01  1.814e-01  -1.489 0.136554    
baumuster_42476                     -2.816e-01  1.853e-01  -1.520 0.128546    
baumuster_42477                     -3.683e-01  1.759e-01  -2.094 0.036260 *  
baumuster_42510                      4.924e+00  1.547e+00   3.183 0.001458 ** 
baumuster_42511                     -4.746e-01  9.839e-01  -0.482 0.629527    
baumuster_42533                     -6.076e-02  1.670e-01  -0.364 0.716072    
baumuster_42539                      1.652e-02  1.709e-01   0.097 0.923006    
baumuster_42543                     -4.336e-01  2.199e-01  -1.971 0.048704 *  
baumuster_42546                     -1.941e-01  1.896e-01  -1.024 0.305952    
baumuster_42573                      3.007e-02  2.882e-01   0.104 0.916896    
baumuster_42906                      1.636e-02  3.219e-01   0.051 0.959471    
baumuster_42923                     -6.118e-01  5.439e-01  -1.125 0.260690    
baumuster_42938                      1.691e-01  2.154e-01   0.785 0.432458    
baumuster_42946                     -3.838e-01  2.315e-01  -1.658 0.097381 .  
baumuster_42951                     -3.511e-01  1.864e-01  -1.883 0.059702 .  
baumuster_42966                     -7.643e-01  3.224e-01  -2.370 0.017780 *  
baumuster_42969                     -2.345e+00  1.540e+00  -1.522 0.127995    
baumuster_42971                     -1.423e-02  2.360e-01  -0.060 0.951919    
baumuster_44156                      2.123e-01  1.538e+00   0.138 0.890238    
baumuster_44157                      1.805e-01  2.171e+00   0.083 0.933712    
baumuster_44206                     -1.348e+00  2.234e-01  -6.035 1.63e-09 ***
baumuster_44207                     -1.099e+00  4.555e-01  -2.413 0.015811 *  
baumuster_44208                     -1.046e+00  4.082e-01  -2.562 0.010415 *  
baumuster_44476                     -7.091e-01  3.013e-01  -2.354 0.018590 *  
baumuster_44477                     -1.761e+00  2.660e-01  -6.620 3.69e-11 ***
baumuster_44478                     -6.385e-01  1.937e-01  -3.297 0.000980 ***
baumuster_44530                     -1.797e-01  2.736e-01  -0.657 0.511283    
baumuster_44533                     -5.080e-01  2.355e-01  -2.158 0.030978 *  
baumuster_44534                     -3.725e-01  3.591e-01  -1.037 0.299638    
baumuster_44632                     -6.837e-01  3.925e-01  -1.742 0.081539 .  
baumuster_44633                      7.526e-01  5.599e-01   1.344 0.178900    
baumuster_44652                     -2.368e+00  1.540e+00  -1.538 0.124055    
baumuster_44653                     -1.415e+00  8.361e-01  -1.693 0.090526 .  
baumuster_44656                     -8.958e-01  1.093e+00  -0.820 0.412450    
baumuster_44702                      3.387e-02  4.403e-01   0.077 0.938692    
baumuster_49061                     -1.147e-01  1.259e+00  -0.091 0.927386    
baumuster_49062                     -3.027e+00  2.171e+00  -1.394 0.163237    
baumuster_49066                     -1.318e+00  1.093e+00  -1.206 0.227840    
baumuster_49071                     -1.568e+00  3.665e-01  -4.278 1.90e-05 ***
baumuster_49072                     -1.780e+00  4.729e-01  -3.763 0.000169 ***
baumuster_49076                     -1.531e+00  2.259e-01  -6.779 1.25e-11 ***
baumuster_49077                     -2.352e-01  6.689e-01  -0.352 0.725105    
baumuster_49101                     -1.701e+00  1.093e+00  -1.556 0.119743    
baumuster_49106                     -8.284e-01  5.294e-01  -1.565 0.117638    
vehicle_age_at_transaction_days     -3.343e-04  6.207e-05  -5.386 7.29e-08 ***
num_purchases_before_this_one       -8.167e-03  4.437e-04 -18.406  < 2e-16 ***
previous_ownership_duration_days     6.643e-04  6.654e-05   9.984  < 2e-16 ***
wl_total_entries_all_time            2.684e-02  3.353e-02   0.800 0.423523    
wl_days_since_last_update_general    3.403e-03  1.417e-03   2.402 0.016300 *  
dmg_n_low_cost_visits_before_tx      4.953e-03  4.767e-03   1.039 0.298805    
dmg_n_medium_cost_visits_before_tx  -3.732e-03  4.246e-03  -0.879 0.379369    
dmg_n_high_cost_visits_before_tx     5.253e-02  8.507e-03   6.175 6.78e-10 ***
dmg_days_since_last_visit_before_tx -2.449e-04  1.562e-04  -1.568 0.116939    
wl_has_used_wishlist                 3.401e+02  1.416e+02   2.403 0.016287 *  
dmg_has_had_damage                  -2.484e+01  1.542e+01  -1.610 0.107349    
interaction_dmg_and_no_wl            2.176e-02  1.443e-01   0.151 0.880103    
vehicle_age_categoryAge_3-5Y         4.216e-01  8.494e-02   4.964 6.99e-07 ***
vehicle_age_categoryAge_6Y+          3.207e-01  1.618e-01   1.982 0.047546 *  
dmg_recency_categoryRec_3M-1Y        4.314e-01  5.534e-02   7.797 6.72e-15 ***
dmg_recency_categoryRec_1Y-2Y        9.531e-01  1.044e-01   9.132  < 2e-16 ***
dmg_recency_categoryRec_>2Y_or_None  1.096e+00  2.072e-01   5.291 1.23e-07 ***
dmg_freq_categoryFreq_1              3.469e-01  7.080e-02   4.899 9.71e-07 ***
dmg_freq_categoryFreq_2-3            2.507e-01  6.199e-02   4.044 5.28e-05 ***
dmg_freq_categoryFreq_4+                    NA         NA      NA       NA    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 2.166 on 17312 degrees of freedom
Multiple R-squared:  0.1813,    Adjusted R-squared:  0.1751 
F-statistic: 29.04 on 132 and 17312 DF,  p-value: < 2.2e-16

Al comparar el nuevo modelo (lm_model_enriched) con el modelo anterior (cuyos resultados de R-cuadrado ajustado eran 0.1621), se observan los siguientes puntos clave basados en el summary(lm_model_enriched):

1. Mejora en la bondad de ajuste:

  • R-cuadrado ajustado: ha aumentado a 0.1751 (desde 0.1621). Esto indica que el nuevo modelo, con las características enriquecidas, explica aproximadamente un 17.51% de la variabilidad en el logaritmo del tiempo hasta la próxima compra, lo que representa una modesta mejora (alrededor de un 1.3% adicional) en el poder explicativo en comparación con el modelo anterior.

    • El modelo sigue siendo estadísticamente significativo en general (F-statistic p-value < 2.2e-16).

2. Impacto de las nuevas características derivadas del análisis de patrones:

  • Indicadores binarios y de interacción:
    • wl_has_used_wishlist: es significativa (*) con un coeficiente positivo grande (3.401e+02). Esto sugiere que el simple hecho de haber usado la wishlist tiene una asociación positiva con el tiempo hasta la siguiente compra, aunque la magnitud del coeficiente es notable y podría merecer una investigación más profunda (posiblemente debido al pequeño número de usuarios de wishlist o interacciones no capturadas).
    • dmg_has_had_damage: no es estadísticamente significativa (p = 0.107349). Puede deberse a que la gran mayoría de clientes tienen al menos un daño, lo que hace que esta variable no aporte información adicional.
    • interaction_dmg_and_no_wl: no es estadísticamente significativa (p = 0.880103). A pesar de ser una regla fuerte en el análisis de patrones, no añade poder predictivo en este modelo lineal en presencia de las otras variables.
  • Nuevas variables categóricas:
    • vehicle_age_category:
      • vehicle_age_categoryAge_3-5Y: altamente significativa (***) con un coeficiente positivo (0.4216).
      • vehicle_age_categoryAge_6Y+: significativa (*) con un coeficiente positivo (0.3207).
      • Esto indica que, en comparación con la categoría base (probablemente “Age_0-2Y”), los vehículos más antiguos (3-5 años y 6+ años) se asocian con un mayor tiempo hasta la siguiente compra.
    • dmg_recency_category: todas las categorías son altamente significativas (***):
      • Rec_3M-1Y: coeficiente positivo (0.4314).
      • Rec_1Y-2Y: coeficiente positivo mayor (0.9531).
      • Rec_>2Y_or_None: coeficiente positivo aún mayor (1.096).
      • Esto sugiere que cuanto más tiempo ha pasado desde la última visita al taller (o si no hubo visitas recientes), mayor es el tiempo esperado hasta la siguiente compra, en comparación con una visita muy reciente (0-3 meses, la categoría base).
    • dmg_freq_category:
      • Freq_1: altamente significativa (***) con un coeficiente positivo (0.3469).
      • Freq_2-3: altamente significativa (***) con un coeficiente positivo (0.2507).
      • Freq_4+: Es NA debido a singularidades (multicolinealidad). Esta categoría puede ser inferida a partir de las otras o de dmg_n_total_visits_before_tx.
      • Esto indica que tener 1, o 2-3 visitas al taller (comparado con 0 visitas, la categoría base) se asocia con un mayor tiempo hasta la siguiente compra.

3. Cambios en la significancia y coeficientes de variables originales:

  • wl_days_since_last_update_general: ahora es significativa (*) con un coeficiente positivo pequeño (0.0034). En el modelo anterior no era significativa.
  • dmg_n_low_cost_visits_before_tx: ha perdido significancia (p = 0.298805).
  • dmg_n_medium_cost_visits_before_tx: ha perdido significancia (p = 0.379369).
  • dmg_n_high_cost_visits_before_tx: ahora es altamente significativa (***) con un coeficiente positivo (0.05253). En el modelo anterior, este coeficiente era NA debido a la multicolinealidad. La introducción de las variables categóricas de daños parece haber ayudado a aislar su efecto.
  • dmg_days_since_last_visit_before_tx: ha perdido significancia (p = 0.116939), ya que su efecto ahora es capturado de manera más efectiva por la nueva variable categórica dmg_recency_category.
  • Las variables vehicle_age_at_transaction_days (negativo, ***), num_purchases_before_this_one (negativo, ***), y previous_ownership_duration_days (positivo, ***) mantienen su alta significancia y la dirección de su efecto.

6.1.6.1 Conclusión del enriquecimiento

El enriquecimiento del modelo con características derivadas del análisis de patrones ha resultado en una ligera mejora en el poder explicativo (R-cuadrado ajustado). Si se indagase más a fondo en la naturaleza de los datos, se podrían encontrar patrones adicionales que podrían ser muy útiles para mejorar aún más el modelo, pero la intuición me dice que el modelo lineal no es el más adecuado para este tipo de datos.

Las nuevas variables categóricas (vehicle_age_category, dmg_recency_category, dmg_freq_category) han demostrado ser predictores importantes y significativos, capturando relaciones que las variables numéricas originales no modelaban tan explícitamente.

Sin embargo, la mejora general es modesta, y algunas de las nuevas variables (como la interacción y dmg_has_had_damage) no aportaron significancia estadística en este modelo lineal. La multicolinealidad también sigue siendo un aspecto a considerar, aunque sus efectos son fácilmente mitigables (se trata, generalmente, de que una variable es combinación lineal de otras).

6.1.7 Análisis de residuos

Para evaluar la adecuación del modelo lm_model_enriched y verificar si se cumplen los supuestos de la regresión lineal, se generan y analizan los siguientes gráficos de diagnóstico de residuos:

# Generar los gráficos de diagnóstico de residuos para el modelo enriquecido
par(mfrow = c(2, 2)) # Organizar los gráficos en una cuadrícula de 2x2
plot(lm_model_enriched)
Warning: not plotting observations with leverage one:
  1004, 8222, 13840, 14336
Warning in sqrt(crit * p * (1 - hh)/hh): NaNs produced
Warning in sqrt(crit * p * (1 - hh)/hh): NaNs produced

par(mfrow = c(1, 1)) # Restaurar la configuración de gráficos a la normalidad

6.1.7.1 Residuos vs. Ajustados

En el primer gráfico, busco principalmente dos cosas: si la relación que mi modelo asume es lineal y si la varianza de los errores es constante (homocedasticidad).

Lo que veo aquí es un poco preocupante. La línea roja, que suaviza la tendencia de los residuos, no es para nada plana; tiene una forma curvada bastante clara. Además, los puntos no se esparcen de forma aleatoria alrededor del cero, sino que forman unas bandas diagonales muy notorias. Esto me sugiere fuertemente que una relación lineal simple no es suficiente para capturar lo que está pasando en mis datos, y que la varianza de los errores tampoco parece ser constante a lo largo de los valores ajustados, lo que apuntaría a heterocedasticidad.

6.1.7.2 Q-Q normal o Q-Q residuos

Este gráfico me ayuda a ver si los residuos de mi modelo se distribuyen de forma normal, lo cual es un supuesto importante.

Idealmente, los puntos deberían caer justo encima de la línea diagonal. En mi gráfico, los puntos se desvían bastante de esa línea, sobre todo en los extremos (las colas). La forma en que se separan, por debajo en la cola izquierda y por encima en la derecha, me indica que mis residuos tienen colas más “pesadas” de lo que tendría una distribución normal; es decir, tengo más valores extremos (tanto positivos como negativos) de lo esperado. Esto significa que el supuesto de normalidad de los errores no se está cumpliendo bien.

6.1.7.3 Escala-Localización

Este es otro gráfico que me ayuda a comprobar la homocedasticidad, es decir, si la dispersión de los residuos es la misma para todos los niveles de los valores ajustados.

Aquí, la línea roja debería ser más o menos horizontal si la varianza fuera constante. Sin embargo, en mi gráfico, esta línea tiene una forma de U invertida, subiendo y luego bajando. Esto refuerza la idea de que la varianza de mis errores no es constante, confirmando la heterocedasticidad que ya sospechaba del primer gráfico. Los puntos también parecen agruparse de forma extraña, no con una dispersión uniforme.

6.1.7.4 Residuos vs. Apalancamiento

Finalmente, este gráfico me sirve para identificar si hay observaciones individuales que podrían estar influyendo demasiado en mi modelo. El apalancamiento me dice si una observación tiene valores extremos en las variables predictoras, y los residuos grandes me indican que el modelo no ajusta bien ese punto. Lo que busco son puntos que tengan ambas cosas: alto apalancamiento y residuo grande, ya que esos son los más problemáticos.

En mi gráfico, veo que la mayoría de los puntos tienen poco apalancamiento (están a la izquierda). Hay algunos puntos con residuos más grandes de lo normal (como el 2783 o el 9692) y otros con más apalancamiento (como el 11504, aunque su residuo es pequeño). Tendría que fijarme si alguno de estos puntos tiene una distancia de Cook alta (las líneas punteadas), lo que indicaría una influencia excesiva. A primera vista, no parece haber un único punto que esté destrozando el modelo, pero sí hay algunos que merecería la pena investigar más de cerca.

6.2 Conclusiones de la regresión lineal

Considerando todo esto, y aunque transformé mi variable objetivo a logaritmo (log_time_until_next_purchase_days) precisamente para intentar mejorar estos aspectos, parece que un modelo lineal simple no es la mejor herramienta para estos datos. Los patrones en los residuos, especialmente esas bandas diagonales y la falta de linealidad y homocedasticidad, me indican que hay algo más complejo en la relación entre mis predictores y el tiempo hasta la siguiente compra que el modelo actual no está capturando.

6.3 Aproximación final. Random Forest

Visto lo visto con la regresión lineal, y cómo los gráficos de residuos confirmaban mi hipótesis, me queda bastante claro que un modelo lineal simple, incluso con la variable transformada a logaritmo, se queda corto para entender cuándo va a ser la próxima compra. Los patrones en los residuos me indican que hay relaciones más complejas entre mis predictores y el tiempo hasta la siguiente compra que el modelo actual, sencillamente, no está capturando.

Así que, he decidido probar con un Random Forest. La idea detrás de este tipo de modelo es bastante ingeniosa: en lugar de depender de un único modelo que intente encontrar la “fórmula mágica” (como hace la regresión lineal), un Random Forest construye muchos árboles de decisión diferentes. Cada uno de estos árboles se entrena con una porción ligeramente distinta de los datos y solo considera un subconjunto aleatorio de las variables predictoras para tomar sus decisiones. Luego, para hacer una predicción final, el modelo combina las predicciones de todos estos árboles individuales (en regresión, como es nuestro caso, suele ser promediándolas).

Lo bueno de este enfoque es que los Random Forest son mucho más flexibles y potentes para capturar relaciones no lineales e interacciones complejas entre variables, que es precisamente donde mi modelo lineal flaque más.

Además, no hacen supuestos tan estrictos sobre cómo se distribuyen los datos o los errores, lo cual es otra ventaja dados los problemas que observé. Y con paquetes como ranger en R, implementarlo y probar si se adapta mejor a la distribución de mis datos es cuestión de añadir unas pocas líneas de código, así que merece la pena el intento.

6.3.1 Random Forest con ranger

Optar por el paquete ranger para esta implementación de Random Forest no es casual. En primer lugar, ranger destaca por su capacidad para manejar eficientemente variables categóricas con una alta cardinalidad –es decir, con muchos niveles distintos– como es el caso de baumuster_4. Esta es una ventaja notable sobre el paquete randomForest más tradicional, que presenta limitaciones en este aspecto. En segundo lugar, ranger es conocido por su optimización en C++, lo que generalmente se traduce en una mayor velocidad de entrenamiento, un factor importante al trabajar con conjuntos de datos de un tamaño considerable y al explorar múltiples configuraciones de modelos.

Adicionalmente, para evaluar la contribución de cada predictor, se ha seleccionado el método de importancia por permutación (importance = 'permutation'). Este enfoque, aunque computacionalmente más intensivo que las medidas basadas en la impureza de los nodos, se considera más robusto y fiable. Funciona evaluando cuánto se degrada el rendimiento del modelo cuando se permutan aleatoriamente los valores de una variable específica, ofreciendo así una medida más directa de su relevancia predictiva real.

library(randomForest)
randomForest 4.7-1.2
Type rfNews() to see new features/changes/bug fixes.

Attaching package: 'randomForest'
The following object is masked from 'package:dplyr':

    combine
The following object is masked from 'package:ggplot2':

    margin
library(ranger)

Attaching package: 'ranger'
The following object is masked from 'package:randomForest':

    importance
set.seed(123)
rf_model_ranger <- ranger(
  formula = log_time_until_next_purchase_days ~ ., 
  data = train_data_enriched, 
  num.trees = 800,
  importance = 'permutation' 
  # ranger maneja NAs por defecto, o puedes usar na.action = na.omit
)
print(rf_model_ranger)
Ranger result

Call:
 ranger(formula = log_time_until_next_purchase_days ~ ., data = train_data_enriched,      num.trees = 800, importance = "permutation") 

Type:                             Regression 
Number of trees:                  800 
Sample size:                      17445 
Number of independent variables:  16 
Mtry:                             4 
Target node size:                 5 
Variable importance mode:         permutation 
Splitrule:                        variance 
OOB prediction error (MSE):       3.650559 
R squared (OOB):                  0.3580519 

Este R-cuadrado OOB (Out-Of-Bag) de aproximadamente 0.358 es bastante revelador. El R² OOB es una estimación del rendimiento del modelo en datos “no vistos”, calculada internamente durante el entrenamiento del Random Forest utilizando las observaciones que no se incluyeron en el bootstrap de cada árbol. Es una métrica bastante fiable, similar a lo que obtendríamos con validación cruzada, pero sin el coste de ejecutarla explícitamente.

Comparado con el R-cuadrado ajustado de nuestro mejor modelo lineal (que rondaba el 0.175), este 0.358 representa una mejora muy sustancial. Prácticamente hemos duplicado la capacidad del modelo para explicar la varianza en el logaritmo del tiempo hasta la siguiente compra.

Esto confirma con bastante fuerza mi sospecha inicial: las relaciones en mis datos son más complejas de lo que un modelo lineal puede capturar. El Random Forest, al ser inherentemente no lineal y capaz de modelar interacciones de forma más natural, se adapta mucho mejor a la estructura subyacente de los datos.

6.3.2 Mejora del modelo Random Forest: ajuste de hiperparámetros

El modelo Random Forest actual (rf_model_ranger) ya muestra una mejora significativa con un R² OOB de ~0.358. Para intentar mejorarlo aún más, podemos realizar un ajuste de hiperparámetros. Nos centraremos en:

  • mtry: número de variables seleccionadas aleatoriamente en cada división.
  • min.node.size: tamaño mínimo de los nodos terminales.

Estos dos son los sospechosos habituales y suelen tener un impacto considerable en el rendimiento del Random Forest, controlando la complejidad y la aleatoriedad de los árboles. mtry gestiona cuántas variables considera cada árbol al hacer una división, influyendo en la diversidad y la fuerza individual de los árboles. Por su parte, min.node.size determina qué tan profundos y especializados pueden llegar a ser los árboles, afectando directamente el riesgo de sobreajuste o subajuste. Encontrar el equilibrio adecuado para ambos es crucial para optimizar el rendimiento del modelo.

Utilizaremos el paquete caret para realizar una búsqueda en cuadrícula (grid search) con validación cruzada. La búsqueda en cuadrícula probará sistemáticamente las combinaciones que le definamos, y la validación cruzada nos dará una estimación más robusta de cómo funcionaría cada combinación en datos no vistos, ayudando a evitar que elijamos parámetros que solo funcionan bien por casualidad en una partición específica de los datos.

library(caret)

# Definir el control de entrenamiento con validación cruzada
train_control <- trainControl(
  method = "cv",      # Validación cruzada
  number = 5,         # Número de folds
  search = "grid",    # Búsqueda en cuadrícula (grid search)
  verboseIter = TRUE  # Muestra el progreso
)

# Definir la cuadrícula de hiperparámetros a probar
tune_grid_ranger <- expand.grid(
  mtry = c(3, 4, 5, 6, 8),             # Valores de mtry a probar
  min.node.size = c(3, 5, 10, 15),     # Valores de min.node.size a probar 
  splitrule = "variance"             # Mantener la regla de división por defecto para regresión
)


# Coger el modelo entrenado si existe (tarda mucho tiempo en entrenar)
if (file.exists("models/rf_tuned_caret_model.rds")) {
  rf_tuned_caret <- readRDS("models/rf_tuned_caret_model.rds")
} else {
  # Entrenar el modelo Random Forest con ranger usando caret
  set.seed(123) # Para reproducibilidad del proceso de tuning
  rf_tuned_caret <- train(
    log_time_until_next_purchase_days ~ .,
    data = train_data_enriched,
    method = "ranger",        # Especificar que queremos usar ranger
    trControl = train_control,
    tuneGrid = tune_grid_ranger,
    importance = 'permutation', # Para calcular importancia
    num.trees = 800             # Mantener el número de árboles o ajustarlo también
                                # (num.trees no se tunea en el grid por defecto en caret para ranger,
                                # se pasa como argumento fijo)
  )
}

# Ver los resultados del ajuste
rf_tuned_caret
Random Forest 

17445 samples
   16 predictor

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 13955, 13957, 13956, 13957, 13955 
Resampling results across tuning parameters:

  mtry  min.node.size  RMSE      Rsquared   MAE     
  3      3             2.078748  0.2709017  1.790886
  3      5             2.079752  0.2699729  1.791448
  3     10             2.082201  0.2678321  1.793783
  3     15             2.076449  0.2714540  1.788315
  4      3             2.020654  0.3070262  1.731477
  4      5             2.021261  0.3063844  1.732114
  4     10             2.021990  0.3062508  1.732572
  4     15             2.022035  0.3062098  1.732814
  5      3             1.982221  0.3285201  1.688571
  5      5             1.981674  0.3292041  1.688255
  5     10             1.980782  0.3293201  1.687041
  5     15             1.984120  0.3273127  1.690482
  6      3             1.955411  0.3417579  1.655819
  6      5             1.955566  0.3417415  1.655892
  6     10             1.955479  0.3418505  1.655906
  6     15             1.957060  0.3407922  1.657425
  8      3             1.928141  0.3532723  1.615436
  8      5             1.927998  0.3534649  1.615172
  8     10             1.928304  0.3535205  1.616054
  8     15             1.929682  0.3527884  1.617934

Tuning parameter 'splitrule' was held constant at a value of variance
RMSE was used to select the optimal model using the smallest value.
The final values used for the model were mtry = 8, splitrule = variance
 and min.node.size = 5.
# Ver el mejor modelo y sus hiperparámetros
rf_tuned_caret$bestTune
   mtry splitrule min.node.size
18    8  variance             5
# Graficar los resultados del tuning
rf_tuned_caret
Random Forest 

17445 samples
   16 predictor

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 13955, 13957, 13956, 13957, 13955 
Resampling results across tuning parameters:

  mtry  min.node.size  RMSE      Rsquared   MAE     
  3      3             2.078748  0.2709017  1.790886
  3      5             2.079752  0.2699729  1.791448
  3     10             2.082201  0.2678321  1.793783
  3     15             2.076449  0.2714540  1.788315
  4      3             2.020654  0.3070262  1.731477
  4      5             2.021261  0.3063844  1.732114
  4     10             2.021990  0.3062508  1.732572
  4     15             2.022035  0.3062098  1.732814
  5      3             1.982221  0.3285201  1.688571
  5      5             1.981674  0.3292041  1.688255
  5     10             1.980782  0.3293201  1.687041
  5     15             1.984120  0.3273127  1.690482
  6      3             1.955411  0.3417579  1.655819
  6      5             1.955566  0.3417415  1.655892
  6     10             1.955479  0.3418505  1.655906
  6     15             1.957060  0.3407922  1.657425
  8      3             1.928141  0.3532723  1.615436
  8      5             1.927998  0.3534649  1.615172
  8     10             1.928304  0.3535205  1.616054
  8     15             1.929682  0.3527884  1.617934

Tuning parameter 'splitrule' was held constant at a value of variance
RMSE was used to select the optimal model using the smallest value.
The final values used for the model were mtry = 8, splitrule = variance
 and min.node.size = 5.

Los hiperparámetros óptimos identificados por caret fueron mtry = 8 y min.node.size = 5. Con esta configuración, el modelo alcanzó un RMSE promedio de validación cruzada de aproximadamente 1.928 y un R-cuadrado de 0.3535.

Esta consistencia entre el R² OOB del modelo base y el R² de validación cruzada del modelo ajustado, a pesar de la exploración de diferentes valores de mtry y min.node.size, sugiere que el rendimiento del modelo Random Forest es robusto.

Aunque el ajuste fino identificó mtry = 8 como ligeramente superior en términos de RMSE, la diferencia en R² con respecto al modelo inicial (con mtry probablemente en 4) no es drástica. Esto indica que el modelo Random Forest, incluso con una configuración de hiperparámetros estándar, ya lograba capturar una porción significativa de la varianza explicable, y el proceso de tuning ha servido para refinar y confirmar la estabilidad de este rendimiento. La validación cruzada, al promediar el rendimiento a través de múltiples divisiones de los datos, proporciona una estimación fiable de cómo el modelo generalizará a datos no vistos, y la proximidad de estas métricas refuerza la confianza en la capacidad predictiva del modelo.

6.3.2.1 Evaluación del modelo Random Forest ajustado. Escala logarítmica

Ya tenemos nuestro Random Forest tuneado y los parámetros que, en teoría, son los mejores, es buena idea echar un primer vistazo a cómo rinde en la misma escala en la que fue entrenado y optimizado: la escala logarítmica de log_time_until_next_purchase_days.

Esto nos sirve para varias cosas:

  1. Comparación directa con el proceso de tuning: caret usó métricas en esta escala (concretamente el RMSE logarítmico) para elegir los mejores hiperparámetros durante la validación cruzada. Así que, evaluar en el conjunto de prueba con estas mismas métricas nos da una idea de si el rendimiento se mantiene o si hemos tenido suerte (o mala suerte) con la división de los datos.

  2. Detectar problemas de sobreajuste (Overfitting): si el rendimiento en la validación cruzada fuera espectacularmente bueno y aquí, en el test set (datos totalmente nuevos), se desplomara, tendríamos un claro síntoma de que el modelo se aprendió demasiado bien los datos de entrenamiento pero no generaliza.

Así que, vamos a calcular el RMSE, R² y MAE en esta escala logarítmica sobre el test_data_enriched.

# Hacer predicciones en el CONJUNTO DE PRUEBA (test_data_enriched)
predictions_test_log <- predict(rf_tuned_caret, newdata = test_data_enriched)
Registered S3 methods overwritten by 'proxy':
  method               from    
  print.registry_field registry
  print.registry_entry registry
# Evaluar rendimiento en el CONJUNTO DE PRUEBA (escala logarítmica)
test_metrics_log <- postResample(pred = predictions_test_log, obs = test_data_enriched$log_time_until_next_purchase_days)
test_metrics_log
     RMSE  Rsquared       MAE 
1.9267715 0.3477028 1.6111977 
# Para obtener el MAE en escala logarítmica 
mae_test_log <- MAE(pred = predictions_test_log, obs = test_data_enriched$log_time_until_next_purchase_days)
mae_test_log
[1] 1.611198

Al pasar el modelo ajustado por el conjunto de prueba (test_data_enriched), obtenemos las siguientes métricas en la escala logarítmica:

  • RMSE (log): 1.9267715

  • Rsquared (log): 0.3477028

  • MAE (log): 1.6111977

¿Qué dicen estos números?

  1. Consistencia con la validación cruzada: el R² de 0.3477 en el conjunto de prueba es muy, muy cercano al R² de 0.3535 que obtuvimos con los mejores parámetros (mtry=8, min.node.size=5) durante la validación cruzada. Lo mismo ocurre con el RMSE (1.927 en test vs 1.928 en CV) y el MAE (1.611 en test vs 1.615 en CV). Esta consistencia es una excelente noticia. Significa que el rendimiento que vimos durante el ajuste de hiperparámetros no fue una casualidad y que el modelo parece generalizar bien a datos completamente nuevos. No hay signos evidentes de sobreajuste aquí, lo cual es un alivio.

  2. Mejora Mantenida: este R² de casi 0.35 sigue representando una mejora sustancial respecto al R² ajustado del modelo lineal enriquecido (que estaba en 0.175). El Random Forest sigue explicando aproximadamente el doble de la varianza en la escala logarítmica.

En definitiva, en la escala en la que el modelo “piensa” y fue optimizado, los resultados en el conjunto de prueba son sólidos y coherentes con lo que esperábamos tras el tuning. Esto nos da confianza en que el proceso de ajuste fue robusto. Ahora, el siguiente paso es ver cómo se traducen estas cifras a la escala original en días, que es la que realmente nos importa para la interpretación práctica.

6.3.2.2 Evaluación del modelo Random Forest ajustado. Escala original

Vale, ya hemos visto que en la escala logarítmica, donde el modelo “vive” y fue optimizado, los resultados son coherentes y la mejora respecto al lineal es clara. Pero, seamos sinceros, a nadie le importa el logaritmo del tiempo hasta la siguiente compra en el mundo real. Lo que queremos saber es: ¿cuántos días de error tenemos?

Así que, ahora toca el paso crucial: traducir esas predicciones logarítmicas de nuevo a la escala original de días y ver qué tal se comporta el modelo ahí. Esto implica:

  1. Revertir la transformación logarítmica: aplicaremos la exponencial (exp(x) - 1) a las predicciones que hizo el modelo en el test_data_enriched.
  2. Calcular las métricas de error en días: volveremos a calcular el MAE y el RMSE, pero esta vez comparando las predicciones en días con los valores reales en días del conjunto de prueba.
  3. Analizar la distribución de los errores: un simple promedio de error (MAE) puede ocultar muchas cosas. Echar un vistazo al resumen de los errores absolutos (mínimo, cuartiles, máximo) nos dará una idea más completa de si los errores son más o menos homogéneos.
# Revertir predicciones a la escala original
predictions_test_original <- exp(predictions_test_log) - 1
# Asegurar que no haya valores negativos si tu variable original no puede tenerlos
predictions_test_original[predictions_test_original < 0] <- 0 

# Necesitas la variable objetivo original para el test_data_enriched
original_target_for_test_enriched <- model_data_processed[-train_indices_enriched, ]$time_until_next_purchase_days

# Comprobación de longitud (muy importante)
if (length(predictions_test_original) == length(original_target_for_test_enriched)) {
  
  # Calcular MAE en la escala original - Conjunto de prueba
  mae_test_original <- mean(abs(predictions_test_original - original_target_for_test_enriched), na.rm = TRUE)
  round(mae_test_original, 2)

  # Calcular RMSE en la escala original - Conjunto de prueba
  rmse_test_original <- sqrt(mean((predictions_test_original - original_target_for_test_enriched)^2, na.rm = TRUE))
  round(rmse_test_original, 2)
  
  # Resumen de errores absolutos en el CONJUNTO DE PRUEBA (días)
  summary(abs(predictions_test_original - original_target_for_test_enriched))
}
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
   0.015    5.962   25.105  165.483  117.990 2799.877 

6.4 Conclusiones finales del modelo Random Forest

6.4.1 Puntos fuertes

  1. Mejora sustancial sobre el modelo lineal: el salto es evidente. El R² OOB inicial del Random Forest (~0.358) y el R² de la validación cruzada del modelo ajustado (~0.353) en la escala logarítmica prácticamente duplican la capacidad explicativa del mejor modelo lineal enriquecido (que andaba por un R² ajustado de 0.175). Esto confirma mi sospecha: las relaciones aquí son más complejas de lo que un modelo lineal puede digerir. El Random Forest, con su flexibilidad para capturar no linealidades e interacciones, se adapta mucho mejor.

  2. Identificación de parámetros clave: el ajuste de hiperparámetros nos llevó a que mtry = 8 y min.node.size = 5 son los que mejor funcionan para este conjunto de datos, según el RMSE en la validación cruzada.

6.4.2 La ducha de realidad: rendimiento en la escala original (días)

Aquí es donde la cosa se pone más “interesante”. Al evaluar el modelo ajustado en el conjunto de prueba y traducir las predicciones de nuevo a días, obtenemos:

  • MAE (Error Absoluto Medio) en días: alrededor de 165 días.
  • RMSE (Raíz del Error Cuadrático Medio) en días: alrededor de 413 días.

Un MAE de unos 5 meses y medio y un RMSE que se dispara bastante más (lo que me dice que hay errores bastante grandes) significa que, aunque el modelo es mejor explicando la varianza en la escala logarítmica, predecir el día exacto de la siguiente compra sigue siendo complicado.

6.4.3 ¿Qué significa todo esto?

  • El modelo Random Forest es, sin duda, un paso adelante. Entiende mejor los datos que el lineal.
  • Sin embargo, la precisión en días para predecir cuándo exactamente un cliente volverá a comprar es limitada. Un error promedio de 165 días es considerable para muchas aplicaciones de negocio que requieran fechas precisas.
  • Esto no significa que el modelo no sirva. Podría ser útil para:
    • Segmentación general: identificar clientes con mayor o menor propensión a recomprar en periodos amplios (ej. “próximos 6-12 meses” vs. “más de 2 años”).
    • Entender importancias relativas: aunque no lo hemos explorado aquí en detalle con el modelo final, la importancia de variables del Random Forest podría dar pistas sobre qué factores influyen más, incluso si la predicción temporal exacta es difícil.
  • La dificultad puede venir de muchos sitios: la propia naturaleza del problema, factores externos no medidos, o que simplemente necesitemos más o diferentes datos/características.