The (extended) ARIMA family of methods is too big to be properly explained in this course.
At NMBU, the course DAT320 goes deeper and explains the data generating processes.
An online book with videos: Forecasting: Principles and Practice.
We therefore skip (almost) directly to the regression models (inspired by phosgene89’s GitHub page, except for their errors) and their usage.
But first we introduce a dataset and the concepts of stationarity and autocorrelation.
Stationarity:
The distribution of the time series is independent of which part of the time series you look at.
Trends, seasonality (cycles of fixed width) and changes in variance lead to non-stationarity.
Differencing (first or second order discrete derivatives) can help.
Seasonal differencing means the difference is not between neighbours but higher lags.
If the data is not stationary, pre-processing or modelling of the specific deviations from stationarity is needed.
Wholesale price index (WPI) data¶
We will illustrate some concepts and models using the WPI data.
# Imports
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
import requests
from io import BytesIO
# Load Wholesale price index (WPI) data
wpi1 = requests.get('https://www.stata-press.com/data/r12/wpi1.dta').content
data = pd.read_stata(BytesIO(wpi1))
data.index = data.t
data['ln_wpi'] = np.log(data['wpi'])
data['D.ln_wpi'] = data['ln_wpi'].diff()
# Set the frequency to Quarterly Start, year ending October
data.index.freq="QS-OCT"
data.head()Autocorrelation¶
Autocorrelation is the correlation between a stationary timeseries and a lagged version of itself.
This is a measure of the time dependence in the series, i.e., lack of independence.
Can be used to indicate the appropriate lag in moving average (MA) models.
Partial autocorrelation is the autocorrelation when controlling for (regressed on) all intermediate time lags.
Can be used to indicate the appropriate lag in autogregressive (AR) models.
# Autocorrelation and partial autocorrelation plots (raw data)
fig, axes = plt.subplots(1, 2, figsize=(15,4))
fig = sm.graphics.tsa.plot_acf(data.iloc[1:]['wpi'], lags=40, ax=axes[0])
fig = sm.graphics.tsa.plot_pacf(data.iloc[1:]['wpi'], lags=40, ax=axes[1])# Graph data
fig, axes = plt.subplots(1, 2, figsize=(12,3))
# Levels
axes[0].plot(data.index._mpl_repr(), data['wpi'], '-')
axes[0].set(title='US Wholesale Price Index')
# Log difference (attempting to improve stationarity)
axes[1].plot(data.index._mpl_repr(), data['D.ln_wpi'], '-')
axes[1].hlines(0, data.index[0], data.index[-1], 'r')
axes[1].set(title='US Wholesale Price Index - difference of logs');# Autocorrelation and partial autocorrelation plots
# after applying the logarithm and differencing
fig, axes = plt.subplots(1, 2, figsize=(12,3))
fig = sm.graphics.tsa.plot_acf(data.iloc[1:]['D.ln_wpi'], lags=40, ax=axes[0])
fig = sm.graphics.tsa.plot_pacf(data.iloc[1:]['D.ln_wpi'], lags=40, ax=axes[1])Autoregressive models in Python¶
Using the statsmodels package, the most complex model is the starting point.
Setting the various parameters of , we can obtain any of the below mentioned models.
SARIMAX(endog, exog=None, order=(1, 0, 0), seasonal_order=(0, 0, 0, 0), trend=None, ...)
For integer values of , all lags up to the integer are included. For more fine-grained control, lists can be applied, e.g., [1,0,1] includes lags 1 and 3, but not 2.
There are also various other parameters, e.g., a trend (none, constant, linear, quadratic, polynomial).
Lag operator¶
Back-shift time points, i.e., extracts the measurement time points before .
Also noted as in some litterature.
Combined with a vector of parameters , we define a polynomial function
This will enable compact notation of the SARIMAX models, exchanging sums with the polynomial function.
AR - autoregressive models¶
For a (single variable) timeseries given by , we can specify the model as:
i.e., the current time is a function of previous time points and a constant.
Here, is a constant, is the coefficient for the -th time lag and is the error.
This can be thought of as stacking subsets of a time series using a moving window and performing ordinary least squares on the resulting matrix/dataframe.
In practice, the fitting is performed using maximum likelihood, so performing ordinary regression will not give exactly the same results.
Using the lag operator, , we can redefine the above equation in the form of a polynomial function, (signs of coefficients will change) as:
# Fit an AR(1) model (badly specified due to non-stationarity)
mod = sm.tsa.statespace.SARIMAX(data['wpi'], trend='c', order=(1,0,0)) # trend='c' adds a constant
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparisonMA - moving average models¶
MA models are functions of previous errors, rather than previous measurements.
We can define an model as:
Here, is the number of time lags and is defined as above, but using the error terms and is with respect to the current model.
# Fit an MA(1) model (disregarding the obvious autocorrelation)
mod = sm.tsa.statespace.SARIMAX(data['wpi'], trend='c', order=(0,0,1))
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparisonARMA - autoregressive moving average models¶
When we take the sum of and models of the same time series, we get models:
which can be reformulated to:
Again, is with respect to the current model, but shares name with the previous models.
This model is learning both from seeing previous samples and from how well these were predicted at previous time steps, thus it can tackle changes in the average.
# Fit an ARMA(1,1) model (badly specified due to non-stationarity)
mod = sm.tsa.statespace.SARIMAX(data['wpi'], trend='c', order=(1,0,1))
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparisonARIMA - autoregressive integrated moving average models¶
To help compensate for lack of stationarity, we add an integration operator, , defined as:
Here, and the degree of differensing is .
For instance with :
An model where is exchanged with would look like this:
The constant is often omitted and assumed absorbed by the integration. Only the time series is integrated, not the errors.
Reformulating this using the integration operator, we get an model:
This model has the properties of the ARMA model, but in addition does the differensing for us for stationarity.
The terms can be reorganised to get predictions on the original scale instead of predicting difference values.
# Fit an ARIMA(1,1,1) model
mod = sm.tsa.statespace.SARIMAX(data['wpi'], trend=None, order=(1,1,1))
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparison#### Fit an ARIMA(1,1,1) model
mod = sm.tsa.statespace.SARIMAX(data['wpi'], trend=None, order=(1,1,1))
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparison# Since the data are quarterly, we can add a seasonal component by including a fourth time lag to moving average
# Fit an ARIMA(1,1,[1,0,0,1]) model
mod = sm.tsa.statespace.SARIMAX(data['wpi'], trend=None, order=(1,1,[1,0,0,1]))
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparison
# What about a linear trend? (trend='t')SARIMA - seasonal autoregressive integrated moving average models¶
SARIMA shares some resemblance with the STL decomposition introduced previously.
For seasons of length , the seasonal part is obtained by applying an ARIMA model with lags, and , and integration time, , that are multiples of , i.e., if , the included time points would be and .
After the seasonal part has been removed, another is applied to which is equivalent to multiplying the two models together.
The model then becomes:
This model has the ability to combine experience from previous timepoints with seasonal trends.
Or if one sets the ARIMA parameters , , , one can have a pure seasonal model.
# Using SARIMA for seasonality instead of the fourth time lag to moving average.
# Fit a SARIMA(1,1,1)(1,1,1,4) model
mod = sm.tsa.statespace.SARIMAX(data['wpi'], trend=None, order=(1,1,1), seasonal_order=(1,1,1,4))
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparison (no improvement here)Exogenous variables¶
As we showed in the previous chapter, it is possible to make a model purely on other variables measured at the same time, .
Including these variables into ARIMA and SARIMA, we get the ARIMAX and SARIMAX models.
In practice, we paste on an extra coefficient vector, , and variables, , to the models, here noted as sums of products :
:
:
The final model, thus includes autogression and moving averages, can perform differences, deals with seasonality and can leverage external variables from the same timepoint as the predictions.
# Read the FinalData sheet of the OilExchange.xlsx file using Pandas again
import pandas as pd
OilExchange = pd.read_excel('../../data/OilExchange.xlsx', sheet_name='FinalData')
OilExchange.index = OilExchange.Date
OilExchange.index.freq = "MS" # Set the frequency to Month Start
OilExchange.head()# Fit a SARIMAX(1,1,1)(1,1,1,12) model with exogenous variables (closing our eyes, as we know little about the data)
mod = sm.tsa.statespace.SARIMAX(OilExchange['PerEURO'], exog=OilExchange.loc[:, OilExchange.columns[3:-6]], \
trend='c', order=(1,1,1), seasonal_order=(1,1,1,12))
res = mod.fit(disp=False)
print(res.summary()) # AIC gives us a measure of fit for comparison (no improvement here)Exercise¶
Play with the OilExchange data.
See if you can improve the fit of the model by adjusting lags and removing the least significant terms in the model.
Interpretation of Model Diagnostic Tests¶
Ljung-Box (L1) (Q)¶
Purpose: Tests for autocorrelation in the residuals.
Null hypothesis (H₀): The residuals are independently distributed (no autocorrelation up to lag L1).
Result:
Q: test statistic
Prob(Q): >0.05 is good
Jarque-Bera (JB)¶
Purpose: Tests whether the residuals are normally distributed (based on skewness and kurtosis).
Null hypothesis (H₀): The residuals are normally distributed.
Result:
JB: test statistic
Prob(JB): >0.05 is good
Heteroskedasticity (H)¶
Purpose: Tests whether the residuals have constant variance (homoskedasticity).
Null hypothesis (H₀): The residuals have constant variance.
Result:
H: test statistic
Prob(H): >0.05 is good
Skew and Kurtosis¶
Skew: Measures asymmetry of the residuals. 0 means symmetric; positive means right-skewed.
Kurtosis: Measures “tailedness” of the residuals. 3 is normal; higher means heavier tails.
In practice:
Independence and constant variance are more important for forecasting than perfect normality.
Non-normality is common in real-world time series models.
SARIMAX prediction¶
One-step-ahead prediction uses the estimated parameters and samples within the lags of the model to predict the next time point.
This means that the predictions will stay relatively close to the true values, never predicting more than one step away from the truth.
Dynamic prediction predicts one step as above, then uses predicted values as input instead of true values.
This means the predictions can deviate from the truth over time.
# Refit the model on the training set (up to 2013-01-01) to estimate the parameters
mod = sm.tsa.statespace.SARIMAX(OilExchange['PerEURO'].loc[:'2013-01-01'], \
OilExchange.loc[:, OilExchange.columns[3:-4]].loc[:'2013-01-01'], \
trend='c', order=(1,1,1), seasonal_order=(1,1,1,12))
res = mod.fit(disp=False)# Get predictions for the whole dataset
mod = sm.tsa.statespace.SARIMAX(OilExchange['PerEURO'], OilExchange.loc[:, OilExchange.columns[3:-4]], \
trend='c', order=(1,1,1), seasonal_order=(1,1,1,12))
res = mod.filter(res.params) # One-step-ahead predictions using parameters from previous fit# In-sample one-step-ahead prediction wrapper function
predict = res.get_prediction()
predict_ci = predict.conf_int()# Dynamic predictions starting from 2013-01-01
predict_dy = res.get_prediction(dynamic='2013-01-01')
predict_dy_ci = predict_dy.conf_int()# Compare the one-step-ahead predictions to the dynamic predictions
fig, ax = plt.subplots(figsize=(9,4))
npre = 4
ax.set(title='Exchange rate', xlabel='Date', ylabel='Per EURO')
# Plot data points
OilExchange.loc['2012-01-01':, 'PerEURO'].plot(ax=ax, style='o', label='Observed')
# Plot predictions
predict.predicted_mean.loc['2012-01-01':].plot(ax=ax, style='r--', label='One-step-ahead forecast')
ci = predict_ci.loc['2012-01-01':]
ax.fill_between(ci.index, ci.iloc[:,0], ci.iloc[:,1], color='r', alpha=0.1)
predict_dy.predicted_mean.loc['2012-01-01':].plot(ax=ax, style='g', label='Dynamic forecast (2013)')
ci = predict_dy_ci.loc['2012-01-01':]
ax.fill_between(ci.index, ci.iloc[:,0], ci.iloc[:,1], color='g', alpha=0.1)
legend = ax.legend(loc='lower right')Questercise¶
Does the model made in the previous exercise improve the predictions as visualized above?