dfm-python 0.5.3__py3-none-any.whl → 0.5.5__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 +9 -5
- dfm_python/config/schema.py +169 -218
- dfm_python/data/utils.py +10 -12
- dfm_python/decoder/__init__.py +0 -0
- dfm_python/encoder/vae.py +0 -121
- dfm_python/lightning/__init__.py +6 -6
- dfm_python/lightning/ddfm_dm.py +581 -0
- dfm_python/lightning/dfm_dm.py +379 -0
- dfm_python/lightning/utils.py +496 -0
- dfm_python/models/base.py +10 -1
- dfm_python/models/ddfm.py +10 -7
- dfm_python/trainer/ddfm.py +2 -2
- {dfm_python-0.5.3.dist-info → dfm_python-0.5.5.dist-info}/METADATA +97 -3
- {dfm_python-0.5.3.dist-info → dfm_python-0.5.5.dist-info}/RECORD +16 -14
- dfm_python/lightning/data_module.py +0 -771
- dfm_python/lightning/scaling.py +0 -444
- {dfm_python-0.5.3.dist-info → dfm_python-0.5.5.dist-info}/WHEEL +0 -0
- {dfm_python-0.5.3.dist-info → dfm_python-0.5.5.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -65,7 +65,7 @@ Note: DFMConfig and SeriesConfig are internal implementation details.
|
|
|
65
65
|
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
|
-
__version__ = "0.5.
|
|
68
|
+
__version__ = "0.5.5"
|
|
69
69
|
|
|
70
70
|
# ============================================================================
|
|
71
71
|
# PUBLIC API DEFINITION
|
|
@@ -85,8 +85,8 @@ __version__ = "0.5.2"
|
|
|
85
85
|
# Configuration (from config/ subpackage)
|
|
86
86
|
from .config import (
|
|
87
87
|
DEFAULT_BLOCK_NAME,
|
|
88
|
-
ConfigSource, YamlSource,
|
|
89
|
-
|
|
88
|
+
ConfigSource, YamlSource, HydraSource,
|
|
89
|
+
make_config_source,
|
|
90
90
|
)
|
|
91
91
|
# Internal imports (for backward compatibility, but not recommended)
|
|
92
92
|
from .config import DFMConfig, SeriesConfig # Internal use only
|
|
@@ -102,7 +102,9 @@ from .utils.time import calculate_rmse
|
|
|
102
102
|
# Users can import these directly from dfm_python
|
|
103
103
|
from .lightning import (
|
|
104
104
|
DFMDataModule,
|
|
105
|
+
DDFMDataModule, # DDFM-specific DataModule
|
|
105
106
|
DFMDataset, # Dataset class (usually not needed directly)
|
|
107
|
+
DDFMDataset, # DDFM Dataset class (usually not needed directly)
|
|
106
108
|
KalmanFilter, # Module class
|
|
107
109
|
EMAlgorithm, # Module class
|
|
108
110
|
)
|
|
@@ -122,8 +124,8 @@ __all__ = [
|
|
|
122
124
|
# Constants
|
|
123
125
|
'DEFAULT_BLOCK_NAME',
|
|
124
126
|
# Config sources
|
|
125
|
-
'ConfigSource', 'YamlSource', '
|
|
126
|
-
'
|
|
127
|
+
'ConfigSource', 'YamlSource', 'HydraSource',
|
|
128
|
+
'make_config_source',
|
|
127
129
|
# Low-level API (functional interface - advanced usage)
|
|
128
130
|
'BaseResult', 'DFMResult', 'DDFMResult', 'calculate_rmse', 'diagnose_series', 'print_series_diagnosis',
|
|
129
131
|
]
|
|
@@ -137,7 +139,9 @@ __all__.extend([
|
|
|
137
139
|
# Lightning modules (mandatory dependency)
|
|
138
140
|
__all__.extend([
|
|
139
141
|
'DFMDataModule',
|
|
142
|
+
'DDFMDataModule', # DDFM-specific DataModule
|
|
140
143
|
'DFMDataset', # Dataset class (usually not needed directly)
|
|
144
|
+
'DDFMDataset', # DDFM Dataset class (usually not needed directly)
|
|
141
145
|
'KalmanFilter', # Module class
|
|
142
146
|
'EMAlgorithm', # Module class
|
|
143
147
|
])
|
dfm_python/config/schema.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
"""Configuration schema for DFM.
|
|
2
2
|
|
|
3
3
|
This module provides the core configuration dataclasses:
|
|
4
|
-
- BaseModelConfig: Base class with shared model structure
|
|
5
|
-
- DFMConfig(BaseModelConfig): Linear DFM with EM algorithm parameters
|
|
6
|
-
- DDFMConfig(BaseModelConfig): Deep DFM with neural network training parameters
|
|
4
|
+
- BaseModelConfig: Base class with shared model structure (series, clock, data handling)
|
|
5
|
+
- DFMConfig(BaseModelConfig): Linear DFM with EM algorithm parameters and block structure
|
|
6
|
+
- DDFMConfig(BaseModelConfig): Deep DFM with neural network training parameters (no blocks)
|
|
7
7
|
- SeriesConfig: Component configurations
|
|
8
8
|
|
|
9
9
|
Note: Parameter classes (Params, FitParams) are in config/params.py
|
|
10
10
|
Note: Validation functions are in config/utils.py
|
|
11
11
|
|
|
12
12
|
The configuration hierarchy:
|
|
13
|
-
- BaseModelConfig: Model structure (series,
|
|
14
|
-
- DFMConfig: Adds EM algorithm parameters (max_iter, threshold, regularization)
|
|
15
|
-
- DDFMConfig: Adds neural network parameters (epochs, learning_rate, encoder_layers)
|
|
13
|
+
- BaseModelConfig: Model structure (series, clock, data handling) - NO blocks
|
|
14
|
+
- DFMConfig: Adds blocks structure and EM algorithm parameters (max_iter, threshold, regularization)
|
|
15
|
+
- DDFMConfig: Adds neural network parameters (epochs, learning_rate, encoder_layers) - NO blocks
|
|
16
16
|
|
|
17
|
-
Blocks are defined as Dict[str, Dict[str, Any]] where each block is a dict with:
|
|
17
|
+
Blocks are DFM-specific and defined as Dict[str, Dict[str, Any]] where each block is a dict with:
|
|
18
18
|
- factors: int (number of factors)
|
|
19
19
|
- ar_lag: int (AR lag order)
|
|
20
20
|
- clock: str (block clock frequency)
|
|
@@ -282,19 +282,18 @@ class BaseModelConfig:
|
|
|
282
282
|
This base class contains the model structure that is common to both
|
|
283
283
|
DFM (linear) and DDFM (deep) models:
|
|
284
284
|
- Series definitions
|
|
285
|
-
- Block structure
|
|
286
285
|
- Clock frequency
|
|
287
286
|
- Data preprocessing (missing data handling)
|
|
288
287
|
|
|
288
|
+
Note: Blocks are DFM-specific and are NOT included in BaseModelConfig.
|
|
289
|
+
DFMConfig adds block structure, while DDFMConfig does not use blocks.
|
|
290
|
+
|
|
289
291
|
Subclasses (DFMConfig, DDFMConfig) add model-specific training parameters.
|
|
290
292
|
"""
|
|
291
293
|
# ========================================================================
|
|
292
294
|
# Model Structure (WHAT - defines the model)
|
|
293
295
|
# ========================================================================
|
|
294
296
|
series: List[SeriesConfig] # Series specifications
|
|
295
|
-
blocks: Dict[str, Dict[str, Any]] # Block configurations (block_name -> {factors, ar_lag, clock, notes})
|
|
296
|
-
block_names: List[str] = field(init=False) # Block names in order (derived from blocks dict)
|
|
297
|
-
factors_per_block: List[int] = field(init=False) # Number of factors per block (derived from blocks)
|
|
298
297
|
|
|
299
298
|
# ========================================================================
|
|
300
299
|
# Shared Data Handling Parameters
|
|
@@ -304,15 +303,155 @@ class BaseModelConfig:
|
|
|
304
303
|
clock: str = 'm' # Base frequency for nowcasting (global clock): 'd', 'w', 'm', 'q', 'sa', 'a' (defaults to 'm' for monthly)
|
|
305
304
|
scaler: Optional[str] = 'standard' # Unified scaler type for all series: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or None (no scaling). Default: 'standard' for unified scaling.
|
|
306
305
|
|
|
306
|
+
def __post_init__(self):
|
|
307
|
+
"""Validate basic model structure.
|
|
308
|
+
|
|
309
|
+
This method performs basic validation of the model configuration:
|
|
310
|
+
- Ensures at least one series is specified
|
|
311
|
+
- Validates clock frequency
|
|
312
|
+
- Auto-generates series_id if not provided
|
|
313
|
+
|
|
314
|
+
Raises
|
|
315
|
+
------
|
|
316
|
+
ValueError
|
|
317
|
+
If any validation check fails, with a descriptive error message
|
|
318
|
+
indicating what needs to be fixed.
|
|
319
|
+
"""
|
|
320
|
+
# Import frequency hierarchy for validation
|
|
321
|
+
from .utils import FREQUENCY_HIERARCHY
|
|
322
|
+
|
|
323
|
+
if not self.series:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
"Model configuration must contain at least one series. "
|
|
326
|
+
"Please add series definitions to your configuration."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Validate global clock
|
|
330
|
+
self.clock = validate_frequency(self.clock)
|
|
331
|
+
|
|
332
|
+
# Auto-generate series_id if not provided
|
|
333
|
+
for i, s in enumerate(self.series):
|
|
334
|
+
if s.series_id is None:
|
|
335
|
+
s.series_id = f"series_{i}"
|
|
336
|
+
if s.series_name is None:
|
|
337
|
+
s.series_name = s.series_id
|
|
338
|
+
|
|
307
339
|
# ========================================================================
|
|
308
|
-
#
|
|
340
|
+
# Helper Methods (snake_case - recommended)
|
|
309
341
|
# ========================================================================
|
|
310
|
-
|
|
342
|
+
|
|
343
|
+
def get_series_ids(self) -> List[str]:
|
|
344
|
+
"""Get list of series IDs (snake_case - recommended)."""
|
|
345
|
+
return [s.series_id if s.series_id is not None else f"series_{i}"
|
|
346
|
+
for i, s in enumerate(self.series)]
|
|
347
|
+
|
|
348
|
+
def get_series_names(self) -> List[str]:
|
|
349
|
+
"""Get list of series names (snake_case - recommended)."""
|
|
350
|
+
return [s.series_name if s.series_name is not None else (s.series_id or f"series_{i}")
|
|
351
|
+
for i, s in enumerate(self.series)]
|
|
352
|
+
|
|
353
|
+
def get_frequencies(self) -> List[str]:
|
|
354
|
+
"""Get list of frequencies (snake_case - recommended)."""
|
|
355
|
+
return [s.frequency for s in self.series]
|
|
356
|
+
|
|
357
|
+
def validate_and_report(self) -> Dict[str, Any]:
|
|
358
|
+
"""Validate configuration and return structured report with issues and suggestions.
|
|
359
|
+
|
|
360
|
+
This method performs validation checks without raising exceptions, returning
|
|
361
|
+
a structured report that can be used for debugging and user guidance.
|
|
362
|
+
|
|
363
|
+
Returns
|
|
364
|
+
-------
|
|
365
|
+
Dict[str, Any]
|
|
366
|
+
Report dictionary with keys:
|
|
367
|
+
- 'valid': bool - Whether configuration is valid
|
|
368
|
+
- 'errors': List[str] - List of error messages
|
|
369
|
+
- 'warnings': List[str] - List of warning messages
|
|
370
|
+
- 'suggestions': List[str] - List of actionable suggestions
|
|
371
|
+
"""
|
|
372
|
+
from .utils import FREQUENCY_HIERARCHY
|
|
373
|
+
|
|
374
|
+
report = {
|
|
375
|
+
'valid': True,
|
|
376
|
+
'errors': [],
|
|
377
|
+
'warnings': [],
|
|
378
|
+
'suggestions': []
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# Check for empty series
|
|
382
|
+
if not self.series:
|
|
383
|
+
report['valid'] = False
|
|
384
|
+
report['errors'].append("Model configuration must contain at least one series.")
|
|
385
|
+
report['suggestions'].append("Add series definitions to your configuration.")
|
|
386
|
+
return report
|
|
387
|
+
|
|
388
|
+
return report
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@dataclass
|
|
392
|
+
class DFMConfig(BaseModelConfig):
|
|
393
|
+
"""Linear DFM configuration - EM algorithm parameters and block structure.
|
|
394
|
+
|
|
395
|
+
This configuration class extends BaseModelConfig with parameters specific
|
|
396
|
+
to linear Dynamic Factor Models trained using the Expectation-Maximization
|
|
397
|
+
(EM) algorithm. DFM uses block structure to organize factors (global + sector-specific).
|
|
398
|
+
|
|
399
|
+
The configuration can be built from:
|
|
400
|
+
- Main settings (estimation parameters) from config/default.yaml
|
|
401
|
+
- Series definitions from config/series/default.yaml or CSV
|
|
402
|
+
- Block definitions from config/blocks/default.yaml
|
|
403
|
+
"""
|
|
404
|
+
# ========================================================================
|
|
405
|
+
# Block Structure (DFM-specific)
|
|
406
|
+
# ========================================================================
|
|
407
|
+
blocks: Dict[str, Dict[str, Any]] = field(default_factory=dict) # Block configurations (block_name -> {factors, ar_lag, clock, notes})
|
|
408
|
+
block_names: List[str] = field(init=False) # Block names in order (derived from blocks dict)
|
|
409
|
+
factors_per_block: List[int] = field(init=False) # Number of factors per block (derived from blocks)
|
|
410
|
+
_cached_blocks: Optional[np.ndarray] = field(default=None, init=False, repr=False) # Internal cache
|
|
411
|
+
|
|
412
|
+
# ========================================================================
|
|
413
|
+
# EM Algorithm Parameters (HOW - controls the algorithm)
|
|
414
|
+
# ========================================================================
|
|
415
|
+
ar_lag: int = 1 # Number of lags in AR transition equation (lookback window). Must be 1 or 2 (maximum supported order is VAR(2))
|
|
416
|
+
threshold: float = 1e-5 # EM convergence threshold
|
|
417
|
+
max_iter: int = 5000 # Maximum EM iterations
|
|
418
|
+
|
|
419
|
+
# ========================================================================
|
|
420
|
+
# Numerical Stability Parameters (transparent and configurable)
|
|
421
|
+
# ========================================================================
|
|
422
|
+
# AR Coefficient Clipping
|
|
423
|
+
clip_ar_coefficients: bool = True # Enable AR coefficient clipping for stationarity
|
|
424
|
+
ar_clip_min: float = -0.99 # Minimum AR coefficient (must be > -1 for stationarity)
|
|
425
|
+
ar_clip_max: float = 0.99 # Maximum AR coefficient (must be < 1 for stationarity)
|
|
426
|
+
warn_on_ar_clip: bool = True # Warn when AR coefficients are clipped (indicates near-unit root)
|
|
427
|
+
|
|
428
|
+
# Data Value Clipping
|
|
429
|
+
clip_data_values: bool = True # Enable clipping of extreme data values
|
|
430
|
+
data_clip_threshold: float = 100.0 # Clip values beyond this many standard deviations
|
|
431
|
+
warn_on_data_clip: bool = True # Warn when data values are clipped (indicates outliers)
|
|
432
|
+
|
|
433
|
+
# Regularization
|
|
434
|
+
use_regularization: bool = True # Enable regularization for numerical stability
|
|
435
|
+
regularization_scale: float = 1e-5 # Scale factor for ridge regularization (relative to trace, default 1e-5)
|
|
436
|
+
min_eigenvalue: float = 1e-8 # Minimum eigenvalue for positive definite matrices
|
|
437
|
+
max_eigenvalue: float = 1e6 # Maximum eigenvalue cap to prevent explosion
|
|
438
|
+
warn_on_regularization: bool = True # Warn when regularization is applied
|
|
439
|
+
|
|
440
|
+
# Damped Updates
|
|
441
|
+
use_damped_updates: bool = True # Enable damped updates when likelihood decreases
|
|
442
|
+
damping_factor: float = 0.8 # Damping factor (0.8 = 80% new, 20% old)
|
|
443
|
+
warn_on_damped_update: bool = True # Warn when damped updates are used
|
|
444
|
+
|
|
445
|
+
# Idiosyncratic Component Augmentation
|
|
446
|
+
augment_idio: bool = True # Enable state augmentation with idiosyncratic components (default: True)
|
|
447
|
+
augment_idio_slow: bool = True # Enable tent-length chains for slower-frequency series (default: True)
|
|
448
|
+
idio_rho0: float = 0.1 # Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
449
|
+
idio_min_var: float = 1e-8 # Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
311
450
|
|
|
312
451
|
def __post_init__(self):
|
|
313
|
-
"""Validate blocks structure and consistency.
|
|
452
|
+
"""Validate blocks structure and consistency for DFM.
|
|
314
453
|
|
|
315
|
-
This method performs comprehensive validation of the
|
|
454
|
+
This method performs comprehensive validation of the DFM configuration:
|
|
316
455
|
- Derives block_names and factors_per_block from blocks dict
|
|
317
456
|
- Ensures at least one series is specified
|
|
318
457
|
- Validates block structure consistency across all series
|
|
@@ -327,24 +466,20 @@ class BaseModelConfig:
|
|
|
327
466
|
If any validation check fails, with a descriptive error message
|
|
328
467
|
indicating what needs to be fixed.
|
|
329
468
|
"""
|
|
469
|
+
# Call parent __post_init__ first (validates series and clock)
|
|
470
|
+
super().__post_init__()
|
|
471
|
+
|
|
330
472
|
# Import frequency hierarchy for validation
|
|
331
473
|
from .utils import FREQUENCY_HIERARCHY
|
|
332
474
|
|
|
333
|
-
if not self.series:
|
|
334
|
-
raise ValueError(
|
|
335
|
-
"Model configuration must contain at least one series. "
|
|
336
|
-
"Please add series definitions to your configuration."
|
|
337
|
-
)
|
|
338
|
-
|
|
339
475
|
if not self.blocks:
|
|
340
476
|
raise ValueError(
|
|
341
|
-
"
|
|
477
|
+
"DFM configuration must contain at least one block. "
|
|
342
478
|
"Please add block definitions to your configuration."
|
|
343
479
|
)
|
|
344
480
|
|
|
345
481
|
# Derive block_names and factors_per_block from blocks dict
|
|
346
482
|
# Ensure global block (first block) is present
|
|
347
|
-
# Find the global block (first block in order or named 'Block_Global')
|
|
348
483
|
block_names_list = list(self.blocks.keys())
|
|
349
484
|
global_block_name = None
|
|
350
485
|
|
|
@@ -359,7 +494,7 @@ class BaseModelConfig:
|
|
|
359
494
|
|
|
360
495
|
if global_block_name is None:
|
|
361
496
|
raise ValueError(
|
|
362
|
-
"
|
|
497
|
+
"DFM configuration must include at least one block. "
|
|
363
498
|
"The first block serves as the global/common factor that all series load on."
|
|
364
499
|
)
|
|
365
500
|
|
|
@@ -370,7 +505,6 @@ class BaseModelConfig:
|
|
|
370
505
|
[self.blocks[name].get('factors', 1) for name in self.block_names])
|
|
371
506
|
|
|
372
507
|
# Validate global clock
|
|
373
|
-
self.clock = validate_frequency(self.clock)
|
|
374
508
|
global_clock_hierarchy = FREQUENCY_HIERARCHY.get(self.clock, 3)
|
|
375
509
|
|
|
376
510
|
# Validate block clocks (must be >= global clock)
|
|
@@ -407,11 +541,6 @@ class BaseModelConfig:
|
|
|
407
541
|
# Auto-generate series_id if not provided and convert blocks to indices
|
|
408
542
|
n_blocks = len(self.block_names)
|
|
409
543
|
for i, s in enumerate(self.series):
|
|
410
|
-
if s.series_id is None:
|
|
411
|
-
s.series_id = f"series_{i}"
|
|
412
|
-
if s.series_name is None:
|
|
413
|
-
s.series_name = s.series_id
|
|
414
|
-
|
|
415
544
|
# Convert block names to indices if needed
|
|
416
545
|
if isinstance(s.blocks, list) and len(s.blocks) > 0:
|
|
417
546
|
if isinstance(s.blocks[0], str):
|
|
@@ -477,24 +606,6 @@ class BaseModelConfig:
|
|
|
477
606
|
f"Each block must have at least one factor."
|
|
478
607
|
)
|
|
479
608
|
|
|
480
|
-
# ========================================================================
|
|
481
|
-
# Helper Methods (snake_case - recommended)
|
|
482
|
-
# ========================================================================
|
|
483
|
-
|
|
484
|
-
def get_series_ids(self) -> List[str]:
|
|
485
|
-
"""Get list of series IDs (snake_case - recommended)."""
|
|
486
|
-
return [s.series_id if s.series_id is not None else f"series_{i}"
|
|
487
|
-
for i, s in enumerate(self.series)]
|
|
488
|
-
|
|
489
|
-
def get_series_names(self) -> List[str]:
|
|
490
|
-
"""Get list of series names (snake_case - recommended)."""
|
|
491
|
-
return [s.series_name if s.series_name is not None else (s.series_id or f"series_{i}")
|
|
492
|
-
for i, s in enumerate(self.series)]
|
|
493
|
-
|
|
494
|
-
def get_frequencies(self) -> List[str]:
|
|
495
|
-
"""Get list of frequencies (snake_case - recommended)."""
|
|
496
|
-
return [s.frequency for s in self.series]
|
|
497
|
-
|
|
498
609
|
def get_blocks_array(self) -> np.ndarray:
|
|
499
610
|
"""Get blocks as numpy array (snake_case - recommended, cached)."""
|
|
500
611
|
if self._cached_blocks is None:
|
|
@@ -502,153 +613,6 @@ class BaseModelConfig:
|
|
|
502
613
|
self._cached_blocks = np.array(blocks_list, dtype=int)
|
|
503
614
|
return self._cached_blocks
|
|
504
615
|
|
|
505
|
-
def validate_and_report(self) -> Dict[str, Any]:
|
|
506
|
-
"""Validate configuration and return structured report with issues and suggestions.
|
|
507
|
-
|
|
508
|
-
This method performs validation checks without raising exceptions, returning
|
|
509
|
-
a structured report that can be used for debugging and user guidance.
|
|
510
|
-
|
|
511
|
-
Returns
|
|
512
|
-
-------
|
|
513
|
-
Dict[str, Any]
|
|
514
|
-
Report dictionary with keys:
|
|
515
|
-
- 'valid': bool - Whether configuration is valid
|
|
516
|
-
- 'errors': List[str] - List of error messages
|
|
517
|
-
- 'warnings': List[str] - List of warning messages
|
|
518
|
-
- 'suggestions': List[str] - List of actionable suggestions
|
|
519
|
-
"""
|
|
520
|
-
from .utils import FREQUENCY_HIERARCHY
|
|
521
|
-
|
|
522
|
-
report = {
|
|
523
|
-
'valid': True,
|
|
524
|
-
'errors': [],
|
|
525
|
-
'warnings': [],
|
|
526
|
-
'suggestions': []
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
# Check for empty series
|
|
530
|
-
if not self.series:
|
|
531
|
-
report['valid'] = False
|
|
532
|
-
report['errors'].append("Model configuration must contain at least one series.")
|
|
533
|
-
report['suggestions'].append("Add series definitions to your configuration.")
|
|
534
|
-
return report
|
|
535
|
-
|
|
536
|
-
# Check for empty blocks
|
|
537
|
-
if not self.blocks:
|
|
538
|
-
report['valid'] = False
|
|
539
|
-
report['errors'].append("Model configuration must contain at least one block.")
|
|
540
|
-
report['suggestions'].append("Add block definitions to your configuration.")
|
|
541
|
-
return report
|
|
542
|
-
|
|
543
|
-
# Check frequency constraints
|
|
544
|
-
global_clock_hierarchy = FREQUENCY_HIERARCHY.get(self.clock, 3)
|
|
545
|
-
for i, s in enumerate(self.series):
|
|
546
|
-
series_freq_hierarchy = FREQUENCY_HIERARCHY.get(s.frequency, 3)
|
|
547
|
-
|
|
548
|
-
for block_idx, loads_on_block in enumerate(s.blocks):
|
|
549
|
-
if loads_on_block == 1:
|
|
550
|
-
if block_idx < len(self.block_names):
|
|
551
|
-
block_name = self.block_names[block_idx]
|
|
552
|
-
block_cfg = self.blocks[block_name]
|
|
553
|
-
block_clock = block_cfg.get('clock', self.clock)
|
|
554
|
-
block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_clock, 3)
|
|
555
|
-
|
|
556
|
-
if series_freq_hierarchy < block_clock_hierarchy:
|
|
557
|
-
valid_freqs = [freq for freq, hier in FREQUENCY_HIERARCHY.items()
|
|
558
|
-
if hier >= block_clock_hierarchy]
|
|
559
|
-
valid_freqs_str = ', '.join(sorted(valid_freqs))
|
|
560
|
-
report['valid'] = False
|
|
561
|
-
report['errors'].append(
|
|
562
|
-
f"Series '{s.series_id}' has frequency '{s.frequency}' which is faster than "
|
|
563
|
-
f"block '{block_name}' clock '{block_clock}'."
|
|
564
|
-
)
|
|
565
|
-
report['suggestions'].append(
|
|
566
|
-
f"For series '{s.series_id}': change frequency to one of [{valid_freqs_str}], "
|
|
567
|
-
f"or set block '{block_name}' clock to '{s.frequency}' or faster."
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
# Check block clock constraints
|
|
571
|
-
for block_name, block_cfg in self.blocks.items():
|
|
572
|
-
block_clock = block_cfg.get('clock', self.clock)
|
|
573
|
-
block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_clock, 3)
|
|
574
|
-
if block_clock_hierarchy < global_clock_hierarchy:
|
|
575
|
-
report['valid'] = False
|
|
576
|
-
report['errors'].append(
|
|
577
|
-
f"Block '{block_name}' has clock '{block_clock}' which is faster than "
|
|
578
|
-
f"global clock '{self.clock}'."
|
|
579
|
-
)
|
|
580
|
-
report['suggestions'].append(
|
|
581
|
-
f"Change block '{block_name}' clock to '{self.clock}' or slower, "
|
|
582
|
-
f"or set global clock to '{block_clock}' or faster."
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
# Check factors_per_block
|
|
586
|
-
if any(f < 1 for f in self.factors_per_block):
|
|
587
|
-
invalid_blocks = [i for i, f in enumerate(self.factors_per_block) if f < 1]
|
|
588
|
-
report['valid'] = False
|
|
589
|
-
report['errors'].append(
|
|
590
|
-
f"factors_per_block must contain positive integers (>= 1). "
|
|
591
|
-
f"Invalid values found at block indices {invalid_blocks}."
|
|
592
|
-
)
|
|
593
|
-
report['suggestions'].append(
|
|
594
|
-
f"Set factors_per_block[{invalid_blocks[0]}] to at least 1 for block '{self.block_names[invalid_blocks[0]]}'."
|
|
595
|
-
)
|
|
596
|
-
|
|
597
|
-
return report
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
@dataclass
|
|
601
|
-
class DFMConfig(BaseModelConfig):
|
|
602
|
-
"""Linear DFM configuration - EM algorithm parameters.
|
|
603
|
-
|
|
604
|
-
This configuration class extends BaseModelConfig with parameters specific
|
|
605
|
-
to linear Dynamic Factor Models trained using the Expectation-Maximization
|
|
606
|
-
(EM) algorithm.
|
|
607
|
-
|
|
608
|
-
The configuration can be built from:
|
|
609
|
-
- Main settings (estimation parameters) from config/default.yaml
|
|
610
|
-
- Series definitions from config/series/default.yaml or CSV
|
|
611
|
-
- Block definitions from config/blocks/default.yaml
|
|
612
|
-
"""
|
|
613
|
-
# ========================================================================
|
|
614
|
-
# EM Algorithm Parameters (HOW - controls the algorithm)
|
|
615
|
-
# ========================================================================
|
|
616
|
-
ar_lag: int = 1 # Number of lags in AR transition equation (lookback window). Must be 1 or 2 (maximum supported order is VAR(2))
|
|
617
|
-
threshold: float = 1e-5 # EM convergence threshold
|
|
618
|
-
max_iter: int = 5000 # Maximum EM iterations
|
|
619
|
-
|
|
620
|
-
# ========================================================================
|
|
621
|
-
# Numerical Stability Parameters (transparent and configurable)
|
|
622
|
-
# ========================================================================
|
|
623
|
-
# AR Coefficient Clipping
|
|
624
|
-
clip_ar_coefficients: bool = True # Enable AR coefficient clipping for stationarity
|
|
625
|
-
ar_clip_min: float = -0.99 # Minimum AR coefficient (must be > -1 for stationarity)
|
|
626
|
-
ar_clip_max: float = 0.99 # Maximum AR coefficient (must be < 1 for stationarity)
|
|
627
|
-
warn_on_ar_clip: bool = True # Warn when AR coefficients are clipped (indicates near-unit root)
|
|
628
|
-
|
|
629
|
-
# Data Value Clipping
|
|
630
|
-
clip_data_values: bool = True # Enable clipping of extreme data values
|
|
631
|
-
data_clip_threshold: float = 100.0 # Clip values beyond this many standard deviations
|
|
632
|
-
warn_on_data_clip: bool = True # Warn when data values are clipped (indicates outliers)
|
|
633
|
-
|
|
634
|
-
# Regularization
|
|
635
|
-
use_regularization: bool = True # Enable regularization for numerical stability
|
|
636
|
-
regularization_scale: float = 1e-5 # Scale factor for ridge regularization (relative to trace, default 1e-5)
|
|
637
|
-
min_eigenvalue: float = 1e-8 # Minimum eigenvalue for positive definite matrices
|
|
638
|
-
max_eigenvalue: float = 1e6 # Maximum eigenvalue cap to prevent explosion
|
|
639
|
-
warn_on_regularization: bool = True # Warn when regularization is applied
|
|
640
|
-
|
|
641
|
-
# Damped Updates
|
|
642
|
-
use_damped_updates: bool = True # Enable damped updates when likelihood decreases
|
|
643
|
-
damping_factor: float = 0.8 # Damping factor (0.8 = 80% new, 20% old)
|
|
644
|
-
warn_on_damped_update: bool = True # Warn when damped updates are used
|
|
645
|
-
|
|
646
|
-
# Idiosyncratic Component Augmentation
|
|
647
|
-
augment_idio: bool = True # Enable state augmentation with idiosyncratic components (default: True)
|
|
648
|
-
augment_idio_slow: bool = True # Enable tent-length chains for slower-frequency series (default: True)
|
|
649
|
-
idio_rho0: float = 0.1 # Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
650
|
-
idio_min_var: float = 1e-8 # Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
651
|
-
|
|
652
616
|
# ========================================================================
|
|
653
617
|
# Factory Methods
|
|
654
618
|
# ========================================================================
|
|
@@ -703,14 +667,12 @@ class DDFMConfig(BaseModelConfig):
|
|
|
703
667
|
This configuration class extends BaseModelConfig with parameters specific
|
|
704
668
|
to Deep Dynamic Factor Models trained using neural networks (autoencoders).
|
|
705
669
|
|
|
706
|
-
Note: DDFM does
|
|
707
|
-
|
|
708
|
-
by DDFM - use num_factors directly to specify the number of factors.
|
|
670
|
+
Note: DDFM does NOT use block structure. Use num_factors directly to specify
|
|
671
|
+
the number of factors. Blocks are DFM-specific and not needed for DDFM.
|
|
709
672
|
|
|
710
673
|
The configuration can be built from:
|
|
711
674
|
- Main settings (training parameters) from config/default.yaml
|
|
712
675
|
- Series definitions from config/series/default.yaml or CSV
|
|
713
|
-
- Block definitions are not used by DDFM (only for DFM compatibility)
|
|
714
676
|
"""
|
|
715
677
|
# ========================================================================
|
|
716
678
|
# Neural Network Training Parameters
|
|
@@ -863,13 +825,9 @@ class DDFMConfig(BaseModelConfig):
|
|
|
863
825
|
config_type = _detect_config_type(data)
|
|
864
826
|
|
|
865
827
|
if config_type == 'ddfm':
|
|
866
|
-
# DDFM does not use block structure -
|
|
867
|
-
# This is required by BaseModelConfig but will be ignored by DDFM
|
|
868
|
-
if not blocks_dict_final:
|
|
869
|
-
blocks_dict_final = {DEFAULT_BLOCK_NAME: {'factors': 1, 'ar_lag': 1, 'clock': 'm'}}
|
|
828
|
+
# DDFM does not use block structure - no blocks needed
|
|
870
829
|
return DDFMConfig(
|
|
871
830
|
series=series_list,
|
|
872
|
-
blocks=blocks_dict_final,
|
|
873
831
|
**DDFMConfig._extract_ddfm(data)
|
|
874
832
|
)
|
|
875
833
|
else:
|
|
@@ -913,9 +871,9 @@ class DDFMConfig(BaseModelConfig):
|
|
|
913
871
|
config_type = _detect_config_type(data)
|
|
914
872
|
|
|
915
873
|
if config_type == 'ddfm':
|
|
874
|
+
# DDFM does not use block structure - no blocks needed
|
|
916
875
|
return DDFMConfig(
|
|
917
876
|
series=series_list,
|
|
918
|
-
blocks=blocks_dict,
|
|
919
877
|
**DDFMConfig._extract_ddfm(data)
|
|
920
878
|
)
|
|
921
879
|
else:
|
|
@@ -984,7 +942,7 @@ def _dfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig'
|
|
|
984
942
|
raise ValueError(f"blocks must be a dict, got {type(blocks_data)}")
|
|
985
943
|
else:
|
|
986
944
|
# Infer blocks from series using helper
|
|
987
|
-
blocks_dict =
|
|
945
|
+
blocks_dict = _infer_blocks(series_list, data)
|
|
988
946
|
|
|
989
947
|
# Determine config type using helper function
|
|
990
948
|
config_type = _detect_config_type(data)
|
|
@@ -1047,19 +1005,15 @@ def _ddfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig
|
|
|
1047
1005
|
raise ValueError(f"blocks must be a dict, got {type(blocks_data)}")
|
|
1048
1006
|
else:
|
|
1049
1007
|
# Infer blocks from series using helper
|
|
1050
|
-
blocks_dict =
|
|
1008
|
+
blocks_dict = _infer_blocks(series_list, data)
|
|
1051
1009
|
|
|
1052
1010
|
# Determine config type using helper function
|
|
1053
1011
|
config_type = _detect_config_type(data)
|
|
1054
1012
|
|
|
1055
1013
|
if config_type == 'ddfm':
|
|
1056
|
-
# DDFM does not use block structure -
|
|
1057
|
-
# This is required by BaseModelConfig but will be ignored by DDFM
|
|
1058
|
-
if not blocks_dict:
|
|
1059
|
-
blocks_dict = {DEFAULT_BLOCK_NAME: {'factors': 1, 'ar_lag': 1, 'clock': 'm'}}
|
|
1014
|
+
# DDFM does not use block structure - no blocks needed
|
|
1060
1015
|
return DDFMConfig(
|
|
1061
1016
|
series=series_list,
|
|
1062
|
-
blocks=blocks_dict,
|
|
1063
1017
|
**DDFMConfig._extract_ddfm(data)
|
|
1064
1018
|
)
|
|
1065
1019
|
else:
|
|
@@ -1073,12 +1027,9 @@ def _ddfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig
|
|
|
1073
1027
|
config_type = _detect_config_type(data)
|
|
1074
1028
|
|
|
1075
1029
|
if config_type == 'ddfm':
|
|
1076
|
-
# DDFM does not use block structure -
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
data = data.copy()
|
|
1080
|
-
data['blocks'] = {DEFAULT_BLOCK_NAME: {'factors': 1, 'ar_lag': 1, 'clock': 'm'}}
|
|
1081
|
-
return DDFMConfig(**data)
|
|
1030
|
+
# DDFM does not use block structure - remove blocks from data if present
|
|
1031
|
+
data_clean = {k: v for k, v in data.items() if k != 'blocks'}
|
|
1032
|
+
return DDFMConfig(**data_clean)
|
|
1082
1033
|
else:
|
|
1083
1034
|
return DFMConfig(**data)
|
|
1084
1035
|
|
dfm_python/data/utils.py
CHANGED
|
@@ -239,12 +239,11 @@ def load_data(
|
|
|
239
239
|
mask = Time >= sample_start
|
|
240
240
|
if isinstance(mask, pd.Series):
|
|
241
241
|
mask = mask.values
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
Time = Time.filter(mask)
|
|
242
|
+
# Ensure mask is boolean numpy array
|
|
243
|
+
mask = np.asarray(mask, dtype=bool)
|
|
244
|
+
Z = Z.iloc[mask] if isinstance(Z, pd.DataFrame) else Z[mask]
|
|
245
|
+
# TimeIndex has filter() method - convert mask to list for filter()
|
|
246
|
+
Time = Time.filter(mask.tolist())
|
|
248
247
|
_logger.info(f"Filtered to start date: {sample_start}")
|
|
249
248
|
|
|
250
249
|
if sample_end is not None:
|
|
@@ -253,12 +252,11 @@ def load_data(
|
|
|
253
252
|
mask = Time <= sample_end
|
|
254
253
|
if isinstance(mask, pd.Series):
|
|
255
254
|
mask = mask.values
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Time = Time.filter(mask)
|
|
255
|
+
# Ensure mask is boolean numpy array
|
|
256
|
+
mask = np.asarray(mask, dtype=bool)
|
|
257
|
+
Z = Z.iloc[mask] if isinstance(Z, pd.DataFrame) else Z[mask]
|
|
258
|
+
# TimeIndex has filter() method - convert mask to list for filter()
|
|
259
|
+
Time = Time.filter(mask.tolist())
|
|
262
260
|
_logger.info(f"Filtered to end date: {sample_end}")
|
|
263
261
|
|
|
264
262
|
# Return raw data (transformations should be applied via custom sktime transformer in DFMDataModule)
|
|
File without changes
|