Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/impulso/conjugate.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ def fit(self, data: VARData) -> FittedVAR:
all with a singleton ``chain`` dimension. The posterior's attrs
carry `in_sample_length` (observations after lag trimming) so
volatility adapters can anchor forecast paths at the true sample
end.
end, and — only when at least one hyperparameter was estimated —
`metropolis_acceptance_rate`, the acceptance rate of the
random-walk Metropolis sampler over the retained draws. On the
fixed-prior fast path no Metropolis chain runs (draws come
straight from the closed-form posterior), so the attr is absent
rather than stamped with a meaningless 1.0.

Raises:
ValueError: If `data` carries exogenous regressors — the
Expand Down Expand Up @@ -128,6 +133,12 @@ def fit(self, data: VARData) -> FittedVAR:
# Volatility adapters anchor forecast scale paths at the true sample
# end (see ConjugateVolatility.forecast_cholesky_path).
posterior.attrs["in_sample_length"] = data.endog.shape[0] - self.lags
# Hyperparameter-sampler quality signal for convergence reporting. Only
# meaningful when the Metropolis chain actually ran: with no free
# hyperparameters `select_and_sample` takes the closed-form fast path and
# returns a placeholder rate of 1.0, so leave the attr off entirely there.
if result["hyperparameters"]:
posterior.attrs["metropolis_acceptance_rate"] = float(result["acceptance_rate"])
idata = az.InferenceData(posterior=posterior)
volatility = self.volatility if self.volatility is not None else Constant()

Expand Down
43 changes: 43 additions & 0 deletions tests/test_conjugate_var.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,49 @@ def test_pandemic_break_fit_forecast_and_irf():
assert np.isfinite(irf).all()


def test_selected_tightness_stamps_metropolis_acceptance_rate():
"""A fit with a free hyperparameter carries the Metropolis rate (issue #178)."""
data = _synthetic_var_data(60, seed=4)
model = ConjugateVAR(lags=1, prior=NIWPrior(select=True), draws=DRAWS, tune=DRAWS, seed=4)

attrs = model.fit(data).idata.posterior.attrs
rate = attrs["metropolis_acceptance_rate"]

assert isinstance(rate, float)
assert np.isfinite(rate)
assert 0.0 < rate <= 1.0


def test_volatility_break_stamps_metropolis_acceptance_rate():
"""A volatility break also frees hyperparameters, so the rate is stamped."""
start = 55
data = _synthetic_var_data(70, seed=5, spike_start_level=start + 1)
model = ConjugateVAR(
lags=1,
prior=NIWPrior(),
volatility=PandemicBreak(start=start),
draws=DRAWS,
tune=DRAWS,
seed=5,
)

rate = model.fit(data).idata.posterior.attrs["metropolis_acceptance_rate"]

assert isinstance(rate, float)
assert 0.0 < rate <= 1.0


def test_fixed_prior_fast_path_omits_metropolis_acceptance_rate():
"""No Metropolis chain runs on the fast path, so the attr is absent, not 1.0."""
data = _synthetic_var_data(60, seed=6)
model = ConjugateVAR(lags=1, prior=NIWPrior(select=False), draws=DRAWS, tune=DRAWS, seed=6)

attrs = model.fit(data).idata.posterior.attrs

assert "in_sample_length" in attrs # the other stamp still lands
assert "metropolis_acceptance_rate" not in attrs


def test_rejects_exog_bearing_data():
"""ConjugateVAR estimates endogenous dynamics only (issue #121)."""
rng = np.random.default_rng(3)
Expand Down
Loading