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:
Este problema es fundamental en la gestión de inventarios y tiene aplicaciones en muchas industrias: retail, moda, alimentos perecederos, tecnología, entre otras.
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 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.
# 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
# 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
## Cantidad óptima teórica: 96 unidades
# 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)
# 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)
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))
)
# 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)
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)
# 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)
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 |
# 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)
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")
# 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)
# 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)
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 |
La simulación de Monte Carlo para el problema del Newsvendor nos permite:
Hallazgos clave:
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.