dfm-python 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dfm_python/__init__.py +2 -2
- dfm_python/{dfm_api.py → api.py} +41 -251
- dfm_python/config.py +72 -7
- dfm_python/core/__init__.py +59 -0
- dfm_python/core/diagnostics.py +274 -0
- dfm_python/core/em.py +1022 -0
- dfm_python/core/grouping.py +54 -0
- dfm_python/core/helpers.py +56 -0
- dfm_python/core/numeric.py +346 -0
- dfm_python/core/results.py +70 -0
- dfm_python/dfm.py +497 -3538
- dfm_python/kalman.py +18 -0
- dfm_python/utils/data_utils.py +3 -5
- dfm_python-0.2.1.dist-info/METADATA +310 -0
- dfm_python-0.2.1.dist-info/RECORD +21 -0
- dfm_python-0.2.0.dist-info/METADATA +0 -647
- dfm_python-0.2.0.dist-info/RECORD +0 -14
- {dfm_python-0.2.0.dist-info → dfm_python-0.2.1.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.0.dist-info → dfm_python-0.2.1.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -50,7 +50,7 @@ Example (Low-level API - For advanced usage):
|
|
|
50
50
|
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
__version__ = "0.2.
|
|
53
|
+
__version__ = "0.2.1"
|
|
54
54
|
|
|
55
55
|
from .config import DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME
|
|
56
56
|
from .config import (
|
|
@@ -69,7 +69,7 @@ from .kalman import run_kf, skf, fis, miss_data
|
|
|
69
69
|
from .news import update_nowcast, news_dfm, para_const
|
|
70
70
|
|
|
71
71
|
# Import high-level API (extends core DFM with convenience methods)
|
|
72
|
-
from .
|
|
72
|
+
from .api import DFM, _dfm_instance, from_yaml, from_spec, from_spec_df, from_dict
|
|
73
73
|
|
|
74
74
|
# Module-level API: expose singleton instance methods and properties
|
|
75
75
|
# This allows: import dfm_python as dfm; dfm.load_config(...); dfm.result
|
dfm_python/{dfm_api.py → api.py}
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""High-level API for Dynamic Factor Model.
|
|
1
|
+
"""High-level API for Dynamic Factor Model (convenience layer).
|
|
2
2
|
|
|
3
|
-
This module provides an object-oriented interface for DFM estimation
|
|
4
|
-
Use this for a simpler, more
|
|
3
|
+
This module provides an object-oriented interface for DFM estimation
|
|
4
|
+
on top of the core `DFM` in `dfm.py`. Use this for a simpler, more
|
|
5
|
+
intuitive workflow:
|
|
5
6
|
|
|
6
7
|
Example:
|
|
7
8
|
>>> import dfm_python as dfm
|
|
@@ -16,7 +17,7 @@ Example:
|
|
|
16
17
|
>>> factors = result.Z
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
from typing import Optional, Union, Dict, Any
|
|
20
|
+
from typing import Optional, Union, Dict, Any, Tuple
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
import numpy as np
|
|
22
23
|
import pandas as pd
|
|
@@ -29,6 +30,7 @@ from .config import (
|
|
|
29
30
|
MergedConfigSource,
|
|
30
31
|
)
|
|
31
32
|
from .dfm import DFM as _DFMCore, DFMResult
|
|
33
|
+
from .core.helpers import safe_get_method, safe_get_attr
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class DFM(_DFMCore):
|
|
@@ -84,53 +86,8 @@ class DFM(_DFMCore):
|
|
|
84
86
|
) -> 'DFM':
|
|
85
87
|
"""Load configuration from various sources.
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
It also supports merging configurations (e.g., YAML base + Spec CSV series).
|
|
90
|
-
|
|
91
|
-
Parameters
|
|
92
|
-
----------
|
|
93
|
-
source : str, Path, dict, DFMConfig, or ConfigSource, optional
|
|
94
|
-
Configuration source. If DFMConfig, used directly.
|
|
95
|
-
If str/Path, treated as YAML file path.
|
|
96
|
-
If dict, treated as dictionary config.
|
|
97
|
-
If ConfigSource, used directly.
|
|
98
|
-
yaml : str or Path, optional
|
|
99
|
-
Explicit YAML file path
|
|
100
|
-
mapping : dict, optional
|
|
101
|
-
Explicit dictionary config
|
|
102
|
-
spec : str or Path, optional
|
|
103
|
-
Explicit spec CSV file path (series definitions only)
|
|
104
|
-
hydra : DictConfig or dict, optional
|
|
105
|
-
Explicit Hydra config
|
|
106
|
-
base : str, Path, dict, or ConfigSource, optional
|
|
107
|
-
Base configuration (for merging). Used with `override`.
|
|
108
|
-
override : str, Path, dict, or ConfigSource, optional
|
|
109
|
-
Override configuration (for merging). Used with `base`.
|
|
110
|
-
Series from override replace base series.
|
|
111
|
-
|
|
112
|
-
Returns
|
|
113
|
-
-------
|
|
114
|
-
DFM
|
|
115
|
-
Self for method chaining.
|
|
116
|
-
|
|
117
|
-
Examples
|
|
118
|
-
--------
|
|
119
|
-
>>> # From YAML file
|
|
120
|
-
>>> dfm.load_config('config/default.yaml')
|
|
121
|
-
>>>
|
|
122
|
-
>>> # From dictionary
|
|
123
|
-
>>> dfm.load_config({'series': [...], 'clock': 'm'})
|
|
124
|
-
>>>
|
|
125
|
-
>>> # Explicit keyword arguments
|
|
126
|
-
>>> dfm.load_config(yaml='config/default.yaml')
|
|
127
|
-
>>> dfm.load_config(spec='data/spec.csv')
|
|
128
|
-
>>>
|
|
129
|
-
>>> # Merge YAML base + Spec CSV series
|
|
130
|
-
>>> dfm.load_config(base='config/default.yaml', override='data/spec.csv')
|
|
131
|
-
>>>
|
|
132
|
-
>>> # Direct DFMConfig object
|
|
133
|
-
>>> dfm.load_config(DFMConfig(...))
|
|
89
|
+
Unified interface for YAML files, dictionaries, spec CSV, or Hydra configs.
|
|
90
|
+
Supports merging configurations (e.g., YAML base + Spec CSV series).
|
|
134
91
|
"""
|
|
135
92
|
# Handle direct DFMConfig
|
|
136
93
|
if isinstance(source, DFMConfig):
|
|
@@ -171,29 +128,7 @@ class DFM(_DFMCore):
|
|
|
171
128
|
data_path: Optional[Union[str, Path]] = None,
|
|
172
129
|
data: Optional[np.ndarray] = None,
|
|
173
130
|
**kwargs) -> 'DFM':
|
|
174
|
-
"""Load data from file or use provided array.
|
|
175
|
-
|
|
176
|
-
Parameters
|
|
177
|
-
----------
|
|
178
|
-
data_path : str or Path, optional
|
|
179
|
-
Path to data file (CSV). If None, must provide `data`.
|
|
180
|
-
data : np.ndarray, optional
|
|
181
|
-
Data matrix directly (T x N). If None, must provide `data_path`.
|
|
182
|
-
**kwargs
|
|
183
|
-
Additional arguments passed to `load_data()`:
|
|
184
|
-
- sample_start: Start date for data sampling
|
|
185
|
-
- sample_end: End date for data sampling
|
|
186
|
-
|
|
187
|
-
Returns
|
|
188
|
-
-------
|
|
189
|
-
DFM
|
|
190
|
-
Self for method chaining.
|
|
191
|
-
|
|
192
|
-
Example:
|
|
193
|
-
>>> dfm.load_data('data/sample_data.csv')
|
|
194
|
-
>>> # or
|
|
195
|
-
>>> dfm.load_data(data=X, sample_start=pd.Timestamp('2000-01-01'))
|
|
196
|
-
"""
|
|
131
|
+
"""Load data from file or use provided array."""
|
|
197
132
|
if self._config is None:
|
|
198
133
|
raise ValueError("Configuration must be loaded before loading data. "
|
|
199
134
|
"Call load_config() first.")
|
|
@@ -203,14 +138,12 @@ class DFM(_DFMCore):
|
|
|
203
138
|
data_path, self._config, **kwargs
|
|
204
139
|
)
|
|
205
140
|
elif data is not None:
|
|
206
|
-
# If data is provided directly, we still need
|
|
207
|
-
# For now, assume it's already in the right format
|
|
208
|
-
# In the future, we might want to add transform_data() call here
|
|
141
|
+
# If data is provided directly, we still need a time index
|
|
209
142
|
self._data = data
|
|
210
143
|
if 'time' in kwargs:
|
|
211
144
|
self._time = kwargs['time']
|
|
212
145
|
else:
|
|
213
|
-
# Generate default
|
|
146
|
+
# Generate default monthly-end index
|
|
214
147
|
self._time = pd.date_range(
|
|
215
148
|
start='2000-01-01',
|
|
216
149
|
periods=len(data),
|
|
@@ -226,25 +159,7 @@ class DFM(_DFMCore):
|
|
|
226
159
|
threshold: Optional[float] = None,
|
|
227
160
|
max_iter: Optional[int] = None,
|
|
228
161
|
**kwargs) -> 'DFM':
|
|
229
|
-
"""Train the DFM model.
|
|
230
|
-
|
|
231
|
-
Parameters
|
|
232
|
-
----------
|
|
233
|
-
threshold : float, optional
|
|
234
|
-
EM convergence threshold. If None, uses config.threshold.
|
|
235
|
-
max_iter : int, optional
|
|
236
|
-
Maximum EM iterations. If None, uses config.max_iter.
|
|
237
|
-
**kwargs
|
|
238
|
-
Additional arguments (for future extensibility).
|
|
239
|
-
|
|
240
|
-
Returns
|
|
241
|
-
-------
|
|
242
|
-
DFM
|
|
243
|
-
Self for method chaining.
|
|
244
|
-
|
|
245
|
-
Example:
|
|
246
|
-
>>> dfm.train(threshold=1e-4, max_iter=100)
|
|
247
|
-
"""
|
|
162
|
+
"""Train the DFM model using EM algorithm."""
|
|
248
163
|
if self._config is None:
|
|
249
164
|
raise ValueError("Configuration must be loaded before training. "
|
|
250
165
|
"Call load_config() first.")
|
|
@@ -262,23 +177,16 @@ class DFM(_DFMCore):
|
|
|
262
177
|
# Attach metadata for convenient OOP access
|
|
263
178
|
if self._time is not None:
|
|
264
179
|
self._result.time_index = self._time
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
180
|
+
series_ids = safe_get_method(self._config, 'get_series_ids')
|
|
181
|
+
if series_ids is not None:
|
|
182
|
+
self._result.series_ids = series_ids
|
|
183
|
+
block_names = safe_get_attr(self._config, 'block_names')
|
|
184
|
+
if block_names is not None:
|
|
185
|
+
self._result.block_names = block_names
|
|
269
186
|
return self
|
|
270
187
|
|
|
271
188
|
def reset(self) -> 'DFM':
|
|
272
|
-
"""Reset all state (config, data, results).
|
|
273
|
-
|
|
274
|
-
Returns
|
|
275
|
-
-------
|
|
276
|
-
DFM
|
|
277
|
-
Self for method chaining.
|
|
278
|
-
|
|
279
|
-
Example:
|
|
280
|
-
>>> dfm.reset() # Clear everything
|
|
281
|
-
"""
|
|
189
|
+
"""Reset all state (config, data, results)."""
|
|
282
190
|
super().__init__()
|
|
283
191
|
self._time = None
|
|
284
192
|
self._original_data = None
|
|
@@ -287,24 +195,29 @@ class DFM(_DFMCore):
|
|
|
287
195
|
# ---------------------------------------------------------------------
|
|
288
196
|
# Inference API
|
|
289
197
|
# ---------------------------------------------------------------------
|
|
290
|
-
def predict(self, horizon: int = 12, *, return_series: bool = True, return_factors: bool = True):
|
|
198
|
+
def predict(self, horizon: int = 12, *, return_series: bool = True, return_factors: bool = True) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]:
|
|
291
199
|
"""Forecast series and/or factors for a given horizon using the trained model.
|
|
292
200
|
|
|
293
201
|
Parameters
|
|
294
202
|
----------
|
|
295
203
|
horizon : int
|
|
296
|
-
Number of periods to forecast
|
|
204
|
+
Number of periods ahead to forecast (default: 12)
|
|
297
205
|
return_series : bool
|
|
298
|
-
|
|
206
|
+
Whether to return forecasted series (default: True)
|
|
299
207
|
return_factors : bool
|
|
300
|
-
|
|
301
|
-
|
|
208
|
+
Whether to return forecasted factors (default: True)
|
|
209
|
+
|
|
302
210
|
Returns
|
|
303
211
|
-------
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
212
|
+
If both return_series and return_factors are True:
|
|
213
|
+
Tuple[np.ndarray, np.ndarray]
|
|
214
|
+
(X_forecast, Z_forecast) where X_forecast is (horizon x N) and Z_forecast is (horizon x m)
|
|
215
|
+
If only return_series is True:
|
|
216
|
+
np.ndarray
|
|
217
|
+
X_forecast (horizon x N)
|
|
218
|
+
If only return_factors is True:
|
|
219
|
+
np.ndarray
|
|
220
|
+
Z_forecast (horizon x m)
|
|
308
221
|
"""
|
|
309
222
|
if self._result is None:
|
|
310
223
|
raise ValueError("Model must be trained before calling predict(). Call train() first.")
|
|
@@ -335,26 +248,7 @@ class DFM(_DFMCore):
|
|
|
335
248
|
|
|
336
249
|
def plot(self, *, kind: str = 'factor', factor_index: int = 0, forecast_horizon: Optional[int] = None,
|
|
337
250
|
save_path: Optional[Union[str, Path]] = None, show: bool = False):
|
|
338
|
-
"""Quick plotting utility for common visualizations.
|
|
339
|
-
|
|
340
|
-
Parameters
|
|
341
|
-
----------
|
|
342
|
-
kind : str
|
|
343
|
-
What to plot. Supported: 'factor' (single factor time series).
|
|
344
|
-
factor_index : int
|
|
345
|
-
Which factor index to plot (default: 0 = first/global).
|
|
346
|
-
forecast_horizon : int, optional
|
|
347
|
-
If provided, overlay deterministic factor forecast.
|
|
348
|
-
save_path : str or Path, optional
|
|
349
|
-
If provided, save the figure to this path.
|
|
350
|
-
show : bool
|
|
351
|
-
If True, display the figure (useful in notebooks).
|
|
352
|
-
|
|
353
|
-
Returns
|
|
354
|
-
-------
|
|
355
|
-
Path | None
|
|
356
|
-
Save path if saved, else None.
|
|
357
|
-
"""
|
|
251
|
+
"""Quick plotting utility for common visualizations."""
|
|
358
252
|
if self._result is None:
|
|
359
253
|
raise ValueError("No results to plot. Train the model first.")
|
|
360
254
|
try:
|
|
@@ -379,7 +273,7 @@ class DFM(_DFMCore):
|
|
|
379
273
|
Z_fore = self.predict(forecast_horizon, return_series=False, return_factors=True)
|
|
380
274
|
# Build forecast dates
|
|
381
275
|
last_date = time_hist[-1]
|
|
382
|
-
# Use same monthly spacing; if freq missing, fallback to monthly
|
|
276
|
+
# Use same monthly-end spacing; if freq missing, fallback to monthly-end
|
|
383
277
|
try:
|
|
384
278
|
forecast_dates = pd.date_range(start=last_date + pd.tseries.frequencies.to_offset('ME'),
|
|
385
279
|
periods=forecast_horizon, freq='ME')
|
|
@@ -391,7 +285,6 @@ class DFM(_DFMCore):
|
|
|
391
285
|
|
|
392
286
|
title_block = None
|
|
393
287
|
if hasattr(self._result, 'block_names') and self._result.block_names:
|
|
394
|
-
# factor grouping is block-wise; use index 0 as 'Global' heuristic
|
|
395
288
|
title_block = self._result.block_names[0] if factor_index == 0 else None
|
|
396
289
|
plt.title(f"Factor {factor_index}" + (f" ({title_block})" if title_block else ""))
|
|
397
290
|
plt.xlabel("Date")
|
|
@@ -428,13 +321,7 @@ def load_config(
|
|
|
428
321
|
base: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
429
322
|
override: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
430
323
|
) -> DFM:
|
|
431
|
-
"""Load configuration (module-level convenience function).
|
|
432
|
-
|
|
433
|
-
This is a convenience function that uses the global DFM instance.
|
|
434
|
-
For multiple models, create separate DFM() instances.
|
|
435
|
-
|
|
436
|
-
See DFM.load_config() for parameter documentation.
|
|
437
|
-
"""
|
|
324
|
+
"""Load configuration (module-level convenience function)."""
|
|
438
325
|
return _dfm_instance.load_config(
|
|
439
326
|
source=source,
|
|
440
327
|
yaml=yaml,
|
|
@@ -477,58 +364,12 @@ def reset() -> DFM:
|
|
|
477
364
|
|
|
478
365
|
# Convenience constructors for cleaner API
|
|
479
366
|
def from_yaml(yaml_path: Union[str, Path]) -> DFM:
|
|
480
|
-
"""Load configuration from YAML file (convenience constructor).
|
|
481
|
-
|
|
482
|
-
This is a cleaner alternative to `load_config(yaml=...)`.
|
|
483
|
-
|
|
484
|
-
Parameters
|
|
485
|
-
----------
|
|
486
|
-
yaml_path : str or Path
|
|
487
|
-
Path to YAML configuration file
|
|
488
|
-
|
|
489
|
-
Returns
|
|
490
|
-
-------
|
|
491
|
-
DFM
|
|
492
|
-
DFM instance for method chaining
|
|
493
|
-
|
|
494
|
-
Examples
|
|
495
|
-
--------
|
|
496
|
-
>>> import dfm_python as dfm
|
|
497
|
-
>>> dfm.from_yaml('config/default.yaml')
|
|
498
|
-
>>> dfm.load_data('data/sample_data.csv')
|
|
499
|
-
>>> dfm.train(max_iter=1)
|
|
500
|
-
"""
|
|
367
|
+
"""Load configuration from YAML file (convenience constructor)."""
|
|
501
368
|
return _dfm_instance.load_config(yaml=yaml_path)
|
|
502
369
|
|
|
503
370
|
|
|
504
371
|
def from_spec(spec_path: Union[str, Path], params: Optional[Params] = None) -> DFM:
|
|
505
|
-
"""Load configuration from spec CSV file (convenience constructor).
|
|
506
|
-
|
|
507
|
-
This is a cleaner alternative to `load_config(config=params, spec=...)`.
|
|
508
|
-
The spec CSV defines all series and their block memberships.
|
|
509
|
-
The params object (optional) provides main settings (threshold, max_iter, etc.).
|
|
510
|
-
|
|
511
|
-
Parameters
|
|
512
|
-
----------
|
|
513
|
-
spec_path : str or Path
|
|
514
|
-
Path to spec CSV file (series definitions)
|
|
515
|
-
params : Params, optional
|
|
516
|
-
Global estimation parameters. If None, uses defaults from Params().
|
|
517
|
-
|
|
518
|
-
Returns
|
|
519
|
-
-------
|
|
520
|
-
DFM
|
|
521
|
-
DFM instance for method chaining
|
|
522
|
-
|
|
523
|
-
Examples
|
|
524
|
-
--------
|
|
525
|
-
>>> from dfm_python.config import Params
|
|
526
|
-
>>> import dfm_python as dfm
|
|
527
|
-
>>> params = Params(max_iter=100, threshold=1e-4)
|
|
528
|
-
>>> dfm.from_spec('data/spec.csv', params=params)
|
|
529
|
-
>>> dfm.load_data('data/sample_data.csv')
|
|
530
|
-
>>> dfm.train(max_iter=params.max_iter)
|
|
531
|
-
"""
|
|
372
|
+
"""Load configuration from spec CSV file (convenience constructor)."""
|
|
532
373
|
from .config import SpecCSVSource
|
|
533
374
|
|
|
534
375
|
if params is None:
|
|
@@ -548,33 +389,7 @@ def from_spec(spec_path: Union[str, Path], params: Optional[Params] = None) -> D
|
|
|
548
389
|
|
|
549
390
|
|
|
550
391
|
def from_spec_df(spec_df: pd.DataFrame, params: Optional[Params] = None) -> DFM:
|
|
551
|
-
"""Load configuration from spec DataFrame (convenience constructor).
|
|
552
|
-
|
|
553
|
-
Same as `from_spec()` but accepts a pandas DataFrame instead of a CSV file path.
|
|
554
|
-
Useful when you already have the spec data in memory.
|
|
555
|
-
|
|
556
|
-
Parameters
|
|
557
|
-
----------
|
|
558
|
-
spec_df : pd.DataFrame
|
|
559
|
-
DataFrame containing series definitions (same format as spec CSV)
|
|
560
|
-
params : Params, optional
|
|
561
|
-
Global estimation parameters. If None, uses defaults from Params().
|
|
562
|
-
|
|
563
|
-
Returns
|
|
564
|
-
-------
|
|
565
|
-
DFM
|
|
566
|
-
DFM instance for method chaining
|
|
567
|
-
|
|
568
|
-
Examples
|
|
569
|
-
--------
|
|
570
|
-
>>> import pandas as pd
|
|
571
|
-
>>> from dfm_python.config import Params
|
|
572
|
-
>>> import dfm_python as dfm
|
|
573
|
-
>>> spec_df = pd.read_csv('data/spec.csv')
|
|
574
|
-
>>> params = Params(max_iter=100)
|
|
575
|
-
>>> dfm.from_spec_df(spec_df, params=params)
|
|
576
|
-
>>> dfm.load_data('data/sample_data.csv')
|
|
577
|
-
"""
|
|
392
|
+
"""Load configuration from spec DataFrame (convenience constructor)."""
|
|
578
393
|
from .data_loader import _load_config_from_dataframe
|
|
579
394
|
|
|
580
395
|
if params is None:
|
|
@@ -593,33 +408,7 @@ def from_spec_df(spec_df: pd.DataFrame, params: Optional[Params] = None) -> DFM:
|
|
|
593
408
|
|
|
594
409
|
|
|
595
410
|
def from_dict(mapping: Dict[str, Any]) -> DFM:
|
|
596
|
-
"""Load configuration from dictionary (convenience constructor).
|
|
597
|
-
|
|
598
|
-
This is a cleaner alternative to `load_config(mapping=...)`.
|
|
599
|
-
Useful for programmatic configuration without YAML or CSV files.
|
|
600
|
-
|
|
601
|
-
Parameters
|
|
602
|
-
----------
|
|
603
|
-
mapping : dict
|
|
604
|
-
Dictionary containing full DFM configuration (series, blocks, params)
|
|
605
|
-
|
|
606
|
-
Returns
|
|
607
|
-
-------
|
|
608
|
-
DFM
|
|
609
|
-
DFM instance for method chaining
|
|
610
|
-
|
|
611
|
-
Examples
|
|
612
|
-
--------
|
|
613
|
-
>>> import dfm_python as dfm
|
|
614
|
-
>>> config_dict = {
|
|
615
|
-
... 'clock': 'm',
|
|
616
|
-
... 'max_iter': 100,
|
|
617
|
-
... 'blocks': {'Block_Global': {'factors': 1, 'clock': 'm'}},
|
|
618
|
-
... 'series': [{'series_id': 's1', 'frequency': 'm', 'transformation': 'lin', 'blocks': [0]}]
|
|
619
|
-
... }
|
|
620
|
-
>>> dfm.from_dict(config_dict)
|
|
621
|
-
>>> dfm.load_data('data/sample_data.csv')
|
|
622
|
-
"""
|
|
411
|
+
"""Load configuration from dictionary (convenience constructor)."""
|
|
623
412
|
return _dfm_instance.load_config(mapping=mapping)
|
|
624
413
|
|
|
625
414
|
|
|
@@ -628,3 +417,4 @@ def from_dict(mapping: Dict[str, Any]) -> DFM:
|
|
|
628
417
|
__all__ = ['DFM', 'load_config', 'load_data', 'train', 'reset',
|
|
629
418
|
'from_yaml', 'from_spec', 'from_spec_df', 'from_dict']
|
|
630
419
|
|
|
420
|
+
|
dfm_python/config.py
CHANGED
|
@@ -1050,7 +1050,28 @@ class DictSource:
|
|
|
1050
1050
|
self.mapping = mapping
|
|
1051
1051
|
|
|
1052
1052
|
def load(self) -> DFMConfig:
|
|
1053
|
-
"""Load configuration from dictionary.
|
|
1053
|
+
"""Load configuration from dictionary.
|
|
1054
|
+
|
|
1055
|
+
If the dictionary is partial (e.g., only max_iter, threshold),
|
|
1056
|
+
it will be merged with a minimal default config.
|
|
1057
|
+
"""
|
|
1058
|
+
# Check if this is a partial config (missing series or blocks)
|
|
1059
|
+
has_series = 'series' in self.mapping and self.mapping['series']
|
|
1060
|
+
has_blocks = 'blocks' in self.mapping and self.mapping['blocks']
|
|
1061
|
+
|
|
1062
|
+
if not has_series or not has_blocks:
|
|
1063
|
+
# This is a partial config - create a minimal default and merge
|
|
1064
|
+
minimal_default = {
|
|
1065
|
+
'series': [],
|
|
1066
|
+
'blocks': {},
|
|
1067
|
+
'clock': 'm',
|
|
1068
|
+
'max_iter': 5000,
|
|
1069
|
+
'threshold': 1e-5
|
|
1070
|
+
}
|
|
1071
|
+
# Merge: mapping takes precedence
|
|
1072
|
+
merged = {**minimal_default, **self.mapping}
|
|
1073
|
+
return DFMConfig.from_dict(merged)
|
|
1074
|
+
|
|
1054
1075
|
return DFMConfig.from_dict(self.mapping)
|
|
1055
1076
|
|
|
1056
1077
|
|
|
@@ -1142,10 +1163,36 @@ class MergedConfigSource:
|
|
|
1142
1163
|
from dataclasses import fields
|
|
1143
1164
|
|
|
1144
1165
|
base_cfg = self.base.load()
|
|
1145
|
-
|
|
1166
|
+
|
|
1167
|
+
# Check if override is a partial config (DictSource with partial dict)
|
|
1168
|
+
override_is_partial = False
|
|
1169
|
+
if isinstance(self.override, DictSource):
|
|
1170
|
+
has_series = 'series' in self.override.mapping and self.override.mapping['series']
|
|
1171
|
+
has_blocks = 'blocks' in self.override.mapping and self.override.mapping['blocks']
|
|
1172
|
+
override_is_partial = not (has_series and has_blocks)
|
|
1173
|
+
|
|
1174
|
+
if override_is_partial:
|
|
1175
|
+
# Handle partial override: merge fields directly without loading full DFMConfig
|
|
1176
|
+
override_dict = self.override.mapping
|
|
1177
|
+
override_cfg = base_cfg # Use base as template
|
|
1178
|
+
else:
|
|
1179
|
+
override_cfg = self.override.load()
|
|
1146
1180
|
|
|
1147
1181
|
# Merge blocks: override takes precedence
|
|
1148
|
-
|
|
1182
|
+
if override_is_partial and 'blocks' in override_dict:
|
|
1183
|
+
# Merge block dicts
|
|
1184
|
+
merged_blocks = {**base_cfg.blocks, **override_dict['blocks']}
|
|
1185
|
+
else:
|
|
1186
|
+
merged_blocks = {**base_cfg.blocks, **override_cfg.blocks}
|
|
1187
|
+
|
|
1188
|
+
# Use override's series if provided and non-empty, otherwise use base's series
|
|
1189
|
+
if override_is_partial:
|
|
1190
|
+
if 'series' in override_dict and override_dict['series']:
|
|
1191
|
+
merged_series = override_dict['series']
|
|
1192
|
+
else:
|
|
1193
|
+
merged_series = base_cfg.series
|
|
1194
|
+
else:
|
|
1195
|
+
merged_series = override_cfg.series if (override_cfg.series and len(override_cfg.series) > 0) else base_cfg.series
|
|
1149
1196
|
|
|
1150
1197
|
# Get all config fields (excluding derived/computed fields)
|
|
1151
1198
|
excluded_fields = {'series', 'blocks', 'block_names', 'factors_per_block', '_cached_blocks'}
|
|
@@ -1155,11 +1202,29 @@ class MergedConfigSource:
|
|
|
1155
1202
|
if field.name not in excluded_fields
|
|
1156
1203
|
}
|
|
1157
1204
|
|
|
1158
|
-
#
|
|
1205
|
+
# Override settings from override_cfg or override_dict
|
|
1206
|
+
if override_is_partial:
|
|
1207
|
+
override_settings = {
|
|
1208
|
+
field.name: override_dict.get(field.name, getattr(base_cfg, field.name))
|
|
1209
|
+
for field in fields(DFMConfig)
|
|
1210
|
+
if field.name not in excluded_fields
|
|
1211
|
+
}
|
|
1212
|
+
else:
|
|
1213
|
+
override_settings = {
|
|
1214
|
+
field.name: getattr(override_cfg, field.name)
|
|
1215
|
+
for field in fields(DFMConfig)
|
|
1216
|
+
if field.name not in excluded_fields
|
|
1217
|
+
and hasattr(override_cfg, field.name)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
# Merge settings: base + override (override takes precedence)
|
|
1221
|
+
merged_settings = {**base_settings, **override_settings}
|
|
1222
|
+
|
|
1223
|
+
# Create merged config: merged settings + merged series + merged blocks
|
|
1159
1224
|
return DFMConfig(
|
|
1160
|
-
series=
|
|
1161
|
-
blocks=merged_blocks,
|
|
1162
|
-
**
|
|
1225
|
+
series=merged_series,
|
|
1226
|
+
blocks=merged_blocks,
|
|
1227
|
+
**merged_settings
|
|
1163
1228
|
)
|
|
1164
1229
|
|
|
1165
1230
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Core utilities for DFM estimation.
|
|
2
|
+
|
|
3
|
+
This package contains core functionality organized by domain:
|
|
4
|
+
- em.py: EM algorithm core (init_conditions, em_step, em_converged)
|
|
5
|
+
- numeric.py: Numerical utilities (matrix operations, regularization, clipping)
|
|
6
|
+
- diagnostics.py: Diagnostic functions and output formatting
|
|
7
|
+
- results.py: Result metrics (RMSE calculation)
|
|
8
|
+
- grouping.py: Frequency grouping utilities
|
|
9
|
+
- helpers.py: Common helper functions for code patterns
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .em import init_conditions, em_step, em_converged
|
|
13
|
+
from .numeric import (
|
|
14
|
+
_ensure_symmetric,
|
|
15
|
+
_compute_principal_components,
|
|
16
|
+
_clean_matrix,
|
|
17
|
+
_ensure_positive_definite,
|
|
18
|
+
_compute_regularization_param,
|
|
19
|
+
_clip_ar_coefficients,
|
|
20
|
+
_apply_ar_clipping,
|
|
21
|
+
_cap_max_eigenvalue,
|
|
22
|
+
_estimate_ar_coefficient,
|
|
23
|
+
_safe_divide,
|
|
24
|
+
_check_finite,
|
|
25
|
+
_ensure_square_matrix,
|
|
26
|
+
)
|
|
27
|
+
from .results import calculate_rmse
|
|
28
|
+
from .grouping import group_series_by_frequency
|
|
29
|
+
from .diagnostics import (
|
|
30
|
+
_display_dfm_tables,
|
|
31
|
+
diagnose_series,
|
|
32
|
+
print_series_diagnosis,
|
|
33
|
+
)
|
|
34
|
+
from .helpers import safe_get_method, safe_get_attr
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
'init_conditions',
|
|
38
|
+
'em_step',
|
|
39
|
+
'em_converged',
|
|
40
|
+
'_ensure_symmetric',
|
|
41
|
+
'_compute_principal_components',
|
|
42
|
+
'_clean_matrix',
|
|
43
|
+
'_ensure_positive_definite',
|
|
44
|
+
'_compute_regularization_param',
|
|
45
|
+
'_clip_ar_coefficients',
|
|
46
|
+
'_apply_ar_clipping',
|
|
47
|
+
'_cap_max_eigenvalue',
|
|
48
|
+
'_estimate_ar_coefficient',
|
|
49
|
+
'_safe_divide',
|
|
50
|
+
'_check_finite',
|
|
51
|
+
'_ensure_square_matrix',
|
|
52
|
+
'calculate_rmse',
|
|
53
|
+
'group_series_by_frequency',
|
|
54
|
+
'_display_dfm_tables',
|
|
55
|
+
'diagnose_series',
|
|
56
|
+
'print_series_diagnosis',
|
|
57
|
+
'safe_get_method',
|
|
58
|
+
'safe_get_attr',
|
|
59
|
+
]
|