cultivars.state_space#

State-space substrates: linear-Gaussian, nonlinear, and regime-switching.

Every model in the package that filters a latent state – an unobserved components model, a mixed-frequency VAR, a term-structure model, a switching autoregression, a stochastic volatility model – is built on one of the four engines here, and the engines share one grammar: construct a fully specified system, hand it data to filter, smooth, or loglikelihood, and read the result. None of them estimates anything. A linear-Gaussian system

\[y_t = Z_t \alpha_t + d_t + \varepsilon_t,\qquad \alpha_{t+1} = T_t \alpha_t + c_t + R_t \eta_t,\]

gets the Kalman filter, the Durbin-Koopman smoother and simulation smoother, and an exact likelihood by the prediction-error decomposition. A discrete \(K\)-state chain over an autoregression gets the Hamilton filter and Kim smoother, also exact. A linear-Gaussian system whose matrices switch with the chain gets Kim’s (1994) collapse and a likelihood labelled as the approximation it is. A system with arbitrary transition and measurement maps gets the extended, unscented and particle filters, each with its matching smoother, chosen by name because an extended likelihood, an unscented one and a particle estimate are three different kinds of number.

The package has one rule that every module keeps: the engine owns the recursions and nothing else. Parameterization, estimation, the mapping from a model’s parameters to system matrices, and the interpretation of a state all belong to the model that uses the engine, which is why a fitted result elsewhere in the package can expose its engine (an MSAR result’s state_space) and why the same instance can be handed a series it was not fitted on. The other half of the rule is that every likelihood says what it is: the linear and discrete engines return exact Gaussian likelihoods, the switching-linear engine an approximation named in its docstring, the particle filter an unbiased Monte Carlo estimate whose seed-to-seed spread is its error bar.

Each module is a leaf: names are imported from the module, not from this package, and each public name is an alias of the _internals class that implements it. The three engines with a single filter sign the shared _StateSpace[F, S] contract – k_endog, k_states, filter, smooth, loglikelihood – generic in their result types so that a regime-probability filter and a state-covariance filter satisfy it without inheriting fields they cannot fill; the nonlinear engine deliberately does not, because it has three filters, not one.

Layout. linear_gaussian is LinearGaussianSSM with KalmanFilterResult and DurbinKoopmanSmootherResult. regime_switching is MarkovSwitchingSSM (Hamilton filter, exact) and RegimeSwitchingLinearSSM (Kim’s collapse), with HamiltonFilterResult, KimFilterResult and KimSmootherResult. nonlinear is NonlinearSSM with RtsSmootherResult, ParticleFilterResult and ParticleSmootherResult; the extended and unscented filters return the linear engine’s KalmanFilterResult because they carry the same moments. The models that consume these engines live where their subject matter does: unobserved_components, regime_switching, stochastic_volatility, mixed_frequency, term_structure and markov_switching.

Example

One AR(1)-plus-noise system, four ways. The nonlinear engine’s unscented filter and the switching engine with two identical regimes reproduce the Kalman likelihood exactly, and the particle filter lands within its Monte Carlo error of it:

>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> state = np.zeros(200)
>>> for t in range(1, 200):
...     state[t] = 0.8 * state[t - 1] + np.sqrt(0.5) * rng.standard_normal()
>>> y = state + rng.standard_normal(200)
>>> linear = linear_gaussian.LinearGaussianSSM(
...     design=[[1.0]], obs_cov=[[1.0]], transition=[[0.8]],
...     selection=[[1.0]], state_cov=[[0.5]],
... )
>>> exact = linear.loglikelihood(y)
>>> round(exact, 2)
-344.14
>>> smooth = nonlinear.NonlinearSSM(
...     lambda s: 0.8 * s, lambda s: s, state_cov=[[0.5]], obs_cov=[[1.0]],
...     initial_state=[0.0], initial_state_cov=[[0.5 / 0.36]],
... )
>>> bool(abs(smooth.unscented_filter(y).loglikelihood - exact) < 1e-8)
True
>>> chain = np.array([[0.9, 0.1], [0.1, 0.9]])
>>> switching = regime_switching.RegimeSwitchingLinearSSM(
...     design=np.ones((2, 1, 1)), obs_cov=np.ones((2, 1, 1)),
...     transition=np.full((2, 1, 1), 0.8), state_cov=np.full((2, 1, 1), 0.5),
...     regime_transition=chain, initial_state_cov=[[0.5 / 0.36]],
... )
>>> bool(abs(switching.loglikelihood(y) - exact) < 1e-8)
True
>>> estimate = smooth.particle_filter(y, n_particles=2000, seed=0).loglikelihood
>>> bool(abs(estimate - exact) < 2.0)
True

Modules#

linear_gaussian

Linear-Gaussian state-space model with Kalman filter and smoother.

nonlinear

Nonlinear and non-Gaussian state-space models: extended, unscented, and particle methods.

regime_switching

Discrete Markov-switching inference: the Hamilton filter, the Kim smoother, and Kim's collapse.