cultivars.forecast.fan#

Fan-chart extraction: quantile paths, and nothing else.

A fan chart is quantiles of the predictive paths at each horizon – a data product, not a figure. For a predictive sample of \(S\) paths, each band edge is the pointwise empirical quantile

\[q_{\tau}(h, j) = \hat F^{-1}_{h, j}(\tau),\]

one per horizon \(h\), series \(j\), and level \(\tau\), and a fan is the stack of those edges at a chosen set of levels: the median, a 68% band, a 90% band, whatever the reader is meant to see. The package draws nothing; fan_chart() hands the arrays to whatever plots them, with the quantile convention stated by the caller rather than baked in.

Two commitments shape the surface. First, the bands are pointwise. A 90% band at horizon \(h\) contains 90% of the draws at that horizon; it does not say that 90% of whole paths lie inside the fan at every horizon, which is a narrower joint statement no fan chart makes. Second, the input is the raw path array and not a result. Any (n_draws, steps, k) block enters – the forecast_paths of a sampled result, the paths of a conditional forecast, a model combination’s pooled draws – so the fan is computed the same way whichever forecaster produced the paths.

Layout. fan_chart() is the whole module; it validates the shape and the levels and defers the arithmetic to numpy.quantile. There is no record type because the quantile array is the product.

References

Britton, E., Fisher, P., & Whitley, J. (1998). The Inflation Report projections: Understanding the fan chart. Bank of England Quarterly Bulletin, 38(1), 30-37.

Example

The default fan of a BVAR’s posterior predictive, and a custom set of levels:

>>> import numpy as np
>>> from cultivars.multivariate.large_dim.bayesian import BVAR
>>> y = np.random.default_rng(0).standard_normal((120, 2))
>>> paths = BVAR(y, order=1).fit(n_draws=200, seed=0).forecast_paths(8, seed=0)
>>> fan_chart(paths).shape
(5, 8, 2)
>>> median, upper = fan_chart(paths, quantiles=(0.5, 0.9))
>>> bool(np.all(upper > median))
True