dfm-python 0.3.0__py3-none-any.whl → 0.4.0__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.
Files changed (62) hide show
  1. dfm_python/__init__.py +168 -52
  2. dfm_python/config/__init__.py +65 -0
  3. dfm_python/{config_sources.py → config/io.py} +9 -9
  4. dfm_python/config/params.py +202 -0
  5. dfm_python/config/results.py +318 -0
  6. dfm_python/{config.py → config/schema.py} +270 -344
  7. dfm_python/{core/utils.py → config/structure.py} +7 -15
  8. dfm_python/config/utils.py +93 -0
  9. dfm_python/encoder/__init__.py +33 -0
  10. dfm_python/encoder/base.py +0 -0
  11. dfm_python/encoder/pca.py +157 -0
  12. dfm_python/encoder/vae.py +291 -0
  13. dfm_python/lightning/__init__.py +48 -0
  14. dfm_python/lightning/callbacks.py +0 -0
  15. dfm_python/lightning/data_module.py +364 -0
  16. dfm_python/lightning/ddfm_module.py +553 -0
  17. dfm_python/lightning/dfm_module.py +466 -0
  18. dfm_python/lightning/utils.py +0 -0
  19. dfm_python/logger/__init__.py +51 -0
  20. dfm_python/logger/inference_logger.py +270 -0
  21. dfm_python/logger/logger.py +110 -0
  22. dfm_python/logger/train_logger.py +309 -0
  23. dfm_python/models/__init__.py +36 -0
  24. dfm_python/models/base.py +94 -0
  25. dfm_python/models/ddfm.py +1120 -0
  26. dfm_python/models/dfm.py +810 -0
  27. dfm_python/models/estimation.py +352 -0
  28. dfm_python/nowcast/__init__.py +28 -0
  29. dfm_python/nowcast/dataview.py +143 -0
  30. dfm_python/nowcast/helpers.py +554 -0
  31. dfm_python/{nowcast.py → nowcast/nowcast.py} +287 -788
  32. dfm_python/nowcast/utils.py +290 -0
  33. dfm_python/ssm/__init__.py +47 -0
  34. dfm_python/ssm/em.py +488 -0
  35. dfm_python/ssm/kalman.py +604 -0
  36. dfm_python/ssm/utils.py +379 -0
  37. dfm_python/trainer/__init__.py +20 -0
  38. dfm_python/trainer/ddfm.py +196 -0
  39. dfm_python/trainer/dfm.py +153 -0
  40. dfm_python/transformations/__init__.py +35 -0
  41. dfm_python/transformations/sktime.py +40 -0
  42. dfm_python/transformations/transformers.py +320 -0
  43. dfm_python/transformations/utils.py +339 -0
  44. dfm_python/utils/__init__.py +201 -0
  45. dfm_python/utils/data.py +706 -0
  46. dfm_python/{core → utils}/diagnostics.py +32 -109
  47. dfm_python/utils/helpers.py +533 -0
  48. dfm_python/utils/statespace.py +1326 -0
  49. dfm_python/{core → utils}/time.py +373 -0
  50. {dfm_python-0.3.0.dist-info → dfm_python-0.4.0.dist-info}/METADATA +198 -33
  51. dfm_python-0.4.0.dist-info/RECORD +53 -0
  52. {dfm_python-0.3.0.dist-info → dfm_python-0.4.0.dist-info}/WHEEL +1 -1
  53. dfm_python/api.py +0 -1071
  54. dfm_python/core/__init__.py +0 -15
  55. dfm_python/core/em.py +0 -1384
  56. dfm_python/core/helpers.py +0 -1337
  57. dfm_python/core/numeric.py +0 -632
  58. dfm_python/data.py +0 -763
  59. dfm_python/dfm.py +0 -963
  60. dfm_python/kalman.py +0 -467
  61. dfm_python-0.3.0.dist-info/RECORD +0 -19
  62. {dfm_python-0.3.0.dist-info → dfm_python-0.4.0.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py CHANGED
@@ -7,6 +7,7 @@ This package implements a comprehensive Dynamic Factor Model framework with supp
7
7
  - Expectation-Maximization (EM) algorithm for parameter estimation
8
8
  - Kalman filtering and smoothing for factor extraction
9
9
  - News decomposition for nowcasting
10
+ - Deep Dynamic Factor Models (DDFM) with nonlinear encoders (optional, requires PyTorch)
10
11
 
11
12
  The package implements a clock-based approach to mixed-frequency DFMs, where all latent
12
13
  factors (global and block-level) are synchronized to a common "clock" frequency, typically
@@ -25,16 +26,39 @@ Key Features:
25
26
 
26
27
  Example (High-level API - Recommended):
27
28
  >>> import dfm_python as dfm
28
- >>> dfm.load_config('config/default.yaml')
29
- >>> dfm.load_data('data/sample_data.csv')
30
- >>> dfm.train(max_iter=1)
31
- >>> Xf, Zf = dfm.predict(horizon=6)
32
- >>> dfm.plot(kind='factor', factor_index=0, forecast_horizon=6, save_path='outputs/factor_forecast.png')
33
- >>> factors = dfm.get_result().Z # Access factors via result
29
+ >>> from dfm_python.lightning import DFMDataModule
30
+ >>> from sktime.transformations.compose import ColumnTransformer
31
+ >>>
32
+ >>> # Linear DFM
33
+ >>> model = dfm.DFM()
34
+ >>> model.load_config('config/default.yaml')
35
+ >>>
36
+ >>> # Create transformer (user must provide)
37
+ >>> transformer = ColumnTransformer([...]) # User-defined sktime transformer
38
+ >>>
39
+ >>> # Create DataModule
40
+ >>> data_module = DFMDataModule(config=model.config, transformer=transformer, data_path='data/sample_data.csv')
41
+ >>> data_module.setup()
42
+ >>>
43
+ >>> # Train
44
+ >>> model.train(data_module, max_iter=100)
45
+ >>> Xf, Zf = model.predict(horizon=6)
46
+ >>>
47
+ >>> # Or use DDFM (separate class)
48
+ >>> ddfm_model = dfm.DDFM(encoder_layers=[64, 32], num_factors=2)
49
+ >>> ddfm_model.load_config('config/default.yaml')
50
+ >>>
51
+ >>> # Create DataModule for DDFM
52
+ >>> data_module_ddfm = DFMDataModule(config=ddfm_model.config, transformer=transformer, data_path='data/sample_data.csv')
53
+ >>> data_module_ddfm.setup()
54
+ >>>
55
+ >>> # Train
56
+ >>> ddfm_model.train(data_module_ddfm, epochs=100)
57
+ >>> Xf, Zf = ddfm_model.predict(horizon=6)
34
58
 
35
59
  Example (Low-level API - For advanced usage):
36
60
  >>> from dfm_python import DFM, DFMConfig, SeriesConfig
37
- >>> from dfm_python.data import load_data # Preferred import
61
+ >>> from dfm_python.transformations.utils import read_data # Preferred import
38
62
  >>> # Option 1: Load from YAML
39
63
  >>> config = load_config('config.yaml')
40
64
  >>> # Option 2: Create directly
@@ -42,62 +66,122 @@ Example (Low-level API - For advanced usage):
42
66
  ... series=[SeriesConfig(frequency='m', transformation='lin', blocks=[1], series_id='series1')],
43
67
  ... block_names=['Global']
44
68
  ... )
45
- >>> X, Time, _ = load_data('data.csv', config) # or use database adapter
69
+ >>> from dfm_python.lightning import DFMDataModule
70
+ >>> from sktime.transformations.compose import ColumnTransformer
71
+ >>>
72
+ >>> # Create transformer (user must provide)
73
+ >>> transformer = ColumnTransformer([...]) # User-defined
74
+ >>>
75
+ >>> # Create DataModule
76
+ >>> data_module = DFMDataModule(config=config, transformer=transformer, data_path='data.csv')
77
+ >>> data_module.setup()
78
+ >>>
79
+ >>> # Fit model
46
80
  >>> model = DFM()
47
- >>> result = model.fit(X, config)
81
+ >>> result = model.fit(data_module, config)
48
82
  >>> factors = result.Z # Extract estimated factors
49
83
 
50
84
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
51
85
  """
52
86
 
53
- __version__ = "0.3.0"
54
-
87
+ __version__ = "0.4.0"
88
+
89
+ # ============================================================================
90
+ # PUBLIC API DEFINITION
91
+ # ============================================================================
92
+ # This __init__.py is the single source of truth for the public API.
93
+ # All symbols exported here are considered stable public API.
94
+ # Internal reorganization should not break these imports.
95
+ #
96
+ # Public API categories:
97
+ # 1. Configuration: DFMConfig, SeriesConfig, BlockConfig, Params, config sources
98
+ # 2. High-level API: DFM, DDFM, module-level convenience functions
99
+ # 3. Core utilities: DFMCore, run_kf, TimeIndex, diagnostics
100
+ # 4. Models: BaseFactorModel, DFMLinear, DDFM (low-level)
101
+ # 5. Nowcasting: Nowcast, result classes, para_const
102
+ # 6. Data & Results: DFMResult, transform_data
103
+ # ============================================================================
104
+
105
+ # Configuration (from config/ subpackage)
55
106
  from .config import (
56
107
  DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME,
57
108
  ConfigSource, YamlSource, DictSource, HydraSource,
58
109
  MergedConfigSource, make_config_source,
59
110
  )
60
- from .data import transform_data
61
- from .dfm import DFMResult
62
- from .core import calculate_rmse, diagnose_series, print_series_diagnosis
63
- from .dfm import DFMCore # Core DFM class from dfm.py
64
- from .kalman import run_kf, skf, fis, miss_data
65
- from .nowcast import Nowcast, para_const, NowcastResult, NewsDecompResult, BacktestResult
66
-
67
- # Import high-level API (extends core DFM with convenience methods)
68
- # Import module-level convenience functions directly from api.py to avoid duplication
69
- from .api import (
70
- DFM, _dfm_instance, from_yaml, from_spec, from_spec_df, from_dict,
71
- load_config, load_data, load_pickle, train, predict, plot, reset
72
- )
73
-
74
- # Expose properties as module-level attributes
75
- # Use property-like access via functions or direct attribute access
76
- # Since 'config' conflicts with the config module, we'll use a different approach
77
- # Users can access: dfm.get_config(), dfm.get_data(), dfm.get_result()
78
- def get_config():
79
- """Get current configuration."""
80
- return _dfm_instance.config
81
-
82
- def get_data():
83
- """Get current data matrix."""
84
- return _dfm_instance.data
85
-
86
- def get_time():
87
- """Get current time index."""
88
- return _dfm_instance.time
89
111
 
90
- def get_result():
91
- """Get training result."""
92
- return _dfm_instance.result
112
+ # Data utilities
113
+ # Note: transform_data and DFMScaler have been removed - users must provide their own sktime transformers to DFMDataModule
114
+
115
+ # Results
116
+ from .config.results import DFMResult, DDFMResult, BaseResult
117
+
118
+ # Utilities (from utils/ subpackage)
119
+ from .utils.diagnostics import diagnose_series, print_series_diagnosis
120
+ from .utils.time import calculate_rmse
121
+ # DFMCore is an alias for DFMLinear (backward compatibility)
122
+ from .models.dfm import DFMLinear
123
+ DFMCore = DFMLinear
124
+
125
+ # PyTorch Lightning modules (recommended)
126
+ try:
127
+ from .lightning import (
128
+ DFMLightningModule,
129
+ DDFMLightningModule,
130
+ DFMDataModule,
131
+ KalmanFilter, # New module class
132
+ EMAlgorithm, # New module class
133
+ )
134
+ _has_lightning = True
135
+ except ImportError:
136
+ _has_lightning = False
137
+ DFMLightningModule = None # type: ignore
138
+ DDFMLightningModule = None # type: ignore
139
+ DFMDataModule = None # type: ignore
140
+ KalmanFilter = None # type: ignore
141
+ EMAlgorithm = None # type: ignore
142
+
143
+ # Nowcasting (from nowcast/ subpackage)
144
+ from .nowcast import (
145
+ Nowcast,
146
+ NowcastResult,
147
+ )
148
+ from .nowcast.helpers import (
149
+ para_const,
150
+ NewsDecompResult,
151
+ BacktestResult,
152
+ )
93
153
 
94
- def get_original_data():
95
- """Get original (untransformed) data matrix."""
96
- return _dfm_instance.original_data
154
+ # Model implementations
155
+ from .models.base import BaseFactorModel
156
+ from .models.dfm import DFMLinear, DFM
157
+ from .models.dfm import (
158
+ from_yaml, from_spec, from_spec_df, from_dict,
159
+ load_config, load_pickle, train, predict, plot, reset, create_model
160
+ )
161
+ # Note: load_data has been removed - use DFMDataModule instead
162
+
163
+ # DDFM high-level API and low-level model (both optional, requires PyTorch)
164
+ try:
165
+ from .models.ddfm import DDFM, DDFMModel
166
+ from .models.ddfm import load_config_ddfm, load_data_ddfm, train_ddfm, predict_ddfm, plot_ddfm, reset_ddfm
167
+ _has_ddfm = True
168
+ except ImportError:
169
+ _has_ddfm = False
170
+ DDFM = None # type: ignore
171
+ DDFMModel = None # type: ignore
172
+ load_config_ddfm = None # type: ignore
173
+ load_data_ddfm = None # type: ignore
174
+ train_ddfm = None # type: ignore
175
+ predict_ddfm = None # type: ignore
176
+ plot_ddfm = None # type: ignore
177
+ reset_ddfm = None # type: ignore
178
+ # DDFM module-level functions removed - create DDFM() instance directly
97
179
 
98
180
  __all__ = [
99
181
  # Core classes
100
182
  'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params', 'DFM', 'DFMCore', 'Nowcast',
183
+ # Model base and implementations
184
+ 'BaseFactorModel', 'DFMLinear',
101
185
  # Nowcast result classes
102
186
  'NowcastResult', 'NewsDecompResult', 'BacktestResult',
103
187
  # Constants
@@ -107,14 +191,46 @@ __all__ = [
107
191
  'MergedConfigSource', 'make_config_source',
108
192
  # High-level API (module-level - recommended)
109
193
  'load_config',
110
- 'load_data', 'load_pickle', 'train', 'predict', 'plot', 'reset',
111
- 'get_config', 'get_data', 'get_time', 'get_result', 'get_original_data',
194
+ 'load_pickle', 'train', 'predict', 'plot', 'reset', 'create_model',
112
195
  # Convenience constructors (cleaner API)
113
196
  'from_yaml', 'from_spec', 'from_spec_df', 'from_dict',
114
197
  # Low-level API (functional interface - advanced usage)
115
- 'transform_data',
116
- 'DFMResult', 'calculate_rmse', 'diagnose_series', 'print_series_diagnosis',
117
- 'run_kf', 'skf', 'fis', 'miss_data',
118
- 'para_const', # Internal utility, kept for backward compatibility
198
+ 'BaseResult', 'DFMResult', 'DDFMResult', 'calculate_rmse', 'diagnose_series', 'print_series_diagnosis',
199
+ 'para_const', # Internal utility for nowcasting
119
200
  ]
120
201
 
202
+ # Add DDFM high-level API and convenience functions if available
203
+ if _has_ddfm:
204
+ __all__.extend([
205
+ 'DDFM', # High-level API class
206
+ 'DDFMModel', # Low-level implementation
207
+ 'load_config_ddfm', 'train_ddfm',
208
+ 'predict_ddfm', 'plot_ddfm', 'reset_ddfm'
209
+ # Note: load_data_ddfm has been removed - use DFMDataModule instead
210
+ ])
211
+
212
+ # Add Lightning modules if available
213
+ if _has_lightning:
214
+ __all__.extend([
215
+ 'DFMLightningModule',
216
+ 'DDFMLightningModule',
217
+ 'DFMDataModule',
218
+ 'KalmanFilter', # New module class
219
+ 'EMAlgorithm', # New module class
220
+ ])
221
+
222
+ # Add Trainer classes if available
223
+ try:
224
+ from .trainer import DFMTrainer, DDFMTrainer
225
+ _has_trainers = True
226
+ except ImportError:
227
+ _has_trainers = False
228
+ DFMTrainer = None # type: ignore
229
+ DDFMTrainer = None # type: ignore
230
+
231
+ if _has_trainers:
232
+ __all__.extend([
233
+ 'DFMTrainer',
234
+ 'DDFMTrainer',
235
+ ])
236
+
@@ -0,0 +1,65 @@
1
+ """Configuration subpackage for DFM.
2
+
3
+ This subpackage provides:
4
+ - Schema (DFMConfig, SeriesConfig, BlockConfig, Params) in schema.py
5
+ - IO (ConfigSource, YamlSource, etc.) in io.py
6
+ """
7
+
8
+ from .schema import (
9
+ BaseModelConfig, DFMConfig, DDFMConfig, SeriesConfig, BlockConfig,
10
+ DEFAULT_GLOBAL_BLOCK_NAME,
11
+ )
12
+ from .params import Params, FitParams
13
+ from .results import BaseResult, DFMResult, DDFMResult, DFMParams
14
+ from .utils import validate_frequency, validate_transformation
15
+ from .io import (
16
+ ConfigSource,
17
+ YamlSource,
18
+ DictSource,
19
+ HydraSource,
20
+ MergedConfigSource,
21
+ make_config_source,
22
+ from_spec,
23
+ # Note: Private functions (_load_config_from_dataframe, _write_series_blocks_yaml)
24
+ # are available from io module but not exported as public API
25
+ )
26
+ from .structure import (
27
+ FREQUENCY_HIERARCHY,
28
+ PERIODS_PER_YEAR,
29
+ get_periods_per_year,
30
+ compute_idio_chain_lengths,
31
+ get_tent_weights_for_pair,
32
+ generate_tent_weights,
33
+ generate_R_mat,
34
+ get_aggregation_structure,
35
+ group_series_by_frequency,
36
+ )
37
+
38
+ __all__ = [
39
+ # Schema
40
+ 'BaseModelConfig', 'DFMConfig', 'DDFMConfig', 'SeriesConfig', 'BlockConfig',
41
+ 'DEFAULT_GLOBAL_BLOCK_NAME',
42
+ # Parameters
43
+ 'Params', 'FitParams',
44
+ # Results
45
+ 'BaseResult', 'DFMResult', 'DDFMResult', 'DFMParams',
46
+ # Utilities
47
+ 'validate_frequency', 'validate_transformation',
48
+ # IO
49
+ 'ConfigSource', 'YamlSource', 'DictSource',
50
+ 'HydraSource', 'MergedConfigSource', 'make_config_source',
51
+ 'from_spec',
52
+ # Structure utilities
53
+ 'FREQUENCY_HIERARCHY',
54
+ 'PERIODS_PER_YEAR',
55
+ 'get_periods_per_year',
56
+ 'compute_idio_chain_lengths',
57
+ 'get_tent_weights_for_pair',
58
+ 'generate_tent_weights',
59
+ 'generate_R_mat',
60
+ 'get_aggregation_structure',
61
+ 'group_series_by_frequency',
62
+ # Note: Private functions (_load_config_from_dataframe, _write_series_blocks_yaml)
63
+ # are available from io module but not exported as public API
64
+ ]
65
+
@@ -15,7 +15,7 @@ from typing import Protocol, Optional, Dict, Any, Union, Tuple
15
15
  from pathlib import Path
16
16
  from dataclasses import is_dataclass, asdict
17
17
 
18
- from .config import DFMConfig, SeriesConfig, BlockConfig, DEFAULT_GLOBAL_BLOCK_NAME
18
+ from .schema import DFMConfig, SeriesConfig, BlockConfig, DEFAULT_GLOBAL_BLOCK_NAME
19
19
  import polars as pl
20
20
 
21
21
 
@@ -378,7 +378,7 @@ def _write_series_blocks_yaml(
378
378
  else:
379
379
  blocks_names = list(series.blocks)
380
380
  else:
381
- blocks_names = ['Block_Global']
381
+ blocks_names = [DEFAULT_GLOBAL_BLOCK_NAME]
382
382
 
383
383
  series_entry = {
384
384
  'series_name': series.series_name,
@@ -482,7 +482,7 @@ def from_spec(
482
482
  # Blocks are already names
483
483
  blocks_names = list(series.blocks)
484
484
  else:
485
- blocks_names = ['Block_Global'] # Default
485
+ blocks_names = [DEFAULT_GLOBAL_BLOCK_NAME] # Default
486
486
 
487
487
  series_entry = {
488
488
  'series_name': series.series_name,
@@ -764,7 +764,7 @@ def _load_config_from_dataframe(df: pl.DataFrame) -> DFMConfig:
764
764
  blocks = []
765
765
 
766
766
  if block_cols:
767
- # CSV format: Block_Global, Block_Consumption, etc. columns with 1/0 values
767
+ # CSV format: global block, block_1, etc. columns with 1/0 values
768
768
  for block_col in block_cols:
769
769
  if block_col in df_dict:
770
770
  block_value = df_dict[block_col][i]
@@ -789,13 +789,13 @@ def _load_config_from_dataframe(df: pl.DataFrame) -> DFMConfig:
789
789
  elif isinstance(blocks_str, list):
790
790
  blocks = blocks_str
791
791
  else:
792
- blocks = ['Block_Global']
792
+ blocks = [DEFAULT_GLOBAL_BLOCK_NAME]
793
793
  else:
794
- blocks = ['Block_Global']
794
+ blocks = [DEFAULT_GLOBAL_BLOCK_NAME]
795
795
 
796
- # Ensure at least Block_Global
796
+ # Ensure at least global block
797
797
  if not blocks:
798
- blocks = ['Block_Global']
798
+ blocks = [DEFAULT_GLOBAL_BLOCK_NAME]
799
799
 
800
800
  # Track block names
801
801
  for block in blocks:
@@ -824,7 +824,7 @@ def _load_config_from_dataframe(df: pl.DataFrame) -> DFMConfig:
824
824
  ))
825
825
 
826
826
  # Create default blocks if none specified
827
- block_names = sorted(block_names_set) if block_names_set else ['Block_Global']
827
+ block_names = sorted(block_names_set) if block_names_set else [DEFAULT_GLOBAL_BLOCK_NAME]
828
828
  blocks = {}
829
829
  for block_name in block_names:
830
830
  blocks[block_name] = BlockConfig(factors=1, ar_lag=1, clock='m')
@@ -0,0 +1,202 @@
1
+ """Parameter dataclasses for DFM models.
2
+
3
+ This module contains parameter configuration classes:
4
+ - Params: Global estimation parameters with defaults
5
+ - FitParams: Optional parameter overrides for model fitting
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Optional
10
+ from .utils import validate_frequency
11
+
12
+
13
+ @dataclass
14
+ class Params:
15
+ """Global estimation parameters for DFM (main settings only).
16
+
17
+ This dataclass contains only the global knobs that control the EM algorithm
18
+ and numerical stability. It does NOT include series or block definitions.
19
+ Use this with spec CSV files to provide main settings while the spec CSV
20
+ defines all series and their block memberships.
21
+
22
+ All parameters have defaults matching the standard YAML configuration.
23
+
24
+ Attributes
25
+ ----------
26
+ ar_lag : int
27
+ AR lag for factor transition equation (typically 1)
28
+ threshold : float
29
+ EM convergence threshold (default: 1e-5)
30
+ max_iter : int
31
+ Maximum EM iterations (default: 5000)
32
+ nan_method : int
33
+ Missing data handling method (1-5, default: 2 = spline interpolation).
34
+ This preprocessing step is followed by Kalman Filter-based missing data
35
+ handling during DFM estimation, following standard practice in state-space
36
+ models (Mariano & Murasawa 2003, Harvey 1989).
37
+ nan_k : int
38
+ Spline parameter for NaN interpolation (default: 3 for cubic spline)
39
+ clock : str
40
+ Base frequency for all latent factors (default: 'm' for monthly)
41
+ clip_ar_coefficients : bool
42
+ Enable AR coefficient clipping for stationarity (default: True)
43
+ ar_clip_min : float
44
+ Minimum AR coefficient (default: -0.99)
45
+ ar_clip_max : float
46
+ Maximum AR coefficient (default: 0.99)
47
+ warn_on_ar_clip : bool
48
+ Warn when AR coefficients are clipped (default: True)
49
+ clip_data_values : bool
50
+ Enable clipping of extreme data values (default: True)
51
+ data_clip_threshold : float
52
+ Clip values beyond this many standard deviations (default: 100.0)
53
+ warn_on_data_clip : bool
54
+ Warn when data values are clipped (default: True)
55
+ use_regularization : bool
56
+ Enable regularization for numerical stability (default: True)
57
+ regularization_scale : float
58
+ Scale factor for ridge regularization (default: 1e-5)
59
+ min_eigenvalue : float
60
+ Minimum eigenvalue for positive definite matrices (default: 1e-8)
61
+ max_eigenvalue : float
62
+ Maximum eigenvalue cap (default: 1e6)
63
+ warn_on_regularization : bool
64
+ Warn when regularization is applied (default: True)
65
+ use_damped_updates : bool
66
+ Enable damped updates when likelihood decreases (default: True)
67
+ damping_factor : float
68
+ Damping factor: 0.8 = 80% new, 20% old (default: 0.8)
69
+ warn_on_damped_update : bool
70
+ Warn when damped updates are used (default: True)
71
+ augment_idio : bool
72
+ Enable state augmentation with idiosyncratic components (default: True)
73
+ augment_idio_slow : bool
74
+ Enable tent-length chains for slower-frequency series (default: True)
75
+ idio_rho0 : float
76
+ Initial AR coefficient for idiosyncratic components (default: 0.1)
77
+ idio_min_var : float
78
+ Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
79
+
80
+ Examples
81
+ --------
82
+ >>> from dfm_python.config import Params
83
+ >>> params = Params(max_iter=100, threshold=1e-4)
84
+ >>> dfm.from_spec('data/spec.csv', params=params)
85
+ """
86
+ # Estimation parameters
87
+ ar_lag: int = 1
88
+ threshold: float = 1e-5
89
+ max_iter: int = 5000
90
+ nan_method: int = 2
91
+ nan_k: int = 3
92
+ clock: str = 'm'
93
+
94
+ # Numerical stability - AR clipping
95
+ clip_ar_coefficients: bool = True
96
+ ar_clip_min: float = -0.99
97
+ ar_clip_max: float = 0.99
98
+ warn_on_ar_clip: bool = True
99
+
100
+ # Numerical stability - Data clipping
101
+ clip_data_values: bool = True
102
+ data_clip_threshold: float = 100.0
103
+ warn_on_data_clip: bool = True
104
+
105
+ # Numerical stability - Regularization
106
+ use_regularization: bool = True
107
+ regularization_scale: float = 1e-5
108
+ min_eigenvalue: float = 1e-8
109
+ max_eigenvalue: float = 1e6
110
+ warn_on_regularization: bool = True
111
+
112
+ # Numerical stability - Damped updates
113
+ use_damped_updates: bool = True
114
+ damping_factor: float = 0.8
115
+ warn_on_damped_update: bool = True
116
+
117
+ # Idiosyncratic component augmentation
118
+ augment_idio: bool = True # Enable state augmentation with idiosyncratic components (default: True)
119
+ augment_idio_slow: bool = True # Enable tent-length chains for slower-frequency series (default: True)
120
+ idio_rho0: float = 0.1 # Initial AR coefficient for idiosyncratic components (default: 0.1)
121
+ idio_min_var: float = 1e-8 # Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
122
+
123
+ def __post_init__(self):
124
+ """Validate parameters."""
125
+ self.clock = validate_frequency(self.clock)
126
+ if self.threshold <= 0:
127
+ raise ValueError(f"threshold must be positive, got {self.threshold}")
128
+ if self.max_iter < 1:
129
+ raise ValueError(f"max_iter must be at least 1, got {self.max_iter}")
130
+ if self.ar_clip_min >= self.ar_clip_max:
131
+ raise ValueError(f"ar_clip_min ({self.ar_clip_min}) must be < ar_clip_max ({self.ar_clip_max})")
132
+ if not (-1 < self.ar_clip_min < 1):
133
+ raise ValueError(f"ar_clip_min must be in (-1, 1), got {self.ar_clip_min}")
134
+ if not (-1 < self.ar_clip_max < 1):
135
+ raise ValueError(f"ar_clip_max must be in (-1, 1), got {self.ar_clip_max}")
136
+
137
+
138
+ @dataclass
139
+ class FitParams:
140
+ """Parameters for fitting DFM models.
141
+
142
+ This dataclass groups all optional parameters that can override
143
+ config values during model fitting. This reduces method signature
144
+ complexity and improves code readability.
145
+
146
+ All parameters are optional. If None, the corresponding value
147
+ from DFMConfig will be used during parameter resolution.
148
+ """
149
+ # Convergence parameters
150
+ threshold: Optional[float] = None
151
+ max_iter: Optional[int] = None
152
+
153
+ # Model structure
154
+ ar_lag: Optional[int] = None
155
+ num_factors: Optional[int] = None
156
+
157
+ # Missing data handling
158
+ nan_method: Optional[int] = None
159
+ nan_k: Optional[int] = None
160
+
161
+ # AR coefficient clipping
162
+ clip_ar_coefficients: Optional[bool] = None
163
+ ar_clip_min: Optional[float] = None
164
+ ar_clip_max: Optional[float] = None
165
+
166
+ # Data clipping
167
+ clip_data_values: Optional[bool] = None
168
+ data_clip_threshold: Optional[float] = None
169
+
170
+ # Regularization
171
+ use_regularization: Optional[bool] = None
172
+ regularization_scale: Optional[float] = None
173
+ min_eigenvalue: Optional[float] = None
174
+ max_eigenvalue: Optional[float] = None
175
+
176
+ # Damping
177
+ use_damped_updates: Optional[bool] = None
178
+ damping_factor: Optional[float] = None
179
+
180
+ def to_dict(self) -> dict:
181
+ """Convert to dictionary, excluding None values."""
182
+ return {k: v for k, v in self.__dict__.items() if v is not None}
183
+
184
+ @classmethod
185
+ def from_kwargs(cls, **kwargs) -> 'FitParams':
186
+ """Create FitParams from keyword arguments.
187
+
188
+ Filters kwargs to only include valid parameter names,
189
+ ignoring any extra arguments.
190
+ """
191
+ valid_params = {
192
+ 'threshold', 'max_iter', 'ar_lag', 'num_factors',
193
+ 'nan_method', 'nan_k',
194
+ 'clip_ar_coefficients', 'ar_clip_min', 'ar_clip_max',
195
+ 'clip_data_values', 'data_clip_threshold',
196
+ 'use_regularization', 'regularization_scale',
197
+ 'min_eigenvalue', 'max_eigenvalue',
198
+ 'use_damped_updates', 'damping_factor'
199
+ }
200
+ filtered = {k: v for k, v in kwargs.items() if k in valid_params}
201
+ return cls(**filtered)
202
+