conditional_forecast#

cultivars.forecast.conditional.conditional_forecast(result, steps, conditions, *, n_draws=1000, seed=None)[source]#

Forecast a closed system given stated future values of some of its variables.

The Waggoner-Zha (1999) construction on whichever kind of result is passed. For a point result the unconditional mean path \(\mu\) is the result’s own forecast(steps), the moving-average matrices \(\Psi_0, \dots, \Psi_{H-1}\) its ma_representation, and the impact matrix \(P\) the lower Cholesky factor of sigma_u; the conditions become the restriction system

\[R \varepsilon = r, \qquad r_i = c_{h_i, j_i} - \mu_{h_i, j_i},\]

one row per conditioned cell, with \(R\) built from the rows of \(\Psi_j P\) that reach that cell, and n_draws shock paths are drawn from \(\mathcal{N}(\varepsilon^\ast, I - R^\top (R R^\top)^{-1} R)\) and propagated through the moving average. For a sampled result the same construction is repeated once per parameter draw: n_draws draws are picked evenly over the retained posterior, each supplies its own \(\mu\), \(\Psi\), and \(P\) from beta_draws and sigma_draws, and contributes one conditional path, so the paths mix parameter and shock uncertainty.

Parameters:
  • result (object) – A fitted closed system. A point result must expose forecast(steps), ma_representation(horizon), sigma_u and names (every VAR-family result does); the conditional paths then carry shock uncertainty at the fitted parameters. A result carrying beta_draws and sigma_draws – the Bayesian VAR family – is propagated draw by draw, and the paths carry parameter uncertainty too.

  • steps (int) – Horizons ahead.

  • conditions (Mapping[str, Sequence[float | None]]) – Variable name to its conditioned path, one entry per horizon from the first, None (or nan) where the variable is free; a path shorter than steps leaves the remaining horizons free.

  • n_draws (int) – Conditional paths to simulate. Under a sampled result they are spread evenly over the retained parameter draws.

  • seed (int | Generator | None) – Seed or generator.

Return type:

ConditionalForecastResult

Returns:

The ConditionalForecastResult.

Raises:
  • SpecificationError – If the horizon or draw count is not positive, a variable is unknown, nothing is conditioned, or the result cannot be propagated.

  • DimensionError – If a condition path is longer than the horizon.

  • NumericalError – If the conditions are linearly dependent or the covariance degenerates.

Note

The result is read by duck typing so that a system from outside the package can be conditioned on: names is required in every case, and then either the sampled trio beta_draws, sigma_draws, _stack_of (with endog, order, trend) or the point trio forecast, ma_representation, sigma_u. A forecast that returns the sampled families’ (steps, k, 3) low/mean/high summary is read at its middle column. Conditions are hard, so a scenario that pins every variable at every horizon has no free shock directions and every path is the same; the modesty reading is then the only content. Under a sampled result the modesty norm is the average of \(\|\varepsilon^\ast\|^2\) over parameter draws, and its p-value is read against \(\chi^2_q\) as if it were a single draw’s norm.

See also

  • ConditionalForecastResult – the record, with the modesty reading and the paths.

  • VARResult – a point result whose forecast, ma_representation, and sigma_u are what the point branch reads.

  • fan_chart() – the bands the paths give.

References

Waggoner, D. F., & Zha, T. (1999). Conditional forecasts in dynamic multivariate models. Review of Economics and Statistics, 81(4), 639-651.

Leeper, E. M., & Zha, T. (2003). Modest policy interventions. Journal of Monetary Economics, 50(8), 1673-1700.

Example

A point VAR with one variable pinned for two horizons, then a Bayesian VAR whose paths also mix over parameter draws:

>>> import numpy as np
>>> from cultivars.multivariate.reduced_form.vector_autoregression import VAR
>>> from cultivars.multivariate.large_dim.bayesian import BVAR
>>> y = np.random.default_rng(0).standard_normal((120, 2))
>>> res = VAR(y, order=1).fit()
>>> scenario = conditional_forecast(res, 3, {"y1": [0.5, 0.5]}, n_draws=200, seed=0)
>>> scenario.n_conditions, scenario.paths.shape, scenario.parameter_uncertainty
(2, (200, 3, 2), False)
>>> bool(np.allclose(scenario.paths[:, :2, 0], 0.5))
True
>>> posterior = BVAR(y, order=1).fit(n_draws=200, seed=0)
>>> sampled = conditional_forecast(posterior, 3, {"y1": [0.5, 0.5]}, n_draws=50, seed=0)
>>> sampled.source, sampled.parameter_uncertainty, sampled.n_draws
('BVARResult', True, 50)

A short path leaves later horizons free, and None or nan frees a cell in the middle:

>>> mixed = conditional_forecast(
...     res, 3, {"y1": [0.5, None], "y2": [float("nan"), 0.2]}, n_draws=20, seed=0
... )
>>> mixed.conditions
array([[0.5, nan],
       [nan, 0.2],
       [nan, nan]])
>>> conditional_forecast(res, 3, {"y1": [None, None]})
Traceback (most recent call last):
    ...
cultivars.exceptions.SpecificationError: every stated condition is None or nan; ...