VAR Model Explained

Vector Autoregression (VAR) models are the workhorse of multivariate time series analysis. Unlike univariate models that analyze a single time series in isolation, VAR treats multiple time series as...

Key Insights

  • VAR models capture interdependencies between multiple time series by regressing each variable on lagged values of itself and all other variables in the system, making them ideal for analyzing economic indicators, financial markets, and other interconnected systems.
  • Proper lag order selection using information criteria (AIC/BIC) is critical—too few lags miss important dynamics while too many lags waste parameters and reduce forecast accuracy due to overfitting.
  • Impulse response functions and Granger causality tests are essential tools for understanding how shocks propagate through the system and which variables genuinely influence others, going beyond simple correlation to reveal directional relationships.

Introduction to Vector Autoregression (VAR)

Vector Autoregression (VAR) models are the workhorse of multivariate time series analysis. Unlike univariate models that analyze a single time series in isolation, VAR treats multiple time series as a system where each variable influences and is influenced by the others over time.

The fundamental insight behind VAR is simple but powerful: today’s values of economic indicators, stock prices, or sensor readings often depend not just on their own past values, but on the past values of related variables. For instance, interest rates, inflation, and GDP growth are inherently interconnected—changes in one ripple through the others with various time lags.

Let’s start by visualizing a typical multivariate time series scenario:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.api import VAR
from statsmodels.tsa.stattools import adfuller, grangercausalitytests
from statsmodels.graphics.tsaplots import plot_acf

# Generate synthetic multivariate time series data
np.random.seed(42)
n = 500

# Simulate two interdependent variables
y1 = np.zeros(n)
y2 = np.zeros(n)
y1[0], y2[0] = 0, 0

for t in range(1, n):
    y1[t] = 0.6 * y1[t-1] + 0.3 * y2[t-1] + np.random.normal(0, 0.5)
    y2[t] = 0.4 * y1[t-1] + 0.5 * y2[t-1] + np.random.normal(0, 0.5)

df = pd.DataFrame({'Variable_1': y1, 'Variable_2': y2})

# Visualize the data
fig, axes = plt.subplots(2, 1, figsize=(12, 6))
df['Variable_1'].plot(ax=axes[0], title='Variable 1 Over Time')
df['Variable_2'].plot(ax=axes[1], title='Variable 2 Over Time')
plt.tight_layout()
plt.show()

This example shows two variables that clearly influence each other—exactly the scenario where VAR excels.

Mathematical Foundation

A VAR model of order p, denoted VAR(p), expresses each variable as a linear combination of its own p lagged values and the p lagged values of all other variables in the system.

For a two-variable system, the VAR(1) model looks like:

y1(t) = c1 + φ11 * y1(t-1) + φ12 * y2(t-1) + ε1(t)
y2(t) = c2 + φ21 * y1(t-1) + φ22 * y2(t-1) + ε2(t)

Where:

  • y1(t) and y2(t) are the variables at time t
  • c1 and c2 are constant terms
  • φij are coefficient parameters
  • ε1(t) and ε2(t) are white noise error terms

In matrix notation:

[y1(t)]   [c1]   [φ11  φ12] [y1(t-1)]   [ε1(t)]
[y2(t)] = [c2] + [φ21  φ22] [y2(t-1)] + [ε2(t)]

The key difference from univariate AR models is that each equation includes lags of all variables, not just its own. This captures the system-wide dynamics.

Stationarity is crucial: VAR models require all variables to be stationary (constant mean, variance, and autocovariance over time). Non-stationary data must be differenced or detrended first.

Let’s implement a simple VAR(1) model:

# Create a simple VAR(1) demonstration
from statsmodels.tsa.api import VAR

# Using our synthetic data from earlier
model = VAR(df)
results = model.fit(maxlags=1)

print("VAR(1) Model Summary:")
print(results.summary())

# Display coefficient matrices
print("\nCoefficient Matrix for Lag 1:")
print(results.params)

Building a VAR Model

Building a robust VAR model involves several critical steps. Let’s work through a complete implementation using real-world-like data:

# Step 1: Load and prepare data
# For this example, we'll use our synthetic data, but in practice
# you'd load real data (e.g., economic indicators, stock prices)
data = df.copy()

# Step 2: Test for stationarity using Augmented Dickey-Fuller test
def check_stationarity(series, name):
    result = adfuller(series, autolag='AIC')
    print(f'\n{name} ADF Test:')
    print(f'ADF Statistic: {result[0]:.4f}')
    print(f'p-value: {result[1]:.4f}')
    print(f'Stationary: {result[1] < 0.05}')
    return result[1] < 0.05

stationary_y1 = check_stationarity(data['Variable_1'], 'Variable_1')
stationary_y2 = check_stationarity(data['Variable_2'], 'Variable_2')

# If non-stationary, difference the data
if not (stationary_y1 and stationary_y2):
    data = data.diff().dropna()
    print("\nData differenced to achieve stationarity")

# Step 3: Determine optimal lag order
model = VAR(data)
lag_order_results = model.select_order(maxlags=15)
print("\nLag Order Selection:")
print(lag_order_results.summary())

# Select based on AIC (or BIC for more parsimony)
optimal_lag = lag_order_results.aic
print(f"\nOptimal lag order (AIC): {optimal_lag}")

# Step 4: Fit the VAR model
model_fitted = model.fit(optimal_lag)
print("\nFitted VAR Model:")
print(model_fitted.summary())

# Step 5: Model diagnostics
# Check residuals for autocorrelation
print("\nDurbin-Watson Statistic (should be close to 2):")
from statsmodels.stats.stattools import durbin_watson
out = durbin_watson(model_fitted.resid)
for col, val in zip(data.columns, out):
    print(f'{col}: {val:.2f}')

The lag order selection is particularly important. AIC (Akaike Information Criterion) and BIC (Bayesian Information Criterion) balance model fit against complexity. AIC tends to select more lags, while BIC is more conservative.

Model Interpretation and Analysis

Once you’ve fitted a VAR model, the real insights come from analyzing variable relationships. Three key tools are essential:

Granger Causality: Does one variable help predict another?

# Granger Causality Tests
# Tests if Variable_1 Granger-causes Variable_2
print("\nGranger Causality Tests:")
print("Does Variable_1 Granger-cause Variable_2?")
gc_result = grangercausalitytests(data[['Variable_2', 'Variable_1']], 
                                   maxlag=optimal_lag, verbose=True)

print("\nDoes Variable_2 Granger-cause Variable_1?")
gc_result = grangercausalitytests(data[['Variable_1', 'Variable_2']], 
                                   maxlag=optimal_lag, verbose=True)

Impulse Response Functions (IRF): How does a shock to one variable affect others over time?

# Generate Impulse Response Functions
irf = model_fitted.irf(10)  # 10 periods ahead

# Plot IRFs
irf.plot(orth=False, figsize=(12, 8))
plt.suptitle('Impulse Response Functions', fontsize=14)
plt.tight_layout()
plt.show()

# Cumulative effects
irf.plot_cum_effects(orth=False, figsize=(12, 8))
plt.suptitle('Cumulative Impulse Response Functions', fontsize=14)
plt.tight_layout()
plt.show()

Forecast Error Variance Decomposition (FEVD): What percentage of forecast error variance in each variable is due to shocks in other variables?

# Forecast Error Variance Decomposition
fevd = model_fitted.fevd(10)
print("\nForecast Error Variance Decomposition:")
fevd.summary()

# Plot FEVD
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fevd.plot(ax=axes[0], plot_stderr=False)
plt.tight_layout()
plt.show()

Forecasting with VAR

VAR models excel at multi-step forecasting. Here’s how to generate and evaluate forecasts:

# Split data into train and test sets
train_size = int(len(data) * 0.8)
train, test = data[:train_size], data[train_size:]

# Fit model on training data
model_train = VAR(train)
model_train_fitted = model_train.fit(optimal_lag)

# Generate forecasts
forecast_steps = len(test)
forecast = model_train_fitted.forecast(train.values[-optimal_lag:], 
                                       steps=forecast_steps)
forecast_df = pd.DataFrame(forecast, 
                          index=test.index, 
                          columns=data.columns)

# Calculate forecast errors
from sklearn.metrics import mean_squared_error, mean_absolute_error

for col in data.columns:
    mse = mean_squared_error(test[col], forecast_df[col])
    mae = mean_absolute_error(test[col], forecast_df[col])
    print(f'\n{col} Forecast Accuracy:')
    print(f'MSE: {mse:.4f}')
    print(f'MAE: {mae:.4f}')

# Visualize forecasts vs. actuals
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
for i, col in enumerate(data.columns):
    axes[i].plot(test.index, test[col], label='Actual', linewidth=2)
    axes[i].plot(forecast_df.index, forecast_df[col], 
                label='Forecast', linestyle='--', linewidth=2)
    axes[i].set_title(f'{col}: Forecast vs. Actual')
    axes[i].legend()
    axes[i].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Practical Considerations and Limitations

VAR models aren’t always the right choice. Here are critical considerations:

When NOT to use VAR:

  • Cointegrated variables: If variables share a long-run equilibrium relationship, use VECM (Vector Error Correction Model) instead
  • Too many variables: VAR suffers from the curse of dimensionality—with k variables and p lags, you estimate k²p + k parameters. A VAR(4) with 10 variables requires 410 parameters!
  • High-frequency data with complex patterns: Consider VARMA models that include moving average terms

Common pitfalls:

  • Ignoring stationarity leads to spurious relationships
  • Over-parameterization from too many lags reduces forecast accuracy
  • Structural breaks invalidate historical relationships
# Demonstrating the curse of dimensionality
def count_parameters(n_vars, lag_order):
    # k²p coefficients + k intercepts
    return n_vars**2 * lag_order + n_vars

print("VAR Model Parameters Required:")
for vars in [2, 5, 10, 20]:
    for lags in [1, 4, 8]:
        params = count_parameters(vars, lags)
        print(f"{vars} variables, {lags} lags: {params} parameters")

Best practices:

  • Always test for stationarity and cointegration before modeling
  • Use cross-validation for lag selection, not just information criteria
  • Check residual diagnostics—autocorrelation indicates model misspecification
  • Consider domain knowledge when interpreting Granger causality (correlation ≠ causation)
  • For forecasting, simpler models often outperform complex ones out-of-sample

VAR models provide a principled framework for analyzing multivariate time series. When applied correctly with proper diagnostics and validation, they reveal insights about system dynamics that univariate methods miss entirely. The key is understanding both their power and their limitations.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.