dfm-python 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dfm_python/__init__.py +60 -0
- dfm_python/config.py +482 -0
- dfm_python/data_loader.py +449 -0
- dfm_python/dfm.py +2366 -0
- dfm_python/kalman.py +455 -0
- dfm_python/news.py +732 -0
- dfm_python/utils/__init__.py +27 -0
- dfm_python/utils/aggregation.py +258 -0
- dfm_python/utils/data_utils.py +165 -0
- dfm_python-0.1.4.dist-info/METADATA +1010 -0
- dfm_python-0.1.4.dist-info/RECORD +13 -0
- dfm_python-0.1.4.dist-info/WHEEL +4 -0
- dfm_python-0.1.4.dist-info/licenses/LICENSE +21 -0
dfm_python/kalman.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""Kalman filter and fixed-interval smoother implementation."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.linalg import inv, pinv
|
|
5
|
+
from typing import Tuple, Dict
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
# Set up logger (optional - only if logging is enabled)
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
if not _logger.handlers:
|
|
12
|
+
# Only configure if logging is enabled (avoid overhead if disabled)
|
|
13
|
+
root_logger = logging.getLogger()
|
|
14
|
+
if root_logger.level <= logging.WARNING:
|
|
15
|
+
_logger.setLevel(logging.WARNING)
|
|
16
|
+
handler = logging.StreamHandler()
|
|
17
|
+
handler.setFormatter(logging.Formatter(
|
|
18
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
19
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
20
|
+
))
|
|
21
|
+
_logger.addHandler(handler)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class KalmanFilterState:
|
|
26
|
+
"""Kalman filter state structure.
|
|
27
|
+
|
|
28
|
+
This dataclass stores the complete state of the Kalman filter after forward
|
|
29
|
+
and backward passes, including prior/posterior estimates and covariances.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
Zm : np.ndarray
|
|
34
|
+
Prior (predicted) factor state estimates, shape (m x nobs).
|
|
35
|
+
Zm[:, t] is the predicted state at time t given observations up to t-1.
|
|
36
|
+
Vm : np.ndarray
|
|
37
|
+
Prior covariance matrices, shape (m x m x nobs).
|
|
38
|
+
Vm[:, :, t] is the covariance of Zm[:, t].
|
|
39
|
+
ZmU : np.ndarray
|
|
40
|
+
Posterior (updated) factor state estimates, shape (m x (nobs+1)).
|
|
41
|
+
ZmU[:, t] is the updated state at time t given observations up to t.
|
|
42
|
+
Includes initial state at t=0.
|
|
43
|
+
VmU : np.ndarray
|
|
44
|
+
Posterior covariance matrices, shape (m x m x (nobs+1)).
|
|
45
|
+
VmU[:, :, t] is the covariance of ZmU[:, t].
|
|
46
|
+
loglik : float
|
|
47
|
+
Log-likelihood of the data under the current model parameters.
|
|
48
|
+
Computed as sum of log-likelihoods at each time step.
|
|
49
|
+
k_t : np.ndarray
|
|
50
|
+
Kalman gain matrix, shape (m x k) where k is number of observed series.
|
|
51
|
+
Used to update state estimates with new observations.
|
|
52
|
+
"""
|
|
53
|
+
Zm: np.ndarray # Prior/predicted factor state (m x nobs)
|
|
54
|
+
Vm: np.ndarray # Prior covariance (m x m x nobs)
|
|
55
|
+
ZmU: np.ndarray # Posterior/updated state (m x (nobs+1))
|
|
56
|
+
VmU: np.ndarray # Posterior covariance (m x m x (nobs+1))
|
|
57
|
+
loglik: float # Log-likelihood
|
|
58
|
+
k_t: np.ndarray # Kalman gain
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def miss_data(y: np.ndarray, C: np.ndarray, R: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
62
|
+
"""Handle missing data by removing NaN observations from the Kalman filter equations.
|
|
63
|
+
|
|
64
|
+
This function implements the standard approach to missing data in Kalman filtering:
|
|
65
|
+
observations with NaN values are removed from the observation vector, observation
|
|
66
|
+
matrix, and covariance matrix. A selection matrix L is returned to restore standard
|
|
67
|
+
dimensions if needed.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
y : np.ndarray
|
|
72
|
+
Vector of observations at time t, shape (k,) where k is number of series.
|
|
73
|
+
Missing values should be NaN.
|
|
74
|
+
C : np.ndarray
|
|
75
|
+
Observation/loading matrix, shape (k x m) where m is state dimension.
|
|
76
|
+
Each row corresponds to a series in y.
|
|
77
|
+
R : np.ndarray
|
|
78
|
+
Covariance matrix for observation residuals, shape (k x k).
|
|
79
|
+
Typically diagonal (idiosyncratic variances).
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
y_clean : np.ndarray
|
|
84
|
+
Reduced observation vector with NaN values removed, shape (k_obs,)
|
|
85
|
+
where k_obs is number of non-missing observations.
|
|
86
|
+
C_clean : np.ndarray
|
|
87
|
+
Reduced observation matrix, shape (k_obs x m).
|
|
88
|
+
Rows corresponding to missing observations are removed.
|
|
89
|
+
R_clean : np.ndarray
|
|
90
|
+
Reduced covariance matrix, shape (k_obs x k_obs).
|
|
91
|
+
Rows and columns corresponding to missing observations are removed.
|
|
92
|
+
L : np.ndarray
|
|
93
|
+
Selection matrix, shape (k x k_obs), used to restore standard dimensions.
|
|
94
|
+
L @ y_clean gives y with zeros for missing values.
|
|
95
|
+
|
|
96
|
+
Notes
|
|
97
|
+
-----
|
|
98
|
+
This function is called at each time step in the Kalman filter to handle
|
|
99
|
+
missing observations. The selection matrix L allows reconstruction of the
|
|
100
|
+
full-dimensional vectors if needed for downstream processing.
|
|
101
|
+
|
|
102
|
+
Examples
|
|
103
|
+
--------
|
|
104
|
+
>>> y = np.array([1.0, np.nan, 3.0])
|
|
105
|
+
>>> C = np.array([[1, 0], [0, 1], [1, 1]])
|
|
106
|
+
>>> R = np.eye(3)
|
|
107
|
+
>>> y_clean, C_clean, R_clean, L = miss_data(y, C, R)
|
|
108
|
+
>>> # y_clean = [1.0, 3.0], C_clean has 2 rows, R_clean is 2x2
|
|
109
|
+
"""
|
|
110
|
+
# Returns True for nonmissing series
|
|
111
|
+
ix = ~np.isnan(y)
|
|
112
|
+
|
|
113
|
+
# Index for columns with nonmissing variables
|
|
114
|
+
e = np.eye(len(y))
|
|
115
|
+
L = e[:, ix]
|
|
116
|
+
|
|
117
|
+
# Remove missing series
|
|
118
|
+
y = y[ix]
|
|
119
|
+
|
|
120
|
+
# Remove missing series from observation matrix
|
|
121
|
+
C = C[ix, :]
|
|
122
|
+
|
|
123
|
+
# Remove missing series from covariance matrix
|
|
124
|
+
R = R[np.ix_(ix, ix)]
|
|
125
|
+
|
|
126
|
+
return y, C, R, L
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def skf(Y: np.ndarray, A: np.ndarray, C: np.ndarray, Q: np.ndarray,
|
|
130
|
+
R: np.ndarray, Z_0: np.ndarray, V_0: np.ndarray) -> KalmanFilterState:
|
|
131
|
+
"""Apply Kalman filter (forward pass).
|
|
132
|
+
|
|
133
|
+
Parameters:
|
|
134
|
+
-----------
|
|
135
|
+
Y : np.ndarray
|
|
136
|
+
Input data (k x nobs), where k = number of series, nobs = time periods
|
|
137
|
+
A : np.ndarray
|
|
138
|
+
Transition matrix (m x m)
|
|
139
|
+
C : np.ndarray
|
|
140
|
+
Observation matrix (k x m)
|
|
141
|
+
Q : np.ndarray
|
|
142
|
+
Covariance for transition equation residuals (m x m)
|
|
143
|
+
R : np.ndarray
|
|
144
|
+
Covariance for observation matrix residuals (k x k)
|
|
145
|
+
Z_0 : np.ndarray
|
|
146
|
+
Initial state vector (m,)
|
|
147
|
+
V_0 : np.ndarray
|
|
148
|
+
Initial state covariance matrix (m x m)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
--------
|
|
152
|
+
KalmanFilterState
|
|
153
|
+
Filter state with prior and posterior estimates
|
|
154
|
+
"""
|
|
155
|
+
# Dimensions
|
|
156
|
+
k, nobs = Y.shape # k series, nobs time periods
|
|
157
|
+
m = C.shape[1] # m factors
|
|
158
|
+
|
|
159
|
+
# Initialize output
|
|
160
|
+
Zm = np.full((m, nobs), np.nan) # Z_t | t-1 (prior)
|
|
161
|
+
Vm = np.full((m, m, nobs), np.nan) # V_t | t-1 (prior)
|
|
162
|
+
ZmU = np.full((m, nobs + 1), np.nan) # Z_t | t (posterior/updated)
|
|
163
|
+
VmU = np.full((m, m, nobs + 1), np.nan) # V_t | t (posterior/updated)
|
|
164
|
+
loglik = 0.0
|
|
165
|
+
|
|
166
|
+
# Set initial values
|
|
167
|
+
Zu = Z_0.copy() # Z_0|0 (In loop, Zu gives Z_t | t)
|
|
168
|
+
Vu = V_0.copy() # V_0|0 (In loop, Vu gives V_t | t)
|
|
169
|
+
|
|
170
|
+
# Store initial values
|
|
171
|
+
ZmU[:, 0] = Zu
|
|
172
|
+
VmU[:, :, 0] = Vu
|
|
173
|
+
|
|
174
|
+
# Kalman filter procedure
|
|
175
|
+
for t in range(nobs):
|
|
176
|
+
# Calculate prior distribution
|
|
177
|
+
# Use transition equation to create prior estimate for factor
|
|
178
|
+
# i.e. Z = Z_t|t-1
|
|
179
|
+
# Check for NaN/Inf in inputs
|
|
180
|
+
if np.any(np.isnan(Zu)) or np.any(np.isinf(Zu)):
|
|
181
|
+
_logger.warning(f"skf: Zu contains NaN/Inf at t={t}, resetting to zeros")
|
|
182
|
+
Zu = np.zeros_like(Zu)
|
|
183
|
+
|
|
184
|
+
Z = A @ Zu
|
|
185
|
+
|
|
186
|
+
# Check for NaN/Inf in Z
|
|
187
|
+
if np.any(np.isnan(Z)) or np.any(np.isinf(Z)):
|
|
188
|
+
_logger.warning(f"skf: Z contains NaN/Inf at t={t}, using previous Zu")
|
|
189
|
+
Z = Zu.copy()
|
|
190
|
+
|
|
191
|
+
# Prior covariance matrix of Z (i.e. V = V_t|t-1)
|
|
192
|
+
# Var(Z) = Var(A*Z + u_t) = Var(A*Z) + Var(u) = A*Vu*A' + Q
|
|
193
|
+
V = A @ Vu @ A.T + Q
|
|
194
|
+
V = 0.5 * (V + V.T) # Trick to make symmetric
|
|
195
|
+
|
|
196
|
+
# Numerical stability: ensure V is positive semi-definite
|
|
197
|
+
# Add small regularization if needed
|
|
198
|
+
try:
|
|
199
|
+
eigenvals = np.linalg.eigvals(V)
|
|
200
|
+
min_eigenval = np.min(eigenvals)
|
|
201
|
+
if min_eigenval < 1e-8:
|
|
202
|
+
# Add regularization to ensure positive semi-definiteness
|
|
203
|
+
V = V + np.eye(V.shape[0]) * (1e-8 - min_eigenval)
|
|
204
|
+
V = 0.5 * (V + V.T) # Re-symmetrize
|
|
205
|
+
except (np.linalg.LinAlgError, ValueError):
|
|
206
|
+
# Eigendecomposition failed - add small regularization
|
|
207
|
+
V = V + np.eye(V.shape[0]) * 1e-8
|
|
208
|
+
V = 0.5 * (V + V.T)
|
|
209
|
+
|
|
210
|
+
# Check for NaN/Inf
|
|
211
|
+
if np.any(np.isnan(V)) or np.any(np.isinf(V)):
|
|
212
|
+
# Fallback: use previous covariance with regularization
|
|
213
|
+
V = Vu + np.eye(V.shape[0]) * 1e-6
|
|
214
|
+
V = 0.5 * (V + V.T)
|
|
215
|
+
|
|
216
|
+
# Calculate posterior distribution
|
|
217
|
+
# Remove missing series: These are removed from Y, C, and R
|
|
218
|
+
Y_t, C_t, R_t, _ = miss_data(Y[:, t], C, R)
|
|
219
|
+
|
|
220
|
+
# Check if y_t contains no data
|
|
221
|
+
if len(Y_t) == 0:
|
|
222
|
+
Zu = Z
|
|
223
|
+
Vu = V
|
|
224
|
+
else:
|
|
225
|
+
# Steps for variance and population regression coefficients:
|
|
226
|
+
# Var(c_t*Z_t + e_t) = c_t Var(Z) c_t' + Var(e) = c_t*V*c_t' + R
|
|
227
|
+
VC = V @ C_t.T
|
|
228
|
+
|
|
229
|
+
# Compute innovation covariance F = C_t @ V @ C_t.T + R_t
|
|
230
|
+
F = C_t @ VC + R_t
|
|
231
|
+
F = 0.5 * (F + F.T) # Ensure symmetry
|
|
232
|
+
|
|
233
|
+
# Numerical stability: regularize F if needed
|
|
234
|
+
try:
|
|
235
|
+
F_eigenvals = np.linalg.eigvals(F)
|
|
236
|
+
min_F_eigenval = np.min(F_eigenvals)
|
|
237
|
+
if min_F_eigenval < 1e-8:
|
|
238
|
+
F = F + np.eye(F.shape[0]) * (1e-8 - min_F_eigenval)
|
|
239
|
+
F = 0.5 * (F + F.T)
|
|
240
|
+
except (np.linalg.LinAlgError, ValueError):
|
|
241
|
+
F = F + np.eye(F.shape[0]) * 1e-8
|
|
242
|
+
F = 0.5 * (F + F.T)
|
|
243
|
+
|
|
244
|
+
# Check for NaN/Inf before inversion
|
|
245
|
+
if np.any(np.isnan(F)) or np.any(np.isinf(F)):
|
|
246
|
+
# Fallback: use identity with large variance
|
|
247
|
+
F = np.eye(F.shape[0]) * 1e6
|
|
248
|
+
_logger.warning(f"skf: F matrix contains NaN/Inf at t={t}, using fallback")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
iF = inv(F)
|
|
252
|
+
except (np.linalg.LinAlgError, ValueError) as e:
|
|
253
|
+
# Matrix inversion failed - use pseudo-inverse with regularization
|
|
254
|
+
F_reg = F + np.eye(F.shape[0]) * 1e-6
|
|
255
|
+
iF = pinv(F_reg)
|
|
256
|
+
_logger.warning(f"skf: F inversion failed at t={t}, using pinv: {type(e).__name__}")
|
|
257
|
+
|
|
258
|
+
# Matrix of population regression coefficients (Kalman gain)
|
|
259
|
+
VCF = VC @ iF
|
|
260
|
+
|
|
261
|
+
# Difference between actual and predicted observation matrix values
|
|
262
|
+
innov = Y_t - C_t @ Z
|
|
263
|
+
|
|
264
|
+
# Check for NaN/Inf in innovation
|
|
265
|
+
if np.any(np.isnan(innov)) or np.any(np.isinf(innov)):
|
|
266
|
+
_logger.warning(f"skf: Innovation contains NaN/Inf at t={t}, skipping update")
|
|
267
|
+
Zu = Z
|
|
268
|
+
Vu = V
|
|
269
|
+
else:
|
|
270
|
+
# Update estimate of factor values (posterior)
|
|
271
|
+
Zu = Z + VCF @ innov
|
|
272
|
+
|
|
273
|
+
# Clean NaN/Inf only (remove excessive clipping during iterations)
|
|
274
|
+
if np.any(~np.isfinite(Zu)):
|
|
275
|
+
Zu = np.nan_to_num(Zu, nan=0.0, posinf=0.0, neginf=0.0)
|
|
276
|
+
|
|
277
|
+
# Update covariance matrix (posterior) for time t
|
|
278
|
+
Vu = V - VCF @ VC.T
|
|
279
|
+
Vu = 0.5 * (Vu + Vu.T) # Approximation trick to make symmetric
|
|
280
|
+
|
|
281
|
+
# Clean NaN/Inf only (keep regularization for PSD)
|
|
282
|
+
if np.any(~np.isfinite(Vu)):
|
|
283
|
+
Vu = np.nan_to_num(Vu, nan=1e-8, posinf=1e6, neginf=1e-8)
|
|
284
|
+
|
|
285
|
+
# Ensure Vu is positive semi-definite
|
|
286
|
+
try:
|
|
287
|
+
Vu_eigenvals = np.linalg.eigvals(Vu)
|
|
288
|
+
min_Vu_eigenval = np.min(Vu_eigenvals)
|
|
289
|
+
if min_Vu_eigenval < 1e-8:
|
|
290
|
+
Vu = Vu + np.eye(Vu.shape[0]) * (1e-8 - min_Vu_eigenval)
|
|
291
|
+
Vu = 0.5 * (Vu + Vu.T)
|
|
292
|
+
except (np.linalg.LinAlgError, ValueError):
|
|
293
|
+
Vu = Vu + np.eye(Vu.shape[0]) * 1e-8
|
|
294
|
+
Vu = 0.5 * (Vu + Vu.T)
|
|
295
|
+
|
|
296
|
+
# Check for NaN/Inf in Vu
|
|
297
|
+
if np.any(np.isnan(Vu)) or np.any(np.isinf(Vu)):
|
|
298
|
+
_logger.warning(f"skf: Vu contains NaN/Inf at t={t}, using V as fallback")
|
|
299
|
+
Vu = V.copy()
|
|
300
|
+
|
|
301
|
+
# Update log-likelihood (with safeguards)
|
|
302
|
+
try:
|
|
303
|
+
det_iF = np.linalg.det(iF)
|
|
304
|
+
if det_iF > 0 and np.isfinite(det_iF):
|
|
305
|
+
log_det = np.log(det_iF)
|
|
306
|
+
innov_term = innov.T @ iF @ innov
|
|
307
|
+
if np.isfinite(innov_term):
|
|
308
|
+
loglik += 0.5 * (log_det - innov_term)
|
|
309
|
+
else:
|
|
310
|
+
_logger.debug(f"skf: innov_term not finite at t={t}, skipping loglik update")
|
|
311
|
+
else:
|
|
312
|
+
_logger.debug(f"skf: det(iF) <= 0 or not finite at t={t}, skipping loglik update")
|
|
313
|
+
except (np.linalg.LinAlgError, ValueError, OverflowError):
|
|
314
|
+
_logger.debug(f"skf: Log-likelihood calculation failed at t={t}")
|
|
315
|
+
|
|
316
|
+
# Store output
|
|
317
|
+
# Store covariance and observation values for t (priors)
|
|
318
|
+
Zm[:, t] = Z
|
|
319
|
+
Vm[:, :, t] = V
|
|
320
|
+
|
|
321
|
+
# Store covariance and state values for t (posteriors)
|
|
322
|
+
# i.e. Zu = Z_t|t & Vu = V_t|t
|
|
323
|
+
ZmU[:, t + 1] = Zu
|
|
324
|
+
VmU[:, :, t + 1] = Vu
|
|
325
|
+
|
|
326
|
+
# Store Kalman gain k_t (from final iteration)
|
|
327
|
+
if len(Y_t) == 0:
|
|
328
|
+
k_t = np.zeros((m, m))
|
|
329
|
+
else:
|
|
330
|
+
k_t = VCF @ C_t
|
|
331
|
+
|
|
332
|
+
return KalmanFilterState(Zm=Zm, Vm=Vm, ZmU=ZmU, VmU=VmU, loglik=loglik, k_t=k_t)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def fis(A: np.ndarray, S: KalmanFilterState) -> KalmanFilterState:
|
|
336
|
+
"""Apply fixed-interval smoother (backward pass).
|
|
337
|
+
|
|
338
|
+
Parameters:
|
|
339
|
+
-----------
|
|
340
|
+
A : np.ndarray
|
|
341
|
+
Transition matrix (m x m)
|
|
342
|
+
S : KalmanFilterState
|
|
343
|
+
State from Kalman filter (SKF)
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
--------
|
|
347
|
+
KalmanFilterState
|
|
348
|
+
State with smoothed estimates added (ZmT, VmT, VmT_1)
|
|
349
|
+
"""
|
|
350
|
+
m, nobs = S.Zm.shape
|
|
351
|
+
|
|
352
|
+
# Initialize output matrices
|
|
353
|
+
ZmT = np.zeros((m, nobs + 1))
|
|
354
|
+
VmT = np.zeros((m, m, nobs + 1))
|
|
355
|
+
|
|
356
|
+
# Fill the final period of ZmT, VmT with SKF posterior values
|
|
357
|
+
ZmT[:, nobs] = S.ZmU[:, nobs]
|
|
358
|
+
VmT[:, :, nobs] = S.VmU[:, :, nobs]
|
|
359
|
+
|
|
360
|
+
# Initialize VmT_1 lag 1 covariance matrix for final period
|
|
361
|
+
VmT_1 = np.zeros((m, m, nobs))
|
|
362
|
+
VmT_1[:, :, nobs - 1] = (np.eye(m) - S.k_t) @ A @ S.VmU[:, :, nobs - 1]
|
|
363
|
+
|
|
364
|
+
# Used for recursion process
|
|
365
|
+
J_2 = S.VmU[:, :, nobs - 1] @ A.T @ pinv(S.Vm[:, :, nobs - 1])
|
|
366
|
+
|
|
367
|
+
# Run smoothing algorithm
|
|
368
|
+
# Loop through time reverse-chronologically (starting at final period nobs-1)
|
|
369
|
+
for t in range(nobs - 1, -1, -1):
|
|
370
|
+
# Store posterior and prior factor covariance values
|
|
371
|
+
VmU = S.VmU[:, :, t]
|
|
372
|
+
Vm1 = S.Vm[:, :, t]
|
|
373
|
+
|
|
374
|
+
# Store previous period smoothed factor covariance and lag-1 covariance
|
|
375
|
+
V_T = VmT[:, :, t + 1]
|
|
376
|
+
V_T1 = VmT_1[:, :, t] if t < nobs - 1 else np.zeros((m, m))
|
|
377
|
+
|
|
378
|
+
J_1 = J_2
|
|
379
|
+
|
|
380
|
+
# Update smoothed factor estimate
|
|
381
|
+
ZmT[:, t] = S.ZmU[:, t] + J_1 @ (ZmT[:, t + 1] - A @ S.ZmU[:, t])
|
|
382
|
+
|
|
383
|
+
# Clean NaN/Inf only (remove excessive clipping)
|
|
384
|
+
if np.any(~np.isfinite(ZmT[:, t])):
|
|
385
|
+
ZmT[:, t] = np.nan_to_num(ZmT[:, t], nan=0.0, posinf=0.0, neginf=0.0)
|
|
386
|
+
|
|
387
|
+
# Update smoothed factor covariance matrix
|
|
388
|
+
VmT[:, :, t] = VmU + J_1 @ (V_T - Vm1) @ J_1.T
|
|
389
|
+
VmT[:, :, t] = 0.5 * (VmT[:, :, t] + VmT[:, :, t].T) # Ensure symmetry
|
|
390
|
+
|
|
391
|
+
# Clean NaN/Inf and ensure PSD (keep only critical regularization)
|
|
392
|
+
if np.any(~np.isfinite(VmT[:, :, t])):
|
|
393
|
+
VmT[:, :, t] = np.nan_to_num(VmT[:, :, t], nan=1e-8, posinf=1e6, neginf=1e-8)
|
|
394
|
+
|
|
395
|
+
if t > 0:
|
|
396
|
+
# Update weight
|
|
397
|
+
J_2 = S.VmU[:, :, t - 1] @ A.T @ pinv(S.Vm[:, :, t - 1])
|
|
398
|
+
|
|
399
|
+
# Update lag 1 factor covariance matrix
|
|
400
|
+
VmT_1[:, :, t - 1] = VmU @ J_2.T + J_1 @ (V_T1 - A @ VmU) @ J_2.T
|
|
401
|
+
|
|
402
|
+
# Add smoothed estimates as attributes
|
|
403
|
+
S.ZmT = ZmT
|
|
404
|
+
S.VmT = VmT
|
|
405
|
+
S.VmT_1 = VmT_1
|
|
406
|
+
|
|
407
|
+
return S
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def run_kf(Y: np.ndarray, A: np.ndarray, C: np.ndarray, Q: np.ndarray,
|
|
411
|
+
R: np.ndarray, Z_0: np.ndarray, V_0: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float]:
|
|
412
|
+
"""Apply Kalman filter and fixed-interval smoother.
|
|
413
|
+
|
|
414
|
+
Parameters:
|
|
415
|
+
-----------
|
|
416
|
+
Y : np.ndarray
|
|
417
|
+
Input data (k x nobs)
|
|
418
|
+
A : np.ndarray
|
|
419
|
+
Transition matrix (m x m)
|
|
420
|
+
C : np.ndarray
|
|
421
|
+
Observation matrix (k x m)
|
|
422
|
+
Q : np.ndarray
|
|
423
|
+
Covariance for transition residuals (m x m)
|
|
424
|
+
R : np.ndarray
|
|
425
|
+
Covariance for observation residuals (k x k)
|
|
426
|
+
Z_0 : np.ndarray
|
|
427
|
+
Initial state (m,)
|
|
428
|
+
V_0 : np.ndarray
|
|
429
|
+
Initial covariance (m x m)
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
--------
|
|
433
|
+
zsmooth : np.ndarray
|
|
434
|
+
Smoothed factor estimates (m x (nobs+1)), zsmooth[:, t+1] = Z_t|T
|
|
435
|
+
Vsmooth : np.ndarray
|
|
436
|
+
Smoothed factor covariance (m x m x (nobs+1)), Vsmooth[:, :, t+1] = Cov(Z_t|T)
|
|
437
|
+
VVsmooth : np.ndarray
|
|
438
|
+
Lag 1 factor covariance (m x m x nobs), Cov(Z_t, Z_t-1|T)
|
|
439
|
+
loglik : float
|
|
440
|
+
Log-likelihood
|
|
441
|
+
"""
|
|
442
|
+
# Kalman filter
|
|
443
|
+
S = skf(Y, A, C, Q, R, Z_0, V_0)
|
|
444
|
+
|
|
445
|
+
# Fixed-interval smoother
|
|
446
|
+
S = fis(A, S)
|
|
447
|
+
|
|
448
|
+
# Organize output
|
|
449
|
+
zsmooth = S.ZmT
|
|
450
|
+
Vsmooth = S.VmT
|
|
451
|
+
VVsmooth = S.VmT_1
|
|
452
|
+
loglik = S.loglik
|
|
453
|
+
|
|
454
|
+
return zsmooth, Vsmooth, VVsmooth, loglik
|
|
455
|
+
|