🇬🇧 English · 🇪🇸 Español
Bayesian VAR with Stochastic Volatility (BVAR-SV)
puremacro.var.bvar_sv implements Bayesian Vector Autoregressions with time-varying residual volatility (BVAR-SV) in pure NumPy/SciPy, following the state-space and MCMC methodologies of Carriero, Clark, and Marcellino (2016, 2019), Kim, Shephard, and Chib (1998), and Carter and Kohn (1994).
Macroeconomic time series are characterized by pronounced regime shifts in volatility—such as the Great Moderation of 1984–2007, the acute shock of the 2007–2008 Global Financial Crisis, and the pandemic disruption of 2020. Standard homoskedastic VAR models distort parameter estimates and produce miscalibrated predictive densities by conflating changes in shock sizes with changes in transmission mechanisms.
BVAR-SV decomposes structural dynamics into constant autoregressive transmission coefficients and time-varying stochastic log-volatilities, providing volatility-conditioned impulse responses, posterior-predictive fan charts and honest predictive-density evaluation.
1. Econometric Framework
1.1 The State-Space Representation
Consider an \(n\)-variable vector autoregression with \(p\) lags:
The reduced-form residuals \(u_t\) follow a time-varying covariance structure:
where: 1. \(A\) is a lower-triangular contemporaneous matrix with unit diagonal capturing contemporaneous structural relations: $\(A = \begin{pmatrix} 1 & 0 & \dots & 0 \\ a_{2,1} & 1 & \dots & 0 \\ \vdots & \vdots & \ddots & 0 \\ a_{n,1} & a_{n,2} & \dots & 1 \end{pmatrix}\)$ 2. \(D_t = \text{diag}\left( \exp(h_{1,t}), \dots, \exp(h_{n,t}) \right)\) contains time-varying residual variances. 3. The reduced-form residual covariance matrix at date \(t\) is: $\(\Sigma_t = A^{-1} D_t (A^{-1})'\)$ 4. The unobserved log-volatility states \(h_{i,t}\) follow independent stationary \(\text{AR}(1)\) processes for each variable \(i = 1, \dots, n\): $\(h_{i,t} - \mu_i = \phi_i (h_{i,t-1} - \mu_i) + \sigma_{h,i} \eta_{i,t}, \quad \eta_{i,t} \sim \mathcal{N}(0, 1), \quad |\phi_i| < 1\)$ with the stationary initial condition \(h_{i,1} \sim \mathcal{N}\left(\mu_i, \, \frac{\sigma_{h,i}^2}{1 - \phi_i^2}\right)\).
1.2 Priors
- VAR coefficients \(\beta = \text{vec}(B')\): independent normal prior. With
minnesota_prior=True(default) the Minnesota prior of the package (lambda1,lambda2,lambda3; own first lag centred at 1) is used; withminnesota_prior=Falseevery lag coefficient gets a diffuse \(\mathcal{N}(0, 100^2)\) prior. In both modes the intercepts get \(\mathcal{N}(0, \texttt{intercept\_prior\_std}^2)\). - Contemporaneous coefficients: \(a_{i,j} \sim \mathcal{N}(0, 100)\).
- Volatility hyper-parameters (fixed hyper-priors): \(\mu_i \sim \mathcal{N}(0, 10)\), \(\phi_i \sim \mathcal{N}(0.85, 0.1)\) truncated to \(|\phi_i| < 0.999\), \(\sigma_{h,i}^2 \sim \text{IG}(2, 0.05)\). The offset in \(\log(\nu_{i,t}^2 + c)\) is \(c = 10^{-6}\).
2. The MCMC Gibbs Sampling Algorithm
The model is estimated via a Gibbs sampler across sequential blocks:
- Step 1: VAR Coefficients (\(\beta\))
Conditional on \(A\) and the volatility paths \(D_{1:T}\), \(\beta\) is drawn jointly from its exact conditional posterior
$\(\beta \mid A, h, Y \sim \mathcal{N}(\bar{\beta}, \bar{V}_\beta), \qquad \bar{V}_\beta^{-1} = V_0^{-1} + \sum_t \Sigma_t^{-1} \otimes x_t x_t'\)$
using the full \((nk \times nk)\) precision-weighted GLS precision (\(k = 1 + np\)). This is exact but \(O((nk)^3)\) per sweep and is meant for small and medium systems; it is not the equation-by-equation triangular algorithm of Carriero, Clark and Marcellino (2019). Draws are accepted only if the companion matrix is stable (posterior truncated to the stationary region). After 50 consecutive rejections the previous \(B\) is kept; such sweeps are counted in
n_stuck_iterations(withn_unstable_rejectionscandidates rejected) and aUserWarningis emitted. - Step 2: Contemporaneous Triangular Coefficients (\(A\)) Each row \(i\) of \(A\) is sampled equation-by-equation (Cogley and Sargent 2005; Primiceri 2005) via a linear regression of the residual \(\hat{u}_{i,t}\) on the preceding residuals \(\hat{u}_{1:i-1,t}\), weighted by \(\exp(-h_{i,t}/2)\).
- Step 3: Kim-Shephard-Chib (1998) 7-Component Mixture Sampler Transforming squared standardized residuals yields a non-Gaussian linear measurement equation: $\(y_{i,t}^* \equiv \log\left( (A \hat{u}_t)_i^2 + c \right) = h_{i,t} + \log(\varepsilon_{i,t}^2)\)$ where \(\log(\varepsilon_{i,t}^2) \sim \log(\chi_1^2)\) is approximated by a 7-component mixture of normal distributions with parameters \((q_k, m_k, v_k^2)\) from Kim, Shephard, and Chib (1998, Table 4). The mixture indicator states \(s_{i,t} \in \{1, \dots, 7\}\) are sampled conditionally on \(h_{i,t}\).
- Step 4: Forward-Filtering Backward-Sampling (FFBS) for Volatility States (\(h_{i,t}\)) Conditional on the mixture indicators, the state-space is conditionally Gaussian. Carter and Kohn (1994) FFBS draws the full state sequence \(h_{i, 1:T}\) jointly in a single backward sweep.
- Step 5: Volatility Parameters \((\mu_i, \phi_i, \sigma_{h,i}^2)\) \(\mu_i\) is drawn from its conjugate normal conditional and \(\sigma_{h,i}^2\) from its conjugate inverse-gamma conditional. \(\phi_i\) uses the independence Metropolis-Hastings step of Kim, Shephard and Chib (1998, §3.3): the proposal is the Gaussian conditional posterior of the AR(1) regression on \(h_{i,2:T}\) and the acceptance probability is the ratio of stationary initial-state densities $\(\log \frac{p(h_{i,1} \mid \phi')}{p(h_{i,1} \mid \phi)} = \tfrac{1}{2}\log(1-\phi'^2) - \tfrac{1}{2}\log(1-\phi^2) + \tfrac{1}{2}(\phi'^2 - \phi^2)\frac{(h_{i,1}-\mu_i)^2}{\sigma_{h,i}^2}.\)$
- Step 6: Multi-Chain Gelman-Rubin Convergence Diagnostics (\(\hat{R}\))
n_chainschains are run (sequentially); chains after the first start from jittered initial values (log-volatility level, \(\phi_i\), \(\sigma_{h,i}^2\)). Each chain is split in two halves and the split-chain \(\hat{R}\) is computed for every VAR coefficient, every \(a_{i,j}\), the mean log-volatility of each variable and each \((\mu_i, \phi_i, \sigma_{h,i})\). Values of \(\hat{R} < 1.1\) indicate adequate mixing; \(\hat{R}\) is a stochastic diagnostic, so increasen_draws/n_burnwhen it fails.
3. Volatility-Conditioned Impulse Responses
Because the impact matrix \(B_t = A^{-1} D_t^{1/2}\) varies across time, structural shock propagation differs depending on the volatility regime.
BVAR_SVResult.irf(horizon=20, t_idx=..., ci=0.9) conditions the impulse responses on the volatility state at a specific date \(t^*\) (an index into the effective sample, i.e. observation \(t^* + p\)):
- High-Volatility Regime (e.g., 2008Q4 or 2020Q2): Evaluates impulse responses when financial or macroeconomic uncertainty is elevated.
- Tranquil Regime (e.g., mid-1990s Great Moderation): Evaluates transmission in quiet macroeconomic conditions.
The result is a BVAR_SV_IRF array of shape \((H+1, n, n)\) ([horizon, response, shock], Cholesky ordering) holding the posterior median, with .lower, .upper and the full .draws \((D, H+1, n, n)\). to_frame(target_idx=None, shock_idx=None, names=None) returns a tidy table; each index acts as an independent filter (pass one, both or neither).
4. Forecasts, Fan Charts and Predictive Scores
forecast(horizon, ci=0.9, seed=None)simulates one posterior-predictive path per retained draw: the log-volatilities are propagated with the AR(1) law of motion, structural shocks are drawn, and the VAR is iterated forward, so parameter, volatility and shock uncertainty are all integrated out. The returnedBVAR_SVForecastexposespaths\((D, H, n)\),h_paths,median,mean,lower,upper,quantile(q), a datedindex(when the data carry a regularDatetimeIndex),to_frame()andplot(levels=(0.5, 0.8, 0.95))fan charts.log_scores/predictive_log_score()is the in-sample log pointwise predictive density (lppd): \(\sum_t \log \frac{1}{D}\sum_d p(y_t \mid \beta^{(d)}, A^{(d)}, h_t^{(d)})\) evaluated with the smoothed volatility draws, which condition on the whole estimation sample. It is a fit measure, not a forecast evaluation.log_score(holdout)is the out-of-sample log predictive score of observations that follow the estimation sample: for every draw the conditional mean uses the realised lags of \(y_{T+j}\) and the volatility is projected from the end-of-sample state with \(h_{T+j} = \mu + \phi (h_{T+j-1} - \mu) + \sigma_h \eta\). Parameters are not re-estimated and the volatility is not re-filtered on hold-out data.log_score()without arguments returns the in-sample lppd.
5. Basic & Advanced Usage
Estimating a BVAR-SV Model
import numpy as np
import pandas as pd
from puremacro.var.bvar_sv import bvar_sv
# 1. Synthetic quarterly dataset (Output, Inflation, Rate) with a volatility spike
rng = np.random.default_rng(42)
T = 180
dates = pd.date_range("1975-01-01", periods=T, freq="QE")
data = np.zeros((T, 3))
for t in range(1, T):
vol_scale = 2.5 if 130 <= t <= 145 else 1.0 # Volatility spike
data[t] = 0.6 * data[t-1] + vol_scale * 0.5 * rng.standard_normal(3)
df = pd.DataFrame(data, index=dates, columns=["Output", "Inflation", "Rate"])
# Hold out the last 12 quarters for out-of-sample density evaluation
train, test = df.iloc[:168], df.iloc[168:]
# 2. Fit the BVAR-SV with the Gibbs sampler (n_draws is per chain: 2 x 2000 pooled draws)
res_sv = bvar_sv(
data=train,
lags=2,
n_draws=2000,
n_burn=1000,
n_chains=2,
minnesota_prior=True,
lambda1=0.2,
lambda2=0.5,
seed=123,
)
# 3. Inspect convergence diagnostics & posterior summary
print(res_sv.summary())
print(f"Max split-chain R-hat: {res_sv.max_rhat:.3f}")
assert res_sv.max_rhat < 1.1, "Chains must pass Gelman-Rubin convergence (< 1.1)"
# 4. Volatility-conditioned IRFs: crisis (t_idx=135) vs calm period (t_idx=50)
irf_crisis = res_sv.irf(horizon=16, t_idx=135, ci=0.90)
irf_calm = res_sv.irf(horizon=16, t_idx=50, ci=0.90)
print(f"Peak Output response (crisis): {irf_crisis.median[:, 0, 0].max():.3f}")
print(f"Peak Output response (calm) : {irf_calm.median[:, 0, 0].max():.3f}")
irf_table = irf_crisis.to_frame(shock_idx=0, names=res_sv.names) # all responses to the Output shock
# 5. Predictive densities: in-sample lppd vs out-of-sample hold-out score
print(f"In-sample lppd (smoothed volatilities): {res_sv.predictive_log_score():.2f}")
print(f"Out-of-sample log score, 12 held-out quarters: {res_sv.log_score(test, seed=0):.2f}")
# 6. Posterior-predictive forecasts with fan charts
fc = res_sv.forecast(horizon=12, ci=0.90, seed=1)
fan_fig = fc.plot(levels=(0.5, 0.8, 0.95))
coverage = np.mean((test.values >= fc.lower) & (test.values <= fc.upper))
print(f"Share of held-out observations inside the 90% predictive band: {coverage:.2f}")
# 7. Volatility paths, conditional SDs and IRF panels; export tables
fig = res_sv.plot(t_idx=135, shock_idx=0, target_idx=0)
latex_table = res_sv.to_latex()
md_table = res_sv.to_markdown()
6. Full API Specification
bvar_sv
bvar_sv(
data: pd.DataFrame | np.ndarray,
lags: int = 4,
n_draws: int = 2000,
n_burn: int = 1000,
minnesota_prior: bool = True,
seed: int | None = None,
*,
lambda1: float = 0.2,
lambda2: float = 0.5,
lambda3: float = 1.0,
intercept_prior_std: float = 1e3,
thin: int = 1,
n_chains: int = 2,
p: int | None = None,
) -> BVAR_SVResult
Parameters:
data: \((T, n)\) dataset of endogenous time series variables; must be finite (NaN / inf raiseValueError).lags/p: Autoregressive lag order \(p\) (default4). The effective sample \(T - p\) must exceed the number of regressors per equation \(k = 1 + np\).n_draws: Post-burn-in MCMC draws retained per chain (default2000, minimum2); the result poolsn_chains * n_drawsdraws.n_burn: Initial burn-in iterations discarded per chain (default1000).minnesota_prior: Whether to apply Minnesota prior shrinkage (defaultTrue); otherwise a diffuse \(\mathcal{N}(0, 100^2)\) prior on the lag coefficients.seed: Reproducibility seed for the MCMC sampler.lambda1: Overall Minnesota shrinkage hyperparameter (default0.2).lambda2: Cross-variable shrinkage hyperparameter (default0.5).lambda3: Lag-decay exponent (default1.0).intercept_prior_std: Prior standard deviation of the intercepts (default1e3), honoured in both prior modes.thin: Thinning interval (default1, minimum1): one draw is retained everythinsweeps untiln_drawsare collected.n_chains: Number of MCMC chains run sequentially for the split-chain Gelman-Rubin diagnostics (default2, minimum1).
A UserWarning is emitted when some sweep exhausted the stability retry budget (see Step 1).
7. Result Interface
The returned BVAR_SVResult container provides:
- Attributes (
D = n_chains * n_drawspooled draws): beta_draws: Posterior draws of VAR coefficients \((D, 1+np, n)\);A_draws\((D, p, n, n)\) andintercept_draws\((D, n)\) are derived views.h_draws: Posterior draws of latent log-volatility paths \((D, T_{eff}, n)\).a_draws: Posterior draws of the contemporaneous impact matrix \(A\) \((D, n, n)\).mu_draws,phi_draws,sigma_h_draws: Volatility AR(1) parameter draws \((D, n)\).r_hat/rhat: Dictionary of split-chain Gelman-Rubin statistics (beta_max,beta_mean,a_max,h_max,h_mean,h_<name>_mean,mu_<name>,phi_<name>,sigma_h_<name>,max).max_rhat: Maximum \(\hat{R}\) observed (r_hat['max']).log_scores: In-sample log pointwise predictive density per observation \((T_{eff},)\) (smoothed volatilities).n_draws,n_burn,n_chains,n_total_draws: Sampler settings and the pooled draw count.n_unstable_rejections,n_stuck_iterations: Stability-rejection accounting (Step 1).- Methods:
.irf(horizon=20, t_idx=-1, ci=0.9): ReturnsBVAR_SV_IRFwith.median,.lower,.upper,.drawsand.to_frame(target_idx=None, shock_idx=None, names=None)..forecast(horizon=8, ci=0.9, seed=None): ReturnsBVAR_SVForecastwith posterior-predictivepaths, bands,to_frame()and.plot()fan charts..log_score(holdout=None, point_by_point=False, seed=None): Out-of-sample log predictive score on a hold-out sample; withoutholdout, the in-sample lppd..predictive_log_score(point_by_point=False): In-sample lppd (total or pointwise)..gelman_rubin(): Ther_hatdictionary..summary(ci=0.9): Text report with sampler settings, convergence status, the in-sample lppd, stability-rejection counts (if any) and, for every variable, the posterior median, central credible interval and \(\hat{R}\) of \(\mu_i\), \(\phi_i\), \(\sigma_{h,i}\)..plot(t_idx=-1, horizon=20, ci=0.9, shock_idx=0, target_idx=None, ax=None): Three panels — log-volatility paths and conditional standard deviations with credible bands for every variable (or onlytarget_idx), and the volatility-conditioned IRF oftarget_idx(first variable by default) toshock_idx..to_frame(),.to_markdown(),.to_latex(),.to_typst(): Posterior mean / SD table of the volatility parameters per variable with the \(\hat{R}\) of its mean log-volatility.
Limitations
- The VAR coefficients are drawn jointly with an \((nk \times nk)\) Cholesky factorisation per sweep; very large systems (say \(nk > 300\)) will be slow. The CCM (2019) triangular equation-by-equation sampler is not implemented.
- The hold-out
log_scoreprojects volatility from the end of the estimation sample and does not re-estimate or re-filter on hold-out data; for a fully recursive evaluation, refit the model at each origin. - Hyper-priors of the volatility processes are fixed (Section 1.2) and not user-configurable.