Factor-Augmented VAR (FAVAR)¶

Vector autoregressions, Part 9b: the whole economy in one small VAR¶

Part 9a handled "big data" by shrinking a large VAR. The Factor-Augmented VAR of Bernanke-Boivin-Eliasz (2005) takes the opposite route — dimension reduction. Instead of a small monetary VAR that omits most of the information the central bank actually watches (and so suffers the price puzzle), it summarises a large panel of macro series by a handful of principal-component factors, puts those few factors and the policy rate in a small VAR, and then — the pay-off — recovers the impulse response of every one of the underlying series to a monetary shock through its factor loadings. One estimation, and the response of the entire economy to a rate hike falls out.

The model¶

Let $X_t$ be a large panel of $n$ (here 169) informational series, transformed to stationarity and standardised, driven by $K$ latent factors $F_t$ and the observed policy rate $R_t$: $$X_t = \Lambda^F F_t + \Lambda^R R_t + e_t,\qquad \begin{bmatrix}F_t\\ R_t\end{bmatrix}=c+\Phi(L)\begin{bmatrix}F_{t-1}\\ R_{t-1}\end{bmatrix}+v_t.$$ Estimation is the BBE two-step: (1) extract the factors $F_t$ as the first $K$ principal components of $X_t$; (2) fit a VAR on $[F_t, R_t]$. The monetary shock is identified recursively with the policy rate ordered last — the factors (real activity, prices) do not respond to the rate within the quarter. To keep that assumption clean we apply the BBE slow-fast correction: the factors are purged of any contemporaneous response to $R_t$, using factors extracted from the slow-moving (real and price) block only. Finally, the response of any series $x_{i}$ is reconstructed from its loading, $\text{IRF}(x_i)=\Lambda_i\,\text{IRF}([F,R])$. Everything is in a self-contained favar.py; bands are a residual bootstrap of the state VAR.

Data: favar_data.csv — 169 FRED-QD series transformed to stationarity and standardised, plus the funds rate; quarterly 1960–2019.

In [1]:
import numpy as np, pandas as pd, re, matplotlib.pyplot as plt
import favar as FV

pan = pd.read_csv("favar_data.csv", index_col=0, parse_dates=True)
R = pan["FFR_LEVEL"].values                                               # funds-rate level (the policy variable)
X = pan.drop(columns=["FEDFUNDS", "FFR_LEVEL"]); names = list(X.columns)
# slow = real + prices; fast = interest rates / money / credit / exchange / stocks (respond to policy within the quarter)
fast = re.compile(r"(FEDFUNDS|TB\dMS|GS\d|AAA|BAA|MORTG|CP3M|COMPAPFF|FFM|M1|M2|MZM|LOAN|REVSL|TOTRES|NONBOR|BOGMBASE|DTC|INVEST|EX(SZ|JP|US|CA)|TWEX|S.P|UMCSENT|VXO)", re.I)
slow = np.array([not bool(fast.search(n)) for n in names])

res = FV.favar(X.values, R, K=4, p=2, H=20, slow_mask=slow)               # PCA factors + [F,R] VAR + slow-fast recursive ID
print("K=4 factors explain %.0f%% of the %d-variable panel; FFR impact response %.2f"
      % (100 * res["evr"][:4].sum(), len(names), res["irf_state"][0, -1]))
# figure: state responses (4 factors + funds rate) to the monetary shock
st = res["irf_state"]; K = res["K"]; Hs = st.shape[0] - 1
slab = ["factor %d" % (j + 1) for j in range(K)] + ["funds rate"]
fig, ax = plt.subplots(1, K + 1, figsize=(15, 3), sharex=True)
for j in range(K + 1):
    ax[j].plot(st[:, j], color="firebrick", lw=1.7); ax[j].axhline(0, color="k", lw=.5)
    ax[j].set_title(slab[j], fontsize=10); ax[j].set_xlabel("quarters"); ax[j].grid(alpha=.25)
fig.suptitle("State responses to a monetary (funds-rate) tightening — the compact [factors, rate] VAR", y=1.04)
fig.tight_layout(); plt.show()
K=4 factors explain 43% of the 169-variable panel; FFR impact response 0.58
No description has been provided for this image

The state responses¶

A monetary tightening raises the funds rate (which then mean-reverts) and moves the latent factors, which reconstruct into a broad contraction. On their own the factors are not interpretable (their signs are arbitrary); their value is that every observable loads on them, so one VAR delivers the response of the whole panel.

In [2]:
# Reconstruct the response of individual series: IRF(x_i) = loading_i @ IRF([F,R]).
irf = res["irf_panel"]                                                    # (H+1, n) responses of all 169 series
show = ["GDPC1", "PCECC96", "GPDIC1", "INDPRO", "PAYEMS", "UNRATE", "CPIAUCSL", "HOUST", "GS10"]
tr = {v: round(float(irf[np.argmax(np.abs(irf[:, names.index(v)])), names.index(v)]), 2) for v in show[:5]}
print("real-side troughs (std):", tr)
print("unemployment peak:", round(irf[np.argmax(np.abs(irf[:, names.index('UNRATE')])), names.index('UNRATE')], 2),
      "| CPI 1q blip then:", np.round(irf[[1, 4, 8], names.index("CPIAUCSL")], 2))
# figure: a 3x3 grid of reconstructed IRFs across the economy
nice = {"GDPC1": "real GDP", "PCECC96": "consumption", "GPDIC1": "investment",
        "INDPRO": "industrial prod.", "PAYEMS": "employment", "UNRATE": "unemployment",
        "CPIAUCSL": "consumer prices", "HOUST": "housing starts", "GS10": "10-year yield"}
fig, ax = plt.subplots(3, 3, figsize=(13, 8.5), sharex=True)
for k, v in enumerate(show):
    a = ax[k // 3, k % 3]; a.plot(irf[:, names.index(v)], color="firebrick", lw=1.6)
    a.axhline(0, color="k", lw=.5); a.set_title(nice[v], fontsize=10); a.grid(alpha=.25)
for j in range(3): ax[2, j].set_xlabel("quarters")
fig.suptitle("Economy-wide response to a monetary tightening, reconstructed from the loadings "
             "(9 of 169 series; std units)", y=1.005)
fig.tight_layout(); plt.show()
real-side troughs (std): {'GDPC1': -0.09, 'PCECC96': -0.1, 'GPDIC1': -0.08, 'INDPRO': -0.09, 'PAYEMS': -0.09}
unemployment peak: 0.1 | CPI 1q blip then: [ 0.12 -0.   -0.01]
No description has been provided for this image

Results — one VAR, the response of everything¶

  • Textbook monetary transmission, across the whole cross-section. A tightening contracts output, consumption, investment, industrial production and employment (troughs all around −0.1 std), raises unemployment, and cools housing — a coherent, economy-wide contraction recovered from a single four-factor VAR. This is the FAVAR pay-off: the response of dozens of series, not the three or ten a VAR can hold directly.
  • The price response, honestly. Consumer prices show a one-quarter blip up then disinflation — a mild, transient price puzzle that the slow-fast identification tames but does not fully remove (as in Bernanke-Boivin-Eliasz's own results). The richer information set shrinks the puzzle a small VAR would show, without eliminating it.
  • Two routes to big data. Part 9a keeps all the variables and shrinks (large BVAR-SV); this notebook compresses them into factors (FAVAR). Shrinkage preserves every series as a modelled object with its own volatility; the factor route buys a much larger information set and a comprehensive impulse-response picture at the cost of treating the individual series as loadings on a few common factors. They are complementary answers to the same problem — how to let a VAR see the whole economy. The R cross-check reproduces the factors and the monetary responses with vars.

References¶

  • Bernanke, B. S., Boivin, J. & Eliasz, P. (2005). Measuring the effects of monetary policy: a factor-augmented vector autoregressive (FAVAR) approach. Quarterly Journal of Economics 120, 387–422.
  • Stock, J. H. & Watson, M. W. (2002). Macroeconomic forecasting using diffusion indexes. J. Business & Economic Statistics 20, 147–162.

Next: favar_R.ipynb — the R cross-check (vars on the extracted factors).