Adding New Models¶
Subclass SamplingModel and implement sample() and sample_posterior_state().
Pass an instance to BVAR to use it for estimation and forecasting.
Required Contract¶
Keep the argument names and defaults shown in the example. BVAR passes these
inputs to sample():
| Arguments | Purpose |
|---|---|
data, n_lags |
Observation array and lag order |
covid_indices, vars_in_levels |
Outlier-dummy locations and level-variable flags |
N_draws, point_only, progressbar |
Draw count, point-estimate mode, progress display |
soc, sur |
Per-fit dummy-prior overrides; None uses the model setting |
rng |
NumPy random generator |
The posterior-update method receives Y, Z, current_state, and rng.
sample() owns prior construction and estimation. Cache the state needed to
update the posterior when conditional forecasting supplies new Y and Z.
Return a SamplingResult with these fields, where n is the number of variables,
k the regressors per equation, and nk = n * k:
| Field | Shape or value |
|---|---|
beta_draws |
(N_draws, nk) |
sigma_draws |
(N_draws, n**2) |
beta_point |
(nk,) |
sigma_point |
(n**2,) |
extras_point |
Auxiliary point state, or None |
extras_draws |
One auxiliary payload per draw, or None |
Flatten coefficients equation by equation and covariance matrices row by row.
SamplingResult rejects mismatched draw counts, including extras_draws.
Posterior state¶
sample_posterior_state() returns a fresh PosteriorState(beta, sigma, extras).
Direct samplers such as NaturalConjugate ignore current_state; MCMC samplers
use it for one Gibbs sweep. IndependentNIW starts from current_state.sigma
and samples new coefficients before updating the covariance.
Store auxiliary state in extras and carry it forward on each update.
See _ExtrasCarryingModel in tests/test_posterior_state.py for an example.
The conditional chain starts from a copy of BVAR.posterior_state_point.
Predictive hooks also receive copies. PosteriorState.copy() copies both arrays
and deep-copies extras; it raises TypeError for payloads it cannot copy.
Keep auxiliary payloads copyable and treat hook inputs as read-only.
Class attributes¶
| Attribute | Effect when True |
|---|---|
requires_burnin |
BVAR discards the first N_burn draws |
supports_ml |
Allows marginal-likelihood hyperparameter optimisation |
supports_point_only |
Allows point-only fits, required for cross-validation |
supports_gaussian_predictive |
Enables the default Gaussian predictive hooks |
supports_girf |
Allows GIRFs, provided Gaussian prediction is also supported |
The last two flags default to True. Set both to False for non-Gaussian models:
the current GIRF implementation requires a Gaussian predictive distribution.
Optional Predictive Hooks¶
Override these hooks to change the predictive distribution:
| Method | Called by | Default (Gaussian) behaviour |
|---|---|---|
sample_innovations |
Recursive and unconditional forecasts | Draws innovations from N(0, state.sigma) |
sample_conditional_forecast |
Conditional-forecast Gibbs loop | Calls draw_constrained_forecasts |
predictive_logpdf |
Cross-validation scoring | Evaluates the multivariate normal log-density |
Each hook receives the full PosteriorState, including extras.
Gaussian models can keep the defaults, which ignore extras.
With supports_gaussian_predictive=False, the defaults raise
NotImplementedError; override every hook your workflow uses.
Minimal Example¶
This direct sampler uses a diffuse, proper conjugate prior, not an improper flat prior. It omits Minnesota shrinkage and rejects SOC and SUR dummy observations. Run this definition and the usage block below in the same Python session.
import numpy as np
from typing import Optional
from scipy.stats import invwishart
from bvar.models import PosteriorState, SamplingModel, SamplingResult
from bvar.utils import construct_Y_Z, get_dimensions
class FlatPrior(SamplingModel):
"""BVAR with a near-flat but proper Bayesian prior."""
requires_burnin: bool = False
supports_ml: bool = False
supports_point_only: bool = True
def __init__(
self,
minnesota: bool = False,
soc: bool = False,
sur: bool = False,
covid: bool = False,
covid_dates: Optional[list] = None,
) -> None:
if soc or sur:
raise ValueError("FlatPrior does not support soc or sur")
if minnesota:
raise ValueError("FlatPrior does not support Minnesota shrinkage")
super().__init__(
minnesota=minnesota,
soc=False,
sur=False,
covid=covid,
covid_dates=covid_dates,
)
def sample(
self,
data,
n_lags,
covid_indices,
vars_in_levels,
N_draws,
point_only=False,
progressbar=True,
*,
soc=None,
sur=None,
rng=None,
) -> SamplingResult:
rng = rng if rng is not None else np.random.default_rng()
if soc or sur:
raise ValueError("FlatPrior does not support soc or sur")
_, n_vars, n_regressors, n_coefficients, _ = get_dimensions(
data, n_lags, covid_indices
)
if self.pars.nu_0 is None:
self.pars.nu_0 = n_vars + 2
if self.pars.S_0 is None:
self.pars.S_0 = np.eye(n_vars) * 1e-4
self.V_A_inv = np.eye(n_regressors) * 1e-6
Y, Z = construct_Y_Z(data, n_lags, covid_indices)
posterior = self._posterior(Y, Z)
mean, _, scale, degrees = posterior
beta_point = mean.T.flatten()
sigma_point = (scale / (degrees - n_vars - 1)).flatten()
beta_draws = np.empty((N_draws, n_coefficients))
sigma_draws = np.empty((N_draws, n_vars**2))
if point_only:
beta_draws[:] = beta_point
sigma_draws[:] = sigma_point
else:
for draw_index in range(N_draws):
state = self._draw_posterior(posterior, rng)
beta_draws[draw_index] = state.beta
sigma_draws[draw_index] = state.sigma
return SamplingResult(
beta_draws=beta_draws,
sigma_draws=sigma_draws,
beta_point=beta_point,
sigma_point=sigma_point,
)
def sample_posterior_state(self, Y, Z, current_state, rng=None) -> PosteriorState:
"""Draw independently of current_state using the updated data."""
rng = rng if rng is not None else np.random.default_rng()
return self._draw_posterior(self._posterior(Y, Z), rng)
def _posterior(self, Y, Z):
precision = self.V_A_inv + Z.T @ Z
chol = np.linalg.cholesky(precision)
mean = np.linalg.solve(chol.T, np.linalg.solve(chol, Z.T @ Y))
residuals = Y - Z @ mean
scale = self.pars.S_0 + residuals.T @ residuals + mean.T @ self.V_A_inv @ mean
return mean, chol, scale, self.pars.nu_0 + Y.shape[0]
def _draw_posterior(self, posterior, rng):
mean, chol, scale, degrees = posterior
sigma = np.atleast_2d(invwishart.rvs(degrees, scale, random_state=rng))
noise = rng.normal(size=mean.shape)
beta = mean + np.linalg.solve(chol.T, noise) @ np.linalg.cholesky(sigma).T
return PosteriorState(beta=beta.T.flatten(), sigma=sigma.flatten())
Wire it up¶
Pass FlatPrior() directly to BVAR; no registration is required.
To expose it as bv.FlatPrior, place the class in
src/bvar/models/flat_prior/model.py, export it from models/__init__.py,
and re-export it from bvar/__init__.py. These exports are optional.
Then use it like any other model:
import bvar as bv
import pandas as pd
data = pd.DataFrame(
np.random.default_rng(42).normal(size=(120, 2)),
index=pd.period_range("2000Q1", periods=120, freq="Q"),
columns=["output", "inflation"],
)
model = FlatPrior(soc=False, sur=False)
bvar = bv.BVAR(n_lags=2, model=model, stationary=False, optimisation_method="none")
bvar.sample(data, N_draws=2000, random_state=42, progressbar=False)
bvar.forecast(H=8, random_state=43, progressbar=False)
Customising the Hyperparameter Interface¶
For additional hyperparameters, override set_priors, _compute_nb_hyper_pars,
fill_in_from_vector, to_vector, and, if needed, hyperparameter_grid.
IndependentNIW shows how to add the cross-variable shrinkage parameter c2.
Checklist¶
- Preserve auxiliary state and override predictive hooks where needed.
- Test draws, point estimates, conditional forecasts, and seeded repeatability.
- Check that unsupported options raise clear errors.
Reproducibility¶
Use the supplied rng for every random draw. Seeds reproduce results within an
installed release, but sampler changes can alter them between releases.
Pin the package version when you need bit-for-bit reproducibility.