dfm-python 0.6.0__py3-none-any.whl → 0.6.1__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/models/dfm/dfm.py
CHANGED
|
@@ -539,13 +539,43 @@ class DFM(BaseFactorModel):
|
|
|
539
539
|
|
|
540
540
|
# Fallback: compute smoothed factors (should rarely happen if EM completed successfully)
|
|
541
541
|
_logger.debug("Computing smoothed factors (cache not available)")
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
542
|
+
try:
|
|
543
|
+
kalman_final = self._create_kalman_filter()
|
|
544
|
+
y_masked = np.ma.masked_invalid(self.data_processed)
|
|
545
|
+
# Use filter_and_smooth() with compute_loglik=False to save time (log-likelihood already known)
|
|
546
|
+
# This prevents SVD convergence failures with ill-conditioned covariance matrices
|
|
547
|
+
smoothed_state_means, _, _, _ = kalman_final.filter_and_smooth(y_masked, compute_loglik=False)
|
|
548
|
+
return smoothed_state_means
|
|
549
|
+
except (ValueError, FloatingPointError, OverflowError) as e:
|
|
550
|
+
if "infs or NaNs" in str(e) or "overflow" in str(e).lower() or "not contain infs" in str(e):
|
|
551
|
+
# Parameters are too unstable to recompute factors
|
|
552
|
+
# This can happen if EM converged to numerically unstable parameters
|
|
553
|
+
# Raise a clear error with diagnostic information
|
|
554
|
+
param_status = {}
|
|
555
|
+
if self.training_state is not None:
|
|
556
|
+
for name, param in [
|
|
557
|
+
('A', self.training_state.A),
|
|
558
|
+
('C', self.training_state.C),
|
|
559
|
+
('Q', self.training_state.Q),
|
|
560
|
+
('R', self.training_state.R),
|
|
561
|
+
('V_0', self.training_state.V_0),
|
|
562
|
+
]:
|
|
563
|
+
if param is not None:
|
|
564
|
+
n_inf = np.sum(np.isinf(param))
|
|
565
|
+
n_nan = np.sum(np.isnan(param))
|
|
566
|
+
if n_inf > 0 or n_nan > 0:
|
|
567
|
+
param_status[name] = f"{n_inf} Inf, {n_nan} NaN"
|
|
568
|
+
|
|
569
|
+
error_msg = (
|
|
570
|
+
f"Cannot compute smoothed factors: final parameters contain non-finite values. "
|
|
571
|
+
f"This indicates numerical instability in the fitted model. "
|
|
572
|
+
f"Parameter status: {param_status if param_status else 'all finite'}. "
|
|
573
|
+
f"Consider: (1) reducing model complexity (fewer factors/blocks), "
|
|
574
|
+
f"(2) increasing regularization, (3) checking data scaling."
|
|
575
|
+
)
|
|
576
|
+
raise NumericalError(error_msg, details=f"Original error: {type(e).__name__}: {e}") from e
|
|
577
|
+
else:
|
|
578
|
+
raise
|
|
549
579
|
|
|
550
580
|
def get_result(self) -> DFMResult:
|
|
551
581
|
"""Extract DFMResult from trained model.
|
dfm_python/ssm/kalman.py
CHANGED
|
@@ -384,44 +384,146 @@ class DFMKalmanFilter:
|
|
|
384
384
|
f"observation_offsets shape mismatch: expected ({N},), got {observation_offsets_final.shape}"
|
|
385
385
|
)
|
|
386
386
|
|
|
387
|
+
# Validate parameters for NaN/Inf before calling _filter (prevents cryptic errors)
|
|
388
|
+
A = self._pykalman.transition_matrices
|
|
389
|
+
C = self._pykalman.observation_matrices
|
|
390
|
+
Q = self._pykalman.transition_covariance
|
|
391
|
+
R = self._pykalman.observation_covariance
|
|
392
|
+
Z_0 = self._pykalman.initial_state_mean
|
|
393
|
+
V_0 = self._pykalman.initial_state_covariance
|
|
394
|
+
|
|
395
|
+
# Check for non-finite values in parameters
|
|
396
|
+
param_checks = {
|
|
397
|
+
'A (transition)': A,
|
|
398
|
+
'C (observation)': C,
|
|
399
|
+
'Q (process noise)': Q,
|
|
400
|
+
'R (observation noise)': R,
|
|
401
|
+
'Z_0 (initial state mean)': Z_0,
|
|
402
|
+
'V_0 (initial state cov)': V_0,
|
|
403
|
+
}
|
|
404
|
+
non_finite_params = []
|
|
405
|
+
for name, param in param_checks.items():
|
|
406
|
+
if param is not None and not np.isfinite(param).all():
|
|
407
|
+
n_inf = np.sum(np.isinf(param)) if param is not None else 0
|
|
408
|
+
n_nan = np.sum(np.isnan(param)) if param is not None else 0
|
|
409
|
+
non_finite_params.append(f"{name}: {n_inf} Inf, {n_nan} NaN")
|
|
410
|
+
|
|
411
|
+
if non_finite_params:
|
|
412
|
+
# Try to recover by stabilizing parameters
|
|
413
|
+
_logger.warning(
|
|
414
|
+
f" Filter: Non-finite values detected in parameters: {', '.join(non_finite_params)}. "
|
|
415
|
+
f"Attempting to stabilize before filtering..."
|
|
416
|
+
)
|
|
417
|
+
# Stabilize all covariance matrices
|
|
418
|
+
if Q is not None and not np.isfinite(Q).all():
|
|
419
|
+
Q = ensure_symmetric(Q)
|
|
420
|
+
Q = np.where(np.isfinite(Q), Q, 0.0)
|
|
421
|
+
Q = cap_max_eigenval(Q, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
|
|
422
|
+
if R is not None and not np.isfinite(R).all():
|
|
423
|
+
R = ensure_symmetric(R)
|
|
424
|
+
R = np.where(np.isfinite(R), R, np.diag(np.diag(R))) # Preserve diagonal structure
|
|
425
|
+
# Ensure R diagonal is positive
|
|
426
|
+
R_diag = np.diag(R)
|
|
427
|
+
R_diag = np.where(np.isfinite(R_diag) & (R_diag > 0), R_diag, 1.0) # Fallback to 1.0
|
|
428
|
+
np.fill_diagonal(R, R_diag)
|
|
429
|
+
R = cap_max_eigenval(R, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
|
|
430
|
+
if V_0 is not None and not np.isfinite(V_0).all():
|
|
431
|
+
V_0 = ensure_symmetric(V_0)
|
|
432
|
+
V_0 = np.where(np.isfinite(V_0), V_0, 0.0)
|
|
433
|
+
V_0 = cap_max_eigenval(V_0, max_eigenval=MAX_EIGENVALUE / 10, symmetric=True, warn=False)
|
|
434
|
+
if A is not None and not np.isfinite(A).all():
|
|
435
|
+
A = np.where(np.isfinite(A), A, 0.0)
|
|
436
|
+
if C is not None and not np.isfinite(C).all():
|
|
437
|
+
C = np.where(np.isfinite(C), C, 0.0)
|
|
438
|
+
if Z_0 is not None and not np.isfinite(Z_0).all():
|
|
439
|
+
Z_0 = np.where(np.isfinite(Z_0), Z_0, 0.0)
|
|
440
|
+
|
|
387
441
|
# Option 4: Try filter with current parameters, retry with more aggressive stabilization if overflow
|
|
388
442
|
try:
|
|
389
443
|
return _filter(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
444
|
+
A,
|
|
445
|
+
C,
|
|
446
|
+
Q,
|
|
447
|
+
R,
|
|
394
448
|
transition_offsets_final,
|
|
395
449
|
observation_offsets_final,
|
|
396
|
-
|
|
397
|
-
|
|
450
|
+
Z_0,
|
|
451
|
+
V_0,
|
|
398
452
|
observations
|
|
399
453
|
)
|
|
400
454
|
except (ValueError, FloatingPointError, OverflowError) as e:
|
|
401
455
|
if "infs or NaNs" in str(e) or "overflow" in str(e).lower():
|
|
402
456
|
_logger.warning(
|
|
403
457
|
f" Filter: Overflow detected in _filter ({type(e).__name__}: {e}). "
|
|
404
|
-
f"Retrying with more aggressively stabilized
|
|
458
|
+
f"Retrying with more aggressively stabilized parameters..."
|
|
405
459
|
)
|
|
406
|
-
# Retry with more aggressively stabilized
|
|
407
|
-
V_0_retry = self._pykalman.initial_state_covariance.copy()
|
|
408
|
-
|
|
460
|
+
# Retry with more aggressively stabilized parameters
|
|
461
|
+
V_0_retry = V_0.copy() if V_0 is not None else self._pykalman.initial_state_covariance.copy()
|
|
462
|
+
V_0_retry = ensure_symmetric(V_0_retry)
|
|
463
|
+
V_0_retry = np.where(np.isfinite(V_0_retry), V_0_retry, 0.0)
|
|
409
464
|
V_0_retry = cap_max_eigenval(V_0_retry, max_eigenval=MAX_EIGENVALUE / 10, symmetric=True, warn=False)
|
|
410
|
-
|
|
411
|
-
Q_retry = self._pykalman.transition_covariance.copy()
|
|
465
|
+
|
|
466
|
+
Q_retry = Q.copy() if Q is not None else self._pykalman.transition_covariance.copy()
|
|
467
|
+
Q_retry = ensure_symmetric(Q_retry)
|
|
468
|
+
Q_retry = np.where(np.isfinite(Q_retry), Q_retry, 0.0)
|
|
412
469
|
Q_retry = cap_max_eigenval(Q_retry, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
|
|
413
470
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
)
|
|
471
|
+
# Also stabilize R (critical: R appears in predicted_observation_covariance = C P C^T + R)
|
|
472
|
+
R_retry = R.copy() if R is not None else self._pykalman.observation_covariance.copy()
|
|
473
|
+
R_retry = ensure_symmetric(R_retry)
|
|
474
|
+
R_retry = np.where(np.isfinite(R_retry), R_retry, np.diag(np.diag(R_retry)))
|
|
475
|
+
# Ensure R diagonal is positive and finite
|
|
476
|
+
R_diag = np.diag(R_retry)
|
|
477
|
+
R_diag = np.where(np.isfinite(R_diag) & (R_diag > 0), R_diag, 1.0)
|
|
478
|
+
np.fill_diagonal(R_retry, R_diag)
|
|
479
|
+
R_retry = cap_max_eigenval(R_retry, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
|
|
480
|
+
|
|
481
|
+
# Also stabilize A if it has eigenvalues > 1 (causes unbounded growth in prediction step)
|
|
482
|
+
# P_{t|t-1} = A P_{t-1|t-1} A^T + Q grows unbounded if A has eigenvalues > 1
|
|
483
|
+
A_retry = A.copy() if A is not None else self._pykalman.transition_matrices.copy()
|
|
484
|
+
if A_retry is not None:
|
|
485
|
+
A_retry = np.where(np.isfinite(A_retry), A_retry, 0.0)
|
|
486
|
+
# Check if A has eigenvalues > 1 (unstable)
|
|
487
|
+
try:
|
|
488
|
+
eigvals = np.linalg.eigvals(A_retry)
|
|
489
|
+
max_eig = np.max(np.abs(eigvals)) # Use absolute value for complex eigenvalues
|
|
490
|
+
if max_eig > 0.99: # Close to or above 1.0
|
|
491
|
+
_logger.warning(
|
|
492
|
+
f" Filter: A has max |eigenvalue| {max_eig:.2e} > 0.99, capping to 0.99 to prevent unbounded growth"
|
|
493
|
+
)
|
|
494
|
+
A_retry = cap_max_eigenval(A_retry, max_eigenval=0.99, symmetric=False, warn=False)
|
|
495
|
+
except (np.linalg.LinAlgError, ValueError):
|
|
496
|
+
# If eigendecomposition fails, just ensure finite and scale down
|
|
497
|
+
_logger.warning(" Filter: Could not compute A eigenvalues, scaling A by 0.9 as safety measure")
|
|
498
|
+
A_retry = A_retry * 0.9
|
|
499
|
+
else:
|
|
500
|
+
A_retry = A
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
return _filter(
|
|
504
|
+
A_retry,
|
|
505
|
+
C,
|
|
506
|
+
Q_retry,
|
|
507
|
+
R_retry,
|
|
508
|
+
transition_offsets_final,
|
|
509
|
+
observation_offsets_final,
|
|
510
|
+
Z_0,
|
|
511
|
+
V_0_retry,
|
|
512
|
+
observations
|
|
513
|
+
)
|
|
514
|
+
except (ValueError, FloatingPointError, OverflowError) as e2:
|
|
515
|
+
# Even retry failed - parameters are too unstable
|
|
516
|
+
_logger.error(
|
|
517
|
+
f" Filter: Retry also failed with {type(e2).__name__}: {e2}. "
|
|
518
|
+
f"Parameters are too unstable to filter. This indicates severe numerical issues."
|
|
519
|
+
)
|
|
520
|
+
raise NumericalError(
|
|
521
|
+
"Kalman filter failed even after parameter stabilization. "
|
|
522
|
+
"Model parameters contain extreme values that cannot be filtered. "
|
|
523
|
+
"This typically indicates: (1) model is too complex for the data, "
|
|
524
|
+
"(2) data scaling issues, (3) numerical instability in EM convergence.",
|
|
525
|
+
details=f"Original error: {type(e).__name__}: {e}, Retry error: {type(e2).__name__}: {e2}"
|
|
526
|
+
) from e2
|
|
425
527
|
else:
|
|
426
528
|
raise
|
|
427
529
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Dynamic Factor Model (DFM) estimation and forecasting in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/dfm-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/yourusername/dfm-python#readme
|
|
@@ -31,7 +31,7 @@ dfm_python/models/__init__.py,sha256=exbooS9GFktHEMEc3mXhgZDosVDwEpeBOrC5v6wBF4I
|
|
|
31
31
|
dfm_python/models/base.py,sha256=tJR9XD-mvokT9e2YKD7la2eo2clA1n0QwosXWCcZHY4,2504
|
|
32
32
|
dfm_python/models/ddfm/ddfm.py,sha256=Ff0oCZZw-lcbofwCFD7FWaA1I-lKvJdCXnmlTysfPgw,65355
|
|
33
33
|
dfm_python/models/dfm/__init__.py,sha256=6Lwt2R-xPRgWDrFL7txyA-HMp_CJGoFr7qHe91n65xE,1074
|
|
34
|
-
dfm_python/models/dfm/dfm.py,sha256=
|
|
34
|
+
dfm_python/models/dfm/dfm.py,sha256=UhaJ5v-2TBfRzXZ0NSF0vysiFe473_WgcwKT6sRSSn8,47889
|
|
35
35
|
dfm_python/models/dfm/initialization.py,sha256=bWQKfrA4B3_KMoW4dXAzRNZDSvyEH4iQOdbR5znnmH0,55247
|
|
36
36
|
dfm_python/models/dfm/mixed_freq.py,sha256=S5nl7r0G8MMdmFHzJpfc0imHsjUeaOA4gHu_gltSq80,16148
|
|
37
37
|
dfm_python/models/dfm/tent.py,sha256=lNB1TKwb9a5E66TsnFDtTsN3FpU4ar-OeuFC8h1Ar04,15436
|
|
@@ -41,7 +41,7 @@ dfm_python/numeric/stability.py,sha256=9qk9RcXm0mppMdwYH7FPhBHXAp3xfbGkToA_n2crX
|
|
|
41
41
|
dfm_python/numeric/statistic.py,sha256=Y9rrv18OKBiholy-R5Bz9tzGCkaziMn0Jk4Ca_KS08Y,14429
|
|
42
42
|
dfm_python/numeric/validator.py,sha256=9DInNp-DplvM4NoniE3tAS1JidMTGO4k4UMD272-h7s,25891
|
|
43
43
|
dfm_python/ssm/__init__.py,sha256=AmmiJPlAy1bctM9AVso7eWKdPMrUbCfAZQHgqZv-KLY,784
|
|
44
|
-
dfm_python/ssm/kalman.py,sha256=
|
|
44
|
+
dfm_python/ssm/kalman.py,sha256=wQYj_SMRcer8BPNUPKCQMXTJ6JZ1bAvmLV2zZN7GSlw,48235
|
|
45
45
|
dfm_python/utils/__init__.py,sha256=JbtEviPIgUJQANv-FXCUQBvyVMULduZK8aY-yOFIQJU,4740
|
|
46
46
|
dfm_python/utils/checkpoint.py,sha256=npdYsNg74KE7wUtU25Wuqs846oXfwYxUJcEzKnh-k6o,11201
|
|
47
47
|
dfm_python/utils/errors.py,sha256=BKMjYldXjdFg-O_pIBo1-WITInNXvNrHk-6IyFHMf-k,1861
|
|
@@ -50,6 +50,6 @@ dfm_python/utils/loss.py,sha256=1KcYwFJmY9l-Uh6Sk-dFpJ2kgMfm0u41Gmgu-5NzaAs,3878
|
|
|
50
50
|
dfm_python/utils/metric.py,sha256=AC_TnJ3eIXiF_maNNexSHzXpLv5Hrf1W88X5pJx9C-Q,15327
|
|
51
51
|
dfm_python/utils/misc.py,sha256=aPUNTRLhKUUiqEWT1tOiV5AJUck_qzbENA0OK3uCdII,11159
|
|
52
52
|
dfm_python/utils/validation.py,sha256=NFpTS4mq36IDDLmbj4crJGoWLLz_BdlS0tKo96BEoNw,1456
|
|
53
|
-
dfm_python-0.6.
|
|
54
|
-
dfm_python-0.6.
|
|
55
|
-
dfm_python-0.6.
|
|
53
|
+
dfm_python-0.6.1.dist-info/METADATA,sha256=aG5pgOcarTHAPzXgkw3qCyuNenEnTBgu46tirm_4iuM,2269
|
|
54
|
+
dfm_python-0.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
55
|
+
dfm_python-0.6.1.dist-info/RECORD,,
|
|
File without changes
|