1. Introducción

Descripción del problema

El presente análisis tiene como propósito estudiar el comportamiento comercial de una tienda deportiva a lo largo del año 2023, a partir de su base de datos de pedidos. Se busca entender qué productos generan mayor ingreso, qué tiendas concentran el mayor volumen de ventas y cómo evolucionan las ventas a lo largo del tiempo.

Contexto de la base de datos

La base de datos contiene 4.761 registros de pedidos realizados entre el 1 de enero y el 31 de diciembre de 2023, distribuidos en 8 tiendas y 8 productos diferentes. Cada registro incluye un identificador único de pedido, la fecha, la tienda, el producto, la cantidad vendida y el precio unitario.

Objetivo del análisis

Identificar patrones de comportamiento en las ventas de la tienda deportiva que permitan orientar decisiones estratégicas sobre inventario, fuerza de ventas y política comercial.

Preguntas e hipótesis de interés

  • H1: Existe estacionalidad en las ventas; ciertos meses concentran mayor actividad comercial.
  • H2: No todas las tiendas tienen el mismo rendimiento; hay una distribución desigual del ingreso.
  • H3: El ingreso total no depende únicamente del volumen de unidades, sino también del mix de productos.
  • H4: Algunos productos tienen alta rotación pero bajo precio unitario, y viceversa.

2. Carga y comprensión de los datos

PASO 1 — Instalar y cargar librerías

# Carga de librerías necesarias para el análisis
library(readxl)      # Lectura de archivos Excel
library(dplyr)       # Manipulación de datos (filtrado, agrupación, etc.)
library(tidyverse)   # Ecosistema de análisis de datos (incluye ggplot2, tidyr, etc.)
library(ggplot2)     # Visualización de datos
library(lubridate)   # Manejo de fechas
library(janitor)     # Limpieza de nombres de variables
library(plotly)      # Gráficos interactivos
library(scales)      # Formato de ejes en gráficos (moneda, porcentajes, etc.)
library(knitr)       # Tablas en el reporte

PASO 2 — Lectura del archivo Excel

# Se lee el archivo Excel desde la ruta local donde se encuentra guardado.
# IMPORTANTE: modifica la ruta según donde tengas guardado el archivo.
ventas <- read_excel("C:/Users/Santiago 211/Desktop/trabajoprogramacion/bd_ventas_ESP.xlsx")

# Vista rápida de las primeras filas
head(ventas, 5)
ID Fecha_ Pedido Cod_Tienda id_producto Qtde Precio_Unitario
AX144 2023-01-15 A1500111 1007 35 630.99
AX4456 2023-07-27 A5200100 1007 33 630.99
AX2320 2023-08-03 A1500108 1005 132 999.00
AX4314 2023-07-04 A1500107 1005 72 999.00
AX151 2023-01-15 A1500111 1007 44 630.99

PASO 3 — Inspección inicial

# Dimensiones: número de filas y columnas
cat("Dimensiones de la base:", nrow(ventas), "filas x", ncol(ventas), "columnas\n\n")
## Dimensiones de la base: 4761 filas x 6 columnas
# Nombres de las variables
cat("Variables disponibles:\n")
## Variables disponibles:
print(names(ventas))
## [1] "ID"              "Fecha_ Pedido"   "Cod_Tienda"      "id_producto"    
## [5] "Qtde"            "Precio_Unitario"
# Estructura completa: tipo de cada variable
str(ventas)
## tibble [4,761 × 6] (S3: tbl_df/tbl/data.frame)
##  $ ID             : chr [1:4761] "AX144" "AX4456" "AX2320" "AX4314" ...
##  $ Fecha_ Pedido  : POSIXct[1:4761], format: "2023-01-15" "2023-07-27" ...
##  $ Cod_Tienda     : chr [1:4761] "A1500111" "A5200100" "A1500108" "A1500107" ...
##  $ id_producto    : num [1:4761] 1007 1007 1005 1005 1007 ...
##  $ Qtde           : num [1:4761] 35 33 132 72 44 46 64 26 27 45 ...
##  $ Precio_Unitario: num [1:4761] 631 631 999 999 631 ...
# Resumen estadístico de todas las variables
summary(ventas)
##       ID            Fecha_ Pedido                  Cod_Tienda       
##  Length:4761        Min.   :2023-01-01 00:00:00   Length:4761       
##  Class :character   1st Qu.:2023-03-19 00:00:00   Class :character  
##  Mode  :character   Median :2023-06-04 00:00:00   Mode  :character  
##                     Mean   :2023-06-16 03:31:43                     
##                     3rd Qu.:2023-09-13 00:00:00                     
##                     Max.   :2023-12-31 00:00:00                     
##   id_producto        Qtde       Precio_Unitario
##  Min.   :1001   Min.   :  1.0   Min.   :119.3  
##  1st Qu.:1003   1st Qu.: 21.0   1st Qu.:159.0  
##  Median :1005   Median : 33.0   Median :210.0  
##  Mean   :1005   Mean   : 40.8   Mean   :386.2  
##  3rd Qu.:1007   3rd Qu.: 47.0   3rd Qu.:631.0  
##  Max.   :1008   Max.   :416.0   Max.   :999.0

Interpretación — Comprensión inicial:

La base cuenta con 4.761 registros y 6 variables. Las variables son:

Variable Tipo Descripción
ID Texto Identificador único del pedido
Fecha_ Pedido Fecha Fecha en que se realizó el pedido
Cod_Tienda Texto Código identificador de la tienda
id_producto Numérico Código del producto vendido
Qtde Entero Cantidad de unidades vendidas
Precio_Unitario Decimal Precio por unidad del producto

Se observa que el período de análisis cubre todo el año 2023 (enero a diciembre), con 8 tiendas y 8 productos distintos.


3. Limpieza y preprocesamiento de datos

PASO 4 — Detección de valores nulos

# Conteo de valores nulos por variable
cat("Valores nulos por variable:\n")
## Valores nulos por variable:
print(colSums(is.na(ventas)))
##              ID   Fecha_ Pedido      Cod_Tienda     id_producto            Qtde 
##               0               0               0               0               0 
## Precio_Unitario 
##               0

PASO 5 — Limpieza de nombres de columnas

# La función clean_names() del paquete janitor estandariza los nombres:
# convierte a minúsculas, elimina espacios y caracteres especiales.
ventas <- ventas %>% clean_names()

cat("Nombres de columnas después de limpiar:\n")
## Nombres de columnas después de limpiar:
print(names(ventas))
## [1] "id"              "fecha_pedido"    "cod_tienda"      "id_producto"    
## [5] "qtde"            "precio_unitario"

PASO 6 — Corrección de tipos de datos y eliminación de duplicados

# Verificamos que la fecha sea tipo Date y el producto sea tratado como factor
ventas <- ventas %>%
  mutate(
    fecha_pedido = as.Date(fecha_pedido),         # Asegurar formato fecha
    id_producto  = as.factor(id_producto),        # Producto como factor (categoría)
    cod_tienda   = as.factor(cod_tienda)          # Tienda como factor (categoría)
  )

# Eliminación de registros duplicados (si los hubiera)
n_antes <- nrow(ventas)
ventas <- ventas %>% distinct()
n_despues <- nrow(ventas)

cat("Registros antes de eliminar duplicados:", n_antes, "\n")
## Registros antes de eliminar duplicados: 4761
cat("Registros después:", n_despues, "\n")
## Registros después: 4761
cat("Duplicados eliminados:", n_antes - n_despues, "\n")
## Duplicados eliminados: 0

PASO 7 — Detección de datos inconsistentes

# Verificamos que no existan cantidades o precios negativos o en cero
cat("Registros con Qtde <= 0:", sum(ventas$qtde <= 0), "\n")
## Registros con Qtde <= 0: 0
cat("Registros con Precio_Unitario <= 0:", sum(ventas$precio_unitario <= 0), "\n")
## Registros con Precio_Unitario <= 0: 0
# Resumen de valores únicos por variable categórica
cat("\nTiendas únicas:", nlevels(ventas$cod_tienda), "\n")
## 
## Tiendas únicas: 8
cat("Productos únicos:", nlevels(ventas$id_producto), "\n")
## Productos únicos: 8

PASO 8 — Creación de nuevas variables

# Se crean variables derivadas útiles para el análisis:
# - ingreso_total: facturación de cada pedido (cantidad x precio)
# - mes: mes del año del pedido (para análisis estacional)
# - trimestre: trimestre del año del pedido
# - dia_semana: día de la semana del pedido

ventas <- ventas %>%
  mutate(
    ingreso_total = qtde * precio_unitario,
    mes           = month(fecha_pedido, label = TRUE, abbr = FALSE),
    trimestre     = quarter(fecha_pedido, with_year = FALSE),
    dia_semana    = wday(fecha_pedido, label = TRUE, abbr = FALSE),
    trimestre_lbl = paste0("T", trimestre)
  )

# Vista de las nuevas variables
head(ventas %>% select(id, fecha_pedido, qtde, precio_unitario, ingreso_total, mes, trimestre_lbl), 5)
id fecha_pedido qtde precio_unitario ingreso_total mes trimestre_lbl
AX144 2023-01-15 35 630.99 22084.65 enero T1
AX4456 2023-07-27 33 630.99 20822.67 julio T3
AX2320 2023-08-03 132 999.00 131868.00 agosto T3
AX4314 2023-07-04 72 999.00 71928.00 julio T3
AX151 2023-01-15 44 630.99 27763.56 enero T1

Interpretación — Limpieza y preprocesamiento:

La base de datos se encuentra en excelente estado: no presenta valores nulos, no tiene registros duplicados ni valores negativos en las variables numéricas clave. Se realizaron los siguientes ajustes: estandarización de nombres de columnas, conversión de variables categóricas a tipo factor, conversión explícita de la fecha, y creación de cuatro nuevas variables derivadas (ingreso_total, mes, trimestre, dia_semana) que serán fundamentales para el análisis.


4. Manipulación y análisis de datos

PASO 9 — Estadísticas descriptivas generales

# Resumen de las variables numéricas clave
ventas %>%
  summarise(
    total_pedidos    = n(),
    total_unidades   = sum(qtde),
    ingreso_total    = sum(ingreso_total),
    ticket_promedio  = mean(ingreso_total),
    precio_prom      = mean(precio_unitario),
    unidades_prom    = mean(qtde),
    precio_min       = min(precio_unitario),
    precio_max       = max(precio_unitario)
  )
total_pedidos total_unidades ingreso_total ticket_promedio precio_prom unidades_prom precio_min precio_max
4761 194271 72861270 72861270 386.1697 40.80466 119.3 999

PASO 10 — Análisis por tienda

# Agrupación y agregación por tienda
# Se ordena de mayor a menor ingreso total
resumen_tienda <- ventas %>%
  group_by(cod_tienda) %>%
  summarise(
    n_pedidos       = n(),
    total_unidades  = sum(qtde),
    ingreso_total   = sum(ingreso_total),
    ticket_promedio = mean(ingreso_total),
    .groups = "drop"
  ) %>%
  arrange(desc(ingreso_total)) %>%
  mutate(
    participacion_pct = round(ingreso_total / sum(ingreso_total) * 100, 1)
  )

resumen_tienda
cod_tienda n_pedidos total_unidades ingreso_total ticket_promedio participacion_pct
A1500111 1008 47528 16149706.0 16149706.0 22.2
A1500107 489 41142 15990616.7 15990616.7 21.9
A1500108 721 37195 14063358.4 14063358.4 19.3
A3100104 745 25447 10060369.9 10060369.9 13.8
A1500109 1136 20563 8225702.7 8225702.7 11.3
A5200100 482 16302 6249207.2 6249207.2 8.6
A5200050 158 5290 1837146.6 1837146.6 2.5
A1500110 22 804 285162.3 285162.3 0.4

PASO 11 — Análisis por producto

# Agrupación por producto: ingresos, unidades y precio promedio
resumen_producto <- ventas %>%
  group_by(id_producto) %>%
  summarise(
    n_pedidos       = n(),
    total_unidades  = sum(qtde),
    precio_prom     = mean(precio_unitario),
    ingreso_total   = sum(ingreso_total),
    .groups = "drop"
  ) %>%
  arrange(desc(ingreso_total)) %>%
  mutate(
    participacion_pct = round(ingreso_total / sum(ingreso_total) * 100, 1)
  )

resumen_producto
id_producto n_pedidos total_unidades precio_prom ingreso_total participacion_pct
1005 574 22334 999.00 22311666 30.6
1007 637 23752 630.99 14987274 20.6
1003 587 24189 499.19 12074907 16.6
1002 563 22483 320.19 7198832 9.9
1004 616 25351 210.00 5323710 7.3
1006 602 24158 159.00 3841122 5.3
1008 611 29944 119.30 3572319 4.9
1001 571 22060 160.99 3551439 4.9

PASO 12 — Análisis de estacionalidad mensual

# Agrupación mensual para detectar estacionalidad
ventas_mes <- ventas %>%
  group_by(mes) %>%
  summarise(
    n_pedidos      = n(),
    total_unidades = sum(qtde),
    ingreso_total  = sum(ingreso_total),
    .groups = "drop"
  )

ventas_mes
mes n_pedidos total_unidades ingreso_total
enero 466 18276 7052940
febrero 424 16468 6339704
marzo 478 19616 7877338
abril 470 19296 7166507
mayo 484 20198 7240702
junio 464 18009 5949407
julio 315 13734 5045747
agosto 329 14724 5740660
septiembre 333 14718 5259929
octubre 364 14306 5453610
noviembre 313 11768 4488934
diciembre 321 13158 5245792

PASO 13 — Análisis por trimestre

# Resumen por trimestre
ventas %>%
  group_by(trimestre_lbl) %>%
  summarise(
    n_pedidos      = n(),
    total_unidades = sum(qtde),
    ingreso_total  = sum(ingreso_total),
    .groups = "drop"
  )
trimestre_lbl n_pedidos total_unidades ingreso_total
T1 1368 54360 21269981
T2 1418 57503 20356615
T3 977 43176 16046336
T4 998 39232 15188337

PASO 14 — Cruce entre tienda y producto (Top combinaciones)

# Cruce entre tienda y producto para identificar combinaciones más rentables
ventas %>%
  group_by(cod_tienda, id_producto) %>%
  summarise(
    ingreso_total = sum(ingreso_total),
    .groups = "drop"
  ) %>%
  arrange(desc(ingreso_total)) %>%
  head(10)
cod_tienda id_producto ingreso_total
A1500107 1005 5394600
A1500108 1005 4482513
A1500111 1005 4471524
A1500111 1007 3359391
A3100104 1005 2982015
A1500107 1003 2870343
A1500107 1007 2839455
A1500108 1007 2645741
A1500109 1005 2539458
A1500108 1003 2397110

5. Visualización de datos

Figura 1 — Ingreso total por tienda

g1 <- ggplot(resumen_tienda, aes(x = reorder(cod_tienda, ingreso_total),
                                  y = ingreso_total,
                                  fill = cod_tienda)) +
  geom_col(show.legend = FALSE, width = 0.7) +
  geom_text(aes(label = paste0(participacion_pct, "%")),
            hjust = -0.1, size = 3.5, fontface = "bold") +
  coord_flip() +
  scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ",")) +
  labs(
    title    = "Figura 1. Ingreso total por tienda — 2023",
    subtitle = "Porcentajes indican participación sobre el total de ingresos",
    x        = "Tienda",
    y        = "Ingreso Total ($)",
    caption  = "Fuente: Base de datos ventas tienda deportiva"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", hjust = 0.5),
        plot.subtitle = element_text(hjust = 0.5, color = "gray50"))

ggplotly(g1)

Interpretación Figura 1: El gráfico revela una distribución desigual del ingreso entre tiendas. La tienda A1500111 lidera con la mayor participación en ingresos, seguida por A1500107 y A1500108. En contraste, las tiendas A1500110 y A5200050 tienen una contribución marginal al total, lo que podría indicar diferencias en tamaño de mercado, cobertura geográfica o eficiencia operativa. Estas brechas merecen una revisión estratégica.


Figura 2 — Ingreso total por producto

g2 <- ggplot(resumen_producto, aes(x = reorder(id_producto, ingreso_total),
                                    y = ingreso_total,
                                    fill = id_producto)) +
  geom_col(show.legend = FALSE, width = 0.7) +
  geom_text(aes(label = paste0(participacion_pct, "%")),
            hjust = -0.1, size = 3.5, fontface = "bold") +
  coord_flip() +
  scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ",")) +
  labs(
    title    = "Figura 2. Ingreso total por producto — 2023",
    subtitle = "Participación porcentual sobre el total de ingresos",
    x        = "Producto",
    y        = "Ingreso Total ($)",
    caption  = "Fuente: Base de datos ventas tienda deportiva"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", hjust = 0.5),
        plot.subtitle = element_text(hjust = 0.5, color = "gray50"))

ggplotly(g2)

Interpretación Figura 2: Los productos se distribuyen en dos grupos claramente diferenciados por precio unitario. Los productos 1007 y 1008 (precio ~$631 y ~$999) concentran una mayor proporción del ingreso total a pesar de tener menos pedidos que los productos más económicos. Esto confirma que el mix de productos tiene un impacto significativo en el ingreso, y que no basta con maximizar el volumen de unidades vendidas.


Figura 3 — Evolución mensual del ingreso

g3 <- ggplot(ventas_mes, aes(x = mes, y = ingreso_total, group = 1)) +
  geom_line(color = "#2196F3", linewidth = 1.2) +
  geom_point(color = "#2196F3", size = 3.5) +
  geom_text(aes(label = dollar(round(ingreso_total / 1000), suffix = "K")),
            vjust = -1.0, size = 3.2, color = "gray30") +
  scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ",")) +
  labs(
    title    = "Figura 3. Evolución mensual del ingreso total — 2023",
    subtitle = "Tendencia a lo largo del año",
    x        = "Mes",
    y        = "Ingreso Total ($)",
    caption  = "Fuente: Base de datos ventas tienda deportiva"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title    = element_text(face = "bold", hjust = 0.5),
    plot.subtitle = element_text(hjust = 0.5, color = "gray50"),
    axis.text.x   = element_text(angle = 30, hjust = 1)
  )

ggplotly(g3)

Interpretación Figura 3: La serie temporal muestra la presencia de estacionalidad en las ventas. Se observan picos de ingreso en determinados meses, lo que sugiere que factores como temporadas deportivas, eventos promocionales o ciclos de consumo influyen en la demanda. Los meses con caídas abruptas deben ser analizados con mayor detalle para identificar causas (falta de stock, menor demanda o estacionalidad natural del mercado).


Figura 4 — Distribución de unidades vendidas por pedido (Boxplot)

g4 <- ggplot(ventas, aes(x = id_producto, y = qtde, fill = id_producto)) +
  geom_boxplot(outlier.colour = "red", outlier.shape = 16,
               outlier.size = 2, show.legend = FALSE) +
  labs(
    title    = "Figura 4. Distribución de unidades por pedido según producto",
    subtitle = "Puntos rojos representan valores atípicos (outliers)",
    x        = "Producto",
    y        = "Unidades por pedido",
    caption  = "Fuente: Base de datos ventas tienda deportiva"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", hjust = 0.5),
        plot.subtitle = element_text(hjust = 0.5, color = "gray50"))

ggplotly(g4)

Interpretación Figura 4: El boxplot muestra la variabilidad en el tamaño de los pedidos para cada producto. Se observan pedidos con volúmenes excepcionalmente altos (outliers en rojo), los cuales podrían corresponder a compras institucionales o mayoristas. La mediana de unidades por pedido es similar entre productos, pero la varianza difiere, lo que sugiere que algunos productos son adquiridos en lotes más variables.


Figura 5 — Mapa de calor: Ingreso por tienda y producto

# Tabla de cruce tienda × producto para el heatmap
heatmap_data <- ventas %>%
  group_by(cod_tienda, id_producto) %>%
  summarise(ingreso_total = sum(ingreso_total), .groups = "drop")

g5 <- ggplot(heatmap_data, aes(x = id_producto, y = cod_tienda, fill = ingreso_total)) +
  geom_tile(color = "white", linewidth = 0.5) +
  geom_text(aes(label = dollar(round(ingreso_total / 1000), suffix = "K")),
            size = 3, color = "white", fontface = "bold") +
  scale_fill_gradient(low = "#AED6F1", high = "#1A5276",
                      labels = dollar_format(prefix = "$", big.mark = ",")) +
  labs(
    title    = "Figura 5. Ingreso total por tienda y producto (mapa de calor)",
    subtitle = "Valores expresados en miles de dólares",
    x        = "Producto",
    y        = "Tienda",
    fill     = "Ingreso ($)",
    caption  = "Fuente: Base de datos ventas tienda deportiva"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", hjust = 0.5),
        plot.subtitle = element_text(hjust = 0.5, color = "gray50"))

ggplotly(g5)

Interpretación Figura 5: El mapa de calor permite identificar de un vistazo qué combinaciones tienda-producto generan el mayor ingreso (celdas de color azul oscuro). Las tiendas líderes concentran los mayores ingresos en los productos de precio alto. Las celdas más claras evidencian tiendas que venden poco de ciertos productos, lo que puede orientar decisiones de surtido por tienda.


Figura 6 — Ingreso por trimestre

ventas_trim <- ventas %>%
  group_by(trimestre_lbl) %>%
  summarise(ingreso_total = sum(ingreso_total), .groups = "drop")

g6 <- ggplot(ventas_trim, aes(x = trimestre_lbl, y = ingreso_total, fill = trimestre_lbl)) +
  geom_col(show.legend = FALSE, width = 0.5) +
  geom_text(aes(label = dollar(round(ingreso_total), prefix = "$", big.mark = ",")),
            vjust = -0.5, fontface = "bold", size = 4) +
  scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ",")) +
  scale_fill_brewer(palette = "Blues") +
  labs(
    title    = "Figura 6. Ingreso total por trimestre — 2023",
    x        = "Trimestre",
    y        = "Ingreso Total ($)",
    caption  = "Fuente: Base de datos ventas tienda deportiva"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold", hjust = 0.5))

ggplotly(g6)

Interpretación Figura 6: El análisis trimestral complementa la visión mensual y permite identificar cuál período del año concentra mayor actividad. Las diferencias entre trimestres pueden orientar la planificación de campañas de marketing, compras de inventario y asignación de personal en cada tienda.


6. Uso de funciones propias

Función 1 — resumen_grupo()

Esta función permite calcular un resumen estadístico completo para cualquier variable de agrupación de la base de datos (por ejemplo, tienda, producto, mes).

# FUNCIÓN 1: resumen_grupo()
# Propósito: calcular métricas de ventas para cualquier variable de agrupación
# Parámetros:
#   - datos: dataframe con los datos de ventas
#   - grupo: nombre de la columna de agrupación (como texto entre comillas)
# Retorna: dataframe con n_pedidos, unidades, ingreso, ticket promedio y participación

resumen_grupo <- function(datos, grupo) {
  resultado <- datos %>%
    group_by(across(all_of(grupo))) %>%
    summarise(
      n_pedidos       = n(),
      total_unidades  = sum(qtde),
      ingreso_total   = round(sum(ingreso_total), 2),
      ticket_promedio = round(mean(ingreso_total), 2),
      precio_promedio = round(mean(precio_unitario), 2),
      .groups = "drop"
    ) %>%
    arrange(desc(ingreso_total)) %>%
    mutate(
      participacion_pct = round(ingreso_total / sum(ingreso_total) * 100, 1)
    )
  return(resultado)
}

# Ejemplo de uso: resumen por tienda
cat("--- Resumen por tienda ---\n")
## --- Resumen por tienda ---
resumen_grupo(ventas, "cod_tienda")
cod_tienda n_pedidos total_unidades ingreso_total ticket_promedio precio_promedio participacion_pct
A1500111 1008 47528 16149706.0 16149706.0 380.08 22.2
A1500107 489 41142 15990616.7 15990616.7 387.89 21.9
A1500108 721 37195 14063358.4 14063358.4 388.34 19.3
A3100104 745 25447 10060369.9 10060369.9 396.91 13.8
A1500109 1136 20563 8225702.7 8225702.7 387.15 11.3
A5200100 482 16302 6249207.2 6249207.2 386.19 8.6
A5200050 158 5290 1837146.6 1837146.6 356.77 2.5
A1500110 22 804 285162.3 285162.3 352.47 0.4
# Ejemplo de uso: resumen por mes
cat("--- Resumen por mes ---\n")
## --- Resumen por mes ---
resumen_grupo(ventas, "mes")
mes n_pedidos total_unidades ingreso_total ticket_promedio precio_promedio participacion_pct
marzo 478 19616 7877338 7877338 394.43 10.8
mayo 484 20198 7240702 7240702 377.39 9.9
abril 470 19296 7166507 7166507 385.69 9.8
enero 466 18276 7052940 7052940 391.01 9.7
febrero 424 16468 6339704 6339704 402.11 8.7
junio 464 18009 5949407 5949407 350.08 8.2
agosto 329 14724 5740660 5740660 394.93 7.9
octubre 364 14306 5453610 5453610 396.62 7.5
septiembre 333 14718 5259929 5259929 369.80 7.2
diciembre 321 13158 5245792 5245792 399.55 7.2
julio 315 13734 5045747 5045747 377.26 6.9
noviembre 313 11768 4488934 4488934 403.88 6.2

Función 2 — clasificar_pedido()

Esta función clasifica cada pedido como “Alto”, “Medio” o “Bajo” según su ingreso total, usando percentiles calculados sobre la propia base de datos.

# FUNCIÓN 2: clasificar_pedido()
# Propósito: clasificar el nivel de ingreso de cada pedido usando percentiles
# Parámetros:
#   - datos: dataframe con la variable ingreso_total
# Retorna: el mismo dataframe con una columna nueva "categoria_pedido"

clasificar_pedido <- function(datos) {
  q33 <- quantile(datos$ingreso_total, 0.33)
  q66 <- quantile(datos$ingreso_total, 0.66)

  datos <- datos %>%
    mutate(
      categoria_pedido = case_when(
        ingreso_total <= q33  ~ "Bajo",
        ingreso_total <= q66  ~ "Medio",
        TRUE                  ~ "Alto"
      )
    )
  return(datos)
}

# Aplicar la función a la base de datos
ventas <- clasificar_pedido(ventas)

# Verificar la distribución de categorías
cat("Distribución de categorías de pedido:\n")
## Distribución de categorías de pedido:
ventas %>%
  count(categoria_pedido) %>%
  mutate(pct = round(n / sum(n) * 100, 1))
categoria_pedido n pct
Alto 1615 33.9
Bajo 1572 33.0
Medio 1574 33.1
# Visualización del uso de la función 2: categorías por tienda
g7 <- ventas %>%
  count(cod_tienda, categoria_pedido) %>%
  ggplot(aes(x = cod_tienda, y = n, fill = categoria_pedido)) +
  geom_col(position = "fill") +
  scale_y_continuous(labels = percent_format()) +
  scale_fill_manual(values = c("Alto" = "#1A5276", "Medio" = "#5DADE2", "Bajo" = "#AED6F1")) +
  labs(
    title    = "Figura 7. Composición de pedidos por categoría según tienda",
    subtitle = "Proporción de pedidos Alto, Medio y Bajo por tienda",
    x        = "Tienda",
    y        = "Proporción",
    fill     = "Categoría",
    caption  = "Fuente: Base de datos ventas tienda deportiva"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title    = element_text(face = "bold", hjust = 0.5),
    plot.subtitle = element_text(hjust = 0.5, color = "gray50"),
    axis.text.x   = element_text(angle = 30, hjust = 1)
  )

ggplotly(g7)

Interpretación Figura 7: La composición de pedidos por categoría revela diferencias importantes entre tiendas. Las tiendas con mayor proporción de pedidos “Alto” generan más ingreso por transacción, independientemente del número total de pedidos. Esto puede estar relacionado con el tipo de cliente atendido (mayoristas vs. minoristas) o el mix de productos ofrecidos en cada punto.


7. Hallazgos y conclusiones

Hallazgos principales

A partir del análisis desarrollado se identificaron los siguientes hallazgos:

  1. Concentración del ingreso en pocas tiendas: Las tres tiendas con mayor ingreso (A1500111, A1500107, A1500108) concentran la mayor parte de las ventas totales de la red. Las tiendas A1500110 y A5200050 tienen una participación marginal.

  2. El mix de productos determina el ingreso: Los productos con mayor precio unitario (1007 ≈ $631, 1008 ≈ $999) generan una proporción desproporcionada del ingreso total en comparación con los productos de bajo precio, a pesar de tener volúmenes de pedido similares.

  3. Estacionalidad evidente: La evolución mensual muestra variaciones claras a lo largo del año, con meses de mayor concentración de ventas que pueden asociarse a temporadas específicas del mercado deportivo.

  4. Pedidos atípicos (outliers) de alto volumen: Se identificaron pedidos con cantidades muy superiores al promedio, lo que podría indicar la existencia de clientes institucionales o mayoristas cuyo comportamiento difiere del cliente minorista típico.

Interpretación de resultados

El desempeño de la tienda deportiva está impulsado principalmente por un subconjunto de tiendas y productos. La gestión del surtido (qué productos ofrece cada tienda) y la estrategia de precios son variables críticas que afectan directamente el ingreso total.

Conclusiones

  • La red de tiendas presenta una distribución heterogénea del ingreso que justifica una revisión de la estrategia comercial por punto de venta.
  • Los productos de alto precio unitario son los principales impulsores del ingreso y deben ser priorizados en la gestión de inventario.
  • La estacionalidad identificada puede aprovecharse para planificar campañas promocionales en los meses de menor actividad.

Limitaciones de los datos

  • La base no contiene información sobre el tipo de producto (nombre, categoría deportiva), lo que limita la interpretación cualitativa del análisis.
  • No se dispone de información sobre costos, por lo que no es posible calcular márgenes de rentabilidad.
  • No se conoce la ubicación geográfica de las tiendas, lo que impide un análisis espacial.
  • La base cubre un único año (2023), por lo que no es posible comparar tendencias interanuales.

Recomendaciones futuras

  • Enriquecer la base con el nombre y categoría de cada producto para profundizar el análisis de mix comercial.
  • Incorporar datos de costos para calcular rentabilidad por tienda y producto.
  • Implementar un modelo de segmentación de clientes (si se dispone de datos de clientes) para identificar patrones de compra.
  • Explorar técnicas de series de tiempo (ARIMA, Prophet) para pronosticar ventas futuras y planificar el inventario con mayor anticipación.

Análisis elaborado como trabajo final del curso Fundamentos de Programación — 2026. Herramientas: R + RStudio + R Markdown.