# Opciones knit y repos
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
options(repos = c(CRAN = "https://cloud.r-project.org/"))

# Reproducibilidad y estética
set.seed(123)
library(ggplot2)
theme_set(theme_minimal(base_size = 13))

library(tidyverse)
library(readxl)
library(lubridate)
library(mclust)

knitr::opts_chunk$set(
  echo = TRUE,
  warning = FALSE,
  message = FALSE
)

Punto 1. Carga y selección de variables

En este punto se carga la base de datos y se seleccionan las categorías de balones y bloques de construcción. Posteriormente, se calculan estadísticas descriptivas para el precio y la cantidad vendida.

# ========================================================================================
#=================== PUNTO 1 CARGA Y SELECCION VARIABLES =================================
# ========================================================================================
library(tidyverse)
library(readxl)

df <- read_excel("jugueteria_ventas_diarias_rtaborda20260626.xlsx") %>%
  filter(categoria %in% c("balones", "bloques de construcción"))

tabla_desc <- df %>%
  group_by(categoria) %>%
  summarise(
    n = n(),
    p_media = mean(precio),
    p_sd = sd(precio),
    p_mediana = median(precio),
    p_iqr = IQR(precio),
    q_media = mean(cantidad),
    q_sd = sd(cantidad),
    q_mediana = median(cantidad),
    q_iqr = IQR(cantidad)
  )

print(tabla_desc)
## # A tibble: 2 × 10
##   categoria        n p_media  p_sd p_mediana p_iqr q_media  q_sd q_mediana q_iqr
##   <chr>        <int>   <dbl> <dbl>     <dbl> <dbl>   <dbl> <dbl>     <dbl> <dbl>
## 1 balones       7304  25011. 1995.     25032 2698.    86.3 36.0         81    45
## 2 bloques de …  7304  90108. 7212.     90000 9842.    21.6  8.96        20    12

Punto 2. Histogramas y distribución de precios

Se analizan gráficamente las distribuciones de los precios y de las cantidades vendidas para las dos categorías seleccionadas.

# ========================================================================================
#=================== PUNTO 2 HISTOGRAMAS Y DISTRIBUCIÓN DE PRECIOS =======================
# ========================================================================================
library(patchwork)

promedios <- df %>%
  group_by(categoria) %>%
  summarise(
    p_media = mean(precio),
    q_media = mean(cantidad)
  )

g_precio <- ggplot(df, aes(x = precio, fill = categoria)) +
  geom_histogram(
    bins = 30,
    alpha = 0.7,
    position = "identity",
    color = "white"
  ) +
  geom_vline(
    data = promedios,
    aes(
      xintercept = p_media,
      color = categoria
    ),
    linetype = "dashed",
    linewidth = 0.9
  ) +
  labs(
    title = "Distribución de Precios",
    x = "Precio (COP)",
    y = "Frecuencia"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

g_cant <- ggplot(df, aes(x = cantidad, fill = categoria)) +
  geom_histogram(
    bins = 30,
    alpha = 0.7,
    position = "identity",
    color = "white"
  ) +
  geom_vline(
    data = promedios,
    aes(
      xintercept = q_media,
      color = categoria
    ),
    linetype = "dashed",
    linewidth = 0.9
  ) +
  labs(
    title = "Distribución de Cantidades",
    x = "Cantidad (Unidades)",
    y = "Frecuencia",
    fill = "Categoría",
    color = "Categoría"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom")

g_precio

g_cant

g_box_p <- ggplot(
  df,
  aes(
    x = categoria,
    y = precio,
    fill = categoria
  )
) +
  geom_boxplot(
    alpha = 0.7,
    outlier.size = 1.5,
    outlier.alpha = 0.3
  ) +
  stat_summary(
    fun = mean,
    geom = "point",
    shape = 18,
    size = 4.5,
    color = "darkred"
  ) +
  labs(
    title = "Dispersión de Precios",
    x = "",
    y = "Precio (COP)"
  ) +
  theme_minimal() +
  theme(
    legend.position = "none",
    plot.title = element_text(
      size = 16,
      face = "bold"
    ),
    axis.title.y = element_text(
      size = 14,
      face = "bold"
    ),
    axis.text.x = element_text(
      size = 13,
      face = "bold",
      color = "black"
    ),
    axis.text.y = element_text(
      size = 12,
      face = "bold",
      color = "black"
    )
  )

g_box_q <- ggplot(
  df,
  aes(
    x = categoria,
    y = cantidad,
    fill = categoria
  )
) +
  geom_boxplot(
    alpha = 0.7,
    outlier.size = 1.5,
    outlier.alpha = 0.3
  ) +
  stat_summary(
    fun = mean,
    geom = "point",
    shape = 18,
    size = 4.5,
    color = "darkred"
  ) +
  labs(
    title = "Dispersión de Cantidades",
    x = "",
    y = "Cantidad (Unidades)"
  ) +
  theme_minimal() +
  theme(
    legend.position = "none",
    plot.title = element_text(
      size = 16,
      face = "bold"
    ),
    axis.title.y = element_text(
      size = 14,
      face = "bold"
    ),
    axis.text.x = element_text(
      size = 13,
      face = "bold",
      color = "black"
    ),
    axis.text.y = element_text(
      size = 12,
      face = "bold",
      color = "black"
    )
  )

print(g_box_p)

print(g_box_q)

Punto 3. Dispersión entre precio y cantidad

Se analiza la relación entre el precio de los productos y la cantidad vendida, diferenciando las categorías.

# ========================================================================================
#=================== PUNTO 3 DISPERCION VS PRECIOS =======================
# ========================================================================================

g_scatter <- ggplot(
  df,
  aes(
    x = precio,
    y = cantidad,
    color = categoria
  )
) +
  geom_point(
    alpha = 0.25,
    size = 1.2
  ) +
  geom_smooth(
    method = "lm",
    se = FALSE,
    linetype = "solid",
    linewidth = 1
  ) +
  geom_vline(
    data = promedios,
    aes(
      xintercept = p_media,
      color = categoria
    ),
    linetype = "dashed"
  ) +
  geom_hline(
    data = promedios,
    aes(
      yintercept = q_media,
      color = categoria
    ),
    linetype = "dashed"
  ) +
  labs(
    title = "Relación Cantidad vs. Precio",
    x = "Precio (COP)",
    y = "Cantidad Demandada (Unidades)",
    color = "Categoría"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom")

g_scatter

Punto 4. Series de tiempo

Se representa la evolución temporal de las ventas mensuales entre 2010 y 2014 para cada categoría.

# ========================================================================================
#=================== PUNTO 4  SERIES DE TIEMPO =======================
# ========================================================================================
library(lubridate)

df_ts <- df %>%
  mutate(
    fecha = as.Date(fecha),
    periodo = floor_date(fecha, unit = "month")
  ) %>%
  group_by(periodo, categoria) %>%
  summarise(
    total_cantidad = sum(cantidad, na.rm = TRUE),
    .groups = "drop"
  )

g_ts <- ggplot(
  df_ts,
  aes(
    x = periodo,
    y = total_cantidad,
    color = categoria
  )
) +
  geom_line(linewidth = 0.9) +
  geom_point(size = 1.3) +
  scale_x_date(
    breaks = seq(
      as.Date("2010-01-01"),
      as.Date("2014-12-01"),
      by = "6 months"
    ),
    date_labels = "%b %Y"
  ) +
  labs(
    title = "Evolución Temporal de Ventas Mensuales (2010 - 2014)",
    x = "Fecha",
    y = "Unidades Totales Vendidas",
    color = "Categoría"
  ) +
  theme_minimal() +
  theme(
    legend.position = "bottom",
    axis.text.x = element_text(
      angle = 45,
      hjust = 1
    )
  )

g_ts

Punto 5. Clustering

Se realiza una segmentación de los almacenes utilizando la cantidad promedio vendida y los ingresos totales como indicadores de desempeño comercial.

# ========================================================================================
#=================== PUNTO 5 CLUSTERING    =======================
# ========================================================================================

library(factoextra)
library(ggrepel)

df_cluster_data <- df %>%
  group_by(id_almacen) %>%
  summarise(
    cant_prom = mean(cantidad),
    ingreso_total = sum(
      as.numeric(precio) *
      as.numeric(cantidad)
    ),
    .groups = "drop"
  ) %>%
  column_to_rownames("id_almacen")

df_scaled <- scale(df_cluster_data)


#Evaluación del número de clústeres
#Se utiliza el método del codo para evaluar el número óptimo de grupos.
g_elbow <- fviz_nbclust(
  df_scaled,
  kmeans,
  method = "wss",
  k.max = 3
) +
  labs(
    title = "Evaluación del Codo (Elbow Method)",
    x = "k",
    y = "WSS"
  ) +
  theme_minimal()

print(g_elbow)

Segmentación de almacenes

Se representan los almacenes según el clúster al que pertenecen, utilizando las variables estandarizadas de cantidad promedio e ingresos totales.

# Segmentación mediante K-means
set.seed(123)

km_res <- kmeans(
  df_scaled,
  centers = 3,
  nstart = 25
)



# Punto 5. Clustering

library(factoextra)
library(ggrepel)

# 1. Preparación y escalado de datos por almacén
df_cluster_data <- df %>%
  group_by(id_almacen) %>%
  summarise(
    cant_prom = mean(cantidad),
    ingreso_total = sum(
      as.numeric(precio) * as.numeric(cantidad)
    ),
    .groups = "drop"
  ) %>%
  column_to_rownames("id_almacen")

df_scaled <- scale(df_cluster_data)

# 2. Evaluación del número de clústeres
g_elbow <- fviz_nbclust(
  df_scaled,
  kmeans,
  method = "wss",
  k.max = 3
) +
  labs(
    title = "Evaluación del Codo (Elbow Method)",
    x = "k",
    y = "WSS"
  ) +
  theme_minimal()

print(g_elbow)

# 3. Creación del modelo K-means
set.seed(123)

km_res <- kmeans(
  df_scaled,
  centers = 3,
  nstart = 25
)

# 4. Preparación de los datos para la gráfica
df_plot <- as.data.frame(df_scaled) %>%
  mutate(
    almacen = rownames(.),
    cluster = as.factor(km_res$cluster)
  )

# 5. Gráfica de segmentación
g_cluster_clean <- ggplot(
  df_plot,
  aes(
    x = cant_prom,
    y = ingreso_total,
    color = cluster
  )
) +
  geom_point(
    aes(shape = cluster),
    size = 4
  ) +
  geom_text_repel(
    aes(label = almacen),
    size = 4.5,
    fontface = "bold",
    box.padding = 0.5,
    point.padding = 0.3
  ) +
  labs(
    title = "Segmentación de Almacenes por Desempeño Comercial",
    x = "Volumen Promedio Diario (Estandarizado)",
    y = "Ingresos Totales (Estandarizados)",
    color = "Clúster",
    shape = "Clúster"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom")

print(g_cluster_clean)

Comparativo de ventas por almacén y categoría

Se compara la venta promedio diaria de cada almacén según la categoría de producto.

g_store_comp <- df %>%
  group_by(id_almacen, categoria) %>%
  summarise(
    cant_prom = mean(cantidad),
    .groups = "drop"
  ) %>%
  ggplot(
    aes(
      x = id_almacen,
      y = cant_prom,
      fill = categoria
    )
  ) +
  geom_col(
    position = "dodge",
    alpha = 0.85,
    width = 0.6
  ) +
  geom_text(
    aes(label = round(cant_prom, 1)),
    position = position_dodge(0.6),
    vjust = -0.5,
    size = 3.5,
    fontface = "bold"
  ) +
  labs(
    title = "Comparativo de Rotación Diaria por Almacén y Categoría",
    x = "Almacén",
    y = "Venta Promedio Diaria (Unidades)",
    fill = "Categoría"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom")

print(g_store_comp)