Objetivo

Implementar y evaluar un modelo de clasificación por medio del algoritmo regresión logística multinomial para predecir el comportamiento de compra de los clientes, si lo hará en la tienda, por internet en línea o ambas.

Descripción

Del portal de kagle.com de esta dirección url https://www.kaggle.com/datasets/shree0910/online-vs-in-store-shopping-behaviour-dataset, se descargó este conjunto de datos que también se puede descargar de la dirección url personal de github.com https://github.com/rpizarrog/machine_learning_r_python_casos_de_estudio/blob/main/datos/online%20vs%20store%20shopping%20dataset.csv.

Ahora bien, el contexto de los datos sería algo como lo siguiente: se trata de información de comportamiento de compras de los clientes en un modelo híbrido desde los que lo hace a través de comercio electrónico como de los que tradicionalmente lo hacen acudiendo a la tienda.

Este conjunto de datos simula el comportamiento del consumidor en el mundo real combinando información del tipo demográfico, hábitos digitales, patrones de compra, preferencias logísticas y factores psicológicos.

Es un conjunto de datos estructurado de 11789 registros y 25 columnas que pueden ser analizadas mediante posibles modelos de clasificación del aprendizaje automático.

Se sugiere incluir los atributos del conjunto de datos, por ejemplo:

• La edad del cliente en años cumplidos, • Los ingresos mensuales, • Horas de uso diarias en internet u horas en línea por día, • Años con el uso con su teléfono celular , • Horas diarias en redes sociales, • Valor numérico de la confianza del cliente en hacer sus pagos, • Puntuación de nivel de comodidad con el uso de la tecnología refiriéndose al uso de aplicaciones en línea, • Número de pedidos en línea por mes, • Número de visitas a tienda por mes, • Promedio de compras en línea, • Promedio de visitas al mes a la tienda, entre otros…

Cargar librerías

Se cargan paquetes previa instalación en R studio; con comentarios el uso de cada paquete, la instalaciópn son se hace una vez por eso los comentarios antes de # install.packages().

# install.packages("readr")
# install.packages("ggplot2")
# install.packages("caret")
# install.packages("dplyr")
# install.packages("flextable")
# install.packages("nnet")

library(readr) # Para cargar datos
library(ggplot2) # Para visualizar datos
library(caret) # Para evaluar modelos de clasificación
library(dplyr) # Para filtrar datos
library(flextable) # Para generar tablas para word
library(nnet) # Para implementar un modelo de regresión logística para clasificación

Crear o cargar funciones

Se preparan funciones que sirven para la correcta ejecución del caso.

f_cargar_datos <- function(ruta_archivo) {
  #------------------------------------------------------------
  #   Importar datos desde un archivo CSV.
  # Argumentos:
  #   ruta_archivo: ruta del archivo a cargar.
  # Retorna:
  #   Un data.frame listo para análisis.
  #------------------------------------------------------------
  
  datos <- read_csv(ruta_archivo)
  return(datos)
}

f_visualizar_head_tail_reducido_word <- function(datos, n = 10) {
  #------------------------------------------------------------
  # f_visualizar_head_tail_reducido_word()
  # Objetivo:
  #   Mostrar primeros n y últimos n registros en una misma tabla,
  #   visualizando únicamente:
  #     - Los primeros 4 atributos
  #     - Los últimos 3 atributos
  #   Insertando una fila con "..." como separador.
  #
  # Nota:
  #   Para evitar conflictos de tipos (numérico vs texto),
  #   se convierte a character SOLO para la tabla de visualización.
  #
  # Argumentos:
  #   datos : data.frame
  #   n     : número de registros a mostrar (default = 10)
  #
  # Retorna:
  #   Objeto flextable compatible con Word.
  #
  # Requiere:
  #   library(dplyr)
  #   library(flextable)
  #------------------------------------------------------------


  total_columnas <- ncol(datos)

  # Índices: primeras 4 y últimas 3 (sin duplicar si hay pocas columnas)
  idx_prim <- 1:min(4, total_columnas)
  idx_ult  <- max(total_columnas - 2, 1):total_columnas
  columnas_seleccionadas <- unique(c(idx_prim, idx_ult))

  # Subconjunto reducido
  datos_reducidos <- datos[, columnas_seleccionadas, drop = FALSE]

  # Head y tail
  head_datos <- head(datos_reducidos, n)
  tail_datos <- tail(datos_reducidos, n)

  # Convertir a character SOLO para evitar choque de tipos en bind_rows()
  head_chr <- as.data.frame(lapply(head_datos, as.character), stringsAsFactors = FALSE)
  tail_chr <- as.data.frame(lapply(tail_datos, as.character), stringsAsFactors = FALSE)

  # Fila separadora "..."
  fila_puntos <- as.data.frame(
    matrix("...", nrow = 1, ncol = ncol(head_chr)),
    stringsAsFactors = FALSE
  )
  colnames(fila_puntos) <- colnames(head_chr)

  # Concatenar
  tabla_final <- bind_rows(head_chr, fila_puntos, tail_chr)

  # Flextable para Word
  tabla <- flextable(tabla_final)
  tabla <- autofit(tabla)

  return(tabla)
}

f_describir_datos <- function(datos) {
  #------------------------------------------------------------
  # f_describir_datos()
  # Objetivo:
  #   Generar estadísticas descriptivas básicas.
  # Uso:
  #   res <- f_describir_datos(datos)
  #   res$summary  # resumen
  #   res$structure # estructura
  #------------------------------------------------------------

  res_summary <- summary(datos)
   # Capturar la estructura como texto (sin imprimir)
  res_str <- paste(capture.output(str(datos)), collapse = "\n")

  
  # Devolver ambos para reutilización
  return(list(summary = res_summary, structure = res_str))
}

f_transformar_factor <- function(datos) {
  #------------------------------------------------------------
  # f_transformar_factor()
  # Objetivo:
  #   Convertir todas las variables tipo character
  #   en variables tipo factor.
  #
  # Argumentos:
  #   datos : data.frame
  #
  # Retorna:
  #   data.frame con variables character convertidas a factor.
  #
  # Nota:
  #   No modifica variables numéricas ni factores existentes.
  #------------------------------------------------------------

  # Identificar columnas tipo character
  columnas_character <- sapply(datos, is.character)
  
  # Convertir a factor
  datos[columnas_character] <- lapply(datos[columnas_character], as.factor)
  
  return(datos)
}


f_balancear_clases_undersamplig <- function(datos, variable_objetivo) {
  #------------------------------------------------------------
  # f_balancear_clases()
  #
  # Objetivo:
  #   Generar un subconjunto balanceado del conjunto de datos
  #   mediante undersampling (submuestreo) de la clase dominante.
  #
  # Descripción metodológica:
  #   Cuando existe desbalance severo entre clases, el modelo
  #   tiende a favorecer la clase mayoritaria. Esta función
  #   equilibra el número de observaciones por clase tomando
  #   aleatoriamente el mismo número de registros que la clase
  #   minoritaria.
  #
  # Argumentos:
  #   datos               : data.frame original
  #   variable_objetivo   : nombre de la variable dependiente (string)
  #
  # Retorna:
  #   data.frame balanceado con igual número de observaciones
  #   por cada clase.
  #
  # Método:
  #   1. Identifica la frecuencia mínima entre clases.
  #   2. Toma una muestra aleatoria sin reemplazo de tamaño igual
  #      a la clase más pequeña.
  #
  # Nota:
  #   Este procedimiento reduce el tamaño total del dataset,
  #   por lo que puede implicar pérdida de información.
  #
  # Requiere:
  #   library(dplyr)
  #------------------------------------------------------------

  
  # Semilla para reproducibilidad
  set.seed(2026)
  
  # Paso 1: Calcular el tamaño de la clase minoritaria
  min_n <- datos %>%
    count(!!sym(variable_objetivo)) %>%   # Cuenta frecuencia por clase
    summarise(min(n)) %>%                 # Obtiene el mínimo
    pull()                                # Extrae el valor numérico

  # Paso 2: Generar muestra balanceada
  datos_balanceados <- datos %>%
    group_by(!!sym(variable_objetivo)) %>% # Agrupa por clase
    sample_n(min_n) %>%                    # Toma min_n observaciones por grupo
    ungroup()                              # Elimina agrupación

  return(datos_balanceados)
}

f_particionar_datos <- function(datos, proporcion_entrenamiento = 0.7) {
  #------------------------------------------------------------
  # f_particionar_datos()
  #
  # Objetivo:
  #   Dividir un conjunto de datos previamente preparado
  #   en dos subconjuntos: entrenamiento y validación.
  #
  # Descripción:
  #   La función realiza una partición aleatoria del dataset.
  #   El subconjunto de entrenamiento se utiliza para ajustar
  #   el modelo, mientras que el subconjunto de validación
  #   permite evaluar el desempeño del modelo en datos no
  #   utilizados durante el entrenamiento.
  #
  # Argumentos:
  #   datos : data.frame con los datos preparados
  #   proporcion_entrenamiento : proporción destinada al
  #                              entrenamiento (default = 0.70)
  #
  # Retorna:
  #   Lista con:
  #     $datos_entrenamiento
  #     $datos_validacion
  #
  # Reproducibilidad:
  #   Se fija la semilla en 2026, correspondiente al año de
  #   edición del libro, garantizando resultados replicables.
  #------------------------------------------------------------

  # Semilla para reproducibilidad
  set.seed(2026)

  # Número total de observaciones
  n <- nrow(datos)

  # Número de observaciones para entrenamiento
  n_train <- floor(proporcion_entrenamiento * n)

  # Selección aleatoria de índices
  indices_train <- sample(seq_len(n), size = n_train)

  # Generar subconjuntos
  datos_entrenamiento <- datos[indices_train, ]
  datos_validacion <- datos[-indices_train, ]

  # Devolver lista con ambos datasets
  return(list(
    datos_entrenamiento = datos_entrenamiento,
    datos_validacion = datos_validacion
  ))
}


f_implementar_modelo_RL_multinomial <- function(datos, variable_dependiente, variables_independientes) {
  #------------------------------------------------------------
  # f_implementar_modelo_RL_multinomial()
  #
  # Objetivo:
  #   Ajustar un modelo de Regresión Logística Multinomial en R
  #   usando nnet::multinom().
  #
  # Argumentos:
  #   datos : data.frame / tibble con los datos preparados
  #   variable_dependiente : nombre (string) de la variable objetivo (factor)
  #   variables_independientes : vector (character) con nombres de predictores
  #
  # Retorna:
  #   Lista con:
  #     $modelo       : modelo entrenado (multinom)
  #     $resumen      : summary(modelo)
  #     $tabla_coef   : tabla con coeficientes, SE, z y p-values
  #     $aic          : AIC del modelo
  #
  # Notas:
  #   - La variable dependiente debe ser factor con >= 3 niveles.
  #   - Los predictores categóricos deben ser factor.
  #------------------------------------------------------------

  # Verificar paquete
  if (!requireNamespace("nnet", quietly = TRUE)) {
    stop("Falta instalar el paquete 'nnet'. Instala con install.packages('nnet').")
  }

  # Validaciones básicas
  if (!is.data.frame(datos)) stop("El argumento 'datos' debe ser un data.frame o tibble.")
  if (!(variable_dependiente %in% names(datos))) stop("La variable dependiente no existe en 'datos'.")
  faltantes <- setdiff(variables_independientes, names(datos))
  if (length(faltantes) > 0) stop(paste("Variables independientes no encontradas:", paste(faltantes, collapse = ", ")))

  # Asegurar que la dependiente sea factor
  datos[[variable_dependiente]] <- as.factor(datos[[variable_dependiente]])
  if (nlevels(datos[[variable_dependiente]]) < 3) {
    stop("La variable dependiente debe tener al menos 3 niveles para multinomial.")
  }

  # Fórmula del modelo
  formula_modelo <- stats::as.formula(
    paste(variable_dependiente, paste(variables_independientes, collapse = " + "), sep = " ~ ")
  )

  # Ajustar modelo (semilla editorial)
  set.seed(2026)
  modelo <- nnet::multinom(formula_modelo, data = datos, trace = FALSE)

  # Resumen
  res <- summary(modelo)

  # Construir tabla de coeficientes con pruebas Wald (aprox.)
  coefs <- res$coefficients
  ses   <- res$standard.errors

  zval <- coefs / ses
  pval <- 2 * (1 - stats::pnorm(abs(zval)))

  # Convertir a tabla larga ordenada
  tabla_coef <- data.frame(
    Clase = rep(rownames(coefs), each = ncol(coefs)),
    Variable = rep(colnames(coefs), times = nrow(coefs)),
    Coef = as.vector(t(coefs)),
    SE   = as.vector(t(ses)),
    Z    = as.vector(t(zval)),
    p_value = as.vector(t(pval)),
    row.names = NULL
  )

  return(list(
    modelo = modelo,
    resumen = res,
    tabla_coef = tabla_coef,
    aic = stats::AIC(modelo)
  ))
}


f_tabla_coeficientes <- function(tabla_coef, decimales = 4) {
  #------------------------------------------------------------
  # f_tabla_coeficientes()
  #
  # Objetivo:
  #   Convertir una tabla de coeficientes (data.frame) en una
  #   tabla profesional para Word usando flextable.
  #
  # Argumentos:
  #   tabla_coef : data.frame con columnas:
  #                Clase, Variable, Coef, SE, Z, p_value
  #   decimales  : número de decimales para columnas numéricas
  #
  # Retorna:
  #   Objeto flextable compatible con Word (officer).
  #
  # Requiere:
  #   library(dplyr)
  #   library(flextable)
  #------------------------------------------------------------

  if (!requireNamespace("flextable", quietly = TRUE)) {
    stop("Falta instalar 'flextable'. Instala con install.packages('flextable').")
  }
  if (!requireNamespace("dplyr", quietly = TRUE)) {
    stop("Falta instalar 'dplyr'. Instala con install.packages('dplyr').")
  }

  library(dplyr)
  library(flextable)

  # Validación mínima de columnas esperadas
  cols_req <- c("Clase", "Variable", "Coef", "SE", "Z", "p_value")
  faltan <- setdiff(cols_req, names(tabla_coef))
  if (length(faltan) > 0) {
    stop(paste("Faltan columnas en tabla_coef:", paste(faltan, collapse = ", ")))
  }

  # Ordenar para salida más legible
  tabla_out <- tabla_coef %>%
    arrange(Clase, Variable)

  # Crear flextable
  ft <- flextable(tabla_out)

  # Etiquetas más "de libro"
  ft <- set_header_labels(
    ft,
    Clase = "Clase",
    Variable = "Variable",
    Coef = "Coeficiente",
    SE = "Error estándar",
    Z = "Z",
    p_value = "p-valor"
  )

  # Formateo numérico
  ft <- colformat_num(ft, j = c("Coef", "SE", "Z"), digits = decimales)
  ft <- colformat_num(ft, j = "p_value", digits = decimales)

  # Estilo recomendado para Word
  ft <- autofit(ft)
  ft <- theme_booktabs(ft)
  ft <- align(ft, align = "center", part = "all")
  ft <- align(ft, j = c("Clase", "Variable"), align = "left", part = "all")

  return(ft)
}

Cargar datos

ruta <- "https://raw.githubusercontent.com/rpizarrog/machine_learning_r_python_casos_de_estudio/refs/heads/main/datos/online%20vs%20store%20shopping%20dataset.csv"
datos <- f_cargar_datos(ruta)

Explorar datos

Al visualizar los datos en modo de tabla, solo se muestran los primeros y últimos 10 registros así como los primeros atributos y últimos tres atributos, esto para generar una vista confortable para este documento.

f_visualizar_head_tail_reducido_word(datos, 10)

age

monthly_income

daily_internet_hours

smartphone_usage_years

gender

city_tier

shopping_preference

56

221111

6.5

12

Other

Tier 3

Store

69

96029

8.2

13

Male

Tier 3

Hybrid

46

19055

6.4

4

Female

Tier 3

Store

32

53170

6.4

11

Female

Tier 1

Store

60

244016

6

5

Male

Tier 3

Store

25

114976

7.6

6

Other

Tier 2

Store

78

43251

4.9

6

Other

Tier 2

Hybrid

38

150604

3.1

10

Other

Tier 2

Store

56

150996

7.3

2

Female

Tier 3

Store

75

63037

6.8

4

Female

Tier 3

Store

...

...

...

...

...

...

...

79

205268

5.7

7

Other

Tier 2

Store

29

169125

6.4

10

Male

Tier 2

Store

52

246500

2.7

13

Male

Tier 3

Store

66

144022

6.1

11

Male

Tier 1

Store

38

229292

5.5

1

Other

Tier 1

Store

67

151087

10.4

4

Other

Tier 1

Store

60

243273

6.1

10

Female

Tier 2

Store

53

154729

4.6

13

Female

Tier 2

Store

65

66116

6.2

3

Other

Tier 1

Store

34

168023

4.7

11

Female

Tier 1

Store

Describir datos

Se muestran los tipos de datos y su estructura.

resumen <- f_describir_datos(datos)
cat(resumen$structure)
## spc_tbl_ [11,789 × 25] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ age                        : num [1:11789] 56 69 46 32 60 25 78 38 56 75 ...
##  $ monthly_income             : num [1:11789] 221111 96029 19055 53170 244016 ...
##  $ daily_internet_hours       : num [1:11789] 6.5 8.2 6.4 6.4 6 7.6 4.9 3.1 7.3 6.8 ...
##  $ smartphone_usage_years     : num [1:11789] 12 13 4 11 5 6 6 10 2 4 ...
##  $ social_media_hours         : num [1:11789] 0.7 2.7 2.1 0.7 0.7 4 3.2 3.7 1.1 2.9 ...
##  $ online_payment_trust_score : num [1:11789] 1 6 10 2 2 1 1 4 5 4 ...
##  $ tech_savvy_score           : num [1:11789] 6 9 8 10 5 7 8 9 3 8 ...
##  $ monthly_online_orders      : num [1:11789] 16 14 2 20 18 31 10 17 28 2 ...
##  $ monthly_store_visits       : num [1:11789] 16 1 0 3 16 6 4 7 0 9 ...
##  $ avg_online_spend           : num [1:11789] 28551 124056 81939 35901 131971 ...
##  $ avg_store_spend            : num [1:11789] 144092 28421 128229 134650 34122 ...
##  $ discount_sensitivity       : num [1:11789] 2 4 9 7 5 2 9 2 7 10 ...
##  $ return_frequency           : num [1:11789] 3 7 4 0 9 8 5 1 6 0 ...
##  $ avg_delivery_days          : num [1:11789] 2 4 5 3 2 3 5 2 4 2 ...
##  $ delivery_fee_sensitivity   : num [1:11789] 6 1 3 3 4 5 5 10 5 2 ...
##  $ free_return_importance     : num [1:11789] 7 3 4 10 2 10 2 10 2 9 ...
##  $ product_availability_online: num [1:11789] 7 4 10 2 5 6 8 8 9 6 ...
##  $ impulse_buying_score       : num [1:11789] 1 9 1 4 8 6 9 8 8 9 ...
##  $ need_touch_feel_score      : num [1:11789] 3 6 1 8 9 3 4 9 2 1 ...
##  $ brand_loyalty_score        : num [1:11789] 6 8 3 2 7 4 9 6 6 5 ...
##  $ environmental_awareness    : num [1:11789] 5 1 3 6 1 4 9 1 6 1 ...
##  $ time_pressure_level        : num [1:11789] 2 7 3 6 6 8 8 4 4 7 ...
##  $ gender                     : chr [1:11789] "Other" "Male" "Female" "Female" ...
##  $ city_tier                  : chr [1:11789] "Tier 3" "Tier 3" "Tier 3" "Tier 1" ...
##  $ shopping_preference        : chr [1:11789] "Store" "Hybrid" "Store" "Store" ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   age = col_double(),
##   ..   monthly_income = col_double(),
##   ..   daily_internet_hours = col_double(),
##   ..   smartphone_usage_years = col_double(),
##   ..   social_media_hours = col_double(),
##   ..   online_payment_trust_score = col_double(),
##   ..   tech_savvy_score = col_double(),
##   ..   monthly_online_orders = col_double(),
##   ..   monthly_store_visits = col_double(),
##   ..   avg_online_spend = col_double(),
##   ..   avg_store_spend = col_double(),
##   ..   discount_sensitivity = col_double(),
##   ..   return_frequency = col_double(),
##   ..   avg_delivery_days = col_double(),
##   ..   delivery_fee_sensitivity = col_double(),
##   ..   free_return_importance = col_double(),
##   ..   product_availability_online = col_double(),
##   ..   impulse_buying_score = col_double(),
##   ..   need_touch_feel_score = col_double(),
##   ..   brand_loyalty_score = col_double(),
##   ..   environmental_awareness = col_double(),
##   ..   time_pressure_level = col_double(),
##   ..   gender = col_character(),
##   ..   city_tier = col_character(),
##   ..   shopping_preference = col_character()
##   .. )
##  - attr(*, "problems")=<externalptr>

Ahora el análisis descriptivo de los datos

resumen$summary
##       age        monthly_income   daily_internet_hours smartphone_usage_years
##  Min.   :18.00   Min.   : 15005   Min.   : 1.000       Min.   : 1.000        
##  1st Qu.:33.00   1st Qu.: 72450   1st Qu.: 4.600       1st Qu.: 4.000        
##  Median :49.00   Median :131916   Median : 6.000       Median : 8.000        
##  Mean   :48.73   Mean   :131704   Mean   : 6.011       Mean   : 7.598        
##  3rd Qu.:64.00   3rd Qu.:190505   3rd Qu.: 7.400       3rd Qu.:11.000        
##  Max.   :79.00   Max.   :249989   Max.   :12.000       Max.   :14.000        
##  social_media_hours online_payment_trust_score tech_savvy_score
##  Min.   :0.000      Min.   : 1.000             Min.   : 1.000  
##  1st Qu.:1.600      1st Qu.: 3.000             1st Qu.: 3.000  
##  Median :2.500      Median : 5.000             Median : 6.000  
##  Mean   :2.514      Mean   : 5.499             Mean   : 5.534  
##  3rd Qu.:3.400      3rd Qu.: 8.000             3rd Qu.: 8.000  
##  Max.   :6.000      Max.   :10.000             Max.   :10.000  
##  monthly_online_orders monthly_store_visits avg_online_spend avg_store_spend 
##  Min.   : 0.00         Min.   : 0.000       Min.   :   523   Min.   :   542  
##  1st Qu.:12.00         1st Qu.: 5.000       1st Qu.: 36797   1st Qu.: 37724  
##  Median :25.00         Median : 9.000       Median : 74859   Median : 75936  
##  Mean   :24.68         Mean   : 9.482       Mean   : 74555   Mean   : 75662  
##  3rd Qu.:37.00         3rd Qu.:14.000       3rd Qu.:112134   3rd Qu.:113313  
##  Max.   :49.00         Max.   :19.000       Max.   :149996   Max.   :149972  
##  discount_sensitivity return_frequency avg_delivery_days
##  Min.   : 1.000       Min.   :0.000    Min.   :1        
##  1st Qu.: 3.000       1st Qu.:2.000    1st Qu.:2        
##  Median : 6.000       Median :4.000    Median :4        
##  Mean   : 5.499       Mean   :4.467    Mean   :4        
##  3rd Qu.: 8.000       3rd Qu.:7.000    3rd Qu.:6        
##  Max.   :10.000       Max.   :9.000    Max.   :7        
##  delivery_fee_sensitivity free_return_importance product_availability_online
##  Min.   : 1.000           Min.   : 1.000         Min.   : 1.000             
##  1st Qu.: 3.000           1st Qu.: 3.000         1st Qu.: 3.000             
##  Median : 5.000           Median : 5.000         Median : 6.000             
##  Mean   : 5.469           Mean   : 5.462         Mean   : 5.519             
##  3rd Qu.: 8.000           3rd Qu.: 8.000         3rd Qu.: 8.000             
##  Max.   :10.000           Max.   :10.000         Max.   :10.000             
##  impulse_buying_score need_touch_feel_score brand_loyalty_score
##  Min.   : 1.000       Min.   : 1.000        Min.   : 1.000     
##  1st Qu.: 3.000       1st Qu.: 3.000        1st Qu.: 3.000     
##  Median : 5.000       Median : 5.000        Median : 6.000     
##  Mean   : 5.486       Mean   : 5.485        Mean   : 5.532     
##  3rd Qu.: 8.000       3rd Qu.: 8.000        3rd Qu.: 8.000     
##  Max.   :10.000       Max.   :10.000        Max.   :10.000     
##  environmental_awareness time_pressure_level    gender         
##  Min.   : 1.000          Min.   : 1.000      Length:11789      
##  1st Qu.: 3.000          1st Qu.: 3.000      Class :character  
##  Median : 5.000          Median : 6.000      Mode  :character  
##  Mean   : 5.449          Mean   : 5.504                        
##  3rd Qu.: 8.000          3rd Qu.: 8.000                        
##  Max.   :10.000          Max.   :10.000                        
##   city_tier         shopping_preference
##  Length:11789       Length:11789       
##  Class :character   Class :character   
##  Mode  :character   Mode  :character   
##                                        
##                                        
## 

Transformar datos

Aquí la ejecución de la función f_transformar_factor(datos), solo se transforman los atributos tipo character a tipo factor para que sirvirán para aplicarlo en modelos de clasificación. sólo se muestran los atributos gender, city_tier y shopping_preference.

datos <- f_transformar_factor(datos)
resumen_transformado <- f_describir_datos(datos)
cat(resumen_transformado$structure)
## spc_tbl_ [11,789 × 25] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ age                        : num [1:11789] 56 69 46 32 60 25 78 38 56 75 ...
##  $ monthly_income             : num [1:11789] 221111 96029 19055 53170 244016 ...
##  $ daily_internet_hours       : num [1:11789] 6.5 8.2 6.4 6.4 6 7.6 4.9 3.1 7.3 6.8 ...
##  $ smartphone_usage_years     : num [1:11789] 12 13 4 11 5 6 6 10 2 4 ...
##  $ social_media_hours         : num [1:11789] 0.7 2.7 2.1 0.7 0.7 4 3.2 3.7 1.1 2.9 ...
##  $ online_payment_trust_score : num [1:11789] 1 6 10 2 2 1 1 4 5 4 ...
##  $ tech_savvy_score           : num [1:11789] 6 9 8 10 5 7 8 9 3 8 ...
##  $ monthly_online_orders      : num [1:11789] 16 14 2 20 18 31 10 17 28 2 ...
##  $ monthly_store_visits       : num [1:11789] 16 1 0 3 16 6 4 7 0 9 ...
##  $ avg_online_spend           : num [1:11789] 28551 124056 81939 35901 131971 ...
##  $ avg_store_spend            : num [1:11789] 144092 28421 128229 134650 34122 ...
##  $ discount_sensitivity       : num [1:11789] 2 4 9 7 5 2 9 2 7 10 ...
##  $ return_frequency           : num [1:11789] 3 7 4 0 9 8 5 1 6 0 ...
##  $ avg_delivery_days          : num [1:11789] 2 4 5 3 2 3 5 2 4 2 ...
##  $ delivery_fee_sensitivity   : num [1:11789] 6 1 3 3 4 5 5 10 5 2 ...
##  $ free_return_importance     : num [1:11789] 7 3 4 10 2 10 2 10 2 9 ...
##  $ product_availability_online: num [1:11789] 7 4 10 2 5 6 8 8 9 6 ...
##  $ impulse_buying_score       : num [1:11789] 1 9 1 4 8 6 9 8 8 9 ...
##  $ need_touch_feel_score      : num [1:11789] 3 6 1 8 9 3 4 9 2 1 ...
##  $ brand_loyalty_score        : num [1:11789] 6 8 3 2 7 4 9 6 6 5 ...
##  $ environmental_awareness    : num [1:11789] 5 1 3 6 1 4 9 1 6 1 ...
##  $ time_pressure_level        : num [1:11789] 2 7 3 6 6 8 8 4 4 7 ...
##  $ gender                     : Factor w/ 3 levels "Female","Male",..: 3 2 1 1 2 3 3 3 1 1 ...
##  $ city_tier                  : Factor w/ 3 levels "Tier 1","Tier 2",..: 3 3 3 1 3 2 2 2 3 3 ...
##  $ shopping_preference        : Factor w/ 3 levels "Hybrid","Online",..: 3 1 3 3 3 3 1 3 3 3 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   age = col_double(),
##   ..   monthly_income = col_double(),
##   ..   daily_internet_hours = col_double(),
##   ..   smartphone_usage_years = col_double(),
##   ..   social_media_hours = col_double(),
##   ..   online_payment_trust_score = col_double(),
##   ..   tech_savvy_score = col_double(),
##   ..   monthly_online_orders = col_double(),
##   ..   monthly_store_visits = col_double(),
##   ..   avg_online_spend = col_double(),
##   ..   avg_store_spend = col_double(),
##   ..   discount_sensitivity = col_double(),
##   ..   return_frequency = col_double(),
##   ..   avg_delivery_days = col_double(),
##   ..   delivery_fee_sensitivity = col_double(),
##   ..   free_return_importance = col_double(),
##   ..   product_availability_online = col_double(),
##   ..   impulse_buying_score = col_double(),
##   ..   need_touch_feel_score = col_double(),
##   ..   brand_loyalty_score = col_double(),
##   ..   environmental_awareness = col_double(),
##   ..   time_pressure_level = col_double(),
##   ..   gender = col_character(),
##   ..   city_tier = col_character(),
##   ..   shopping_preference = col_character()
##   .. )
##  - attr(*, "problems")=<externalptr>
resumen_transformado$summary[,c(23, 24, 25)] # solo las columnas que se necesitan
##     gender      city_tier    shopping_preference
##  Female:3931   Tier 1:3982   Hybrid:  369       
##  Male  :3966   Tier 2:3858   Online: 1176       
##  Other :3892   Tier 3:3949   Store :10244       
##                                                 
##                                                 
## 

Desarrollo

Undersamplig

Unsersamplig significa obtener una muestra con valores de frecuencia de clase de la variable dependiente con valores equilibrados.

Aquí la ejecución de la función f_balancear_clases_undersamplig() previamente codificada, el resultado son un conjunto de datos con registros balanceados con similares frecuncias, sin embargo hubo pérdida de información que no sería tan recomendable.

datos_balanceados <- f_balancear_clases_undersamplig(datos, "shopping_preference")
table(datos_balanceados$shopping_preference)
## 
## Hybrid Online  Store 
##    369    369    369

Particonando datos

Se crean dos conjuntos de datos el 70% para datos de entrenamiento y el 30% para datos de validación.

Datos de entrenamiento

particion <- f_particionar_datos(datos_balanceados)

datos_entrenamiento <- particion$datos_entrenamiento
datos_validacion <- particion$datos_validacion

Aquí los datos de entrenamiento

f_visualizar_head_tail_reducido_word(datos_entrenamiento, n=6)

age

monthly_income

daily_internet_hours

smartphone_usage_years

gender

city_tier

shopping_preference

27

133209

4.8

2

Male

Tier 1

Online

25

235084

6.9

3

Female

Tier 3

Store

18

27699

7.3

11

Other

Tier 3

Hybrid

24

101623

4.3

7

Other

Tier 2

Hybrid

26

22273

5.7

11

Male

Tier 2

Online

31

136685

7.2

2

Male

Tier 1

Hybrid

...

...

...

...

...

...

...

45

206494

6.7

8

Other

Tier 3

Store

30

242603

4.1

5

Female

Tier 2

Store

32

95145

8

8

Male

Tier 1

Online

51

120494

5.5

4

Female

Tier 1

Hybrid

78

68546

6.9

3

Female

Tier 3

Hybrid

36

51558

6.1

12

Female

Tier 2

Store

Datos de validación

Aquí los datos de validación que deben ser excluyentes.

f_visualizar_head_tail_reducido_word(datos_validacion, n=6)

age

monthly_income

daily_internet_hours

smartphone_usage_years

gender

city_tier

shopping_preference

70

197296

8.6

13

Male

Tier 2

Hybrid

59

51787

7.2

2

Male

Tier 2

Hybrid

29

75840

2.3

14

Male

Tier 2

Hybrid

43

161708

7.8

14

Male

Tier 1

Hybrid

38

215798

6.8

11

Other

Tier 3

Hybrid

25

135334

6.3

6

Other

Tier 1

Hybrid

...

...

...

...

...

...

...

52

43582

4.5

4

Other

Tier 3

Store

55

36173

3.2

1

Male

Tier 3

Store

25

122779

5.1

13

Other

Tier 1

Store

43

182606

5.2

4

Female

Tier 2

Store

30

52851

4

5

Male

Tier 3

Store

62

97971

7.4

5

Female

Tier 3

Store

Construir modelo

Se implementa un modelo de regresión loística multinomial con la función llamada f_implementar_modelo_RL_multinomial () previamente preparada.

Mandar llamar la función que construye el modelo de regresión logística con los datos de entrenamiento y mandar llamar la función para observar la salida de los coeficientes consola.

v_dependiente <- "shopping_preference"
v_independientes <- setdiff(names(datos_entrenamiento), v_dependiente)

resultado <- f_implementar_modelo_RL_multinomial(
  datos = datos_entrenamiento,
  variable_dependiente = v_dependiente,
  variables_independientes = v_independientes
)

print(resultado$tabla_coef, width = 200)
##     Clase                    Variable          Coef           SE             Z   p_value
## 1  Online                 (Intercept) -2.874676e+02 2.536561e-06 -1.133296e+08 0.0000000
## 2  Online                         age -5.154600e-01 2.670773e-04 -1.930003e+03 0.0000000
## 3  Online              monthly_income  2.474522e-04 2.186312e-02  1.131825e-02 0.9909695
## 4  Online        daily_internet_hours  1.360142e+02 6.021557e-05  2.258787e+06 0.0000000
## 5  Online      smartphone_usage_years  4.306641e+00 6.326054e-05  6.807784e+04 0.0000000
## 6  Online          social_media_hours  8.053809e+00 4.531561e-05  1.777270e+05 0.0000000
## 7  Online  online_payment_trust_score  9.380885e+01 1.257533e-04  7.459753e+05 0.0000000
## 8  Online            tech_savvy_score  1.768306e+02 1.545921e-04  1.143853e+06 0.0000000
## 9  Online       monthly_online_orders  2.994965e+01 3.111594e-04  9.625178e+04 0.0000000
## 10 Online        monthly_store_visits -6.387067e+01 2.663272e-04 -2.398203e+05 0.0000000
## 11 Online            avg_online_spend -2.763410e-04 4.117890e-02 -6.710743e-03 0.9946456
## 12 Online             avg_store_spend -1.003629e-01 1.832960e-01 -5.475452e-01 0.5840042
## 13 Online        discount_sensitivity  3.407696e+00 7.684348e-05  4.434594e+04 0.0000000
## 14 Online            return_frequency  2.338544e+00 1.321700e-05  1.769346e+05 0.0000000
## 15 Online           avg_delivery_days  3.175492e+00 2.555066e-05  1.242822e+05 0.0000000
## 16 Online    delivery_fee_sensitivity  4.631437e+00 6.279266e-06  7.375761e+05 0.0000000
## 17 Online      free_return_importance -8.092997e-01 6.483750e-06 -1.248197e+05 0.0000000
## 18 Online product_availability_online  7.979686e+01 6.963771e-05  1.145886e+06 0.0000000
## 19 Online        impulse_buying_score  9.438627e-01 1.569387e-05  6.014213e+04 0.0000000
## 20 Online       need_touch_feel_score -2.046017e+02 1.286416e-04 -1.590478e+06 0.0000000
## 21 Online         brand_loyalty_score -8.410928e-01 4.422431e-05 -1.901879e+04 0.0000000
## 22 Online     environmental_awareness -1.697487e+00 2.088825e-05 -8.126514e+04 0.0000000
## 23 Online         time_pressure_level -1.152261e+00 8.102696e-05 -1.422071e+04 0.0000000
## 24 Online                  genderMale -1.240903e+00 4.392817e-06 -2.824845e+05 0.0000000
## 25 Online                 genderOther  2.742280e+00 1.005893e-06  2.726215e+06 0.0000000
## 26 Online             city_tierTier 2 -3.282261e+01 1.539574e-05 -2.131928e+06 0.0000000
## 27 Online             city_tierTier 3 -8.221837e+01 5.664700e-11 -1.451416e+12 0.0000000
## 28  Store                 (Intercept) -1.453205e+02 9.211706e-05 -1.577564e+06 0.0000000
## 29  Store                         age  1.424170e+00 8.170720e-03  1.743017e+02 0.0000000
## 30  Store              monthly_income -1.660146e-03 1.190090e-01 -1.394974e-02 0.9888701
## 31  Store        daily_internet_hours -8.782473e+01 1.171871e-03 -7.494404e+04 0.0000000
## 32  Store      smartphone_usage_years -1.221478e+01 9.584414e-05 -1.274442e+05 0.0000000
## 33  Store          social_media_hours -1.863344e+00 1.575587e-04 -1.182635e+04 0.0000000
## 34  Store  online_payment_trust_score -6.946297e+01 3.324116e-04 -2.089667e+05 0.0000000
## 35  Store            tech_savvy_score -9.710349e+01 7.878193e-04 -1.232560e+05 0.0000000
## 36  Store       monthly_online_orders -1.511785e+01 4.320219e-04 -3.499325e+04 0.0000000
## 37  Store        monthly_store_visits  2.970240e+01 1.160304e-03  2.559882e+04 0.0000000
## 38  Store            avg_online_spend -5.626877e-04 6.513041e-01 -8.639402e-04 0.9993107
## 39  Store             avg_store_spend  5.537741e-02 1.750993e+00  3.162628e-02 0.9747701
## 40  Store        discount_sensitivity  3.605478e+00 4.812752e-04  7.491510e+03 0.0000000
## 41  Store            return_frequency  1.120195e+01 6.778570e-04  1.652554e+04 0.0000000
## 42  Store           avg_delivery_days  1.492670e+01 4.207130e-04  3.547954e+04 0.0000000
## 43  Store    delivery_fee_sensitivity -1.629546e+01 8.836798e-04 -1.844046e+04 0.0000000
## 44  Store      free_return_importance  1.476000e+01 1.209598e-03  1.220241e+04 0.0000000
## 45  Store product_availability_online -4.562531e+01 5.242943e-04 -8.702232e+04 0.0000000
## 46  Store        impulse_buying_score -1.244499e+01 8.668911e-05 -1.435589e+05 0.0000000
## 47  Store       need_touch_feel_score  8.905526e+01 4.405350e-04  2.021525e+05 0.0000000
## 48  Store         brand_loyalty_score  2.308823e+01 2.658463e-04  8.684805e+04 0.0000000
## 49  Store     environmental_awareness  3.401189e+00 1.995083e-04  1.704786e+04 0.0000000
## 50  Store         time_pressure_level  1.646759e+01 1.003648e-03  1.640774e+04 0.0000000
## 51  Store                  genderMale  1.175781e+02 3.586633e-05  3.278230e+06 0.0000000
## 52  Store                 genderOther  1.629011e+02 1.279810e-04  1.272853e+06 0.0000000
## 53  Store             city_tierTier 2 -6.480110e+01 8.178363e-05 -7.923480e+05 0.0000000
## 54  Store             city_tierTier 3 -9.959389e+01 4.592075e-05 -2.168821e+06 0.0000000

Evaluar el modelo

Se ejecuta la función f_evaluar_modelo():

f_evaluar_modelo <- function(modelo, datos_validacion) {
  #------------------------------------------------------------
  # f_evaluar_modelo()
  #
  # Objetivo:
  #   Evaluar un modelo de clasificación (p.ej. multinomial)
  #   usando los datos de validación, calculando la matriz de
  #   confusión y métricas derivadas.
  #
  # Criterio de aceptación (definido por el usuario):
  #   - Si Accuracy >= 0.70 => modelo aceptado (en términos de exactitud)
  #   - Si Accuracy <  0.70 => modelo no aceptado
  #
  # Argumentos:
  #   modelo          : modelo entrenado (ej. nnet::multinom)
  #   datos_validacion: data.frame/tibble con variables predictoras
  #                    y la variable objetivo.
  #
  # Retorna:
  #   Lista con:
  #     $matriz_confusion        : tabla (conteos)
  #     $accuracy                : exactitud global
  #     $accuracy_por_clase      : (vector) sensibilidad/recall por clase
  #     $precision_por_clase     : (vector) precision por clase
  #     $f1_por_clase            : (vector) F1 por clase
  #     $macro_f1                : promedio simple de F1 por clase
  #     $decision                : "ACEPTAR" / "NO ACEPTAR"
  #     $predicciones            : factor de clases predichas
  #
  # Requiere:
  #   (base R) + stats
  #------------------------------------------------------------

  # Ajusta aquí si tu variable objetivo tiene otro nombre
  variable_objetivo <- "shopping_preference"

  if (!(variable_objetivo %in% names(datos_validacion))) {
    stop(paste("No existe la variable objetivo", variable_objetivo, "en datos_validacion."))
  }

  # Asegurar que sea factor y que sus niveles estén presentes
  y_true <- as.factor(datos_validacion[[variable_objetivo]])

  # Predicciones (clase)
  y_pred <- predict(modelo, newdata = datos_validacion, type = "class")
  y_pred <- as.factor(y_pred)

  # Alinear niveles (importante para tabla consistente)
  niveles <- union(levels(y_true), levels(y_pred))
  y_true <- factor(y_true, levels = niveles)
  y_pred <- factor(y_pred, levels = niveles)

  # Matriz de confusión (conteos)
  cm <- table(Real = y_true, Predicho = y_pred)

  # Accuracy global
  accuracy <- sum(diag(cm)) / sum(cm)

  # Métricas por clase:
  # Recall/Sensibilidad = TP / (TP + FN) = diag / rowSums
  recall <- diag(cm) / rowSums(cm)

  # Precision = TP / (TP + FP) = diag / colSums
  precision <- diag(cm) / colSums(cm)

  # F1 = 2 * (P*R)/(P+R)
  f1 <- 2 * (precision * recall) / (precision + recall)

  # Manejar NaN (puede ocurrir si una clase no se predijo o no aparece)
  recall[is.na(recall)] <- 0
  precision[is.na(precision)] <- 0
  f1[is.na(f1)] <- 0

  macro_f1 <- mean(f1)

  # Decisión según tu criterio
  decision <- ifelse(accuracy >= 0.70, "ACEPTAR", "NO ACEPTAR")

  return(list(
    matriz_confusion = cm,
    accuracy = accuracy,
    recall_por_clase = recall,
    precision_por_clase = precision,
    f1_por_clase = f1,
    macro_f1 = macro_f1,
    decision = decision,
    predicciones = y_pred
  ))
}

Aquí el resultado de la evaluación del modelo, por el momento, solo el valor del estadístico accuracy:

evaluacion <- f_evaluar_modelo(modelo = resultado$modelo, datos_validacion = datos_validacion)

evaluacion$matriz_confusion
##         Predicho
## Real     Hybrid Online Store
##   Hybrid     95     10     3
##   Online      2    104     0
##   Store       3      0   116
evaluacion$accuracy
## [1] 0.9459459

El modelo se acepta en término de solo exactitud global dado que modelo acierta en el 94% de los registros del conjunto de datos, que significa que para un nuevo, hay una probabilidad de tener certeza en un 94%.

Que significa este 94% depende del contexto de los datos si son de salud hay que buscar modelos muy exactos tal vez 99%, si es industria o servicios tal vez 95% si es en términos sociológicos tal vez 90%, si no hay tanto trigo tal ve un 80% o hasta el 70% de solo exactitud habiendo recfreado predicciones y evaluado con la matriz de confusión.

Interpretación

Con programación R, el caso siguió la metodología sugerida, se declaró el objetivo; se describieron los datos; se incluyó el desarrollo y la interpretación del mismo.

Para el correcto funcionamiento del caso, se presentó la carga de librerías y se indicaron las funciones necesarias; se cargaron, transformaron y prepararon los datos; se utilizó un modelo de regresión logística para cumplir con el objetivo de resolver una tarea de predicción; se utilizó la técnica de matriz de confusión para solo evaluar mediante el estadístico accuracy o exactitud del modelo predictivo.

La evaluación del modelo fue que este acierta en el 94% de los compradores es decir que hay una probabilidad de que el modelo acierte al 94% una predicción de si es comprador de tienda, en línea o ambas.