Put confidence bands on a VAR impulse response¶
tsecon.var_irf gives you the point path and nothing else — deliberately, so
that the cheap call stays cheap. The moment you want to say whether a response
is distinguishable from zero, switch to tsecon.var_irf_bands, which takes
the same (data, lags, horizon, orth) arguments and returns a dict with
point, se, lower, and upper.
The recipe¶
import numpy as np, tsecon
rng = np.random.default_rng(0) # a toy 2-variable macro system
A = np.array([[0.55, 0.10], [0.35, 0.60]]) # VAR(1) coefficients
L = np.array([[1.00, 0.00], [0.50, 0.80]]) # correlated innovations
y = np.zeros((240, 2))
for t in range(1, 240):
y[t] = A @ y[t - 1] + L @ rng.standard_normal(2)
b = tsecon.var_irf_bands(y, lags=2, horizon=10, method="bootstrap",
n_boot=2000, seed=0, alpha=0.10) # 90% percentile bands
point, lower, upper = (np.array(b[k])[:, 1, 0] for k in ("point", "lower", "upper"))
for h in (0, 2, 4, 6, 8, 10):
print(f"h={h:2d} {point[h]:+.3f} [{lower[h]:+.3f}, {upper[h]:+.3f}]")
h= 0 +0.511 [+0.406, +0.607]
h= 2 +0.680 [+0.533, +0.797]
h= 4 +0.471 [+0.293, +0.594]
h= 6 +0.253 [+0.095, +0.372]
h= 8 +0.118 [+0.003, +0.227]
h=10 +0.049 [-0.024, +0.139]
Reading it¶
Every array is indexed [horizon][response][shock], exactly like var_irf, so
[:, 1, 0] is "variable 1's response to a shock in variable 0". The response
is hump-shaped, peaks at h=2, still excludes zero at h=8, and no longer does
by h=10 — that last line is the finding, not a footnote. Slicing the band
arrays the same way you slice var_irf means you can drop banded responses into
an existing plot without reshaping anything.
Two knobs decide what the band means:
alphais the non-coverage:alpha=0.10is a 90% band,alpha=0.32the 68% band macro papers often plot.seedmakes the bootstrap reproducible. Report it. A bootstrap band without a seed and ann_bootis not a replicable number.
The asymptotic alternative¶
method="asymptotic" — the default — gives Lütkepohl delta-method standard
errors and Wald bands (point ± z·se) with no resampling. It is instant, and it
hands you a usable se array:
a = tsecon.var_irf_bands(y, lags=2, horizon=10, method="asymptotic", alpha=0.10)
print("method:", a["method"], " n_boot:", a["n_boot"])
for h in (0, 4, 8):
print(f"h={h:2d} se {np.array(a['se'])[h, 1, 0]:.3f} "
f"[{np.array(a['lower'])[h, 1, 0]:+.3f}, {np.array(a['upper'])[h, 1, 0]:+.3f}]")
method: asymptotic n_boot: None
h= 0 se 0.058 [+0.415, +0.606]
h= 4 se 0.091 [+0.322, +0.620]
h= 8 se 0.072 [-0.001, +0.236]
Note that the two methods disagree about h=8: the bootstrap band excludes
zero, the asymptotic one just barely includes it. That disagreement is
information — delta-method bands are symmetric by construction and are known to
undercover at longer horizons in short samples, which is why method="bootstrap"
is the one to reach for when the answer matters. n_boot comes back None on
the asymptotic branch so a logged result always records which method ran.
Gotchas¶
orth=Trueis the default and it is an identification assumption. The bands describe sampling uncertainty about a Cholesky-ordered response; they say nothing about whether the ordering is defensible. For a causal reading, go to sign restrictions or a local projection.- Check stability first.
tsecon.var_fit(y, lags=2)["is_stable"]must beTrue; near-unit-root systems make long-horizon bands unreliable under either method. bias_correct=Trueapplies the Kilian (1998) bootstrap bias correction on the bootstrap branch — worth trying when the VAR is persistent.- Want cumulative (level) responses? Pass
cumulative=True; it means the same thing here as invar_irf.