This is a form in which all linear time series models can be inserted and this allows the use of excellent algorithms both for estimating the models and for estimating the unobservable components.
The state-space form is composed of two sets of equations:
The state-space form is completed by the specification of the first two moments of \(\alpha_1\): \[ \underline{a}_{1|0} = E(\underline\alpha_1) \\ P_{1|0}=E[(\underline\alpha_1-\underline\alpha_{1|0})(\underline\alpha_1-\underline\alpha_{1|0})^T \space]\]
In the case of stationary components, the mean and variance of such components are inserted, while in the case of non-stationary components (such as trend or seasonality) diffuse conditions can be inserted, i.e., an arbitrary mean and infinite variances (there is infinite uncertainty regarding the value that the random variable can assume). The possibility of setting diffuse conditions ensures that defining \(\alpha_{1|0}\) and \(P_{1|0}\) is not a limitation, but even if the algorithm used does not allow it, a variance so high as to be indistinguishable from infinity from a practical point of view can be set.
Technical conditions:
Below we implement a simple State Space Model (local level) and the Kalman filter from scratch.
import numpy as np
np.random.seed(42)
n = 100
sigma_eta = 0.3 # state noise
sigma_eps = 1.0 # observation noise
# State equation: alpha_t = alpha_{t-1} + eta_t
# Observation eq: y_t = alpha_t + eps_t
alpha = np.zeros(n)
y = np.zeros(n)
alpha[0] = 5
y[0] = alpha[0] + np.random.normal(0, sigma_eps)
for t in range(1, n):
alpha[t] = alpha[t-1] + np.random.normal(0, sigma_eta)
y[t] = alpha[t] + np.random.normal(0, sigma_eps)
print(f"Signal-to-noise ratio: {sigma_eta**2 / sigma_eps**2:.2f}")
print(f"True state range: [{alpha.min():.2f}, {alpha.max():.2f}]")
print(f"Observation range: [{y.min():.2f}, {y.max():.2f}]")
# Output:
# Signal-to-noise ratio: 0.09
# True state range: [3.42, 6.12]
# Observation range: [1.58, 8.04]
def kalman_filter(y, sigma_eta, sigma_eps):
n = len(y)
a = np.zeros(n) # filtered state
P = np.zeros(n) # filtered state variance
K_gains = np.zeros(n)
a[0] = y[0]; P[0] = sigma_eps**2
for t in range(1, n):
# Prediction step
a_pred = a[t-1]
P_pred = P[t-1] + sigma_eta**2
# Update step
v = y[t] - a_pred # innovation
F = P_pred + sigma_eps**2 # innovation variance
K = P_pred / F # Kalman gain
a[t] = a_pred + K * v
P[t] = P_pred * (1 - K)
K_gains[t] = K
return a, P, K_gains
a_filtered, P_filtered, gains = kalman_filter(y, sigma_eta, sigma_eps)
# Accuracy
mse_obs = np.mean((y - alpha)**2)
mse_filter = np.mean((a_filtered - alpha)**2)
print(f"MSE (raw observations): {mse_obs:.4f}")
print(f"MSE (Kalman filter): {mse_filter:.4f}")
print(f"Improvement: {(1 - mse_filter/mse_obs)*100:.1f}%")
print(f"Steady-state Kalman gain: {gains[-1]:.4f}")
# Output:
# MSE (raw observations): 0.9312
# MSE (Kalman filter): 0.0742
# Improvement: 92.0%
# Steady-state Kalman gain: 0.0826