dfm-python 0.2.0__py3-none-any.whl → 0.2.2__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 +2 -2
- dfm_python/{dfm_api.py → api.py} +41 -251
- dfm_python/config.py +72 -7
- dfm_python/core/__init__.py +59 -0
- dfm_python/core/diagnostics.py +274 -0
- dfm_python/core/em.py +1022 -0
- dfm_python/core/grouping.py +54 -0
- dfm_python/core/helpers.py +56 -0
- dfm_python/core/numeric.py +346 -0
- dfm_python/core/results.py +70 -0
- dfm_python/dfm.py +497 -3538
- dfm_python/kalman.py +18 -0
- dfm_python/utils/data_utils.py +3 -5
- dfm_python-0.2.2.dist-info/METADATA +312 -0
- dfm_python-0.2.2.dist-info/RECORD +21 -0
- dfm_python-0.2.0.dist-info/METADATA +0 -647
- dfm_python-0.2.0.dist-info/RECORD +0 -14
- {dfm_python-0.2.0.dist-info → dfm_python-0.2.2.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.0.dist-info → dfm_python-0.2.2.dist-info}/licenses/LICENSE +0 -0
dfm_python/core/em.py
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
"""Expectation-Maximization core routines (initialization and one EM iteration).
|
|
2
|
+
|
|
3
|
+
This module contains:
|
|
4
|
+
- init_conditions: compute initial A, C, Q, R, Z_0, V_0
|
|
5
|
+
- em_step: perform one EM iteration (E-step via Kalman, M-step updates)
|
|
6
|
+
- em_converged: check EM convergence
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional, Tuple, Dict, Any, TypedDict
|
|
10
|
+
import logging
|
|
11
|
+
import numpy as np
|
|
12
|
+
from scipy.linalg import inv, pinv, block_diag
|
|
13
|
+
|
|
14
|
+
# Common exception types for numerical operations
|
|
15
|
+
_NUMERICAL_EXCEPTIONS = (
|
|
16
|
+
np.linalg.LinAlgError,
|
|
17
|
+
ValueError,
|
|
18
|
+
ZeroDivisionError,
|
|
19
|
+
OverflowError,
|
|
20
|
+
FloatingPointError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ..kalman import run_kf
|
|
24
|
+
from ..config import DFMConfig
|
|
25
|
+
from .numeric import (
|
|
26
|
+
_ensure_symmetric,
|
|
27
|
+
_ensure_square_matrix,
|
|
28
|
+
_compute_principal_components,
|
|
29
|
+
_clean_matrix,
|
|
30
|
+
_ensure_positive_definite,
|
|
31
|
+
_compute_regularization_param,
|
|
32
|
+
_apply_ar_clipping,
|
|
33
|
+
_cap_max_eigenvalue,
|
|
34
|
+
_estimate_ar_coefficient,
|
|
35
|
+
_safe_divide,
|
|
36
|
+
_check_finite,
|
|
37
|
+
)
|
|
38
|
+
from .grouping import group_series_by_frequency
|
|
39
|
+
from ..utils.data_utils import rem_nans_spline
|
|
40
|
+
from ..utils.aggregation import (
|
|
41
|
+
FREQUENCY_HIERARCHY,
|
|
42
|
+
generate_R_mat,
|
|
43
|
+
get_tent_weights_for_pair,
|
|
44
|
+
generate_tent_weights,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NaNHandlingOptions(TypedDict, total=False):
|
|
51
|
+
"""Options for handling missing data (NaN values).
|
|
52
|
+
|
|
53
|
+
Attributes
|
|
54
|
+
----------
|
|
55
|
+
method : int
|
|
56
|
+
Method for handling NaNs:
|
|
57
|
+
- 1: Spline interpolation (recommended)
|
|
58
|
+
- 2: Forward fill then backward fill
|
|
59
|
+
- 3: Mean imputation
|
|
60
|
+
k : int
|
|
61
|
+
Spline interpolation order (only used if method=1).
|
|
62
|
+
Typically 3 (cubic spline).
|
|
63
|
+
"""
|
|
64
|
+
method: int
|
|
65
|
+
k: int
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_config_param(config: Optional[DFMConfig], param_name: str, default: Any) -> Any:
|
|
69
|
+
"""Get configuration parameter with fallback to default.
|
|
70
|
+
|
|
71
|
+
This helper function provides a clean way to access config parameters
|
|
72
|
+
that may not exist, with type-safe defaults.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
config : DFMConfig, optional
|
|
77
|
+
Configuration object
|
|
78
|
+
param_name : str
|
|
79
|
+
Name of the parameter to retrieve
|
|
80
|
+
default : Any
|
|
81
|
+
Default value if parameter doesn't exist
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
Any
|
|
86
|
+
Parameter value or default
|
|
87
|
+
"""
|
|
88
|
+
if config is None:
|
|
89
|
+
return default
|
|
90
|
+
return getattr(config, param_name, default)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def em_converged(loglik: float, previous_loglik: float, threshold: float = 1e-4,
|
|
94
|
+
check_decreased: bool = True) -> Tuple[bool, bool]:
|
|
95
|
+
"""Check whether EM algorithm has converged.
|
|
96
|
+
|
|
97
|
+
Returns (converged, decreased).
|
|
98
|
+
"""
|
|
99
|
+
converged = False
|
|
100
|
+
decrease = False
|
|
101
|
+
|
|
102
|
+
if check_decreased and (loglik - previous_loglik) < -1e-3:
|
|
103
|
+
_logger.warning(f"Likelihood decreased from {previous_loglik:.4f} to {loglik:.4f}")
|
|
104
|
+
decrease = True
|
|
105
|
+
|
|
106
|
+
delta_loglik = abs(loglik - previous_loglik)
|
|
107
|
+
avg_loglik = (abs(loglik) + abs(previous_loglik) + np.finfo(float).eps) / 2
|
|
108
|
+
if (delta_loglik / avg_loglik) < threshold:
|
|
109
|
+
converged = True
|
|
110
|
+
return converged, decrease
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def init_conditions(
|
|
114
|
+
x: np.ndarray,
|
|
115
|
+
r: np.ndarray,
|
|
116
|
+
p: int,
|
|
117
|
+
blocks: np.ndarray,
|
|
118
|
+
opt_nan: NaNHandlingOptions,
|
|
119
|
+
Rcon: Optional[np.ndarray],
|
|
120
|
+
q: Optional[np.ndarray],
|
|
121
|
+
nQ: Optional[int],
|
|
122
|
+
i_idio: np.ndarray,
|
|
123
|
+
clock: str = 'm',
|
|
124
|
+
tent_weights_dict: Optional[Dict[str, np.ndarray]] = None,
|
|
125
|
+
frequencies: Optional[np.ndarray] = None
|
|
126
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
127
|
+
"""Compute initial parameter estimates for DFM via PCA and OLS.
|
|
128
|
+
|
|
129
|
+
This function computes initial values for the DFM parameters:
|
|
130
|
+
- A: Transition matrix (via AR regression on factors)
|
|
131
|
+
- C: Loading matrix (via PCA on data residuals)
|
|
132
|
+
- Q: Innovation covariance (via residual variance)
|
|
133
|
+
- R: Observation covariance (via idiosyncratic variance)
|
|
134
|
+
- Z_0: Initial state (via unconditional mean)
|
|
135
|
+
- V_0: Initial covariance (via stationary covariance)
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
x : np.ndarray
|
|
140
|
+
Standardized data matrix (T x N)
|
|
141
|
+
r : np.ndarray
|
|
142
|
+
Number of factors per block (n_blocks,)
|
|
143
|
+
p : int
|
|
144
|
+
AR lag order (typically 1)
|
|
145
|
+
blocks : np.ndarray
|
|
146
|
+
Block structure (N x n_blocks)
|
|
147
|
+
opt_nan : NaNHandlingOptions
|
|
148
|
+
Options for NaN handling with 'method' (int) and 'k' (int) keys.
|
|
149
|
+
See NaNHandlingOptions for details.
|
|
150
|
+
Rcon : np.ndarray, optional
|
|
151
|
+
Constraint matrix for tent kernel aggregation
|
|
152
|
+
q : np.ndarray, optional
|
|
153
|
+
Constraint vector for tent kernel aggregation
|
|
154
|
+
nQ : int, optional
|
|
155
|
+
Number of slower-frequency series
|
|
156
|
+
i_idio : np.ndarray
|
|
157
|
+
Indicator array (1 for clock frequency, 0 for slower)
|
|
158
|
+
clock : str
|
|
159
|
+
Clock frequency ('m', 'q', 'sa', 'a')
|
|
160
|
+
tent_weights_dict : dict, optional
|
|
161
|
+
Dictionary mapping frequency pairs to tent weights
|
|
162
|
+
frequencies : np.ndarray, optional
|
|
163
|
+
Array of frequencies for each series
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
A : np.ndarray
|
|
168
|
+
Initial transition matrix (m x m)
|
|
169
|
+
C : np.ndarray
|
|
170
|
+
Initial loading matrix (N x m)
|
|
171
|
+
Q : np.ndarray
|
|
172
|
+
Initial innovation covariance (m x m)
|
|
173
|
+
R : np.ndarray
|
|
174
|
+
Initial observation covariance (N x N)
|
|
175
|
+
Z_0 : np.ndarray
|
|
176
|
+
Initial state vector (m,)
|
|
177
|
+
V_0 : np.ndarray
|
|
178
|
+
Initial covariance matrix (m x m)
|
|
179
|
+
"""
|
|
180
|
+
# Determine pC (tent length)
|
|
181
|
+
if Rcon is None or q is None:
|
|
182
|
+
pC = 1
|
|
183
|
+
else:
|
|
184
|
+
pC = Rcon.shape[1]
|
|
185
|
+
ppC = int(max(p, pC))
|
|
186
|
+
n_blocks = blocks.shape[1]
|
|
187
|
+
|
|
188
|
+
# Balance NaNs
|
|
189
|
+
xBal, _ = rem_nans_spline(x, method=opt_nan['method'], k=opt_nan['k'])
|
|
190
|
+
T, N = xBal.shape
|
|
191
|
+
|
|
192
|
+
# Determine pC from tent weights if provided
|
|
193
|
+
pC = 1
|
|
194
|
+
if tent_weights_dict:
|
|
195
|
+
for tent_weights in tent_weights_dict.values():
|
|
196
|
+
if tent_weights is not None and len(tent_weights) > pC:
|
|
197
|
+
pC = len(tent_weights)
|
|
198
|
+
elif Rcon is not None:
|
|
199
|
+
pC = Rcon.shape[1]
|
|
200
|
+
|
|
201
|
+
# Infer nQ from frequencies if needed
|
|
202
|
+
if nQ is None and frequencies is not None:
|
|
203
|
+
clock_h = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
204
|
+
nQ = sum(1 for f in frequencies if FREQUENCY_HIERARCHY.get(f, 3) > clock_h)
|
|
205
|
+
elif nQ is None:
|
|
206
|
+
nQ = 0
|
|
207
|
+
|
|
208
|
+
indNaN = np.isnan(xBal)
|
|
209
|
+
xNaN = xBal.copy()
|
|
210
|
+
xNaN[indNaN] = np.nan
|
|
211
|
+
data_residuals = xBal
|
|
212
|
+
resNaN = xNaN.copy()
|
|
213
|
+
|
|
214
|
+
C = None
|
|
215
|
+
A = None
|
|
216
|
+
Q = None
|
|
217
|
+
V_0 = None
|
|
218
|
+
|
|
219
|
+
if pC > 1:
|
|
220
|
+
indNaN[:pC - 1, :] = True
|
|
221
|
+
|
|
222
|
+
for i in range(n_blocks):
|
|
223
|
+
r_i = int(r[i])
|
|
224
|
+
factor_projection_lagged = None
|
|
225
|
+
ar_coeffs = None
|
|
226
|
+
F = None # Initialize F at block level to avoid UnboundLocalError
|
|
227
|
+
|
|
228
|
+
block_loadings = np.zeros((N, int(r_i * ppC)))
|
|
229
|
+
idx_i = np.where(blocks[:, i] == 1)[0]
|
|
230
|
+
|
|
231
|
+
freq_groups = group_series_by_frequency(idx_i, frequencies, clock)
|
|
232
|
+
idx_clock_freq = freq_groups.get(clock, np.array([], dtype=int))
|
|
233
|
+
|
|
234
|
+
if len(idx_clock_freq) > 0:
|
|
235
|
+
try:
|
|
236
|
+
res_block = data_residuals[:, idx_clock_freq].copy()
|
|
237
|
+
finite_rows = np.all(np.isfinite(res_block), axis=1)
|
|
238
|
+
n_finite = int(np.sum(finite_rows))
|
|
239
|
+
n_total = len(finite_rows)
|
|
240
|
+
if n_finite < max(2, len(idx_clock_freq) + 1):
|
|
241
|
+
_logger.warning(
|
|
242
|
+
f"init_conditions: Block {i+1} has insufficient data; using identity covariance."
|
|
243
|
+
)
|
|
244
|
+
raise ValueError("insufficient data")
|
|
245
|
+
res_block_clean = res_block[finite_rows, :]
|
|
246
|
+
if res_block_clean.size == 0:
|
|
247
|
+
cov_res = np.eye(len(idx_clock_freq))
|
|
248
|
+
elif res_block_clean.ndim == 1:
|
|
249
|
+
var_val = np.var(res_block_clean, ddof=0)
|
|
250
|
+
var_val = 1.0 if (np.isnan(var_val) or np.isinf(var_val) or var_val < 1e-10) else var_val
|
|
251
|
+
cov_res = np.array([[var_val]])
|
|
252
|
+
elif res_block_clean.ndim == 2:
|
|
253
|
+
if len(idx_clock_freq) == 1 or res_block_clean.shape[1] == 1:
|
|
254
|
+
series_data = res_block_clean.flatten()
|
|
255
|
+
var_val = np.var(series_data, ddof=0)
|
|
256
|
+
var_val = 1.0 if (np.isnan(var_val) or np.isinf(var_val) or var_val < 1e-10) else var_val
|
|
257
|
+
cov_res = np.array([[var_val]])
|
|
258
|
+
else:
|
|
259
|
+
cov_res = np.cov(res_block_clean.T) if res_block_clean.shape[0] >= 2 else np.eye(len(idx_clock_freq))
|
|
260
|
+
else:
|
|
261
|
+
cov_res = np.eye(len(idx_clock_freq))
|
|
262
|
+
if np.any(~np.isfinite(cov_res)):
|
|
263
|
+
cov_res = np.eye(len(idx_clock_freq))
|
|
264
|
+
d, v = _compute_principal_components(cov_res, int(r_i), block_idx=i)
|
|
265
|
+
if np.sum(v) < 0:
|
|
266
|
+
v = -v
|
|
267
|
+
d_pos = np.maximum(d, 1e-8)
|
|
268
|
+
sqrt_d = np.sqrt(d_pos)
|
|
269
|
+
v_scaled = v * float(sqrt_d) if np.isscalar(sqrt_d) or (isinstance(sqrt_d, np.ndarray) and sqrt_d.ndim == 0) else v @ np.diag(sqrt_d)
|
|
270
|
+
if v_scaled.ndim == 2 and v_scaled.shape[1] > 0:
|
|
271
|
+
for col_idx in range(v_scaled.shape[1]):
|
|
272
|
+
col_norm = np.linalg.norm(v_scaled[:, col_idx])
|
|
273
|
+
if col_norm > 0:
|
|
274
|
+
d_val = float(d_pos) if np.isscalar(d_pos) or (isinstance(d_pos, np.ndarray) and d_pos.ndim == 0) else d_pos[col_idx]
|
|
275
|
+
v_scaled[:, col_idx] = v_scaled[:, col_idx] / col_norm * np.sqrt(d_val)
|
|
276
|
+
block_loadings[idx_clock_freq, :int(r_i)] = v_scaled
|
|
277
|
+
f = data_residuals[:, idx_clock_freq] @ v_scaled
|
|
278
|
+
F = None
|
|
279
|
+
max_lag = max(p + 1, pC)
|
|
280
|
+
for kk in range(max_lag):
|
|
281
|
+
lag_data = f[pC - kk:T - kk, :]
|
|
282
|
+
F = lag_data if F is None else np.hstack([F, lag_data])
|
|
283
|
+
if F is not None:
|
|
284
|
+
factor_projection_lagged = F[:, :int(r_i * pC)]
|
|
285
|
+
else:
|
|
286
|
+
factor_projection_lagged = None
|
|
287
|
+
for freq, idx_iFreq in freq_groups.items():
|
|
288
|
+
if freq == clock:
|
|
289
|
+
continue
|
|
290
|
+
tent_weights = tent_weights_dict.get(freq) if (tent_weights_dict and freq in tent_weights_dict) else get_tent_weights_for_pair(freq, clock)
|
|
291
|
+
if tent_weights is None:
|
|
292
|
+
clock_h = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
293
|
+
freq_h = FREQUENCY_HIERARCHY.get(freq, 3)
|
|
294
|
+
n_periods_est = freq_h - clock_h + 1
|
|
295
|
+
if 0 < n_periods_est <= 12:
|
|
296
|
+
tent_weights = generate_tent_weights(n_periods_est, 'symmetric')
|
|
297
|
+
_logger.warning(f"init_conditions: generated symmetric tent weights for '{freq}'")
|
|
298
|
+
else:
|
|
299
|
+
raise ValueError(f"init_conditions: cannot determine tent weights for '{freq}'")
|
|
300
|
+
R_mat_freq, q_freq = generate_R_mat(tent_weights)
|
|
301
|
+
pC_freq = len(tent_weights)
|
|
302
|
+
if factor_projection_lagged.shape[1] < r_i * pC_freq:
|
|
303
|
+
factor_projection_freq = np.hstack([
|
|
304
|
+
factor_projection_lagged,
|
|
305
|
+
np.zeros((factor_projection_lagged.shape[0], r_i * pC_freq - factor_projection_lagged.shape[1]))
|
|
306
|
+
])
|
|
307
|
+
else:
|
|
308
|
+
factor_projection_freq = factor_projection_lagged[:, :int(r_i * pC_freq)]
|
|
309
|
+
Rcon_i = np.kron(R_mat_freq, np.eye(int(r_i)))
|
|
310
|
+
q_i = np.kron(q_freq, np.zeros(int(r_i)))
|
|
311
|
+
for j in idx_iFreq:
|
|
312
|
+
series_data = resNaN[pC_freq:, j]
|
|
313
|
+
if len(series_data) < factor_projection_freq.shape[0] and len(series_data) > 0:
|
|
314
|
+
series_data_padded = np.full(factor_projection_freq.shape[0], np.nan)
|
|
315
|
+
series_data_padded[:len(series_data)] = series_data
|
|
316
|
+
series_data = series_data_padded
|
|
317
|
+
if np.sum(~np.isnan(series_data)) < factor_projection_freq.shape[1] + 2:
|
|
318
|
+
series_data = data_residuals[pC_freq:, j]
|
|
319
|
+
finite_mask = ~np.isnan(series_data)
|
|
320
|
+
factor_projection_clean = factor_projection_freq[finite_mask, :]
|
|
321
|
+
series_data_clean = series_data[finite_mask]
|
|
322
|
+
if len(series_data_clean) > 0 and factor_projection_clean.shape[0] > 0:
|
|
323
|
+
try:
|
|
324
|
+
gram = factor_projection_clean.T @ factor_projection_clean
|
|
325
|
+
gram_inv = inv(gram)
|
|
326
|
+
loadings = gram_inv @ factor_projection_clean.T @ series_data_clean
|
|
327
|
+
if Rcon_i is not None and q_i is not None and Rcon_i.size > 0:
|
|
328
|
+
constraint_term = gram_inv @ Rcon_i.T @ inv(Rcon_i @ gram_inv @ Rcon_i.T) @ (Rcon_i @ loadings - q_i)
|
|
329
|
+
loadings = loadings - constraint_term
|
|
330
|
+
block_loadings[j, :int(pC_freq * r_i)] = loadings[:int(pC_freq * r_i)]
|
|
331
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
332
|
+
block_loadings[j, :int(pC_freq * r_i)] = 0.0
|
|
333
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
334
|
+
_logger.warning(f"init_conditions: Block {i+1} initialization failed, using fallback.")
|
|
335
|
+
if len(idx_clock_freq) > 0:
|
|
336
|
+
block_loadings[idx_clock_freq, :int(r_i)] = np.eye(len(idx_clock_freq), int(r_i))[:len(idx_clock_freq), :int(r_i)]
|
|
337
|
+
if factor_projection_lagged is not None:
|
|
338
|
+
expected_width = int(pC * r_i)
|
|
339
|
+
if factor_projection_lagged.shape[1] != expected_width:
|
|
340
|
+
_logger.warning(f"init_conditions: factor projection width mismatch for block {i+1}, resetting.")
|
|
341
|
+
factor_projection_lagged = None
|
|
342
|
+
if factor_projection_lagged is not None:
|
|
343
|
+
factor_projection_padded = np.vstack([np.zeros((pC - 1, int(pC * r_i))), factor_projection_lagged])
|
|
344
|
+
if factor_projection_padded.shape[0] < T:
|
|
345
|
+
factor_projection_padded = np.vstack([factor_projection_padded, np.zeros((T - factor_projection_padded.shape[0], int(pC * r_i)))])
|
|
346
|
+
else:
|
|
347
|
+
factor_projection_padded = np.zeros((T, int(pC * r_i)))
|
|
348
|
+
block_loadings_residual = block_loadings[:, :int(pC * r_i)]
|
|
349
|
+
if factor_projection_padded.shape[0] != data_residuals.shape[0]:
|
|
350
|
+
if factor_projection_padded.shape[0] > data_residuals.shape[0]:
|
|
351
|
+
factor_projection_padded = factor_projection_padded[:data_residuals.shape[0], :]
|
|
352
|
+
else:
|
|
353
|
+
factor_projection_padded = np.vstack([factor_projection_padded, np.zeros((data_residuals.shape[0] - factor_projection_padded.shape[0], factor_projection_padded.shape[1]))])
|
|
354
|
+
data_residuals[:, idx_i] = data_residuals[:, idx_i] - factor_projection_padded @ block_loadings_residual[idx_i, :].T
|
|
355
|
+
resNaN = data_residuals.copy()
|
|
356
|
+
resNaN[indNaN] = np.nan
|
|
357
|
+
C = block_loadings if C is None else np.hstack([C, block_loadings])
|
|
358
|
+
if len(idx_clock_freq) > 0 and F is not None:
|
|
359
|
+
z = F[:, :int(r_i)]
|
|
360
|
+
Z_lag = F[:, int(r_i):int(r_i * (p + 1))] if F.shape[1] > int(r_i) else np.zeros((F.shape[0], int(r_i * p)))
|
|
361
|
+
block_transition = np.zeros((int(r_i * ppC), int(r_i * ppC)))
|
|
362
|
+
if Z_lag.shape[0] > 0 and Z_lag.shape[1] > 0:
|
|
363
|
+
try:
|
|
364
|
+
ar_coeffs = inv(Z_lag.T @ Z_lag) @ Z_lag.T @ z
|
|
365
|
+
block_transition[:int(r_i), :int(r_i * p)] = ar_coeffs.T
|
|
366
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
367
|
+
block_transition[:int(r_i), :int(r_i * p)] = 0.0
|
|
368
|
+
if r_i * (ppC - 1) > 0:
|
|
369
|
+
block_transition[int(r_i):, :int(r_i * (ppC - 1))] = np.eye(int(r_i * (ppC - 1)))
|
|
370
|
+
block_innovation_cov = np.zeros((int(ppC * r_i), int(ppC * r_i)))
|
|
371
|
+
if len(z) > 0:
|
|
372
|
+
innovation_residuals = z - Z_lag @ ar_coeffs if ar_coeffs is not None else z
|
|
373
|
+
innovation_residuals = np.nan_to_num(innovation_residuals, nan=0.0, posinf=0.0, neginf=0.0)
|
|
374
|
+
if innovation_residuals.shape[1] > 1:
|
|
375
|
+
try:
|
|
376
|
+
Q_block = np.cov(innovation_residuals.T)
|
|
377
|
+
if np.any(~np.isfinite(Q_block)):
|
|
378
|
+
Q_block = np.eye(int(r_i)) * 0.1
|
|
379
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
380
|
+
Q_block = np.eye(int(r_i)) * 0.1
|
|
381
|
+
else:
|
|
382
|
+
variance = np.var(innovation_residuals)
|
|
383
|
+
if not np.isfinite(variance) or variance <= 0:
|
|
384
|
+
variance = 0.1
|
|
385
|
+
Q_block = np.array([[variance]]) if innovation_residuals.ndim > 1 else np.eye(int(r_i)) * variance
|
|
386
|
+
block_innovation_cov[:int(r_i), :int(r_i)] = Q_block
|
|
387
|
+
block_transition_clean = _clean_matrix(block_transition, 'loading')
|
|
388
|
+
block_innovation_cov_clean = _clean_matrix(block_innovation_cov, 'covariance', default_nan=0.0)
|
|
389
|
+
try:
|
|
390
|
+
kron_transition = np.kron(block_transition_clean, block_transition_clean)
|
|
391
|
+
identity_kron = np.eye(int((r_i * ppC) ** 2)) - kron_transition
|
|
392
|
+
innovation_cov_flat = block_innovation_cov_clean.flatten()
|
|
393
|
+
init_cov_block = np.reshape(inv(identity_kron) @ innovation_cov_flat, (int(r_i * ppC), int(r_i * ppC)))
|
|
394
|
+
if np.any(~np.isfinite(init_cov_block)):
|
|
395
|
+
raise ValueError("invalid init_cov_block")
|
|
396
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
397
|
+
_logger.warning(f"init_conditions: initial covariance failed for block {i+1}; using diagonal fallback.")
|
|
398
|
+
init_cov_block = np.eye(int(r_i * ppC)) * 0.1
|
|
399
|
+
if A is None:
|
|
400
|
+
A, Q, V_0 = block_transition, block_innovation_cov, init_cov_block
|
|
401
|
+
else:
|
|
402
|
+
A = block_diag(A, block_transition)
|
|
403
|
+
Q = block_diag(Q, block_innovation_cov)
|
|
404
|
+
V_0 = block_diag(V_0, init_cov_block)
|
|
405
|
+
else:
|
|
406
|
+
block_transition = np.eye(int(r_i * ppC)) * 0.9
|
|
407
|
+
block_innovation_cov = np.eye(int(r_i * ppC)) * 0.1
|
|
408
|
+
init_cov_block = np.eye(int(r_i * ppC)) * 0.1
|
|
409
|
+
if A is None:
|
|
410
|
+
A, Q, V_0 = block_transition, block_innovation_cov, init_cov_block
|
|
411
|
+
else:
|
|
412
|
+
A = block_diag(A, block_transition)
|
|
413
|
+
Q = block_diag(Q, block_innovation_cov)
|
|
414
|
+
V_0 = block_diag(V_0, init_cov_block)
|
|
415
|
+
|
|
416
|
+
eyeN = np.eye(N)[:, i_idio.astype(bool)]
|
|
417
|
+
C = eyeN if C is None else np.hstack([C, eyeN])
|
|
418
|
+
|
|
419
|
+
if nQ > 0 and frequencies is not None:
|
|
420
|
+
clock_h = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
421
|
+
slower_indices = [j for j in range(N) if j < len(frequencies) and FREQUENCY_HIERARCHY.get(frequencies[j], 3) > clock_h]
|
|
422
|
+
Rdiag = np.nanvar(resNaN, axis=0)
|
|
423
|
+
Rdiag = np.where((np.isnan(Rdiag) | np.isinf(Rdiag)), 1e-4, Rdiag)
|
|
424
|
+
slower_idio_blocks = []
|
|
425
|
+
slower_series_count: Dict[str, int] = {}
|
|
426
|
+
slower_series_indices: Dict[str, list] = {}
|
|
427
|
+
slower_idio_dims = [] # Track dimensions for BQ
|
|
428
|
+
for j in range(N):
|
|
429
|
+
if j < len(frequencies):
|
|
430
|
+
freq = frequencies[j]
|
|
431
|
+
if FREQUENCY_HIERARCHY.get(freq, 3) > clock_h:
|
|
432
|
+
slower_series_count[freq] = slower_series_count.get(freq, 0) + 1
|
|
433
|
+
slower_series_indices.setdefault(freq, []).append(j)
|
|
434
|
+
for freq, idx_list in slower_series_indices.items():
|
|
435
|
+
tent_weights = tent_weights_dict.get(freq) if (tent_weights_dict and freq in tent_weights_dict) else get_tent_weights_for_pair(freq, clock)
|
|
436
|
+
if tent_weights is None:
|
|
437
|
+
clock_h = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
438
|
+
freq_h = FREQUENCY_HIERARCHY.get(freq, 3)
|
|
439
|
+
n_periods_est = freq_h - clock_h + 1
|
|
440
|
+
if 0 < n_periods_est <= 12:
|
|
441
|
+
tent_weights = generate_tent_weights(n_periods_est, 'symmetric')
|
|
442
|
+
else:
|
|
443
|
+
raise ValueError(f"init_conditions: cannot determine tent weights for '{freq}'")
|
|
444
|
+
n_periods = len(tent_weights)
|
|
445
|
+
idio_block = np.zeros((N, n_periods * len(idx_list)))
|
|
446
|
+
for idx, j in enumerate(idx_list):
|
|
447
|
+
col_start = idx * n_periods
|
|
448
|
+
idio_block[j, col_start:col_start + n_periods] = tent_weights
|
|
449
|
+
slower_idio_blocks.append(idio_block)
|
|
450
|
+
slower_idio_dims.append(n_periods * len(idx_list)) # Track dimension for this frequency
|
|
451
|
+
if slower_idio_blocks:
|
|
452
|
+
slower_idio_full = np.hstack(slower_idio_blocks)
|
|
453
|
+
C = np.hstack([C, slower_idio_full])
|
|
454
|
+
Rdiag = np.where((np.isnan(Rdiag) | np.isinf(Rdiag) | (Rdiag < 0)), 1e-4, Rdiag)
|
|
455
|
+
R = np.diag(Rdiag)
|
|
456
|
+
else:
|
|
457
|
+
var_values = np.nanvar(resNaN, axis=0)
|
|
458
|
+
var_values = np.where((np.isnan(var_values) | np.isinf(var_values)), 1e-4, var_values)
|
|
459
|
+
var_values = np.maximum(var_values, 1e-4)
|
|
460
|
+
R = np.diag(var_values)
|
|
461
|
+
slower_idio_dims = []
|
|
462
|
+
|
|
463
|
+
ii_idio = np.where(i_idio)[0]
|
|
464
|
+
n_idio = len(ii_idio)
|
|
465
|
+
BM = np.zeros((n_idio, n_idio))
|
|
466
|
+
SM = np.zeros((n_idio, n_idio))
|
|
467
|
+
for idx, i in enumerate(ii_idio):
|
|
468
|
+
R[i, i] = 1e-4
|
|
469
|
+
res_i_full = data_residuals[:, i]
|
|
470
|
+
res_i = resNaN[:, i]
|
|
471
|
+
leadZero = int(np.argmax(~np.isnan(res_i))) if np.any(~np.isnan(res_i)) else 0
|
|
472
|
+
endZero = int(np.argmax(~np.isnan(res_i[::-1]))) if np.any(~np.isnan(res_i)) else 0
|
|
473
|
+
res_i_trunc = res_i_full[leadZero:T - endZero] if endZero > 0 else res_i_full[leadZero:]
|
|
474
|
+
if len(res_i_trunc) > 1:
|
|
475
|
+
res_i_copy = res_i_trunc.copy()
|
|
476
|
+
X_ar = res_i_copy[:-1].reshape(-1, 1)
|
|
477
|
+
y_ar = res_i_copy[1:].reshape(-1, 1)
|
|
478
|
+
XTX = X_ar.T @ X_ar
|
|
479
|
+
if XTX.size == 1 and XTX[0, 0] != 0 and np.isfinite(XTX[0, 0]):
|
|
480
|
+
try:
|
|
481
|
+
BM_val = (1.0 / XTX[0, 0]) * (X_ar.T @ y_ar)
|
|
482
|
+
BM_val_clean = BM_val[0, 0] if BM_val.size > 0 else 0.1
|
|
483
|
+
BM_val_clean = 0.1 if (np.isnan(BM_val_clean) or np.isinf(BM_val_clean)) else BM_val_clean
|
|
484
|
+
BM[idx, idx] = BM_val_clean
|
|
485
|
+
residuals = res_i_trunc[1:] - res_i_trunc[:-1] * BM[idx, idx]
|
|
486
|
+
var_val = np.var(residuals)
|
|
487
|
+
SM[idx, idx] = var_val if np.isfinite(var_val) else 0.1
|
|
488
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
489
|
+
BM[idx, idx] = 0.1
|
|
490
|
+
SM[idx, idx] = 0.1
|
|
491
|
+
else:
|
|
492
|
+
BM[idx, idx] = 0.1
|
|
493
|
+
SM[idx, idx] = 0.1
|
|
494
|
+
else:
|
|
495
|
+
BM[idx, idx] = 0.1
|
|
496
|
+
SM[idx, idx] = 0.1
|
|
497
|
+
# Monthly idio init covariance
|
|
498
|
+
eye_diag = np.diag(np.eye(BM.shape[0]))
|
|
499
|
+
BM_diag_sq = np.diag(BM) ** 2
|
|
500
|
+
denom_val = np.where(np.abs(eye_diag - BM_diag_sq) < 1e-10, 1.0, eye_diag - BM_diag_sq)
|
|
501
|
+
denom_viM = np.diag(1.0 / denom_val)
|
|
502
|
+
initViM = denom_viM @ SM
|
|
503
|
+
# Slower idio init: create BQ and SQ with proper dimensions
|
|
504
|
+
n_slower_idio = sum(slower_idio_dims) if slower_idio_dims else 0
|
|
505
|
+
if n_slower_idio > 0:
|
|
506
|
+
# Create transition and innovation covariance for slower idio components
|
|
507
|
+
# Each slower idio component follows an AR(1) process with tent weights
|
|
508
|
+
BQ = np.eye(n_slower_idio) * 0.9 # AR coefficient for slower idio
|
|
509
|
+
SQ = np.eye(n_slower_idio) * 0.1 # Innovation variance
|
|
510
|
+
initViQ = np.eye(n_slower_idio) * 0.1 # Initial covariance
|
|
511
|
+
else:
|
|
512
|
+
BQ = np.array([]).reshape(0, 0)
|
|
513
|
+
SQ = np.array([]).reshape(0, 0)
|
|
514
|
+
initViQ = np.array([]).reshape(0, 0)
|
|
515
|
+
A = block_diag(A, BM, BQ)
|
|
516
|
+
Q = block_diag(Q, SM, SQ)
|
|
517
|
+
Z_0 = np.zeros(A.shape[0])
|
|
518
|
+
# Ensure all covariance matrices are square
|
|
519
|
+
V_0 = _ensure_square_matrix(V_0, method='diag')
|
|
520
|
+
initViM = _ensure_square_matrix(initViM, method='diag')
|
|
521
|
+
initViQ = _ensure_square_matrix(initViQ, method='diag')
|
|
522
|
+
V_0 = block_diag(V_0, initViM, initViQ)
|
|
523
|
+
# Final clean/validate using _check_finite for consistency
|
|
524
|
+
outputs = {'A': A, 'C': C, 'Q': Q, 'R': R, 'Z_0': Z_0, 'V_0': V_0}
|
|
525
|
+
for param_name, param_value in outputs.items():
|
|
526
|
+
if param_value.size > 0 and not _check_finite(param_value, param_name):
|
|
527
|
+
# Clean based on parameter type
|
|
528
|
+
if param_name in ['A', 'Q', 'V_0']:
|
|
529
|
+
outputs[param_name] = _clean_matrix(param_value, 'covariance', default_nan=0.0)
|
|
530
|
+
elif param_name == 'R':
|
|
531
|
+
outputs[param_name] = _clean_matrix(param_value, 'diagonal', default_nan=1e-4)
|
|
532
|
+
elif param_name == 'C':
|
|
533
|
+
outputs[param_name] = _clean_matrix(param_value, 'loading')
|
|
534
|
+
elif param_name == 'Z_0':
|
|
535
|
+
outputs[param_name] = np.zeros_like(param_value)
|
|
536
|
+
return outputs['A'], outputs['C'], outputs['Q'], outputs['R'], outputs['Z_0'], outputs['V_0']
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def em_step(
|
|
540
|
+
y: np.ndarray,
|
|
541
|
+
A: np.ndarray,
|
|
542
|
+
C: np.ndarray,
|
|
543
|
+
Q: np.ndarray,
|
|
544
|
+
R: np.ndarray,
|
|
545
|
+
Z_0: np.ndarray,
|
|
546
|
+
V_0: np.ndarray,
|
|
547
|
+
r: np.ndarray,
|
|
548
|
+
p: int,
|
|
549
|
+
R_mat: Optional[np.ndarray],
|
|
550
|
+
q: Optional[np.ndarray],
|
|
551
|
+
nQ: Optional[int],
|
|
552
|
+
i_idio: np.ndarray,
|
|
553
|
+
blocks: np.ndarray,
|
|
554
|
+
tent_weights_dict: Optional[Dict[str, np.ndarray]] = None,
|
|
555
|
+
clock: str = 'm',
|
|
556
|
+
frequencies: Optional[np.ndarray] = None,
|
|
557
|
+
config: Optional[DFMConfig] = None
|
|
558
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]:
|
|
559
|
+
"""Perform one EM iteration (E-step + M-step) and return updated parameters.
|
|
560
|
+
|
|
561
|
+
This function performs a single iteration of the Expectation-Maximization algorithm:
|
|
562
|
+
|
|
563
|
+
1. **E-step**: Run Kalman filter/smoother to compute expected sufficient statistics
|
|
564
|
+
- E[Z_t | Y] (smoothed factors)
|
|
565
|
+
- E[Z_t Z_t' | Y] (smoothed factor covariances)
|
|
566
|
+
- E[Z_t Z_{t-1}' | Y] (smoothed factor cross-covariances)
|
|
567
|
+
|
|
568
|
+
2. **M-step**: Update parameters to maximize expected log-likelihood
|
|
569
|
+
- C: Loading matrix (via regression of data on factors)
|
|
570
|
+
- R: Observation covariance (via residual variance)
|
|
571
|
+
- A: Transition matrix (via AR regression on factors)
|
|
572
|
+
- Q: Innovation covariance (via innovation variance)
|
|
573
|
+
- V_0: Initial covariance (via stationary covariance)
|
|
574
|
+
|
|
575
|
+
Parameters
|
|
576
|
+
----------
|
|
577
|
+
y : np.ndarray
|
|
578
|
+
Observation matrix (n x T) with missing data
|
|
579
|
+
A, C, Q, R : np.ndarray
|
|
580
|
+
Current parameter estimates
|
|
581
|
+
Z_0, V_0 : np.ndarray
|
|
582
|
+
Current initial state and covariance
|
|
583
|
+
r : np.ndarray
|
|
584
|
+
Number of factors per block
|
|
585
|
+
p : int
|
|
586
|
+
AR lag order
|
|
587
|
+
R_mat : np.ndarray, optional
|
|
588
|
+
Constraint matrix for tent kernel aggregation
|
|
589
|
+
q : np.ndarray, optional
|
|
590
|
+
Constraint vector for tent kernel aggregation
|
|
591
|
+
nQ : int
|
|
592
|
+
Number of slower-frequency series
|
|
593
|
+
i_idio : np.ndarray
|
|
594
|
+
Indicator array (1 for clock frequency, 0 for slower)
|
|
595
|
+
blocks : np.ndarray
|
|
596
|
+
Block structure (N x n_blocks)
|
|
597
|
+
tent_weights_dict : dict, optional
|
|
598
|
+
Dictionary mapping frequency pairs to tent weights
|
|
599
|
+
clock : str
|
|
600
|
+
Clock frequency
|
|
601
|
+
frequencies : np.ndarray, optional
|
|
602
|
+
Array of frequencies for each series
|
|
603
|
+
config : DFMConfig, optional
|
|
604
|
+
Configuration object for numerical stability parameters
|
|
605
|
+
|
|
606
|
+
Returns
|
|
607
|
+
-------
|
|
608
|
+
C_new : np.ndarray
|
|
609
|
+
Updated loading matrix (N x m)
|
|
610
|
+
R_new : np.ndarray
|
|
611
|
+
Updated observation covariance (N x N)
|
|
612
|
+
A_new : np.ndarray
|
|
613
|
+
Updated transition matrix (m x m)
|
|
614
|
+
Q_new : np.ndarray
|
|
615
|
+
Updated innovation covariance (m x m)
|
|
616
|
+
Z_0_new : np.ndarray
|
|
617
|
+
Updated initial state (m,)
|
|
618
|
+
V_0_new : np.ndarray
|
|
619
|
+
Updated initial covariance (m x m)
|
|
620
|
+
loglik : float
|
|
621
|
+
Log-likelihood value for this iteration
|
|
622
|
+
"""
|
|
623
|
+
# Validate and clean input parameters
|
|
624
|
+
if not _check_finite(A, "A"):
|
|
625
|
+
_logger.warning("em_step: A contains NaN/Inf, regularizing")
|
|
626
|
+
A = np.eye(A.shape[0]) * 0.9 + _clean_matrix(A, 'loading') * 0.1
|
|
627
|
+
if not _check_finite(Q, "Q"):
|
|
628
|
+
_logger.warning("em_step: Q contains NaN/Inf, regularizing")
|
|
629
|
+
Q = _clean_matrix(Q, 'covariance', default_nan=1e-6)
|
|
630
|
+
if not _check_finite(R, "R"):
|
|
631
|
+
_logger.warning("em_step: R contains NaN/Inf, regularizing")
|
|
632
|
+
R = _clean_matrix(R, 'diagonal', default_nan=1e-4, default_inf=1e4)
|
|
633
|
+
if not _check_finite(C, "C"):
|
|
634
|
+
_logger.warning("em_step: C contains NaN/Inf, regularizing")
|
|
635
|
+
C = _clean_matrix(C, 'loading')
|
|
636
|
+
if not _check_finite(Z_0, "Z_0"):
|
|
637
|
+
_logger.warning("em_step: Z_0 contains NaN/Inf, resetting to zeros")
|
|
638
|
+
Z_0 = np.zeros_like(Z_0)
|
|
639
|
+
if not _check_finite(V_0, "V_0"):
|
|
640
|
+
_logger.warning("em_step: V_0 contains NaN/Inf, using regularized identity")
|
|
641
|
+
V_0 = np.eye(V_0.shape[0]) * 0.1
|
|
642
|
+
|
|
643
|
+
n, T = y.shape
|
|
644
|
+
if nQ is None and frequencies is not None:
|
|
645
|
+
clock_h = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
646
|
+
nQ = sum(1 for f in frequencies if FREQUENCY_HIERARCHY.get(f, 3) > clock_h)
|
|
647
|
+
elif nQ is None:
|
|
648
|
+
nQ = 0
|
|
649
|
+
|
|
650
|
+
pC = 1
|
|
651
|
+
if tent_weights_dict:
|
|
652
|
+
for tent_weights in tent_weights_dict.values():
|
|
653
|
+
if tent_weights is not None and len(tent_weights) > pC:
|
|
654
|
+
pC = len(tent_weights)
|
|
655
|
+
elif R_mat is not None:
|
|
656
|
+
pC = R_mat.shape[1]
|
|
657
|
+
ppC = int(max(p, pC))
|
|
658
|
+
num_blocks = blocks.shape[1]
|
|
659
|
+
|
|
660
|
+
if config is None:
|
|
661
|
+
from types import SimpleNamespace
|
|
662
|
+
config = SimpleNamespace(
|
|
663
|
+
clip_ar_coefficients=True,
|
|
664
|
+
ar_clip_min=-0.99,
|
|
665
|
+
ar_clip_max=0.99,
|
|
666
|
+
warn_on_ar_clip=True,
|
|
667
|
+
clip_data_values=True,
|
|
668
|
+
data_clip_threshold=100.0,
|
|
669
|
+
warn_on_data_clip=True,
|
|
670
|
+
use_regularization=True,
|
|
671
|
+
regularization_scale=1e-5,
|
|
672
|
+
min_eigenvalue=1e-8,
|
|
673
|
+
max_eigenvalue=1e6,
|
|
674
|
+
warn_on_regularization=True,
|
|
675
|
+
use_damped_updates=True,
|
|
676
|
+
damping_factor=0.8,
|
|
677
|
+
warn_on_damped_update=True,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
zsmooth, vsmooth, vvsmooth, loglik = run_kf(y, A, C, Q, R, Z_0, V_0)
|
|
681
|
+
Zsmooth = zsmooth.T
|
|
682
|
+
|
|
683
|
+
A_new = A.copy()
|
|
684
|
+
Q_new = Q.copy()
|
|
685
|
+
V_0_new = V_0.copy()
|
|
686
|
+
|
|
687
|
+
for i in range(num_blocks):
|
|
688
|
+
r_i = int(r[i])
|
|
689
|
+
factor_lag_size = r_i * p
|
|
690
|
+
factor_start_idx = int(np.sum(r[:i]) * ppC)
|
|
691
|
+
t_start = factor_start_idx
|
|
692
|
+
t_end = int(factor_start_idx + r_i * ppC)
|
|
693
|
+
b_subset = slice(factor_start_idx, factor_start_idx + factor_lag_size)
|
|
694
|
+
|
|
695
|
+
expected_factor_outer = Zsmooth[b_subset, 1:] @ Zsmooth[b_subset, 1:].T + np.sum(vsmooth[b_subset, :, :][:, b_subset, 1:], axis=2)
|
|
696
|
+
expected_factor_lag_outer = Zsmooth[b_subset, :-1] @ Zsmooth[b_subset, :-1].T + np.sum(vsmooth[b_subset, :, :][:, b_subset, :-1], axis=2)
|
|
697
|
+
expected_factor_lag_cross = Zsmooth[b_subset, 1:] @ Zsmooth[b_subset, :-1].T + np.sum(vvsmooth[b_subset, :, :][:, b_subset, :], axis=2)
|
|
698
|
+
|
|
699
|
+
expected_factor_lag_outer = _clean_matrix(expected_factor_lag_outer, 'covariance', default_nan=0.0)
|
|
700
|
+
expected_factor_lag_cross = _clean_matrix(expected_factor_lag_cross, 'general', default_nan=0.0)
|
|
701
|
+
|
|
702
|
+
block_transition = A[t_start:t_end, t_start:t_end].copy()
|
|
703
|
+
block_innovation_cov = Q[t_start:t_end, t_start:t_end].copy()
|
|
704
|
+
try:
|
|
705
|
+
expected_lag_sub = expected_factor_lag_outer[:factor_lag_size, :factor_lag_size]
|
|
706
|
+
min_eigenval = _get_config_param(config, "min_eigenvalue", 1e-8)
|
|
707
|
+
warn_reg = _get_config_param(config, "warn_on_regularization", True)
|
|
708
|
+
expected_lag_sub, _ = _ensure_positive_definite(expected_lag_sub, min_eigenval, warn_reg)
|
|
709
|
+
try:
|
|
710
|
+
eigenvals = np.linalg.eigvals(expected_lag_sub)
|
|
711
|
+
cond_num = (np.max(eigenvals) / max(np.min(eigenvals), 1e-12)) if np.max(eigenvals) > 0 else 1.0
|
|
712
|
+
expected_lag_inv = pinv(expected_lag_sub, cond=1e-8) if cond_num > 1e12 else inv(expected_lag_sub)
|
|
713
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
714
|
+
expected_lag_inv = pinv(expected_lag_sub)
|
|
715
|
+
transition_update = expected_factor_lag_cross[:r_i, :factor_lag_size] @ expected_lag_inv
|
|
716
|
+
transition_update, _ = _apply_ar_clipping(transition_update, config)
|
|
717
|
+
block_transition[:r_i, :factor_lag_size] = transition_update
|
|
718
|
+
block_innovation_cov[:r_i, :r_i] = (
|
|
719
|
+
expected_factor_outer[:r_i, :r_i] -
|
|
720
|
+
block_transition[:r_i, :factor_lag_size] @ expected_factor_lag_cross[:r_i, :factor_lag_size].T
|
|
721
|
+
) / T
|
|
722
|
+
block_innovation_cov = _clean_matrix(block_innovation_cov, 'covariance', default_nan=0.0)
|
|
723
|
+
innovation_cov_reg, _ = _ensure_positive_definite(block_innovation_cov[:r_i, :r_i], min_eigenval, warn_reg)
|
|
724
|
+
block_innovation_cov[:r_i, :r_i] = innovation_cov_reg
|
|
725
|
+
max_eig = _get_config_param(config, "max_eigenvalue", 1e6)
|
|
726
|
+
block_innovation_cov[:r_i, :r_i] = _cap_max_eigenvalue(block_innovation_cov[:r_i, :r_i], max_eigenval=max_eig)
|
|
727
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
728
|
+
if np.allclose(block_transition[:r_i, :factor_lag_size], 0):
|
|
729
|
+
block_transition[:r_i, :factor_lag_size] = np.random.randn(r_i, factor_lag_size) * 0.1
|
|
730
|
+
else:
|
|
731
|
+
block_transition[:r_i, :factor_lag_size] *= 0.95
|
|
732
|
+
if np.any(~np.isfinite(block_transition)):
|
|
733
|
+
block_transition = _clean_matrix(block_transition, 'loading', default_nan=0.0, default_inf=0.99)
|
|
734
|
+
block_transition, _ = _apply_ar_clipping(block_transition, config)
|
|
735
|
+
A_new[t_start:t_end, t_start:t_end] = block_transition
|
|
736
|
+
Q_new[t_start:t_end, t_start:t_end] = block_innovation_cov
|
|
737
|
+
V_0_block = _clean_matrix(vsmooth[t_start:t_end, t_start:t_end, 0], 'covariance', default_nan=0.0)
|
|
738
|
+
V_0_block, _ = _ensure_positive_definite(V_0_block, min_eigenval, warn_reg)
|
|
739
|
+
V_0_new[t_start:t_end, t_start:t_end] = V_0_block
|
|
740
|
+
|
|
741
|
+
idio_start_idx = int(np.sum(r) * ppC)
|
|
742
|
+
n_idio = int(np.sum(i_idio))
|
|
743
|
+
i_subset = slice(idio_start_idx, idio_start_idx + n_idio)
|
|
744
|
+
i_subset_slice = slice(i_subset.start, i_subset.stop)
|
|
745
|
+
Z_idio = Zsmooth[i_subset_slice, 1:]
|
|
746
|
+
n_idio_actual = Z_idio.shape[0]
|
|
747
|
+
expected_idio_current_sq = np.sum(Z_idio**2, axis=1)
|
|
748
|
+
vsmooth_idio_block = vsmooth[i_subset_slice, :, :][:, i_subset_slice, 1:]
|
|
749
|
+
vsmooth_idio_sum = np.sum(vsmooth_idio_block, axis=2)
|
|
750
|
+
vsmooth_idio_diag = np.diag(vsmooth_idio_sum[:n_idio_actual, :n_idio_actual]) if vsmooth_idio_sum.ndim == 2 else np.zeros(n_idio_actual)
|
|
751
|
+
expected_idio_current_sq = expected_idio_current_sq + vsmooth_idio_diag
|
|
752
|
+
Z_idio_lag = Zsmooth[i_subset_slice, :-1]
|
|
753
|
+
expected_idio_lag_sq = np.sum(Z_idio_lag**2, axis=1)
|
|
754
|
+
vsmooth_lag_block = vsmooth[i_subset_slice, :, :][:, i_subset_slice, :-1]
|
|
755
|
+
vsmooth_lag_sum = np.sum(vsmooth_lag_block, axis=2)
|
|
756
|
+
vsmooth_lag_diag = np.diag(vsmooth_lag_sum[:n_idio_actual, :n_idio_actual]) if vsmooth_lag_sum.ndim == 2 else np.zeros(n_idio_actual)
|
|
757
|
+
expected_idio_lag_sq = expected_idio_lag_sq + vsmooth_lag_diag
|
|
758
|
+
min_cols = min(Z_idio.shape[1], Z_idio_lag.shape[1])
|
|
759
|
+
expected_idio_cross = np.sum(Z_idio[:, :min_cols] * Z_idio_lag[:, :min_cols], axis=1)
|
|
760
|
+
vvsmooth_block = vvsmooth[i_subset_slice, :, :][:, i_subset_slice, :]
|
|
761
|
+
vvsmooth_sum = np.sum(vvsmooth_block, axis=2)
|
|
762
|
+
vvsmooth_diag = np.diag(vvsmooth_sum[:n_idio_actual, :n_idio_actual]) if vvsmooth_sum.ndim == 2 else np.zeros(n_idio_actual)
|
|
763
|
+
expected_idio_cross = expected_idio_cross + vvsmooth_diag
|
|
764
|
+
ar_coeffs_diag, _ = _estimate_ar_coefficient(expected_idio_cross, expected_idio_lag_sq, vsmooth_sum=vsmooth_lag_diag)
|
|
765
|
+
block_transition_idio = np.diag(ar_coeffs_diag)
|
|
766
|
+
innovation_cov_diag = (np.maximum(expected_idio_current_sq, 0.0) - ar_coeffs_diag * expected_idio_cross) / T
|
|
767
|
+
innovation_cov_diag = np.maximum(innovation_cov_diag, 1e-8)
|
|
768
|
+
block_innovation_cov_idio = np.diag(innovation_cov_diag)
|
|
769
|
+
i_subset_size = i_subset.stop - i_subset.start
|
|
770
|
+
if n_idio_actual == i_subset_size:
|
|
771
|
+
A_new[i_subset, i_subset] = block_transition_idio
|
|
772
|
+
Q_new[i_subset, i_subset] = block_innovation_cov_idio
|
|
773
|
+
elif n_idio_actual < i_subset_size:
|
|
774
|
+
A_new[i_subset.start:i_subset.start + n_idio_actual, i_subset.start:i_subset.start + n_idio_actual] = block_transition_idio
|
|
775
|
+
Q_new[i_subset.start:i_subset.start + n_idio_actual, i_subset.start:i_subset.start + n_idio_actual] = block_innovation_cov_idio
|
|
776
|
+
else:
|
|
777
|
+
A_new[i_subset, i_subset] = block_transition_idio[:i_subset_size, :i_subset_size]
|
|
778
|
+
Q_new[i_subset, i_subset] = block_innovation_cov_idio[:i_subset_size, :i_subset_size]
|
|
779
|
+
vsmooth_sub = vsmooth[i_subset_slice, :, :][:, i_subset_slice, 0]
|
|
780
|
+
vsmooth_diag = np.diag(vsmooth_sub[:n_idio_actual, :n_idio_actual]) if vsmooth_sub.ndim == 2 else np.zeros(n_idio_actual)
|
|
781
|
+
for idx in range(min(n_idio_actual, i_subset_size)):
|
|
782
|
+
V_0_new[i_subset.start + idx, i_subset.start + idx] = vsmooth_diag[idx] if idx < len(vsmooth_diag) else 0.0
|
|
783
|
+
|
|
784
|
+
Z_0 = Zsmooth[0, :].copy()
|
|
785
|
+
nanY = np.isnan(y)
|
|
786
|
+
y_clean = y.copy()
|
|
787
|
+
y_clean[nanY] = 0
|
|
788
|
+
bl = np.unique(blocks, axis=0)
|
|
789
|
+
n_bl = bl.shape[0]
|
|
790
|
+
bl_idx_same_freq = None
|
|
791
|
+
bl_idx_slower_freq = None
|
|
792
|
+
R_con_list = []
|
|
793
|
+
for i in range(num_blocks):
|
|
794
|
+
r_i = int(r[i])
|
|
795
|
+
bl_col_clock_freq = np.repeat(bl[:, i:i+1], r_i, axis=1)
|
|
796
|
+
bl_col_clock_freq = np.hstack([bl_col_clock_freq, np.zeros((n_bl, r_i * (ppC - 1)))])
|
|
797
|
+
bl_col_slower_freq = np.repeat(bl[:, i:i+1], r_i * ppC, axis=1)
|
|
798
|
+
if bl_idx_same_freq is None:
|
|
799
|
+
bl_idx_same_freq = bl_col_clock_freq
|
|
800
|
+
bl_idx_slower_freq = bl_col_slower_freq
|
|
801
|
+
else:
|
|
802
|
+
bl_idx_same_freq = np.hstack([bl_idx_same_freq, bl_col_clock_freq])
|
|
803
|
+
bl_idx_slower_freq = np.hstack([bl_idx_slower_freq, bl_col_slower_freq])
|
|
804
|
+
if R_mat is not None:
|
|
805
|
+
R_con_list.append(np.kron(R_mat, np.eye(r_i)))
|
|
806
|
+
if bl_idx_same_freq is not None:
|
|
807
|
+
bl_idx_same_freq = bl_idx_same_freq.astype(bool)
|
|
808
|
+
bl_idx_slower_freq = bl_idx_slower_freq.astype(bool)
|
|
809
|
+
else:
|
|
810
|
+
bl_idx_same_freq = np.array([]).reshape(n_bl, 0).astype(bool)
|
|
811
|
+
bl_idx_slower_freq = np.array([]).reshape(n_bl, 0).astype(bool)
|
|
812
|
+
R_con = block_diag(*R_con_list) if len(R_con_list) > 0 else np.array([])
|
|
813
|
+
q_con = np.zeros((np.sum(r.astype(int)) * R_mat.shape[0], 1)) if (R_mat is not None and q is not None) else np.array([])
|
|
814
|
+
|
|
815
|
+
i_idio_same = i_idio
|
|
816
|
+
n_idio_same = int(np.sum(i_idio_same))
|
|
817
|
+
c_i_idio = np.cumsum(i_idio.astype(int))
|
|
818
|
+
C_new = C.copy()
|
|
819
|
+
for i in range(n_bl):
|
|
820
|
+
bl_i = bl[i, :]
|
|
821
|
+
rs = int(np.sum(r[bl_i.astype(bool)]))
|
|
822
|
+
idx_i = np.where((blocks == bl_i).all(axis=1))[0]
|
|
823
|
+
freq_groups = group_series_by_frequency(idx_i, frequencies, clock)
|
|
824
|
+
idx_clock_freq = freq_groups.get(clock, np.array([], dtype=int))
|
|
825
|
+
n_i = len(idx_clock_freq)
|
|
826
|
+
if n_i == 0:
|
|
827
|
+
continue
|
|
828
|
+
bl_idx_same_freq_i = np.where(bl_idx_same_freq[i, :])[0]
|
|
829
|
+
if len(bl_idx_same_freq_i) == 0:
|
|
830
|
+
continue
|
|
831
|
+
rs_actual = len(bl_idx_same_freq_i)
|
|
832
|
+
if rs_actual != rs:
|
|
833
|
+
rs = rs_actual
|
|
834
|
+
denom_size = n_i * rs
|
|
835
|
+
denom = np.zeros((denom_size, denom_size))
|
|
836
|
+
nom = np.zeros((n_i, rs))
|
|
837
|
+
i_idio_i = i_idio_same[idx_clock_freq]
|
|
838
|
+
i_idio_ii = np.cumsum(i_idio.astype(int))[idx_clock_freq]
|
|
839
|
+
i_idio_ii = i_idio_ii[i_idio_i.astype(bool)]
|
|
840
|
+
for t in range(T):
|
|
841
|
+
nan_mask = ~nanY[idx_clock_freq, t]
|
|
842
|
+
Wt = np.diag(nan_mask.astype(float))
|
|
843
|
+
if t + 1 < Zsmooth.shape[0]:
|
|
844
|
+
Z_block_same_freq_row = Zsmooth[t + 1, bl_idx_same_freq_i]
|
|
845
|
+
ZZZ = Z_block_same_freq_row.reshape(-1, 1) @ Z_block_same_freq_row.reshape(1, -1)
|
|
846
|
+
else:
|
|
847
|
+
ZZZ = np.zeros((rs, rs))
|
|
848
|
+
if t + 1 < vsmooth.shape[2]:
|
|
849
|
+
V_block_same_freq = vsmooth[np.ix_(bl_idx_same_freq_i, bl_idx_same_freq_i, [t + 1])]
|
|
850
|
+
V_block_same_freq = V_block_same_freq[:, :, 0] if V_block_same_freq.ndim == 3 else V_block_same_freq
|
|
851
|
+
if V_block_same_freq.shape != (rs, rs):
|
|
852
|
+
V_block_same_freq = np.zeros((rs, rs))
|
|
853
|
+
else:
|
|
854
|
+
V_block_same_freq = np.zeros((rs, rs))
|
|
855
|
+
expected_shape = (denom_size, denom_size)
|
|
856
|
+
try:
|
|
857
|
+
kron_result = np.kron(ZZZ + V_block_same_freq, Wt)
|
|
858
|
+
if kron_result.shape == expected_shape:
|
|
859
|
+
denom += kron_result
|
|
860
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
861
|
+
pass
|
|
862
|
+
if t + 1 < Zsmooth.shape[0]:
|
|
863
|
+
y_vec = y_clean[idx_clock_freq, t].reshape(-1, 1)
|
|
864
|
+
Z_vec_row = Zsmooth[t + 1, bl_idx_same_freq_i].reshape(1, -1)
|
|
865
|
+
y_term = y_vec @ Z_vec_row
|
|
866
|
+
else:
|
|
867
|
+
y_term = np.zeros((len(idx_clock_freq), rs_actual))
|
|
868
|
+
if len(i_idio_ii) > 0 and t + 1 < Zsmooth.shape[0]:
|
|
869
|
+
idio_idx = (idio_start_idx + i_idio_ii).astype(int)
|
|
870
|
+
if idio_idx.max() < Zsmooth.shape[1]:
|
|
871
|
+
idio_Z_col = Zsmooth[t + 1, idio_idx].reshape(-1, 1)
|
|
872
|
+
idio_Z_outer = idio_Z_col @ Z_vec_row
|
|
873
|
+
if t + 1 < vsmooth.shape[2]:
|
|
874
|
+
idio_V = vsmooth[np.ix_(idio_idx, bl_idx_same_freq_i, [t + 1])]
|
|
875
|
+
idio_V = idio_V[:, :, 0] if idio_V.ndim == 3 else idio_V
|
|
876
|
+
else:
|
|
877
|
+
idio_V = np.zeros((len(i_idio_ii), rs_actual))
|
|
878
|
+
idio_term = Wt[:, i_idio_i.astype(bool)] @ (idio_Z_outer + idio_V)
|
|
879
|
+
else:
|
|
880
|
+
idio_term = np.zeros((len(idx_clock_freq), rs_actual))
|
|
881
|
+
else:
|
|
882
|
+
idio_term = np.zeros((len(idx_clock_freq), rs_actual))
|
|
883
|
+
nom += y_term - idio_term
|
|
884
|
+
try:
|
|
885
|
+
scale_factor = _get_config_param(config, "regularization_scale", 1e-5)
|
|
886
|
+
warn_reg = _get_config_param(config, "warn_on_regularization", True)
|
|
887
|
+
reg_param, _ = _compute_regularization_param(denom, scale_factor, warn_reg)
|
|
888
|
+
denom_reg = denom + np.eye(denom.shape[0]) * reg_param
|
|
889
|
+
vec_C = inv(denom_reg) @ nom.flatten()
|
|
890
|
+
C_update = vec_C.reshape(n_i, rs)
|
|
891
|
+
C_update = _clean_matrix(C_update, 'loading', default_nan=0.0, default_inf=0.0)
|
|
892
|
+
C_scale = np.std(C_update[C_update != 0]) if np.any(C_update != 0) else 1.0
|
|
893
|
+
C_max = max(10.0, C_scale * 5)
|
|
894
|
+
C_update = np.clip(C_update, -C_max, C_max)
|
|
895
|
+
for ii, row_idx in enumerate(idx_clock_freq):
|
|
896
|
+
for jj, col_idx in enumerate(bl_idx_same_freq_i):
|
|
897
|
+
C_new[row_idx, col_idx] = C_update[ii, jj]
|
|
898
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
899
|
+
pass
|
|
900
|
+
for freq, idx_iFreq in freq_groups.items():
|
|
901
|
+
if freq == clock:
|
|
902
|
+
continue
|
|
903
|
+
tent_weights = tent_weights_dict.get(freq) if (tent_weights_dict and freq in tent_weights_dict) else get_tent_weights_for_pair(freq, clock)
|
|
904
|
+
if tent_weights is None:
|
|
905
|
+
clock_h = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
906
|
+
freq_h = FREQUENCY_HIERARCHY.get(freq, 3)
|
|
907
|
+
n_periods_est = freq_h - clock_h + 1
|
|
908
|
+
if 0 < n_periods_est <= 12:
|
|
909
|
+
tent_weights = generate_tent_weights(n_periods_est, 'symmetric')
|
|
910
|
+
_logger.warning(f"em_step: generated symmetric tent weights for '{freq}'")
|
|
911
|
+
else:
|
|
912
|
+
raise ValueError(f"em_step: cannot determine tent weights for '{freq}'")
|
|
913
|
+
pC_freq = len(tent_weights)
|
|
914
|
+
rs_full = rs * pC_freq
|
|
915
|
+
R_mat_freq, q_freq = generate_R_mat(tent_weights)
|
|
916
|
+
R_con_i = np.kron(R_mat_freq, np.eye(int(rs)))
|
|
917
|
+
q_con_i = np.kron(q_freq, np.zeros(int(rs)))
|
|
918
|
+
if i < bl_idx_slower_freq.shape[0]:
|
|
919
|
+
bl_idx_slower_freq_i = np.where(bl_idx_slower_freq[i, :])[0]
|
|
920
|
+
if len(bl_idx_slower_freq_i) >= rs_full:
|
|
921
|
+
bl_idx_slower_freq_i = bl_idx_slower_freq_i[:rs_full]
|
|
922
|
+
elif len(bl_idx_slower_freq_i) > 0:
|
|
923
|
+
bl_idx_slower_freq_i = np.pad(bl_idx_slower_freq_i, (0, rs_full - len(bl_idx_slower_freq_i)), mode='edge')
|
|
924
|
+
else:
|
|
925
|
+
continue
|
|
926
|
+
else:
|
|
927
|
+
continue
|
|
928
|
+
if R_con_i.size > 0:
|
|
929
|
+
no_c = ~np.any(R_con_i, axis=1)
|
|
930
|
+
R_con_i = R_con_i[~no_c, :]
|
|
931
|
+
q_con_i = q_con_i[~no_c]
|
|
932
|
+
for j in idx_iFreq:
|
|
933
|
+
rps_actual = len(bl_idx_slower_freq_i) if len(bl_idx_slower_freq_i) > 0 else rs_full
|
|
934
|
+
denom = np.zeros((rps_actual, rps_actual))
|
|
935
|
+
nom = np.zeros((1, rps_actual))
|
|
936
|
+
idx_j_slower = sum(1 for k in idx_iFreq if k < j and (frequencies is None or k >= len(frequencies) or frequencies[k] == freq))
|
|
937
|
+
for t in range(T):
|
|
938
|
+
nan_val = ~nanY[j, t]
|
|
939
|
+
Wt = np.array([[float(nan_val)]]) if np.isscalar(nan_val) else np.diag(nan_val.astype(float))
|
|
940
|
+
if len(bl_idx_slower_freq_i) == 0:
|
|
941
|
+
continue
|
|
942
|
+
valid_bl_idx = bl_idx_slower_freq_i[bl_idx_slower_freq_i < Zsmooth.shape[1]]
|
|
943
|
+
if len(valid_bl_idx) == 0:
|
|
944
|
+
continue
|
|
945
|
+
if t + 1 < Zsmooth.shape[0]:
|
|
946
|
+
Z_row = Zsmooth[t + 1, valid_bl_idx]
|
|
947
|
+
Z_col = Z_row.reshape(-1, 1)
|
|
948
|
+
ZZZ = Z_col @ Z_row.reshape(1, -1)
|
|
949
|
+
valid_vs_idx = valid_bl_idx[valid_bl_idx < vsmooth.shape[0]]
|
|
950
|
+
if len(valid_vs_idx) > 0:
|
|
951
|
+
V_block = vsmooth[np.ix_(valid_vs_idx, valid_vs_idx, [t + 1])]
|
|
952
|
+
V_block = V_block[:, :, 0] if V_block.ndim == 3 else V_block
|
|
953
|
+
if V_block.shape != ZZZ.shape:
|
|
954
|
+
min_size = min(V_block.shape[0], ZZZ.shape[0])
|
|
955
|
+
V_block = V_block[:min_size, :min_size]
|
|
956
|
+
ZZZ = ZZZ[:min_size, :min_size]
|
|
957
|
+
else:
|
|
958
|
+
V_block = np.zeros_like(ZZZ)
|
|
959
|
+
else:
|
|
960
|
+
Z_row = np.zeros(rps_actual)
|
|
961
|
+
Z_col = np.zeros((rps_actual, 1))
|
|
962
|
+
ZZZ = np.zeros((rps_actual, rps_actual))
|
|
963
|
+
V_block = np.zeros((rps_actual, rps_actual))
|
|
964
|
+
if Wt.shape == (1, 1):
|
|
965
|
+
denom += (ZZZ + V_block) * Wt[0, 0]
|
|
966
|
+
else:
|
|
967
|
+
denom += np.kron(ZZZ + V_block, Wt)
|
|
968
|
+
nom += y_clean[j, t] * Z_row.reshape(1, -1)
|
|
969
|
+
try:
|
|
970
|
+
scale_factor = _get_config_param(config, "regularization_scale", 1e-5)
|
|
971
|
+
warn_reg = _get_config_param(config, "warn_on_regularization", True)
|
|
972
|
+
reg_param, _ = _compute_regularization_param(denom, scale_factor, warn_reg)
|
|
973
|
+
denom_reg = denom + np.eye(denom.shape[0]) * reg_param
|
|
974
|
+
C_i = inv(denom_reg) @ nom.T
|
|
975
|
+
C_i = _clean_matrix(C_i, 'loading', default_nan=0.0, default_inf=0.0)
|
|
976
|
+
C_scale = np.std(C_i[C_i != 0]) if np.any(C_i != 0) else 1.0
|
|
977
|
+
C_max = max(10.0, C_scale * 5)
|
|
978
|
+
C_i = np.clip(C_i, -C_max, C_max)
|
|
979
|
+
if len(bl_idx_slower_freq_i) > 0:
|
|
980
|
+
C_update = C_i.flatten()[:len(bl_idx_slower_freq_i)]
|
|
981
|
+
for k, col_idx in enumerate(bl_idx_slower_freq_i):
|
|
982
|
+
C_new[j, col_idx] = C_update[k]
|
|
983
|
+
except _NUMERICAL_EXCEPTIONS:
|
|
984
|
+
pass
|
|
985
|
+
|
|
986
|
+
R_diag = np.zeros(n)
|
|
987
|
+
n_obs_per_series = np.zeros(n, dtype=int)
|
|
988
|
+
for t in range(T):
|
|
989
|
+
Z_t = Zsmooth[t + 1, :].reshape(-1, 1)
|
|
990
|
+
vsmooth_t = vsmooth[:, :, t + 1]
|
|
991
|
+
y_pred = (C_new @ Z_t).flatten()
|
|
992
|
+
for i in range(n):
|
|
993
|
+
if np.isnan(y[i, t]):
|
|
994
|
+
continue
|
|
995
|
+
n_obs_per_series[i] += 1
|
|
996
|
+
resid_sq = (y[i, t] - y_pred[i]) ** 2
|
|
997
|
+
C_i = C_new[i, :].reshape(1, -1)
|
|
998
|
+
var_factor = (C_i @ vsmooth_t @ C_i.T)[0, 0]
|
|
999
|
+
R_diag[i] += resid_sq + var_factor
|
|
1000
|
+
n_obs_per_series = np.maximum(n_obs_per_series, 1)
|
|
1001
|
+
R_diag = R_diag / n_obs_per_series
|
|
1002
|
+
mean_var = np.mean(R_diag[R_diag > 0]) if np.any(R_diag > 0) else 1e-4
|
|
1003
|
+
min_var = np.maximum(mean_var * 1e-6, 1e-8)
|
|
1004
|
+
R_diag = np.maximum(R_diag, min_var)
|
|
1005
|
+
valid_mask = np.isfinite(R_diag) & (R_diag > 0)
|
|
1006
|
+
if np.any(valid_mask):
|
|
1007
|
+
median_var = np.median(R_diag[valid_mask])
|
|
1008
|
+
R_diag = np.where(valid_mask, R_diag, median_var)
|
|
1009
|
+
else:
|
|
1010
|
+
R_diag.fill(1e-4)
|
|
1011
|
+
R_new = np.diag(R_diag)
|
|
1012
|
+
Q_new = _clean_matrix(Q_new, 'covariance', default_nan=0.0)
|
|
1013
|
+
min_eigenval = _get_config_param(config, "min_eigenvalue", 1e-8)
|
|
1014
|
+
warn_reg = _get_config_param(config, "warn_on_regularization", True)
|
|
1015
|
+
max_eigenval = _get_config_param(config, "max_eigenvalue", 1e6)
|
|
1016
|
+
Q_new, _ = _ensure_positive_definite(Q_new, min_eigenval, warn_reg)
|
|
1017
|
+
Q_new = _cap_max_eigenvalue(Q_new, max_eigenval=max_eigenval)
|
|
1018
|
+
V_0_new = _clean_matrix(V_0_new, 'covariance', default_nan=0.0)
|
|
1019
|
+
V_0_new, _ = _ensure_positive_definite(V_0_new, min_eigenval, warn_reg)
|
|
1020
|
+
return C_new, R_new, A_new, Q_new, Z_0, V_0_new, loglik
|
|
1021
|
+
|
|
1022
|
+
|