dfm-python 0.6.0__tar.gz → 0.6.11__tar.gz

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.
Files changed (132) hide show
  1. {dfm_python-0.6.0 → dfm_python-0.6.11}/PKG-INFO +1 -1
  2. {dfm_python-0.6.0 → dfm_python-0.6.11}/config/dfm_finance.yaml +1 -1
  3. {dfm_python-0.6.0 → dfm_python-0.6.11}/config/dfm_macro.yaml +1 -1
  4. dfm_python-0.6.11/failure_reports/finance_failure_20260127_183841.json +22 -0
  5. dfm_python-0.6.11/failure_reports/macro_failure_20260127_183842.json +15 -0
  6. dfm_python-0.6.11/models/dfm_finance.pkl +0 -0
  7. dfm_python-0.6.11/models/dfm_macro.pkl +0 -0
  8. {dfm_python-0.6.0 → dfm_python-0.6.11}/pyproject.toml +1 -1
  9. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/dfm/dfm.py +37 -7
  10. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/ssm/kalman.py +125 -23
  11. dfm_python-0.6.11/tutorial/tutorial_exchange_dfm.py +205 -0
  12. dfm_python-0.6.11/tutorial/tutorial_finance_dfm.py +234 -0
  13. dfm_python-0.6.11/tutorial/tutorial_macro_dfm.py +205 -0
  14. {dfm_python-0.6.0 → dfm_python-0.6.11}/uv.lock +1 -1
  15. dfm_python-0.6.0/tutorial/tutorial_exchange_dfm.py +0 -57
  16. dfm_python-0.6.0/tutorial/tutorial_finance_dfm.py +0 -84
  17. dfm_python-0.6.0/tutorial/tutorial_macro_dfm.py +0 -56
  18. {dfm_python-0.6.0 → dfm_python-0.6.11}/.cursor/agents/test-runner.md +0 -0
  19. {dfm_python-0.6.0 → dfm_python-0.6.11}/.git +0 -0
  20. {dfm_python-0.6.0 → dfm_python-0.6.11}/.gitignore +0 -0
  21. {dfm_python-0.6.0 → dfm_python-0.6.11}/.python-version +0 -0
  22. {dfm_python-0.6.0 → dfm_python-0.6.11}/=0.11.0 +0 -0
  23. {dfm_python-0.6.0 → dfm_python-0.6.11}/MANIFEST.in +0 -0
  24. {dfm_python-0.6.0 → dfm_python-0.6.11}/config/ddfm_exchange.yaml +0 -0
  25. {dfm_python-0.6.0 → dfm_python-0.6.11}/config/ddfm_finance.yaml +0 -0
  26. {dfm_python-0.6.0 → dfm_python-0.6.11}/config/ddfm_macro.yaml +0 -0
  27. {dfm_python-0.6.0 → dfm_python-0.6.11}/config/dfm_exchange.yaml +0 -0
  28. {dfm_python-0.6.0 → dfm_python-0.6.11}/data/exchange_rate.csv +0 -0
  29. {dfm_python-0.6.0 → dfm_python-0.6.11}/data/finance.csv +0 -0
  30. {dfm_python-0.6.0 → dfm_python-0.6.11}/data/macro.csv +0 -0
  31. {dfm_python-0.6.0 → dfm_python-0.6.11}/models/dfm_exchange.pkl +0 -0
  32. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/__init__.py +0 -0
  33. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/__init__.py +0 -0
  34. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/adapter.py +0 -0
  35. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/constants.py +0 -0
  36. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/schema/__init__.py +0 -0
  37. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/schema/block.py +0 -0
  38. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/schema/model.py +0 -0
  39. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/schema/params.py +0 -0
  40. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/schema/results.py +0 -0
  41. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/config/types.py +0 -0
  42. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/dataset/__init__.py +0 -0
  43. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/dataset/ddfm_dataset.py +0 -0
  44. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/dataset/dfm_dataset.py +0 -0
  45. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/dataset/time.py +0 -0
  46. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/decoder/__init__.py +0 -0
  47. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/decoder/base.py +0 -0
  48. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/decoder/linear.py +0 -0
  49. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/decoder/mlp.py +0 -0
  50. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/encoder/__init__.py +0 -0
  51. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/encoder/base.py +0 -0
  52. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/encoder/pca.py +0 -0
  53. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/encoder/simple_autoencoder.py +0 -0
  54. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/functional/__init__.py +0 -0
  55. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/functional/em.py +0 -0
  56. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/logger/__init__.py +0 -0
  57. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/logger/dfm_logger.py +0 -0
  58. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/logger/inference_logger.py +0 -0
  59. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/logger/logger.py +0 -0
  60. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/logger/train_logger.py +0 -0
  61. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/__init__.py +0 -0
  62. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/base.py +0 -0
  63. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/ddfm/ddfm.py +0 -0
  64. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/dfm/__init__.py +0 -0
  65. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/dfm/initialization.py +0 -0
  66. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/dfm/mixed_freq.py +0 -0
  67. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/models/dfm/tent.py +0 -0
  68. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/numeric/builder.py +0 -0
  69. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/numeric/estimator.py +0 -0
  70. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/numeric/stability.py +0 -0
  71. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/numeric/statistic.py +0 -0
  72. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/numeric/validator.py +0 -0
  73. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/ssm/__init__.py +0 -0
  74. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/__init__.py +0 -0
  75. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/checkpoint.py +0 -0
  76. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/errors.py +0 -0
  77. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/helper.py +0 -0
  78. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/loss.py +0 -0
  79. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/metric.py +0 -0
  80. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/misc.py +0 -0
  81. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/dfm_python/utils/validation.py +0 -0
  82. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/__init__.py +0 -0
  83. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/__init__.py +0 -0
  84. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/schema/__init__.py +0 -0
  85. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/schema/test_block.py +0 -0
  86. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/schema/test_model.py +0 -0
  87. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/schema/test_results.py +0 -0
  88. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/test_adapter.py +0 -0
  89. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/test_constants.py +0 -0
  90. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/test_tent_weights_config.py +0 -0
  91. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/config/test_types.py +0 -0
  92. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/conftest.py +0 -0
  93. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/dataset/test_ddfm_dataset.py +0 -0
  94. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/dataset/test_dfm_dataset.py +0 -0
  95. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/decoder/__init__.py +0 -0
  96. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/decoder/test_linear.py +0 -0
  97. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/decoder/test_mlp.py +0 -0
  98. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/encoder/__init__.py +0 -0
  99. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/encoder/test_base.py +0 -0
  100. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/encoder/test_pca.py +0 -0
  101. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/encoder/test_simple_autoencoder.py +0 -0
  102. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/functional/__init__.py +0 -0
  103. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/functional/test_em.py +0 -0
  104. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/functional/test_em_correctness.py +0 -0
  105. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/functional/test_em_properties.py +0 -0
  106. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/functional/test_invariance.py +0 -0
  107. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/functional/test_macro_overflow.py +0 -0
  108. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/logger/__init__.py +0 -0
  109. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/logger/test_inference_logger.py +0 -0
  110. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/logger/test_logger.py +0 -0
  111. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/logger/test_train_logger.py +0 -0
  112. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/models/__init__.py +0 -0
  113. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/models/test_ddfm.py +0 -0
  114. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/models/test_dfm.py +0 -0
  115. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/numeric/__init__.py +0 -0
  116. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/numeric/test_builder.py +0 -0
  117. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/numeric/test_cov_rowvar_regression.py +0 -0
  118. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/numeric/test_estimator.py +0 -0
  119. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/numeric/test_initialization.py +0 -0
  120. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/numeric/test_stability.py +0 -0
  121. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/numeric/test_validator.py +0 -0
  122. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/ssm/__init__.py +0 -0
  123. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/ssm/test_kalman.py +0 -0
  124. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/ssm/test_kalman_correctness.py +0 -0
  125. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/utils/__init__.py +0 -0
  126. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/utils/test_errors.py +0 -0
  127. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/utils/test_helper.py +0 -0
  128. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/utils/test_metric.py +0 -0
  129. {dfm_python-0.6.0 → dfm_python-0.6.11}/src/test/utils/test_misc.py +0 -0
  130. {dfm_python-0.6.0 → dfm_python-0.6.11}/tutorial/tutorial_exchange_ddfm.py +0 -0
  131. {dfm_python-0.6.0 → dfm_python-0.6.11}/tutorial/tutorial_finance_ddfm.py +0 -0
  132. {dfm_python-0.6.0 → dfm_python-0.6.11}/tutorial/tutorial_macro_ddfm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.6.0
3
+ Version: 0.6.11
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
@@ -7,7 +7,7 @@
7
7
 
8
8
  # EM Algorithm Parameters
9
9
  threshold: 1.0e-05 # EM convergence threshold
10
- max_iter: 50 # Maximum EM iterations
10
+ max_iter: 12 # Maximum EM iterations
11
11
 
12
12
  # Data Handling
13
13
  clock: d # Daily clock frequency (finance.csv uses daily index via date_id)
@@ -6,7 +6,7 @@
6
6
 
7
7
  # EM Algorithm Parameters
8
8
  threshold: 1.0e-05 # EM convergence threshold
9
- max_iter: 50 # Maximum EM iterations
9
+ max_iter: 12 # Maximum EM iterations
10
10
 
11
11
  # Numerical Stability Parameters
12
12
  clip_ar_coefficients: true
@@ -0,0 +1,22 @@
1
+ {
2
+ "experiment": "finance",
3
+ "step": "Step 2: Creating Dataset",
4
+ "timestamp": "20260127_183841",
5
+ "error_type": "AttributeError",
6
+ "error_message": "'DFMDataset' object has no attribute 'obs_dim'",
7
+ "traceback": "Traceback (most recent call last):\n File \"/data/nowcasting-kr/dfm-python/tutorial/tutorial_finance_dfm.py\", line 150, in main\n print(f\" Dataset created: obs_dim={dataset.obs_dim}, state_dim={dataset.state_dim if hasattr(dataset, 'state_dim') else 'N/A'}\")\n ^^^^^^^^^^^^^^^\nAttributeError: 'DFMDataset' object has no attribute 'obs_dim'\n",
8
+ "context": {
9
+ "config_max_iter": 12,
10
+ "config_blocks": {
11
+ "Block_ALL": null,
12
+ "Block_EI": null,
13
+ "Block_MD": null,
14
+ "Block_PSV": null
15
+ },
16
+ "data_shape": [
17
+ 1000,
18
+ 99
19
+ ],
20
+ "filtered_series_count": 27
21
+ }
22
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "experiment": "macro",
3
+ "step": "Step 2: Creating Dataset",
4
+ "timestamp": "20260127_183842",
5
+ "error_type": "AttributeError",
6
+ "error_message": "'DFMDataset' object has no attribute 'obs_dim'",
7
+ "traceback": "Traceback (most recent call last):\n File \"/data/nowcasting-kr/dfm-python/tutorial/tutorial_macro_dfm.py\", line 123, in main\n print(f\" Dataset created: obs_dim={dataset.obs_dim}, state_dim={dataset.state_dim if hasattr(dataset, 'state_dim') else 'N/A'}\")\n ^^^^^^^^^^^^^^^\nAttributeError: 'DFMDataset' object has no attribute 'obs_dim'\n",
8
+ "context": {
9
+ "config_max_iter": 12,
10
+ "data_shape": [
11
+ 2538,
12
+ 15
13
+ ]
14
+ }
15
+ }
Binary file
Binary file
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dfm-python"
3
- version = "0.6.0"
3
+ version = "0.6.11"
4
4
  description = "Dynamic Factor Model (DFM) estimation and forecasting in Python"
5
5
  requires-python = ">=3.10"
6
6
  license = {text = "MIT"}
@@ -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
- kalman_final = self._create_kalman_filter()
543
- y_masked = np.ma.masked_invalid(self.data_processed)
544
- # Use filter_and_smooth() with compute_loglik=False to save time (log-likelihood already known)
545
- # This prevents SVD convergence failures with ill-conditioned covariance matrices
546
- smoothed_state_means, _, _, _ = kalman_final.filter_and_smooth(y_masked, compute_loglik=False)
547
-
548
- return smoothed_state_means
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.
@@ -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
- self._pykalman.transition_matrices,
391
- self._pykalman.observation_matrices,
392
- self._pykalman.transition_covariance,
393
- self._pykalman.observation_covariance,
444
+ A,
445
+ C,
446
+ Q,
447
+ R,
394
448
  transition_offsets_final,
395
449
  observation_offsets_final,
396
- self._pykalman.initial_state_mean,
397
- self._pykalman.initial_state_covariance,
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 V_0..."
458
+ f"Retrying with more aggressively stabilized parameters..."
405
459
  )
406
- # Retry with more aggressively stabilized V_0
407
- V_0_retry = self._pykalman.initial_state_covariance.copy()
408
- # Cap at smaller value for retry
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
- # Also ensure Q is well-bounded
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
- return _filter(
415
- self._pykalman.transition_matrices,
416
- self._pykalman.observation_matrices,
417
- Q_retry,
418
- self._pykalman.observation_covariance,
419
- transition_offsets_final,
420
- observation_offsets_final,
421
- self._pykalman.initial_state_mean,
422
- V_0_retry,
423
- observations
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
 
@@ -0,0 +1,205 @@
1
+ """Tutorial: DFM for Exchange Data
2
+
3
+ This tutorial demonstrates the complete workflow for training and prediction
4
+ using exchange rate data.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+ import traceback
10
+ import json
11
+ from datetime import datetime
12
+
13
+ project_root = Path(__file__).parent.parent
14
+ sys.path.insert(0, str(project_root / "src"))
15
+
16
+ import pandas as pd
17
+ import numpy as np
18
+ from sklearn.preprocessing import StandardScaler
19
+ from dfm_python import DFM, DFMDataset
20
+ from dfm_python.config import DFMConfig
21
+ import hydra
22
+ from omegaconf import DictConfig, OmegaConf
23
+
24
+ def save_failure_report(step: str, error: Exception, context: dict, project_root: Path):
25
+ """Save detailed failure report to file."""
26
+ reports_dir = project_root / "failure_reports"
27
+ reports_dir.mkdir(exist_ok=True)
28
+
29
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
30
+ report_file = reports_dir / f"exchange_failure_{timestamp}.json"
31
+
32
+ report = {
33
+ "experiment": "exchange",
34
+ "step": step,
35
+ "timestamp": timestamp,
36
+ "error_type": type(error).__name__,
37
+ "error_message": str(error),
38
+ "traceback": traceback.format_exc(),
39
+ "context": context
40
+ }
41
+
42
+ with open(report_file, 'w') as f:
43
+ json.dump(report, f, indent=2, default=str)
44
+
45
+ print(f"\n[FAILURE] Error report saved to: {report_file}")
46
+ return report_file
47
+
48
+ def check_model_state(model, step: str):
49
+ """Check model state for numerical issues and return diagnostics."""
50
+ diagnostics = {}
51
+
52
+ if model is None:
53
+ return diagnostics
54
+
55
+ try:
56
+ if hasattr(model, 'training_state') and model.training_state is not None:
57
+ state = model.training_state
58
+ diagnostics['training_state'] = {}
59
+
60
+ for param_name in ['A', 'C', 'Q', 'R', 'V_0']:
61
+ if hasattr(state, param_name):
62
+ param = getattr(state, param_name)
63
+ if param is not None:
64
+ diagnostics['training_state'][param_name] = {
65
+ 'shape': param.shape if hasattr(param, 'shape') else None,
66
+ 'has_nan': bool(np.any(np.isnan(param))) if isinstance(param, np.ndarray) else None,
67
+ 'has_inf': bool(np.any(np.isinf(param))) if isinstance(param, np.ndarray) else None,
68
+ 'min': float(np.nanmin(param)) if isinstance(param, np.ndarray) else None,
69
+ 'max': float(np.nanmax(param)) if isinstance(param, np.ndarray) else None,
70
+ }
71
+
72
+ if hasattr(model, 'result') and model.result is not None:
73
+ result = model.result
74
+ diagnostics['result'] = {
75
+ 'converged': result.converged if hasattr(result, 'converged') else None,
76
+ 'num_iter': result.num_iter if hasattr(result, 'num_iter') else None,
77
+ 'loglik': float(result.loglik) if hasattr(result, 'loglik') and result.loglik is not None else None,
78
+ }
79
+
80
+ if hasattr(model, 'data_processed') and model.data_processed is not None:
81
+ data = model.data_processed
82
+ diagnostics['data_processed'] = {
83
+ 'shape': data.shape if hasattr(data, 'shape') else None,
84
+ 'has_nan': bool(np.any(np.isnan(data))) if isinstance(data, np.ndarray) else None,
85
+ 'has_inf': bool(np.any(np.isinf(data))) if isinstance(data, np.ndarray) else None,
86
+ }
87
+ except Exception as e:
88
+ diagnostics['diagnostic_error'] = f"Failed to collect diagnostics: {str(e)}"
89
+
90
+ return diagnostics
91
+
92
+ @hydra.main(version_base=None, config_path="../config", config_name="dfm_exchange")
93
+ def main(cfg: DictConfig) -> None:
94
+ print("=" * 80)
95
+ print("DFM Tutorial: Exchange Data")
96
+ print("=" * 80)
97
+
98
+ start_time = datetime.now()
99
+ model = None
100
+ dataset = None
101
+ config = None
102
+ df = None
103
+
104
+ try:
105
+ print("\n[Step 1] Loading data...")
106
+ try:
107
+ df = pd.read_csv(project_root / "data" / "exchange_rate.csv")
108
+ if "date" in df.columns:
109
+ df["date"] = pd.to_datetime(df["date"])
110
+ print(f" Data shape: {df.shape}")
111
+ except Exception as e:
112
+ context = {
113
+ "data_shape": df.shape if df is not None else None,
114
+ "data_columns": list(df.columns) if df is not None else None,
115
+ }
116
+ save_failure_report("Step 1: Loading data", e, context, project_root)
117
+ raise
118
+
119
+ try:
120
+ print("\n[Step 2] Creating Dataset...")
121
+ config = DFMConfig.from_dict(OmegaConf.to_container(cfg, resolve=True))
122
+ dataset = DFMDataset(config=config, data=df, time_index="date")
123
+ obs_dim = len(dataset.variables.columns)
124
+ print(f" Dataset created: obs_dim={obs_dim}")
125
+ except Exception as e:
126
+ context = {
127
+ "config_max_iter": cfg.get('max_iter', None),
128
+ "data_shape": df.shape if df is not None else None,
129
+ }
130
+ save_failure_report("Step 2: Creating Dataset", e, context, project_root)
131
+ raise
132
+
133
+ try:
134
+ print("\n[Step 3] Training DFM model...")
135
+ model = DFM(dataset=dataset, config=config, scaler=StandardScaler())
136
+ model.fit()
137
+
138
+ result = model.result
139
+ print(f" Converged: {result.converged}, Iterations: {result.num_iter}")
140
+
141
+ # Check for numerical issues after training
142
+ diagnostics = check_model_state(model, "after_training")
143
+ if diagnostics.get('training_state'):
144
+ for param_name, param_info in diagnostics['training_state'].items():
145
+ if param_info.get('has_nan') or param_info.get('has_inf'):
146
+ print(f" WARNING: {param_name} contains NaN or Inf!")
147
+ except Exception as e:
148
+ context = {
149
+ "config_max_iter": cfg.get('max_iter', None),
150
+ "dataset_obs_dim": dataset.obs_dim if dataset is not None else None,
151
+ "elapsed_time": str(datetime.now() - start_time),
152
+ }
153
+ diagnostics = check_model_state(model, "during_training")
154
+ context.update(diagnostics)
155
+ save_failure_report("Step 3: Training DFM model", e, context, project_root)
156
+ raise
157
+
158
+ try:
159
+ print("\n[Step 4] Making predictions...")
160
+ X_forecast, Z_forecast = model.predict(horizon=6)
161
+ print(f" Forecast shape: {X_forecast.shape}")
162
+
163
+ # Check forecast for issues
164
+ if np.any(np.isnan(X_forecast)) or np.any(np.isinf(X_forecast)):
165
+ print(f" WARNING: Forecast contains NaN or Inf!")
166
+ except Exception as e:
167
+ context = {
168
+ "result_converged": result.converged if 'result' in locals() else None,
169
+ "result_iterations": result.num_iter if 'result' in locals() else None,
170
+ }
171
+ diagnostics = check_model_state(model, "during_prediction")
172
+ context.update(diagnostics)
173
+ save_failure_report("Step 4: Making predictions", e, context, project_root)
174
+ raise
175
+
176
+ try:
177
+ print("\n[Step 5] Saving model...")
178
+ model_path = project_root / "models" / "dfm_exchange.pkl"
179
+ model_path.parent.mkdir(exist_ok=True)
180
+ model.save(model_path)
181
+ print(f" Model saved to: {model_path}")
182
+ except Exception as e:
183
+ context = {
184
+ "model_path": str(model_path) if 'model_path' in locals() else None,
185
+ "result_converged": result.converged if 'result' in locals() else None,
186
+ }
187
+ diagnostics = check_model_state(model, "during_saving")
188
+ context.update(diagnostics)
189
+ save_failure_report("Step 5: Saving model", e, context, project_root)
190
+ raise
191
+
192
+ print("\n" + result.summary())
193
+
194
+ elapsed = datetime.now() - start_time
195
+ print(f"\n[SUCCESS] Tutorial completed in {elapsed}")
196
+
197
+ except Exception as e:
198
+ elapsed = datetime.now() - start_time
199
+ print(f"\n[FAILURE] Tutorial failed after {elapsed}")
200
+ print(f"Error: {type(e).__name__}: {str(e)}")
201
+ raise
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()