Modelos ARMA: generalizaciones y resumen.

Modelos ARIMA

En algunos casos, la serie de datos \(x_t\) no es estacionaria, y no alcanza con “sacarle el trend” para volverla estacionaria.

Ejemplo: consideremos el paseo al azar:

\[x_{t} = x_{t-1} + \delta + w_t\]

con \(w_t\) ruido blanco de varianza \(\sigma^2_w\). Este proceso no es estacionario (tiene una tendencia \(\delta t\) de pendiente \(\delta\)).

## Simulación de un paseo al azar con deriva
delta = 0.1
w = np.random.normal(loc=0,scale=1,size=2000)
x = np.cumsum(w+delta)
x = pd.Series(x)
x.plot()
plt.title(f"Paseo al azar con deriva δ = {delta} y varianza del ruido unitaria");
../_images/7d28bbb4fdbf63046d19af2482a132a95ee8f5450df9f3c501a97f1a8ddcdc55.png

Idea 1: Ajuste lineal

\[x_t = \beta_0 + \beta_1 t + w_t\]

con \(w_t\) ruido blanco gaussiano.

from statsmodels.formula.api import ols
fit = ols("x~x.index.values", data=x).fit()
fit.summary()
OLS Regression Results
Dep. Variable: x R-squared: 0.956
Model: OLS Adj. R-squared: 0.956
Method: Least Squares F-statistic: 4.346e+04
Date: Mon, 19 May 2025 Prob (F-statistic): 0.00
Time: 16:23:27 Log-Likelihood: -7366.5
No. Observations: 2000 AIC: 1.474e+04
Df Residuals: 1998 BIC: 1.475e+04
Df Model: 1
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 4.8141 0.430 11.183 0.000 3.970 5.658
x.index.values 0.0777 0.000 208.476 0.000 0.077 0.078
Omnibus: 226.571 Durbin-Watson: 0.011
Prob(Omnibus): 0.000 Jarque-Bera (JB): 63.079
Skew: 0.048 Prob(JB): 2.01e-14
Kurtosis: 2.135 Cond. No. 2.31e+03


Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 2.31e+03. This might indicate that there are
strong multicollinearity or other numerical problems.
x.plot()
fit.fittedvalues.plot();
../_images/4c2506051c9376498dd0e21e75f91879b4bdac4495d45c5acb2e2a33e3fb7357.png

Miremos los residuos del ajuste:

fit.resid.plot();
plt.title("Residuos del ajuste");
../_images/3da874e6d4b23edf5cc26a2b02b3fb418c60cb7a33f8e659251c3dddaf10a1e9.png
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
plot_acf(fit.resid, bartlett_confint=False);
../_images/bdf24ddbbc29cc6939f3e3d7264e4dac6185737d5c634b56a3cc085be0a8f398.png

Mejor idea: tomar diferencias de la serie.

Sea:

\[y_t = x_t - x_{t-1} = x_{t-1} + \delta + w_t - x_{t-1} = \delta +w_t\]

Es decir, al tomar diferencia la serie se vuelve ruido blanco puro en este caso (estacionario), a menos de la media.

y = x.diff().dropna() #diferencio la serie en pandas directamente. El dropna es para eliminar el primer valor que no tengo.
y.plot()
plt.title(f"Serie de diferencias, media = {np.mean(y)}");
../_images/4e3b6083f703b74023ef630ceeb0586b3921b197bfbfe963edfc26560ca32ec6.png

Miremos ahora la autocorrelación de las diferencias:

plot_acf(y,bartlett_confint=False);
../_images/1e245b2fb6fe8114f5ddf832bbe2eb98067246ed740c0db5f7f958a9a89f8450.png

Y también chequeamos si son aproximadamente Gaussianos:

from statsmodels.graphics.api import qqplot
qqplot(y,line='s');
../_images/70c757397131812dfc76d9cd3e6d34253a2766886ef693bc65646120d186c75e.png

Ejemplo no tan ejemplo…

En data/eur_vs_usd.csv esta la variación diaria del precio del euro frente al del dólar. Analicemos esta serie.

eur = pd.read_csv("../data/eur_vs_usd.csv").dropna()
eur = pd.Series(eur["US dollar/Euro (EXR.D.USD.EUR.SP00.A)"].values,index=eur["DATE"])
eur.plot();
../_images/1fcb0f92214fae8ce918629a41f634f4b9566f744d2adbea6868c6b2c03e479f.png

Miremos la autocorrelación y vemos que no es estacionaria.

plot_acf(eur, bartlett_confint=False);
../_images/c0ba960ad31eb556f68be4c1a22465353bcd0b6c348cc6f5815382dbbfdbce26.png

Probemos ahora el truco de tomar diferencias:

y = eur.diff().dropna()
y.plot();
plt.title(f"Media de las diferencias {np.mean(y)} +- {2*np.std(y)/np.sqrt(y.size)}");
../_images/1e919daece282c461faa56527d5009c71cb7596d7d26677ac210ac92d1f5b09e.png

Miremos la autocorrelación de la serie diferenciada, se asemeja a ruido blanco:

plot_acf(y);
../_images/8881b4c70824975e913bfa24837b6f2c0e8d9cc1145f5850137efeb81a35a6e6.png

Sin embargo, la serie no es estacionaria. Presenta momentos de mayor y menor varianza (volatilidad). En particular la distribución se aleja de una Gaussiana.

qqplot(y,line='s');
../_images/8892a3d68d8f2c057b1fbc66d562c5d05572e1ea651dd920ba7be60771eaf446.png

Interpretación:

  • A corto plazo, la serie se comporta como un paseo al azar sin deriva (la media de las diferencias da \(0\)).

  • Pero a largo plazo, la serie presenta heterocedasticidad, palabra difícil para decir varianza variable.

Ejemplo: serie con trend lineal.

Supongamos que: $\(x_t = \mu_t + y_t\)$

con \(\mu_t = \beta_0 + \beta_1 t\) e \(y_t\) estacionario. Entonces (introduciendo el operador \(\nabla\) para las diferencias):

\[\nabla x_t = x_{t}- x_{t-1} = \mu_t + y_t - \mu_{t-1} - y_{t-1} = \beta_1 + \nabla y_t.\]

Es decir, la nueva serie “diferenciada” tiene una media \(\beta_1\) que sale de la pendiente de la recta de tendencia y una componente estacionaria compuesta por las diferencias de la serie \(y\) anterior.

Nota: en general, si el “trend” es un polinomio de grado \(n\), diferenciar \(n\) veces elimina el trend.

Modelo ARIMA

Esto lleva a la definición del modelo \(ARIMA(p,d,q)\) genérico.

Definición: decimos que una serie \(x_t\) es \(ARIMA(p,d,q)\) si: $\(y_t = \nabla^d x_t\)$

es un \(ARMA(p,q)\). Es decir, si diferenciar la serie \(d\) veces produce un proceso \(ARMA\). El nuevo parámetro \(d\) es la cantidad de veces a diferenciar.

Ajuste de modelos ARIMA

Para el ajuste de modelos ARIMA, simplemente podemos reutilizar todo lo visto para \(ARMA(p,q)\) una vez que sabemos cuántas veces hay que diferenciar (\(d\)). O sea, lo único nuevo es “descubrir” el \(d\) necesario para que la serie quede estacionaria. Típicamente no se usa \(d\) muy grande.

Ejemplo

Consideremos que tenemos una serie \(x_t\) de la forma:

\[x_t = \beta_0 + \beta_1 t + y_t\]

con \(y_t\) tal que \(z_t = \nabla y_t\) es autorregresivo de orden \(1\) y coeficiente \(\phi\).

from statsmodels.tsa.api import arma_generate_sample
n=500
beta0 = 50
beta1 = 3
phi=0.9
z = arma_generate_sample([1,-phi],[1],n)
y = np.cumsum(z) #Esto genera la serie y a partir de la x, acumulando que es la operación contraria a diferenciar.
x = beta0+beta1*np.arange(0,n)+y
x = pd.Series(x)
x.plot();
../_images/c5108d89c01bf5628f14372f122951f706ac8871508e6d975935694fd1c2540b.png

Analicemos ahora el proceso \(\nabla x\), es decir las diferencias.

#Al diferenciar recupero z pero con media beta1.
dx = x.diff().dropna()
dx.plot()
plt.title(f"Proceso de diferencias, μ = {np.mean(dx)}");
../_images/4d2587bc04904454b360d3f43c0efb634909c7a826d2558608183abe21615940.png
#las correlaciones confirman la estacionariedad (parece un AR(1) en este caso)
plot_pacf(dx);
../_images/78a879d403e3e7221d15d5e66c894e969f4dd7d782bce31b38ac0ceee9976a05.png
##Ajusto directo un ARIMA a la serie x usando statsplots
## el parámetro trend="t" permite que la serie diferenciada conserve el drift 
## la media del resultado de diferenciar
from statsmodels.tsa.api import ARIMA

fit = ARIMA(x,order=(1,1,0), trend="t").fit()
fit.summary()
SARIMAX Results
Dep. Variable: y No. Observations: 500
Model: ARIMA(1, 1, 0) Log Likelihood -700.929
Date: Mon, 19 May 2025 AIC 1407.859
Time: 16:24:43 BIC 1420.497
Sample: 0 HQIC 1412.818
- 500
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
x1 3.0749 0.353 8.721 0.000 2.384 3.766
ar.L1 0.8740 0.023 38.628 0.000 0.830 0.918
sigma2 0.9690 0.064 15.148 0.000 0.844 1.094
Ljung-Box (L1) (Q): 0.33 Jarque-Bera (JB): 2.49
Prob(Q): 0.57 Prob(JB): 0.29
Heteroskedasticity (H): 0.95 Skew: -0.16
Prob(H) (two-sided): 0.73 Kurtosis: 2.87


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).

Analicemos los residuos del ajuste:

res = fit.resid.iloc[1:] #saco el primer residuo porque no tiene sentido luego de diferenciar
res.plot();
../_images/0ddf1b9ee943dab650f8b8ff7ec33cc9474130ae8292519f3876fcd595fbe915.png
plot_acf(res, bartlett_confint=False);
../_images/0b058402d8c218d9e1e7bc619bc0cc81ed4585cb414e01812faa36f5c057f945.png
qqplot(res,line="s");
../_images/fd1b9593360b24fd42a53b6e2b44ebfc437e9d5ff08214d3e738663a4a0678a5.png
#Usamos el modelo ajustado para predecir a futuro
h = 50 #horizonte de predicción
predicciones = fit.get_prediction(start=x.size,end=x.size+h)
xhat = predicciones.predicted_mean
confint = predicciones.conf_int(alpha=0.05) #alpha es la confianza del intervalo
x.plot()
xhat.plot()
plt.legend(["Observado","Predicción"])
plt.fill_between(xhat.index,confint["lower y"], confint["upper y"], alpha=0.2);
plt.title(f"Predicción de la serie x a {h} muestras");
../_images/628826a7140a6aaf761259239bf6acea742c4349f182b0a0cd6abde2b65ea5bf.png

Resumen: ARMA, ARIMA, ajuste, predicción.

Con todo lo visto, hemos construido una especie de receta (debida a Box-Jenkins) para trabajar con este tipo de modelos, a saber:

  • Graficar los datos

  • Trasnformar los datos (por ejemplo, transformación logarítmica, o detrend o ambos, diferenciación).

  • Identificar los órdenes de dependencia (acf, pacf).

  • Estimación de parámetros (fit, básicamente mínimos cuadrados o máxima verosimilitud).

  • Diagnóstico (análisis de residuos por ejemplo).

  • Elección del modelo (criterios de información tipo AIC, evitar overfitting, etc.)

  • Predicción en base a estimadores lineales calculados recursivamente e intervalos de confianza.

Ejemplo completo: la serie GNP

Esta serie contiene el Producto Nacional Bruto (GNP) de Estados Unidos medido trimestralmente desde 1947 a 2002.

gnp = astsa.gnp
gnp.plot();
../_images/9a420649f9aed6184cc1377fefd4c10ec5de8c0dcc9baed82de7c76afe352f35.png
#Transformo por log
x=np.log(gnp)
x.plot();
../_images/6bd2d092f03cfc3640fdb6f39cf2176fe5d268750aa697cce0fbf239c2e17f69.png

Hago la ACF solo par ver que no es estacionaria

plot_acf(x, bartlett_confint=False);
../_images/13192c7aed43793be234fd1ad667f9aeb072e9eb8393b0f4678d61f5f1742f17.png

Diferencio la serie una vez y logro estacionariedad

y=x.diff().dropna()
y.plot()
plt.title(f"Serie de diferencias, μ = {np.mean(y)}");
../_images/adf77e6b0724ae003cb4c02d8ac361cb58d46fb07b7e6c05a4557bd39ae7df48.png

Analizamos ACF y PACF

plot_acf(y, bartlett_confint=False);
../_images/faaa1c7ce3750d6d8f2bc5d5be9386a35121c58c59d4a2d613c28eb25c8608e3.png
plot_pacf(y);
../_images/70dfbed55779d726a98657c2bcf410cf4e1c141d677844265cc089ecda489627.png

Mirando lo anterior podemos inclinarnos por varios modelos para \(x\):

  • ARIMA(1,1,0) ya que diferenciamos una vez y vemos que la PACF corta en 1.

  • ARIMA(0,1,2) ya que diferenciamos una vez y vemos que la ACF corta en 2.

  • Otras posibilidades a testear. Probemos y hagamos diagnóstico

Modelo 1: ARIMA(1,1,0)

fit = ARIMA(x,order=(1,1,0), trend="t").fit()
res = fit.resid[1:]; #acordarse de sacar el primero
print(f"RMSE: {np.std(res)}")
fit.summary()
RMSE: 0.009502645075030325
SARIMAX Results
Dep. Variable: value No. Observations: 223
Model: ARIMA(1, 1, 0) Log Likelihood 718.610
Date: Mon, 19 May 2025 AIC -1431.220
Time: 16:25:36 BIC -1421.012
Sample: 03-31-1947 HQIC -1427.099
- 09-30-2002
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
x1 0.0083 0.001 8.297 0.000 0.006 0.010
ar.L1 0.3481 0.055 6.288 0.000 0.240 0.457
sigma2 9.023e-05 6.47e-06 13.943 0.000 7.76e-05 0.000
Ljung-Box (L1) (Q): 0.18 Jarque-Bera (JB): 23.64
Prob(Q): 0.67 Prob(JB): 0.00
Heteroskedasticity (H): 0.19 Skew: 0.13
Prob(H) (two-sided): 0.00 Kurtosis: 4.58


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
fig, axs = plt.subplots(1, 3)
res.plot(ax=axs[0])
axs[0].title.set_text(f"RMSE: {np.std(res)}")
plot_acf(res, ax=axs[1], bartlett_confint=False)
qqplot(res,line='s', ax=axs[2]);
../_images/3a7b915d6e67ee89a94dc6de15948404a5b228cbd1a11f1b26c8520bf8ea65e3.png

Modelo 2: ARIMA(0,1,2)

fit = ARIMA(x,order=(0,1,2), trend="t").fit()
res = fit.resid[1:]; #acordarse de sacar el primero
print(f"RMSE: {np.std(res)}")
fit.summary()
RMSE: 0.00944678036519239
SARIMAX Results
Dep. Variable: value No. Observations: 223
Model: ARIMA(0, 1, 2) Log Likelihood 719.908
Date: Mon, 19 May 2025 AIC -1431.816
Time: 16:25:37 BIC -1418.206
Sample: 03-31-1947 HQIC -1426.321
- 09-30-2002
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
x1 0.0083 0.001 8.389 0.000 0.006 0.010
ma.L1 0.3067 0.054 5.649 0.000 0.200 0.413
ma.L2 0.2249 0.056 4.028 0.000 0.115 0.334
sigma2 8.92e-05 6.49e-06 13.752 0.000 7.65e-05 0.000
Ljung-Box (L1) (Q): 0.03 Jarque-Bera (JB): 21.82
Prob(Q): 0.86 Prob(JB): 0.00
Heteroskedasticity (H): 0.19 Skew: 0.13
Prob(H) (two-sided): 0.00 Kurtosis: 4.51


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
fig, axs = plt.subplots(1, 3)
res.plot(ax=axs[0])
axs[0].title.set_text(f"RMSE: {np.std(res)}")
plot_acf(res, ax=axs[1], bartlett_confint=False)
qqplot(res,line='s', ax=axs[2]);
../_images/c385f5fc3370bb08a7bfd6c3c70eb5e4028f4698f0c75854d0bae8a132b14449.png

Como vemos, dan bastante parecidos, con leve ventaja para el \(MA(2)\). Probemos combinar ambas en un ARIMA(1,1,1), es decir, media móvil de orden 1 y autorregresivo de orden 1 luego de diferenciar.

fit = ARIMA(x,order=(1,1,1), trend="t").fit()
res = fit.resid[1:]; #acordarse de sacar el primero
print(f"RMSE: {np.std(res)}")
fit.summary()
RMSE: 0.009484904399350236
SARIMAX Results
Dep. Variable: value No. Observations: 223
Model: ARIMA(1, 1, 1) Log Likelihood 719.022
Date: Mon, 19 May 2025 AIC -1430.044
Time: 16:25:38 BIC -1416.433
Sample: 03-31-1947 HQIC -1424.549
- 09-30-2002
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
x1 0.0083 0.001 7.651 0.000 0.006 0.010
ar.L1 0.4854 0.162 2.992 0.003 0.167 0.803
ma.L1 -0.1552 0.178 -0.871 0.384 -0.505 0.194
sigma2 8.994e-05 6.55e-06 13.728 0.000 7.71e-05 0.000
Ljung-Box (L1) (Q): 0.00 Jarque-Bera (JB): 22.25
Prob(Q): 0.95 Prob(JB): 0.00
Heteroskedasticity (H): 0.19 Skew: 0.15
Prob(H) (two-sided): 0.00 Kurtosis: 4.52


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
fig, axs = plt.subplots(1, 3)
res.plot(ax=axs[0])
axs[0].title.set_text(f"RMSE: {np.std(res)}")
plot_acf(res, ax=axs[1], bartlett_confint=False)
qqplot(res,line='s', ax=axs[2]);
../_images/439f3e85abeb801e679555bf8c37728a8037d37fc85d97b4447cafb534cff0a4.png

Validación de residuos: estadístico de Ljung-Box-Pierce

La serie anterior de residuos parece haber quedado “blanca”. Sin embargo, es bueno disponer de un test que permita evaluar si la ACF en su conjunto es razonablemente blanca en lugar de mirar lag a lag. Para ello se usa el estadístico de Ljung-Box-Pierce.

\[Q = n(n+2) \sum_{h=1}^H \frac{\hat{\rho}^2_e(h)}{n-h},\]

donde \(H\) es una ventana. La idea de este estadístico es acumular varias correlaciones en la ventana \(H\) para ver si en su conjunto son todas despreciables (en lugar de una a una).

El estadístico \(Q\) es asintóticamente \(\chi^2_{H-p-q}\) por lo que si el valor de \(Q\) es grande (más que el cuantil \(\alpha\) de la \(\chi^2\)) rechazamos la hipótesis de independencia.

En general lo que se hace es mirar los \(p\)-valores, es decir cuánta probabilidad queda a la derecha de \(Q\). Si es pequeño (ej: \(p<0.05\)) se rechaza la hipótesis.

from statsmodels.stats.api import acorr_ljungbox
acorr_ljungbox(res,10)
lb_stat lb_pvalue
1 0.003281 0.954324
2 1.197290 0.549556
3 1.811029 0.612538
4 3.849914 0.426699
5 8.023301 0.154956
6 8.289443 0.217656
7 8.906550 0.259435
8 9.149590 0.329834
9 9.838461 0.363720
10 10.428248 0.403758

Predicción:

Como último paso pasamos a la predicción a \(2\) años (8 trimestres):

#Usamos el modelo ajustado para predecir a futuro
h = 32 #horizonte de predicción
predicciones = fit.get_prediction(start=gnp.size,end=gnp.size+h)
xhat = predicciones.predicted_mean
confint = predicciones.conf_int(alpha=0.05) #alpha es la confianza del intervalo
x.plot()
xhat.plot()
plt.legend(["Observado","Predicción"])
plt.fill_between(xhat.index,confint["lower value"], confint["upper value"], alpha=0.2);
plt.title(f"Predicción de la serie log(gnp) a {h} trimestres");
../_images/c860c031ef7e17689592e1c4ffbfef0170a1a69f7c764a86c753db6fa14650f3.png

Ahora podemos volver a la variable original, deshaciendo la transformación \(x\to \log(x)\) mediante \(y\to \exp(y)\):

gnp.plot()
estimated_gnp = np.exp(xhat)
confint_exp = np.exp(confint)
estimated_gnp.plot()
plt.legend(["Observado","Predicción"])
plt.fill_between(xhat.index,confint_exp["lower value"], confint_exp["upper value"], alpha=0.2);
plt.title(f"Predicción de la serie gnp a {h} trimestres");
../_images/5fd4ccc56644fb6f6127263af80715f457920a25002b7979b2172ee08f906d2b.png

Modelos ARIMA estacionales (SARIMA)

Una variante muy utilizada de los modelos ARMA (ARIMA) es aquella que agrega dependencia estacional. Es decir, existe algún período conocido \(s\) de la serie llamado componente estacional que se conoce tiene influencia en el valor actual. Por ejemplo \(s=12\) en series anuales muestreadas mensualmente.

Ejemplo (AR estacional puro)

Supongamos que la serie sigue la siguiente ecuación:

\[x_{t} = \Phi x_{t-12} + w_t\]

con \(\Phi\) un coeficiente y \(w_t\) ruido blanco Gaussiano de varianza \(\sigma^2_w\)

El proceso anterior lo podemos pensar simplemente como un proceso \(AR(12)\), pero cuyos coeficientes intermedios son todos \(0\).

Phi=0.9
coefs = np.concatenate(([1],np.zeros(11),[-Phi]))
print(f"Coeficientes autorregresivos = {coefs}")
x = arma_generate_sample(coefs,[1],480)
x = pd.Series(x)
x.plot();
Coeficientes autorregresivos = [ 1.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.  -0.9]
../_images/21d848dcf93bc31d33fc9e25cff4882998b475b552559702f5cacb33cb590a00.png

Miremos la ACF y PACF de esta serie:

plot_acf(x, lags=36,bartlett_confint=False);
../_images/f3ecde3349a2cabf30126ff3cc0f362a9a041a998d5f9ffc2be4e7d32eab082f.png
plot_pacf(x);
../_images/d2e8e96d69eafbed835c7b2848f04567eb336c4601f26cc3f677a7af4fefb631.png

Y en principio se puede realizar el ajuste de la misma manera que antes:

fit = ARIMA(x,order=(12,0,0), trend="n").fit()
fit.summary()
SARIMAX Results
Dep. Variable: y No. Observations: 480
Model: ARIMA(12, 0, 0) Log Likelihood -671.830
Date: Mon, 19 May 2025 AIC 1369.660
Time: 16:25:53 BIC 1423.919
Sample: 0 HQIC 1390.988
- 480
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
ar.L1 0.0207 0.020 1.034 0.301 -0.019 0.060
ar.L2 0.0091 0.023 0.393 0.694 -0.036 0.054
ar.L3 0.0010 0.022 0.046 0.964 -0.042 0.044
ar.L4 0.0138 0.023 0.587 0.557 -0.032 0.060
ar.L5 -0.0115 0.023 -0.505 0.614 -0.056 0.033
ar.L6 0.0140 0.024 0.590 0.555 -0.033 0.061
ar.L7 -0.0031 0.022 -0.143 0.886 -0.046 0.040
ar.L8 0.0025 0.021 0.120 0.904 -0.038 0.043
ar.L9 0.0145 0.022 0.672 0.502 -0.028 0.057
ar.L10 -0.0187 0.021 -0.910 0.363 -0.059 0.022
ar.L11 0.0251 0.020 1.253 0.210 -0.014 0.064
ar.L12 0.8835 0.020 43.494 0.000 0.844 0.923
sigma2 0.9248 0.065 14.189 0.000 0.797 1.053
Ljung-Box (L1) (Q): 1.21 Jarque-Bera (JB): 0.90
Prob(Q): 0.27 Prob(JB): 0.64
Heteroskedasticity (H): 1.06 Skew: 0.08
Prob(H) (two-sided): 0.73 Kurtosis: 2.86


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).

Sin embargo, si uno sabe que los coeficientes son \(0\), es mejor obviarlos para lograr un mejor ajuste del coeficiente no nulo. Esto se logra proponiendo un modelo \(ARMA(p,q)\times(P,Q)_s\). Aquí \((p,q)\) son las componentes ARMA como antes, y \((P,Q)_s\) indican dependencia a estaciones pasadas (dadas por el período \(s\)).

En la función ARIMA de statsmodels esto se hace pasándole el parámetro seasonal_order=(P,D,Q,s) donde \(P\) es la parte autorregresiva estacional, \(D\) la diferenciación estacional (no va en este caso), \(Q\) la parte media móvil estacional, y \(s\) el período, en este caso \(12\).

fit = ARIMA(x,order=(0,0,0), seasonal_order=(1, 0, 0, 12),trend="n").fit()
fit.summary()
SARIMAX Results
Dep. Variable: y No. Observations: 480
Model: ARIMA(1, 0, 0, 12) Log Likelihood -674.201
Date: Mon, 19 May 2025 AIC 1352.402
Time: 16:25:55 BIC 1360.750
Sample: 0 HQIC 1355.683
- 480
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
ar.S.L12 0.8966 0.019 46.546 0.000 0.859 0.934
sigma2 0.9329 0.063 14.899 0.000 0.810 1.056
Ljung-Box (L1) (Q): 0.47 Jarque-Bera (JB): 0.86
Prob(Q): 0.49 Prob(JB): 0.65
Heteroskedasticity (H): 1.07 Skew: 0.09
Prob(H) (two-sided): 0.68 Kurtosis: 2.89


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).

Ejemplo: combinación de AR estacional y MA local.

Consideremos el proceso:

\[x_t = \Phi x_{t-12} + w_t + \theta w_{t-1}\]

En este caso tenemos un proceso \(ARMA(0,1)\times(1,0)_{12}\).

Consideremos la siguiente serie de nacimientos en EEUU durante el “baby boom”:

birth = astsa.birth
birth.plot();
../_images/bb601401690db390da92b75e8d09989a119c67e885372013978a80e45cefae93.png

Diferenciemos una vez para lograr estacionariedad:

x = birth.diff().dropna()
x.plot();
../_images/43c3f0469a8e7e65c9999e90ba707d8319353579c8bd63559ae7531396b35022.png
plot_acf(x, bartlett_confint=False);
../_images/e3df6d72d3c7c55f5aab21520b72835bd4942f9706ac6089dac424187afc4691.png
plot_pacf(x);
../_images/7fd84d04327aeb738171b60f80b4b99c77df89d9fb62f070454ee0915f6fb326.png

Ajustamos ahora el modelo anterior \(x_t = \Phi x_{t-12} + w_t + \theta w_{t-1}\), es decir, \(ARMA(0,1)\times(1,0)_{12}\).

fit = ARIMA(x,order=(0,0,1), seasonal_order=(1, 0, 0, 12),trend="n").fit()
fit.summary()
SARIMAX Results
Dep. Variable: value No. Observations: 372
Model: ARIMA(0, 0, 1)x(1, 0, [], 12) Log Likelihood -1318.522
Date: Mon, 19 May 2025 AIC 2643.044
Time: 16:26:06 BIC 2654.801
Sample: 02-29-1948 HQIC 2647.713
- 01-31-1979
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
ma.L1 -0.5035 0.043 -11.823 0.000 -0.587 -0.420
ar.S.L12 0.8697 0.023 37.533 0.000 0.824 0.915
sigma2 66.9941 4.620 14.500 0.000 57.939 76.049
Ljung-Box (L1) (Q): 2.76 Jarque-Bera (JB): 1.90
Prob(Q): 0.10 Prob(JB): 0.39
Heteroskedasticity (H): 0.99 Skew: 0.11
Prob(H) (two-sided): 0.94 Kurtosis: 3.27


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).

Agreguemos una predicción a futuro:

#Usamos el modelo ajustado para predecir a futuro
h = 24 #horizonte de predicción
predicciones = fit.get_prediction(start=birth.size,end=birth.size+h)
xhat = predicciones.predicted_mean
confint = predicciones.conf_int(alpha=0.05) #alpha es la confianza del intervalo
x.plot()
xhat.plot()
plt.legend(["Observado","Predicción"])
plt.fill_between(xhat.index,confint["lower value"], confint["upper value"], alpha=0.2);
plt.title(f"Predicción de la serie diff(birth) a {h} meses");
../_images/e92216240a0a151a30319292c3572422ac8f20339d4698afc51bc8bacaf1579a.png

Modelo SARIMA general

En el ejemplo anterior, tuvimos que diferenciar la serie birth para lograr algo estacionario. Esto nos lleva al modelo SARIMA general, en el cual los parámetros son:

  • \((p,d,q)\), las componentes locales del modelo sarima. \(d\) es la cantidad de veces que hay que diferenciar con la muestra anterior.

  • \((P,D,Q)\), las componentes estacionales del modelo sarima. \(D\) es la cantidad de veces que hay que diferenciar con la estación anterior.

  • \(s\) es la frecuencia de las estaciones.

Para el caso anterior, podemos directamente ajustar un modelo \(SARIMA(0,1,1)\times (1,0,0)_{12}\) pasándole el problema de diferenciar al ajuste.

birth
fit = ARIMA(birth,order=(0,1,1), seasonal_order=(1, 0, 0, 12),trend="n").fit()
fit.summary()
SARIMAX Results
Dep. Variable: value No. Observations: 373
Model: ARIMA(0, 1, 1)x(1, 0, [], 12) Log Likelihood -1318.523
Date: Mon, 19 May 2025 AIC 2643.046
Time: 16:26:11 BIC 2654.803
Sample: 01-31-1948 HQIC 2647.715
- 01-31-1979
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
ma.L1 -0.5035 0.043 -11.823 0.000 -0.587 -0.420
ar.S.L12 0.8697 0.023 37.533 0.000 0.824 0.915
sigma2 66.9942 4.620 14.500 0.000 57.939 76.050
Ljung-Box (L1) (Q): 2.76 Jarque-Bera (JB): 1.90
Prob(Q): 0.10 Prob(JB): 0.39
Heteroskedasticity (H): 0.99 Skew: 0.11
Prob(H) (two-sided): 0.94 Kurtosis: 3.27


Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
#Usamos el modelo ajustado para predecir a futuro
h = 24 #horizonte de predicción
predicciones = fit.get_prediction(start=birth.size,end=birth.size+h)
xhat = predicciones.predicted_mean
confint = predicciones.conf_int(alpha=0.05) #alpha es la confianza del intervalo
birth.plot()
xhat.plot()
plt.legend(["Observado","Predicción"])
plt.fill_between(xhat.index,confint["lower value"], confint["upper value"], alpha=0.2);
plt.title(f"Predicción de la serie diff(birth) a {h} meses");
../_images/681c2714ba189ad5a8267a747a8fe7bb70164fa92e7f93b6072dac5000549152.png

Modelos GARCH

Esta familia de modelos permiten modelar series cuya varianza no es estacionaria, como la que vimos anteriormente para el EUR vs USD

y = eur.diff().dropna()
y.plot();
plt.title(f"Media de las diferencias {np.mean(y)} +- {2*np.std(y)/np.sqrt(y.size)}");
../_images/1e919daece282c461faa56527d5009c71cb7596d7d26677ac210ac92d1f5b09e.png

Idea:

Modelar la varianza de la serie como un proceso autorregresivo. Si la serie está centrada, esto es equivalente a ajustar un modelo ARMA a los valores de \(x_t^2\). Este modelo se llama \(GARCH\): Generalized autorregressive conditionally heteroskedastic.

y2 = y**2
y2.plot();
../_images/4f5191c49fce5cc551f332d93540412226eb39acec1900278db883b73d65a2f9.png
plot_acf(y2,bartlett_confint=False);
../_images/c01428a85c61c82640c12f5761d3a1880c68857453ec9a07bb151f34658ce0e8.png
plot_pacf(y2);
../_images/5b30c4bfa2950e456f8f1e820c32ea4138735ca2075d314a50f03798e0429a8f.png

La biblioteca statsmodels no incluye modelos tipo GARCH por lo que requerimos la biblioteca arch

from arch import arch_model
fit = arch_model(y, mean='Zero', vol='GARCH', p=1,q=1, rescale=True).fit();
fit.summary()
Iteration:      1,   Func. Count:      5,   Neg. LLF: 3542121476.1780243
Iteration:      2,   Func. Count:     12,   Neg. LLF: 6432.502659518372
Iteration:      3,   Func. Count:     16,   Neg. LLF: 13054.967420877505
Iteration:      4,   Func. Count:     21,   Neg. LLF: 8158.432872015319
Iteration:      5,   Func. Count:     28,   Neg. LLF: 9559.09420278957
Iteration:      6,   Func. Count:     33,   Neg. LLF: 8652.224376988284
Iteration:      7,   Func. Count:     39,   Neg. LLF: 7699.221113600659
Iteration:      8,   Func. Count:     45,   Neg. LLF: 6407.88196659977
Iteration:      9,   Func. Count:     49,   Neg. LLF: 6407.647697238508
Iteration:     10,   Func. Count:     53,   Neg. LLF: 6407.620967274368
Iteration:     11,   Func. Count:     57,   Neg. LLF: 6407.619060270876
Iteration:     12,   Func. Count:     61,   Neg. LLF: 6407.6189735249945
Iteration:     13,   Func. Count:     64,   Neg. LLF: 6407.6189735213
Optimization terminated successfully    (Exit mode 0)
            Current function value: 6407.6189735249945
            Iterations: 13
            Function evaluations: 64
            Gradient evaluations: 13
Zero Mean - GARCH Model Results
Dep. Variable: None R-squared: 0.000
Mean Model: Zero Mean Adj. R-squared: 0.000
Vol Model: GARCH Log-Likelihood: -6407.62
Distribution: Normal AIC: 12821.2
Method: Maximum Likelihood BIC: 12841.6
No. Observations: 6499
Date: Mon, May 19 2025 Df Residuals: 6499
Time: 16:26:23 Df Model: 0
Volatility Model
coef std err t P>|t| 95.0% Conf. Int.
omega 1.2267e-03 5.081e-04 2.414 1.577e-02 [2.309e-04,2.223e-03]
alpha[1] 0.0282 3.649e-03 7.720 1.168e-14 [2.102e-02,3.532e-02]
beta[1] 0.9694 3.914e-03 247.714 0.000 [ 0.962, 0.977]


Covariance estimator: robust
#Obtenemos los valores de varianza estimada por el modelo ajustado
#Horizon=1 es que predice el valor siguiente, start=0 es que arranca al comienzo de la serie)
predicciones = fit.forecast(horizon=1,start=0).variance
predicciones = pd.Series(predicciones["h.1"])
desvio = np.sqrt(predicciones)*np.std(y)
desvio.plot();
plt.title("Desvío estándar estimado");
../_images/f40dca67a2088c2239edc7ecf1bbd44d29eea227bab1b2334032da801dfcd291.png
y.plot(alpha=0.3)
(2*desvio).plot()
(-2*desvio).plot();
../_images/109e86a134365de9b9eda5dec58caded432574f466c1631d4b51a0d126db9bfd.png

Ejercicios:

Aplicación a la serie de temperaturas globales.

Utilizando la serie gtemp de temperaturas globales ya vista anteriormente, ajustar un modelo \(ARIMA(p,d,q)\) adecuado utilizando las técnicas anteriores.

gtemp = astsa.gtemp
gtemp.plot();
../_images/3996f26844ea8c05282829be9395d5f4b9979ef06e240cfd3384e3059cf63492.png

Aplicación a la serie de pasajeros aéreos.

Utilizando la serie AirPassengers vista anteriormente, ajustar un modelo tipo \(SARIMA\) adecuado (diferenciando y transformando si es necesario) de estacionalidad \(12\). Comparar con los ajustes realizados previamente en base a senos y cosenos.

df = pd.read_csv('../data/international-airline-passengers.csv', names = ['year','passengers'], header=0)
air = pd.Series(df["passengers"].values, index=df['year'])
air.plot();
plt.title("Total mensual de pasajeros en el período 1949-1960")
plt.ylabel("Miles de pasajeros");
../_images/6d302334f83e341434669a4a0d7d538325dd98b1a0d585c47884ed2c224a0f48.png