cultivars.univariate.unobserved_components#
Structural time series: decomposition with a likelihood.
Harvey’s unobserved-components family writes a series as the sum of interpretable pieces, each with its own law of motion and its own innovation,
with the trend \(\mu_t\) one of three specifications in Harvey’s nomenclature – the local level \(\mu_{t+1} = \mu_t + \eta_t\), the local linear trend \(\mu_{t+1} = \mu_t + \beta_t + \eta_t\), \(\beta_{t+1} = \beta_t + \zeta_t\), and the smooth trend that sets \(\eta_t \equiv 0\) so only the slope moves and the level is an integrated random walk – the cycle \(\psi_t\) a damped bivariate rotation with decay \(\rho\) and frequency \(\lambda_c\), whose period \(2\pi / \lambda_c\) is estimated rather than imposed, and the seasonal \(\gamma_t\) the stochastic trigonometric form, all harmonics sharing one innovation variance. The pieces are estimated jointly by exact maximum likelihood through the Kalman filter and reported by the fixed-interval smoother. That is the difference between this and an ad-hoc filter: the components come with a likelihood, information criteria and full-sample uncertainty bands, and the end-of-sample estimates degrade honestly – their variances grow – instead of silently, which is where the Hodrick-Prescott filter is at its worst. The connection is exact: the smooth trend’s smoothed level is the HP trend at \(\lambda = \sigma^2_\varepsilon / \sigma^2_\zeta\), so this module estimates the penalty the filter asks the user to assert.
Two commitments shape the surface. First, one initialization statement
rather than a hidden constant: the nonstationary states – trend and
seasonal – start from an approximate-diffuse prior with variance
1e6, the cycle from its exact stationary covariance. The likelihood
is exact Gaussian given that prior, the summary says so, and the
consequence is stated on the result: llf is comparable across
specifications on one sample, but differs from a strict-diffuse
likelihood by a constant in the number of diffuse states. Second, every
component is a smoothed path with a band, never a point estimate alone,
and the record carries nothing it cannot rebuild – the system matrices
are re-derived from the public variances on demand, so state_space,
forecast and simulate cannot drift from the reported fit.
Layout. UnobservedComponents validates trend, cycle and
seasonal on _UnobservedComponentsModel in _internals, whose
_fit_structural hands a _StructuralObjective – log variances and
sigmoid-mapped cycle shape, so the surface is unconstrained – to
_maximize_likelihood under L-BFGS-B from data-scaled starts, then
smooths through _LinearGaussianStateSpace._from_structural_system
and packs a _StructuralFit. The system matrices and the state layout
come from _structural_matrices in _internals._systems; the
parameter record is _StructuralParameters.
UnobservedComponentsResult rebuilds that record from its public
fields, exposes the fitted system as a
LinearGaussianSSM, and
takes the summary, the aligned series and the comparison machinery from
_SummaryMixin, _SeriesMixin and _ComparisonMixin. The
constant-variance assumption is relaxed in
UCSV; the
penalized smoothers this family subsumes are in
filters.
References
Harvey, A. C. (1989). Forecasting, Structural Time Series Models and the Kalman Filter. Cambridge University Press.
Clark, P. K. (1987). The cyclical component of U.S. economic activity. Quarterly Journal of Economics, 102(4), 797-814.
Harvey, A. C., & Jaeger, A. (1993). Detrending, stylized facts and the business cycle. Journal of Applied Econometrics, 8(3), 231-247.
Durbin, J., & Koopman, S. J. (2012). Time Series Analysis by State Space Methods (2nd ed.). Oxford University Press.
Example
The smooth trend estimates the Hodrick-Prescott penalty instead of assuming it, and reproduces the HP trend at that penalty:
>>> import numpy as np
>>> from cultivars.spectral.filters import HodrickPrescottFilter
>>> rng = np.random.default_rng(7)
>>> n = 300
>>> slope = np.cumsum(0.01 * rng.standard_normal(n))
>>> y = np.cumsum(slope) + 0.5 * rng.standard_normal(n)
>>> res = UnobservedComponents(y, trend="smooth").fit()
>>> penalty = res.sigma2_irregular / res.sigma2_slope
>>> bool(1000 < penalty < 2500)
True
>>> hp = HodrickPrescottFilter(penalty=penalty).filter(y).trend[:, 0]
>>> bool(np.max(np.abs(hp - res.level)) < 1e-6)
True
>>> bool(res.level_std[0] > 1.5 * res.level_std[n // 2])
True
Classes#
Structural time-series decomposition by exact maximum likelihood. |
|
A fitted structural decomposition: components, variances and bands. |