SVAR identification II — external instruments and long-run restrictions¶

Vector autoregressions, Part 2 — Structural VAR: external instruments & long-run restrictions¶

This is the second half of **Part 2, *Structural VAR***. Its companion notebook (sign restrictions) replaced the recursive (Cholesky) ordering with sign restrictions; here we complete the modern identification toolkit with two further schemes, each pinning the structural impact matrix a different way:

  • External-instrument / proxy SVAR (Stock-Watson 2012/18; Mertens-Ravn 2013) — bring in an outside variable correlated with the target shock but nothing else.
  • Long-run restrictions (Blanchard-Quah 1989) — impose what a shock does in the limit (e.g. demand shocks are neutral for output in the long run).

Both are built in a new module svar_id.py that imports the bvar.py posterior and adds the identification (the original bvar.py is untouched; the svar_sign.py module from the sign-restriction notebook supplies the shared MA-IRF machinery).

Prior hyperparameters. Every macro reduced form below is fit with bvar.py's Minnesota prior at λ₁ = 0.2 (overall tightness), λ₂ = 0.5 (the traditional scalar cross-variable shrinkage of Litterman 1986 — not the data-driven default), and λ₃ = 1 (linear lag decay), with the funds-rate system given a near-unit-root own-persistence prior mean of 0.9.

Method 1 — the external-instrument (proxy) SVAR¶

The reduced-form residuals $\varepsilon_t = P u_t$ mix the structural shocks $u_t$; identification is choosing the column of $P$ for the shock of interest. Sign/Cholesky pick it from internal assumptions. A proxy picks it from an external instrument $z_t$ — an observed series that is relevant (correlated with the target shock $u_{1t}$) and exogenous (uncorrelated with the others). Then $$\mathbb{E}[\varepsilon_t\,z_t] = P\,\mathbb{E}[u_t z_t] = P\begin{pmatrix}\phi\\0\\\vdots\\0\end{pmatrix} = \phi\,p_1,$$ so the covariance of the residuals with the instrument is the target impact vector $p_1$, up to scale — computed by simply regressing the residuals on $z_t$: $\hat b = (\varepsilon'z)/(z'z)$. We normalize $\hat b$ so the policy variable moves by 1 pp on impact, and trace $\Theta_h\hat b$ for the IRFs. Doing this for every bvar.py posterior draw gives the bands.

The instrument. For the US monetary shock we use the Romer & Romer (2004) narrative shock — the part of the funds-rate change that the FOMC's own Greenbook forecasts could not explain — from Ramey's (2016) replication data (ramey_monetary_data.csv, monthly 1969–2007). The VAR is the standard 5-variable system:

  • log IP — the Industrial Production index (FRED series INDPRO), the Federal Reserve's monthly index of real output in manufacturing, mining, and utilities. It is the workhorse real-activity variable in monthly monetary VARs, standing in for the business cycle because GDP is only quarterly.
  • unemployment rate,
  • log CPI — the Consumer Price Index (the price level),
  • log Pcom — an index of commodity prices (included to help tame the price puzzle), and
  • FFR — the federal funds rate, the monetary-policy instrument.

The three index variables enter in logs ×100, so their impulse responses read directly as percent deviations from baseline (e.g. a −1.16% IP trough = industrial production about 1.2% below where it would otherwise be).

In [1]:
import numpy as np, pandas as pd, matplotlib.pyplot as plt
from statsmodels.tsa.api import VAR
import bvar, svar_id as si

d = pd.read_csv("ramey_monetary_data.csv")
Y = np.column_stack([d["LIP"]*100, d["UNEMP"], d["LCPI"]*100, d["LPCOM"]*100, d["FFR"]])   # logs x100 -> %
z = d["RRSHOCK"].values; lab = ["log IP", "Unemp", "log CPI", "log Pcom", "FFR"]; p = 12; H = 48
fit = bvar.filter_stationary(bvar.gibbs_minnesota(Y, p=p, own_mean=0.9, lam2=0.5, ndraw=3000, burn=1000, seed=1))
res = si.proxy_svar(fit, Y, z, policy_idx=4, H=H, norm=1.0)                # normalize: shock raises FFR by 1pp
lo, med, hi = si.bands(res)
chol = VAR(pd.DataFrame(Y, columns=lab)).fit(p).irf(H).orth_irfs[:, :, 4]  # recursive IRF, FFR ordered last
print("PROXY-SVAR (Romer-Romer instrument), monetary tightening normalized to +1pp FFR:")
print("  output (log IP) trough: %.2f%% at month %d   (clear, LARGE contraction)" % (med[:, 0].min(), med[:, 0].argmin()))
print("  Cholesky output trough: %.2f%%                (recursive VAR badly understates it)" % chol[:, 0].min())
print("  CPI response at 24m: %.2f%%                   (price puzzle: stubborn even with commodity prices)" % med[24, 2])

for k, i in enumerate([0, 2, 4]):
    if k == 0: fig, ax = plt.subplots(1, 3, figsize=(14, 3.6))
    ax[k].fill_between(range(H+1), lo[:, i], hi[:, i], color="darkgreen", alpha=.2, label="proxy 68% band")
    ax[k].plot(med[:, i], color="darkgreen", lw=1.6, label="proxy-SVAR median")
    ax[k].plot(chol[:, i], color="navy", lw=1.3, ls="--", label="Cholesky")
    ax[k].axhline(0, color="0.6", lw=.6); ax[k].set_title("Response of %s" % lab[i]); ax[k].set_xlabel("months")
ax[0].legend(fontsize=7, loc="lower left")
fig.suptitle("External-instrument (proxy) SVAR: the Romer-Romer narrative shock identifies the monetary shock without an ordering.\n"
             "Output contracts ~1% (far more than the recursive VAR); the price response stays puzzling (a well-known difficulty)", y=1.07)
plt.tight_layout(); plt.show()
PROXY-SVAR (Romer-Romer instrument), monetary tightening normalized to +1pp FFR:
  output (log IP) trough: -1.16% at month 33   (clear, LARGE contraction)
  Cholesky output trough: -0.35%                (recursive VAR badly understates it)
  CPI response at 24m: 0.69%                   (price puzzle: stubborn even with commodity prices)
No description has been provided for this image

Reading the proxy-SVAR¶

  • The external instrument reveals a big, credible output effect. The Romer-Romer-identified tightening drives output down ~1% — while the recursive Cholesky IRF (navy) has output barely moving (trough ~−0.35%). This is Coibion's (2012) reconciliation: narrative/external identification implies substantially larger real effects of monetary policy than a recursive VAR, which contaminates the shock with systematic policy reactions. No ordering was assumed — the instrument did the work.
  • The price puzzle is stubborn. Even with a narrative instrument and commodity prices in the system, the CPI response stays positive for years. That is not a bug in the method; it is a genuine, much-debated empirical difficulty (Ramey 2016) — a reminder that credible identification is necessary but not sufficient to make every response behave.
  • Relevance matters. A weak instrument (low correlation with the shock) gives an imprecise, unreliable impact vector — the proxy-SVAR's Achilles heel, the analogue of weak instruments in IV regression.

Method 2 — long-run restrictions (Blanchard-Quah)¶

Instead of restricting the impact, restrict the permanent effect. In a VAR whose first variable is a growth rate, the cumulative response is the level, and its long-run value is governed by the long-run multiplier $\Xi=(I-\sum_l\Pi_l)^{-1}$. Blanchard-Quah (1989) identify two shocks by declaring that only one has a permanent effect on output: the long-run structural matrix $\Xi P$ must be lower-triangular, so $$\Xi P = \operatorname{chol}\!\big(\Xi\,\Sigma\,\Xi'\big),\qquad\Longrightarrow\qquad P=\Xi^{-1}\operatorname{chol}\!\big(\Xi\Sigma\Xi'\big).$$ The result is a supply shock (permanent effect on output — technology, the production frontier) and a demand shock (only transitory — it dies out as prices adjust). We fit it on $[\text{output growth},\ \text{unemployment}]$ (Blanchard-Quah's original pairing) from the quarterly macrodata, and plot the cumulative output response so the permanent-vs-transitory distinction is visible.

In [2]:
import statsmodels.api as sm
mdd = sm.datasets.macrodata.load_pandas().data
Yb = np.column_stack([400*np.diff(np.log(mdd["realgdp"].values)), mdd["unemp"].values[1:]])   # [output growth, unemployment]
fb = bvar.filter_stationary(bvar.gibbs_minnesota(Yb, p=2, lam2=0.5, ndraw=4000, burn=1000, seed=1))
rb = si.blanchard_quah(fb, H=40); irf = rb["irf"]                         # (ndraw, H+1, 2, 2): [.,h,var,shock]
sgn = np.sign(irf[:, 0, 0, :])[:, None, None, :]; irf = irf * sgn          # normalize each shock to raise output on impact
cumout = np.cumsum(irf[:, :, 0, :], axis=1)                                # cumulative output level (level = cumsum of growth)
co_lo, co_md, co_hi = np.percentile(cumout, [16, 50, 84], axis=0); un_md = np.median(irf[:, :, 1, :], axis=0)
print("BLANCHARD-QUAH long-run restrictions [output growth, unemployment]:")
print("  cumulative OUTPUT to PERMANENT (supply) shock: h=0,20,40 = %s   (settles: permanent)" % np.round(co_md[[0,20,40],0], 2))
print("  cumulative OUTPUT to TRANSITORY (demand) shock: h=0,20,40 = %s   (returns to ~0)" % np.round(co_md[[0,20,40],1], 2))

fig, ax = plt.subplots(1, 2, figsize=(12, 3.6)); labs = ["permanent (supply) shock", "transitory (demand) shock"]; cs = ["firebrick", "steelblue"]
for k in range(2):
    ax[0].fill_between(range(41), co_lo[:, k], co_hi[:, k], color=cs[k], alpha=.15); ax[0].plot(co_md[:, k], color=cs[k], lw=1.6, label=labs[k])
    ax[1].plot(un_md[:, k], color=cs[k], lw=1.6, label=labs[k])
for a, t in zip(ax, ["Cumulative OUTPUT level response", "Unemployment response"]):
    a.axhline(0, color="0.6", lw=.6); a.set_title(t); a.set_xlabel("quarters"); a.legend(fontsize=8)
fig.suptitle("Long-run (Blanchard-Quah) identification: the SUPPLY shock moves output permanently; the DEMAND shock only transitorily", y=1.03)
plt.tight_layout(); plt.show()
BLANCHARD-QUAH long-run restrictions [output growth, unemployment]:
  cumulative OUTPUT to PERMANENT (supply) shock: h=0,20,40 = [2.39 2.6  2.51]   (settles: permanent)
  cumulative OUTPUT to TRANSITORY (demand) shock: h=0,20,40 = [2.2  0.6  0.11]   (returns to ~0)
No description has been provided for this image

Results — three identifications, three kinds of restriction¶

  • Long-run restrictions work cleanly here. The supply shock lifts output to a permanent ~2.5% higher level; the demand shock's output effect dies out — exactly the Blanchard-Quah decomposition, with both shocks lowering unemployment on impact (Okun's law). Where the restriction is credible (output is driven by a permanent technology component) it is very informative.
  • A menu, not a winner. Across this Part 2 the same reduced form has been identified four ways — recursive, sign, external-instrument, long-run — each imposing a different kind of belief (a timing zero, an IRF sign, an outside instrument, a limit). None is universally right; the craft is matching the restriction you can defend to the question you are asking. The external instrument is the current gold standard for a single well-measured shock; long-run and sign restrictions shine when a credible instrument is unavailable.
  • All of it rides the same posterior. Every band here is a function of the bvar.py reduced-form draws — identification and Bayesian inference compose cleanly.

References¶

  • Stock, J. H. & Watson, M. W. (2018). Identification and estimation of dynamic causal effects with an application to external instruments. Economic Journal 128, 917–948.
  • Mertens, K. & Ravn, M. O. (2013). The dynamic effects of personal and corporate income tax changes in the US. American Economic Review 103, 1212–1247.
  • Romer, C. D. & Romer, D. H. (2004). A new measure of monetary shocks: derivation and implications. AER 94, 1055–1084. • Coibion, O. (2012). Are the effects of monetary policy shocks big or small? AEJ: Macro 4, 1–32.
  • Blanchard, O. J. & Quah, D. (1989). The dynamic effects of aggregate demand and supply disturbances. AER 79, 655–673.

Next: bvar_identification_R.ipynb — the R cross-check (vars::BQ + a proxy-SVAR).