dfm-python 0.4.51__py3-none-any.whl → 0.5.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 +3 -20
- dfm_python/config/adapter.py +1 -2
- dfm_python/config/results.py +55 -1
- dfm_python/config/schema.py +15 -15
- dfm_python/data/__init__.py +2 -0
- dfm_python/data/dataloader.py +3 -3
- dfm_python/data/utils.py +2 -3
- dfm_python/encoder/vae.py +15 -0
- dfm_python/lightning/__init__.py +2 -5
- dfm_python/lightning/data_module.py +116 -31
- dfm_python/lightning/scaling.py +32 -26
- dfm_python/logger/inference_logger.py +1 -1
- dfm_python/logger/logger.py +23 -10
- dfm_python/logger/train_logger.py +1 -1
- dfm_python/models/__init__.py +2 -0
- dfm_python/models/base.py +433 -161
- dfm_python/models/ddfm.py +568 -1579
- dfm_python/models/dfm.py +205 -47
- dfm_python/models/mcmc.py +491 -0
- dfm_python/models/utils.py +470 -0
- dfm_python/ssm/em.py +534 -53
- dfm_python/ssm/kalman.py +336 -30
- dfm_python/ssm/utils.py +65 -17
- dfm_python/trainer/__init__.py +10 -3
- dfm_python/trainer/ddfm.py +5 -4
- dfm_python/trainer/dfm.py +3 -3
- dfm_python/utils/__init__.py +9 -32
- dfm_python/utils/data.py +68 -3
- dfm_python/utils/helpers.py +1 -1
- dfm_python/utils/statespace.py +10 -7
- dfm_python/utils/time.py +3 -3
- dfm_python-0.5.1.dist-info/METADATA +416 -0
- dfm_python-0.5.1.dist-info/RECORD +44 -0
- dfm_python/nowcast/__init__.py +0 -37
- dfm_python/nowcast/dataview.py +0 -143
- dfm_python/nowcast/helpers.py +0 -356
- dfm_python/nowcast/nowcast.py +0 -1263
- dfm_python/nowcast/splitters.py +0 -441
- dfm_python/nowcast/transformers.py +0 -330
- dfm_python/nowcast/utils.py +0 -285
- dfm_python-0.4.51.dist-info/METADATA +0 -1299
- dfm_python-0.4.51.dist-info/RECORD +0 -49
- {dfm_python-0.4.51.dist-info → dfm_python-0.5.1.dist-info}/WHEEL +0 -0
- {dfm_python-0.4.51.dist-info → dfm_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -6,7 +6,6 @@ This package implements a comprehensive Dynamic Factor Model framework with supp
|
|
|
6
6
|
- Tent kernel aggregation for low-to-high frequency mapping
|
|
7
7
|
- Expectation-Maximization (EM) algorithm for parameter estimation
|
|
8
8
|
- Kalman filtering and smoothing for factor extraction
|
|
9
|
-
- News decomposition for nowcasting
|
|
10
9
|
- Deep Dynamic Factor Models (DDFM) with nonlinear encoders (requires PyTorch)
|
|
11
10
|
|
|
12
11
|
The package implements a clock-based approach to mixed-frequency DFMs, where all latent
|
|
@@ -23,7 +22,6 @@ Key Features:
|
|
|
23
22
|
- Flexible block structure for factor modeling
|
|
24
23
|
- Robust handling of missing data (internal spline interpolation)
|
|
25
24
|
- Automatic standardization and data clipping
|
|
26
|
-
- News decomposition for forecast updates
|
|
27
25
|
|
|
28
26
|
Example (Standard Lightning Pattern):
|
|
29
27
|
>>> from dfm_python import DFM, DFMDataModule, DFMTrainer
|
|
@@ -67,7 +65,7 @@ Note: DFMConfig and SeriesConfig are internal implementation details.
|
|
|
67
65
|
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
68
66
|
"""
|
|
69
67
|
|
|
70
|
-
__version__ = "0.
|
|
68
|
+
__version__ = "0.5.0"
|
|
71
69
|
|
|
72
70
|
# ============================================================================
|
|
73
71
|
# PUBLIC API DEFINITION
|
|
@@ -81,8 +79,7 @@ __version__ = "0.4.51"
|
|
|
81
79
|
# 2. High-level API: DFM, DDFM, module-level convenience functions
|
|
82
80
|
# 3. Core utilities: TimeIndex, diagnostics
|
|
83
81
|
# 4. Models: BaseFactorModel, DDFM (low-level)
|
|
84
|
-
# 5.
|
|
85
|
-
# 6. Data & Results: DFMResult
|
|
82
|
+
# 5. Data & Results: DFMResult
|
|
86
83
|
# ============================================================================
|
|
87
84
|
|
|
88
85
|
# Configuration (from config/ subpackage)
|
|
@@ -110,17 +107,6 @@ from .lightning import (
|
|
|
110
107
|
EMAlgorithm, # Module class
|
|
111
108
|
)
|
|
112
109
|
|
|
113
|
-
# Nowcasting (from nowcast/ subpackage)
|
|
114
|
-
from .nowcast import (
|
|
115
|
-
Nowcast,
|
|
116
|
-
NowcastResult,
|
|
117
|
-
)
|
|
118
|
-
from .nowcast.helpers import (
|
|
119
|
-
para_const,
|
|
120
|
-
NewsDecompResult,
|
|
121
|
-
BacktestResult,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
110
|
# Model implementations
|
|
125
111
|
from .models.base import BaseFactorModel
|
|
126
112
|
from .models.dfm import DFM
|
|
@@ -130,11 +116,9 @@ from .models.ddfm import DDFM, DDFMModel
|
|
|
130
116
|
|
|
131
117
|
__all__ = [
|
|
132
118
|
# Core classes
|
|
133
|
-
'DFM',
|
|
119
|
+
'DFM',
|
|
134
120
|
# Model base and implementations
|
|
135
121
|
'BaseFactorModel',
|
|
136
|
-
# Nowcast result classes
|
|
137
|
-
'NowcastResult', 'NewsDecompResult', 'BacktestResult',
|
|
138
122
|
# Constants
|
|
139
123
|
'DEFAULT_BLOCK_NAME',
|
|
140
124
|
# Config sources
|
|
@@ -142,7 +126,6 @@ __all__ = [
|
|
|
142
126
|
'MergedConfigSource', 'make_config_source',
|
|
143
127
|
# Low-level API (functional interface - advanced usage)
|
|
144
128
|
'BaseResult', 'DFMResult', 'DDFMResult', 'calculate_rmse', 'diagnose_series', 'print_series_diagnosis',
|
|
145
|
-
'para_const', # Internal utility for nowcasting
|
|
146
129
|
]
|
|
147
130
|
|
|
148
131
|
# DDFM high-level API (PyTorch is mandatory)
|
dfm_python/config/adapter.py
CHANGED
|
@@ -214,8 +214,7 @@ class YamlSource:
|
|
|
214
214
|
frequency=series_data.get('frequency', 'm'),
|
|
215
215
|
transformation=series_data.get('transformation', 'lin'),
|
|
216
216
|
blocks=series_data.get('blocks', []),
|
|
217
|
-
release_date=release_date
|
|
218
|
-
scaler=series_data.get('scaler', None) # Optional, for per-series scaling
|
|
217
|
+
release_date=release_date
|
|
219
218
|
))
|
|
220
219
|
|
|
221
220
|
# If no series loaded from separate files, try to get from main config
|
dfm_python/config/results.py
CHANGED
|
@@ -6,8 +6,9 @@ This module contains dataclasses for storing DFM estimation results and paramete
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import warnings
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from typing import Optional, List, Dict, Any
|
|
9
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
10
10
|
from abc import ABC
|
|
11
|
+
from datetime import datetime
|
|
11
12
|
|
|
12
13
|
from .schema import DFMConfig
|
|
13
14
|
|
|
@@ -278,6 +279,59 @@ class DDFMResult(BaseResult):
|
|
|
278
279
|
use_idiosyncratic: Optional[bool] = None # Whether idio components were used
|
|
279
280
|
|
|
280
281
|
|
|
282
|
+
@dataclass
|
|
283
|
+
class NowcastResult:
|
|
284
|
+
"""Result from a single nowcast calculation.
|
|
285
|
+
|
|
286
|
+
This dataclass contains all information from a nowcast operation,
|
|
287
|
+
including the nowcast value, metadata about the data view, and
|
|
288
|
+
optional diagnostic information.
|
|
289
|
+
|
|
290
|
+
Attributes
|
|
291
|
+
----------
|
|
292
|
+
target_series : str
|
|
293
|
+
Target series ID that was nowcasted.
|
|
294
|
+
target_period : datetime
|
|
295
|
+
Target period for the nowcast (the period being estimated).
|
|
296
|
+
view_date : datetime
|
|
297
|
+
View date (when data is available). This determines which
|
|
298
|
+
data points are masked/unmasked in the nowcast calculation.
|
|
299
|
+
nowcast_value : float
|
|
300
|
+
The calculated nowcast value for the target series.
|
|
301
|
+
confidence_interval : Tuple[float, float], optional
|
|
302
|
+
Confidence interval (lower, upper) for the nowcast, if available.
|
|
303
|
+
factors_at_view : np.ndarray, optional
|
|
304
|
+
Factor values at the view_date (m,). These are the updated
|
|
305
|
+
factor states after applying the data view masking.
|
|
306
|
+
dfm_result : BaseResult, optional
|
|
307
|
+
Full DFM/DDFM result for this view. Can be used for further
|
|
308
|
+
analysis or diagnostics.
|
|
309
|
+
data_availability : Dict[str, int], optional
|
|
310
|
+
Dictionary with keys 'n_available' and 'n_missing' indicating
|
|
311
|
+
how many data points were available vs missing in the data view.
|
|
312
|
+
|
|
313
|
+
Examples
|
|
314
|
+
--------
|
|
315
|
+
>>> from dfm_python import DFM
|
|
316
|
+
>>> import numpy as np
|
|
317
|
+
>>> model = DFM()
|
|
318
|
+
>>> trainer.fit(model, data_module)
|
|
319
|
+
>>> # Update state with new data, then predict
|
|
320
|
+
>>> X_std = np.random.randn(10, 5) # Standardized data
|
|
321
|
+
>>> model.update(X_std)
|
|
322
|
+
>>> forecast = model.predict(horizon=1)
|
|
323
|
+
>>> print(f"Forecast: {forecast[0, 0]}")
|
|
324
|
+
"""
|
|
325
|
+
target_series: str
|
|
326
|
+
target_period: datetime
|
|
327
|
+
view_date: datetime
|
|
328
|
+
nowcast_value: float
|
|
329
|
+
confidence_interval: Optional[Tuple[float, float]] = None # (lower, upper)
|
|
330
|
+
factors_at_view: Optional[np.ndarray] = None # Factor values at view_date
|
|
331
|
+
dfm_result: Optional[BaseResult] = None # Full DFM/DDFM result for this view
|
|
332
|
+
data_availability: Optional[Dict[str, int]] = None # n_available, n_missing
|
|
333
|
+
|
|
334
|
+
|
|
281
335
|
@dataclass
|
|
282
336
|
class FitParams:
|
|
283
337
|
"""Parameter overrides for DFM model fitting.
|
dfm_python/config/schema.py
CHANGED
|
@@ -200,11 +200,6 @@ class SeriesConfig:
|
|
|
200
200
|
- Positive value (1-31): Day of month when data is released
|
|
201
201
|
- Negative value: Days before end of previous month when data is released
|
|
202
202
|
Example: 25 = released on 25th of each month, -5 = released 5 days before end of previous month
|
|
203
|
-
scaler : str, optional
|
|
204
|
-
Scaler type for this series: 'standard', 'robust', 'minmax', 'maxabs',
|
|
205
|
-
'quantile', or None/null (no scaling). Used by create_scaling_transformer_from_config()
|
|
206
|
-
to automatically create per-series scaling transformers. If None, uses default
|
|
207
|
-
scaling strategy (typically 'standard').
|
|
208
203
|
"""
|
|
209
204
|
# Required fields (no defaults)
|
|
210
205
|
frequency: str
|
|
@@ -215,7 +210,6 @@ class SeriesConfig:
|
|
|
215
210
|
series_name: Optional[str] = None # Optional metadata for display
|
|
216
211
|
units: Optional[str] = None # Optional metadata for display only (used in news.py output)
|
|
217
212
|
release_date: Optional[int] = None # Release date for pseudo real-time nowcasting
|
|
218
|
-
scaler: Optional[str] = None # Scaler type: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or None
|
|
219
213
|
|
|
220
214
|
def __post_init__(self):
|
|
221
215
|
"""Validate fields after initialization."""
|
|
@@ -308,6 +302,7 @@ class BaseModelConfig:
|
|
|
308
302
|
nan_method: int = 2 # Missing data handling method (1-5). Preprocessing step before Kalman Filter-based handling
|
|
309
303
|
nan_k: int = 3 # Spline parameter for NaN interpolation (cubic spline)
|
|
310
304
|
clock: str = 'm' # Base frequency for nowcasting (global clock): 'd', 'w', 'm', 'q', 'sa', 'a' (defaults to 'm' for monthly)
|
|
305
|
+
scaler: Optional[str] = 'standard' # Unified scaler type for all series: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or None (no scaling). Default: 'standard' for unified scaling.
|
|
311
306
|
|
|
312
307
|
# ========================================================================
|
|
313
308
|
# Internal cache (not user-configurable)
|
|
@@ -403,6 +398,11 @@ class BaseModelConfig:
|
|
|
403
398
|
f"Block '{block_name}' validation failed: AR lag must be at least 1, got {ar_lag}. "
|
|
404
399
|
f"Please set ar_lag >= 1 for block '{block_name}'."
|
|
405
400
|
)
|
|
401
|
+
if ar_lag > 2:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"Block '{block_name}' validation failed: AR lag must be at most 2 (maximum supported VAR order is VAR(2)), got {ar_lag}. "
|
|
404
|
+
f"Please set ar_lag to 1 (VAR(1)) or 2 (VAR(2)) for block '{block_name}'."
|
|
405
|
+
)
|
|
406
406
|
|
|
407
407
|
# Auto-generate series_id if not provided and convert blocks to indices
|
|
408
408
|
n_blocks = len(self.block_names)
|
|
@@ -613,7 +613,7 @@ class DFMConfig(BaseModelConfig):
|
|
|
613
613
|
# ========================================================================
|
|
614
614
|
# EM Algorithm Parameters (HOW - controls the algorithm)
|
|
615
615
|
# ========================================================================
|
|
616
|
-
ar_lag: int = 1 # Number of lags in AR transition equation (lookback window)
|
|
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
617
|
threshold: float = 1e-5 # EM convergence threshold
|
|
618
618
|
max_iter: int = 5000 # Maximum EM iterations
|
|
619
619
|
|
|
@@ -660,6 +660,7 @@ class DFMConfig(BaseModelConfig):
|
|
|
660
660
|
'nan_method': data.get('nan_method', 2),
|
|
661
661
|
'nan_k': data.get('nan_k', 3),
|
|
662
662
|
'clock': data.get('clock', 'm'),
|
|
663
|
+
'scaler': data.get('scaler', 'standard'), # Unified scaler for all series (default: 'standard')
|
|
663
664
|
}
|
|
664
665
|
|
|
665
666
|
@classmethod
|
|
@@ -716,12 +717,12 @@ class DDFMConfig(BaseModelConfig):
|
|
|
716
717
|
# ========================================================================
|
|
717
718
|
encoder_layers: Optional[List[int]] = None # Hidden layer dimensions for encoder (default: [64, 32])
|
|
718
719
|
num_factors: Optional[int] = None # Number of factors (inferred from config if None)
|
|
719
|
-
activation: str = '
|
|
720
|
+
activation: str = 'relu' # Activation function ('tanh', 'relu', 'sigmoid', default: 'relu' to match original DDFM)
|
|
720
721
|
use_batch_norm: bool = True # Use batch normalization in encoder (default: True)
|
|
721
722
|
learning_rate: float = 0.001 # Learning rate for Adam optimizer (default: 0.001)
|
|
722
723
|
epochs: int = 100 # Number of training epochs (default: 100)
|
|
723
|
-
batch_size: int =
|
|
724
|
-
factor_order: int = 1 # VAR lag order for factor dynamics
|
|
724
|
+
batch_size: int = 100 # Batch size for training (default: 100 to match original DDFM)
|
|
725
|
+
factor_order: int = 1 # VAR lag order for factor dynamics. Must be 1 or 2 (maximum supported order is VAR(2), default: 1)
|
|
725
726
|
use_idiosyncratic: bool = True # Model idio components with AR(1) dynamics (default: True)
|
|
726
727
|
min_obs_idio: int = 5 # Minimum observations for idio AR(1) estimation (default: 5)
|
|
727
728
|
|
|
@@ -750,15 +751,15 @@ class DDFMConfig(BaseModelConfig):
|
|
|
750
751
|
def _extract_ddfm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
751
752
|
"""Extract DDFM-specific parameters from config dict."""
|
|
752
753
|
base_params = cls._extract_base(data)
|
|
753
|
-
# Handle both direct keys and ddfm_ prefix format
|
|
754
|
+
# Handle both direct keys and ddfm_ prefix format
|
|
754
755
|
base_params.update({
|
|
755
756
|
'encoder_layers': data.get('encoder_layers') or data.get('ddfm_encoder_layers', None),
|
|
756
757
|
'num_factors': data.get('num_factors') or data.get('ddfm_num_factors', None),
|
|
757
|
-
'activation': data.get('activation') or data.get('ddfm_activation', '
|
|
758
|
+
'activation': data.get('activation') or data.get('ddfm_activation', 'relu'),
|
|
758
759
|
'use_batch_norm': data.get('use_batch_norm', data.get('ddfm_use_batch_norm', True)),
|
|
759
760
|
'learning_rate': data.get('learning_rate', data.get('ddfm_learning_rate', 0.001)),
|
|
760
761
|
'epochs': data.get('epochs', data.get('ddfm_epochs', 100)),
|
|
761
|
-
'batch_size': data.get('batch_size', data.get('ddfm_batch_size',
|
|
762
|
+
'batch_size': data.get('batch_size', data.get('ddfm_batch_size', 100)),
|
|
762
763
|
'factor_order': data.get('factor_order', data.get('ddfm_factor_order', 1)),
|
|
763
764
|
'use_idiosyncratic': data.get('use_idiosyncratic', data.get('ddfm_use_idiosyncratic', True)),
|
|
764
765
|
'min_obs_idio': data.get('min_obs_idio', data.get('ddfm_min_obs_idio', 5)),
|
|
@@ -832,8 +833,7 @@ class DDFMConfig(BaseModelConfig):
|
|
|
832
833
|
transformation=series_cfg.get('transformation', 'lin'),
|
|
833
834
|
blocks=series_blocks,
|
|
834
835
|
units=series_cfg.get('units', None), # Optional, for display only
|
|
835
|
-
release_date=series_cfg.get('release_date', None)
|
|
836
|
-
scaler=series_cfg.get('scaler', None) # Optional, for per-series scaling
|
|
836
|
+
release_date=series_cfg.get('release_date', None) # Optional, for nowcasting
|
|
837
837
|
))
|
|
838
838
|
|
|
839
839
|
# Convert blocks_dict to dict of block properties
|
dfm_python/data/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ This module provides:
|
|
|
4
4
|
- Dataset classes: DFMDataset (linear DFM), DDFMDataset (deep DDFM)
|
|
5
5
|
- DataLoader factories: create_dfm_dataloader, create_ddfm_dataloader
|
|
6
6
|
- Data reading: read_data, load_data
|
|
7
|
+
- Nowcast data classes moved to src.nowcasting in main project
|
|
7
8
|
|
|
8
9
|
Note: Transformation functions are application-specific and should be provided
|
|
9
10
|
by users in their preprocessing pipelines. The package does not include
|
|
@@ -13,6 +14,7 @@ transformation utilities to remain generic.
|
|
|
13
14
|
from .dataset import DFMDataset, DDFMDataset
|
|
14
15
|
from .dataloader import create_dfm_dataloader, create_ddfm_dataloader
|
|
15
16
|
from .utils import read_data, load_data
|
|
17
|
+
# Nowcast data classes moved to src.nowcasting in main project
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
|
18
20
|
# Datasets
|
dfm_python/data/dataloader.py
CHANGED
|
@@ -101,7 +101,7 @@ def create_dfm_dataloader(
|
|
|
101
101
|
|
|
102
102
|
def create_ddfm_dataloader(
|
|
103
103
|
dataset: DDFMDataset,
|
|
104
|
-
batch_size: int =
|
|
104
|
+
batch_size: int = 100,
|
|
105
105
|
shuffle: bool = True,
|
|
106
106
|
num_workers: int = 0,
|
|
107
107
|
pin_memory: bool = True
|
|
@@ -115,8 +115,8 @@ def create_ddfm_dataloader(
|
|
|
115
115
|
----------
|
|
116
116
|
dataset : DDFMDataset
|
|
117
117
|
DDFM dataset instance with windowed sequences
|
|
118
|
-
batch_size : int, default
|
|
119
|
-
Batch size for training
|
|
118
|
+
batch_size : int, default 100
|
|
119
|
+
Batch size for training (matches original DDFM)
|
|
120
120
|
shuffle : bool, default True
|
|
121
121
|
Whether to shuffle samples (typically True for training)
|
|
122
122
|
num_workers : int, default 0
|
dfm_python/data/utils.py
CHANGED
|
@@ -17,7 +17,6 @@ from ..config import DFMConfig
|
|
|
17
17
|
from ..config.utils import FREQUENCY_HIERARCHY
|
|
18
18
|
from ..utils.time import TimeIndex, parse_timestamp
|
|
19
19
|
from ..utils.helpers import (
|
|
20
|
-
safe_get_attr,
|
|
21
20
|
get_frequencies,
|
|
22
21
|
get_series_ids,
|
|
23
22
|
)
|
|
@@ -209,7 +208,7 @@ def load_data(
|
|
|
209
208
|
Time : TimeIndex
|
|
210
209
|
Time index for the data (aligned to clock frequency)
|
|
211
210
|
Z : np.ndarray
|
|
212
|
-
Original untransformed data (T x N), same as X
|
|
211
|
+
Original untransformed data (T x N), same as X
|
|
213
212
|
|
|
214
213
|
Raises
|
|
215
214
|
------
|
|
@@ -267,7 +266,7 @@ def load_data(
|
|
|
267
266
|
_logger.info(f"Loaded data: {X.shape[0]} time periods, {X.shape[1]} series (raw, not transformed)")
|
|
268
267
|
|
|
269
268
|
# Validate data quality
|
|
270
|
-
clock =
|
|
269
|
+
clock = getattr(config, 'clock', 'm')
|
|
271
270
|
clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
272
271
|
|
|
273
272
|
frequencies = get_frequencies(config)
|
dfm_python/encoder/vae.py
CHANGED
|
@@ -454,6 +454,21 @@ def extract_decoder_params(decoder) -> Tuple[np.ndarray, np.ndarray]:
|
|
|
454
454
|
# Decoder weight is already (N x m), so no transpose needed
|
|
455
455
|
C = weight
|
|
456
456
|
|
|
457
|
+
# Check for NaN in extracted C matrix (indicates numerical instability during training)
|
|
458
|
+
if np.any(np.isnan(C)):
|
|
459
|
+
nan_count = np.sum(np.isnan(C))
|
|
460
|
+
nan_ratio = nan_count / C.size
|
|
461
|
+
from ..logger import get_logger
|
|
462
|
+
_logger = get_logger(__name__)
|
|
463
|
+
_logger.warning(
|
|
464
|
+
f"extract_decoder_params: C matrix contains {nan_count}/{C.size} NaN values ({nan_ratio:.1%}). "
|
|
465
|
+
f"This indicates the decoder weights contain NaN, likely due to numerical instability during training. "
|
|
466
|
+
f"Possible causes: (1) learning rate too high, (2) gradient explosion, (3) unstable training."
|
|
467
|
+
)
|
|
468
|
+
# Replace NaN with zeros to prevent further issues
|
|
469
|
+
C = np.nan_to_num(C, nan=0.0)
|
|
470
|
+
_logger.warning("Replaced NaN values in C matrix with zeros.")
|
|
471
|
+
|
|
457
472
|
return C, bias
|
|
458
473
|
|
|
459
474
|
|
dfm_python/lightning/__init__.py
CHANGED
|
@@ -28,15 +28,14 @@ from .scaling import (
|
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
# DFMTrainingState is defined in models.dfm and exported here for convenience
|
|
31
|
-
# Note: dfm_module.py has been removed - DFM class implements EM directly
|
|
32
31
|
from ..models.dfm import DFMTrainingState
|
|
33
32
|
|
|
34
33
|
__all__ = [
|
|
35
34
|
# Kalman filter
|
|
36
|
-
'KalmanFilter',
|
|
35
|
+
'KalmanFilter',
|
|
37
36
|
'KalmanFilterState',
|
|
38
37
|
# EM algorithm
|
|
39
|
-
'EMAlgorithm',
|
|
38
|
+
'EMAlgorithm',
|
|
40
39
|
'EMStepParams',
|
|
41
40
|
# Data handling
|
|
42
41
|
'DFMDataModule',
|
|
@@ -50,7 +49,5 @@ __all__ = [
|
|
|
50
49
|
'NoScalingStrategy',
|
|
51
50
|
# Training state (defined in models.dfm)
|
|
52
51
|
'DFMTrainingState',
|
|
53
|
-
# DDFMLightningModule removed: deprecated and unused (DDFM uses DDFMModel directly)
|
|
54
|
-
# DDFMTrainingState removed: duplicate (also defined in models.ddfm)
|
|
55
52
|
]
|
|
56
53
|
|
|
@@ -43,13 +43,11 @@ def _get_scaler(transformer: Any) -> Optional[Any]:
|
|
|
43
43
|
|
|
44
44
|
This function depends on sktime's pipeline structure to traverse
|
|
45
45
|
TransformerPipeline and sklearn transformers (StandardScaler, etc.).
|
|
46
|
-
Note: TabularToSeriesAdaptor support is for backward compatibility only.
|
|
47
46
|
|
|
48
47
|
Parameters
|
|
49
48
|
----------
|
|
50
49
|
transformer : Any
|
|
51
|
-
Transformer to search (
|
|
52
|
-
TransformerPipeline, sklearn Pipeline, etc.)
|
|
50
|
+
Transformer to search (StandardScaler, TransformerPipeline, sklearn Pipeline, etc.)
|
|
53
51
|
|
|
54
52
|
Returns
|
|
55
53
|
-------
|
|
@@ -60,9 +58,7 @@ def _get_scaler(transformer: Any) -> Optional[Any]:
|
|
|
60
58
|
if transformer is None:
|
|
61
59
|
return None
|
|
62
60
|
|
|
63
|
-
# Check if transformer is
|
|
64
|
-
# Note: New code should use StandardScaler() directly per sktime docs
|
|
65
|
-
# This check is kept for backward compatibility with old code that may still use TabularToSeriesAdaptor
|
|
61
|
+
# Check if transformer is wrapped (TabularToSeriesAdaptor or similar)
|
|
66
62
|
if hasattr(transformer, 'transformer'):
|
|
67
63
|
# Recursively search the wrapped transformer
|
|
68
64
|
wrapped = transformer.transformer
|
|
@@ -78,8 +74,7 @@ def _get_scaler(transformer: Any) -> Optional[Any]:
|
|
|
78
74
|
if scaler is not None:
|
|
79
75
|
return scaler
|
|
80
76
|
|
|
81
|
-
# Check if transformer is
|
|
82
|
-
# Unified scaling with StandardScaler is preferred per sktime docs
|
|
77
|
+
# Check if transformer is ColumnEnsembleTransformer (or ColumnTransformer)
|
|
83
78
|
if hasattr(transformer, 'transformers'):
|
|
84
79
|
for name, trans, cols in transformer.transformers:
|
|
85
80
|
scaler = _get_scaler(trans)
|
|
@@ -187,34 +182,76 @@ def _get_scale(scaler: Any, data: np.ndarray) -> Optional[np.ndarray]:
|
|
|
187
182
|
def _normalize_wx(wx: np.ndarray) -> np.ndarray:
|
|
188
183
|
"""Normalize Wx to avoid division by zero.
|
|
189
184
|
|
|
185
|
+
This function replaces zero or NaN values in Wx with 1.0 to prevent
|
|
186
|
+
division by zero during standardization/unstandardization.
|
|
187
|
+
|
|
190
188
|
Parameters
|
|
191
189
|
----------
|
|
192
190
|
wx : np.ndarray
|
|
193
|
-
Scale values (N,)
|
|
191
|
+
Scale values (N,), may contain zeros or NaN
|
|
194
192
|
|
|
195
193
|
Returns
|
|
196
194
|
-------
|
|
197
195
|
np.ndarray
|
|
198
|
-
Normalized scale values with zeros replaced by 1.0
|
|
196
|
+
Normalized scale values with zeros and NaN replaced by 1.0
|
|
199
197
|
"""
|
|
200
|
-
|
|
198
|
+
# Replace both zero and NaN with 1.0
|
|
199
|
+
return np.where((wx == 0) | np.isnan(wx), 1.0, wx)
|
|
201
200
|
|
|
202
201
|
|
|
203
202
|
def _compute_mx_wx(data: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
204
203
|
"""Compute Mx and Wx from data as fallback.
|
|
205
204
|
|
|
205
|
+
This function computes mean (Mx) and standard deviation (Wx) from data,
|
|
206
|
+
handling missing values (NaN) by using nan-aware functions. If NaN values
|
|
207
|
+
are detected, a warning is issued recommending imputation.
|
|
208
|
+
|
|
206
209
|
Parameters
|
|
207
210
|
----------
|
|
208
211
|
data : np.ndarray
|
|
209
|
-
Processed data array (T x N)
|
|
212
|
+
Processed data array (T x N), may contain NaN values
|
|
210
213
|
|
|
211
214
|
Returns
|
|
212
215
|
-------
|
|
213
216
|
Tuple[np.ndarray, np.ndarray]
|
|
214
|
-
(Mx, Wx) tuple where Mx is mean and Wx is normalized std
|
|
217
|
+
(Mx, Wx) tuple where Mx is mean and Wx is normalized std.
|
|
218
|
+
NaN values are handled using nanmean and nanstd.
|
|
219
|
+
|
|
220
|
+
Notes
|
|
221
|
+
-----
|
|
222
|
+
If data contains NaN values, this function will:
|
|
223
|
+
1. Use np.nanmean() and np.nanstd() to compute statistics ignoring NaN
|
|
224
|
+
2. Issue a warning recommending data imputation for better results
|
|
225
|
+
3. Continue processing with available data
|
|
215
226
|
"""
|
|
216
|
-
|
|
217
|
-
|
|
227
|
+
# Check for NaN values
|
|
228
|
+
has_nan = np.any(np.isnan(data))
|
|
229
|
+
if has_nan:
|
|
230
|
+
nan_count = np.sum(np.isnan(data))
|
|
231
|
+
nan_pct = 100.0 * nan_count / data.size
|
|
232
|
+
_logger.warning(
|
|
233
|
+
f"Data contains {nan_count} NaN values ({nan_pct:.1f}%). "
|
|
234
|
+
f"Using nanmean/nanstd to compute standardization parameters. "
|
|
235
|
+
f"For better results, consider imputing missing values before creating DataModule. "
|
|
236
|
+
f"Suggested approaches: forward-fill, backward-fill, or interpolation."
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Use nan-aware functions to handle missing values
|
|
240
|
+
mx = np.nanmean(data, axis=0)
|
|
241
|
+
wx = np.nanstd(data, axis=0)
|
|
242
|
+
|
|
243
|
+
# Check if any series have all NaN values (would result in NaN std)
|
|
244
|
+
nan_std_mask = np.isnan(wx)
|
|
245
|
+
if np.any(nan_std_mask):
|
|
246
|
+
n_nan_std = np.sum(nan_std_mask)
|
|
247
|
+
_logger.warning(
|
|
248
|
+
f"{n_nan_std} series have all NaN values, resulting in NaN standard deviation. "
|
|
249
|
+
f"These will be normalized to 1.0 to avoid division by zero. "
|
|
250
|
+
f"Consider imputing or removing these series."
|
|
251
|
+
)
|
|
252
|
+
# Replace NaN std with 1.0 (will be normalized by _normalize_wx anyway)
|
|
253
|
+
wx = np.where(nan_std_mask, 1.0, wx)
|
|
254
|
+
|
|
218
255
|
wx = _normalize_wx(wx)
|
|
219
256
|
return mx, wx
|
|
220
257
|
|
|
@@ -261,15 +298,17 @@ class DFMDataModule(lightning_pl.LightningDataModule):
|
|
|
261
298
|
|
|
262
299
|
This DataModule handles data loading for DFM/DDFM models.
|
|
263
300
|
|
|
264
|
-
**Important**:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
301
|
+
**Important**: DFM and DDFM can handle missing data (NaN values) implicitly:
|
|
302
|
+
- **DFM**: Uses Kalman filter's `handle_missing_data()` method to skip NaN observations
|
|
303
|
+
- **DDFM**: Uses state-space model and MCMC procedure to handle missing data through
|
|
304
|
+
idiosyncratic component estimation
|
|
268
305
|
|
|
269
306
|
**Usage Pattern**:
|
|
270
|
-
-
|
|
307
|
+
- Data can contain NaN values - models will handle them implicitly
|
|
271
308
|
- If `pipeline=None`, a passthrough transformer is used by default (no-op)
|
|
272
309
|
- Users can optionally provide their preprocessing pipeline to extract statistics (Mx/Wx)
|
|
310
|
+
- For better performance, users can preprocess data (imputation, scaling) before passing,
|
|
311
|
+
but it's not required - models will handle missing data automatically
|
|
273
312
|
|
|
274
313
|
For linear DFM, this uses DFMDataset which returns full sequences.
|
|
275
314
|
For DDFM, use DDFMDataModule which uses DDFMDataset with windowing.
|
|
@@ -311,9 +350,10 @@ class DFMDataModule(lightning_pl.LightningDataModule):
|
|
|
311
350
|
data_path : str or Path, optional
|
|
312
351
|
Path to data file (CSV). If None, data must be provided.
|
|
313
352
|
data : np.ndarray or pd.DataFrame, optional
|
|
314
|
-
|
|
315
|
-
-
|
|
316
|
-
-
|
|
353
|
+
Data array or DataFrame. Can contain NaN values - DFM/DDFM will handle them:
|
|
354
|
+
- DFM: Uses Kalman filter to implicitly handle missing data
|
|
355
|
+
- DDFM: Uses state-space model and MCMC to handle missing data
|
|
356
|
+
- Standardized/scaled data (mean=0, std=1) is recommended for better performance
|
|
317
357
|
- Feature-engineered if needed
|
|
318
358
|
If None, data_path must be provided.
|
|
319
359
|
time_index : TimeIndex, optional
|
|
@@ -417,20 +457,22 @@ class DFMDataModule(lightning_pl.LightningDataModule):
|
|
|
417
457
|
This method is called by Lightning to set up the data module.
|
|
418
458
|
It loads preprocessed data and extracts statistics from the pipeline for forecasting/nowcasting.
|
|
419
459
|
|
|
420
|
-
**Important**:
|
|
421
|
-
|
|
460
|
+
**Important**: DFM and DDFM can handle missing data (NaN values) implicitly:
|
|
461
|
+
- **DFM**: Uses Kalman filter's `handle_missing_data()` method
|
|
462
|
+
- **DDFM**: Uses state-space model and MCMC procedure
|
|
422
463
|
|
|
423
464
|
**Pipeline Purpose**:
|
|
424
465
|
- The pipeline is used to extract statistics (e.g., Mx/Wx from StandardScaler) needed for
|
|
425
466
|
forecasting and nowcasting operations, not to preprocess the data
|
|
426
|
-
- Data is already preprocessed - the pipeline is fitted to extract standardization parameters
|
|
427
467
|
- The pipeline is fitted on the **entire dataset** to extract statistics
|
|
428
468
|
- If `val_split` is provided, it's used for validation during training (e.g., DDFM neural network)
|
|
429
469
|
|
|
430
470
|
**Usage**:
|
|
431
|
-
-
|
|
471
|
+
- Data can contain NaN values - models will handle them automatically
|
|
432
472
|
- Optionally provide pipeline (e.g., TransformerPipeline with StandardScaler) to extract Mx/Wx for predictions
|
|
433
473
|
- If pipeline=None, uses passthrough (no statistics extracted, Mx/Wx computed from data)
|
|
474
|
+
- For better performance, users can preprocess data (imputation, scaling) before passing,
|
|
475
|
+
but it's not required
|
|
434
476
|
"""
|
|
435
477
|
# Load data if not already provided
|
|
436
478
|
if self.data is None:
|
|
@@ -451,6 +493,9 @@ class DFMDataModule(lightning_pl.LightningDataModule):
|
|
|
451
493
|
self.time_index = Time
|
|
452
494
|
|
|
453
495
|
# Convert to pandas DataFrame if needed
|
|
496
|
+
# Note: DFM/DDFM can handle NaN values implicitly via Kalman filter
|
|
497
|
+
# (DFM uses handle_missing_data() in Kalman filter, DDFM uses state-space model)
|
|
498
|
+
# So we don't validate NaN here - let the models handle it
|
|
454
499
|
if isinstance(self.data, np.ndarray):
|
|
455
500
|
series_ids = self.config.get_series_ids()
|
|
456
501
|
X_df = pd.DataFrame(self.data, columns=series_ids)
|
|
@@ -505,7 +550,10 @@ class DFMDataModule(lightning_pl.LightningDataModule):
|
|
|
505
550
|
f"automatically extracted from the pipeline."
|
|
506
551
|
) from e
|
|
507
552
|
|
|
508
|
-
#
|
|
553
|
+
# Note: DFM/DDFM can handle NaN values implicitly via Kalman filter
|
|
554
|
+
# So we don't validate NaN here - let the models handle it
|
|
555
|
+
|
|
556
|
+
# Ensure output is pandas DataFrame with compatible index
|
|
509
557
|
if not isinstance(X_transformed, pd.DataFrame):
|
|
510
558
|
# If transformer returned numpy array, convert to pandas
|
|
511
559
|
if isinstance(X_transformed, np.ndarray):
|
|
@@ -532,6 +580,45 @@ class DFMDataModule(lightning_pl.LightningDataModule):
|
|
|
532
580
|
# Create generic column names
|
|
533
581
|
X_transformed = pd.DataFrame(X_transformed,
|
|
534
582
|
columns=[f'feature_{i}' for i in range(n_cols)])
|
|
583
|
+
|
|
584
|
+
# Ensure index is compatible (DatetimeIndex, PeriodIndex, or RangeIndex)
|
|
585
|
+
# Use input DataFrame index if length matches
|
|
586
|
+
if len(X_transformed) == len(X_df):
|
|
587
|
+
if isinstance(X_df.index, (pd.DatetimeIndex, pd.PeriodIndex, pd.RangeIndex)):
|
|
588
|
+
X_transformed.index = X_df.index
|
|
589
|
+
else:
|
|
590
|
+
# Try to convert to DatetimeIndex
|
|
591
|
+
try:
|
|
592
|
+
X_transformed.index = pd.to_datetime(X_df.index)
|
|
593
|
+
except (ValueError, TypeError):
|
|
594
|
+
# Fallback to RangeIndex
|
|
595
|
+
X_transformed.index = pd.RangeIndex(start=0, stop=len(X_transformed))
|
|
596
|
+
else:
|
|
597
|
+
# Length doesn't match - use RangeIndex
|
|
598
|
+
X_transformed.index = pd.RangeIndex(start=0, stop=len(X_transformed))
|
|
599
|
+
|
|
600
|
+
# Ensure DataFrame index is compatible (DatetimeIndex, PeriodIndex, or RangeIndex)
|
|
601
|
+
# This handles cases where pipeline output has incompatible index
|
|
602
|
+
if isinstance(X_transformed, pd.DataFrame):
|
|
603
|
+
if not isinstance(X_transformed.index, (pd.DatetimeIndex, pd.PeriodIndex, pd.RangeIndex)):
|
|
604
|
+
# Try to use input DataFrame index if length matches
|
|
605
|
+
if len(X_transformed) == len(X_df):
|
|
606
|
+
if isinstance(X_df.index, (pd.DatetimeIndex, pd.PeriodIndex, pd.RangeIndex)):
|
|
607
|
+
X_transformed.index = X_df.index
|
|
608
|
+
else:
|
|
609
|
+
# Try to convert to DatetimeIndex
|
|
610
|
+
try:
|
|
611
|
+
X_transformed.index = pd.to_datetime(X_df.index)
|
|
612
|
+
except (ValueError, TypeError):
|
|
613
|
+
# Fallback to RangeIndex
|
|
614
|
+
X_transformed.index = pd.RangeIndex(start=0, stop=len(X_transformed))
|
|
615
|
+
else:
|
|
616
|
+
# Try to convert existing index
|
|
617
|
+
try:
|
|
618
|
+
X_transformed.index = pd.to_datetime(X_transformed.index)
|
|
619
|
+
except (ValueError, TypeError):
|
|
620
|
+
# Fallback to RangeIndex
|
|
621
|
+
X_transformed.index = pd.RangeIndex(start=0, stop=len(X_transformed))
|
|
535
622
|
elif hasattr(X_transformed, 'to_numpy'):
|
|
536
623
|
# Has to_numpy method (pandas DataFrame) - convert to numpy then back to DataFrame
|
|
537
624
|
# Try to preserve column names if available
|
|
@@ -572,9 +659,7 @@ class DFMDataModule(lightning_pl.LightningDataModule):
|
|
|
572
659
|
# This is optional - some pipelines may not have standardization
|
|
573
660
|
# Mx and Wx are already initialized in __init__
|
|
574
661
|
try:
|
|
575
|
-
# Extract scaler from pipeline (
|
|
576
|
-
# Supports any scaler type: StandardScaler, MinMaxScaler, RobustScaler, etc.
|
|
577
|
-
# Note: New code should use StandardScaler() directly per sktime docs (unified scaling)
|
|
662
|
+
# Extract scaler from pipeline (supports TransformerPipeline, StandardScaler, etc.)
|
|
578
663
|
scaler = _get_scaler(pipeline_to_use)
|
|
579
664
|
|
|
580
665
|
if scaler is not None:
|