cultivars.multivariate.reduced_form.functional#
The functional vector autoregression: dynamics for observations that are curves.
A yield curve, a demographic profile, an intraday volatility signature – each observation is a function evaluated on a grid, and modelling every grid point as its own variable buys nothing but a singular covariance. The functional VAR factors the problem instead: write each curve as a mean plus coordinates in a small basis,
run an ordinary VAR on the score vector \(\xi_t\), and map everything the VAR produces – fitted values, forecasts, impulse responses – back through the basis matrix \(\Phi = [\phi_j(s_k)]\) to curve space. This is the Diebold-Li dynamic Nelson-Siegel idea in general form: the operator autoregression of Bosq, truncated to a finite basis so that it becomes a VAR the rest of the package already knows how to estimate, test and propagate.
Two commitments shape the surface. First, the design is composition rather
than inheritance, and deliberately so. The factor dynamics are a
VAR, with
every coefficient table, diagnostic, Granger test and decomposition that
implies, and the fitted factor result rides on the functional result as
.factors rather than being re-wrapped method by method. What this module
adds is exactly the part that is functional – the projection, its quality,
and the maps between curve space and score space – and it names the floor
that the projection puts under every curve-level number: no fitted or
forecast curve can beat the reconstruction error of the basis itself.
Second, the basis is the modelling decision and is left to the caller.
Functional principal components are estimated from the sample and are
optimal in mean square for it; Nelson-Siegel fixes the three-factor
level-slope-curvature structure the term-structure literature runs on, with
the decay either fixed or profiled; B-splines are the local, shape-agnostic
choice when neither a data-driven nor a yield-curve basis fits the problem.
The three share one estimation path and one result type, so a comparison
across bases is a comparison of reconstruction floors and factor fits, not of
machinery.
Layout. FunctionalVAR validates the panel through
_validate_curves and the grid in place, builds the basis in
_fpca_basis (a thin SVD of the centered panel), _nelson_siegel_basis
(_nelson_siegel_loadings from _core._polynomials at a fixed or
profiled decay) or _bspline_basis (a clamped scipy.interpolate.BSpline
design at quantile knots), projects the curves with _projection_scores
from _core, and fits a
VAR to
the scores. FunctionalVARResult takes its summary from
_SummaryMixin and otherwise holds the basis, the mean curve, the scores
and the factor result, with the curve-level maps as thin products with
\(\Phi\). The dedicated yield-curve models that fix the Nelson-Siegel
structure and estimate the decay jointly are in
term_structure; the factor
models that estimate loadings from a wide panel rather than a grid are in
large_dim.
References
Bosq, D. (2000). Linear Processes in Function Spaces. Springer.
Diebold, F. X., & Li, C. (2006). Forecasting the term structure of government bond yields. Journal of Econometrics, 130(2), 337-364.
Ramsay, J. O., & Silverman, B. W. (2005). Functional Data Analysis. Springer.
Kokoszka, P., & Reimherr, M. (2017). Introduction to Functional Data Analysis. Chapman and Hall/CRC.
Example
Yield curves on ten maturities generated from level, slope and curvature factors at decay 0.6. The Nelson-Siegel basis profiles the decay back to the grid candidate nearest 0.6 and names the factors; three principal components reach the same reconstruction floor without the names. A level shock moves every maturity by one at impact, and the factor VAR’s own surface answers the factor-level questions:
>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> maturities = np.array([0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0])
>>> x = 0.6 * maturities
>>> slope, curvature = (1 - np.exp(-x)) / x, (1 - np.exp(-x)) / x - np.exp(-x)
>>> loadings = np.column_stack([np.ones(10), slope, curvature])
>>> factors = np.zeros((240, 3))
>>> for t in range(1, 240):
... factors[t] = [0.1, -0.15, 0.1] + [0.98, 0.9, 0.8] * factors[t - 1]
... factors[t] = factors[t] + rng.standard_normal(3) * [0.2, 0.3, 0.4]
>>> yields = factors @ loadings.T + 0.02 * rng.standard_normal((240, 10))
>>> ns = FunctionalVAR(yields, maturities, order=1, basis="nelson-siegel").fit()
>>> pc = FunctionalVAR(yields, maturities, order=1, basis="fpca", n_components=3).fit()
>>> round(ns.decay, 3), ns.factors.names
(0.597, ('level', 'slope', 'curvature'))
>>> bool(abs(ns.reconstruction_rmse - pc.reconstruction_rmse) < 0.001)
True
>>> bool(np.allclose(ns.irf_curves(0, orthogonalized=False)[0, :, 0], 1.0))
True
>>> ns.factors.coefficients[0].diagonal().round(1)
array([1. , 0.8, 0.8])
Classes#
A vector autoregression for observations that are curves on a grid. |
|
A fitted functional vector autoregression. |