dfm-python 0.6.1__tar.gz → 0.6.13__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 (150) hide show
  1. {dfm_python-0.6.1 → dfm_python-0.6.13}/.gitignore +2 -1
  2. {dfm_python-0.6.1 → dfm_python-0.6.13}/PKG-INFO +1 -1
  3. {dfm_python-0.6.1 → dfm_python-0.6.13}/config/ddfm_exchange.yaml +5 -0
  4. {dfm_python-0.6.1 → dfm_python-0.6.13}/config/ddfm_finance.yaml +5 -0
  5. {dfm_python-0.6.1 → dfm_python-0.6.13}/config/ddfm_macro.yaml +5 -0
  6. {dfm_python-0.6.1 → dfm_python-0.6.13}/config/dfm_exchange.yaml +1 -1
  7. {dfm_python-0.6.1 → dfm_python-0.6.13}/config/dfm_finance.yaml +1 -1
  8. {dfm_python-0.6.1 → dfm_python-0.6.13}/config/dfm_macro.yaml +1 -1
  9. {dfm_python-0.6.1 → dfm_python-0.6.13}/pyproject.toml +1 -1
  10. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/__init__.py +1 -1
  11. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/schema/params.py +119 -35
  12. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/schema/results.py +3 -3
  13. dfm_python-0.6.13/src/dfm_python/dataset/ddfm_dataset.py +228 -0
  14. dfm_python-0.6.13/src/dfm_python/functional/cauchy.py +236 -0
  15. dfm_python-0.6.13/src/dfm_python/functional/krylov.py +204 -0
  16. dfm_python-0.6.13/src/dfm_python/layer/mlp.py +272 -0
  17. dfm_python-0.6.13/src/dfm_python/layer/pca.py +304 -0
  18. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/logger/__init__.py +5 -2
  19. dfm_python-0.6.13/src/dfm_python/logger/ddfm_logger.py +23 -0
  20. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/logger/inference_logger.py +1 -8
  21. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/logger/train_logger.py +1 -8
  22. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/base.py +51 -1
  23. dfm_python-0.6.13/src/dfm_python/models/ddfm/__init__.py +12 -0
  24. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/ddfm/ddfm.py +313 -496
  25. dfm_python-0.6.13/src/dfm_python/models/ddfm/decoder.py +263 -0
  26. dfm_python-0.6.1/src/dfm_python/encoder/simple_autoencoder.py → dfm_python-0.6.13/src/dfm_python/models/ddfm/encoder.py +204 -43
  27. dfm_python-0.6.13/src/dfm_python/models/ddfm/sampling.py +442 -0
  28. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/dfm/initialization.py +1 -1
  29. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/dfm/mixed_freq.py +24 -3
  30. dfm_python-0.6.13/src/dfm_python/models/ivdfm/decoder.py +0 -0
  31. dfm_python-0.6.13/src/dfm_python/models/ivdfm/encoder.py +0 -0
  32. dfm_python-0.6.13/src/dfm_python/models/ivdfm/ivdfm.py +708 -0
  33. dfm_python-0.6.13/src/dfm_python/numeric/builder.py +407 -0
  34. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/utils/__init__.py +2 -2
  35. dfm_python-0.6.13/src/dfm_python/utils/loss.py +330 -0
  36. dfm_python-0.6.13/src/test/dataset/test_ddfm_dataset.py +171 -0
  37. dfm_python-0.6.13/src/test/dataset/test_dfm_dataset.py +0 -0
  38. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/models/test_ddfm.py +14 -15
  39. dfm_python-0.6.13/src/test/models/test_ddfm_sampling.py +261 -0
  40. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/models/test_dfm.py +24 -7
  41. dfm_python-0.6.13/src/test/numeric/test_builder.py +156 -0
  42. dfm_python-0.6.13/tutorial/tutorial_exchange_ddfm.py +167 -0
  43. dfm_python-0.6.13/tutorial/tutorial_exchange_dfm.py +205 -0
  44. dfm_python-0.6.13/tutorial/tutorial_finance_ddfm.py +186 -0
  45. dfm_python-0.6.13/tutorial/tutorial_finance_dfm.py +234 -0
  46. dfm_python-0.6.13/tutorial/tutorial_macro_ddfm.py +184 -0
  47. dfm_python-0.6.13/tutorial/tutorial_macro_dfm.py +205 -0
  48. dfm_python-0.6.1/.git +0 -1
  49. dfm_python-0.6.1/models/dfm_exchange.pkl +0 -0
  50. dfm_python-0.6.1/models/dfm_macro.pkl +0 -0
  51. dfm_python-0.6.1/src/dfm_python/dataset/ddfm_dataset.py +0 -497
  52. dfm_python-0.6.1/src/dfm_python/decoder/__init__.py +0 -16
  53. dfm_python-0.6.1/src/dfm_python/decoder/base.py +0 -78
  54. dfm_python-0.6.1/src/dfm_python/decoder/linear.py +0 -72
  55. dfm_python-0.6.1/src/dfm_python/decoder/mlp.py +0 -128
  56. dfm_python-0.6.1/src/dfm_python/encoder/__init__.py +0 -38
  57. dfm_python-0.6.1/src/dfm_python/encoder/base.py +0 -111
  58. dfm_python-0.6.1/src/dfm_python/encoder/pca.py +0 -271
  59. dfm_python-0.6.1/src/dfm_python/numeric/builder.py +0 -200
  60. dfm_python-0.6.1/src/dfm_python/utils/loss.py +0 -100
  61. dfm_python-0.6.1/src/test/decoder/__init__.py +0 -2
  62. dfm_python-0.6.1/src/test/decoder/test_linear.py +0 -40
  63. dfm_python-0.6.1/src/test/decoder/test_mlp.py +0 -29
  64. dfm_python-0.6.1/src/test/encoder/__init__.py +0 -2
  65. dfm_python-0.6.1/src/test/encoder/test_base.py +0 -43
  66. dfm_python-0.6.1/src/test/encoder/test_pca.py +0 -100
  67. dfm_python-0.6.1/src/test/encoder/test_simple_autoencoder.py +0 -184
  68. dfm_python-0.6.1/src/test/functional/test_invariance.py +0 -348
  69. dfm_python-0.6.1/src/test/functional/test_macro_overflow.py +0 -240
  70. dfm_python-0.6.1/src/test/numeric/test_builder.py +0 -40
  71. dfm_python-0.6.1/tutorial/tutorial_exchange_ddfm.py +0 -329
  72. dfm_python-0.6.1/tutorial/tutorial_exchange_dfm.py +0 -57
  73. dfm_python-0.6.1/tutorial/tutorial_finance_ddfm.py +0 -283
  74. dfm_python-0.6.1/tutorial/tutorial_finance_dfm.py +0 -84
  75. dfm_python-0.6.1/tutorial/tutorial_macro_ddfm.py +0 -312
  76. dfm_python-0.6.1/tutorial/tutorial_macro_dfm.py +0 -56
  77. {dfm_python-0.6.1 → dfm_python-0.6.13}/.cursor/agents/test-runner.md +0 -0
  78. {dfm_python-0.6.1 → dfm_python-0.6.13}/.python-version +0 -0
  79. {dfm_python-0.6.1 → dfm_python-0.6.13}/=0.11.0 +0 -0
  80. {dfm_python-0.6.1 → dfm_python-0.6.13}/MANIFEST.in +0 -0
  81. {dfm_python-0.6.1 → dfm_python-0.6.13}/data/exchange_rate.csv +0 -0
  82. {dfm_python-0.6.1 → dfm_python-0.6.13}/data/finance.csv +0 -0
  83. {dfm_python-0.6.1 → dfm_python-0.6.13}/data/macro.csv +0 -0
  84. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/__init__.py +0 -0
  85. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/adapter.py +0 -0
  86. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/constants.py +0 -0
  87. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/schema/__init__.py +0 -0
  88. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/schema/block.py +0 -0
  89. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/schema/model.py +0 -0
  90. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/config/types.py +0 -0
  91. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/dataset/__init__.py +0 -0
  92. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/dataset/dfm_dataset.py +0 -0
  93. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/dataset/time.py +0 -0
  94. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/functional/__init__.py +0 -0
  95. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/functional/em.py +0 -0
  96. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/logger/dfm_logger.py +0 -0
  97. /dfm_python-0.6.1/src/test/dataset/test_ddfm_dataset.py → /dfm_python-0.6.13/src/dfm_python/logger/ivdfm_logger.py +0 -0
  98. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/logger/logger.py +0 -0
  99. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/__init__.py +0 -0
  100. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/dfm/__init__.py +0 -0
  101. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/dfm/dfm.py +0 -0
  102. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/models/dfm/tent.py +0 -0
  103. /dfm_python-0.6.1/src/test/dataset/test_dfm_dataset.py → /dfm_python-0.6.13/src/dfm_python/models/ivdfm/__init__.py +0 -0
  104. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/numeric/estimator.py +0 -0
  105. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/numeric/stability.py +0 -0
  106. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/numeric/statistic.py +0 -0
  107. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/numeric/validator.py +0 -0
  108. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/ssm/__init__.py +0 -0
  109. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/ssm/kalman.py +0 -0
  110. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/utils/checkpoint.py +0 -0
  111. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/utils/errors.py +0 -0
  112. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/utils/helper.py +0 -0
  113. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/utils/metric.py +0 -0
  114. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/utils/misc.py +0 -0
  115. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/dfm_python/utils/validation.py +0 -0
  116. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/__init__.py +0 -0
  117. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/__init__.py +0 -0
  118. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/schema/__init__.py +0 -0
  119. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/schema/test_block.py +0 -0
  120. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/schema/test_model.py +0 -0
  121. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/schema/test_results.py +0 -0
  122. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/test_adapter.py +0 -0
  123. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/test_constants.py +0 -0
  124. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/test_tent_weights_config.py +0 -0
  125. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/config/test_types.py +0 -0
  126. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/conftest.py +0 -0
  127. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/functional/__init__.py +0 -0
  128. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/functional/test_em.py +0 -0
  129. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/functional/test_em_correctness.py +0 -0
  130. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/functional/test_em_properties.py +0 -0
  131. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/logger/__init__.py +0 -0
  132. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/logger/test_inference_logger.py +0 -0
  133. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/logger/test_logger.py +0 -0
  134. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/logger/test_train_logger.py +0 -0
  135. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/models/__init__.py +0 -0
  136. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/numeric/__init__.py +0 -0
  137. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/numeric/test_cov_rowvar_regression.py +0 -0
  138. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/numeric/test_estimator.py +0 -0
  139. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/numeric/test_initialization.py +0 -0
  140. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/numeric/test_stability.py +0 -0
  141. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/numeric/test_validator.py +0 -0
  142. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/ssm/__init__.py +0 -0
  143. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/ssm/test_kalman.py +0 -0
  144. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/ssm/test_kalman_correctness.py +0 -0
  145. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/utils/__init__.py +0 -0
  146. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/utils/test_errors.py +0 -0
  147. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/utils/test_helper.py +0 -0
  148. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/utils/test_metric.py +0 -0
  149. {dfm_python-0.6.1 → dfm_python-0.6.13}/src/test/utils/test_misc.py +0 -0
  150. {dfm_python-0.6.1 → dfm_python-0.6.13}/uv.lock +0 -0
@@ -214,4 +214,5 @@ __marimo__/
214
214
  Nowcasting/
215
215
  agent/
216
216
 
217
- *.db
217
+ *.db
218
+ *.pkl
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.6.1
3
+ Version: 0.6.13
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
@@ -20,6 +20,11 @@ tolerance: 0.0005 # Convergence tolerance (matching original tolerance=0.0005)
20
20
  disp: 10 # Display frequency for training progress (matching original disp=10)
21
21
  seed: 3 # Random seed (matching original seed=3)
22
22
 
23
+ # Interpolation Parameters
24
+ interpolation_method: linear # Interpolation method for missing values
25
+ interpolation_limit: 10 # Maximum consecutive NaNs to interpolate
26
+ interpolation_limit_direction: both # Direction to fill ('forward', 'backward', or 'both')
27
+
23
28
  # Data Handling (used by Dataset, not Model)
24
29
  clock: d # Daily clock frequency (all series are daily)
25
30
  scaler: null # No scaler - data should be pre-scaled before passing to Dataset
@@ -18,6 +18,11 @@ tolerance: 0.0005 # Convergence tolerance
18
18
  disp: 10 # Display frequency for training progress
19
19
  seed: null # Random seed (None uses default)
20
20
 
21
+ # Interpolation Parameters
22
+ interpolation_method: linear # Interpolation method for missing values
23
+ interpolation_limit: 10 # Maximum consecutive NaNs to interpolate
24
+ interpolation_limit_direction: both # Direction to fill ('forward', 'backward', or 'both')
25
+
21
26
  # Data Handling (used by Dataset, not Model)
22
27
  clock: m # Monthly clock frequency
23
28
  scaler: standard # Scaler for target series: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or null
@@ -20,6 +20,11 @@ tolerance: 0.0005 # Convergence tolerance
20
20
  disp: 10 # Display frequency for training progress
21
21
  seed: null # Random seed (None uses default)
22
22
 
23
+ # Interpolation Parameters
24
+ interpolation_method: linear # Interpolation method for missing values
25
+ interpolation_limit: 10 # Maximum consecutive NaNs to interpolate
26
+ interpolation_limit_direction: both # Direction to fill ('forward', 'backward', or 'both')
27
+
23
28
  # Data Handling (used by Dataset, not Model)
24
29
  clock: w # Weekly clock frequency (base frequency for latent factors)
25
30
  scaler: standard # Scaler for target series: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or null
@@ -6,7 +6,7 @@
6
6
  # NOTE: Exchange tutorial is long daily data (T~7.6k). Full EM can take hours.
7
7
  # Keep tutorial runs short by capping iterations and loosening convergence.
8
8
  threshold: 1.0e-04 # EM convergence threshold (tutorial-friendly)
9
- max_iter: 50 # Maximum EM iterations (tutorial-friendly)
9
+ max_iter: 10 # Maximum EM iterations (tutorial-friendly)
10
10
 
11
11
  # Data Handling
12
12
  clock: d # Daily clock frequency (base frequency for latent factors)
@@ -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: 10 # 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: 5 # Maximum EM iterations
9
+ max_iter: 10 # Maximum EM iterations
10
10
 
11
11
  # Numerical Stability Parameters
12
12
  clip_ar_coefficients: true
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dfm-python"
3
- version = "0.6.1"
3
+ version = "0.6.13"
4
4
  description = "Dynamic Factor Model (DFM) estimation and forecasting in Python"
5
5
  requires-python = ">=3.10"
6
6
  license = {text = "MIT"}
@@ -63,7 +63,7 @@ Note: DFMConfig uses frequency dict to specify series (column names -> frequenci
63
63
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
64
64
  """
65
65
 
66
- __version__ = "0.5.60"
66
+ __version__ = "0.6.13"
67
67
 
68
68
  # ============================================================================
69
69
  # PUBLIC API DEFINITION
@@ -163,11 +163,11 @@ class DFMModelState:
163
163
  )
164
164
 
165
165
  @dataclass
166
- class DDFMTrainingState:
167
- """Training state for DDFM model.
166
+ class DDFMModelState:
167
+ """DDFM model state including training state and state-space parameters.
168
168
 
169
- Stores the current state of DDFM training, including convergence status,
170
- loss, factors, and residuals. Used for checkpointing and resuming training.
169
+ Consolidates all DDFM model state for checkpointing: training state (convergence,
170
+ loss, factors, residuals) and fitted state-space parameters.
171
171
 
172
172
  Attributes
173
173
  ----------
@@ -184,15 +184,84 @@ class DDFMTrainingState:
184
184
  last_neurons : np.ndarray, optional
185
185
  Last layer neurons for MLP decoder (n_mc_samples x T x num_neurons) or (T x num_neurons).
186
186
  For linear decoder, this equals factors.
187
+ F : np.ndarray, optional
188
+ Transition matrix (m x m) - VAR(1) dynamics for factors (A in paper)
189
+ Q : np.ndarray, optional
190
+ Process noise covariance (m x m) - innovation covariance (W in original code)
191
+ mu_0 : np.ndarray, optional
192
+ Initial state mean (m,) - initial factor values
193
+ Sigma_0 : np.ndarray, optional
194
+ Initial state covariance (m x m) - initial uncertainty
195
+ H : np.ndarray, optional
196
+ Observation matrix (N x m) - decoder weights (measurement equation, theta in paper)
197
+ R : np.ndarray, optional
198
+ Observation noise covariance (N x N) - typically diagonal, small values
187
199
  """
200
+ # Training state
188
201
  num_iter: int = 0
189
202
  loss_now: Optional[float] = None
190
203
  converged: bool = False
191
204
  eps: Optional[np.ndarray] = None
192
205
  factors: Optional[np.ndarray] = None
193
206
  last_neurons: Optional[np.ndarray] = None
207
+ # State-space parameters (fitted during build_state_space)
208
+ F: Optional[np.ndarray] = None # Transition matrix (m x m) - A in paper
209
+ Q: Optional[np.ndarray] = None # Process noise covariance (m x m) - W in original code
210
+ mu_0: Optional[np.ndarray] = None # Initial state mean (m,)
211
+ Sigma_0: Optional[np.ndarray] = None # Initial state covariance (m x m)
212
+ H: Optional[np.ndarray] = None # Observation matrix (N x m) - theta in paper
213
+ R: Optional[np.ndarray] = None # Observation noise covariance (N x N)
194
214
 
195
- def sync_from_model(self, model: Any) -> 'DDFMTrainingState':
215
+ @classmethod
216
+ def from_model(cls, model: Any) -> 'DDFMModelState':
217
+ """Create DDFMModelState from DDFM model instance.
218
+
219
+ Parameters
220
+ ----------
221
+ model : DDFM
222
+ DDFM model instance
223
+
224
+ Returns
225
+ -------
226
+ DDFMModelState
227
+ Model state dataclass
228
+ """
229
+ # Get training state from model attributes
230
+ factors = getattr(model, 'factors', None)
231
+ eps = getattr(model, 'eps', None)
232
+ last_neurons = getattr(model, 'last_neurons', None)
233
+ num_iter = getattr(model, '_num_iter', 0)
234
+ loss_now = getattr(model, 'loss_now', None)
235
+ converged = getattr(model, '_converged', False)
236
+
237
+ # Get state-space parameters from training_state
238
+ F = Q = mu_0 = Sigma_0 = H = R = None
239
+ if hasattr(model, 'training_state') and model.training_state is not None:
240
+ ts = model.training_state
241
+ if hasattr(ts, 'F'):
242
+ F = ts.F
243
+ Q = ts.Q
244
+ mu_0 = ts.mu_0
245
+ Sigma_0 = ts.Sigma_0
246
+ H = ts.H
247
+ R = ts.R
248
+
249
+ return cls(
250
+ num_iter=num_iter,
251
+ loss_now=loss_now,
252
+ converged=converged,
253
+ eps=eps,
254
+ factors=factors,
255
+ last_neurons=last_neurons,
256
+ F=F,
257
+ Q=Q,
258
+ mu_0=mu_0,
259
+ Sigma_0=Sigma_0,
260
+ H=H,
261
+ R=R
262
+ )
263
+
264
+ def sync_from_model(self, model: Any) -> 'DDFMModelState':
196
265
  """Update this dataclass from model instance attributes.
197
266
 
198
267
  Parameters
@@ -202,7 +271,7 @@ class DDFMTrainingState:
202
271
 
203
272
  Returns
204
273
  -------
205
- DDFMTrainingState
274
+ DDFMModelState
206
275
  Self (for chaining)
207
276
  """
208
277
  if hasattr(model, '_has_factors') and model._has_factors:
@@ -212,37 +281,52 @@ class DDFMTrainingState:
212
281
  self.num_iter = getattr(model, '_num_iter', 0)
213
282
  self.loss_now = getattr(model, 'loss_now', None)
214
283
  self.converged = getattr(model, '_converged', False)
284
+
285
+ # Sync state-space parameters if available
286
+ if hasattr(model, 'training_state') and model.training_state is not None:
287
+ ts = model.training_state
288
+ if hasattr(ts, 'F'):
289
+ self.F = ts.F
290
+ self.Q = ts.Q
291
+ self.mu_0 = ts.mu_0
292
+ self.Sigma_0 = ts.Sigma_0
293
+ self.H = ts.H
294
+ self.R = ts.R
295
+
215
296
  return self
216
-
217
- @dataclass
218
- class DDFMStateSpaceParams:
219
- """State-space model parameters for DDFM (fitted during model training).
220
-
221
- These parameters are computed during DDFM.build_state_space() and represent
222
- the fitted state-space model structure. Similar to DFMStateSpaceParams, this contains
223
- the fitted state-space parameters but for DDFM. Naming follows original paper conventions.
224
297
 
225
- Parameters
226
- ----------
227
- F : np.ndarray
228
- Transition matrix (m x m) - VAR(1) dynamics for factors (A in paper)
229
- Q : np.ndarray
230
- Process noise covariance (m x m) - innovation covariance (W in original code)
231
- mu_0 : np.ndarray
232
- Initial state mean (m,) - initial factor values
233
- Sigma_0 : np.ndarray
234
- Initial state covariance (m x m) - initial uncertainty
235
- H : np.ndarray
236
- Observation matrix (N x m) - decoder weights (measurement equation, theta in paper)
237
- R : np.ndarray
238
- Observation noise covariance (N x N) - typically diagonal, small values
239
- """
240
- F: np.ndarray # Transition matrix (m x m) - A in paper
241
- Q: np.ndarray # Process noise covariance (m x m) - W in original code
242
- mu_0: np.ndarray # Initial state mean (m,)
243
- Sigma_0: np.ndarray # Initial state covariance (m x m)
244
- H: np.ndarray # Observation matrix (N x m) - theta in paper
245
- R: np.ndarray # Observation noise covariance (N x N)
298
+ def apply_to_model(self, model: Any) -> None:
299
+ """Apply this state to DDFM model instance.
300
+
301
+ Parameters
302
+ ----------
303
+ model : DDFM
304
+ DDFM model instance to update
305
+ """
306
+ # Apply training state
307
+ if self.factors is not None:
308
+ model.factors = self.factors
309
+ if self.eps is not None:
310
+ model.eps = self.eps
311
+ if self.last_neurons is not None:
312
+ model.last_neurons = self.last_neurons
313
+ model._num_iter = self.num_iter
314
+ model.loss_now = self.loss_now
315
+ model._converged = self.converged
316
+
317
+ # Apply state-space parameters if available
318
+ if self.F is not None and self.Q is not None and self.mu_0 is not None:
319
+ if hasattr(model, 'training_state'):
320
+ model.training_state.F = self.F
321
+ model.training_state.Q = self.Q
322
+ model.training_state.mu_0 = self.mu_0
323
+ model.training_state.Sigma_0 = self.Sigma_0
324
+ model.training_state.H = self.H
325
+ model.training_state.R = self.R
326
+
327
+ # Backward compatibility aliases
328
+ DDFMTrainingState = DDFMModelState
329
+ DDFMStateSpaceParams = DDFMModelState
246
330
 
247
331
  # Backward compatibility alias
248
332
  DFMStateSpaceParams = DFMModelState
@@ -13,7 +13,7 @@ from typing import Optional, List, Dict, Any, Tuple
13
13
  from datetime import datetime
14
14
 
15
15
  from .model import DFMConfig
16
- from .params import DDFMStateSpaceParams
16
+ from .params import DDFMModelState
17
17
 
18
18
 
19
19
 
@@ -311,7 +311,7 @@ class DDFMResult(BaseResult):
311
311
  Architecture of the encoder network used.
312
312
  use_idiosyncratic : bool, optional
313
313
  Whether idiosyncratic components were modeled.
314
- fit_params : DDFMStateSpaceParams, optional
314
+ fit_params : DDFMModelState, optional
315
315
  State-space model parameters created during fit (F, Q, mu_0, sigma_0, H, R).
316
316
  These are computed during build_state_space() and represent the fitted
317
317
  state-space model structure.
@@ -365,7 +365,7 @@ class DDFMResult(BaseResult):
365
365
  use_idiosyncratic: Optional[bool] = None # Whether idio components were used
366
366
 
367
367
  # State-space parameters (fitted during training)
368
- fit_params: Optional[DDFMStateSpaceParams] = None
368
+ fit_params: Optional[DDFMModelState] = None
369
369
 
370
370
  # Training hyperparameters
371
371
  training_max_iter: Optional[int] = None
@@ -0,0 +1,228 @@
1
+ """PyTorch Dataset for Deep Dynamic Factor Model (DDFM)."""
2
+
3
+ import torch
4
+ from torch.utils.data import Dataset
5
+ import numpy as np
6
+ import pandas as pd
7
+ try:
8
+ import polars as pl
9
+ _has_polars = True
10
+ PolarsDataFrame = pl.DataFrame
11
+ except ImportError:
12
+ pl = None
13
+ _has_polars = False
14
+ PolarsDataFrame = type(None) # Dummy type for type hints when polars not available
15
+ from typing import Tuple, List, Optional, Union
16
+ from sklearn.preprocessing import StandardScaler, RobustScaler, MinMaxScaler
17
+
18
+ from ..config.constants import DEFAULT_TORCH_DTYPE
19
+
20
+
21
+ class DDFMDataset(Dataset):
22
+ """Dataset for DDFM training.
23
+
24
+ Scales target series if scaler provided.
25
+
26
+ All columns are treated as targets by default. When covariates are specified,
27
+ they are excluded from targets (targets = all_columns - covariates).
28
+
29
+ Parameters
30
+ ----------
31
+ data : pd.DataFrame | PolarsDataFrame
32
+ Input data. Target series will be scaled if scaler provided.
33
+ time_idx : str
34
+ Time index column name.
35
+ covariates : List[str], optional
36
+ Series to exclude from targets (used for factor extraction but not forecasted).
37
+ If None, all series are targets (default).
38
+ scaler : StandardScaler | RobustScaler | MinMaxScaler, optional
39
+ Scaler instance to scale target series. If None, no scaling.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ data: Union[pd.DataFrame, PolarsDataFrame],
45
+ time_idx: str,
46
+ covariates: Optional[List[str]] = None,
47
+ scaler: Optional[Union[StandardScaler, RobustScaler, MinMaxScaler]] = None,
48
+ ):
49
+ if _has_polars and isinstance(data, pl.DataFrame):
50
+ data = data.to_pandas()
51
+
52
+ data = data.copy()
53
+ data.sort_index(inplace=True)
54
+
55
+ self.time_idx = time_idx
56
+ self.time_index = pd.Index(data[time_idx]) if time_idx in data.columns else data.index
57
+ self.data_original = data.copy()
58
+
59
+ # Get variables (all columns excluding time_idx)
60
+ variables = [col for col in data.columns if col != time_idx] if time_idx in data.columns else list(data.columns)
61
+
62
+ # Filter covariates and compute targets
63
+ covariates = [c for c in (covariates or []) if c in variables]
64
+ self.covariates = covariates
65
+ self.target_series = [col for col in variables if col not in covariates]
66
+
67
+ # Split into covariates (X) and targets (y)
68
+ data_for_split = data.drop(columns=[time_idx]) if time_idx in data.columns else data
69
+ y = data_for_split[self.target_series]
70
+ X = data_for_split.drop(columns=self.target_series) if self.target_series else pd.DataFrame()
71
+
72
+ # Scale covariates separately (temporary scaler, not stored)
73
+ if scaler is not None and not X.empty:
74
+ X = pd.DataFrame(type(scaler)().fit_transform(X.values), index=X.index, columns=X.columns)
75
+
76
+ # Scale targets and store scaler for prediction
77
+ if scaler is not None:
78
+ y = pd.DataFrame(scaler.fit_transform(y.values), index=y.index, columns=y.columns)
79
+
80
+ self.scaler = scaler
81
+
82
+ # Store processed data
83
+ self.data = pd.concat([X, y], axis=1) if not X.empty else y
84
+ self.X = X.values if not X.empty else np.empty((len(y), 0))
85
+ self.y = y.values
86
+ self.missing_y = y.isna().values
87
+ self.observed_y = ~self.missing_y
88
+
89
+ @property
90
+ def target_shape(self) -> Tuple[int, int]:
91
+ """Target shape (T, num_target_series)."""
92
+ return self.y.shape
93
+
94
+ @property
95
+ def data_shape(self) -> Tuple[int, int]:
96
+ """Data shape (T, num_features + num_target_series)."""
97
+ return self.X.shape[0], self.X.shape[1] + self.y.shape[1]
98
+
99
+ @property
100
+ def all_columns_are_targets(self) -> bool:
101
+ """Whether all columns are target series (no covariates)."""
102
+ return len(self.covariates) == 0
103
+
104
+ def split_features_and_targets(self, data: pd.DataFrame) -> Tuple[Optional[pd.DataFrame], pd.DataFrame]:
105
+ """Split DataFrame into features (X) and targets (y)."""
106
+ if self.all_columns_are_targets:
107
+ return None, data
108
+ X = data.drop(columns=self.target_series)
109
+ y = data[self.target_series]
110
+ return X, y
111
+
112
+ @property
113
+ def target_indices(self) -> np.ndarray:
114
+ """Target series column indices in processed data (self.data, excluding time_idx)."""
115
+ # target_series are column names in the processed data (self.data)
116
+ # self.data contains X + y (or just y), but not time_idx
117
+ return np.array([self.data.columns.get_loc(col) for col in self.target_series])
118
+
119
+ @classmethod
120
+ def from_dataset(cls, new_data: Union[pd.DataFrame, PolarsDataFrame], dataset: 'DDFMDataset') -> 'DDFMDataset':
121
+ """Create new dataset with new data, preserving configuration."""
122
+ return cls(
123
+ data=new_data,
124
+ time_idx=dataset.time_idx,
125
+ covariates=dataset.covariates,
126
+ scaler=dataset.scaler
127
+ )
128
+
129
+ def create_autoencoder_dataset(
130
+ self,
131
+ X: Optional[torch.Tensor],
132
+ y_tmp: torch.Tensor,
133
+ y_actual: torch.Tensor,
134
+ eps_draw: torch.Tensor
135
+ ) -> 'AutoencoderDataset':
136
+ """Create a single AutoencoderDataset with corrupted targets."""
137
+ return AutoencoderDataset(X=X, y_corrupted=y_tmp - eps_draw, y_clean=y_actual)
138
+
139
+ def create_pretrain_dataset(
140
+ self,
141
+ data: pd.DataFrame,
142
+ device: Optional[torch.device] = None
143
+ ) -> 'AutoencoderDataset':
144
+ """Create AutoencoderDataset for pre-training (no corruption, clean data)."""
145
+ if device is None:
146
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
147
+
148
+ X_df, y_df = self.split_features_and_targets(data)
149
+ X = None if X_df is None else torch.from_numpy(X_df.values).to(dtype=DEFAULT_TORCH_DTYPE, device=device)
150
+ y = torch.from_numpy(y_df.values).to(dtype=DEFAULT_TORCH_DTYPE, device=device)
151
+
152
+ return AutoencoderDataset(X=X, y_corrupted=y, y_clean=y)
153
+
154
+ def create_autoencoder_datasets_list(
155
+ self,
156
+ n_mc_samples: int,
157
+ mu_eps: np.ndarray,
158
+ std_eps: np.ndarray,
159
+ X: Union[np.ndarray, pd.DataFrame],
160
+ y_tmp: Union[np.ndarray, pd.DataFrame],
161
+ y_actual: np.ndarray,
162
+ rng: np.random.RandomState,
163
+ device: Optional[torch.device] = None
164
+ ) -> List['AutoencoderDataset']:
165
+ """Create AutoencoderDataset instances with pre-sampled MC noise."""
166
+ if device is None:
167
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
168
+
169
+ X_array = X.values if isinstance(X, pd.DataFrame) else X
170
+ y_tmp_array = y_tmp.values if isinstance(y_tmp, pd.DataFrame) else y_tmp
171
+ T = y_tmp_array.shape[0]
172
+
173
+ # Pre-sample all MC noise at once
174
+ eps_draws = rng.multivariate_normal(mu_eps, np.diag(std_eps), (n_mc_samples, T))
175
+
176
+ # Convert to tensors once
177
+ X_tensor = torch.from_numpy(X_array).to(dtype=DEFAULT_TORCH_DTYPE, device=device) if X_array.size > 0 else None
178
+ y_tmp_tensor = torch.from_numpy(y_tmp_array).to(dtype=DEFAULT_TORCH_DTYPE, device=device)
179
+ y_actual_tensor = torch.from_numpy(y_actual).to(dtype=DEFAULT_TORCH_DTYPE, device=device)
180
+ eps_draws_tensor = torch.from_numpy(eps_draws).to(dtype=DEFAULT_TORCH_DTYPE, device=device)
181
+
182
+ # Create datasets
183
+ return [
184
+ self.create_autoencoder_dataset(X_tensor, y_tmp_tensor, y_actual_tensor, eps_draws_tensor[i])
185
+ for i in range(n_mc_samples)
186
+ ]
187
+
188
+
189
+ class AutoencoderDataset:
190
+ """Container for autoencoder training data with corrupted inputs and clean targets.
191
+
192
+ Stores pre-loaded tensors for efficient direct slicing. All tensors are expected
193
+ to be on the correct device. Use direct tensor slicing (e.g., `dataset.full_input[i:j]`)
194
+ rather than indexing.
195
+
196
+ Parameters
197
+ ----------
198
+ X : torch.Tensor, optional
199
+ Features (T, N_features) - lags, dummies, etc. Not corrupted.
200
+ y_corrupted : torch.Tensor
201
+ Corrupted targets (T, num_target_series).
202
+ y_clean : torch.Tensor
203
+ Clean targets (T, num_target_series) for reconstruction.
204
+ """
205
+
206
+ def __init__(
207
+ self,
208
+ X: Optional[torch.Tensor],
209
+ y_corrupted: torch.Tensor,
210
+ y_clean: torch.Tensor
211
+ ):
212
+ self.X = X
213
+ self.y_corrupted = y_corrupted
214
+ self.y_clean = y_clean
215
+ # Pre-compute full_input once (optimization: avoid torch.cat on every access)
216
+ if self.X is not None:
217
+ self._full_input = torch.cat([self.X, self.y_corrupted], dim=1)
218
+ else:
219
+ self._full_input = self.y_corrupted
220
+
221
+ @property
222
+ def full_input(self) -> torch.Tensor:
223
+ """Full autoencoder input: clean X features + corrupted y targets."""
224
+ return self._full_input
225
+
226
+ def __len__(self) -> int:
227
+ """Return number of time steps."""
228
+ return self.y_corrupted.shape[0]