2. Fitting an AR in Python: order with AIC and BIC
Level 9: Time Series - ARIMA and GARCH
The statsmodels library provides the autoregressive model ready to use. The modern class is AutoReg (the old AR and statsmodels.tsa.arima_model.ARIMA are deprecated; if you see code using them, update it).
We start from the S&P 500 log returns and make a split that respects the temporal ordering. No train_test_split with shuffle=True: in time series, shuffling is cheating.
import numpy as np
import yfinance as yf
from statsmodels.tsa.api import AutoReg
df = yf.download('^GSPC', start='2016-01-01', end='2022-12-30')['Close']
Re = np.log(df).diff().dropna() # log returns
X = Re.values
train_size = int(len(X) * 0.66)
train, test = X[1:train_size], X[train_size:]Fitting an AR with 30 lags and reading the summary:
max_lag = 30
model = AutoReg(train, lags=max_lag, trend='n').fit()
print(model.summary())
aic_value = model.aic
print('AIC value:', aic_value)Two details that matter more than they look:
trend='n'means *no constant*: no intercept is estimated. It makes sense with log returns, whose mean is practically zero. If you usetrend='c'(the default),model.params[0]will be the constant and the lag coefficients start at index 1. Getting that numbering wrong is the number one cause of hand-built forecasts going astray.- The AIC (Akaike information criterion) measures the balance between fit and complexity: it penalizes adding parameters. It does not measure out-of-sample predictive ability, it measures penalized in-sample likelihood.
Before choosing the order it is worth looking at the residuals of the fitted model:
_ = tsplot(model.resid, lags=max_lag) # series, ACF, PACF, QQ and probplotIf the residuals look like discrete white noise, the model has captured the linear structure. If autocorrelation remains, the order is insufficient.
Order selection by grid search. Instead of fixing p by eye, we sweep the possible orders and keep the one with the lowest AIC:
max_lag = 30
best_aic = np.inf
best_order = 0
best_model = None
for lag in range(1, max_lag + 1):
model = AutoReg(train, lags=lag, trend='n').fit()
if model.aic < best_aic:
best_aic = model.aic
best_order = lag
best_model = model
print('Best AR order:', best_order)
print('Best AIC:', best_aic)
print(best_model.summary())Note that the loop refits the model from scratch at every iteration: each AR(lag) is a different regression, not a nested model being extended.
The same scheme works with BIC (the Bayesian criterion) by replacing result.aic with result.bic. The practical difference: the BIC penalizes extra parameters more harshly because its penalty grows with the logarithm of the sample size. With long financial series, the BIC almost always picks a smaller order than the AIC. In trading that is usually a virtue: fewer parameters means less overfitting to the noise in the sample and a more robust strategy when the market regime changes.
An honest warning: choosing the order by AIC on the training set and then measuring the error on the test set is correct. Choosing it by looking at the test error and then presenting that same error as the backtest result is not: that is selection bias, and it is one of the most common ways of inflating results without realizing it.
To go deeper into this lesson
- S&P 500in the glossary
- Walk-forward testingin the glossary
- What the S&P 500 is: how it is built, who decides and how you invest in itguide