Markov-switching VAR¶
Vector autoregressions, Part 11: discrete regimes, discovered by the data¶
Part 5's time-varying-parameter VAR let the economy's dynamics drift smoothly. But some changes are not gradual — they are switches: the economy flips between a calm state and a turbulent one, between active and passive policy, between expansion and recession. The Markov-switching VAR (Hamilton 1989; Krolzig 1997) models exactly that: the VAR's parameters jump between a small number of regimes, governed by a latent Markov chain that the data infer. Where Part 10 imposed the Great-Moderation break to identify shocks, the MS-VAR is handed no dates at all — it discovers the regimes, and (as we'll see) rediscovers 1984 on its own. Built from scratch with a Gibbs sampler whose centrepiece is the forward-filter-backward-sample of the hidden regime path.
The model and the sampler¶
An MSIH-VAR — switching intercept and covariance, constant autoregressive dynamics: $$y_t = c_{S_t} + B_1 y_{t-1} + \dots + B_p y_{t-p} + \varepsilon_t,\qquad \varepsilon_t\sim\mathcal N(0,\Sigma_{S_t}),$$ where $S_t\in\{1,\dots,K\}$ is a latent Markov chain with transition matrix $P$ ($P_{jk}=\Pr(S_t=k\mid S_{t-1}=j)$). Each regime has its own mean level $c_k$ and shock covariance $\Sigma_k$; $P$'s diagonal governs how persistent the regimes are.
Gibbs sampler (from scratch). Three blocks cycle:
- The regime path $S_{1:T}$ — forward-filter-backward-sample. Run the Hamilton filter forward: predict $\Pr(S_t\mid y_{1:t-1})$ through $P$, then update with each regime's VAR likelihood $\mathcal N(y_t;c_k+B x_t,\Sigma_k)$. Then sample backwards: draw $S_T$ from its filtered distribution and $S_t\mid S_{t+1}$ down to $t=1$. This is the discrete-state analogue of the Carter-Kohn smoother.
- The parameters — given $S$, each regime's intercept and covariance are conjugate Normal / inverse-Wishart draws; the constant lag matrix $B$ is a GLS draw across regimes.
- The transition matrix $P$ — each row is a Dirichlet draw from the counted regime transitions.
We fix the label-switching by ordering regimes by volatility (regime 0 = calm, regime 1 = turbulent). Data: the same monetary system as Parts 4/10 (bvar_sv_data.csv: GDP growth, inflation, the funds rate), $K=2$ regimes, $p=2$.
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import msvar as MS
d = pd.read_csv("bvar_sv_data.csv", index_col=0, parse_dates=True)
lab = ["GDP growth", "inflation", "funds rate"]; Y = d[["gdp_growth", "inflation", "tbill"]].values
dates = d.index; p = 2
fit = MS.gibbs_msvar(Y, p, K=2, ndraw=4000, burn=2500, seed=1) # Gibbs with FFBS of the regime path
pturb = fit["S"].mean(0) # smoothed P(turbulent regime | data)
tdates = dates[p:]; Pm = fit["P"].mean(0)
sd = np.sqrt(fit["Sigma"].mean(0)[:, [0, 1, 2], [0, 1, 2]])
print("regime shock volatilities (gdp/infl/tbill): calm", np.round(sd[0], 1), " turbulent", np.round(sd[1], 1))
print("P(turbulent) pre-1984 %.2f post-1984 %.2f (the Great Moderation, found endogenously)"
% (pturb[tdates < pd.Timestamp("1984-01-01")].mean(), pturb[tdates >= pd.Timestamp("1984-01-01")].mean()))
print("persistence: calm %.2f (avg %.0fq) turbulent %.2f (avg %.0fq)" % (Pm[0,0], 1/(1-Pm[0,0]), Pm[1,1], 1/(1-Pm[1,1])))
# NBER recession quarters over the sample (peak -> trough)
recessions = [("1969-12", "1970-11"), ("1973-11", "1975-03"), ("1980-01", "1980-07"),
("1981-07", "1982-11"), ("1990-07", "1991-03"), ("2001-03", "2001-11"), ("2007-12", "2009-06")]
inrec = np.zeros(len(tdates), bool)
for a, b in recessions:
inrec |= (tdates >= pd.Timestamp(a)) & (tdates <= pd.Timestamp(b))
print("P(turbulent) in NBER recessions %.2f vs expansions %.2f" % (pturb[inrec].mean(), pturb[~inrec].mean()))
# figure: smoothed probability of the turbulent regime, NBER recessions shaded -> msvar_1.png
fig, ax = plt.subplots(figsize=(14, 4.5))
ax.plot(tdates, pturb, color="firebrick", lw=1.6)
ax.fill_between(tdates, pturb, color="firebrick", alpha=.35)
for a, b in recessions:
ax.axvspan(pd.Timestamp(a), pd.Timestamp(b), color="gray", alpha=.2)
ax.axvline(pd.Timestamp("1984-01-01"), color="navy", ls="--", lw=1.2)
ax.text(pd.Timestamp("1984-06-01"), 0.97, "Great Moderation begins (found endogenously)",
color="navy", fontsize=9, va="top")
ax.set_ylim(0, 1.05); ax.set_ylabel("P(turbulent regime | data)"); ax.grid(alpha=.25)
ax.set_title("Markov-switching VAR: smoothed probability of the high-volatility regime (shaded = NBER recessions).\n"
"The model discovers the Great Moderation and the turbulent episodes on its own - no dates imposed")
fig.tight_layout(); plt.show()
regime shock volatilities (gdp/infl/tbill): calm [2.4 0.8 0.4] turbulent [5.1 1.5 1.8] P(turbulent) pre-1984 0.40 post-1984 0.03 (the Great Moderation, found endogenously) persistence: calm 0.96 (avg 26q) turbulent 0.83 (avg 6q) P(turbulent) in NBER recessions 0.57 vs expansions 0.13
The regimes the data found¶
- The Great Moderation, rediscovered without a date. The turbulent (high-volatility) regime is active 40% of the time before 1984 but only 3% after — the model draws the Great-Moderation line on its own, from the data, precisely where Part 10 had to assume it. The calm regime, once entered around the mid-1980s, essentially never leaves (bar 2008).
- Turbulent = volatile and recessionary. The turbulent regime carries 2–4× the shock volatility of the calm one in all three variables (GDP-growth std 5.1 vs 2.4), and its smoothed probability spikes in the NBER recessions (average probability 0.57 in recessions vs 0.13 in expansions) as well as across the high-inflation 1970s. It is the postwar economy's "stress" state.
- Persistence differs sharply. The transition matrix implies the calm regime lasts ~26 quarters on average and the turbulent one ~6 — long stable stretches punctuated by shorter bouts of turbulence, the hallmark of Markov-switching dynamics.
print("transition matrix (posterior mean):\n", np.round(Pm, 2))
print("long-run regime shares:", np.round(np.linalg.matrix_power(Pm, 500)[0], 2))
# figure: regime-specific shock volatilities (bars) + transition matrix / durations / long-run shares -> msvar_2.png
lr = np.linalg.matrix_power(Pm, 500)[0]
fig, (axb, axt) = plt.subplots(1, 2, figsize=(14, 4.2), gridspec_kw={"width_ratios": [1.3, 1]})
x = np.arange(len(lab)); w = 0.4
axb.bar(x - w / 2, sd[0], w, color="steelblue", label="calm regime")
axb.bar(x + w / 2, sd[1], w, color="firebrick", label="turbulent regime")
axb.set_xticks(x); axb.set_xticklabels(lab)
axb.set_ylabel("shock std. dev."); axb.set_title("Shock volatility by regime")
axb.legend()
txt = ("Transition matrix (posterior mean):\n"
" to calm to turbulent\n"
"calm %.2f %.2f\n"
"turbulent %.2f %.2f\n\n"
"expected duration: calm %.0f q, turbulent %.0f q\n"
"long-run share: calm %.0f%%, turbulent %.0f%%\n\n"
"the turbulent regime is %.1fx more volatile (GDP)\n"
"and concentrated pre-1984 + in recessions"
% (Pm[0, 0], Pm[0, 1], Pm[1, 0], Pm[1, 1],
1 / (1 - Pm[0, 0]), 1 / (1 - Pm[1, 1]),
100 * lr[0], 100 * lr[1], sd[1, 0] / sd[0, 0]))
axt.axis("off")
axt.text(0.0, 0.95, txt, va="top", ha="left", family="monospace", fontsize=11, transform=axt.transAxes)
fig.suptitle("Two endogenous macro regimes: a persistent calm state and a volatile one")
fig.tight_layout(); plt.show()
transition matrix (posterior mean): [[0.96 0.04] [0.17 0.83]] long-run regime shares: [0.82 0.18]
Results — switches, not drift¶
- A different picture of change from Part 5. The TVP-VAR let every coefficient wander continuously; the MS-VAR says the important variation is a jump between two states. Both are true descriptions of a changing economy, but they answer different questions: how much has the world drifted? versus which regime are we in right now? The MS-VAR delivers a probabilistic chronology — a dated, regime-by-regime narrative of postwar macro — that a smooth-drift model cannot.
- Endogenous structural breaks. Because the regimes are inferred, the MS-VAR is a break detector: it locates the Great Moderation and the turbulent episodes without prior information, complementing the imposed-split identification of Part 10 and the continuous drift of Part 5. Together the three notebooks span the ways a VAR can be non-constant: smooth drift, imposed regimes, and data-discovered regimes.
- The building block. The forward-filter-backward-sample of a hidden discrete state is the same machinery behind regime-switching term-structure, business-cycle dating, and Markov-switching volatility models — here assembled from scratch for a VAR. The R cross-check recovers the same regime chronology with a Markov-switching model on GDP growth.
References¶
- Hamilton, J. D. (1989). A new approach to the economic analysis of nonstationary time series and the business cycle. Econometrica 57, 357–384.
- Krolzig, H.-M. (1997). Markov-Switching Vector Autoregressions. Springer.
- Kim, C.-J. & Nelson, C. R. (1999). State-Space Models with Regime Switching. MIT Press.
- Sims, C. A. & Zha, T. (2006). Were there regime switches in US monetary policy? American Economic Review 96, 54–81.
Data: bvar_sv_data.csv — GDP growth, inflation, funds rate, quarterly 1959–2019 (FRED-QD).
Next: msvar_R.ipynb — the R cross-check (MSwM).