En este documento se desarrollan los cuatro ejercicios solicitados en la Actividad 2: (1) pronóstico de ventas semanales con tres modelos vistos en clase, (2) el mismo análisis aplicado a la base de datos de ventas históricas de leche saborizada Hershey (Lechitas), (3) el caso Vintage Restaurant, y (4) el caso Carlson Department Store.
Para los ejercicios 1 y 2 se utilizan los tres modelos de pronóstico trabajados en clase:
pesos
en el código de este documento y todo se recalcula automáticamente.semana <- 1:12
ventas <- c(17, 21, 19, 23, 18, 16, 20, 11, 22, 20, 15, 22)
ej1 <- data.frame(Semana = semana, Ventas = ventas)
tabla(ej1, digits = 0, caption = "Ventas semanales (Ejercicio 1)")
| Semana | Ventas |
|---|---|
| 1 | 17 |
| 2 | 21 |
| 3 | 19 |
| 4 | 23 |
| 5 | 18 |
| 6 | 16 |
| 7 | 20 |
| 8 | 11 |
| 9 | 22 |
| 10 | 20 |
| 11 | 15 |
| 12 | 22 |
ggplot(ej1, aes(Semana, Ventas)) +
geom_line(color = "#2C7C91") +
geom_point(color = "#2C7C91", size = 2) +
scale_x_continuous(breaks = semana) +
labs(title = "Serie de tiempo: Ventas Semanales", x = "Semana", y = "Ventas") +
theme_minimal()
naive_pron <- c(NA, head(ventas, -1))
ej1_naive <- data.frame(Semana = semana, Ventas = ventas, Pronostico = naive_pron)
tabla(ej1_naive, caption = "Modelo Naive - Ejercicio 1")
| Semana | Ventas | Pronostico |
|---|---|---|
| 1 | 17 | NA |
| 2 | 21 | 17 |
| 3 | 19 | 21 |
| 4 | 23 | 19 |
| 5 | 18 | 23 |
| 6 | 16 | 18 |
| 7 | 20 | 16 |
| 8 | 11 | 20 |
| 9 | 22 | 11 |
| 10 | 20 | 22 |
| 11 | 15 | 20 |
| 12 | 22 | 15 |
met_naive_1 <- metricas(ventas, naive_pron)
tabla(met_naive_1, caption = "Métricas de exactitud - Naive (Ejercicio 1)")
| ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|
| 0.45 | 5 | 32.82 | 5.73 | 28.56 |
k <- 3
sma_pron <- rep(NA_real_, length(ventas))
for (t in (k + 1):length(ventas)) {
sma_pron[t] <- mean(ventas[(t - k):(t - 1)])
}
ej1_sma <- data.frame(Semana = semana, Ventas = ventas, Pronostico = round(sma_pron, 2))
tabla(ej1_sma, caption = "Modelo Promedio Móvil Simple k=3 - Ejercicio 1")
| Semana | Ventas | Pronostico |
|---|---|---|
| 1 | 17 | NA |
| 2 | 21 | NA |
| 3 | 19 | NA |
| 4 | 23 | 19.00 |
| 5 | 18 | 21.00 |
| 6 | 16 | 20.00 |
| 7 | 20 | 19.00 |
| 8 | 11 | 18.00 |
| 9 | 22 | 15.67 |
| 10 | 20 | 17.67 |
| 11 | 15 | 17.67 |
| 12 | 22 | 19.00 |
met_sma_1 <- metricas(ventas, sma_pron)
tabla(met_sma_1, caption = "Métricas de exactitud - Promedio Móvil Simple (Ejercicio 1)")
| ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|
| 0 | 3.7 | 16.96 | 4.12 | 22.17 |
pesos <- c(0.2, 0.3, 0.5) # peso para t-3, t-2, t-1 (el más reciente pesa más)
wma_pron <- rep(NA_real_, length(ventas))
for (t in (k + 1):length(ventas)) {
wma_pron[t] <- sum(ventas[(t - k):(t - 1)] * pesos)
}
ej1_wma <- data.frame(Semana = semana, Ventas = ventas, Pronostico = round(wma_pron, 2))
tabla(ej1_wma, caption = "Modelo Promedio Móvil Ponderado k=3 (pesos 0.5/0.3/0.2) - Ejercicio 1")
| Semana | Ventas | Pronostico |
|---|---|---|
| 1 | 17 | NA |
| 2 | 21 | NA |
| 3 | 19 | NA |
| 4 | 23 | 19.2 |
| 5 | 18 | 21.4 |
| 6 | 16 | 19.7 |
| 7 | 20 | 18.0 |
| 8 | 11 | 18.4 |
| 9 | 22 | 14.7 |
| 10 | 20 | 18.3 |
| 11 | 15 | 18.8 |
| 12 | 22 | 17.9 |
met_wma_1 <- metricas(ventas, wma_pron)
tabla(met_wma_1, caption = "Métricas de exactitud - Promedio Móvil Ponderado (Ejercicio 1)")
| ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|
| 0.07 | 4.13 | 20.65 | 4.54 | 24.61 |
resumen_1 <- rbind(
cbind(Modelo = "Naive", met_naive_1),
cbind(Modelo = "Promedio Móvil Simple (k=3)", met_sma_1),
cbind(Modelo = "Promedio Móvil Ponderado (k=3)", met_wma_1)
)
tabla(resumen_1, caption = "Comparación de modelos - Ejercicio 1 (Ventas Semanales)")
| Modelo | ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|---|
| Naive | 0.45 | 5.00 | 32.82 | 5.73 | 28.56 |
| Promedio Móvil Simple (k=3) | 0.00 | 3.70 | 16.96 | 4.12 | 22.17 |
| Promedio Móvil Ponderado (k=3) | 0.07 | 4.13 | 20.65 | 4.54 | 24.61 |
De acuerdo con el MAPE (que permite comparar el error en términos porcentuales, independientemente de la escala de la serie), el modelo con menor error es Promedio Móvil Simple (k=3).
En una serie tan corta y con variaciones semana a semana relativamente grandes (de 11 a 23 unidades), el modelo Naive tiende a sobrerreaccionar a los cambios recientes, mientras que los promedios móviles (simple y ponderado) suavizan el ruido de corto plazo. El promedio móvil ponderado, al dar más peso a la observación más reciente, reacciona un poco más rápido que el promedio móvil simple ante cambios de nivel, sin perder toda la capacidad de suavizamiento. Dado que la serie no muestra una tendencia ni una estacionalidad clara (parece fluctuar alrededor de un nivel relativamente estable, entre 15 y 23), un modelo de promedios (simple o ponderado) es más adecuado que el Naive puro para pronosticar el corto plazo, y es preferible sobre modelos de tendencia o estacionalidad que no aplican a una serie de solo 12 observaciones sin patrón evidente.
Se aplican las mismas instrucciones y los mismos tres modelos del Ejercicio 1 a la base de datos Ventas_Históricas_Lechitas.xlsx (ventas mensuales de leche saborizada Hershey México, en miles de dólares, de enero 2017 a diciembre 2019).
fechas <- seq(as.Date("2017-01-01"), as.Date("2019-12-01"), by = "month")
y2017 <- c(25520.51, 23740.11, 26253.58, 25868.43, 27072.87, 27150.5, 27067.1, 28145.25, 27546.29, 28400.37, 27441.98, 27852.47)
y2018 <- c(28463.69, 26996.11, 29768.2, 29292.51, 29950.68, 30099.17, 30851.26, 32271.76, 31940.74, 32995.93, 32197.12, 31984.82)
y2019 <- c(32496.44, 31287.28, 33376.02, 32949.77, 34004.11, 33757.89, 32927.3, 34324.12, 35151.28, 36133.07, 34799.91, 34846.17)
ventas2 <- c(y2017, y2018, y2019)
periodo <- 1:length(ventas2)
ej2 <- data.frame(Periodo = periodo, Fecha = fechas, Ventas = round(ventas2, 2))
tabla(head(ej2, 12), caption = "Ventas mensuales Lechitas (primeros 12 meses, ver serie completa en la gráfica)")
| Periodo | Fecha | Ventas |
|---|---|---|
| 1 | 2017-01-01 | 25520.51 |
| 2 | 2017-02-01 | 23740.11 |
| 3 | 2017-03-01 | 26253.58 |
| 4 | 2017-04-01 | 25868.43 |
| 5 | 2017-05-01 | 27072.87 |
| 6 | 2017-06-01 | 27150.50 |
| 7 | 2017-07-01 | 27067.10 |
| 8 | 2017-08-01 | 28145.25 |
| 9 | 2017-09-01 | 27546.29 |
| 10 | 2017-10-01 | 28400.37 |
| 11 | 2017-11-01 | 27441.98 |
| 12 | 2017-12-01 | 27852.47 |
ggplot(ej2, aes(Fecha, Ventas)) +
geom_line(color = "#B5443C") +
geom_point(color = "#B5443C", size = 1.5) +
labs(title = "Serie de tiempo: Ventas mensuales Lechitas (2017-2019)", x = "Mes", y = "Ventas (miles de $)") +
theme_minimal()
La serie muestra una tendencia creciente clara a lo largo de los tres años, junto con una ligera estacionalidad anual (caídas relativas en febrero y repuntes hacia fin de año), características muy distintas a la serie del Ejercicio 1.
naive_pron2 <- c(NA, head(ventas2, -1))
met_naive_2 <- metricas(ventas2, naive_pron2)
tabla(met_naive_2, caption = "Métricas de exactitud - Naive (Ejercicio 2)")
| ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|
| 266.45 | 902.85 | 1237975 | 1112.64 | 3.01 |
sma_pron2 <- rep(NA_real_, length(ventas2))
for (t in (k + 1):length(ventas2)) {
sma_pron2[t] <- mean(ventas2[(t - k):(t - 1)])
}
met_sma_2 <- metricas(ventas2, sma_pron2)
tabla(met_sma_2, caption = "Métricas de exactitud - Promedio Móvil Simple (Ejercicio 2)")
| ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|
| 591.01 | 870.47 | 1063496 | 1031.26 | 2.81 |
wma_pron2 <- rep(NA_real_, length(ventas2))
for (t in (k + 1):length(ventas2)) {
wma_pron2[t] <- sum(ventas2[(t - k):(t - 1)] * pesos)
}
met_wma_2 <- metricas(ventas2, wma_pron2)
tabla(met_wma_2, caption = "Métricas de exactitud - Promedio Móvil Ponderado (Ejercicio 2)")
| ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|
| 492.27 | 818.64 | 940425.7 | 969.76 | 2.64 |
comp2 <- data.frame(
Fecha = fechas,
Real = ventas2,
Naive = naive_pron2,
SMA = sma_pron2,
WMA = wma_pron2
) %>% pivot_longer(cols = c(Real, Naive, SMA, WMA), names_to = "Serie", values_to = "Valor")
ggplot(filter(comp2, Fecha >= as.Date("2019-01-01")), aes(Fecha, Valor, color = Serie)) +
geom_line(linewidth = 0.9) +
labs(title = "Real vs. pronósticos - último año (2019)", x = "Mes", y = "Ventas (miles de $)") +
theme_minimal()
resumen_2 <- rbind(
cbind(Modelo = "Naive", met_naive_2),
cbind(Modelo = "Promedio Móvil Simple (k=3)", met_sma_2),
cbind(Modelo = "Promedio Móvil Ponderado (k=3)", met_wma_2)
)
tabla(resumen_2, caption = "Comparación de modelos - Ejercicio 2 (Lechitas)")
| Modelo | ME | MAE | MSE | RMSE | MAPE(%) |
|---|---|---|---|---|---|
| Naive | 266.45 | 902.85 | 1237975.5 | 1112.64 | 3.01 |
| Promedio Móvil Simple (k=3) | 591.01 | 870.47 | 1063496.1 | 1031.26 | 2.81 |
| Promedio Móvil Ponderado (k=3) | 492.27 | 818.64 | 940425.7 | 969.76 | 2.64 |
El modelo con menor MAPE es Promedio Móvil Ponderado (k=3).
Como la serie de Lechitas tiene una tendencia sostenida al alza, cualquiera de los tres modelos (Naive, promedio móvil simple y ponderado) queda sistemáticamente por debajo del valor real, ya que ninguno incorpora explícitamente una componente de tendencia: todos “persiguen” a la serie desde atrás. El modelo Naive es el que reacciona más rápido a ese crecimiento (usa únicamente el dato inmediato anterior), por lo que en series con tendencia clara suele tener menor error que los promedios móviles, que al promediar 3 meses se retrasan más frente al crecimiento sostenido. Para esta serie, un modelo que incorpore tendencia (p. ej. suavización exponencial con tendencia u regresión lineal) sería más apropiado que cualquiera de los tres modelos aquí comparados, pero de los tres, el indicado por el MAPE es la mejor opción disponible.
El Vintage Restaurant, en la isla Captiva (Florida), cuenta con ventas mensuales de alimentos y bebidas (en miles de dólares) para sus primeros tres años de operación. Karen Payne, la propietaria, requiere pronosticar las ventas mensuales del cuarto año.
meses <- factor(month.name, levels = month.name)
Year1 <- c(242, 235, 232, 178, 184, 140, 145, 152, 110, 130, 152, 206)
Year2 <- c(263, 238, 247, 193, 193, 149, 157, 161, 122, 130, 167, 230)
Year3 <- c(282, 255, 265, 205, 210, 160, 166, 174, 126, 148, 173, 235)
ej3 <- data.frame(Mes = meses, `Año 1` = Year1, `Año 2` = Year2, `Año 3` = Year3, check.names = FALSE)
tabla(ej3, digits = 0, caption = "Ventas mensuales Vintage Restaurant (miles de $)")
| Mes | Año 1 | Año 2 | Año 3 |
|---|---|---|---|
| January | 242 | 263 | 282 |
| February | 235 | 238 | 255 |
| March | 232 | 247 | 265 |
| April | 178 | 193 | 205 |
| May | 184 | 193 | 210 |
| June | 140 | 149 | 160 |
| July | 145 | 157 | 166 |
| August | 152 | 161 | 174 |
| September | 110 | 122 | 126 |
| October | 130 | 130 | 148 |
| November | 152 | 167 | 173 |
| December | 206 | 230 | 235 |
ventas3 <- c(Year1, Year2, Year3)
t3 <- 1:36
fecha3 <- seq(as.Date("2021-01-01"), by = "month", length.out = 36)
serie3 <- ts(ventas3, start = c(2021, 1), frequency = 12)
autoplot(serie3) +
labs(title = "Vintage Restaurant: ventas mensuales (Año 1 a Año 3)",
x = "Año", y = "Ventas (miles de $)") +
theme_minimal()
Comentario. La serie muestra dos patrones principales: (1) una tendencia creciente de un año a otro (las ventas totales pasan de 2,106 a 2,250 y a 2,399 miles de dólares), y (2) una estacionalidad anual muy marcada: las ventas son altas en los meses de enero a marzo y en diciembre (temporada alta turística/vacacional), y caen fuertemente entre junio y octubre, con un mínimo en septiembre.
Se calculan mediante el método de descomposición (razón al promedio móvil centrado de 12 periodos).
desc3 <- decompose(serie3, type = "multiplicative")
indices <- desc3$figure
names(indices) <- month.name
tabla(data.frame(Mes = month.name, `Índice Estacional` = round(indices, 3), check.names = FALSE),
caption = "Índices estacionales mensuales - Vintage Restaurant")
| Mes | Índice Estacional | |
|---|---|---|
| January | January | 1.44 |
| February | February | 1.30 |
| March | March | 1.34 |
| April | April | 1.04 |
| May | May | 1.05 |
| June | June | 0.80 |
| July | July | 0.83 |
| August | August | 0.85 |
| September | September | 0.63 |
| October | October | 0.70 |
| November | November | 0.85 |
| December | December | 1.16 |
ggplot(data.frame(Mes = factor(month.name, levels = month.name), Indice = indices),
aes(Mes, Indice)) +
geom_col(fill = "#2C7C91") +
geom_hline(yintercept = 1, linetype = "dashed", color = "grey40") +
labs(title = "Índices estacionales mensuales", y = "Índice estacional") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
Comentario. Los meses con índice estacional mayor a 1 (temporada alta) son enero, febrero, marzo y diciembre; el mes de septiembre presenta el índice más bajo (temporada baja), seguido de octubre. Esto coincide con la intuición del negocio: Captiva Island es un destino vacacional de invierno, por lo que las ventas del restaurante son más altas en los meses de temporada de nieve en el norte de EE. UU. (turistas que escapan del frío) y en diciembre por las fiestas decembrinas, mientras que caen en los meses de verano/inicio de otoño, que además coincide con la temporada de huracanes en Florida (menor afluencia turística). Los índices estacionales sí tienen sentido intuitivo para este tipo de negocio.
desestacional <- serie3 / rep(indices, 3)
df_deses <- data.frame(Fecha = fecha3, Real = as.numeric(serie3), Desestacionalizada = as.numeric(desestacional))
ggplot(df_deses, aes(Fecha)) +
geom_line(aes(y = Real, color = "Serie original")) +
geom_line(aes(y = Desestacionalizada, color = "Desestacionalizada"), linewidth = 1) +
labs(title = "Serie original vs. desestacionalizada", y = "Ventas (miles de $)", color = "") +
theme_minimal()
modelo_tendencia <- lm(Desestacionalizada ~ Fecha, data = df_deses %>% mutate(Fecha = t3))
tabla(broom_coef <- as.data.frame(summary(modelo_tendencia)$coefficients), digits = 4,
caption = "Regresión lineal de tendencia sobre la serie desestacionalizada")
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 169.3494 | 1.0926 | 154.9951 | 0 |
| Fecha | 1.0213 | 0.0515 | 19.8323 | 0 |
Comentario. Una vez removido el efecto estacional, la serie desestacionalizada muestra una tendencia lineal creciente y sostenida a lo largo de los 36 meses: la pendiente estimada de la regresión (1.021 miles de $ por mes) es positiva y estadísticamente distinta de cero, confirmando que el crecimiento del restaurante no es solo estacional sino un crecimiento real del negocio en el tiempo.
b0 <- coef(modelo_tendencia)[1]
b1 <- coef(modelo_tendencia)[2]
t_year4 <- 37:48
tendencia_year4 <- b0 + b1 * t_year4
pron_decomp <- tendencia_year4 * indices
tabla(data.frame(Mes = month.name, `Tendencia` = round(tendencia_year4, 1),
`Índice Estacional` = round(indices, 3),
`Pronóstico (Descomposición)` = round(pron_decomp, 1), check.names = FALSE),
caption = "Pronóstico Año 4 - Método de descomposición")
| Mes | Tendencia | Índice Estacional | Pronóstico (Descomposición) | |
|---|---|---|---|---|
| January | January | 207.1 | 1.44 | 299.0 |
| February | February | 208.2 | 1.30 | 270.5 |
| March | March | 209.2 | 1.34 | 281.2 |
| April | April | 210.2 | 1.04 | 218.9 |
| May | May | 211.2 | 1.05 | 221.7 |
| June | June | 212.2 | 0.80 | 169.9 |
| July | July | 213.3 | 0.83 | 176.6 |
| August | August | 214.3 | 0.85 | 182.8 |
| September | September | 215.3 | 0.63 | 135.2 |
| October | October | 216.3 | 0.70 | 151.5 |
| November | November | 217.4 | 0.85 | 185.3 |
| December | December | 218.4 | 1.16 | 253.2 |
df3 <- data.frame(t = t3, Ventas = ventas3, Mes = factor(rep(month.name, 3), levels = month.name))
modelo_dummy <- lm(Ventas ~ t + Mes, data = df3)
tabla(as.data.frame(summary(modelo_dummy)$coefficients), digits = 3,
caption = "Regresión con variables dummy estacionales (mes base: enero)")
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 249.108 | 2.746 | 90.727 | 0 |
| t | 1.017 | 0.076 | 13.467 | 0 |
| MesFebruary | -20.684 | 3.627 | -5.703 | 0 |
| MesMarch | -16.368 | 3.629 | -4.510 | 0 |
| MesApril | -73.385 | 3.633 | -20.199 | 0 |
| MesMay | -70.736 | 3.639 | -19.440 | 0 |
| MesJune | -117.753 | 3.646 | -32.299 | 0 |
| MesJuly | -112.438 | 3.654 | -30.768 | 0 |
| MesAugust | -107.122 | 3.664 | -29.233 | 0 |
| MesSeptember | -151.139 | 3.676 | -41.114 | 0 |
| MesOctober | -135.490 | 3.689 | -36.725 | 0 |
| MesNovember | -108.507 | 3.704 | -29.295 | 0 |
| MesDecember | -49.858 | 3.720 | -13.402 | 0 |
nuevo <- data.frame(t = 37:48, Mes = factor(month.name, levels = month.name))
pron_dummy <- predict(modelo_dummy, newdata = nuevo)
tabla(data.frame(Mes = month.name, `Pronóstico (Regresión Dummy)` = round(pron_dummy, 1), check.names = FALSE),
caption = "Pronóstico Año 4 - Regresión con variables ficticias")
| Mes | Pronóstico (Regresión Dummy) |
|---|---|
| January | 286.8 |
| February | 267.1 |
| March | 272.4 |
| April | 216.4 |
| May | 220.1 |
| June | 174.1 |
| July | 180.4 |
| August | 186.8 |
| September | 143.8 |
| October | 160.4 |
| November | 188.4 |
| December | 248.1 |
resumen_3 <- data.frame(
Mes = month.name,
`Descomposición` = round(pron_decomp, 1),
`Regresión Dummy` = round(as.numeric(pron_dummy), 1),
check.names = FALSE
)
tabla(resumen_3, caption = "Pronósticos Año 4 - Vintage Restaurant (ambos métodos)")
| Mes | Descomposición | Regresión Dummy | |
|---|---|---|---|
| January | January | 299.0 | 286.8 |
| February | February | 270.5 | 267.1 |
| March | March | 281.2 | 272.4 |
| April | April | 218.9 | 216.4 |
| May | May | 221.7 | 220.1 |
| June | June | 169.9 | 174.1 |
| July | July | 176.6 | 180.4 |
| August | August | 182.8 | 186.8 |
| September | September | 135.2 | 143.8 |
| October | October | 151.5 | 160.4 |
| November | November | 185.3 | 188.4 |
| December | December | 253.2 | 248.1 |
df_comp3 <- data.frame(Mes = factor(month.name, levels = month.name),
Descomposicion = pron_decomp,
RegresionDummy = as.numeric(pron_dummy)) %>%
pivot_longer(-Mes, names_to = "Metodo", values_to = "Pronostico")
ggplot(df_comp3, aes(Mes, Pronostico, color = Metodo, group = Metodo)) +
geom_line(linewidth = 1) + geom_point() +
labs(title = "Pronóstico Año 4 - Vintage Restaurant", y = "Ventas pronosticadas (miles de $)") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
real_enero <- 295
pron_decomp_enero <- pron_decomp[1]
pron_dummy_enero <- as.numeric(pron_dummy[1])
error_decomp <- real_enero - pron_decomp_enero
error_dummy <- real_enero - pron_dummy_enero
tabla(data.frame(
`Método` = c("Descomposición", "Regresión Dummy"),
`Pronóstico Enero Año 4` = round(c(pron_decomp_enero, pron_dummy_enero), 1),
`Ventas Reales` = real_enero,
`Error (Real - Pronóstico)` = round(c(error_decomp, error_dummy), 1),
`Error %` = round(c(error_decomp, error_dummy) / real_enero * 100, 2),
check.names = FALSE
), caption = "Error de pronóstico - enero Año 4")
| Método | Pronóstico Enero Año 4 | Ventas Reales | Error (Real - Pronóstico) | Error % | |
|---|---|---|---|---|---|
| January | Descomposición | 299.0 | 295 | -4.0 | -1.36 |
| Regresión Dummy | 286.8 | 295 | 8.2 | 2.80 |
La serie de ventas de Vintage Restaurant combina una tendencia de crecimiento clara con una estacionalidad muy marcada (temporada alta en invierno/diciembre, temporada baja en septiembre). Ambos métodos de pronóstico (descomposición clásica y regresión con variables dummy) capturan razonablemente bien este patrón y arrojan pronósticos similares para el Año 4. El error de pronóstico de enero, comparado contra la venta real de $295,000, resulta relativamente moderado (unos cuantos puntos porcentuales), lo cual es esperable dado que es un solo mes y ambos modelos están sujetos a la variabilidad normal del negocio (clima, eventos puntuales, etc.).
Para reducir la incertidumbre del procedimiento de pronóstico y que Karen no se confunda con la diferencia entre el pronóstico y la venta real, se recomienda: (1) comunicar los pronósticos siempre acompañados de un rango o intervalo de confianza (no como una cifra única y exacta), (2) actualizar el modelo cada mes conforme se tenga la venta real (pronóstico “rodante”), de forma que el error de un mes no se arrastre a los siguientes, y (3) monitorear el error de pronóstico mes a mes (por ejemplo con el MAPE acumulado) para detectar a tiempo si el patrón estacional o la tendencia están cambiando y el modelo necesita reajustarse.
Carlson Department Store sufrió daños por un huracán y permaneció cerrada de septiembre a diciembre. Se busca estimar: (1) las ventas que Carlson habría tenido de no haber ocurrido el huracán, (2) las ventas que el condado habría tenido de no haber ocurrido el huracán, y (3) la pérdida de ventas de Carlson en ese periodo, además de argumentar sobre el posible exceso de ventas relacionado con el huracán.
Ventas mensuales de Carlson y ventas totales de tiendas departamentales del condado, en millones de dólares, para los 48 meses previos al huracán (septiembre Año 0 - agosto Año 4) y los 4 meses de cierre (septiembre - diciembre Año 4).
meses_fiscal <- c("Sep","Oct","Nov","Dic","Ene","Feb","Mar","Abr","May","Jun","Jul","Ago")
carlson_1 <- c(1.71, 1.90, 2.74, 4.20, 1.45, 1.80, 2.03, 1.99, 2.32, 2.20, 2.13, 2.43) # Año1: Sep-Ago
carlson_2 <- c(1.90, 2.13, 2.56, 4.16, 2.31, 1.89, 2.02, 2.23, 2.39, 2.14, 2.27, 2.21) # Año2
carlson_3 <- c(1.89, 2.29, 2.83, 4.04, 2.31, 1.99, 2.42, 2.45, 2.57, 2.42, 2.40, 2.50) # Año3
carlson_4 <- c(2.09, 2.54, 2.97, 4.35, 2.56, 2.28, 2.69, 2.48, 2.73, 2.37, 2.31, 2.23) # Año4 (pre-huracán, Sep Año3-Ago Año4)
carlson_48 <- c(carlson_1, carlson_2, carlson_3, carlson_4)
condado_1 <- c(55.8, 56.4, 71.4, 117.6, 46.8, 48.0, 60.0, 57.6, 61.8, 58.2, 56.4, 63.0)
condado_2 <- c(57.6, 53.4, 71.4, 114.0, 46.8, 48.6, 59.4, 58.2, 60.6, 55.2, 51.0, 58.8)
condado_3 <- c(49.8, 54.6, 65.4, 102.0, 43.8, 45.6, 57.6, 53.4, 56.4, 52.8, 54.0, 60.6)
condado_4 <- c(47.4, 54.6, 67.8, 100.2, 48.0, 51.6, 57.6, 58.2, 60.0, 57.0, 57.6, 61.8)
condado_cierre <- c(69.0, 75.0, 85.2, 121.8) # Sep, Oct, Nov, Dic del Año en que ocurrió el huracán (ventas reales, tienda cerrada)
condado_48 <- c(condado_1, condado_2, condado_3, condado_4)
t4 <- 1:48
df4 <- data.frame(t = t4, Mes = factor(rep(meses_fiscal, 4), levels = meses_fiscal),
Carlson = carlson_48, Condado = condado_48)
tabla(data.frame(Periodo = meses_fiscal, `Carlson Año4` = tail(carlson_48, 12),
`Condado Año4` = tail(condado_48, 12), check.names = FALSE),
caption = "Últimos 12 meses antes del huracán (referencia)")
| Periodo | Carlson Año4 | Condado Año4 |
|---|---|---|
| Sep | 2.09 | 47.4 |
| Oct | 2.54 | 54.6 |
| Nov | 2.97 | 67.8 |
| Dic | 4.35 | 100.2 |
| Ene | 2.56 | 48.0 |
| Feb | 2.28 | 51.6 |
| Mar | 2.69 | 57.6 |
| Abr | 2.48 | 58.2 |
| May | 2.73 | 60.0 |
| Jun | 2.37 | 57.0 |
| Jul | 2.31 | 57.6 |
| Ago | 2.23 | 61.8 |
df4_long <- df4 %>% mutate(Fecha = seq(as.Date("2000-09-01"), by = "month", length.out = 48)) %>%
pivot_longer(c(Carlson, Condado), names_to = "Serie", values_to = "Ventas")
ggplot(df4_long, aes(Fecha, Ventas, color = Serie)) +
geom_line() +
facet_wrap(~Serie, scales = "free_y", ncol = 1) +
labs(title = "Ventas mensuales Carlson vs. Condado (48 meses previos al huracán)",
y = "Ventas ($ millones)") +
theme_minimal() + theme(legend.position = "none")
Se ajusta un modelo de regresión con tendencia y variables dummy estacionales (mes) sobre los 48 meses previos al huracán, y se pronostican los 4 meses de cierre (septiembre-diciembre).
modelo_condado <- lm(Condado ~ t + Mes, data = df4)
tabla(as.data.frame(summary(modelo_condado)$coefficients), digits = 3,
caption = "Regresión de tendencia + estacionalidad - Ventas del Condado")
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | 54.400 | 1.752 | 31.058 | 0.000 |
| t | -0.092 | 0.035 | -2.640 | 0.012 |
| MesOct | 2.192 | 2.293 | 0.956 | 0.346 |
| MesNov | 16.534 | 2.294 | 7.208 | 0.000 |
| MesDic | 56.076 | 2.295 | 24.431 | 0.000 |
| MesEne | -5.932 | 2.297 | -2.582 | 0.014 |
| MesFeb | -3.740 | 2.299 | -1.626 | 0.113 |
| MesMar | 6.552 | 2.302 | 2.846 | 0.007 |
| MesAbr | 4.845 | 2.306 | 2.101 | 0.043 |
| MesMay | 7.787 | 2.310 | 3.371 | 0.002 |
| MesJun | 3.979 | 2.314 | 1.719 | 0.094 |
| MesJul | 3.021 | 2.319 | 1.302 | 0.201 |
| MesAgo | 9.413 | 2.325 | 4.049 | 0.000 |
nuevo4 <- data.frame(t = 49:52, Mes = factor(c("Sep","Oct","Nov","Dic"), levels = meses_fiscal))
condado_pron_sinh <- predict(modelo_condado, newdata = nuevo4)
tabla(data.frame(Mes = c("Sep","Oct","Nov","Dic"),
`Condado pronosticado (sin huracán)` = round(condado_pron_sinh, 2),
`Condado real (con huracán)` = condado_cierre,
`Exceso ( real - pronosticado )` = round(condado_cierre - condado_pron_sinh, 2),
check.names = FALSE),
caption = "Ventas del condado: pronosticadas (sin huracán) vs. reales (con huracán)")
| Mes | Condado pronosticado (sin huracán) | Condado real (con huracán) | Exceso ( real - pronosticado ) |
|---|---|---|---|
| Sep | 49.89 | 69.0 | 19.11 |
| Oct | 51.99 | 75.0 | 23.01 |
| Nov | 66.24 | 85.2 | 18.96 |
| Dic | 105.69 | 121.8 | 16.11 |
Se modela la relación entre las ventas de Carlson y las ventas del condado (ambas comparten la tendencia y estacionalidad del mercado local), mediante una regresión de Carlson en función de las ventas del condado y el tiempo.
modelo_carlson <- lm(Carlson ~ Condado + t, data = df4)
tabla(as.data.frame(summary(modelo_carlson)$coefficients), digits = 4,
caption = "Regresión de ventas Carlson en función de ventas del Condado y tendencia")
| Estimate | Std. Error | t value | Pr(>|t|) | |
|---|---|---|---|---|
| (Intercept) | -0.0655 | 0.1301 | -0.5036 | 0.617 |
| Condado | 0.0357 | 0.0018 | 19.5452 | 0.000 |
| t | 0.0138 | 0.0021 | 6.6401 | 0.000 |
nuevo4_carlson <- data.frame(t = 49:52, Condado = condado_pron_sinh)
carlson_pron_sinh <- predict(modelo_carlson, newdata = nuevo4_carlson)
tabla(data.frame(Mes = c("Sep","Oct","Nov","Dic"),
`Carlson pronosticado (sin huracán)` = round(carlson_pron_sinh, 3),
`Carlson real (tienda cerrada)` = 0,
check.names = FALSE),
caption = "Ventas de Carlson pronosticadas (escenario sin huracán)")
| Mes | Carlson pronosticado (sin huracán) | Carlson real (tienda cerrada) |
|---|---|---|
| Sep | 2.39 | 0 |
| Oct | 2.48 | 0 |
| Nov | 3.00 | 0 |
| Dic | 4.42 | 0 |
perdida_total <- sum(carlson_pron_sinh)
tabla(data.frame(Mes = c("Sep","Oct","Nov","Dic", "TOTAL"),
`Ventas perdidas ($ millones)` = c(round(carlson_pron_sinh, 3), round(perdida_total, 3)),
check.names = FALSE),
caption = "Pérdida de ventas estimada - Carlson Department Store")
| Mes | Ventas perdidas ($ millones) | |
|---|---|---|
| 1 | Sep | 2.39 |
| 2 | Oct | 2.48 |
| 3 | Nov | 3.00 |
| 4 | Dic | 4.42 |
| TOTAL | 12.30 |
La pérdida de ventas estimada para Carlson Department Store durante los cuatro meses que permaneció cerrada asciende a 12.3 millones de dólares.
exceso_total <- sum(condado_cierre - condado_pron_sinh)
tabla(data.frame(
Mes = c("Sep","Oct","Nov","Dic","TOTAL"),
`Condado real` = c(condado_cierre, sum(condado_cierre)),
`Condado pronosticado (sin huracán)` = c(round(condado_pron_sinh,2), round(sum(condado_pron_sinh),2)),
`Exceso de ventas del condado` = c(round(condado_cierre - condado_pron_sinh, 2), round(exceso_total, 2)),
check.names = FALSE
), caption = "Exceso de ventas del condado atribuible a la actividad post-huracán")
| Mes | Condado real | Condado pronosticado (sin huracán) | Exceso de ventas del condado | |
|---|---|---|---|---|
| 1 | Sep | 69.0 | 49.89 | 19.11 |
| 2 | Oct | 75.0 | 51.99 | 23.01 |
| 3 | Nov | 85.2 | 66.24 | 18.96 |
| 4 | Dic | 121.8 | 105.69 | 16.11 |
| TOTAL | 351.0 | 273.80 | 77.20 |
Las ventas reales de las tiendas departamentales del condado durante septiembre-diciembre (351 millones) superan de forma consistente en los cuatro meses al pronóstico de lo que habrían sido sin el huracán (273.8 millones), generando un exceso estimado de 77.2 millones de dólares en el condado. Esto es consistente con la inyección de más de $8,000 millones en ayuda federal y pagos de seguros que documenta el caso: ese dinero se tradujo en mayor actividad comercial (reconstrucción, reposición de bienes, etc.) que benefició a las demás tiendas departamentales que sí permanecieron abiertas.
Este hallazgo sustenta el argumento a favor de que sí hubo un exceso de ventas relacionado con el huracán en el condado — pero es clave notar que Carlson no participó de ese exceso porque estuvo cerrada; de hecho, es razonable argumentar ante la aseguradora que la pérdida de Carlson debería incluir no solo sus ventas “normales” esperadas, sino también una porción proporcional de ese exceso de actividad comercial que sus competidores sí captaron mientras ella estaba fuera de operación (venta que Carlson habría tenido la oportunidad de captar de no haber estado cerrada). Por ello, la estimación de pérdida de la sección anterior (basada únicamente en la relación histórica Carlson-condado antes del huracán) debe considerarse un piso conservador de la pérdida real de Carlson.
A lo largo de los cuatro ejercicios se compararon distintos modelos de pronóstico de series de tiempo: modelos simples sin tendencia ni estacionalidad (Naive, promedios móviles simple y ponderado), adecuados para series estables de corto plazo como la del Ejercicio 1; y modelos que incorporan tendencia y estacionalidad (descomposición clásica y regresión con variables dummy), necesarios para series con estos patrones más complejos como Lechitas, Vintage Restaurant y Carlson Department Store. La elección del modelo correcto depende siempre de las características propias de cada serie (tendencia, estacionalidad, variabilidad), y su desempeño debe evaluarse con métricas de error como el MAE, MSE, RMSE y, sobre todo, el MAPE cuando se busca comparabilidad entre series de distinta escala.