Este informe desarrolla un ejercicio de pronóstico aplicando modelos
SARIMA (vía metodología manual de Box-Jenkins y vía
generación automática con auto.arima) y modelos de
Holt-Winters, sobre tres series económicas de El
Salvador publicadas por la Secretaría Ejecutiva del Consejo Monetario
Centroamericano (SECMCA):
Para cada serie se generan pronósticos hasta diciembre de 2026 con los tres enfoques, y posteriormente se evalúa su desempeño predictivo mediante validación cruzada de un paso adelante con origen móvil (rolling-origin one-step cross-validation), calculando el MAPE (Error Porcentual Absoluto Medio) de cada modelo.
pib <- c(
4227.33,4436.39,4312.85,4625.05,4375.00,4579.71,4498.45,4994.77,
4727.87,5163.09,5032.40,5360.43,5176.52,5401.29,5225.97,5582.38,
5208.33,5550.62,5428.02,5803.99,5434.08,5701.62,5589.75,5868.02,
5617.57,5870.22,5828.24,6122.20,5695.10,6181.80,5962.28,6352.24,
5934.25,6271.06,6150.90,6622.97,6158.89,6529.02,6480.43,6852.52,
6402.12,6714.13,6691.76,7073.12,6463.49,5377.69,6119.98,6960.03,
6748.64,7208.52,7239.80,7846.18,7556.38,7957.48,7930.34,8425.91,
7913.51,8422.35,8372.77,8856.80,8301.40,8728.83,8539.41,9310.10,
8657.02,9134.58,9089.93,9826.57
)
y_pib <- ts(pib, start = c(2009, 1), frequency = 4)
plot(y_pib, main = "PIB trimestral a precios corrientes - El Salvador",
ylab = "Millones de US$", xlab = "Año", col = "steelblue4", lwd = 2)
grid()
La serie muestra una tendencia creciente clara, estacionalidad trimestral (el cuarto trimestre es sistemáticamente el más alto) y una caída marcada en el segundo trimestre de 2020 por la pandemia.
Paso 1 - Prueba de estacionariedad. Se aplica la prueba Dickey-Fuller Aumentada (ADF) sobre la serie en niveles:
adf.test(y_pib)
##
## Augmented Dickey-Fuller Test
##
## data: y_pib
## Dickey-Fuller = -1.4482, Lag order = 4, p-value = 0.7993
## alternative hypothesis: stationary
cat("Diferencias regulares sugeridas:", ndiffs(y_pib), "\n")
## Diferencias regulares sugeridas: 1
cat("Diferencias estacionales sugeridas:", nsdiffs(y_pib), "\n")
## Diferencias estacionales sugeridas: 1
El p-value alto (no se rechaza H0 de raíz unitaria) confirma que la serie no es estacionaria. Las pruebas automáticas sugieren una diferencia regular (d=1) y una diferencia estacional (D=1, periodo 4).
Paso 2 - Diferenciación e identificación con ACF/PACF.
d_pib <- diff(diff(y_pib, lag = 4), lag = 1)
adf.test(na.omit(d_pib))
##
## Augmented Dickey-Fuller Test
##
## data: na.omit(d_pib)
## Dickey-Fuller = -6.3771, Lag order = 3, p-value = 0.01
## alternative hypothesis: stationary
par(mfrow = c(1, 2))
acf(ts(as.numeric(d_pib), frequency = 1), lag.max = 16,
main = "ACF - PIB diff(1) y diff estacional(4)")
pacf(ts(as.numeric(d_pib), frequency = 1), lag.max = 16,
main = "PACF - PIB diff(1) y diff estacional(4)")
Tras diferenciar, la serie ya es estacionaria (ADF rechaza H0). En la ACF se observa un único rezago significativo en el lag estacional 4 (negativo), y la PACF muestra un decaimiento gradual en los múltiplos de 4 (lags 4, 8, 12). Este patrón -ACF que corta después del rezago estacional, PACF que decae- es característico de un componente MA estacional de orden 1: SMA(1).
Paso 3 - Estimación de modelos candidatos y selección por AICc.
cand_pib <- list(
"(0,1,0)(0,1,1)[4]" = Arima(y_pib, order = c(0,1,0), seasonal = list(order = c(0,1,1), period = 4)),
"(1,1,0)(0,1,1)[4]" = Arima(y_pib, order = c(1,1,0), seasonal = list(order = c(0,1,1), period = 4)),
"(0,1,1)(0,1,1)[4]" = Arima(y_pib, order = c(0,1,1), seasonal = list(order = c(0,1,1), period = 4))
)
data.frame(Modelo = names(cand_pib),
AICc = round(sapply(cand_pib, function(m) m$aicc), 2))
## Modelo AICc
## (0,1,0)(0,1,1)[4] (0,1,0)(0,1,1)[4] 883.94
## (1,1,0)(0,1,1)[4] (1,1,0)(0,1,1)[4] 884.15
## (0,1,1)(0,1,1)[4] (0,1,1)(0,1,1)[4] 883.76
El modelo ARIMA(0,1,1)(0,1,1)[4] obtiene el menor AICc.
Paso 4 - Diagnóstico de residuos.
modelo_pib_manual <- cand_pib[["(0,1,1)(0,1,1)[4]"]]
summary(modelo_pib_manual)
## Series: y_pib
## ARIMA(0,1,1)(0,1,1)[4]
##
## Coefficients:
## ma1 sma1
## -0.2168 -0.8378
## s.e. 0.1399 0.1090
##
## sigma^2 = 62513: log likelihood = -438.68
## AIC=883.35 AICc=883.76 BIC=889.78
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE ACF1
## Training set 17.89335 236.808 123.8607 0.1330671 1.908733 0.3424359 0.009868482
checkresiduals(modelo_pib_manual)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(0,1,1)(0,1,1)[4]
## Q* = 3.4098, df = 6, p-value = 0.7559
##
## Model df: 2. Total lags used: 8
Los residuos no muestran autocorrelación significativa (prueba de Ljung-Box con p-value alto) y se distribuyen aproximadamente como ruido blanco, por lo que el modelo ARIMA(0,1,1)(0,1,1)[4] se acepta como el modelo SARIMA identificado manualmente para el PIB.
auto.arima)modelo_pib_auto <- auto.arima(y_pib, stepwise = FALSE, approximation = FALSE)
summary(modelo_pib_auto)
## Series: y_pib
## ARIMA(1,0,0)(0,1,1)[4] with drift
##
## Coefficients:
## ar1 sma1 drift
## 0.8176 -0.7784 72.0990
## s.e. 0.0870 0.1507 10.9996
##
## sigma^2 = 60944: log likelihood = -443.83
## AIC=895.66 AICc=896.34 BIC=904.29
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE
## Training set -2.09545 233.8164 124.3587 -0.2416389 1.921945 0.3438129
## ACF1
## Training set -0.07426543
checkresiduals(modelo_pib_auto)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(1,0,0)(0,1,1)[4] with drift
## Q* = 1.946, df = 6, p-value = 0.9246
##
## Model df: 2. Total lags used: 8
auto.arima selecciona ARIMA(1,0,0)(0,1,1)[4] con
deriva (drift), un modelo distinto al identificado manualmente
pero con residuos igualmente limpios.
fc_pib_manual <- forecast(modelo_pib_manual, h = 4)
fc_pib_auto <- forecast(modelo_pib_auto, h = 4)
tabla_pib_sarima <- data.frame(
Trimestre = c("2026 Q1", "2026 Q2", "2026 Q3", "2026 Q4"),
Manual = round(as.numeric(fc_pib_manual$mean), 1),
Automatico = round(as.numeric(fc_pib_auto$mean), 1)
)
knitr::kable(tabla_pib_sarima, caption = "Pronóstico PIB (millones US$) - SARIMA")
| Trimestre | Manual | Automatico |
|---|---|---|
| 2026 Q1 | 9326.7 | 9230.0 |
| 2026 Q2 | 9633.9 | 9468.1 |
| 2026 Q3 | 9612.2 | 9368.9 |
| 2026 Q4 | 10174.5 | 9905.8 |
plot(fc_pib_manual, main = "PIB - Pronóstico SARIMA manual ARIMA(0,1,1)(0,1,1)[4]",
ylab = "Millones de US$")
hw_pib <- HoltWinters(y_pib)
hw_pib
## Holt-Winters exponential smoothing with trend and additive seasonal component.
##
## Call:
## HoltWinters(x = y_pib)
##
## Smoothing parameters:
## alpha: 0.7032607
## beta : 0.01350015
## gamma: 0.2780591
##
## Coefficients:
## [,1]
## a 9407.67152
## b 69.24043
## s1 -182.13154
## s2 56.48718
## s3 -45.89580
## s4 354.54821
fc_hw_pib <- forecast(hw_pib, h = 4)
plot(fc_hw_pib, main = "PIB - Pronóstico Holt-Winters", ylab = "Millones de US$")
tabla_pib_hw <- data.frame(
Trimestre = c("2026 Q1", "2026 Q2", "2026 Q3", "2026 Q4"),
`Holt-Winters` = round(as.numeric(fc_hw_pib$mean), 1)
)
knitr::kable(tabla_pib_hw, caption = "Pronóstico PIB (millones US$) - Holt-Winters")
| Trimestre | Holt.Winters |
|---|---|
| 2026 Q1 | 9294.8 |
| 2026 Q2 | 9602.6 |
| 2026 Q3 | 9569.5 |
| 2026 Q4 | 10039.2 |
f_pib_manual <- function(x, h) forecast(Arima(x, order = c(0,1,1),
seasonal = list(order = c(0,1,1), period = 4)), h = h)
f_pib_auto <- function(x, h) forecast(auto.arima(x, seasonal = TRUE), h = h)
f_pib_hw <- function(x, h) forecast(HoltWinters(x), h = h)
e_pib_manual <- tsCV(y_pib, f_pib_manual, h = 1, initial = 48)
e_pib_auto <- tsCV(y_pib, f_pib_auto, h = 1, initial = 48)
e_pib_hw <- tsCV(y_pib, f_pib_hw, h = 1, initial = 48)
mape <- function(e, actual) mean(abs(e / actual), na.rm = TRUE) * 100
tabla_mape_pib <- data.frame(
Modelo = c("SARIMA manual", "SARIMA automático", "Holt-Winters"),
`MAPE CV (%)` = round(c(mape(e_pib_manual, y_pib), mape(e_pib_auto, y_pib),
mape(e_pib_hw, y_pib)), 3)
)
knitr::kable(tabla_mape_pib, caption = "MAPE de validación cruzada - PIB")
| Modelo | MAPE.CV…. |
|---|---|
| SARIMA manual | 2.698 |
| SARIMA automático | 4.551 |
| Holt-Winters | 2.589 |
Interpretación: con una ventana inicial de 12 años
(48 trimestres) y validación de un paso adelante sobre los 20 trimestres
restantes, los tres modelos muestran un error de pronóstico bajo (MAPE
entre 2.6% y 4.6%), lo que indica que predicen el PIB trimestral con un
margen de error aceptable. El modelo de Holt-Winters y
el SARIMA manual muestran el menor error relativo,
mientras que la versión automática de auto.arima (que
incorpora una deriva en lugar de doble diferenciación) es algo menos
precisa en este caso particular, aunque la diferencia es pequeña en
términos absolutos.
ipc <- c(
99.65,99.53,100.00,100.08,100.36,100.40,99.99,99.79,99.73,99.03,100.41,100.00,
100.44,100.57,100.89,100.71,100.50,100.96,100.98,100.81,101.11,101.78,102.24,102.13,
102.77,102.96,103.63,106.71,107.23,107.29,107.57,107.69,107.40,107.32,107.48,107.29,
107.65,108.00,108.16,108.83,108.53,107.94,107.61,107.80,108.24,108.33,108.18,108.13,
108.59,109.05,109.54,108.85,108.69,108.91,108.78,108.87,109.06,108.91,109.00,108.98,
109.51,109.72,109.99,109.47,109.72,110.15,110.77,111.04,110.91,110.97,110.40,109.50,
108.69,108.56,109.10,109.11,109.33,109.24,109.16,108.82,108.41,110.76,110.69,110.61,
110.67,110.37,110.32,110.05,110.13,110.24,110.12,109.85,109.51,109.79,109.78,109.58,
110.39,110.69,110.92,111.00,111.19,111.26,111.24,111.10,111.22,111.36,111.62,111.81,
111.96,112.05,111.93,111.97,112.11,112.26,112.42,112.71,112.76,113.02,112.82,112.30,
112.24,112.44,112.69,112.87,113.01,112.85,112.56,112.16,111.99,112.04,112.17,112.29,
112.15,112.00,112.09,111.69,111.94,112.59,112.49,111.82,111.56,111.81,111.98,112.20,
112.49,113.19,114.08,114.81,114.84,115.51,116.36,116.63,117.10,117.95,118.92,119.06,
119.79,120.73,121.71,122.32,123.43,124.47,124.99,125.56,125.87,126.76,127.63,127.77,
128.21,128.97,129.08,128.97,128.88,129.18,129.17,129.44,129.67,130.14,130.32,129.34,
129.76,130.00,130.08,130.45,130.71,131.09,131.47,130.96,130.42,130.04,129.93,129.71,
130.16,130.08,130.26,130.30,130.44,130.86,131.29,130.81,130.89,131.26,131.41,130.89,
131.00,131.60,132.18,133.12,133.74
)
y_ipc <- ts(ipc, start = c(2009, 1), frequency = 12)
plot(y_ipc, main = "Índice de Precios al Consumidor (IPC general) - El Salvador",
ylab = "Nivel del índice", xlab = "Año", col = "darkorange3", lwd = 2)
grid()
El IPC muestra una tendencia creciente moderada hasta 2021, un salto inflacionario fuerte entre 2021 y 2023 (post-pandemia / shock de precios internacionales) y una relativa estabilización posterior, sin un patrón estacional tan marcado como el PIB.
Paso 1 - Estacionariedad.
adf.test(y_ipc)
##
## Augmented Dickey-Fuller Test
##
## data: y_ipc
## Dickey-Fuller = -1.2737, Lag order = 5, p-value = 0.8805
## alternative hypothesis: stationary
cat("Diferencias regulares sugeridas:", ndiffs(y_ipc), "\n")
## Diferencias regulares sugeridas: 1
cat("Diferencias estacionales sugeridas:", nsdiffs(y_ipc), "\n")
## Diferencias estacionales sugeridas: 0
La serie no es estacionaria en niveles; se requiere una diferencia regular (d=1). La prueba automática no sugiere diferenciación estacional (D=0), aunque se explora la posible correlación estacional vía componentes AR/MA estacionales en lugar de diferenciación.
Paso 2 - Diferenciación e identificación con ACF/PACF.
d_ipc <- diff(y_ipc, lag = 1)
adf.test(na.omit(d_ipc))
##
## Augmented Dickey-Fuller Test
##
## data: na.omit(d_ipc)
## Dickey-Fuller = -4.4634, Lag order = 5, p-value = 0.01
## alternative hypothesis: stationary
par(mfrow = c(1, 2))
acf(ts(as.numeric(d_ipc), frequency = 1), lag.max = 36,
main = "ACF - IPC diferenciada (d=1)")
pacf(ts(as.numeric(d_ipc), frequency = 1), lag.max = 36,
main = "PACF - IPC diferenciada (d=1)")
La serie diferenciada ya es estacionaria. La ACF muestra un rezago 1 significativo positivo que decae gradualmente, y la PACF muestra un corte después del rezago 1 junto con un rezago leve en 12 (componente estacional). Este patrón sugiere un componente AR(1) regular y un posible componente estacional MA(1) de periodo 12, además de una posible deriva (drift) dado el comportamiento de tendencia de la inflación acumulada.
Paso 3 - Estimación de modelos candidatos y selección por AICc.
cand_ipc <- list(
"(1,1,1)(0,0,1)[12]" = Arima(y_ipc, order = c(1,1,1), seasonal = list(order = c(0,0,1), period = 12)),
"(1,1,0)(0,0,1)[12]+drift" = Arima(y_ipc, order = c(1,1,0), seasonal = list(order = c(0,0,1), period = 12), include.drift = TRUE),
"(1,1,1)(1,0,1)[12]+drift" = Arima(y_ipc, order = c(1,1,1), seasonal = list(order = c(1,0,1), period = 12), include.drift = TRUE),
"(2,1,1)(0,0,1)[12]+drift" = Arima(y_ipc, order = c(2,1,1), seasonal = list(order = c(0,0,1), period = 12), include.drift = TRUE)
)
data.frame(Modelo = names(cand_ipc),
AICc = round(sapply(cand_ipc, function(m) m$aicc), 2))
## Modelo AICc
## (1,1,1)(0,0,1)[12] (1,1,1)(0,0,1)[12] 264.30
## (1,1,0)(0,0,1)[12]+drift (1,1,0)(0,0,1)[12]+drift 258.26
## (1,1,1)(1,0,1)[12]+drift (1,1,1)(1,0,1)[12]+drift 262.13
## (2,1,1)(0,0,1)[12]+drift (2,1,1)(0,0,1)[12]+drift 260.41
El modelo ARIMA(1,1,0)(0,0,1)[12] con deriva obtiene el menor AICc.
Paso 4 - Diagnóstico de residuos.
modelo_ipc_manual <- cand_ipc[["(1,1,0)(0,0,1)[12]+drift"]]
summary(modelo_ipc_manual)
## Series: y_ipc
## ARIMA(1,1,0)(0,0,1)[12] with drift
##
## Coefficients:
## ar1 sma1 drift
## 0.2857 0.2205 0.1650
## s.e. 0.0670 0.0735 0.0516
##
## sigma^2 = 0.1971: log likelihood = -125.03
## AIC=258.07 AICc=258.26 BIC=271.42
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE
## Training set 0.001738745 0.4396687 0.2978318 -0.000594307 0.2637463 0.1372658
## ACF1
## Training set -0.006939247
checkresiduals(modelo_ipc_manual)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(1,1,0)(0,0,1)[12] with drift
## Q* = 33.65, df = 22, p-value = 0.0533
##
## Model df: 2. Total lags used: 24
La prueba de Ljung-Box se ubica en el límite de significancia (p ≈ 0.05), lo que indica que queda una autocorrelación residual leve, esperable dada la complejidad del comportamiento inflacionario reciente; sin embargo, el modelo captura adecuadamente la mayor parte de la estructura de la serie y se adopta como el modelo manual identificado.
auto.arima)modelo_ipc_auto <- auto.arima(y_ipc, stepwise = FALSE, approximation = FALSE)
summary(modelo_ipc_auto)
## Series: y_ipc
## ARIMA(1,1,0)(0,0,1)[12] with drift
##
## Coefficients:
## ar1 sma1 drift
## 0.2857 0.2205 0.1650
## s.e. 0.0670 0.0735 0.0516
##
## sigma^2 = 0.1971: log likelihood = -125.03
## AIC=258.07 AICc=258.26 BIC=271.42
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE
## Training set 0.001738745 0.4396687 0.2978318 -0.000594307 0.2637463 0.1372658
## ACF1
## Training set -0.006939247
checkresiduals(modelo_ipc_auto)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(1,1,0)(0,0,1)[12] with drift
## Q* = 33.65, df = 22, p-value = 0.0533
##
## Model df: 2. Total lags used: 24
auto.arima selecciona ARIMA(1,1,0)(0,0,1)[12]
con deriva, exactamente la misma especificación obtenida con el
procedimiento manual de identificación, lo que valida la elección hecha
a partir del análisis visual de la ACF/PACF.
fc_ipc_manual <- forecast(modelo_ipc_manual, h = 7)
fc_ipc_auto <- forecast(modelo_ipc_auto, h = 7)
tabla_ipc_sarima <- data.frame(
Mes = format(seq(as.Date("2026-06-01"), by = "month", length.out = 7), "%b-%Y"),
Manual = round(as.numeric(fc_ipc_manual$mean), 2),
Automatico = round(as.numeric(fc_ipc_auto$mean), 2)
)
knitr::kable(tabla_ipc_sarima, caption = "Pronóstico IPC general - SARIMA")
| Mes | Manual | Automatico |
|---|---|---|
| Jun-2026 | 134.08 | 134.08 |
| Jul-2026 | 134.33 | 134.33 |
| Aug-2026 | 134.40 | 134.40 |
| Sep-2026 | 134.58 | 134.58 |
| Oct-2026 | 134.82 | 134.82 |
| Nov-2026 | 135.00 | 135.00 |
| Dec-2026 | 135.02 | 135.02 |
plot(fc_ipc_manual, main = "IPC - Pronóstico SARIMA manual ARIMA(1,1,0)(0,0,1)[12]+drift",
ylab = "Nivel del índice")
hw_ipc <- HoltWinters(y_ipc)
hw_ipc
## Holt-Winters exponential smoothing with trend and additive seasonal component.
##
## Call:
## HoltWinters(x = y_ipc)
##
## Smoothing parameters:
## alpha: 0.840539
## beta : 0.1002815
## gamma: 1
##
## Coefficients:
## [,1]
## a 133.20099403
## b 0.21733518
## s1 0.59328915
## s2 0.41368130
## s3 -0.14392994
## s4 -0.35677944
## s5 -0.22836291
## s6 -0.19890078
## s7 -0.65293483
## s8 -0.38647214
## s9 -0.05890413
## s10 0.24468592
## s11 0.48181483
## s12 0.53900597
fc_hw_ipc <- forecast(hw_ipc, h = 7)
plot(fc_hw_ipc, main = "IPC - Pronóstico Holt-Winters", ylab = "Nivel del índice")
tabla_ipc_hw <- data.frame(
Mes = format(seq(as.Date("2026-06-01"), by = "month", length.out = 7), "%b-%Y"),
`Holt-Winters` = round(as.numeric(fc_hw_ipc$mean), 2)
)
knitr::kable(tabla_ipc_hw, caption = "Pronóstico IPC general - Holt-Winters")
| Mes | Holt.Winters |
|---|---|
| Jun-2026 | 134.01 |
| Jul-2026 | 134.05 |
| Aug-2026 | 133.71 |
| Sep-2026 | 133.71 |
| Oct-2026 | 134.06 |
| Nov-2026 | 134.31 |
| Dec-2026 | 134.07 |
f_ipc_manual <- function(x, h) forecast(Arima(x, order = c(1,1,0),
seasonal = list(order = c(0,0,1), period = 12), include.drift = TRUE), h = h)
f_ipc_auto <- function(x, h) forecast(auto.arima(x, seasonal = TRUE), h = h)
f_ipc_hw <- function(x, h) forecast(HoltWinters(x), h = h)
e_ipc_manual <- tsCV(y_ipc, f_ipc_manual, h = 1, initial = 150)
e_ipc_auto <- tsCV(y_ipc, f_ipc_auto, h = 1, initial = 150)
e_ipc_hw <- tsCV(y_ipc, f_ipc_hw, h = 1, initial = 150)
tabla_mape_ipc <- data.frame(
Modelo = c("SARIMA manual", "SARIMA automático", "Holt-Winters"),
`MAPE CV (%)` = round(c(mape(e_ipc_manual, y_ipc), mape(e_ipc_auto, y_ipc),
mape(e_ipc_hw, y_ipc)), 3)
)
knitr::kable(tabla_mape_ipc, caption = "MAPE de validación cruzada - IPC")
| Modelo | MAPE.CV…. |
|---|---|
| SARIMA manual | 0.250 |
| SARIMA automático | 0.267 |
| Holt-Winters | 0.290 |
Interpretación: los tres modelos logran un MAPE extremadamente bajo (entre 0.25% y 0.29%), lo cual es coherente con la naturaleza del IPC: es un índice de variación suave y persistente mes a mes, por lo que un pronóstico de un paso adelante (basado en el valor previo más un pequeño ajuste) acierta con gran precisión. El SARIMA manual (idéntico al automático) resulta el más preciso, seguido muy de cerca por la versión automática, y Holt-Winters queda levemente por detrás, aunque las diferencias entre los tres modelos son mínimas en términos prácticos.
hidro <- c(
101.71,93.97,149.53,96.28,
112.07,128.51,128.47,167.53,139.48,143.59,116.46,140.00,152.20,169.09,136.08,120.74,
112.82,97.10,131.95,143.68,162.51,107.81,132.04,127.23,106.52,129.90,127.78,114.39,
125.77,100.54,111.09,65.87,29.79,53.36,75.73,57.25,71.73,74.83,91.58,81.36,
124.39,108.04,145.58,113.48,138.09,164.20,170.94,146.58,146.68,174.96,172.97,200.09,
150.78,180.91,265.99,275.49,188.09,264.20,305.72,200.63,224.95,220.88,247.63,147.73,
223.36,202.00,203.09,185.03,207.53,225.29,188.08,229.80,240.41,205.84,186.69,190.69,
176.43,189.96,215.51,227.91,227.68,203.01,177.83,211.79,147.99,198.96,162.72,179.72,
209.13,158.97,192.69,200.53,189.13,177.54,262.91,175.30,177.24,206.81,180.84,207.55,
179.03,157.20,230.00,250.91
)
y_hidro <- ts(hidro, start = c(2017, 9), frequency = 12)
plot(y_hidro, main = "Importación de hidrocarburos (valor) - El Salvador",
ylab = "Millones de US$", xlab = "Año", col = "darkred", lwd = 2)
grid()
Esta serie es mucho más volátil: refleja directamente los precios internacionales del petróleo. Se observa la caída abrupta en 2020 (pandemia / colapso de precios del petróleo) y el fuerte repunte en 2022 (guerra en Ucrania), sin un patrón estacional claro.
Paso 1 - Estacionariedad.
adf.test(y_hidro)
##
## Augmented Dickey-Fuller Test
##
## data: y_hidro
## Dickey-Fuller = -2.1024, Lag order = 4, p-value = 0.5342
## alternative hypothesis: stationary
cat("Diferencias regulares sugeridas:", ndiffs(y_hidro), "\n")
## Diferencias regulares sugeridas: 1
cat("Diferencias estacionales sugeridas:", nsdiffs(y_hidro), "\n")
## Diferencias estacionales sugeridas: 0
Paso 2 - Diferenciación e identificación con ACF/PACF.
d_hidro <- diff(y_hidro, lag = 1)
adf.test(na.omit(d_hidro))
##
## Augmented Dickey-Fuller Test
##
## data: na.omit(d_hidro)
## Dickey-Fuller = -4.7872, Lag order = 4, p-value = 0.01
## alternative hypothesis: stationary
par(mfrow = c(1, 2))
acf(ts(as.numeric(d_hidro), frequency = 1), lag.max = 30,
main = "ACF - Hidrocarburos diferenciada (d=1)")
pacf(ts(as.numeric(d_hidro), frequency = 1), lag.max = 30,
main = "PACF - Hidrocarburos diferenciada (d=1)")
La ACF muestra un único rezago significativo en el lag 1 (negativo y de magnitud considerable), mientras que la PACF decae gradualmente en los primeros rezagos. Este patrón -ACF que corta abruptamente, PACF que tail-off- es la firma clásica de un proceso MA(1). No se observa un patrón estacional claro en ningún múltiplo de 12.
Paso 3 - Estimación de modelos candidatos y selección por AICc.
cand_hidro <- list(
"(0,1,1)" = Arima(y_hidro, order = c(0,1,1)),
"(1,1,1)" = Arima(y_hidro, order = c(1,1,1)),
"(0,1,1)(0,0,1)[12]" = Arima(y_hidro, order = c(0,1,1), seasonal = list(order = c(0,0,1), period = 12)),
"(1,1,0)" = Arima(y_hidro, order = c(1,1,0))
)
data.frame(Modelo = names(cand_hidro),
AICc = round(sapply(cand_hidro, function(m) m$aicc), 2))
## Modelo AICc
## (0,1,1) (0,1,1) 1003.22
## (1,1,1) (1,1,1) 1005.22
## (0,1,1)(0,0,1)[12] (0,1,1)(0,0,1)[12] 1005.01
## (1,1,0) (1,1,0) 1015.14
El modelo ARIMA(0,1,1) -sin componente estacional- obtiene el menor AICc.
Paso 4 - Diagnóstico de residuos.
modelo_hidro_manual <- cand_hidro[["(0,1,1)"]]
summary(modelo_hidro_manual)
## Series: y_hidro
## ARIMA(0,1,1)
##
## Coefficients:
## ma1
## -0.5891
## s.e. 0.0747
##
## sigma^2 = 960.8: log likelihood = -499.55
## AIC=1003.1 AICc=1003.22 BIC=1008.37
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE ACF1
## Training set 2.738386 30.6967 24.01432 -2.163135 16.67212 0.520236 -0.03305953
checkresiduals(modelo_hidro_manual)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(0,1,1)
## Q* = 16.695, df = 20, p-value = 0.6727
##
## Model df: 1. Total lags used: 21
Los residuos no presentan autocorrelación significativa, por lo que ARIMA(0,1,1) se adopta como el modelo manual para esta serie.
auto.arima)modelo_hidro_auto <- auto.arima(y_hidro, stepwise = FALSE, approximation = FALSE)
summary(modelo_hidro_auto)
## Series: y_hidro
## ARIMA(2,1,0)
##
## Coefficients:
## ar1 ar2
## -0.5799 -0.3704
## s.e. 0.0928 0.0950
##
## sigma^2 = 949.5: log likelihood = -498.47
## AIC=1002.94 AICc=1003.18 BIC=1010.84
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE
## Training set 2.336257 30.36652 24.50947 -2.057824 16.88777 0.5309628
## ACF1
## Training set -0.05949318
checkresiduals(modelo_hidro_auto)
##
## Ljung-Box test
##
## data: Residuals from ARIMA(2,1,0)
## Q* = 16.065, df = 19, p-value = 0.6529
##
## Model df: 2. Total lags used: 21
auto.arima selecciona ARIMA(2,1,0), un
modelo de magnitud de ajuste casi idéntica (AICc muy similar) al
identificado manualmente, también con residuos limpios.
fc_hidro_manual <- forecast(modelo_hidro_manual, h = 8)
fc_hidro_auto <- forecast(modelo_hidro_auto, h = 8)
tabla_hidro_sarima <- data.frame(
Mes = format(seq(as.Date("2026-05-01"), by = "month", length.out = 8), "%b-%Y"),
Manual = round(as.numeric(fc_hidro_manual$mean), 1),
Automatico = round(as.numeric(fc_hidro_auto$mean), 1)
)
knitr::kable(tabla_hidro_sarima, caption = "Pronóstico Hidrocarburos (millones US$) - SARIMA")
| Mes | Manual | Automatico |
|---|---|---|
| May-2026 | 220 | 211.8 |
| Jun-2026 | 220 | 226.7 |
| Jul-2026 | 220 | 232.6 |
| Aug-2026 | 220 | 223.7 |
| Sep-2026 | 220 | 226.7 |
| Oct-2026 | 220 | 228.2 |
| Nov-2026 | 220 | 226.2 |
| Dec-2026 | 220 | 226.8 |
plot(fc_hidro_manual, main = "Hidrocarburos - Pronóstico SARIMA manual ARIMA(0,1,1)",
ylab = "Millones de US$")
Nótese que el modelo manual ARIMA(0,1,1), al no tener componente autorregresivo, produce un pronóstico plano (constante) para todos los horizontes más allá del primer paso, mientras que el modelo automático ARIMA(2,1,0) sí captura cierta oscilación adicional gracias a su componente AR(2).
hw_hidro <- HoltWinters(y_hidro)
hw_hidro
## Holt-Winters exponential smoothing with trend and additive seasonal component.
##
## Call:
## HoltWinters(x = y_hidro)
##
## Smoothing parameters:
## alpha: 0.3768447
## beta : 0
## gamma: 0.2548004
##
## Coefficients:
## [,1]
## a 212.69767168
## b 0.02680507
## s1 1.21270868
## s2 6.49909064
## s3 11.61554805
## s4 -7.08457520
## s5 -5.94296648
## s6 15.70770844
## s7 1.87947039
## s8 -2.90048868
## s9 -3.31477014
## s10 -21.89684739
## s11 15.52846005
## s12 20.63753845
fc_hw_hidro <- forecast(hw_hidro, h = 8)
plot(fc_hw_hidro, main = "Hidrocarburos - Pronóstico Holt-Winters", ylab = "Millones de US$")
tabla_hidro_hw <- data.frame(
Mes = format(seq(as.Date("2026-05-01"), by = "month", length.out = 8), "%b-%Y"),
`Holt-Winters` = round(as.numeric(fc_hw_hidro$mean), 1)
)
knitr::kable(tabla_hidro_hw, caption = "Pronóstico Hidrocarburos (millones US$) - Holt-Winters")
| Mes | Holt.Winters |
|---|---|
| May-2026 | 213.9 |
| Jun-2026 | 219.3 |
| Jul-2026 | 224.4 |
| Aug-2026 | 205.7 |
| Sep-2026 | 206.9 |
| Oct-2026 | 228.6 |
| Nov-2026 | 214.8 |
| Dec-2026 | 210.0 |
f_hidro_manual <- function(x, h) forecast(Arima(x, order = c(0,1,1)), h = h)
f_hidro_auto <- function(x, h) forecast(auto.arima(x, seasonal = TRUE), h = h)
f_hidro_hw <- function(x, h) forecast(HoltWinters(x), h = h)
e_hidro_manual <- tsCV(y_hidro, f_hidro_manual, h = 1, initial = 72)
e_hidro_auto <- tsCV(y_hidro, f_hidro_auto, h = 1, initial = 72)
e_hidro_hw <- tsCV(y_hidro, f_hidro_hw, h = 1, initial = 72)
tabla_mape_hidro <- data.frame(
Modelo = c("SARIMA manual", "SARIMA automático", "Holt-Winters"),
`MAPE CV (%)` = round(c(mape(e_hidro_manual, y_hidro), mape(e_hidro_auto, y_hidro),
mape(e_hidro_hw, y_hidro)), 3)
)
knitr::kable(tabla_mape_hidro, caption = "MAPE de validación cruzada - Hidrocarburos")
| Modelo | MAPE.CV…. |
|---|---|
| SARIMA manual | 13.127 |
| SARIMA automático | 13.127 |
| Holt-Winters | 12.266 |
Interpretación: el MAPE sube notablemente (entre 12% y 13%) respecto a las otras dos series, lo que es esperable dado que esta serie depende de precios internacionales del petróleo, altamente volátiles e impredecibles con la sola información histórica de la propia serie. Holt-Winters logra el menor error, mientras que ambas versiones de SARIMA (manual y automática) obtienen un MAPE prácticamente idéntico, lo cual sugiere que, para esta serie en particular, la elección entre un componente MA(1) puro o un AR(2) tiene un efecto marginal sobre la capacidad predictiva de un paso adelante.
resumen <- data.frame(
Serie = rep(c("PIB trimestral", "IPC general", "Hidrocarburos"), each = 3),
Modelo = rep(c("SARIMA manual", "SARIMA automático", "Holt-Winters"), times = 3),
`MAPE CV (%)` = round(c(
mape(e_pib_manual, y_pib), mape(e_pib_auto, y_pib), mape(e_pib_hw, y_pib),
mape(e_ipc_manual, y_ipc), mape(e_ipc_auto, y_ipc), mape(e_ipc_hw, y_ipc),
mape(e_hidro_manual, y_hidro), mape(e_hidro_auto, y_hidro), mape(e_hidro_hw, y_hidro)
), 3)
)
knitr::kable(resumen, caption = "Resumen de MAPE de validación cruzada por serie y modelo")
| Serie | Modelo | MAPE.CV…. |
|---|---|---|
| PIB trimestral | SARIMA manual | 2.698 |
| PIB trimestral | SARIMA automático | 4.551 |
| PIB trimestral | Holt-Winters | 2.589 |
| IPC general | SARIMA manual | 0.250 |
| IPC general | SARIMA automático | 0.267 |
| IPC general | Holt-Winters | 0.290 |
| Hidrocarburos | SARIMA manual | 13.127 |
| Hidrocarburos | SARIMA automático | 13.127 |
| Hidrocarburos | Holt-Winters | 12.266 |
auto.arima, lo que
reafirma la validez del proceso de identificación visual basado en
ACF/PACF.En términos generales, Holt-Winters resultó ser, en las tres series, igual o más preciso que los modelos SARIMA en la validación cruzada de un paso adelante, lo cual es un resultado razonable dado que estas series combinan tendencia y estacionalidad de forma relativamente estable, condiciones bajo las cuales el suavizado exponencial triple suele comportarse muy bien. Sin embargo, los modelos SARIMA -tanto el identificado manualmente como el automático- ofrecen una base estadística más rigurosa (pruebas de estacionariedad, identificación de estructura de autocorrelación, diagnóstico formal de residuos) y produjeron pronósticos consistentes con los de Holt-Winters, lo que brinda mayor confianza en las proyecciones a diciembre de 2026 presentadas en este informe.
Fuente de los datos: Secretaría Ejecutiva del Consejo Monetario Centroamericano (SECMCA), datos no ajustados estacionalmente ni por calendario para El Salvador.