Introducción al Problema del Newsvendor

¿Qué es el problema del Newsvendor?

El problema del Newsvendor (o vendedor de periódicos) es un modelo clásico de gestión de inventario que aborda la decisión de cuántas unidades de un producto ordenar cuando:

  1. La demanda del producto es incierta pero con una distribución de probabilidad conocida
  2. El producto tiene un ciclo de vida corto o perecedero (como periódicos o productos de temporada)
  3. Las decisiones de pedido deben tomarse antes de conocer la demanda real
  4. Existen dos tipos de costos:
    • Costo de sobrestock: cuando se ordena demasiado y quedan unidades sin vender
    • Costo de substock: cuando se ordena poco y se pierden ventas potenciales

Este problema es fundamental en la gestión de inventarios y tiene aplicaciones en muchas industrias: retail, moda, alimentos perecederos, tecnología, entre otras.

Formulación matemática

Sea: - \(q\) = cantidad a ordenar (unidades) - \(D\) = demanda aleatoria (unidades) - \(c\) = costo unitario de compra ($) - \(p\) = precio unitario de venta ($) - \(s\) = valor unitario de salvamento ($)

Entonces:

  • El costo de sobrestock por unidad es \(c - s\) (lo que pagué menos lo que recupero)
  • El costo de substock por unidad es \(p - c\) (el margen de ganancia perdido)

El problema busca encontrar la cantidad óptima \(q^*\) que maximice la ganancia esperada.

La solución óptima teórica es:

\[F(q^*) = \frac{p-c}{p-s}\]

Donde \(F\) es la función de distribución acumulada (CDF) de la demanda.

Simulación de Monte Carlo

Configuración de parámetros

# Parámetros del problema
set.seed(123)  # Para reproducibilidad
n_sim <- 10000  # Número de simulaciones

# Parámetros económicos
precio_venta <- 12      # Precio de venta por unidad
costo_compra <- 8       # Costo de compra por unidad
valor_salvamento <- 3   # Valor de salvamento por unidad no vendida

# Parámetros de la demanda (distribución normal)
media_demanda <- 100    # Media de la demanda diaria
sd_demanda <- 30        # Desviación estándar de la demanda

Cálculo teórico de la cantidad óptima

# Ratio crítico (critical ratio)
critical_ratio <- (precio_venta - costo_compra) / (precio_venta - valor_salvamento)

# Cantidad óptima teórica (usando la inversa de la CDF normal)
q_optimo <- qnorm(critical_ratio, media_demanda, sd_demanda)
q_optimo <- round(q_optimo)

cat("Ratio Crítico:", critical_ratio, "\n")
## Ratio Crítico: 0.4444444
cat("Cantidad óptima teórica:", q_optimo, "unidades\n")
## Cantidad óptima teórica: 96 unidades

Simulación de diferentes niveles de pedido

# Generar rangos de cantidades de pedido para evaluar
min_q <- max(0, media_demanda - 3*sd_demanda)
max_q <- media_demanda + 3*sd_demanda
cantidades <- seq(from = min_q, to = max_q, by = 5)

# Matriz para almacenar resultados
resultados <- matrix(0, nrow = length(cantidades), ncol = 3)
colnames(resultados) <- c("Cantidad", "Beneficio_Medio", "Desv_Estandar")
resultados[, 1] <- cantidades

# Generar demandas aleatorias para todas las simulaciones
demandas <- rnorm(n_sim, media_demanda, sd_demanda)
demandas <- pmax(0, demandas)  # La demanda no puede ser negativa

# Realizar simulación para cada cantidad
for (i in 1:length(cantidades)) {
  q <- cantidades[i]
  
  # Calcular beneficios para esta cantidad
  beneficios <- sapply(demandas, function(d) {
    ventas <- min(q, d)
    sobrantes <- max(0, q - d)
    beneficio <- ventas * precio_venta + sobrantes * valor_salvamento - q * costo_compra
    return(beneficio)
  })
  
  # Guardar resultados
  resultados[i, 2] <- mean(beneficios)
  resultados[i, 3] <- sd(beneficios)
}

# Convertir a dataframe
df_resultados <- as.data.frame(resultados)

Visualización de resultados

# Gráfico de beneficio esperado vs. cantidad ordenada
p <- ggplot(df_resultados, aes(x = Cantidad, y = Beneficio_Medio)) +
  geom_line(color = "#3498db", size = 1.2) +
  geom_point(color = "#2c3e50", size = 2) +
  geom_vline(xintercept = q_optimo, linetype = "dashed", color = "#e74c3c", size = 1) +
  annotate("text", x = q_optimo + 10, y = min(df_resultados$Beneficio_Medio) + 20, 
           label = paste("Q óptimo =", q_optimo), color = "#e74c3c") +
  labs(title = "Beneficio Esperado vs. Cantidad Ordenada",
       x = "Cantidad Ordenada (q)",
       y = "Beneficio Esperado ($)") +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.title = element_text(face = "bold"),
    panel.grid.major = element_line(color = "#f0f0f0"),
    panel.grid.minor = element_blank()
  )

ggplotly(p)

Análisis de sensibilidad

Simulación detallada para la cantidad óptima

Ahora realizaremos una simulación detallada para la cantidad óptima teórica y examinaremos la distribución de los beneficios.

# Simulación detallada para q óptimo
q <- q_optimo
beneficios_optimo <- numeric(n_sim)
demanda_simulada <- numeric(n_sim)
ventas_simuladas <- numeric(n_sim)
sobrantes_simulados <- numeric(n_sim)
faltantes_simulados <- numeric(n_sim)

for (i in 1:n_sim) {
  demanda <- max(0, rnorm(1, media_demanda, sd_demanda))
  ventas <- min(q, demanda)
  sobrantes <- max(0, q - demanda)
  faltantes <- max(0, demanda - q)
  
  beneficio <- ventas * precio_venta + sobrantes * valor_salvamento - q * costo_compra
  
  demanda_simulada[i] <- demanda
  ventas_simuladas[i] <- ventas
  sobrantes_simulados[i] <- sobrantes
  faltantes_simulados[i] <- faltantes
  beneficios_optimo[i] <- beneficio
}

# Crear dataframe con resultados detallados
df_detallado <- data.frame(
  Simulacion = 1:n_sim,
  Demanda = demanda_simulada,
  Ventas = ventas_simuladas,
  Sobrantes = sobrantes_simulados,
  Faltantes = faltantes_simulados,
  Beneficio = beneficios_optimo
)

# Resumen estadístico
resumen <- data.frame(
  Estadistica = c("Media", "Desviación estándar", "Mínimo", "Máximo", 
                  "Percentil 5%", "Percentil 95%", "Prob. Beneficio Negativo"),
  Valor = c(mean(beneficios_optimo), sd(beneficios_optimo), min(beneficios_optimo), 
            max(beneficios_optimo), quantile(beneficios_optimo, 0.05), 
            quantile(beneficios_optimo, 0.95), mean(beneficios_optimo < 0))
)

Visualización de la distribución de beneficios

# Histograma de beneficios
p2 <- ggplot(df_detallado, aes(x = Beneficio)) +
  geom_histogram(bins = 30, fill = "#3498db", color = "#ffffff", alpha = 0.8) +
  geom_vline(xintercept = mean(beneficios_optimo), color = "#e74c3c", linetype = "dashed", size = 1) +
  annotate("text", x = mean(beneficios_optimo) + 20, y = 50, 
           label = paste("Media =", round(mean(beneficios_optimo), 2)), color = "#e74c3c") +
  labs(title = "Distribución de Beneficios con Cantidad Óptima",
       x = "Beneficio ($)",
       y = "Frecuencia") +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.title = element_text(face = "bold"),
    panel.grid.major = element_line(color = "#f0f0f0"),
    panel.grid.minor = element_blank()
  )

ggplotly(p2)

Análisis de escenarios

Evaluemos cómo cambia la cantidad óptima si varían los parámetros económicos.

# Función para calcular la cantidad óptima
calcular_q_optimo <- function(p, c, s, media, sd) {
  critical_ratio <- (p - c) / (p - s)
  q_optimo <- qnorm(critical_ratio, media, sd)
  return(round(q_optimo))
}

# Crear escenarios
escenarios <- expand.grid(
  precio_venta = c(10, 12, 15),
  costo_compra = c(6, 8, 9),
  valor_salvamento = c(1, 3, 5)
)

# Filtrar escenarios válidos (precio > costo > salvamento)
escenarios <- escenarios %>%
  filter(precio_venta > costo_compra & costo_compra > valor_salvamento)

# Calcular cantidad óptima para cada escenario
escenarios$q_optimo <- mapply(calcular_q_optimo, 
                             escenarios$precio_venta, 
                             escenarios$costo_compra, 
                             escenarios$valor_salvamento,
                             MoreArgs = list(media = media_demanda, sd = sd_demanda))

# Calcular el ratio crítico
escenarios$critical_ratio <- (escenarios$precio_venta - escenarios$costo_compra) / 
                            (escenarios$precio_venta - escenarios$valor_salvamento)

Tabla de resultados por escenario

# Ordenar por cantidad óptima
escenarios_ordenados <- escenarios %>%
  arrange(q_optimo)

# Mostrar tabla de resultados
knitr::kable(escenarios_ordenados, 
             col.names = c("Precio venta", "Costo compra", "Valor salvamento", 
                           "Cantidad óptima", "Ratio crítico"),
             caption = "Análisis de escenarios para diferentes parámetros económicos",
             digits = 2)
Análisis de escenarios para diferentes parámetros económicos
Precio venta Costo compra Valor salvamento Cantidad óptima Ratio crítico
10 9 1 63 0.11
10 9 3 68 0.14
10 9 5 75 0.20
10 8 1 77 0.22
12 9 1 82 0.27
10 8 3 83 0.29
12 9 3 87 0.33
12 8 1 90 0.36
10 8 5 92 0.40
15 9 1 95 0.43
12 9 5 95 0.43
10 6 1 96 0.44
12 8 3 96 0.44
15 8 1 100 0.50
15 9 3 100 0.50
12 6 1 103 0.55
10 6 3 105 0.57
12 8 5 105 0.57
15 8 3 106 0.58
15 9 5 108 0.60
15 6 1 111 0.64
12 6 3 113 0.67
15 8 5 116 0.70
15 6 3 120 0.75
10 6 5 125 0.80
12 6 5 132 0.86
15 6 5 138 0.90

Visualización del impacto de los parámetros económicos

# Gráfico para ver relación entre ratio crítico y cantidad óptima
p3 <- ggplot(escenarios, aes(x = critical_ratio, y = q_optimo, 
                            color = factor(precio_venta), 
                            shape = factor(costo_compra))) +
  geom_point(size = 3, alpha = 0.8) +
  labs(title = "Impacto del Ratio Crítico en la Cantidad Óptima",
       x = "Ratio Crítico",
       y = "Cantidad Óptima",
       color = "Precio Venta",
       shape = "Costo Compra") +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.title = element_text(face = "bold"),
    panel.grid.major = element_line(color = "#f0f0f0"),
    panel.grid.minor = element_blank(),
    legend.position = "bottom"
  )

ggplotly(p3)

Análisis de la distribución de la demanda

Evaluemos el impacto de diferentes distribuciones de la demanda en la cantidad óptima.

# Parámetros de diferentes distribuciones
# 1. Normal
# 2. Lognormal
# 3. Gamma

# Generar muestras de cada distribución
n_samples <- 10000
set.seed(123)

# Ajustar parámetros para que todas tengan aproximadamente la misma media
demanda_normal <- rnorm(n_samples, media_demanda, sd_demanda)
demanda_normal <- pmax(0, demanda_normal)  # Truncar en cero

# Lognormal con media similar
mu_log <- log(media_demanda^2 / sqrt(media_demanda^2 + sd_demanda^2))
sigma_log <- sqrt(log(1 + (sd_demanda^2 / media_demanda^2)))
demanda_lognormal <- rlnorm(n_samples, mu_log, sigma_log)

# Gamma con media similar
shape <- (media_demanda^2) / (sd_demanda^2)
scale <- (sd_demanda^2) / media_demanda
demanda_gamma <- rgamma(n_samples, shape = shape, scale = scale)

# Crear dataframe para visualización
df_distribuciones <- data.frame(
  Normal = demanda_normal,
  Lognormal = demanda_lognormal,
  Gamma = demanda_gamma
)

# Convertir a formato largo para ggplot
df_long <- df_distribuciones %>%
  tidyr::pivot_longer(cols = everything(),
                     names_to = "Distribucion",
                     values_to = "Demanda")

Comparación visual de las distribuciones

# Histogramas superpuestos
p4 <- ggplot(df_long, aes(x = Demanda, fill = Distribucion)) +
  geom_histogram(position = "identity", alpha = 0.4, bins = 30) +
  labs(title = "Comparación de Distribuciones de Demanda",
       x = "Demanda",
       y = "Frecuencia") +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.title = element_text(face = "bold"),
    panel.grid.major = element_line(color = "#f0f0f0"),
    panel.grid.minor = element_blank(),
    legend.position = "bottom"
  )

ggplotly(p4)

Impacto en la cantidad óptima

# Calcular cantidad óptima para cada distribución
calcular_q_optimo_sim <- function(demandas, precio, costo, salvamento) {
  # Ordenar las demandas
  demandas_ord <- sort(demandas)
  
  # Calcular el índice correspondiente al ratio crítico
  ratio <- (precio - costo) / (precio - salvamento)
  indice <- ceiling(length(demandas) * ratio)
  
  # Devolver el valor correspondiente
  return(demandas_ord[indice])
}

# Calcular cantidades óptimas
q_opt_normal <- calcular_q_optimo_sim(demanda_normal, precio_venta, costo_compra, valor_salvamento)
q_opt_lognormal <- calcular_q_optimo_sim(demanda_lognormal, precio_venta, costo_compra, valor_salvamento)
q_opt_gamma <- calcular_q_optimo_sim(demanda_gamma, precio_venta, costo_compra, valor_salvamento)

# Crear tabla de resultados
resultados_dist <- data.frame(
  Distribucion = c("Normal", "Lognormal", "Gamma"),
  Media = c(mean(demanda_normal), mean(demanda_lognormal), mean(demanda_gamma)),
  Desviacion = c(sd(demanda_normal), sd(demanda_lognormal), sd(demanda_gamma)),
  Cantidad_Optima = c(q_opt_normal, q_opt_lognormal, q_opt_gamma)
)

knitr::kable(resultados_dist, 
             col.names = c("Distribución", "Media", "Desviación Estándar", "Cantidad Óptima"),
             caption = "Impacto de la distribución en la cantidad óptima",
             digits = 2)
Impacto de la distribución en la cantidad óptima
Distribución Media Desviación Estándar Cantidad Óptima
Normal 99.93 29.95 95.84
Lognormal 99.76 30.06 91.64
Gamma 100.22 29.76 92.82

Conclusiones

La simulación de Monte Carlo para el problema del Newsvendor nos permite:

  1. Determinar la cantidad óptima a ordenar considerando la incertidumbre en la demanda
  2. Cuantificar el riesgo asociado con diferentes niveles de pedido
  3. Evaluar escenarios con diferentes parámetros económicos
  4. Analizar el impacto de diferentes distribuciones de la demanda

Hallazgos clave:

  • La cantidad óptima a ordenar para nuestro caso base es de 96 unidades
  • Esta cantidad genera un beneficio esperado de $292.2
  • El ratio crítico es 0.44, lo que significa que estamos optimizando para tener una probabilidad del 44.4% de satisfacer toda la demanda
  • Diferentes distribuciones de demanda pueden llevar a diferentes cantidades óptimas, incluso con la misma media y varianza
  • Los parámetros económicos tienen un impacto significativo en la cantidad óptima; en particular, cuanto mayor es la diferencia entre el precio de venta y el costo, mayor será la cantidad óptima

La simulación de Monte Carlo es una herramienta poderosa para este tipo de problemas, ya que permite modelar la incertidumbre de forma explícita y evaluar múltiples escenarios, proporcionando insights valiosos para la toma de decisiones.