Introducción

En este ejercicio se realiza la simulación de un Centro de Distribución Central (CDC) con el fin de evaluar su desempeño operativo bajo un horizonte de trabajo de 8 horas. A partir de parámetros definidos como la capacidad de almacenamiento, tiempos de procesamiento en recepción, almacenamiento, alistamiento y despacho, así como la frecuencia de llegada de pedidos, se construye un modelo en R con la librería simmer que permite representar el flujo de pedidos en el sistema.

El objetivo principal es calcular e interpretar una serie de Indicadores Clave de Desempeño (KPIs) que reflejan la eficiencia y nivel de servicio del CDC, entre los cuales se encuentran:

Con estos resultados se busca analizar la capacidad de respuesta del CDC, identificar posibles cuellos de botella y evaluar si los recursos disponibles permiten satisfacer la demanda de manera eficiente.

Código de la simulación

Cargar librerías necesarias

# ---------- LIBRERÍAS ----------
library(simmer) #librería de simulación de eventos discretos
library(simmer.plot) #permite visualizar fácilmente resultados de simulación
library(ggplot2) #libreria para graficar
library(dplyr)   # libreria para el manejo, transformación y análisis de datos.

Aquí se definen las condiciones del sistema (turno de trabajo, frecuencia de pedidos, tiempos deterministas de procesos y capacidad del almacén).

# ---------- PARÁMETROS ----------
sim_time <- 8 * 60          # 8 horas en minutos
order_interval <- 5         # 1 pedido cada 5 minutos
warehouse_capacity <- 100   # capacidad máxima
receiving_time <- 2         # tiempo de recepción
storage_time   <- 1         # tiempo de almacenamiento
picking_time   <- 3         # tiempo de picking
shipping_time  <- 2         # tiempo de envío
initial_inventory <- 50     # inventario inicial (supuesto)

# Variables globales (inventario, backorders, etc.)
inventory <- initial_inventory # nivel de inventario disponible en cada momento.
backorders <- 0           # pedidos pendientes por falta de stock
fulfilled_orders <- 0     # pedidos atendidos con éxito
total_orders <- 0         # pedidos generados
inventory_movements <- 0  # para turnover

Se crea el entorno de simulación con 3 recursos:

# ---------- AMBIENTE ----------
env <- simmer("CDC#1")

# Recursos
env %>%
  add_resource("ReceivingDock", capacity = 1) %>%
  add_resource("ShippingDock", capacity = 1) %>%
  add_resource("WarehouseSpace", capacity = warehouse_capacity)
## simmer environment: CDC#1 | now: 0 | next: 
## { Monitor: in memory }
## { Resource: ReceivingDock | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Resource: ShippingDock | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Resource: WarehouseSpace | monitored: TRUE | server status: 0(100) | queue status: 0(Inf) }

Aquí se modela el camino que sigue un pedido dentro del CDC:

  1. Recepción → el pedido llega y ocupa el muelle de recepción (2 min).

  2. Almacenamiento → se incrementa el inventario en 1 unidad y se registra movimiento (1 min).

  3. Picking:

-    Si hay inventario disponible → se descuenta 1 unidad, se atiende el pedido, pasa al muelle de envío (3 + 2 min).

-    Si **NO** hay inventario → se registra como backorder (pedido insatisfecho).
# ---------- TRAJECTORY ----------
order_traj <- trajectory("OrderFlow") %>%
  # Llega pedido (demanda 1 unidad)
  set_attribute("start_time", function() {now(env)}) %>%
  seize("ReceivingDock", 1) %>%
  timeout(receiving_time) %>%
  release("ReceivingDock", 1) %>%
  # Almacenamiento incrementa inventario (entrada de stock)
  set_attribute("stock_in", function() {
    inventory <<- min(inventory + 1, warehouse_capacity)
    inventory_movements <<- inventory_movements + 1
    return(inventory)
  }) %>%
  timeout(storage_time) %>%
  # Picking (se necesita inventario)
  branch(
    option = function() {
      total_orders <<- total_orders + 1
      if (inventory > 0) {
        inventory <<- inventory - 1
        fulfilled_orders <<- fulfilled_orders + 1
        return(1) # va al flujo normal
      } else {
        backorders <<- backorders + 1
        return(2) # flujo alterno (no hay stock)
      }
    },
    continue = c(TRUE, TRUE),
    trajectory("Fulfilled") %>%
      timeout(picking_time) %>%
      seize("ShippingDock", 1) %>%
      timeout(shipping_time) %>%
      release("ShippingDock", 1) %>%
      set_attribute("end_time", function() {now(env)}),
    trajectory("Backorder") %>%
      set_attribute("end_time", function() {now(env)})
  )

Para la sección de Generador tenemos:

# ---------- GENERADOR ----------
env %>%
  add_generator("Order", order_traj, at(seq(0, sim_time, by = order_interval))) %>%
  run(until = sim_time)
## simmer environment: CDC#1 | now: 480 | next: 480
## { Monitor: in memory }
## { Resource: ReceivingDock | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Resource: ShippingDock | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Resource: WarehouseSpace | monitored: TRUE | server status: 0(100) | queue status: 0(Inf) }
## { Source: Order | monitored: 1 | n_generated: 97 }

Se calculan los indicadores clave:

  1. Order Cycle Time: Tiempo promedio desde que llega un pedido hasta que se despacha.

  2. Dock Utilization: Proporción de tiempo que los muelles estuvieron ocupados.

  3. Order Fill Rate: Porcentaje de pedidos atendidos correctamente.

  4. Inventory Turnover: Veces que el inventario se renovó durante la simulación.

  5. Backorders: Número de pedidos no atendidos por falta de inventario.

# ---------- KPIs ----------
arrivals <- get_mon_arrivals(env)
resources <- get_mon_resources(env)

# 1. Order Cycle Time
arrivals$cycle_time <- arrivals$end_time - arrivals$start_time
avg_cycle_time <- mean(arrivals$cycle_time, na.rm = TRUE)

# 2. Dock Utilization
dock_util <- resources %>%
  dplyr::filter(resource %in% c("ReceivingDock", "ShippingDock")) %>%
  dplyr::group_by(resource) %>%
  dplyr::summarise(utilization = sum((lead(time, default = sim_time) - time) * 
                                       server) / sim_time)

# 3. Order Fill Rate
order_fill_rate <- ifelse(total_orders > 0, fulfilled_orders / total_orders, 0)

# 4. Inventory Turnover
inventory_turnover <- ifelse(mean(c(inventory, initial_inventory)) > 0,
                             inventory_movements / ((initial_inventory + inventory) / 2),
                             NA)

# 5. Backorders
backorders_total <- backorders

Resultados

Se imprimen los valores calculados para cada KPI en la consola y se genera un histograma con la distribución de los tiempos de ciclo de los pedidos.

# ---------- RESULTADOS ----------
cat("----- KPIs -----\n")
## ----- KPIs -----
cat("1. Avg Order Cycle Time (min):", round(avg_cycle_time, 2), "\n")
## 1. Avg Order Cycle Time (min): 8
cat("2. Dock Utilization:\n")
## 2. Dock Utilization:
print(dock_util)
## # A tibble: 2 × 2
##   resource      utilization
##   <chr>               <dbl>
## 1 ReceivingDock       0.4  
## 2 ShippingDock        0.396
cat("3. Order Fill Rate:", round(order_fill_rate * 100, 2), "%\n")
## 3. Order Fill Rate: 100 %
cat("4. Inventory Turnover:", round(inventory_turnover, 2), "\n")
## 4. Inventory Turnover: 1.92
cat("5. Backorders:", backorders_total, "\n")
## 5. Backorders: 0
# ---------- GRÁFICO DE TIEMPO DE CICLO ----------
ggplot(arrivals, aes(x = cycle_time)) +
  geom_histogram(binwidth = 1, fill = "steelblue", color = "black") +
  labs(title = "Distribución del tiempo de ciclo", x = "Tiempo de ciclo (min)",
       y = "Frecuencia")

1. Avg Order Cycle Time (8 min)

2. Dock Utilization (Receiving: 40%, Shipping: 39.6%)

3. Order Fill Rate (100%)

4. Inventory Turnover (1.92)

5. Backorders (0)