---
title: "Vector Autoregressions"
author: ""
abstract: |
Univariate models are powerful, but they answer only one kind of question:
how does a variable evolve over time given its own past? Many of the most
important questions in macroeconomics require something more — an account of
how variables move together, how a shock to one propagates through a system,
and how much of any variable's fluctuation is driven by shocks that originate
elsewhere. This chapter develops the vector autoregression (VAR), the
workhorse multivariate model in empirical macroeconomics. We begin by showing
how the VAR arises naturally when single-equation models fail because of
endogeneity — a problem flagged in Chapter 3 and resolved here. We build the
reduced-form VAR, establish the conditions under which it is well-behaved,
and show how to choose lag length. We then develop the core analytical tools:
Granger causality for testing predictive relationships, structural
identification via the Cholesky decomposition, impulse response functions for
tracing the dynamic effects of shocks, and forecast error variance
decomposition for attributing fluctuations to their sources. The running
example throughout is a three-variable system — US GDP growth, CPI inflation,
and the federal funds rate — the same series introduced in Chapters 4 and 5.
The chapter closes by examining what happens when one or more variables are
integrated, and why that case requires the cointegration framework developed
in Chapter 8.
jupyter: python3
format:
html:
toc: true
toc-depth: 3
toc-title: "In this chapter"
number-sections: true
code-fold: true
code-summary: "Show code"
code-tools: true
theme: cosmo
css: styles.css
highlight-style: github
fig-align: center
fig-cap-location: bottom
fig-responsive: true
html-math-method: mathjax
embed-resources: false
execute:
echo: true
warning: false
message: false
cache: false
---
```{python}
#| label: setup
#| include: false
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import matplotlib.ticker as mticker
from statsmodels.tsa.api import VAR
from statsmodels.tsa.stattools import adfuller, kpss, grangercausalitytests
import pandas_datareader.data as web
from datetime import datetime
import warnings
warnings.filterwarnings("ignore")
# ── EO Brand Palette ───────────────────────────────────────────────────────────
EO_CHARCOAL = "#36454F"
EO_COPPER = "#B87333"
EO_SAGE = "#87A96B"
EO_SKYBLUE = "#5B9BD5"
EO_TERRACOTTA = "#D4745E"
EO_LAVENDER = "#8E7AB5"
EO_COLORS = [EO_COPPER, EO_SKYBLUE, EO_SAGE,
EO_TERRACOTTA, EO_LAVENDER, EO_CHARCOAL]
PAGE_BG = "#FAFAF8"
# ── Global rcParams ────────────────────────────────────────────────────────────
mpl.rcParams.update({
"figure.figsize": (6, 3),
"figure.dpi": 150,
"figure.facecolor": PAGE_BG,
"figure.edgecolor": PAGE_BG,
"axes.facecolor": PAGE_BG,
"axes.edgecolor": EO_CHARCOAL,
"axes.linewidth": 0.7,
"axes.grid": True,
"axes.grid.axis": "y",
"axes.spines.top": False,
"axes.spines.right": False,
"axes.titlesize": 9,
"axes.titleweight": "bold",
"axes.titlecolor": EO_CHARCOAL,
"axes.titlelocation": "left",
"axes.labelsize": 8,
"axes.labelcolor": EO_CHARCOAL,
"axes.labelweight": "normal",
"axes.prop_cycle": mpl.cycler(color=EO_COLORS),
"grid.color": "#E5E5E5",
"grid.linewidth": 0.5,
"grid.linestyle": "--",
"grid.alpha": 0.8,
"xtick.color": EO_CHARCOAL,
"ytick.color": EO_CHARCOAL,
"xtick.labelsize": 7,
"ytick.labelsize": 7,
"xtick.direction": "out",
"ytick.direction": "out",
"xtick.major.size": 3,
"ytick.major.size": 3,
"lines.linewidth": 1.2,
"lines.solid_capstyle": "round",
"legend.frameon": True,
"legend.framealpha": 0.9,
"legend.edgecolor": "#CCCCCC",
"legend.facecolor": PAGE_BG,
"legend.fontsize": 6,
"legend.title_fontsize": 6,
"font.family": "serif",
"font.serif": ["Palatino Linotype", "Palatino", "Georgia",
"DejaVu Serif"],
"font.sans-serif": ["Calibri", "Arial", "DejaVu Sans"],
"font.size": 8,
"text.color": EO_CHARCOAL,
"savefig.dpi": 300,
"savefig.bbox": "tight",
"savefig.facecolor": PAGE_BG,
})
def eo_suptitle(fig, title, **kwargs):
defaults = dict(fontsize=9, fontweight="bold",
color=EO_CHARCOAL, fontfamily="Calibri", y=1.01)
defaults.update(kwargs)
fig.suptitle(title, **defaults)
def eo_style_ax(ax):
for obj in [ax.title, ax.xaxis.label, ax.yaxis.label]:
obj.set_fontfamily("Calibri")
RECESSIONS = [
("1960-04-01", "1961-02-01"),
("1969-12-01", "1970-11-01"),
("1973-11-01", "1975-03-01"),
("1980-01-01", "1980-07-01"),
("1981-07-01", "1982-11-01"),
("1990-07-01", "1991-03-01"),
("2001-03-01", "2001-11-01"),
("2007-12-01", "2009-06-01"),
]
def shade_recessions(ax, start=None, end=None):
for rec_start, rec_end in RECESSIONS:
s, e = pd.Timestamp(rec_start), pd.Timestamp(rec_end)
if start is not None and e < pd.Timestamp(start):
continue
if end is not None and s > pd.Timestamp(end):
continue
ax.axvspan(s, e, color=EO_CHARCOAL, alpha=0.08, lw=0)
```
```{python}
#| label: data-download
#| include: false
#| cache: true
from pathlib import Path
DATA_PATH = Path("../../data/raw")
start = datetime(1954, 1, 1)
end = datetime(2019, 12, 31) # pre-COVID sample throughout
# ── GDP growth (annualised QoQ, percent) ──────────────────────────────────────
gdp_raw = pd.read_csv(DATA_PATH / "GDPC1.csv", index_col="date", parse_dates=True).loc[str(start):str(end)]
gdp_raw.columns = ["Real GDP"]
gdp_raw["Log GDP"] = np.log(gdp_raw["Real GDP"])
gdp_raw["GDP Growth"] = gdp_raw["Log GDP"].diff() * 400
# ── CPI inflation (annualised QoQ, percent) ───────────────────────────────────
cpi_raw = pd.read_csv(DATA_PATH / "CPIAUCSL.csv", index_col="date", parse_dates=True).loc[str(start):str(end)]
cpi_raw.columns = ["CPI"]
cpi_raw["Log CPI"] = np.log(cpi_raw["CPI"])
cpi_raw["CPI Inflation"] = cpi_raw["Log CPI"].diff() * 400
# ── Federal funds rate (percent, quarterly average) ───────────────────────────
ffr_raw = pd.read_csv(DATA_PATH / "FEDFUNDS.csv", index_col="date", parse_dates=True).loc[str(start):str(end)]
ffr_raw.columns = ["FFR"]
ffr_q = ffr_raw.resample("QS").mean() # average within quarter
# ── Merge on quarterly frequency ──────────────────────────────────────────────
data = (gdp_raw[["GDP Growth"]]
.join(cpi_raw[["CPI Inflation"]], how="inner")
.join(ffr_q, how="inner")
.dropna())
# Restrict to sample with full FFR coverage (funds rate begins 1954Q3)
data = data.loc["1954-07-01":"2019-10-01"].copy()
SAMPLE_START = data.index[0].strftime("%Y-%m-%d")
SAMPLE_END = data.index[-1].strftime("%Y-%m-%d")
```
::: {.callout-note}
## Learning Objectives
By the end of this chapter, you will be able to:
- Explain why single-equation models fail when regressors are endogenous, and
show how the reduced-form VAR arises as the natural solution
- Write down a VAR($p$) model in both equation-by-equation and compact matrix
form, and state the stationarity condition in terms of the companion matrix
- Select VAR lag length using information criteria and the likelihood ratio test,
and interpret the penalty structure relative to the univariate case
- Test for Granger causality using the block exclusion F-test and interpret
results for a three-variable system
- Explain the identification problem in structural VARs, apply the Cholesky
decomposition as a recursive identification scheme, and articulate the economic
assumption it embeds
- Derive impulse response functions from the VMA representation and interpret
the dynamic response of each variable to a structural shock
- Construct a forecast error variance decomposition and interpret how the share
of variance attributed to each shock evolves with horizon
- Diagnose a VAR estimated on integrated variables and explain why that case
requires the cointegration framework in Chapter 8
:::
This chapter extends our toolkit from one variable to many. The arc follows
naturally from where Chapter 6 left us: we can model a single series, test
for breaks in its parameters, and produce honest out-of-sample forecasts — but
we cannot ask how shocks in one variable propagate to another, or whether a
joint policy change shifted two series simultaneously. Section 7.1 builds the
case for a multivariate framework by revisiting the endogeneity problem from
Chapter 3 and showing how it motivates the VAR structure. Section 7.2 formalises
the model. Sections 7.3 and 7.4 cover the two diagnostic tools that precede
structural analysis: lag selection and Granger causality. Section 7.5 addresses
structural identification — the Cholesky decomposition — which is needed before
we can interpret shocks. Sections 7.6 and 7.7 then develop impulse response
functions and forecast error variance decomposition, the two main outputs of
a structural VAR analysis. The chapter closes in Section 7.8 by flagging what
goes wrong when variables are integrated, motivating the cointegration framework
of Chapter 8.
## Why Univariate Models Are Not Enough {#sec-motivation}
Chapter 3 introduced the ARMAX model and attached a warning: if the exogenous
regressor $x_t$ is itself influenced by $y_t$ — if causality runs in both
directions — then the ARMAX coefficient on $x_t$ is biased and inconsistent.
We flagged this when adding the federal funds rate to the jobless claims model,
noted that the funds rate almost certainly responds to labor market conditions,
and concluded that the negative coefficient we estimated should not be read as
a structural effect of monetary policy. The coefficient was a reduced-form
mixture, not a causal parameter.
That warning carries a natural follow-up question: if we cannot safely add
one variable as an exogenous regressor in the other's equation, what should
we do instead? The answer is to give up the idea of a single equation altogether
and model both variables jointly. This is the logic of the [VAR](https://en.wikipedia.org/wiki/Vector_autoregression).
### A Structural System and Its Reduced Form
To see where the VAR comes from, start with a simple example involving two
variables: output growth $g_t$ and inflation $\pi_t$. Suppose the true
data-generating process involves genuine two-way dependence between them.
The [Phillips curve](https://en.wikipedia.org/wiki/Phillips_curve) — one of the most studied relationships in macroeconomics
— captures exactly this: inflation rises when output is above potential, and
central banks use something close to this trade-off in their own forecasting
models. A minimal structural representation is:
$$\begin{aligned}
g_t &= \alpha_{10} - \alpha_{12}\,\pi_t + \gamma_{11}\,g_{t-1} +
\gamma_{12}\,\pi_{t-1} + \varepsilon^g_t \\
\pi_t &= \alpha_{20} - \alpha_{21}\,g_t + \gamma_{21}\,g_{t-1} +
\gamma_{22}\,\pi_{t-1} + \varepsilon^\pi_t
\end{aligned}$$
Each equation has a contemporaneous term on the right-hand side: output growth
appears directly in the inflation equation, and inflation appears directly in
the growth equation. The structural shocks $\varepsilon^g_t$ and
$\varepsilon^\pi_t$ are assumed to be uncorrelated with each other — they
represent genuine independent disturbances to each variable — but notice that
neither equation can be estimated by OLS. In the first equation, $\pi_t$ is
correlated with $\varepsilon^g_t$ because $\pi_t$ itself depends on $g_t$
through the second equation. Each contemporaneous regressor is endogenous
by construction.
The standard solution is to **solve the system for the endogenous variables**.
Write the two equations in matrix form:
$$\begin{bmatrix} 1 & \alpha_{12} \\ \alpha_{21} & 1 \end{bmatrix}
\begin{bmatrix} g_t \\ \pi_t \end{bmatrix}
=
\begin{bmatrix} \alpha_{10} \\ \alpha_{20} \end{bmatrix}
+
\begin{bmatrix} \gamma_{11} & \gamma_{12} \\ \gamma_{21} & \gamma_{22} \end{bmatrix}
\begin{bmatrix} g_{t-1} \\ \pi_{t-1} \end{bmatrix}
+
\begin{bmatrix} \varepsilon^g_t \\ \varepsilon^\pi_t \end{bmatrix}$$
Call the left-hand coefficient matrix $\mathbf{A}_0$, the lag coefficient
matrix $\mathbf{A}_1$, and the structural shock vector $\boldsymbol{\varepsilon}_t$.
Premultiplying both sides by $\mathbf{A}_0^{-1}$ isolates the current-period
variables:
$$\begin{bmatrix} g_t \\ \pi_t \end{bmatrix}
= \mathbf{A}_0^{-1}
\begin{bmatrix} \alpha_{10} \\ \alpha_{20} \end{bmatrix}
+ \mathbf{A}_0^{-1}\mathbf{A}_1
\begin{bmatrix} g_{t-1} \\ \pi_{t-1} \end{bmatrix}
+ \mathbf{A}_0^{-1}\boldsymbol{\varepsilon}_t$$
This is the **[reduced form](https://en.wikipedia.org/wiki/Reduced_form)**. Each variable is expressed as a function of
its own lags, the lags of all other variables, and a composite error term
$\mathbf{u}_t = \mathbf{A}_0^{-1}\boldsymbol{\varepsilon}_t$. There are no
contemporaneous terms on the right-hand side — the system is self-contained.
The reduced form can be estimated consistently by OLS, equation by equation.
The price we pay is that the composite errors $\mathbf{u}_t$ are no longer
structurally interpretable. They are linear combinations of the original
structural shocks $\varepsilon^g_t$ and $\varepsilon^\pi_t$, mixed together
by the elements of $\mathbf{A}_0^{-1}$. A shock to the first reduced-form
equation is not a pure output shock — it contains contamination from the
inflation shock, in proportions determined by the unknown entries of
$\mathbf{A}_0$. Recovering the structural shocks from the reduced-form
residuals is the **identification problem**, which Section 7.5 addresses
directly.
::: {.callout-warning icon=false}
## Reduced-Form Coefficients Are Not Structural Parameters
The coefficients estimated in each VAR equation are the elements of
$\mathbf{A}_0^{-1}\mathbf{A}_1$ — they mix together the structural slope
parameters in ways that generally cannot be disentangled without additional
identifying restrictions. This is the same message as the ARMAX endogeneity
warning in Chapter 3, now stated in matrix form. The reduced-form VAR is
appropriate for forecasting, for Granger causality tests, and as the
starting point for structural analysis — but the coefficients themselves
do not have direct structural interpretations.
:::
### Extending to Three Variables
The two-variable sketch above extends directly to three or more variables. For
our running example we use three series that jointly capture the macroeconomic
environment in which monetary policy operates: **annualised real GDP growth**
($g_t$, percent), **annualised CPI inflation** ($\pi_t$, percent), and the
**federal funds rate** ($r_t$, percent). The structural motivation extends
naturally: a third equation describes how the central bank sets the funds rate
in response to output and inflation — a Taylor-rule-like reaction function —
while output and inflation both respond to the interest rate with a lag. As
before, each equation has contemporaneous right-hand-side variables, and as
before, the reduced form that emerges from solving the system has no
contemporaneous terms: only lags and composite errors.
```{python}
#| label: fig-three-series
#| fig-cap: "GDP growth, CPI inflation, and the federal funds rate, 1954Q3–2019Q4.
#| GDP growth and CPI inflation reject the unit root null comfortably; the FFR
#| does not, but the lag polynomial roots confirm the estimated VAR is stable
#| (Section 7.2). The co-movement during recessions and the joint Volcker-era
#| spike in inflation and rates illustrate why a univariate model for any one
#| series gives an incomplete picture of the system."
fig, axes = plt.subplots(3, 1, figsize=(6, 6), sharex=True)
series = [
("GDP Growth", EO_COPPER, "GDP Growth (ann. %)"),
("CPI Inflation", EO_SKYBLUE, "CPI Inflation (ann. %)"),
("FFR", EO_TERRACOTTA, "Fed Funds Rate (%)"),
]
for ax, (col, color, ylabel) in zip(axes, series):
ax.plot(data.index, data[col], color=color, lw=1.0)
ax.axhline(0, color=EO_CHARCOAL, lw=0.5, ls="--", alpha=0.4)
shade_recessions(ax, start=SAMPLE_START, end=SAMPLE_END)
ax.set_xlim(data.index[0], data.index[-1])
ax.set_ylabel(ylabel, fontsize=7)
eo_style_ax(ax)
axes[-1].set_xlabel("Quarter")
eo_suptitle(fig, "Three-Variable VAR System: GDP Growth, Inflation, and the Funds Rate")
fig.tight_layout()
plt.show()
```
*Three-panel time series for the VAR system. GDP growth (copper) is volatile
and mean-reverting; CPI inflation (sky blue) shows the 1970s surge and the
Volcker-era disinflation clearly; the federal funds rate (terracotta) tracks
and often leads the inflation cycle. All three series share the pre-COVID
sample restriction used throughout this textbook.*
The visual confirms two things immediately. First, the three series move
together in economically recognisable ways — recessions coincide with output
contractions, inflation surges, and often large policy rate changes. A model
that treats any one of them in isolation misses those joint dynamics. Second,
GDP growth and CPI inflation are clearly stationary; the FFR is more
persistent, but formal stability checks after estimation confirm the VAR in
levels is well-behaved, as Section 7.2 discusses.
## The Reduced-Form VAR {#sec-varmodel}
### From Two Variables to $n$
The two-variable reduced form derived in Section 7.1 has an obvious
generalisation. Let $\mathbf{y}_t$ be an $n \times 1$ vector of stationary
time series. A **VAR of order $p$** — written VAR($p$) — models the current
vector as a linear function of the previous $p$ vectors plus a vector of
white noise innovations:
$$\mathbf{y}_t = \mathbf{c} + \mathbf{A}_1 \mathbf{y}_{t-1} +
\mathbf{A}_2 \mathbf{y}_{t-2} + \cdots +
\mathbf{A}_p \mathbf{y}_{t-p} + \mathbf{u}_t \tag{7.1}$$
where $\mathbf{c}$ is an $n \times 1$ vector of intercepts, each $\mathbf{A}_i$
is an $n \times n$ matrix of coefficients, and $\mathbf{u}_t$ is an $n \times 1$
vector of reduced-form innovations satisfying:
$$\mathbb{E}[\mathbf{u}_t] = \mathbf{0}, \qquad
\mathbb{E}[\mathbf{u}_t \mathbf{u}_t'] = \boldsymbol{\Sigma}_u, \qquad
\mathbb{E}[\mathbf{u}_t \mathbf{u}_s'] = \mathbf{0} \text{ for } t \neq s$$
The covariance matrix $\boldsymbol{\Sigma}_u$ is symmetric and positive
definite but not generally diagonal — the reduced-form innovations in different
equations are correlated, precisely because they are mixtures of the structural
shocks as shown in Section 7.1.
For our three-variable system ($n = 3$, with
$\mathbf{y}_t = [g_t,\, \pi_t,\, r_t]'$), equation (7.1) at lag $p = 1$
written out explicitly gives:
$$\begin{bmatrix} g_t \\ \pi_t \\ r_t \end{bmatrix}
= \begin{bmatrix} c_1 \\ c_2 \\ c_3 \end{bmatrix}
+ \begin{bmatrix}
a_{11} & a_{12} & a_{13} \\
a_{21} & a_{22} & a_{23} \\
a_{31} & a_{32} & a_{33}
\end{bmatrix}
\begin{bmatrix} g_{t-1} \\ \pi_{t-1} \\ r_{t-1} \end{bmatrix}
+ \begin{bmatrix} u^g_t \\ u^\pi_t \\ u^r_t \end{bmatrix}$$
Each row is a regression equation. The first row says that GDP growth today
depends on last quarter's GDP growth ($a_{11}$), last quarter's inflation
($a_{12}$), and last quarter's funds rate ($a_{13}$), plus a composite
innovation $u^g_t$. The same structure applies to each equation. With $p$
lags, each equation has $np$ slope coefficients plus one intercept, so
$np + 1$ parameters per equation and $n(np + 1)$ in the full VAR. For
$n = 3$ and $p = 2$ that is $3(3 \times 2 + 1) = 21$ parameters total —
the count grows quickly, which is why lag selection (Section 7.3) matters.
`statsmodels` reports this count inclusive of intercepts, so it matches
the formula directly.
::: {.callout-note}
## Why OLS Works Equation by Equation
Because the right-hand side of each VAR equation contains only lagged
values — no contemporaneous terms — every regressor is predetermined and
uncorrelated with the current-period innovation $\mathbf{u}_t$. OLS applied
to each equation separately is therefore consistent and, under Gaussian
innovations, equivalent to maximum likelihood. In practice `statsmodels`
estimates all equations simultaneously using the system representation, but
the equation-by-equation OLS intuition is exact and useful for understanding
what the estimator is doing.
:::
### The Stationarity Condition
Recall from Chapter 3 that an AR($p$) model has exactly $p$ characteristic
roots, and the model is stationary when all of them lie outside the unit circle
— equivalently, when their reciprocals lie inside it. The VAR($p$) generalises
this in a precise way: a system of $n$ variables each modelled with $p$ lags
has exactly $np$ characteristic roots. For our three-variable VAR($p$), that
is $3p$ roots in total — three times as many as a univariate AR($p$) of the
same order. The stationarity requirement is the same: every one of those $np$
roots must lie strictly inside the unit circle.
A concrete example makes the count clear. Consider our three-variable system
($n = 3$, variables: GDP growth, CPI inflation, FFR):
- **VAR(1):** one lag matrix $\mathbf{A}_1$ is $3 \times 3$. The companion
matrix $\mathbf{F} = \mathbf{A}_1$ is also $3 \times 3$, giving $np = 3$
roots — one per variable. This is what `statsmodels` reports when $p = 1$.
- **VAR(2):** two lag matrices $\mathbf{A}_1$ and $\mathbf{A}_2$, each $3 \times 3$.
The companion matrix is $6 \times 6$, giving $np = 6$ roots — two per variable.
- **VAR(6):** the companion matrix is $18 \times 18$, giving $np = 18$ roots —
six per variable.
The key point is that adding lags does not merely add one root: it adds $n$
roots simultaneously, one for each variable in the system. At $p = 6$ with
$n = 3$ we must verify 18 roots, all of which need to lie strictly inside the
unit circle for the VAR to be stable.
Why $np$? Intuitively, each of the $n$ equations contributes $p$ dynamic
relationships to the system. Stacking the $p$ lags of all $n$ variables into
a single first-order system — the companion form — produces an $np \times np$
matrix $\mathbf{F}$ whose eigenvalues are exactly those $np$ roots. The
companion form is simply a bookkeeping device that reduces the higher-order
vector system to a first-order one, the same trick used for AR($p$) models in
Chapter 3.
::: {.callout-note}
## The Companion Matrix
Any VAR($p$) can be written as a first-order system by stacking the current
and $p-1$ lagged vectors into $\boldsymbol{\xi}_t = [\mathbf{y}_t',\,
\mathbf{y}_{t-1}',\, \ldots,\, \mathbf{y}_{t-p+1}']'$:
$$\boldsymbol{\xi}_t = \mathbf{F}\,\boldsymbol{\xi}_{t-1} + \tilde{\mathbf{u}}_t \tag{7.2}$$
The **companion matrix** $\mathbf{F}$ is $np \times np$ and collects all the
VAR coefficient matrices along its first block row, with identity matrices
below forming a block-shift structure:
$$\mathbf{F} = \begin{bmatrix}
\mathbf{A}_1 & \mathbf{A}_2 & \cdots & \mathbf{A}_{p-1} & \mathbf{A}_p \\
\mathbf{I}_n & \mathbf{0} & \cdots & \mathbf{0} & \mathbf{0} \\
\mathbf{0} & \mathbf{I}_n & \cdots & \mathbf{0} & \mathbf{0} \\
\vdots & \vdots & \ddots & \vdots & \vdots \\
\mathbf{0} & \mathbf{0} & \cdots & \mathbf{I}_n & \mathbf{0}
\end{bmatrix}$$
The eigenvalues of $\mathbf{F}$ are the $np$ characteristic roots of the VAR.
We will also use $\mathbf{F}$ directly when deriving impulse response functions
in Section 7.6.
:::
::: {.callout-note}
## Definition 7.1 — VAR Stability
A VAR($p$) with $n$ variables is **stable** (covariance-stationary) if and
only if all $np$ eigenvalues of the companion matrix $\mathbf{F}$ lie strictly
inside the unit circle:
$$|\lambda_i(\mathbf{F})| < 1 \quad \text{for all } i = 1, \ldots, np$$
For our three-variable system estimated at lag order $p$, this means checking
$3p$ eigenvalues. At $p = 1$ there are 3 roots; at $p = 6$ — the lag order
selected after diagnostics in Section 7.3.6 — there are 18. `statsmodels`
computes and reports all of them automatically after estimation.
:::
The consequences of instability are the same as in the univariate case. A root
on the unit circle means the system has a stochastic trend — shocks have
permanent effects and variances grow without bound. A root outside means the
system is explosive. Either failure makes OLS inference unreliable, impulse
responses non-convergent, and forecasts misleading in exactly the ways
described in Chapter 4. Checking the modulus of every companion eigenvalue
after estimation is therefore not a formality but a diagnostic step that
validates everything that follows.
::: {.callout-warning icon=false}
## VAR Estimation Requires Stationary Variables
**Before estimating a VAR, every variable in the system must be tested for
unit roots and confirmed to be stationary.** A VAR estimated on $I(1)$
variables produces unreliable inference: $t$-statistics do not follow standard
distributions, information criteria are distorted, and impulse responses do
not converge to zero.
If one or more variables are $I(1)$, there are two responses. Differencing
before inclusion removes the unit root but discards information about long-run
relationships between the series. The vector error correction model (VECM)
retains that long-run information by explicitly modelling cointegrating
relationships among the $I(1)$ variables. We defer the VECM to Chapter 8.
For now: if your series are not stationary, do not estimate a VAR in levels.
:::
```{python}
#| label: unit-root-checks
#| include: false
# Run ADF tests on each series to verify stationarity before VAR estimation
adf_results = {}
for col in ["GDP Growth", "CPI Inflation", "FFR"]:
result = adfuller(data[col].dropna(), autolag="AIC", regression="c")
adf_results[col] = {"stat": result[0], "pval": result[1],
"crit_5": result[4]["5%"]}
```
```{python}
#| label: tbl-adf
header = (f"{'Series':<18} {'ADF stat':>10} {'p-value':>10} "
f"{'Crit. (5%)':>12} {'Decision':>12}")
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
for col, res in adf_results.items():
decision = "Stationary" if res["pval"] < 0.05 else "Unit root?"
print(f"{col:<18} {res['stat']:>10.3f} {res['pval']:>10.4f} "
f"{res['crit_5']:>12.3f} {decision:>12}")
print(sep)
print("Note: ADF with intercept, lag length by AIC. H\u2080: unit root.")
```
*Unit root pre-checks for the three VAR variables, 1954Q3–2019Q4. GDP growth
and CPI inflation reject the null comfortably. The FFR does not reject
(ADF = −1.954, p = 0.307), consistent with its well-known persistence over
long samples. A non-rejection by the ADF does not necessarily mean the
series is integrated — the test has low power against highly persistent but
stationary processes. The definitive check is whether the estimated VAR is
stable, assessed via the roots of the lag polynomial in Table 7.2.*
## VAR Estimation and Model Selection {#sec-lagselection}
This section develops the full model selection and estimation workflow for
the VAR in six parts. We begin by explaining why lag length matters more in
the multivariate case than in the univariate one. We then introduce
information criteria and the likelihood ratio test as the two standard tools
for choosing the lag order and apply them to our running example. With a
candidate lag order in hand, we estimate the initial VAR(1), verify its
stability, and run residual diagnostics — which reveal that the BIC-preferred
model is misspecified. We explain the iterative process a researcher follows
to arrive at a working model, and close with the VAR(6) that underlies all
structural analysis in the remainder of the chapter.
### Why Lag Length Matters More in a VAR
In a univariate AR($p$) model, adding one lag costs one parameter. In a
VAR($p$) with $n$ variables, adding one lag costs $n^2$ parameters — one for
each cell in the new coefficient matrix $\mathbf{A}_p$. For our three-variable
system, moving from $p = 1$ to $p = 2$ adds nine parameters simultaneously.
At $p = 4$, the VAR has $3 \times (3 \times 4) + 3 = 39$ free parameters,
and the number of observations consumed by presample initialisation grows
with $p$ as well. The tradeoff between dynamic richness and estimation
precision is sharper in the multivariate case, which is why lag selection
deserves explicit attention before any structural analysis is attempted.
The conceptual problem is the same as in the univariate case. Too few lags
leaves residual serial correlation in $\mathbf{u}_t$, violating the white
noise assumption and biasing everything downstream — including Granger
causality tests, IRFs, and FEVDs. Too many lags wastes degrees of freedom,
inflates standard errors, and can destabilise the coefficient estimates,
particularly in short samples. We want the smallest $p$ consistent with
white noise residuals.
### Information Criteria for VARs
The AIC, BIC, and HQC from Chapters 3 and 4 extend directly to VARs with
two modifications. First, the goodness-of-fit term becomes the
log-determinant of the residual covariance matrix,
$\ln|\hat{\boldsymbol{\Sigma}}_u(p)|$, rather than a scalar residual
variance — it plays the same role but summarises the joint fit of all $n$
equations in a single number. Second, each additional lag costs $n^2$
parameters rather than one, because a new $n \times n$ coefficient matrix
$\mathbf{A}_p$ enters every equation simultaneously. For our three-variable
system the penalty per lag is nine, compared to one in the corresponding
univariate AR. In a five-variable VAR it would be 25, and in a ten-variable
VAR it would be 100 — one reason practitioners favour small tightly
parameterised systems and resist adding variables that are not directly
relevant to the question at hand.
The relative behaviour of the three criteria mirrors the univariate case.
BIC imposes the heaviest penalty per parameter and tends to select shorter
lag orders; it is consistent in large samples but can underfit in moderate
samples. AIC tolerates more lags and tends to capture richer dynamics at
the cost of some parsimony. HQC sits between them. It is common practice
to report all three, treat each selected order as a candidate, and use
residual diagnostics — rather than any single criterion — to determine the
working model. As the empirical results will show, information criteria give
a starting point but not a final answer.
### The Likelihood Ratio Test
As a complement to information criteria, the **[likelihood ratio (LR) test](https://en.wikipedia.org/wiki/Likelihood-ratio_test)**
compares two nested lag orders directly. The null hypothesis is $p = p_0$
(the restricted model); the alternative is $p = p_1 > p_0$ (the unrestricted
model). The test statistic is:
$$\text{LR} = (T - p_1 n)\left[\ln|\hat{\boldsymbol{\Sigma}}_u(p_0)| -
\ln|\hat{\boldsymbol{\Sigma}}_u(p_1)|\right] \tag{7.3}$$
Under the null, $\text{LR} \sim \chi^2(n^2(p_1 - p_0))$. The degrees of
freedom equal the number of restrictions imposed: each lag that is dropped
removes $n^2$ coefficients from the system. The finite-sample correction
$(T - p_1 n)$ in place of $T$ improves size in small samples.
The LR test is applied sequentially in practice: start at $p_{\max}$, test
down against $p_{\max} - 1$, and continue until the null is first rejected,
taking that lag order as the selected value (equation 7.3). When the LR test
and information criteria agree, confidence in the selection is higher; when
they disagree, the range of recommendations defines a set of candidates worth
examining with residual diagnostics.
### Lag Selection Results
```{python}
#| label: lag-selection
# Fit VAR at each lag order up to p_max = 6 and collect criteria
p_max = 6
var_data = data[["GDP Growth", "CPI Inflation", "FFR"]]
model = VAR(var_data)
# Collect AIC / BIC / HQC for p = 1 ... p_max
rows = []
for p in range(1, p_max + 1):
r = model.fit(p)
rows.append({"p": p, "AIC": r.aic, "BIC": r.bic, "HQC": r.hqic,
"logdet": float(np.log(np.linalg.det(r.sigma_u))),
"T": len(r.resid)})
ic_df = pd.DataFrame(rows).set_index("p")
aic_min = ic_df["AIC"].idxmin()
bic_min = ic_df["BIC"].idxmin()
hqc_min = ic_df["HQC"].idxmin()
# ── Information criteria table ─────────────────────────────────────────────
header = f"{'p':>3} {'AIC':>10} {'BIC':>10} {'HQC':>10}"
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
for p, row in ic_df.iterrows():
a_star = "*" if p == aic_min else " "
b_star = "*" if p == bic_min else " "
h_star = "*" if p == hqc_min else " "
print(f"{p:>3} {row['AIC']:>9.3f}{a_star} "
f"{row['BIC']:>9.3f}{b_star} "
f"{row['HQC']:>9.3f}{h_star}")
print(sep)
print("* denotes minimum. Sample: 1954Q3–2019Q4. n = 3 variables.")
# ── Sequential LR test (top-down: p_max down to p=1) ──────────────────────
print()
print("Sequential LR test (H\u2080: restricted model; top-down from p_max=6):")
n_vars = var_data.shape[1]
lh = f"{'H0: p':>8} {'H1: p':>8} {'LR stat':>10} {'df':>6} {'p-value':>10} {'Reject?':>8}"
ls = "─" * len(lh)
print(ls)
print(lh)
print(ls)
for p1 in range(p_max, 1, -1):
p0 = p1 - 1
T_eff = ic_df.loc[p1, "T"]
lrstat = (T_eff - p1 * n_vars) * (ic_df.loc[p0, "logdet"] -
ic_df.loc[p1, "logdet"])
df = n_vars ** 2
from scipy.stats import chi2
pval = 1 - chi2.cdf(lrstat, df)
reject = "Yes" if pval < 0.05 else "No"
print(f"{p0:>8} {p1:>8} {lrstat:>10.3f} {df:>6} {pval:>10.4f} {reject:>8}")
print(ls)
print("df = n² = 9 per step. Reject H₀ → retain the unrestricted model.")
```
*Lag-order selection criteria and sequential LR test for the three-variable
VAR, 1954Q3–2019Q4. The top panel shows information criteria: BIC selects
$p = 1$, HQC selects $p = 3$, and AIC selects $p = 5$. The bottom panel
shows the sequential LR test working top-down from $p = 6$: each step tests
whether the additional $n^2 = 9$ parameters of the higher-order model are
jointly significant. Where the LR test and information criteria agree,
confidence in the selection is higher; where they disagree, residual
diagnostics determine the working model.*
The selected lag order governs everything downstream in this chapter. We use
it for all subsequent estimation — Granger causality tests, IRFs, and FEVDs —
and report it explicitly so that every result can be traced back to this choice.
If AIC and BIC disagree, we carry both candidates forward as a robustness
check when the results are sensitive to lag length.
### Estimated VAR(1)
Like the ARMA identification workflow in Chapter 3, VAR model selection is
iterative. We begin with the BIC-preferred lag order, estimate the model,
verify stability, inspect the residuals, and revise upward if the diagnostics
indicate that systematic dynamics remain. The output below is for the
BIC-selected VAR(1); Section 7.3.7 presents the working model after the
iterative revision.
The `statsmodels` summary produces one block per equation — each variable's
own lags and the cross-variable lags, along with standard errors,
$t$-statistics, and equation-level fit statistics. This is the multivariate
analogue of the ARMA summary in Chapter 3: a separate regression
table for each endogenous variable, sharing the same right-hand side structure.
```{python}
#| label: var-summary
#| include: false
p_sel = int(ic_df["BIC"].idxmin())
var_fit = VAR(var_data).fit(p_sel)
```
```{python}
#| label: tbl-var-full
# Print the complete VAR(1) summary — one block per equation.
print(var_fit.summary())
```
*Estimated VAR(1) by OLS, 1954Q3–2019Q4. The output is one regression table
per endogenous variable. Each block lists: constant, own lag, and lags of
the other two variables. These are reduced-form mixtures — they do not have
direct structural interpretations without the Cholesky identification of
Section 7.5.*
### Stability: VAR(1)
We verify the system is stable by examining the roots of the lag polynomial —
the values of $z$ for which
$\det(\mathbf{I} - \mathbf{A}_1 z - \cdots - \mathbf{A}_p z^p) = 0$.
::: {.callout-warning icon=false}
## Roots vs Eigenvalues: A Convention to Watch
The stability condition can be stated two equivalent ways. In terms of the
**companion matrix** $\mathbf{F}$, stability requires all *eigenvalues* to
lie strictly *inside* the unit circle ($|\lambda_i| < 1$). In terms of the
**lag polynomial**, stability requires all *roots* to lie strictly *outside*
the unit circle ($|z_i| > 1$). The two conditions are equivalent because
lag polynomial roots are the reciprocals of companion eigenvalues:
$z_i = 1/\lambda_i$.
`statsmodels` reports lag polynomial **roots** (not eigenvalues), so the
stability criterion is that all reported moduli exceed one. A modulus of 7.56
means the corresponding companion eigenvalue is $1/7.56 = 0.13$ — well inside
the unit circle, confirming stability. A modulus near 1.0 from below would
signal near-instability; a modulus below 1.0 would signal an actual unit root.
:::
```{python}
#| label: tbl-eigenvalues
p_sel = int(ic_df["BIC"].idxmin())
var_fit = VAR(var_data).fit(p_sel)
roots = var_fit.roots
comp_eigs = 1.0 / np.array(roots)
stable = all(r > 1.0 for r in roots)
col_w = 14
header = (f"{'Root':>6} {'|Root|':>{col_w}} "
f"{'|Comp. eig.|':>{col_w}} {'Outside circle?':>16}")
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
for i, (r, e) in enumerate(
sorted(zip(roots, comp_eigs), key=lambda x: -x[0]), 1):
outside = "Yes" if abs(r) > 1.0 else "NO — UNSTABLE"
print(f"{i:>6} {abs(r):>{col_w}.4f} "
f"{abs(e):>{col_w}.4f} {outside:>16}")
print(sep)
print(f"VAR({p_sel}), n=3: {len(roots)} roots. Stable: {'Yes' if stable else 'No'}.")
print("Stability: |root| > 1 ↔ |companion eigenvalue| < 1.")
```
*Lag polynomial roots and companion eigenvalue moduli for the VAR(1). All
three roots exceed one and all companion eigenvalues fall strictly inside the
unit circle — the VAR(1) is stable despite the FFR's ambiguous ADF result.*
```{python}
#| label: fig-unit-circle
#| fig-cap: "Companion matrix eigenvalues for the VAR(1) plotted against the
#| unit circle. All three eigenvalues are real (no complex pairs at p=1)
#| and lie strictly inside the circle, confirming stability."
theta = np.linspace(0, 2 * np.pi, 300)
fig, ax = plt.subplots(figsize=(4, 4))
ax.plot(np.cos(theta), np.sin(theta),
color=EO_CHARCOAL, lw=0.8, ls="--", alpha=0.5, label="Unit circle")
ax.axhline(0, color=EO_CHARCOAL, lw=0.4, alpha=0.4)
ax.axvline(0, color=EO_CHARCOAL, lw=0.4, alpha=0.4)
# Reconstruct companion matrix from fit.coefs (works for any p)
n_v = var_data.shape[1]
p_v = p_sel
F1 = np.zeros((n_v * p_v, n_v * p_v))
for lag in range(p_v):
F1[:n_v, lag * n_v:(lag + 1) * n_v] = var_fit.coefs[lag]
if p_v > 1:
F1[n_v:, :n_v * (p_v - 1)] = np.eye(n_v * (p_v - 1))
eig1 = np.linalg.eigvals(F1)
ax.scatter(eig1.real, eig1.imag, color=EO_COPPER, s=60,
zorder=5, label="Companion eigenvalues")
ax.set_xlim(-1.3, 1.3)
ax.set_ylim(-1.3, 1.3)
ax.set_aspect("equal")
ax.set_xlabel("Real part")
ax.set_ylabel("Imaginary part")
ax.legend(fontsize=6, loc="upper right")
ax.set_title("VAR(1) Stability", fontsize=8)
eo_style_ax(ax)
fig.tight_layout()
plt.show()
```
*All three VAR(1) companion eigenvalues are real and lie on the horizontal
axis, well inside the unit circle. The VAR(1) is stable — but stability
is a necessary, not sufficient, condition for a well-specified model.
The residual diagnostics below reveal a more serious problem.*
### Residual Diagnostics: VAR(1)
Does the estimated VAR(1) actually remove all systematic dynamics from the
data? A VAR with too few lags will leave autocorrelation in the residuals —
$\mathbf{u}_t$ will not be white noise, and every downstream result (Granger
tests, IRFs, FEVDs) will be unreliable. The standard diagnostic is the
**multivariate portmanteau test**, which tests the null hypothesis that all
residual autocovariance matrices up to lag $h$ are jointly zero:
$$H_0:\; \boldsymbol{\Gamma}(1) = \boldsymbol{\Gamma}(2) = \cdots =
\boldsymbol{\Gamma}(h) = \mathbf{0}$$
Under the null the test statistic is asymptotically $\chi^2$; rejection
means residual autocorrelation is present and the lag order should be
increased.
```{python}
#| label: var1-diagnostics
#| include: false
from statsmodels.stats.stattools import durbin_watson
from statsmodels.tsa.stattools import acf as sm_acf
from scipy.stats import jarque_bera
var_cols = var_data.columns.tolist()
resids_1 = var_fit.resid # VAR(1) residuals
port_1 = var_fit.test_whiteness(nlags=12, adjusted=True)
diag_rows_1 = []
for col in var_cols:
r = resids_1[col].values
dw = durbin_watson(r)
jb_stat, jb_p = jarque_bera(r)
diag_rows_1.append({"eq": col, "DW": dw,
"JB stat": jb_stat, "JB p": jb_p})
diag_df_1 = pd.DataFrame(diag_rows_1)
```
```{python}
#| label: tbl-diagnostics-var1
print(f"Multivariate portmanteau test — VAR(1) residuals (h=12, adjusted)")
print(f" Statistic : {port_1.test_statistic:.3f}")
print(f" df : {port_1.df}")
print(f" p-value : {port_1.pvalue:.4f}")
decision = ("No evidence of autocorrelation"
if port_1.pvalue > 0.05 else "Residual autocorrelation detected")
print(f" Decision : {decision}")
print()
header = f"{'Equation':<20} {'DW':>8} {'JB stat':>10} {'JB p':>10} {'Normal?':>10}"
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
for _, row in diag_df_1.iterrows():
normal = "Yes" if row["JB p"] > 0.05 else "No"
print(f"{row['eq']:<20} {row['DW']:>8.3f} "
f"{row['JB stat']:>10.3f} {row['JB p']:>10.4f} {normal:>10}")
print(sep)
print("DW ≈ 2: no first-order autocorrelation. JB H\u2080: residuals normal.")
```
*Residual diagnostics for the VAR(1). The portmanteau test rejects
decisively (test statistic 232.1, df 99, p = 0.000) — strong evidence
of residual autocorrelation. All three equations also fail Jarque-Bera,
indicating heavy-tailed residuals. The VAR(1) is stable but misspecified.*
```{python}
#| label: fig-resid-acf-var1
#| fig-cap: "Residual ACF — VAR(1). Bars outside the dashed 95 percent
#| confidence bands indicate significant autocorrelation. CPI inflation
#| shows a cluster of bars at lags 3–6; the FFR has a large spike at lag
#| 7; GDP growth is borderline at lags 1 and 5. The VAR(1) leaves
#| substantial systematic dynamics in the residuals."
n_lags_acf = 16
fig, axes = plt.subplots(1, 3, figsize=(6, 2.5), sharey=True)
colors = [EO_COPPER, EO_SKYBLUE, EO_TERRACOTTA]
conf_bound = 1.96 / np.sqrt(len(resids_1))
for ax, col, color in zip(axes, var_cols, colors):
acf_vals = sm_acf(resids_1[col].values, nlags=n_lags_acf, fft=True)[1:]
lags = np.arange(1, n_lags_acf + 1)
ax.bar(lags, acf_vals, color=color, alpha=0.7, width=0.6)
ax.axhline( conf_bound, color=EO_CHARCOAL, lw=0.8, ls="--", alpha=0.6)
ax.axhline(-conf_bound, color=EO_CHARCOAL, lw=0.8, ls="--", alpha=0.6)
ax.axhline(0, color=EO_CHARCOAL, lw=0.4)
ax.set_xlim(0.5, n_lags_acf + 0.5)
ax.set_title(col, fontsize=7)
ax.set_xlabel("Lag", fontsize=7)
eo_style_ax(ax)
axes[0].set_ylabel("ACF", fontsize=7)
eo_suptitle(fig, "Residual ACF — VAR(1) Equations")
fig.tight_layout()
plt.show()
```
*Residual ACF for the VAR(1). The patterns are clear: CPI inflation has a
cluster of significant bars at lags 3–6, suggesting unmodelled quarterly
price dynamics; the FFR shows a large spike at lag 7, consistent with a
business-cycle frequency the single lag cannot capture; GDP growth is
borderline at lags 1 and 5. These are exactly the patterns that motivate
increasing the lag order.*
The VAR(1) diagnostics deliver a clear verdict: the model is stable but
misspecified. The portmanteau test (p = 0.000) and the ACF plots tell the
same story — one lag is not enough to capture the dynamic interdependencies
in this three-variable system. The right response is the same as in
Chapter 3 when an AR(1) failed to whiten a persistent series: add lags
until the residuals are clean.
This is the natural teaching moment for iterative model building. In the
ARMA context, a significant ACF spike at lag $q$ prompted us to add an MA
term. Here, significant ACF spikes across multiple lags and multiple
equations prompt us to increase $p$ — the unit of revision in the
multivariate case is the entire lag matrix $\mathbf{A}_p$, adding $n^2$
parameters at once. We increase $p$ one step at a time, re-estimate, and
re-run the portmanteau test, until the residuals are as clean as the data
allow.
### From VAR(1) to VAR(6): How a Researcher Proceeds
```{python}
#| label: var-diagnostics
#| include: false
# ── Iterative lag order search (runs silently; results shown in VAR(6) section) ─
p_final = p_sel
var_fit_d = var_fit
port_hist = []
passed = False
for p_try in range(p_sel, p_max + 1):
fit_try = VAR(var_data).fit(p_try)
port_try = fit_try.test_whiteness(nlags=12, adjusted=True)
port_hist.append({"p": p_try,
"stat": port_try.test_statistic,
"df": port_try.df,
"pval": port_try.pvalue})
if port_try.pvalue > 0.05:
p_final = p_try
var_fit_d = fit_try
passed = True
break
if not passed:
p_final = p_max
var_fit_d = VAR(var_data).fit(p_max)
resids_d = var_fit_d.resid
pass_note = ("passed"
if passed
else f"did not pass at any order up to {p_max}; using VAR({p_max})")
diag_rows = []
for col in var_cols:
r = resids_d[col].values
dw = durbin_watson(r)
jb_stat, jb_p = jarque_bera(r)
diag_rows.append({"eq": col, "DW": dw,
"JB stat": jb_stat, "JB p": jb_p})
diag_df = pd.DataFrame(diag_rows)
```
The VAR(1) diagnostics have delivered a clear verdict: the model is stable
but misspecified. One lag is not enough to capture the dynamic
interdependencies in this system. A researcher facing this result does not
stop — they increase the lag order and repeat the diagnostic cycle.
The workflow is the same as ARMA model selection in Chapter 3, just applied
to the system as a whole. There, a significant ACF spike at lag $q$ prompted
adding an MA term; here, systematic residual autocorrelation across multiple
equations prompts adding an entire lag matrix $\mathbf{A}_p$ — nine new
parameters at once in our three-variable system. The logic is the same:
the model is expanded until the residuals are consistent with white noise.
Concretely, the researcher takes the following steps:
1. **Increase $p$ by one.** Re-estimate the VAR at $p = 2$.
2. **Re-run the portmanteau test.** Does the new model whiten the residuals?
3. **Re-inspect the ACF plots.** Have the problematic patterns been absorbed?
4. **If yes:** accept $p = 2$ as the working model. **If no:** go to step 1.
This process continues until either the portmanteau test passes or the
candidate range is exhausted. The candidate range should be set by economic
reasoning: for quarterly data, $p_{\max} = 4$ captures one full year of
lags; $p_{\max} = 6$ captures a year and a half, which is typically
sufficient for business cycle dynamics. We set $p_{\max} = 6$.
In our application no lag order within the candidate range passes the
portmanteau test at the five-percent level. This does not mean we abandon
the VAR — it means we use the highest-order model available, VAR(6), and
document the limitation honestly.
::: {.callout-note}
## Why VAR(6)? No Information Criterion Selected It
This is a good question. BIC selected $p = 1$, HQC selected $p = 3$, and
AIC selected $p = 5$. None pointed to $p = 6$.
The answer is that VAR(6) is selected by a different criterion: the
**residual diagnostic criterion**. Information criteria balance fit against
parsimony and give you a candidate model. But a candidate model that violates
its own assumptions — white noise residuals — is not an acceptable working
model regardless of how well it scores on AIC or BIC. When the residuals
fail the portmanteau test, the diagnostic overrides the information criterion.
We increase $p$ until the residuals pass, or until we exhaust the candidate
range. In this application we exhaust the range at $p = 6$. VAR(6) is
selected not because it minimises any criterion but because it is the most
dynamically rich model we examined and the one whose residuals are closest
to white noise within the candidate set.
The broader lesson: information criteria are a starting point. They tell
you the most efficient use of parameters given the model class. Residual
diagnostics tell you whether the model class — at that parameter count — is
adequate. Both inputs are needed to select a working model.
:::
The VAR(6) estimation, stability checks, and residual diagnostics follow.
### The Working Model: VAR(6)
```{python}
#| label: tbl-var-final
print(var_fit_d.summary())
```
*Estimated VAR(6) by OLS, 1954Q3–2019Q4. Each equation has 18 slope
coefficients (six lags of each of the three variables) plus an intercept
— 57 parameters in total across the system. These are reduced-form
mixtures; structural interpretation requires the Cholesky identification
of Section 7.5.*
With $n = 3$ and $p = 6$, the companion matrix is $18 \times 18$ with 18
eigenvalues to check. All are real and all fall inside the unit circle —
the VAR(6) is stable.
```{python}
#| label: tbl-eigenvalues-var6
#| include: false
roots6 = var_fit_d.roots
comp_eigs6 = 1.0 / np.array(roots6)
stable6 = all(r > 1.0 for r in roots6)
```
```{python}
#| label: tbl-roots-var6
col_w = 14
header = (f"{'Root':>6} {'|Root|':>{col_w}} "
f"{'|Comp. eig.|':>{col_w}} {'Outside circle?':>16}")
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
for i, (r, e) in enumerate(
sorted(zip(roots6, comp_eigs6), key=lambda x: -x[0]), 1):
outside = "Yes" if abs(r) > 1.0 else "NO — UNSTABLE"
print(f"{i:>6} {abs(r):>{col_w}.4f} "
f"{abs(e):>{col_w}.4f} {outside:>16}")
print(sep)
print(f"VAR(6), n=3: {len(roots6)} roots. Stable: {'Yes' if stable6 else 'No'}.")
```
*Lag polynomial roots and companion eigenvalue moduli for the VAR(6).
All 18 roots are real and all companion eigenvalues lie strictly inside
the unit circle — the VAR(6) is stable.*
```{python}
#| label: fig-unit-circle-var6
#| fig-cap: "Companion matrix eigenvalues for the VAR(6) plotted against
#| the unit circle. All 18 eigenvalues are real and lie strictly inside
#| the circle. Compare with the VAR(1) unit circle figure: the eigenvalues
#| are more numerous and spread differently, but all remain well inside
#| the stability boundary."
theta = np.linspace(0, 2 * np.pi, 300)
fig, ax = plt.subplots(figsize=(4, 4))
ax.plot(np.cos(theta), np.sin(theta),
color=EO_CHARCOAL, lw=0.8, ls="--", alpha=0.5, label="Unit circle")
ax.axhline(0, color=EO_CHARCOAL, lw=0.4, alpha=0.4)
ax.axvline(0, color=EO_CHARCOAL, lw=0.4, alpha=0.4)
n_v = var_data.shape[1]
p_v = p_final
F6 = np.zeros((n_v * p_v, n_v * p_v))
for lag in range(p_v):
F6[:n_v, lag * n_v:(lag + 1) * n_v] = var_fit_d.coefs[lag]
if p_v > 1:
F6[n_v:, :n_v * (p_v - 1)] = np.eye(n_v * (p_v - 1))
eig6 = np.linalg.eigvals(F6)
ax.scatter(eig6.real, eig6.imag, color=EO_COPPER, s=40,
zorder=5, label="Companion eigenvalues")
ax.set_xlim(-1.3, 1.3)
ax.set_ylim(-1.3, 1.3)
ax.set_aspect("equal")
ax.set_xlabel("Real part")
ax.set_ylabel("Imaginary part")
ax.legend(fontsize=6, loc="upper right")
ax.set_title("VAR(6) Stability", fontsize=8)
eo_style_ax(ax)
fig.tight_layout()
plt.show()
```
*All 18 VAR(6) companion eigenvalues are real and lie on the horizontal
axis, well inside the unit circle. The VAR(6) is stable.*
### Residual Diagnostics: VAR(6)
The VAR(6) was selected by exhausting the candidate range — no lag order up
to $p = 6$ passed the portmanteau test. The table below shows the full
search record, followed by the equation-level diagnostics at VAR(6).
```{python}
#| label: tbl-diagnostics
print("Portmanteau search record (h=12, adjusted Ljung-Box):")
ph = f"{'p':>4} {'Statistic':>12} {'df':>6} {'p-value':>10} {'Pass?':>8}"
ps = "─" * len(ph)
print(ps)
print(ph)
print(ps)
for row in port_hist:
row_passed = "Yes" if row["pval"] > 0.05 else "No"
print(f"{row['p']:>4} {row['stat']:>12.3f} {row['df']:>6} "
f"{row['pval']:>10.4f} {row_passed:>8}")
print(ps)
print(f"Working model: VAR({p_final}) — {pass_note}.")
print()
header = f"{'Equation':<20} {'DW':>8} {'JB stat':>10} {'JB p':>10} {'Normal?':>10}"
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
for _, row in diag_df.iterrows():
normal = "Yes" if row["JB p"] > 0.05 else "No"
print(f"{row['eq']:<20} {row['DW']:>8.3f} "
f"{row['JB stat']:>10.3f} {row['JB p']:>10.4f} {normal:>10}")
print(sep)
print("DW ≈ 2: no first-order autocorrelation. JB H\u2080: residuals normal.")
```
*Residual diagnostics for the VAR(6). The portmanteau statistic has fallen
from 232 to 97 but still rejects at conventional levels — likely due to
non-Gaussianity rather than genuine remaining autocorrelation, as the
Durbin-Watson statistics (all near 2) confirm. All three equations fail
Jarque-Bera, consistent with the heavy-tailed macro data in this sample.*
```{python}
#| label: fig-resid-acf
#| fig-cap: "Residual ACF — VAR(6). Bars inside the dashed 95 percent
#| confidence bands indicate no significant autocorrelation. Compare
#| with the VAR(1) ACF: the systematic clusters and spikes have been
#| largely eliminated. Isolated bars outside the bands remain but are
#| consistent with the non-Gaussian residuals rather than dynamic
#| misspecification."
n_lags_acf = 16
fig, axes = plt.subplots(1, 3, figsize=(6, 2.5), sharey=True)
colors = [EO_COPPER, EO_SKYBLUE, EO_TERRACOTTA]
conf_bound = 1.96 / np.sqrt(len(resids_d))
for ax, col, color in zip(axes, var_cols, colors):
acf_vals = sm_acf(resids_d[col].values, nlags=n_lags_acf, fft=True)[1:]
lags = np.arange(1, n_lags_acf + 1)
ax.bar(lags, acf_vals, color=color, alpha=0.7, width=0.6)
ax.axhline( conf_bound, color=EO_CHARCOAL, lw=0.8, ls="--", alpha=0.6)
ax.axhline(-conf_bound, color=EO_CHARCOAL, lw=0.8, ls="--", alpha=0.6)
ax.axhline(0, color=EO_CHARCOAL, lw=0.4)
ax.set_xlim(0.5, n_lags_acf + 0.5)
ax.set_title(col, fontsize=7)
ax.set_xlabel("Lag", fontsize=7)
eo_style_ax(ax)
axes[0].set_ylabel("ACF", fontsize=7)
eo_suptitle(fig, "Residual ACF — VAR(6) Equations")
fig.tight_layout()
plt.show()
```
*Residual ACF for the VAR(6). The systematic patterns visible in the VAR(1)
ACF — the inflation cluster at lags 3–6, the FFR spike at lag 7, the GDP
borderline bars — have been substantially reduced. Most bars now fall within
the confidence bands. The Durbin-Watson statistics close to 2 confirm that
first-order autocorrelation has been removed from all three equations. We
proceed with VAR(6) as the working model.*
## Granger Causality {#sec-granger}
### Does the Past of One Variable Help Predict Another?
Here is a deceptively simple question: does knowing the history of the federal
funds rate help us forecast GDP growth, beyond what GDP growth's own history
already tells us? If the answer is yes — if adding lagged funds rate values
to the GDP growth equation reduces forecast errors — then the funds rate
**Granger-causes** GDP growth. If the answer is no — if those lags are
collectively redundant — then the funds rate does not Granger-cause GDP
growth, in the specific sense that its past carries no marginal predictive
content for the growth equation.
[Granger causality](https://en.wikipedia.org/wiki/Granger_causality) is a statement about **predictive precedence**, not about
structural or economic causality. Two examples make this clear.
People carry umbrellas before it rains. If we ran a Granger causality test,
umbrella-carrying would predict rain: lagged umbrella counts would enter the
rain equation with significant coefficients. But umbrellas do not cause rain.
Both are driven by a common third factor — forecasted weather — and the
umbrella simply comes first in the observable sequence. The Granger test
picks up the predictive precedence; it says nothing about the mechanism.
The same logic applies to gift shopping before Christmas. Retail sales surge
in November and December, reliably preceding the 25th of December every year.
A time series test would find that gift purchases Granger-cause Christmas —
but of course the date of Christmas is fixed by the calendar, not by consumer
behaviour. The shopping is a leading indicator of a fixed event, not its
cause.
Both examples share the same structure: a third variable (weather forecast,
the calendar) drives both the leading indicator and the outcome, creating
predictive precedence without causal content. In macroeconomics the same
problem is pervasive. If financial markets anticipate a recession, asset
prices will fall before output does — asset prices will Granger-cause GDP
growth, but the true cause may be an underlying shock that markets priced
in first. Finding that the funds rate Granger-causes GDP growth does not mean
that monetary policy mechanically drives output; it means that past values of
the funds rate contain information about future GDP growth that is not already
in past values of GDP growth itself. Whether that predictive content reflects
a genuine policy transmission or merely the Fed's response to information
that markets had already priced in is a structural question. The direction
of a genuine causal mechanism may or may not be recoverable from this
reduced-form test — that question belongs to the structural identification
of Section 7.5. But as a first-pass description of the predictive
relationships in the system, Granger causality is both tractable and
informative.
::: {.callout-note}
## Definition 7.2 — Granger Causality
Variable $x$ **Granger-causes** variable $y$ if past values of $x$ contain
information that helps predict $y$, beyond the information already contained
in past values of $y$ alone (and, in a multivariate system, the past values
of all other variables in the system). Formally, in the VAR framework:
$$x \not\to_G y \iff \alpha_{ij,\ell} = 0 \quad
\text{for all lags } \ell = 1, \ldots, p$$
where $\alpha_{ij,\ell}$ are the coefficients on lags of $x_j$ in the
equation for $y_i$. Granger non-causality is the joint hypothesis that the
entire block of those coefficients is zero.
:::
### The Block Exclusion F-Test
Testing Granger causality in a VAR is an exercise in testing a set of
zero restrictions on the coefficient matrices. In the equation for variable
$i$, the hypothesis that variable $j$ does not Granger-cause variable $i$
(equation 7.4) is:
$$H_0:\; A_{ij,1} = A_{ij,2} = \cdots = A_{ij,p} = 0 \tag{7.4}$$
This is a system of $p$ linear restrictions on one equation of the VAR.
The standard test is the **block exclusion F-test**: estimate the unrestricted
equation (with all $p$ lags of variable $j$ included) and the restricted
equation (with those $p$ lags excluded), and compare their residual sums of
squares. Under the null, the test statistic follows an $F(p,\, T - np - 1)$
distribution in finite samples, or asymptotically $\chi^2(p)/p$.
In a three-variable VAR, there are $3 \times 2 = 6$ directional Granger
causality hypotheses: each variable may or may not Granger-cause each of the
other two. We test all six and report them in a single table, which makes
the full predictive structure of the system legible at a glance.
::: {.callout-warning icon=false}
## Granger Causality Is Not Structural Causality
A finding that $x$ Granger-causes $y$ means that lagged $x$ predicts $y$
in a reduced-form regression. It does not mean that $x$ structurally drives
$y$, that the relationship is stable across regimes, or that intervening on
$x$ will change $y$. In particular, Granger causality can run from effect to
cause if agents anticipate future movements in the causal variable. The
federal funds rate may Granger-cause output not because policy drives
production but because markets and firms react to anticipated policy changes
before they occur. Structural identification — the subject of Section 7.5 —
is required to move from predictive precedence to causal interpretation.
:::
### Empirical Results
```{python}
#| label: granger-tests
#| include: false
# Use diagnostics-approved lag order for Granger tests
var_fit = var_fit_d
p_sel = p_final
cols = ["GDP Growth", "CPI Inflation", "FFR"]
labels = {"GDP Growth": "GDP Growth", "CPI Inflation": "CPI Inflation", "FFR": "FFR"}
# Run all pairwise Granger tests: does col_j Granger-cause col_i?
# grangercausalitytests expects [y, x] column order
granger_pvals = {}
for caused in cols:
for cause in cols:
if cause == caused:
continue
pair = var_data[[caused, cause]]
res = grangercausalitytests(pair, maxlag=p_sel)
# F-test p-value at lag p_sel
pval = res[p_sel][0]["ssr_ftest"][1]
granger_pvals[(cause, caused)] = pval
```
```{python}
#| label: tbl-granger
var_labels = {"GDP Growth": "GDP Growth",
"CPI Inflation": "CPI Inflation",
"FFR": "FFR"}
def stars(p):
if p < 0.01: return "***"
if p < 0.05: return "** "
if p < 0.10: return "* "
return " "
# Build list of all directional pairs in readable order
pairs = [(cause, caused)
for cause in cols
for caused in cols
if cause != caused]
header = f"{'Cause':>16} {'':>4} {'Effect':>16} {'p-value':>10} {'':>4}"
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
prev_cause = None
for cause, caused in pairs:
if prev_cause is not None and cause != prev_cause:
print() # blank line between cause blocks
p = granger_pvals[(cause, caused)]
sig = stars(p)
print(f"{cause:>16} {'→':>4} {caused:>16} {p:>10.4f} {sig:>4}")
prev_cause = cause
print(sep)
print("H₀: row cause does not Granger-cause column effect.")
print("*** p<0.01 ** p<0.05 * p<0.10")
```
*Granger causality p-values for the three-variable VAR (GDP growth, CPI
inflation, FFR), 1954Q3–2019Q4. Each cell reports the p-value for the block
exclusion F-test; the null hypothesis is that the row variable does not
Granger-cause the column variable.*
The results at VAR(6) sharpen the picture relative to the VAR(1) results
and reveal a cleaner monetary transmission story. Three relationships are
significant at conventional levels.
GDP growth strongly Granger-causes the FFR (p = 0.000): past output
growth is a powerful predictor of future rate changes, consistent with the
Fed responding to real activity — stronger growth predicts tightening.
CPI inflation also Granger-causes the FFR (p = 0.049), confirming the
price-stability mandate in the data, though the relationship is weaker than
for output. Notably, at VAR(6) inflation's predictive content for the funds
rate is now statistically significant, whereas at VAR(1) it was not (p = 0.739
at the parsimonious model) — the additional lags reveal a delayed
inflation-response channel that the parsimonious model missed.
The FFR Granger-causes both GDP growth (p = 0.000) and CPI inflation
(p = 0.001). The first confirms the monetary transmission channel: past
rate changes contain information about future output that output's own
history cannot supply. The second confirms that policy rate movements
predict future inflation — consistent with monetary policy affecting prices
with a lag, though the Cholesky IRF will tell us whether this is a
disinflationary effect or a price puzzle.
Two relationships are absent. GDP growth does not Granger-cause CPI
inflation (p = 0.396), and CPI inflation does not Granger-cause GDP
growth (p = 0.459). The first result is the reduced-form Phillips curve
non-finding: past output growth has no marginal predictive content for
inflation once inflation's own lags and the other variables are controlled
for. This does not contradict the Phillips curve — the contemporaneous and
structural relationship between output and prices is better seen in the
IRFs — but it does mean the reduced-form feedback runs primarily through
the policy rate rather than directly.
What these tests cannot tell us is whether the FFR *structurally* drives
output and inflation — that is the identification question addressed in
Section 7.5, where we separate the endogenous policy response from genuine
policy shocks.
## SVAR Identification and the Cholesky Decomposition {#sec-svar}
### The Identification Problem
The reduced-form VAR we have estimated so far is useful for forecasting and
for Granger causality tests. But its residuals — the vectors $\mathbf{u}_t$
— are not structurally interpretable. Recall from Section 7.1 that these
residuals are linear combinations of the underlying structural shocks:
$\mathbf{u}_t = \mathbf{A}_0^{-1}\boldsymbol{\varepsilon}_t$, where
$\boldsymbol{\varepsilon}_t = [\varepsilon^g_t,\, \varepsilon^\pi_t,\,
\varepsilon^r_t]'$ are the genuine independent disturbances to output,
inflation, and monetary policy. The reduced-form residual $u^r_t$ in the
funds rate equation is not a pure monetary policy shock. It contains a mixture
of all three structural shocks, in proportions determined by the unknown
elements of $\mathbf{A}_0^{-1}$.
To ask how a *monetary policy shock* affects GDP growth — the central question
of applied monetary economics — we need to disentangle the three structural
shocks from the three composite residuals. This requires exactly as many
restrictions on $\mathbf{A}_0$ as there are free parameters to recover. With
$n = 3$ variables, $\mathbf{A}_0$ has $n^2 = 9$ elements. Normalising the
diagonal to ones (so each structural shock has unit contemporaneous effect on
its own variable) leaves $n^2 - n = 6$ off-diagonal elements to identify. The
covariance matrix of the reduced-form residuals $\boldsymbol{\Sigma}_u =
\mathbf{A}_0^{-1}\boldsymbol{\Omega}(\mathbf{A}_0^{-1})'$ — where
$\boldsymbol{\Omega}$ is diagonal with structural shock variances — supplies
$n(n+1)/2 = 6$ unique equations (the distinct elements of the symmetric
matrix). So we have exactly 6 equations and 6 unknowns: the system is
**just-identified**. These equations are nonlinear in the elements of
$\mathbf{A}_0$, but the Cholesky restriction — imposing a lower-triangular
structure — makes the system triangular, so it can be solved sequentially
from the top row down, one element at a time. This is what makes Cholesky
identification not just just-identified but analytically tractable.
### Cholesky Identification
The most widely used identification scheme imposes a **recursive (triangular)
structure** on $\mathbf{A}_0$. The idea is to order the variables from most
exogenous to least exogenous and assume that variable $i$ does not respond
contemporaneously to shocks in variables $j > i$ — that is, shocks propagate
down the ordering within the period, but not up.
For our system, a natural ordering is: GDP growth first, CPI inflation second,
federal funds rate third. The economic rationale is the timing of information
within a quarter. Real output and prices are determined by slow-moving
production and pricing decisions; the Fed observes these and sets the funds
rate in response. Within a single quarter, the assumption is:
- A monetary policy shock ($\varepsilon^r_t$) can affect GDP growth and
inflation only with a lag — the funds rate is the *last* to move
contemporaneously.
- An inflation shock ($\varepsilon^\pi_t$) can affect the funds rate
contemporaneously (the Fed reacts to prices within the quarter) but not
GDP growth.
- An output shock ($\varepsilon^g_t$) can affect both inflation and the
funds rate contemporaneously.
This gives $\mathbf{A}_0$ a lower-triangular structure:
$$\mathbf{A}_0 = \begin{bmatrix}
1 & 0 & 0 \\
a_{21} & 1 & 0 \\
a_{31} & a_{32} & 1
\end{bmatrix}$$
The zeros in the upper triangle are the identifying restrictions: inflation
does not respond contemporaneously to a monetary policy shock ($a_{12} = 0$),
and output responds to neither inflation nor policy within the period
($a_{13} = a_{23} = 0$, or equivalently the first row has no off-diagonal
terms in $\mathbf{A}_0^{-1}$).
::: {.callout-note}
## The Cholesky Decomposition
The Cholesky decomposition of $\boldsymbol{\Sigma}_u$ factors it as
$\boldsymbol{\Sigma}_u = \mathbf{P}\mathbf{P}'$ where $\mathbf{P}$ is lower
triangular with positive diagonal entries. This is exactly the matrix
$\mathbf{A}_0^{-1}\boldsymbol{\Omega}^{1/2}$ implied by the recursive
identification. In practice, identification is implemented by applying the
Cholesky factor $\mathbf{P}$ to the reduced-form residuals: the orthogonalised
shocks $\hat{\boldsymbol{\varepsilon}}_t = \mathbf{P}^{-1}\mathbf{u}_t$ are
uncorrelated by construction and have unit variance. These orthogonalised
shocks are then used to compute impulse responses and FEVDs.
:::
### A Numerical Example
To see where the off-diagonal elements of $\mathbf{P}$ come from, work through
the $2 \times 2$ case with variables $g_t$ (GDP growth) and $\pi_t$
(inflation). Suppose the reduced-form residual covariance matrix is:
$$\boldsymbol{\Sigma}_u = \begin{bmatrix} 4.0 & 1.2 \\ 1.2 & 9.0 \end{bmatrix}$$
The diagonal entries are the residual variances ($\sigma^2_{u^g} = 4$,
$\sigma^2_{u^\pi} = 9$); the off-diagonal entry is the covariance
($\sigma_{u^g u^\pi} = 1.2$), which is non-zero because the reduced-form
residuals mix structural shocks together.
The Cholesky factor $\mathbf{P}$ is lower triangular:
$\mathbf{P} = \begin{bmatrix} p_{11} & 0 \\ p_{21} & p_{22} \end{bmatrix}$.
The condition $\mathbf{P}\mathbf{P}' = \boldsymbol{\Sigma}_u$ expands to:
$$\begin{bmatrix} p_{11} & 0 \\ p_{21} & p_{22} \end{bmatrix}
\begin{bmatrix} p_{11} & p_{21} \\ 0 & p_{22} \end{bmatrix}
= \begin{bmatrix} p_{11}^2 & p_{11}p_{21} \\ p_{11}p_{21} & p_{21}^2 + p_{22}^2 \end{bmatrix}
= \begin{bmatrix} 4.0 & 1.2 \\ 1.2 & 9.0 \end{bmatrix}$$
The triangular structure makes this **sequentially solvable**, row by row:
**Step 1 — top-left:** $p_{11}^2 = 4.0$, so $p_{11} = \sqrt{4.0} = 2.0$.
This is the standard deviation of the first structural shock.
**Step 2 — off-diagonal:** $p_{11}\,p_{21} = 1.2$, so
$p_{21} = 1.2 / p_{11} = 1.2 / 2.0 = 0.6$.
This is the key step — the off-diagonal element of $\mathbf{P}$ is determined
entirely by the covariance between the two reduced-form residuals and the
diagonal element already computed. It captures how much of the inflation
residual is driven by the output shock (the Cholesky ordering says the output
shock can affect inflation contemporaneously, so this cross-loading is
permitted).
**Step 3 — bottom-right:** $p_{21}^2 + p_{22}^2 = 9.0$, so
$p_{22} = \sqrt{9.0 - 0.6^2} = \sqrt{8.64} \approx 2.94$.
This is the standard deviation of the inflation structural shock — the part of
the inflation residual not explained by the output shock.
The result is:
$$\mathbf{P} = \begin{bmatrix} 2.0 & 0 \\ 0.6 & 2.94 \end{bmatrix}$$
The orthogonalised shocks are $\boldsymbol{\varepsilon}_t = \mathbf{P}^{-1}\mathbf{u}_t$.
One can verify that $\mathbb{E}[\boldsymbol{\varepsilon}_t\boldsymbol{\varepsilon}_t'] =
\mathbf{P}^{-1}\boldsymbol{\Sigma}_u(\mathbf{P}^{-1})' = \mathbf{I}_2$ — the
shocks are uncorrelated with unit variance. The three-variable case follows
the same sequential logic, adding a third row: solve the diagonal, then the
two off-diagonals in order, each depending only on entries already determined
above it.
::: {.callout-warning icon=false}
## The Ordering Assumption Is Substantive
The Cholesky ordering is not a technical choice — it is an economic one. A
different ordering of the same three variables produces different structural
shocks and different impulse responses. Reversing the ordering so that the
funds rate comes first (as it would if we believed policy was set before
output and prices are determined within the quarter) gives a completely
different set of orthogonalised shocks. There is no ordering that is
obviously correct in all applications, and practitioners routinely check
robustness to alternative orderings. The ordering we adopt here — output,
inflation, policy rate — is the most common in the monetary policy VAR
literature following Christiano, Eichenbaum, and Evans (1999), and its
economic rationale is that quarterly GDP and prices are predetermined relative
to the policy instrument within the period.
:::
### Sign Restrictions as an Alternative
Cholesky identification achieves identification through zero restrictions on
the contemporaneous impact matrix — specific variables are assumed not to
respond to specific shocks within the period. An alternative approach,
increasingly used in modern empirical work, identifies shocks through
**sign restrictions** instead.
Rather than imposing exact zeros, sign restrictions specify only the
*direction* of a shock's contemporaneous impact. A contractionary monetary
policy shock, for example, is defined as one that raises the funds rate,
reduces output, and reduces inflation — or at minimum raises the rate
and reduces inflation, with output agnostic. Any draw of $\mathbf{A}_0$
consistent with those sign patterns is treated as a valid identification.
The set of admissible identifications is typically large, and impulse
responses are reported as ranges across that set rather than as point
estimates.
Sign restrictions are attractive because they require fewer strong assumptions
— exact zeros in $\mathbf{A}_0$ are strong claims — and because they can
encode economic theory more directly. A researcher who is confident that a
monetary tightening reduces prices but agnostic about timing can say exactly
that, without committing to a specific lag structure. The cost is that results
are reported as identified sets rather than point estimates, which can be wide
when the restrictions are weak. We use Cholesky identification throughout
this chapter; sign restrictions are left as a conceptual alternative for
projects and further reading.
## Impulse Response Functions {#sec-irf}
The reduced-form VAR describes how each variable evolves as a linear function
of its own past and the past of all other variables. But the object of
greatest interest in structural macroeconomics is not the past — it is the
future. Specifically: if the economy is hit by an unexpected shock today,
how does each variable respond over the next several quarters? This is the
question that **[impulse response functions (IRFs)](https://en.wikipedia.org/wiki/Impulse_response)** answer.
### A Numerical Example
Before writing down the general formula, it helps to see the recursion
produce actual numbers. Start with the simplest possible case: a univariate
AR(1), $y_t = \phi y_{t-1} + u_t$. Suppose $\phi = 0.7$ and a unit shock
$u_0 = 1$ hits at time $0$. The effect on $y_h$ is:
$$y_0 = 1, \quad y_1 = 0.7, \quad y_2 = 0.49, \quad y_3 = 0.343, \quad \ldots$$
The IRF at horizon $h$ is simply $\phi^h = 0.7^h$. The shock decays
geometrically, reaching half its initial size by around $h = 2$ and
essentially zero by $h = 10$. This is exactly what the MA($\infty$)
representation of an AR(1) says: $y_t = \sum_{h=0}^\infty \phi^h u_{t-h}$,
so the coefficient on $u_{t-h}$ is $\phi^h$.
Now extend to a bivariate VAR(1) with variables $g_t$ (GDP growth) and
$\pi_t$ (inflation). The values below are illustrative — chosen to keep the
arithmetic readable, not estimated from any data — but they are
economically plausible: moderate own-persistence, small cross-variable
effects, and a Cholesky factor that allows output shocks to have a
contemporaneous effect on inflation. Suppose:
$$\mathbf{A}_1 = \begin{bmatrix} 0.6 & 0.2 \\ 0.1 & 0.5 \end{bmatrix}, \qquad
\mathbf{P} = \begin{bmatrix} 1.0 & 0 \\ 0.3 & 0.8 \end{bmatrix}$$
where $\mathbf{A}_1$ is the lag-one coefficient matrix and $\mathbf{P}$ is
the Cholesky factor. At $h = 0$, the response to a unit output shock
($\varepsilon^g = 1$, $\varepsilon^\pi = 0$) is read directly from the first
column of $\mathbf{P}$:
$$\boldsymbol{\Theta}_0 \mathbf{e}_1 = \mathbf{P}\,\mathbf{e}_1
= \begin{bmatrix} 1.0 \\ 0.3 \end{bmatrix}$$
Why the first column? The full matrix $\boldsymbol{\Theta}_0 = \mathbf{P}$
gives the contemporaneous responses to *all* structural shocks simultaneously
— its columns are the $h = 0$ responses to each shock in turn. To isolate
the response to shock $j$ only, we apply the unit vector $\mathbf{e}_j$
(all zeros except a 1 in position $j$), giving
$\mathbf{P}\mathbf{e}_j = $ column $j$ of $\mathbf{P}$. For shock 1 (the
output shock), we take column 1. For shock 2 (the inflation shock), we
would take column 2, and so on. The diagonal of $\mathbf{P}$ gives only
the own-shock own-variable responses — the $h = 0$ values on the main
diagonal of the IRF grid — but not the cross-variable contemporaneous
impacts. The full column is needed to trace the complete contemporaneous
effect of a single shock across all variables.
GDP growth jumps by 1 on impact (the unit normalisation, from $p_{11} = 1$);
inflation jumps by 0.3 contemporaneously because $p_{21} = 0.3$ — this is
the cross-variable contemporaneous impact permitted by the Cholesky ordering,
which allows output shocks to affect inflation within the period. At $h = 1$, the system propagates the
impact through $\mathbf{A}_1$:
$$\boldsymbol{\Theta}_1 \mathbf{e}_1 = \mathbf{A}_1 \mathbf{P}\,\mathbf{e}_1
= \begin{bmatrix} 0.6 & 0.2 \\ 0.1 & 0.5 \end{bmatrix}
\begin{bmatrix} 1.0 \\ 0.3 \end{bmatrix}
= \begin{bmatrix} 0.66 \\ 0.25 \end{bmatrix}$$
At $h = 2$, the response propagates through $\mathbf{A}_1$ again:
$$\boldsymbol{\Theta}_2 \mathbf{e}_1 = \mathbf{A}_1^2 \mathbf{P}\,\mathbf{e}_1
= \begin{bmatrix} 0.6 & 0.2 \\ 0.1 & 0.5 \end{bmatrix}
\begin{bmatrix} 0.66 \\ 0.25 \end{bmatrix}
= \begin{bmatrix} 0.446 \\ 0.191 \end{bmatrix}$$
The pattern is clear: at each horizon, multiply by $\mathbf{A}_1$ again.
In the VAR(1) case, $\boldsymbol{\Theta}_h = \mathbf{A}_1^h \mathbf{P}$, so
the IRF is computed by repeatedly powering the coefficient matrix — exactly
the multivariate analogue of $\phi^h$ in the scalar case. With $p > 1$ lags,
the companion matrix $\mathbf{F}$ plays the role of $\mathbf{A}_1$, as the
callout below shows.
### From VAR to VMA: The General Formula
::: {.callout-note}
## The VMA Representation and IRF Formula
For a VAR($p$) written in companion form (equation 7.2)
$\boldsymbol{\xi}_t = \mathbf{F}\boldsymbol{\xi}_{t-1} + \tilde{\mathbf{u}}_t$,
recursive backward substitution gives the VMA($\infty$) representation:
$$\mathbf{y}_t = \boldsymbol{\mu} + \sum_{h=0}^{\infty}
\boldsymbol{\Theta}_h\,\boldsymbol{\varepsilon}_{t-h}$$
where $\boldsymbol{\varepsilon}_t = \mathbf{P}^{-1}\mathbf{u}_t$ are the
Cholesky-orthogonalised structural shocks ($\mathbb{E}[\boldsymbol{\varepsilon}_t
\boldsymbol{\varepsilon}_t'] = \mathbf{I}_n$) and the **orthogonalised IRF
matrix** at horizon $h$ is:
$$\boldsymbol{\Theta}_h = \mathbf{J}\,\mathbf{F}^h\,\mathbf{J}'\,\mathbf{P}$$
with $\mathbf{J} = [\mathbf{I}_n\;\mathbf{0}\;\cdots\;\mathbf{0}]$ the
selection matrix extracting the first $n$ rows of the $np$-dimensional
companion system. The $(i,j)$ element $\theta_{ij,h}$ is the **impulse
response function**: the response of variable $i$ to a unit structural shock
$j$, $h$ periods after the shock, all other shocks held at zero.
In a stable VAR all eigenvalues of $\mathbf{F}$ lie inside the unit circle,
so $\mathbf{F}^h \to \mathbf{0}$ and all IRFs decay to zero — shocks are
transitory. The diagonal panels of the IRF grid start at 1.0 by the
unit-variance normalisation of $\boldsymbol{\varepsilon}_t$.
:::
### Orthogonalisation
The reduced-form shocks $\mathbf{u}_t$ are correlated —
$\boldsymbol{\Sigma}_u$ is not diagonal — so a "unit shock to $u^r_t$" is
not a well-defined experiment: because $u^g_t$, $u^\pi_t$, and $u^r_t$ move
together, hitting one in isolation is not possible. The Cholesky factor
$\mathbf{P}$ (Section 7.5) converts them to orthogonal structural shocks
$\boldsymbol{\varepsilon}_t = \mathbf{P}^{-1}\mathbf{u}_t$ with
$\mathbb{E}[\boldsymbol{\varepsilon}_t\boldsymbol{\varepsilon}_t'] = \mathbf{I}_n$.
Substituting into the VMA gives the orthogonalised representation with
$\boldsymbol{\Theta}_h = \boldsymbol{\Phi}_h\mathbf{P}$ replacing
$\boldsymbol{\Phi}_h$, and it is these orthogonalised matrices that produce
structurally interpretable IRFs.
Confidence bands are computed by bootstrapping the VAR residuals: repeatedly
redrawing residuals with replacement, re-estimating the VAR and Cholesky
factor, recomputing $\boldsymbol{\Theta}_h$ at each draw, and taking the
16th and 84th percentiles of the resulting distribution.
### Empirical IRFs
The 3×3 grid of panels is the standard presentation format for a
Cholesky-identified VAR with $n$ variables: $n$ columns for the $n$ shocks,
$n$ rows for the $n$ response variables, giving $n^2$ panels in total. The
layout has a useful built-in diagnostic: the diagonal panels show the
response of each variable to its *own* shock. Because the structural shocks
are normalised to unit variance, the contemporaneous own response — the value
at horizon $h = 0$ on the diagonal — is always exactly 1 by construction.
If you ever see a different value on the diagonal at $h = 0$, the shocks have
been scaled differently (for instance, to one standard deviation of the
reduced-form residual) or the axes have been re-ordered. The unit diagonal
at $h = 0$ is the quickest way to verify that the plot is reading as intended.
```{python}
#| label: irf-estimation
#| include: false
# ── Use the diagnostics-approved lag order throughout ─────────────────────────
# p_final and var_fit_d are set in the var-diagnostics cell above.
# All IRF and FEVD calculations use the final lag order, not the BIC-selected one.
var_fit = var_fit_d
p_sel = p_final
n_vars = var_data.shape[1]
periods = 20
nrep = 500
rng = np.random.default_rng(42)
# ── Point-estimate IRFs ────────────────────────────────────────────────────────
irf_obj = var_fit.irf(periods=periods)
irf_raw = irf_obj.orth_irfs # shape (periods+1, n, n)
# statsmodels orth_irfs scales column j by the std dev of structural shock j,
# so irf_raw[0, j, j] = Cholesky diagonal element j (not 1.0 in general).
# Divide each column j by its own h=0 diagonal to get unit-normalised IRFs.
scale = irf_raw[0, np.arange(n_vars), np.arange(n_vars)] # shape (n,)
irf_point = irf_raw / scale[np.newaxis, np.newaxis, :] # broadcast over h and i
# ── Manual residual bootstrap for confidence bands ────────────────────────────
T = len(var_data) - p_sel
resids = var_fit.resid.values # convert to numpy array (T, n)
coefs = var_fit.coefs # shape (p, n, n)
intercept = var_fit.intercept # shape (n,)
boot_irfs = np.zeros((nrep, periods + 1, n_vars, n_vars))
for b in range(nrep):
# Resample residuals with replacement
idx = rng.integers(0, T, size=T)
resid_b = resids[idx] # integer indexing on numpy array
# Rebuild series from bootstrapped residuals
y_b = var_data.values.copy()
for t in range(p_sel, len(var_data)):
fitted = intercept.copy()
for lag in range(p_sel):
fitted += coefs[lag] @ y_b[t - lag - 1]
y_b[t] = fitted + resid_b[t - p_sel]
# Re-estimate VAR, compute and unit-normalise orthogonalised IRFs
try:
df_b = pd.DataFrame(y_b, index=var_data.index,
columns=var_data.columns)
fit_b = VAR(df_b).fit(p_sel)
irf_b = fit_b.irf(periods=periods)
raw_b = irf_b.orth_irfs
scale_b = raw_b[0, np.arange(n_vars), np.arange(n_vars)]
boot_irfs[b] = raw_b / scale_b[np.newaxis, np.newaxis, :]
except Exception:
boot_irfs[b] = irf_point # fallback to point estimate on rare failures
# 16th / 84th percentile bands (one-standard-deviation equivalent)
irf_lower = np.percentile(boot_irfs, 16, axis=0)
irf_upper = np.percentile(boot_irfs, 84, axis=0)
```
```{python}
#| label: fig-irfs
#| fig-cap: "Orthogonalised impulse responses for the three-variable VAR,
#| 1954Q3–2019Q4. Each column corresponds to one structural shock
#| (Cholesky-identified, ordering: GDP growth → CPI inflation → FFR);
#| each row shows the response of one variable. Shaded bands are
#| 16th–84th percentile bootstrap confidence intervals (500 draws).
#| By construction, diagonal panels equal 1 at horizon 0 — each variable's
#| response to its own unit structural shock. Horizons are in quarters."
labels = ["GDP Growth", "CPI Inflation", "FFR"]
colors = [EO_COPPER, EO_SKYBLUE, EO_TERRACOTTA]
shock_labels = ["Output shock", "Inflation shock", "Policy shock"]
horizons = np.arange(periods + 1)
fig, axes = plt.subplots(n_vars, n_vars, figsize=(6, 6), sharex=True)
for j in range(n_vars): # shock (column)
for i in range(n_vars): # response (row)
ax = axes[i, j]
color = colors[i]
point = irf_point[:, i, j]
lo = irf_lower[:, i, j]
hi = irf_upper[:, i, j]
ax.fill_between(horizons, lo, hi, color=color, alpha=0.18, lw=0)
ax.plot(horizons, point, color=color, lw=1.2)
ax.axhline(0, color=EO_CHARCOAL, lw=0.5, ls="--", alpha=0.5)
ax.set_xlim(0, periods)
eo_style_ax(ax)
if i == 0:
ax.set_title(shock_labels[j], fontsize=7)
if j == 0:
ax.set_ylabel(labels[i], fontsize=7)
if i == n_vars - 1:
ax.set_xlabel("Quarters", fontsize=7)
eo_suptitle(fig, "Impulse Response Functions — Three-Variable VAR")
fig.tight_layout()
plt.show()
```
*Orthogonalised impulse response functions for the three-variable VAR (GDP
growth, CPI inflation, federal funds rate), 1954Q3–2019Q4. Cholesky ordering:
output → inflation → policy rate. Shaded bands are 16th–84th percentile
bootstrap confidence intervals from 500 residual resamples. Diagonal panels
start at 1.0 by construction — the unit own-shock normalisation.*
The 3×3 layout gives a complete picture of the joint dynamics of the system.
Reading across any row traces how a given variable responds to each of the
three shocks; reading down any column shows how a given shock propagates
across the entire system. The diagonal — own-shock responses — starts at 1.0
in every panel by construction of the unit-variance normalisation, and decays
toward zero as the shock dissipates in a stable VAR.
The response of GDP growth to a contractionary policy shock (top-right panel)
is the central test of the monetary transmission mechanism. GDP growth falls
sharply on impact, reaching a trough of around −1.5 percentage points within
the first two quarters before recovering gradually toward zero. The response
is clearly negative and precisely estimated — the confidence band stays well
below zero for approximately four to five quarters. This is the textbook
monetary transmission channel: a surprise tightening depresses real activity
with a short but discernible lag, consistent with policy operating through
credit conditions, investment, and aggregate demand.
The response of CPI inflation to a policy shock (middle-right panel) shows
the **price puzzle**: inflation rises on impact, peaking around 0.3 percentage
points at $h = 1$ before declining slowly toward zero. This is the opposite
of the theoretical prediction — a contractionary shock should reduce prices —
and is a well-known finding in monetary VARs estimated without commodity
prices or other forward-looking inflation indicators. The puzzle arises because
our three-variable system cannot fully distinguish a genuine policy shock from
an endogenous rate increase in response to anticipated inflation. The VARX
result showing that oil prices improve fit (Section 7.8) is directly relevant:
adding oil price growth as an exogenous control would likely reduce or
eliminate the price puzzle by absorbing the inflationary information the Fed
is responding to.
The response of the FFR to an output shock (bottom-left panel) is hump-shaped
— the funds rate rises gradually, peaks around $h = 5$ at approximately 0.25
percentage points, then decays back toward zero over the subsequent fifteen
quarters. This is the systematic component of monetary policy: the Fed raises
rates in response to stronger growth, consistent with the Taylor rule intuition
from Section 7.1. The precise timing and magnitude of this response is
informative: the peak at $h = 5$ means the Fed tightens roughly five quarters
after an output expansion, suggesting a delayed systematic reaction rather than
immediate within-quarter adjustment.
## Forecast Error Variance Decomposition {#sec-fevd}
### How Much of the Variance Comes From Each Shock?
The impulse response function answers a *path* question: given a shock today,
how does each variable move over the next $h$ periods? The **[forecast error
variance decomposition (FEVD)](https://en.wikipedia.org/wiki/Variance_decomposition_of_forecast_errors)** answers an *attribution* question: of all the
uncertainty in our $h$-step-ahead forecast of variable $i$, what fraction is
due to each structural shock?
To see why this question is non-trivial, consider what happens after a single
policy shock — a surprise increase in the federal funds rate. At $h = 0$ the
shock hits the funds rate equation directly. But through the VAR's coefficient
matrices it then propagates forward: higher rates dampen GDP growth over the
next several quarters; weaker growth in turn reduces inflationary pressure;
lower inflation feeds back into the funds rate as the Fed responds. By $h = 8$,
a shock that originated entirely in the policy equation has generated forecast
errors in *all three* variables — not because multiple shocks occurred, but
because a single shock rippled through the system.
If we imagine hitting the economy with all three structural shocks
simultaneously — output, inflation, and policy shocks all firing at once —
the total forecast uncertainty in GDP growth at horizon $h$ is the sum of
their individual contributions. The FEVD asks: what share of that total comes
from each source? A policy shock that ripples strongly into GDP growth will
claim a large share of GDP growth's forecast variance at horizons where its
IRF is large. An inflation shock whose effect on GDP growth is small and
short-lived will claim a small share. The FEVD translates the IRF paths into
a single percentage at each horizon: the fraction of forecast uncertainty
attributable to each structural disturbance.
### Deriving the Decomposition
The $h$-step-ahead forecast error for variable $i$ is the cumulative effect
of all structural shocks that occur between the forecast origin and the target
period. From the VMA representation derived in Section 7.6:
$$y_{i,t+h} - \hat{y}_{i,t+h|t} = \sum_{s=0}^{h-1} \sum_{j=1}^{n}
\theta_{ij,s}\,\varepsilon_{j,t+h-s} \tag{7.5}$$
The forecast error variance of variable $i$ at horizon $h$ is therefore:
$$\text{MSE}_{i}(h) = \sum_{s=0}^{h-1} \sum_{j=1}^{n} \theta_{ij,s}^2
= \sum_{j=1}^{n} \underbrace{\sum_{s=0}^{h-1} \theta_{ij,s}^2}_{\text{contribution of shock } j}
\tag{7.6}$$
where the second equality uses the fact that structural shocks are orthogonal
($\mathbb{E}[\varepsilon_{j,t}\varepsilon_{k,t}] = 0$ for $j \neq k$) so
cross-terms vanish. The **FEVD share** of shock $j$ in the variance of
variable $i$ at horizon $h$ is:
$$\omega_{ij}(h) = \frac{\displaystyle\sum_{s=0}^{h-1} \theta_{ij,s}^2}
{\displaystyle\sum_{k=1}^{n}\sum_{s=0}^{h-1} \theta_{ik,s}^2} \tag{7.7}$$
By construction $\sum_{j=1}^{n} \omega_{ij}(h) = 1$ at every horizon: the
shares sum to 100 percent (equations 7.5–7.7). At $h = 1$, the FEVD equals
the contemporaneous impact of each shock — only the $h=0$ IRF coefficient
matters, so the decomposition reflects the Cholesky ordering directly. At longer horizons,
the cumulative squared IRF paths determine the shares, and the ordering
assumption becomes less influential as later-horizon dynamics dominate.
::: {.callout-note}
## Definition 7.3 — Forecast Error Variance Decomposition
The **FEVD share** $\omega_{ij}(h)$ is the fraction of the $h$-step forecast
error variance of variable $i$ that is attributable to structural shock $j$:
$$\omega_{ij}(h) = \frac{\sum_{s=0}^{h-1} \theta_{ij,s}^2}
{\sum_{k=1}^{n}\sum_{s=0}^{h-1} \theta_{ik,s}^2}, \qquad
\sum_{j=1}^{n}\omega_{ij}(h) = 1$$
The shares are horizon-dependent: a shock that dominates at short horizons
may be less important at long horizons if other shocks have slower-building
but more persistent effects.
:::
### Empirical FEVD
```{python}
#| label: fevd-compute
#| include: false
fevd_obj = var_fit.fevd(periods=periods)
# fevd_obj.decomp has shape (n, periods, n):
# decomp[i, h, j] = share of shock j in variance of variable i at horizon h+1
fevd_decomp = fevd_obj.decomp # (n, periods, n)
```
```{python}
#| label: tbl-fevd
labels = ["GDP Growth", "CPI Inflation", "FFR"]
shock_names = ["Output", "Inflation", "Policy"]
horizons_tbl = [1, 4, 8, 12, 20]
col_w = 12
h_w = 6
for i, var_label in enumerate(labels):
header = (f"\n{'':>{h_w}}" +
"".join(f"{s:>{col_w}}" for s in shock_names))
sep = "─" * (h_w + col_w * len(shock_names))
print(f"Dependent variable: {var_label}")
print(sep)
print(header)
print(sep)
for h in horizons_tbl:
row = f"{'h='+str(h):>{h_w}}"
for j in range(len(shock_names)):
share = fevd_decomp[i, h - 1, j] * 100
row += f"{share:>{col_w}.1f}"
print(row)
print(sep)
print("Shares in percent; rows sum to 100.")
```
*Forecast error variance decompositions for the three-variable VAR (GDP
growth, CPI inflation, FFR), 1954Q3–2019Q4. Shares in percent; rows sum
to 100. Cholesky ordering: output → inflation → policy rate.*
```{python}
#| label: fig-fevd
#| fig-cap: "Forecast error variance decompositions for the three-variable VAR,
#| 1954Q3–2019Q4. Each panel shows the fraction of forecast error variance in
#| one variable attributable to each structural shock (Cholesky ordering:
#| output → inflation → policy rate) as the horizon grows from 1 to 20
#| quarters. Stacked areas sum to 100 percent at every horizon."
var_labels = ["GDP Growth", "CPI Inflation", "FFR"]
shock_names = ["Output shock", "Inflation shock", "Policy shock"]
colors = [EO_COPPER, EO_SKYBLUE, EO_TERRACOTTA]
horizons_p = np.arange(1, periods + 1)
fig, axes = plt.subplots(1, 3, figsize=(6, 3), sharey=True)
for i, (ax, var_label) in enumerate(zip(axes, var_labels)):
# shares: shape (periods, n_shocks) — fevd_decomp[i, h, j]
shares = np.array([fevd_decomp[i, h, :] * 100
for h in range(periods)]) # (periods, n)
bottom = np.zeros(periods)
for j, (shock_name, color) in enumerate(zip(shock_names, colors)):
ax.fill_between(horizons_p, bottom, bottom + shares[:, j],
color=color, alpha=0.75, lw=0,
label=shock_name if i == 0 else None)
bottom += shares[:, j]
ax.set_xlim(1, periods)
ax.set_ylim(0, 100)
ax.set_xlabel("Quarters", fontsize=7)
ax.set_title(var_label, fontsize=8)
eo_style_ax(ax)
axes[0].set_ylabel("Share of forecast variance (%)", fontsize=7)
fig.legend(loc="lower center", ncol=3, fontsize=6,
bbox_to_anchor=(0.5, -0.12), frameon=True)
eo_suptitle(fig, "Forecast Error Variance Decomposition")
fig.tight_layout()
plt.show()
```
*Stacked area FEVD for the three-variable VAR(6). GDP growth variance (left)
is dominated by the output shock throughout, but the policy shock (terracotta)
claims a visible 7–9 percent share from $h = 4$ onward — larger than at
VAR(1). CPI inflation variance (centre) is dominated by the own-shock (sky
blue) at all horizons, with output and policy shocks making only modest
inroads by $h = 20$. FFR variance (right) shows the most dramatic
redistribution: the policy own-shock (terracotta) falls from 91 percent at
$h = 1$ to 24 percent at $h = 20$ as the output shock (copper) rises to
51 percent — quantifying the Fed's systematic response to real conditions.*
The results tell a consistent story across all three variables, and differ
meaningfully from the VAR(1) estimates — illustrating why model selection
and diagnostic checking matter before structural analysis.
**GDP growth** remains predominantly self-driven but the policy shock now
claims a meaningful share at medium horizons. The output shock accounts for
100 percent at $h = 1$ by Cholesky construction, falls to 92 percent by
$h = 4$, and stabilises around 87 percent at $h = 20$. Policy shocks account
for 7–9 percent of GDP growth variance from $h = 4$ onward — small in absolute
terms but larger than the 1–2 percent seen in the VAR(1) estimates. The
message is nuanced: autonomous output shocks dominate business cycle
fluctuations, but the unexpected component of monetary policy accounts for
a non-trivial share of medium-run output variance. This is consistent with
the IRF finding that policy shocks produce a sharp GDP growth contraction
of around 1.5 percentage points.
**CPI inflation** is dominated by its own shocks throughout, but the pattern
differs strikingly from GDP growth. At $h = 1$ inflation is entirely
self-driven by Cholesky construction (100 percent own-shock). By $h = 4$
the own-shock share is still 92 percent, but by $h = 20$ it has declined to
84 percent, with output shocks accounting for 9 percent and policy shocks
for 7 percent. The relatively modest policy share — despite the Granger
causality finding that the FFR predicts inflation — is consistent with the
price puzzle in the IRF: what the Granger test picks up as "FFR predicts
inflation" is partly the Fed raising rates in anticipation of inflation that
then materialises, not a structural disinflationary effect. The persistent
dominance of own-shock reflects genuine autonomous inflation dynamics —
supply shocks, expectation shifts — that are not driven by either output
or monetary policy within this sample.
**FFR variance** shows the most dramatic horizon redistribution in the
system. At $h = 1$, 91 percent of policy rate variance comes from policy
shocks — the Fed is making largely discretionary decisions. But by $h = 4$
the output shock share has surged to 36 percent, and by $h = 12$ it reaches
52 percent, where it stabilises. The inflation shock share grows more slowly,
reaching 25 percent by $h = 20$. By the twenty-quarter horizon, only 24
percent of FFR variance is attributable to autonomous policy innovations —
the majority reflects the endogenous response to real and nominal conditions.
This is a strong quantitative statement of the Fed's systematic behaviour:
at business cycle frequencies, most interest rate variation is predictable
from output and inflation dynamics, not from discretionary surprises.
## Adding Exogenous Variables: The VARX {#sec-varx}
The VAR treats every variable in the system symmetrically — each one has its
own equation, its own structural shock, and contributes to the FEVD of every
other variable. This is the right framework when we believe all variables are
jointly determined and we want to understand how shocks propagate among them.
But sometimes we want to condition on a variable without modelling it. There
are at least three situations where this arises.
First, the variable may be **genuinely exogenous** to the system — determined
outside the domestic economy and therefore unaffected by shocks to any of the
endogenous variables. Oil prices are the canonical example in macro VARs: US
output and inflation cannot plausibly move world crude prices, so treating oil
as endogenous would model a feedback channel that does not exist, waste degrees
of freedom, and potentially distort the identified shocks.
Second, we may simply **not care about the variable's dynamics**. A researcher
studying monetary transmission wants to know how a policy shock affects output
and inflation — not what drives oil prices. Adding oil as an exogenous control
absorbs its influence on the endogenous variables without requiring a model
for the oil market itself.
Third, the variable may be a **policy instrument or external shock** that is
best treated as a conditioning variable for identification purposes — commodity
prices as an inflation predictor, foreign interest rates as a transmission
channel, or a fiscal policy index as a control for budget conditions.
The ARMAX model in Chapter 3 handled this case for a single equation. The
multivariate extension is the **VARX**, which appends a matrix of exogenous
regressors to the reduced-form VAR.
### The VARX($p$, $s$) Model
Let $\mathbf{x}_t$ be a $k \times 1$ vector of exogenous variables. The
VARX($p$, $s$) model is:
$$\mathbf{y}_t = \mathbf{c} + \sum_{\ell=1}^{p}\mathbf{A}_\ell\,\mathbf{y}_{t-\ell}
+ \sum_{j=0}^{s}\mathbf{B}_j\,\mathbf{x}_{t-j} + \mathbf{u}_t \tag{7.8}$$
where $\mathbf{B}_j$ is an $n \times k$ matrix of coefficients on the $j$-th
lag of the exogenous block. The lag order $s$ governs how many lags of
$\mathbf{x}_t$ enter each equation; $s = 0$ means only contemporaneous values,
$s = 1$ adds one lag, and so on. Each equation receives the same exogenous
regressors — this is what distinguishes the VARX from a set of unrelated
ARMAX equations. The endogenous block $\mathbf{u}_t$ retains its
interpretation as before: correlated reduced-form innovations that mix
the structural shocks together.
Estimation is still OLS equation by equation, with the exogenous regressors
treated as fixed regressors in each equation. Lag selection for $p$ and $s$
proceeds jointly using AIC or BIC, with the penalty now covering both
the $n^2 p$ endogenous slope coefficients and the $nk(s+1)$ exogenous
coefficients (equation 7.8).
::: {.callout-note}
## What Changes — and What Does Not
**Changes:** The coefficient matrices $\mathbf{B}_j$ absorb the linear effect
of $\mathbf{x}_t$ on each endogenous variable. IRFs and FEVDs for the
endogenous block are computed exactly as in the pure VAR, but the baseline
forecasts are shifted by the exogenous variables' paths. Scenario analysis —
asking how the endogenous variables would respond under a specific path for
$\mathbf{x}_t$ — is straightforward: feed the assumed $\mathbf{x}_t$ path
into the forecast equations.
**Does not change:** The exogenous variables do not receive structural shocks
in the SVAR sense. They do not appear in the FEVD of the endogenous variables
(unless the FEVD is explicitly extended to include exogenous contributions,
which requires additional modelling assumptions). The identification problem
for the endogenous block is unchanged: the Cholesky ordering applies only
to $\boldsymbol{\Sigma}_u$, not to $\mathbf{x}_t$.
:::
### Why Lagged Exogenous Variables Matter in a VARX but Not in an ARMAX
It is worth asking why the
VARX includes $s$ lags of $\mathbf{x}_t$ rather than just the contemporaneous
value. In a single-equation ARMAX, the argument for omitting lagged $x_t$ is
compelling: if $x_t$ is serially correlated, its past values are correlated
with past values of $y_t$, which are already captured by the AR lags. Adding
$x_{t-1}$ explicitly provides little marginal information beyond what the AR
component already absorbs.
The VARX case is different for two reasons.
First, $\mathbf{x}_t$ is by assumption **outside the endogenous system** —
that is the entire motivation for treating it as exogenous. If oil prices have
dynamics that are partly orthogonal to domestic output, inflation, and the
funds rate, then past values of $\mathbf{y}_t$ do not fully reconstruct past
values of $\mathbf{x}_t$. The information in $\mathbf{x}_{t-1}$ is genuinely
not redundant given $\mathbf{y}_{t-1}, \mathbf{y}_{t-2}, \ldots$
Second, the transmission from $\mathbf{x}_t$ to $\mathbf{y}_t$ may be
**delayed**. An oil price shock in quarter $t$ may affect GDP growth in
quarter $t+1$ through investment and spending decisions that take time to
adjust. In the ARMAX, the AR lags pick up this delayed effect indirectly —
the contemporaneous impact of $x_t$ on $y_t$ creates a correlation between
$y_{t+1}$ and $x_t$ that the AR component absorbs. In the VARX, where
$\mathbf{x}_t$ is orthogonal to $\mathbf{u}_t$ by assumption, the only way
to capture a delayed effect of $\mathbf{x}_{t-1}$ on $\mathbf{y}_t$ is to
include $\mathbf{x}_{t-1}$ explicitly.
The clean summary: in an ARMAX the AR component and $x_t$ share the same
dependent variable, so lagged $x_t$ is indeed largely redundant given enough
AR lags. In a VARX, $\mathbf{x}_t$ lives outside the endogenous system and
its lags carry information that the endogenous lags cannot reconstruct. The
practical implication is that $s$ should be chosen by information criteria
alongside $p$, rather than set to zero by default.
### Stationarity of Exogenous Variables
The stationarity requirement for endogenous variables is clear: all
$\mathbf{y}_t$ must be $I(0)$. For exogenous variables the answer is more
nuanced, and getting it wrong has the same consequences as including $I(1)$
variables in levels in a pure VAR.
::: {.callout-warning icon=false}
## Stationarity Rules for Exogenous Variables
- **$\mathbf{x}_t$ is $I(0)$:** include in levels. No adjustment needed.
- **$\mathbf{x}_t$ is $I(1)$, not cointegrated with $\mathbf{y}_t$:** include
in first differences. Including the level would introduce a stochastic trend
into each equation's error term, producing spurious coefficient estimates
for the same reason as a static regression of $I(1)$ on $I(1)$ variables.
- **$\mathbf{x}_t$ is $I(1)$, cointegrated with $\mathbf{y}_t$:** the correct
framework is a VECMX — a vector error correction model with exogenous
variables — which includes the cointegrating residual as an error correction
term. This is Chapter 8 territory. The practical rule: if
you suspect cointegration between an exogenous and an endogenous variable,
treat the exogenous variable as endogenous and use the VECM framework.
When in doubt, first-difference the $I(1)$ exogenous variable. The cost of
over-differencing (losing level information) is smaller than the cost of
spurious regression.
:::
### Empirical Example: Oil Prices as an Exogenous Variable
Oil price shocks are one of the most studied external drivers of US
macroeconomic fluctuations. The 1973 OPEC embargo, the 1979 Iranian
Revolution, and the 2007–08 price surge all coincided with US recessions,
suggesting that oil prices carry information about future output and inflation
beyond what the domestic VAR already captures. We add the annualised
quarterly growth rate of WTI crude oil prices as an exogenous variable —
$\Delta \ln(\text{oil}_t) \times 400$ — to our three-variable VAR.
Oil price growth is $I(0)$ (oil prices in levels are $I(1)$, but their log
growth rate is stationary), so no transformation beyond the log-differencing
is required. We include contemporaneous oil price growth only ($s = 0$) as
a first pass, consistent with the interpretation that oil prices affect the
domestic economy within the quarter.
```{python}
#| label: varx-data
#| include: false
# ── Download WTI crude oil price (quarterly average) ─────────────────────────
from pathlib import Path
DATA_PATH = Path("../../data/raw")
oil_raw = pd.read_csv(DATA_PATH / "WTISPLC.csv", index_col="date", parse_dates=True).loc["1954-01-01":"2019-12-31"]
oil_raw.columns = ["Oil"]
oil_q = oil_raw.resample("QS").mean()
oil_q["Oil Growth"] = np.log(oil_q["Oil"]).diff() * 400 # ann. log growth
# ── ADF test on oil price growth ──────────────────────────────────────────────
oil_adf = adfuller(oil_q["Oil Growth"].dropna(), autolag="AIC", regression="c")
# ── Align with var_data sample ────────────────────────────────────────────────
exog = (oil_q[["Oil Growth"]]
.reindex(var_data.index)
.dropna())
varx_data = var_data.loc[exog.index]
```
```{python}
#| label: tbl-varx-adf
print(f"ADF test — WTI oil price growth (annualised %, quarterly)")
print(f" Statistic : {oil_adf[0]:.3f}")
print(f" p-value : {oil_adf[1]:.4f}")
print(f" Crit (5%) : {oil_adf[4]['5%']:.3f}")
print(f" Decision : {'Stationary — include in levels' if oil_adf[1] < 0.05 else 'Unit root?'}")
```
*ADF pre-check for WTI oil price growth, the exogenous variable added to the
VARX. The test statistic of −11.567 (p = 0.000) rejects the unit root null
decisively — oil price growth is stationary and suitable for direct inclusion
as a contemporaneous exogenous regressor without further transformation.*
```{python}
#| label: varx-estimation
#| include: false
# ── Estimate VARX at the diagnostics-approved lag order ──────────────────────
# In statsmodels, exog is passed to VAR() at construction, not to .fit()
varx_fit = VAR(varx_data, exog=exog.values).fit(p_final, trend="c")
# ── Compare information criteria: pure VAR vs VARX ───────────────────────────
var_bic = var_fit_d.bic
varx_bic = varx_fit.bic
```
```{python}
#| label: tbl-varx-compare
print("Model comparison: VAR vs VARX (oil price growth as exogenous)")
print()
header = f"{'Model':<20} {'BIC':>12} {'ΔBIC':>10}"
sep = "─" * len(header)
print(sep)
print(header)
print(sep)
print(f"{'VAR(' + str(p_final) + ')':<20} {var_bic:>12.3f} {'—':>10}")
delta = varx_bic - var_bic
better = "VARX preferred" if delta < 0 else "VAR preferred"
print(f"{'VARX(' + str(p_final) + ',0)':<20} {varx_bic:>12.3f} {delta:>10.3f}")
print(sep)
print(f"Δ BIC = VARX − VAR. Negative favours VARX. {better}.")
print("Note: BIC reported per observation by statsmodels.")
```
*BIC comparison between the pure VAR(6) and the VARX(6,0) with contemporaneous
oil price growth, 1954Q3–2019Q4. The VARX has a lower BIC (2.226 vs 2.406,
$\Delta = -0.180$), indicating that adding contemporaneous oil price growth
improves fit enough to more than compensate for the additional three parameters
(one per equation). The VARX is preferred.*
The BIC result is economically interpretable. The VARX's lower BIC confirms
that domestic output, inflation, and the funds rate have a systematic
contemporaneous relationship with oil price movements that six lags of the
endogenous variables alone cannot capture — consistent with the supply-shock
narrative. Oil price surprises shift the inflation equation directly within
the quarter, compress output, and trigger a policy response, all in ways that
are orthogonal to the VAR's own dynamics. Conditioning on oil price growth
therefore tightens the VAR residuals and improves the identification of the
domestic monetary policy shock.
For applications involving series plausibly affected by external
drivers — commodity prices, trade volumes, foreign interest rates — the VARX
offers a principled way to condition on those drivers without modelling them.
The workflow is: test the exogenous variable for stationarity, transform if
necessary, pass it as the `exog` argument to `statsmodels` `VAR`, and compare
information criteria against the pure VAR to assess whether inclusion is
warranted. When the VARX is preferred, re-running the Granger causality
tests and IRF analysis on the VARX is recommended: the structural shocks will
be better identified once the exogenous information is absorbed into the
residuals, and results that appeared significant — or insignificant — in the
pure VAR may change once the external driver is properly controlled for.
## When Variables Are I(1) {#sec-integrated}
Everything in this chapter has assumed that all three variables in the VAR
are stationary. The ADF tests in Section 7.2 confirmed this cleanly for
GDP growth and CPI inflation; for the FFR the ADF did not reject, but the
lag polynomial roots in Table 7.2 confirmed that the estimated VAR is stable
— the definitive check. Many applications are less fortunate: price levels,
nominal GDP, exchange rates, and interest rates over very long samples can
exhibit behaviour consistent with genuine unit roots that survive both tests.
If we attempt to estimate a VAR in levels with $I(1)$ variables, we face the
same problem identified in Chapter 4 for univariate regressions. OLS
coefficient estimates are still consistent — the VAR is at least not
spurious in the same way as a static regression — but the standard
distributions for $t$-statistics, $F$-statistics, and information criteria
no longer apply. Lag selection by AIC or BIC is unreliable. Granger causality
F-tests have non-standard distributions under the null. Impulse responses
computed from a VAR in $I(1)$ levels do not converge to zero as $h \to
\infty$ — they approach a non-zero constant, reflecting the permanent effects
of shocks in an integrated system. The unit-eigenvalue structure of the
companion matrix that stationarity rules out is present by construction.
The naive fix is to difference every $I(1)$ variable before including it in
the VAR. A VAR in first differences — $\Delta \mathbf{y}_t$ — restores
stationarity and makes all the tools of this chapter valid. But differencing
has a cost: it removes all information about long-run levels. If GDP and
consumption share a common stochastic trend — if they are cointegrated — a
VAR in differences cannot capture that relationship. The error correction
mechanism, which describes how deviations from the long-run relationship are
corrected over time, is invisible to a differenced VAR.
### Cointegration and the Path to Chapter 8
Two $I(1)$ series are **cointegrated** if there exists a linear combination
of them that is $I(0)$ — stationary. The classic example is consumption and
income: neither is stationary in levels, but the ratio of consumption to
income (or equivalently, their log difference) fluctuates around a stable
mean. A VAR in differences applied to cointegrated series is
misspecified — it omits the error correction term that describes the
long-run equilibrium, and its forecasts and IRFs are therefore distorted at
long horizons.
The correct model for a system of cointegrated $I(1)$ variables is the
**vector error correction model (VECM)**. The VECM rewrites the VAR in a way
that explicitly separates the short-run dynamics (changes in each variable,
the differenced part) from the long-run equilibrium relationship (the
cointegrating vector, the levels part). It nests the VAR in differences as a
special case — when there is no cointegration — and adds the error correction
terms that describe how the system returns to its long-run attractor after
a shock.
::: {.callout-warning icon=false}
## Do Not Estimate a VAR in Levels with I(1) Variables
If unit root tests suggest that one or more variables in your system are
$I(1)$, estimating a VAR in levels produces unreliable inference. The correct
path is:
1. Test each variable for unit roots (Chapter 4 tools).
2. Test for cointegration among the $I(1)$ variables (Johansen test —
Chapter 8).
3. If cointegration is found, estimate a VECM.
4. If no cointegration is found, estimate a VAR in first differences.
A VAR in levels with $I(1)$ variables is neither of these. Do not use it.
:::
## Looking Ahead {#sec-lookahead7}
This chapter built the complete reduced-form and structural VAR toolkit:
from the endogeneity motivation through lag selection, Granger causality,
Cholesky identification, impulse responses, and variance decomposition. The
tools are powerful, but they rest on a maintained assumption that every
variable in the system is stationary. Section 7.9 showed what happens when
that assumption fails and pointed toward two responses: difference the
variables, or model the long-run relationship explicitly. The first option
discards information about long-run relationships between variables; the
second requires a framework that does not yet exist in our toolkit.
**Chapter 8 — Cointegration and the Vector Error Correction Model** develops
that framework. When two or more $I(1)$ variables share a common stochastic
trend — when they are **cointegrated** — the **vector error correction model
(VECM)** provides a complete description of their joint dynamics. The VECM
reparameterises the VAR in levels so that short-run dynamics and the long-run
equilibrium relationship appear as distinct, estimable components. Determining
how many long-run relationships exist requires the **Johansen trace test**, a
procedure that examines the rank of the long-run coefficient matrix
$\boldsymbol{\Pi}$ through its eigenvalues: a rank of zero means no
cointegration and a VAR in first differences is appropriate; a positive rank
means the variables share common stochastic trends and the VECM is required.
The running example will be the US yield curve — the 10-year Treasury yield
and the 3-month T-bill rate. The **expectations hypothesis of the term
structure** predicts that these two rates are cointegrated with a cointegrating
vector of $(1, -1)'$: both rates are driven by the same monetary policy and
inflation expectations, so their spread should be stationary around a constant
term premium rather than drifting. That spread is also one of the most reliable
recession indicators in macroeconomics — yield curve inversions have preceded
every US recession since 1954 — and the VECM gives us a precise language for
understanding why: an inversion is an extreme deviation from the long-run
equilibrium, and the error correction mechanism predicts the forces that
eventually restore a positive spread.
## Key Terms {#sec-keyterms7}
::: {.callout-note icon=false}
## Glossary
**Vector autoregression VAR($p$)** — A system of $n$ equations in which each
variable is modelled as a linear function of $p$ lags of all variables in
the system plus a composite innovation:
$\mathbf{y}_t = \mathbf{c} + \sum_{\ell=1}^{p}\mathbf{A}_\ell\mathbf{y}_{t-\ell} + \mathbf{u}_t$.
The reduced form is estimable by OLS equation by equation; its coefficients
are not structurally interpretable without additional identifying restrictions.
**Companion matrix** $\mathbf{F}$ — The $np \times np$ matrix that writes
any VAR($p$) as a first-order system
$\boldsymbol{\xi}_t = \mathbf{F}\boldsymbol{\xi}_{t-1} + \tilde{\mathbf{u}}_t$.
Its eigenvalues are the $np$ characteristic roots of the VAR. Stability
requires all eigenvalues inside the unit circle. Also used to compute IRFs
via $\boldsymbol{\Theta}_h = \mathbf{J}\mathbf{F}^h\mathbf{J}'\mathbf{P}$.
**Granger causality** — Variable $x$ Granger-causes variable $y$ if past
values of $x$ have marginal predictive content for $y$ beyond the information
in past values of $y$ (and all other variables in the system). Tested by the
block exclusion F-test: $H_0$ is that all $p$ coefficients on lags of $x$ in
the $y$ equation are jointly zero. A statement about predictive precedence,
not structural causality.
**Structural VAR (SVAR)** — A VAR augmented with identifying restrictions on
the contemporaneous impact matrix $\mathbf{A}_0$ that allow the reduced-form
composite residuals $\mathbf{u}_t = \mathbf{A}_0^{-1}\boldsymbol{\varepsilon}_t$
to be decomposed into interpretable structural shocks
$\boldsymbol{\varepsilon}_t$. With $n$ variables, just-identification requires
$n(n-1)/2$ restrictions beyond the $n$ diagonal normalisations.
**Cholesky identification** — A recursive identifying scheme that imposes a
lower-triangular structure on $\mathbf{A}_0$, equivalent to factoring
$\boldsymbol{\Sigma}_u = \mathbf{P}\mathbf{P}'$ and defining orthogonalised
shocks as $\boldsymbol{\varepsilon}_t = \mathbf{P}^{-1}\mathbf{u}_t$. The
ordering of variables determines which contemporaneous effects are set to
zero; the ordering is an economic assumption, not a statistical one.
**Impulse response function (IRF)** — $\text{IRF}_{ij}(h) = \theta_{ij,h}$,
the response of variable $i$ to a unit structural shock $j$ at horizon $h$,
computed as the $(i,j)$ element of $\boldsymbol{\Theta}_h =
\mathbf{J}\mathbf{F}^h\mathbf{J}'\mathbf{P}$. In a stable VAR all IRFs
converge to zero. Presented as the $n \times n$ grid of panels, with diagonal
own-shock responses starting at 1 by the unit-variance normalisation.
**Forecast error variance decomposition (FEVD)** — The share
$\omega_{ij}(h)$ of the $h$-step forecast error variance of variable $i$
attributable to structural shock $j$:
$\omega_{ij}(h) = \sum_{s=0}^{h-1}\theta_{ij,s}^2 \,/\, \sum_{k}\sum_{s=0}^{h-1}\theta_{ik,s}^2$.
Shares sum to 100 percent at every horizon. Horizon-dependent: short-run
and long-run attributions can differ substantially.
**Price puzzle** — The empirical finding in some estimated monetary VARs
that a contractionary policy shock is followed by a *rise* in inflation,
contradicting theory. Typically attributed to imperfect identification: the
funds rate increase is partly an endogenous response to anticipated inflation
not yet visible in the CPI, so the estimated IRF conflates the policy action
with the inflationary pressure it was responding to.
**Sign restrictions** — An identification scheme that recovers structural
shocks by restricting the *sign* of their contemporaneous or short-run
impacts rather than setting specific elements to zero. A contractionary
policy shock is defined as one that raises the funds rate and reduces
inflation; any $\mathbf{A}_0$ consistent with those sign patterns is
admissible. Results are reported as identified sets (ranges of IRFs) rather
than point estimates.
**VARX($p$, $s$)** — A VAR($p$) augmented with $s + 1$ lags of a $k \times 1$
vector of exogenous variables $\mathbf{x}_t$:
$\mathbf{y}_t = \mathbf{c} + \sum_\ell \mathbf{A}_\ell \mathbf{y}_{t-\ell} +
\sum_j \mathbf{B}_j \mathbf{x}_{t-j} + \mathbf{u}_t$.
Exogenous variables are conditioned on but not modelled; they do not receive
structural shocks and do not enter the FEVD of the endogenous block. Exogenous
variables must be $I(0)$; $I(1)$ exogenous variables should be first-differenced
before inclusion unless cointegration with the endogenous block is modelled
explicitly via a VECMX.
**Cointegration** — Two or more $I(1)$ series are cointegrated if a linear
combination of them is $I(0)$. Cointegrated series share a common stochastic
trend and cannot drift arbitrarily far apart. A VAR in first differences
applied to cointegrated series is misspecified — it omits the error
correction mechanism that describes the long-run equilibrium. The appropriate
model is the VECM, developed in Chapter 8 following the Johansen test for
cointegrating rank.
**Vector error correction model (VECM)** — A reparameterisation of a VAR
for cointegrated $I(1)$ variables that separates short-run dynamics (in
first differences) from the long-run equilibrium relationship (the
cointegrating vector, which acts as an error correction term pulling the
system back toward equilibrium after shocks). The VECM is a special case
of a state space model: the cointegrating residual is an unobserved state
estimated optimally by the Kalman filter. Full treatment in Chapter 11.
:::