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,

\[y_t = \mu_t + \psi_t + \gamma_t + \varepsilon_t, \qquad \varepsilon_t \sim N(0, \sigma^2_\varepsilon),\]

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#

UnobservedComponents

Structural time-series decomposition by exact maximum likelihood.

UnobservedComponentsResult

A fitted structural decomposition: components, variances and bands.