Introducción

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):

  1. PIB trimestral a precios corrientes (2009Q1 - 2025Q4), en millones de US$.
  2. IPC general (enero 2009 - mayo 2026), nivel del índice.
  3. Importación de hidrocarburos (septiembre 2017 - abril 2026), en millones de US$.

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.

Serie 1: PIB trimestral a precios corrientes

Datos

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.

a) Metodología manual (Box-Jenkins) para SARIMA

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.

b) SARIMA automático (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.

Pronósticos SARIMA a diciembre 2026

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")
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$")

Holt-Winters

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")
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

Validación cruzada (rolling-origin) y MAPE - PIB

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")
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.

Serie 2: IPC general

Datos

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.

a) Metodología manual (Box-Jenkins) para SARIMA

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.

b) SARIMA automático (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.

Pronósticos SARIMA a diciembre 2026

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")
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")

Holt-Winters

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")
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

Validación cruzada (rolling-origin) y MAPE - IPC

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")
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.

Serie 3: Importación de hidrocarburos

Datos

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.

a) Metodología manual (Box-Jenkins) para SARIMA

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.

b) SARIMA automático (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.

Pronósticos SARIMA a diciembre 2026

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")
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).

Holt-Winters

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")
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

Validación cruzada (rolling-origin) y MAPE - Hidrocarburos

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")
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 comparativo y conclusiones

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")
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

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.