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.
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.
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.
# 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
# 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 |
# 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.
# 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
# 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"
# 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
# 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
# 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.
# 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 |
# 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 |
# 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 |
# 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 |
# 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 |
# 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 |
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.
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.
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).
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.
# 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.
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.
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 |
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.
A partir del análisis desarrollado se identificaron los siguientes hallazgos:
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.
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.
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.
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.
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.
Análisis elaborado como trabajo final del curso Fundamentos de Programación — 2026. Herramientas: R + RStudio + R Markdown.