Práctica 2

Selección, limpieza y transformación de datos

Author

Dr. Jorge Párraga Álava

Minería de datos

Introducción

En esta práctica, abordamos el desafío de predecir las ventas mensuales de una cadena de supermercados en Ecuador. El objetivo principal es aplicar técnicas de selección, limpieza y transformación de datos para preparar un conjunto de datos que sea adecuado para el desarrollo de un modelo predictivo. Este modelo permitirá a la empresa optimizar su inventario, planificar campañas de marketing de manera más efectiva y mejorar la gestión de la cadena de suministro.

Los datos disponibles incluyen información detallada sobre productos, ubicaciones, clientes y promociones, lo que ofrece una base rica para el análisis. Como primer paso en este proyecto, se requiere llevar a cabo un proceso exhaustivo de selección, limpieza y transformación de los datos, garantizando que estén en las condiciones óptimas para un análisis y modelado precisos.

Configuración Inicial

# Bibliotecas -------------------------------------------------------------
if(!require(tidyverse)) install.packages("tidyverse") 

# Config -------------------------------------------------------------
# Para notación no numérica.
options(scipen=999) 
# Para reproducibilidad.
set.seed(2026) 

0. Dominio del problema

En este paso previo, desarrollamos una comprensión profunda del problema que enfrentamos y lo traducimos en datos “entendibles” para la máquina. Respondamos a las preguntas clave:

  1. ¿Cuál es el problema específico que estamos tratando de resolver?

    Estamos tratando de predecir las ventas mensuales de una cadena de supermercados en Ecuador en función de factores como el histórico de ventas, la ubicación, el tipo de producto, las características del cliente y las promociones.

  2. ¿Es factible lograr nuestros objetivos con los datos disponibles? ¿Cómo podemos alcanzarlos?

    Sí, es factible. Tenemos acceso a un conjunto de datos histórico de ventas que incluye información sobre productos, ubicaciones, clientes y promociones.

  3. ¿Cuáles son los beneficios si nuestra solución funciona y cuáles son las consecuencias si falla?

    Si nuestra solución funciona:

    • La cadena de supermercados podrá optimizar su inventario por ciudad y tipo de producto.
    • Se podrán planificar campañas de marketing de manera más efectiva, enfocándose en productos y segmentos de clientes específicos.
    • Mejorará la gestión de la cadena de suministro, reduciendo costos y desperdicios.
    • Aumentarán las ganancias al tener una mejor previsión de la demanda.

    Si la solución falla:

    • La cadena podría enfrentar pérdidas financieras debido a exceso o falta de inventario.
    • Podrían surgir dificultades en la gestión de la cadena de suministro y la planificación de promociones.
    • Se podrían tomar decisiones de negocio basadas en predicciones incorrectas, lo que afectaría la competitividad de la empresa.
  4. ¿Qué tipo de problema se va a resolver: Predictivo/Descriptivo?

    Estamos abordando principalmente un problema predictivo, ya que queremos predecir las ventas futuras en función de datos históricos y variables predictoras. Sin embargo, también incluiremos elementos descriptivos para entender mejor los patrones de compra y el comportamiento del cliente.

  5. ¿El objetivo es predecir, segmentar, …?

    Nuestro objetivo principal es predecir las ventas mensuales. Adicionalmente, podríamos resolver otras tareas:

    • Segmentación de clientes para comprender mejor el comportamiento de compra de diferentes grupos.
    • Análisis de tendencias de ventas por ubicación y tipo de producto.
  6. ¿De dónde vendrán los datos, cuánto cuesta conseguirlos?

    Los datos de ventas históricas provienen de la base de datos interna de la cadena de supermercados, en concreto de los sistemas de gestión de la empresa. El costo de adquirir estos datos es mínimo, ya que la mayoría estaría disponible internamente. Sin embargo, podría haber costos asociados con la limpieza y preparación de los datos, así como con la implementación de sistemas para recopilar datos adicionales si fuera necesario.

1. Selección

En esta etapa, se realiza la selección del conjunto de datos. Como este es proporcionado por la empresa, únicamente lo cargamos.

 # Cargar datos 
ventas_ecuador <- read.csv("ventas_ecuador.csv")
# mostrar un vista previa de datos
glimpse(ventas_ecuador)
Rows: 2,350
Columns: 9
$ fecha                <chr> "2019-01-01", "2019-01-02", "2019-01-03", "2019-0…
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 19, 46, 6…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 32, 42, 1…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, 2, 5, 4, 1…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…

Se observa que el conjunto de datos tiene 2350 instancias y 10 variables. Se muestran ejemplos de los datos que contiene cada columna así como su tipo de variable.

2. Limpieza

La limpieza de datos es un paso crucial para garantizar la calidad de nuestro análisis. Abordaremos varios aspectos:

2.1 Limpieza

Eliminación de observaciones duplicadas

# Verificamos si hay duplicados
ventas_repetidas <- ventas_ecuador |> 
  group_by_all() |> 
  filter(n() > 1) |> 
  ungroup()
# Vemos las repetidas
glimpse(ventas_repetidas)
Rows: 20
Columns: 9
$ fecha                <chr> "2019-01-01", "2019-01-02", "2019-01-03", "2019-0…
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 49, 31, 3…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 24, 24, 4…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 2, 4, 5, 4, 3, 4, 4…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…
# Eliminamos duplicados
ventas_ecuador_limpio <- ventas_ecuador |> distinct()


cat("Filas originales:", nrow(ventas_ecuador), "\n")
Filas originales: 2350 
cat("Filas con repetidos:", nrow(ventas_repetidas), "\n")
Filas con repetidos: 20 
cat("Filas después de eliminar duplicados:", nrow(ventas_ecuador_limpio), "\n")
Filas después de eliminar duplicados: 2340 

Valores faltantes

# Verificamos los NA
valores_faltantes <- ventas_ecuador_limpio |> summarise_all(~sum(is.na(.)))
# Mostramos los Na en cada variable
glimpse(valores_faltantes)
Rows: 1
Columns: 9
$ fecha                <int> 0
$ ciudad               <int> 0
$ producto             <int> 0
$ ventas               <int> 0
$ unidades_vendidas    <int> 0
$ edad_cliente         <int> 51
$ promocion            <int> 0
$ satisfaccion_cliente <int> 0
$ tasa_devolucion      <int> 0

Observamos los faltantes en la variable edad_cliente. Para corregir los NA realizamos lo siguiente:

ventas_ecuador_limpio <- ventas_ecuador_limpio |>
  mutate(
    # Reemplazamos los NA en 'edad_cliente' con la mediana de la columna
    edad_cliente = ifelse(is.na(edad_cliente), median(edad_cliente, na.rm = TRUE), edad_cliente)
  )

# Verificamos nuevamente si quedan NA en 'edad_cliente'
valores_faltantes <- ventas_ecuador_limpio |>
  summarise(edad_cliente = sum(is.na(edad_cliente)))

glimpse(valores_faltantes)
Rows: 1
Columns: 1
$ edad_cliente <int> 0

Errores estructurales

Vamos a asegurarnos de que los nombres de las ciudades estén escritos correctamente:

# Verificamos las formas de escritura de cada ciudad
ventas_ecuador_limpio |> 
  count(ciudad)
      ciudad   n
1     Cuenca 471
2  GUAYAQUIL   1
3  Guayaquil 452
4      Manta 478
5 Portoviejo 470
6      Quito 465
7     cuenca   1
8 portoviejo   1
9      quito   1

Observamos que hay diversas formas de escritura de una misma ciudad. Para corregir eso realizamos lo siguiente:

# Convertimos todos los nombre a formato titulo con str_to_title
ventas_ecuador_limpio <- ventas_ecuador_limpio |>
  mutate(ciudad = str_to_title(ciudad))
# Verificamos
ventas_ecuador_limpio |> 
  count(ciudad)
      ciudad   n
1     Cuenca 472
2  Guayaquil 453
3      Manta 478
4 Portoviejo 471
5      Quito 466

Fechas fuera de rango

Verificaremos si hay fechas fuera del rango esperado:

# Obtener el rango actual de fechas en el dataset
rango_fechas <- ventas_ecuador_limpio |>
  summarise(
    fecha_min = min(fecha),
    fecha_max = max(fecha)
  )
print(rango_fechas) 
   fecha_min  fecha_max
1 2019-01-01 2026-12-31

Se evidencia que algunas fechas son posteriores al 2026. Estos casos serán eliminados.

# Filtrar para eliminar fechas superiores al año 2026
ventas_ecuador_limpio <- ventas_ecuador_limpio |>
  filter(year(fecha) <= 2026)

# Verificar el nuevo rango de fechas
nuevo_rango_fechas <- ventas_ecuador_limpio |>
  summarise(
    fecha_min = min(fecha),
    fecha_max = max(fecha)
  )

print(nuevo_rango_fechas)
   fecha_min  fecha_max
1 2019-01-01 2026-12-31

2.2 Enriquecimiento

En esta etapa, agregaremos información adicional para enriquecer nuestro conjunto de datos.

Enriquecimiento geográfico

Añadiremos información sobre la región de cada ciudad:

ventas_ecuador_enriquecido <- ventas_ecuador_limpio |>
  mutate(
    region = case_when(
      ciudad %in% c("Quito") ~ "Sierra",
      ciudad %in% c("Guayaquil", "Manta", "Portoviejo") ~ "Costa",
      ciudad == "Cuenca" ~ "Austro",
      TRUE ~ "Otra"
    )
  )
glimpse(ventas_ecuador_enriquecido)
Rows: 2,340
Columns: 10
$ fecha                <chr> "2019-01-01", "2019-01-02", "2019-01-03", "2019-0…
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 19, 46, 6…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 32, 42, 1…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, 2, 5, 4, 1…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…
$ region               <chr> "Sierra", "Costa", "Costa", "Sierra", "Costa", "C…

Enriquecimiento demográfico

Categorizaremos a los clientes por grupo de edad:

ventas_ecuador_enriquecido <- ventas_ecuador_enriquecido |>
  mutate(
    edad_grupo = cut(edad_cliente, 
                     breaks = c(0, 25, 40, 60, Inf),
                     labels = c("Joven", "Adulto joven", "Adulto", "Adulto mayor"),
                     right = FALSE)
  )
glimpse(ventas_ecuador_enriquecido)
Rows: 2,340
Columns: 11
$ fecha                <chr> "2019-01-01", "2019-01-02", "2019-01-03", "2019-0…
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 19, 46, 6…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 32, 42, 1…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, 2, 5, 4, 1…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…
$ region               <chr> "Sierra", "Costa", "Costa", "Sierra", "Costa", "C…
$ edad_grupo           <fct> Joven, Joven, Adulto, Adulto, Adulto joven, Adult…

Enriquecimiento de productos

Añadiremos una categorización de productos:

ventas_ecuador_enriquecido <- ventas_ecuador_enriquecido |>
  mutate(
    categoria_producto = case_when(
      producto %in% c("Arroz", "Atún", "Leche", "Pan") ~ "Alimentos básicos",
      producto %in% c("Frutas", "Verduras") ~ "Productos frescos",
      producto %in% c("Carne", "Pollo") ~ "Proteínas",
      TRUE ~ "Otro"
    )
  )
glimpse(ventas_ecuador_enriquecido)
Rows: 2,340
Columns: 12
$ fecha                <chr> "2019-01-01", "2019-01-02", "2019-01-03", "2019-0…
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 19, 46, 6…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 32, 42, 1…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, 2, 5, 4, 1…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…
$ region               <chr> "Sierra", "Costa", "Costa", "Sierra", "Costa", "C…
$ edad_grupo           <fct> Joven, Joven, Adulto, Adulto, Adulto joven, Adult…
$ categoria_producto   <chr> "Alimentos básicos", "Alimentos básicos", "Produc…

2.3 Análisis Exploratorio de Datos (EDA)

El EDA nos ayudará a comprender mejor la naturaleza de nuestros datos. Analizaremos cada variable considerando si es numérica o categórica.

# Asegurémonos de que estamos trabajando con el conjunto de datos de calidad
ventas_ecuador_enriquecido <- ventas_ecuador_enriquecido |>
  mutate(fecha = as.Date(fecha))  # Asegurarse de que fecha es de tipo Date
glimpse(ventas_ecuador_enriquecido)
Rows: 2,340
Columns: 12
$ fecha                <date> 2019-01-01, 2019-01-02, 2019-01-03, 2019-01-04, …
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 19, 46, 6…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 32, 42, 1…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, 2, 5, 4, 1…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…
$ region               <chr> "Sierra", "Costa", "Costa", "Sierra", "Costa", "C…
$ edad_grupo           <fct> Joven, Joven, Adulto, Adulto, Adulto joven, Adult…
$ categoria_producto   <chr> "Alimentos básicos", "Alimentos básicos", "Produc…

Variable: fecha (Temporal)

Primero, observamos la distribución tipo cuartiles de la variable fecha.

# Resumen básico
summary(ventas_ecuador_enriquecido$fecha)
        Min.      1st Qu.       Median         Mean      3rd Qu.         Max. 
"2019-01-01" "2020-08-07" "2022-03-15" "2022-03-15" "2023-10-17" "2026-12-31" 

Luego, la distribución temporal de las fechas.

# Visualización de la distribución temporal
ggplot(ventas_ecuador_enriquecido, aes(x = fecha)) +
  geom_histogram(binwidth = 7, fill = "skyblue", color = "black") +
  labs(title = "Distribución de Ventas a lo largo del tiempo",
       x = "Fecha", y = "Número de Ventas")

Y finalmente el comportamiento de las ventas por mes

# Ventas por mes
ventas_ecuador_enriquecido |>
  mutate(mes = floor_date(fecha, "month")) |>
  group_by(mes) |>
  summarise(total_ventas = sum(ventas)) |>
  ggplot(aes(x = mes, y = total_ventas)) +
  geom_line() +
  geom_point() +
  labs(title = "Ventas Totales por Mes",
       x = "Mes", y = "Ventas totales")

A partir de los análisis concluimos que las ventas muestran cierta estacionalidad mensual, con picos y valles a lo largo del año.

Variable: ciudad (Cualitativa)

Para la variable ciudad analizamos la cantidad de registros para cada ciudad.

# Frecuencia de ventas por ciudad
ventas_ecuador_enriquecido |> count(ciudad) 
      ciudad   n
1     Cuenca 472
2  Guayaquil 453
3      Manta 478
4 Portoviejo 471
5      Quito 466

Ahora analizamos la distribución de ventas por ciudad mediante un diagrama de cajas.

# Visualización de ventas por ciudad
ggplot(ventas_ecuador_enriquecido, aes(x = ciudad, y = ventas)) +
  geom_boxplot(fill = "lightgreen") +
  labs(title = "Distribución de ventas (cantidad) por ciudad",
       x = "Ciudad", y = "Ventas (cantidad)") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Finalmente, observamos las ventas totales por ciudad.

# Ventas totales por ciudad
ventas_ecuador_enriquecido |>
  group_by(ciudad) |>
  summarise(total_ventas = sum(ventas)) |>
  ggplot(aes(x = reorder(ciudad, -total_ventas), y = total_ventas)) +
  geom_bar(stat = "identity", fill = "coral") +
  labs(title = "Ventas totales ($) por ciudad",
       x = "Ciudad", y = "Ventas totales ($)") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Finalmente podemos darnos cuenta que la cantidad de ventas entre ciudades parecen muy similares, con pequeña cantidad superior acumulada en dólares en las ventas de Quito.

Variable: producto (Cualitativa)

Para la variable producto analizamos la cantidad de registros de cada ítem.

# Frecuencia de ventas por producto
ventas_ecuador_enriquecido |> count(producto) 
  producto   n
1    Arroz 298
2     Atún 279
3    Carne 280
4   Frutas 307
5    Leche 287
6      Pan 296
7    Pollo 286
8 Verduras 307

Observamos que en total hay 8 distintos productos. Ahora veamos las ventas registradas de cada uno.

# Visualización de ventas por producto
ggplot(ventas_ecuador_enriquecido, aes(x = producto, y = ventas)) +
  geom_boxplot(fill = "lightblue") +
  labs(title = "Distribución de ventas por producto",
       x = "Producto", y = "Ventas (cantidad)") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

# Ventas totales por producto
ventas_ecuador_enriquecido |>
  group_by(producto) |>
  summarise(total_ventas = sum(ventas)) |>
  ggplot(aes(x = reorder(producto, -total_ventas), y = total_ventas)) +
  geom_bar(stat = "identity", fill = "gray") +
  labs(title = "Ventas totales ($) por Producto",
       x = "Producto", y = "Ventas totales ($)") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Con el análisis podemos concluir que las ventas de varios productos parecen ser homogéneas, sin embargo, las verduras generan más ingresos a la empresa.

Variable: ventas (Cuantitativa)

Para la variable ventas, que es la variable a predecir, realizamos estadística descriptiva.

# Resumen estadístico
ventas_resumen <- ventas_ecuador_enriquecido |>
  summarise(
    conteo = n(),
    media = mean(ventas, na.rm = TRUE),
    mediana = median(ventas, na.rm = TRUE),
    desviacion_estandar = sd(ventas, na.rm = TRUE),
    min = min(ventas, na.rm = TRUE),
    max = max(ventas, na.rm = TRUE),
    q1 = quantile(ventas, 0.25, na.rm = TRUE),
    q3 = quantile(ventas, 0.75, na.rm = TRUE)
  )

print(ventas_resumen)
  conteo    media mediana desviacion_estandar   min    max       q1      q3
1   2340 273.8428 270.615            128.0231 50.03 499.34 168.1675 383.075

También, visualizamos la cantidad de ventas.

# Histograma de ventas
ggplot(ventas_ecuador_enriquecido, aes(x = ventas)) +
  geom_histogram(bins = 30, fill = "pink", color = "black") +
  labs(title = "Distribución de ventas (cantidad)",
       x = "Ventas", y = "Frecuencia")

Finalmente, mostramos el diagrama de cajas de la variable.

# Boxplot de ventas
ggplot(ventas_ecuador_enriquecido, aes(y = ventas)) +
  geom_boxplot(fill = "lightyellow") +
  labs(title = "Boxplot de ventas",
       y = "Ventas")

Del análisis se puede concluir que las ventas tienen una distribución bastante simétrica y no parecen presentar valores atípicos extremos. La mayoría de las ventas se concentran en el rango de USD200 a USD400, con una mediana alrededor de USD300.

Variable: unidades_vendidas (Cuantitativa)

Mostramos un resumen descriptivo de la variable.

# Resumen estadístico
unidades_resumen <- ventas_ecuador_enriquecido |>
  summarise(
    conteo = n(),
    media = mean(unidades_vendidas, na.rm = TRUE),
    mediana = median(unidades_vendidas, na.rm = TRUE),
    desviacion_estandar = sd(unidades_vendidas, na.rm = TRUE),
    min = min(unidades_vendidas, na.rm = TRUE),
    max = max(unidades_vendidas, na.rm = TRUE),
    q1 = quantile(unidades_vendidas, 0.25, na.rm = TRUE),
    q3 = quantile(unidades_vendidas, 0.75, na.rm = TRUE)
  )

print(unidades_resumen)
  conteo    media mediana desviacion_estandar min max q1 q3
1   2340 25.71068      26            14.44651   1  50 13 39

De igual modo, generamos un diagrama de columnas de las unidades vendidas.

# Diagrama de columnas de unidades vendidas
ggplot(ventas_ecuador_enriquecido, aes(x = as.factor(unidades_vendidas))) +
  geom_bar(fill = "magenta", color = "black") +
  labs(title = "Distribución de Unidades vendidas",
       x = "Unidades vendidas", y = "Frecuencia") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))

Finalmente, analizamos las unidades vendidas por productos.

 # Gráfico de cajas de unidades vendidas por producto
ggplot(ventas_ecuador_enriquecido, aes(x = producto, y = unidades_vendidas, fill = producto)) +
  geom_boxplot() +
  labs(title = "Distribución de Unidades vendidas por Producto",
       x = "Producto", y = "Unidades vendidas") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Observamos que mayoritariamente se venden 16 unidades de productos. La cantidad vendida de cada producto es similar.

Variable: edad_cliente (Cuantitativa)

Para la edad del cliente analizamos su comportamiento a través de un histograma.

# Histograma de edad del cliente
ggplot(ventas_ecuador_enriquecido, aes(x = edad_cliente)) +
  geom_histogram(bins = 20, fill = "orange", color = "black") +
  labs(title = "Distribución de Edad de los clientes",
       x = "Edad", y = "Frecuencia")

Ahora analizamos la relación entre edad del cliente y ventas.

# Crear rangos de edad
ventas_ecuador_enriquecido$edad_rango <- cut(ventas_ecuador_enriquecido$edad_cliente, breaks = seq(0, 100, by = 10), right = FALSE)

# Gráfico de cajas por rango de edad
ggplot(ventas_ecuador_enriquecido, aes(x = edad_rango, y = ventas, fill = edad_rango)) +
  geom_boxplot() +
  labs(title = "Distribución de Ventas por rango de edad del cliente",
       x = "Rango de Edad", y = "Ventas") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  theme_minimal()

A partir de las visualizaciones observamos que mayoritariamente los clientes tienen cerca de 40 años. Pero los que mayores ventas han representado a la empresa son aquellos en el rango de 20 a 30 años.

Variable: promocion (Cualitativa)

Analizaremos la variable promocion, que es cualitativa y representa si una venta fue realizada con o sin promoción. Primero, calcularemos la frecuencia de ventas en cada categoría de promoción.

# Cálculo de la frecuencia de ventas con y sin promoción
promocion_freq <- ventas_ecuador_enriquecido |>
  count(promocion) |>
  rename(Frecuencia = n) |>
  mutate(Porcentaje = (Frecuencia / sum(Frecuencia)) * 100)

# Mostrar la tabla de frecuencias
print(promocion_freq) 
  promocion Frecuencia Porcentaje
1     FALSE       1854   79.23077
2      TRUE        486   20.76923

Ahora, visualizaremos cómo varían las ventas entre las ventas con promoción y las ventas sin promoción usando un gráfico de cajas (boxplot).

# Comparación de ventas con y sin promoción
ggplot(ventas_ecuador_enriquecido, aes(x = factor(promocion), y = ventas, fill = factor(promocion))) +
  geom_boxplot() +
  scale_fill_manual(values = c("purple", "lightseagreen"), 
                    labels = c("Sin promoción", "Con promoción")) +
  labs(title = "Comparación de Ventas con y sin promoción",
       x = "Promoción", y = "Ventas", fill = "Promoción")

De los análisis inferimos que la mayoría de las ventas fue realizada sin promoción, lo que representa cerca del 20%. Al observar las ventas no se evidencia que las mismas sean mayores cuando se hacen con promociones.

Variable: satisfaccion_cliente (Cualitativa)

Primero calcularemos la frecuencia de cada nivel de satisfacción.

# Cálculo de la frecuencia de niveles de satisfacción, incluyendo porcentaje
satisfaccion_freq <- ventas_ecuador_limpio |>
  count(satisfaccion_cliente) |>
  rename(Frecuencia = n) |>
  mutate(Porcentaje = (Frecuencia / sum(Frecuencia)) * 100)

# Mostrar la tabla de frecuencias y porcentajes
print(satisfaccion_freq)
  satisfaccion_cliente Frecuencia Porcentaje
1                    1        469   20.04274
2                    2        484   20.68376
3                    3        441   18.84615
4                    4        484   20.68376
5                    5        462   19.74359

Ahora exploraremos la relación entre la satisfacción del cliente y las ventas mediante un gráfico de cajas (boxplot).

# Relación entre satisfacción del cliente y ventas
ggplot(ventas_ecuador_limpio, aes(x = factor(satisfaccion_cliente), y = ventas)) +
  geom_boxplot(fill = "lightgreen") +
  labs(title = "Relación entre Satisfacción del cliente y ventas",
       x = "Nivel de Satisfacción", y = "Ventas")

La variable satisfacción del cliente tiene cinco categorías que están bastante balanceadas en cerca del 20% cada una. En cuanto a estos niveles y las ventas, también son homogéneas con una pequeña baja en ventas en el nivel de satisfacción 3.

Conclusiones del EDA

Estos hallazgos proporcionan insights valiosos que pueden informar decisiones de negocio y estrategias de marketing, así como ayudar en la selección de características para modelos predictivos.

3. Transformación

En esta etapa, modificaremos la forma de nuestros datos para prepararlos para el análisis.

Eliminación de valores ausentes

Ya verificamos que no hay valores ausentes en nuestro conjunto de datos.

Discretización de atributos

# Discretizaremos la variable 'ventas' en categorías.
ventas_ecuador_transformado <- ventas_ecuador_enriquecido |>
  mutate(
    ventas_categoria = case_when(
      ventas < 150 ~ "Bajo",
      ventas >= 150 & ventas < 300 ~ "Medio",
      ventas >= 300 ~ "Alto"
    )
  )

glimpse(ventas_ecuador_transformado)
Rows: 2,340
Columns: 14
$ fecha                <date> 2019-01-01, 2019-01-02, 2019-01-03, 2019-01-04, …
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 19, 46, 6…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 32, 42, 1…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, 2, 5, 4, 1…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…
$ region               <chr> "Sierra", "Costa", "Costa", "Sierra", "Costa", "C…
$ edad_grupo           <fct> Joven, Joven, Adulto, Adulto, Adulto joven, Adult…
$ categoria_producto   <chr> "Alimentos básicos", "Alimentos básicos", "Produc…
$ edad_rango           <fct> "[20,30)", "[20,30)", "[40,50)", "[50,60)", "[30,…
$ ventas_categoria     <chr> "Alto", "Bajo", "Alto", "Medio", "Alto", "Medio",…

Numerización de atributos categóricos

Convertiremos algunas variables categóricas en numéricas:

# Convertiremos algunas variables categóricas en numéricas:
ventas_ecuador_transformado <- ventas_ecuador_transformado |>
  mutate(
    ciudad_numerica = as.numeric(factor(ciudad)), 
    promocion_numerica = as.numeric(promocion)
  )

# Verificamos el resultado
glimpse(ventas_ecuador_transformado)
Rows: 2,340
Columns: 16
$ fecha                <date> 2019-01-01, 2019-01-02, 2019-01-03, 2019-01-04, …
$ ciudad               <chr> "Quito", "Guayaquil", "Guayaquil", "Quito", "Mant…
$ producto             <chr> "Leche", "Atún", "Verduras", "Carne", "Atún", "At…
$ ventas               <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, 235.28, 11…
$ unidades_vendidas    <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37, 19, 46, 6…
$ edad_cliente         <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36, 32, 42, 1…
$ promocion            <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, T…
$ satisfaccion_cliente <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, 2, 5, 4, 1…
$ tasa_devolucion      <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.07, 0.01, 0…
$ region               <chr> "Sierra", "Costa", "Costa", "Sierra", "Costa", "C…
$ edad_grupo           <fct> Joven, Joven, Adulto, Adulto, Adulto joven, Adult…
$ categoria_producto   <chr> "Alimentos básicos", "Alimentos básicos", "Produc…
$ edad_rango           <fct> "[20,30)", "[20,30)", "[40,50)", "[50,60)", "[30,…
$ ventas_categoria     <chr> "Alto", "Bajo", "Alto", "Medio", "Alto", "Medio",…
$ ciudad_numerica      <dbl> 5, 2, 2, 5, 3, 3, 4, 3, 1, 1, 3, 2, 2, 4, 2, 2, 2…
$ promocion_numerica   <dbl> 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0…

Normalización de atributos numéricos

### Normalización de atributos numéricos
ventas_ecuador_transformado <- ventas_ecuador_transformado |>
  mutate(
    ventas_normalizadas = as.vector(scale(ventas)),
    unidades_vendidas_normalizadas = as.vector(scale(unidades_vendidas))
  )

# Verificamos el resultado
glimpse(ventas_ecuador_transformado)
Rows: 2,340
Columns: 18
$ fecha                          <date> 2019-01-01, 2019-01-02, 2019-01-03, 20…
$ ciudad                         <chr> "Quito", "Guayaquil", "Guayaquil", "Qui…
$ producto                       <chr> "Leche", "Atún", "Verduras", "Carne", "…
$ ventas                         <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, …
$ unidades_vendidas              <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37,…
$ edad_cliente                   <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36,…
$ promocion                      <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE…
$ satisfaccion_cliente           <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, …
$ tasa_devolucion                <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.0…
$ region                         <chr> "Sierra", "Costa", "Costa", "Sierra", "…
$ edad_grupo                     <fct> Joven, Joven, Adulto, Adulto, Adulto jo…
$ categoria_producto             <chr> "Alimentos básicos", "Alimentos básicos…
$ edad_rango                     <fct> "[20,30)", "[20,30)", "[40,50)", "[50,6…
$ ventas_categoria               <chr> "Alto", "Bajo", "Alto", "Medio", "Alto"…
$ ciudad_numerica                <dbl> 5, 2, 2, 5, 3, 3, 4, 3, 1, 1, 3, 2, 2, …
$ promocion_numerica             <dbl> 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, …
$ ventas_normalizadas            <dbl> 0.97964482, -1.55036743, 1.09540519, -0…
$ unidades_vendidas_normalizadas <dbl> 1.61210680, 0.36613109, 0.57379370, -0.…

Reducción de dimensionalidad

Para este ejemplo, no realizaremos reducción de dimensionalidad, pero en un conjunto de datos más grande, podríamos utilizar técnicas como PCA (Análisis de Componentes Principales).

Aumento de dimensionalidad

Crearemos algunas características nuevas. Esto es crear mes, dia, si es fin de semana y ventas por unidad a partir de variables disponibles en el conjunto de datos.

ventas_ecuador_transformado <- ventas_ecuador_transformado |>
  mutate(
    mes = month(fecha),
    dia_semana = wday(fecha),
    es_fin_semana = ifelse(dia_semana %in% c(1, 7), 1, 0),
    ventas_por_unidad = ventas / unidades_vendidas
  )
glimpse(ventas_ecuador_transformado)
Rows: 2,340
Columns: 22
$ fecha                          <date> 2019-01-01, 2019-01-02, 2019-01-03, 20…
$ ciudad                         <chr> "Quito", "Guayaquil", "Guayaquil", "Qui…
$ producto                       <chr> "Leche", "Atún", "Verduras", "Carne", "…
$ ventas                         <dbl> 399.26, 75.36, 414.08, 250.49, 313.35, …
$ unidades_vendidas              <int> 49, 31, 34, 19, 31, 37, 42, 20, 42, 37,…
$ edad_cliente                   <int> 24, 24, 45, 53, 34, 39, 18, 45, 59, 36,…
$ promocion                      <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE…
$ satisfaccion_cliente           <int> 2, 4, 5, 4, 3, 4, 4, 1, 2, 3, 5, 1, 4, …
$ tasa_devolucion                <dbl> 0.08, 0.09, 0.02, 0.04, 0.02, 0.03, 0.0…
$ region                         <chr> "Sierra", "Costa", "Costa", "Sierra", "…
$ edad_grupo                     <fct> Joven, Joven, Adulto, Adulto, Adulto jo…
$ categoria_producto             <chr> "Alimentos básicos", "Alimentos básicos…
$ edad_rango                     <fct> "[20,30)", "[20,30)", "[40,50)", "[50,6…
$ ventas_categoria               <chr> "Alto", "Bajo", "Alto", "Medio", "Alto"…
$ ciudad_numerica                <dbl> 5, 2, 2, 5, 3, 3, 4, 3, 1, 1, 3, 2, 2, …
$ promocion_numerica             <dbl> 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, …
$ ventas_normalizadas            <dbl> 0.97964482, -1.55036743, 1.09540519, -0…
$ unidades_vendidas_normalizadas <dbl> 1.61210680, 0.36613109, 0.57379370, -0.…
$ mes                            <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ dia_semana                     <dbl> 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 1, …
$ es_fin_semana                  <dbl> 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, …
$ ventas_por_unidad              <dbl> 8.148163, 2.430968, 12.178824, 13.18368…

Actividad calificada

Contexto

En 2026, las plataformas de streaming de música y podcasts (como Spotify, Apple Music y Amazon Music) generan enormes volúmenes de datos de comportamiento de usuario. La capacidad de analizar y preparar correctamente estos datos es una habilidad clave para cualquier científico de datos. En esta actividad trabajarás con un dataset real de sesiones de una plataforma de streaming ecuatoriana llamada SonidEC, que busca entender los hábitos de consumo de sus usuarios para mejorar su sistema de recomendaciones y optimizar su catálogo.

Dataset

El archivo streaming_sonidec.csv es proporcionado por el docente y debe ser colocado en la carpeta Datos/ del proyecto. Contiene registros de sesiones de escucha de usuarios y tiene las siguientes variables:

Variable Tipo Descripción
usuario_id Entero ID único del usuario
fecha_sesion Fecha Fecha de la sesión (2023–2026)
genero_musical Categórica Reggaeton, Pop, Rock, Electrónica, Salsa, Indie
dispositivo Categórica Móvil, Computador, Tablet, SmartTV
duracion_min Numérico Duración de la sesión en minutos
canciones_escuchadas Entero Número de canciones en la sesión
skip_rate Numérico Proporción de canciones omitidas (0–1)
plan Categórica Gratuito, Premium, Familiar
provincia Categórica Pichincha, Guayas, Manabí, Azuay, Loja
rating_sesion Entero Calificación de la sesión del 1 al 5

⚠️ Algunas variables contienen valores faltantes (NA) y errores intencionales que deberán ser tratados durante la limpieza.

Instrucciones

1. Configuración del proyecto

Crear un nuevo proyecto en RStudio con el nombre MD_Streaming. Dentro del proyecto, organizar las carpetas de la siguiente manera:

  • Scripts/ — para almacenar los scripts de código R.
  • Datos/ — para almacenar el dataset de la actividad.
  • Resultados/ — para guardar los gráficos y tablas exportadas.

2. Pipeline de preparación de datos

Dentro del directorio Scripts/, crear un script R llamado pipeline_streaming.R. En este script codificar las siguientes etapas:

Etapa 1 — Selección y carga

  • Cargar el dataset con read.csv().
  • Mostrar una vista previa con glimpse() y summary().
  • Documentar cuántas filas y columnas tiene el dataset.

Etapa 2 — Limpieza

  • Detectar y eliminar filas duplicadas. Reportar cuántas se encontraron.
  • Tratar los valores faltantes (NA) en duracion_min, skip_rate y rating_sesion:
    • Imputar duracion_min con la mediana.
    • Imputar skip_rate con la media.
    • Imputar rating_sesion con la moda (el valor más frecuente).
  • Verificar que no queden fechas fuera del rango 2023–2026 y eliminarlas si existen.
  • Estandarizar el formato de la variable provincia usando str_to_title().

Etapa 3 — Enriquecimiento

  • Crear la variable zona a partir de provincia:
    • Sierra: Pichincha, Azuay, Loja
    • Costa: Guayas, Manabí
  • Crear la variable segmento_usuario a partir de duracion_min:
    • “Oyente casual”: menos de 20 minutos
    • “Oyente regular”: entre 20 y 60 minutos
    • “Oyente intensivo”: más de 60 minutos
  • Extraer de fecha_sesion las variables: anio, mes, dia_semana y es_fin_semana.

Etapa 4 — EDA (Análisis Exploratorio)

Realizar el análisis exploratorio de acuerdo con el tipo de variable. Para cada una de las siguientes variables generar al menos un gráfico apropiado y un párrafo de conclusión:

  • genero_musical (Cualitativa): frecuencia y ventas/sesiones por género.
  • dispositivo (Cualitativa): distribución de uso por dispositivo.
  • duracion_min (Cuantitativa): histograma y boxplot; detectar outliers.
  • skip_rate (Cuantitativa): histograma; interpretar qué significa un skip_rate alto.
  • plan (Cualitativa): comparar duración de sesión según tipo de plan.
  • segmento_usuario (Cualitativa derivada): distribución de segmentos.
  • es_fin_semana (Binaria): ¿las sesiones son más largas los fines de semana?

Adicionalmente, responder con código y comentarios las siguientes preguntas de negocio:

  • ¿Qué género musical concentra la mayor cantidad de minutos escuchados?
  • ¿Los usuarios Premium tienen un skip_rate menor que los usuarios Gratuitos?
  • ¿Qué provincia registra más sesiones y cuál tiene mayor duración promedio?
  • ¿Qué mes del año tiene el mayor número de sesiones?
  • ¿Existe relación entre el número de canciones escuchadas y la duración de la sesión?

Etapa 5 — Transformación

  • Discretizar skip_rate en tres categorías: "Bajo" (< 0.3), "Medio" (0.3–0.6), "Alto" (> 0.6).
  • Codificar numéricamente plan y dispositivo usando as.numeric(factor(...)).
  • Normalizar duracion_min y canciones_escuchadas con scale().
  • Crear la variable eficiencia_sesion = canciones_escuchadas / duracion_min.

3. Documentación

  • Agregar comentarios en cada sección del script explicando qué se hace y por qué.
  • Incluir al inicio del script un encabezado con: nombre del curso, práctica, integrantes del grupo, fecha y descripción general del script.

Entrega

Comprimir el proyecto completo en formato .zip con el nombre MD_Practica_2_GrupoX, donde X es el número de grupo. Subir en el plazo indicado en el aula virtual.

Criterios de Evaluación

Criterio Porcentaje
Estructura y organización del proyecto 10%
Limpieza y enriquecimiento de datos 25%
EDA: gráficos apropiados y conclusiones 40%
Transformación de variables 15%
Documentación y comentarios en el código 10%