dfm-python 0.4.51__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. dfm_python/__init__.py +3 -20
  2. dfm_python/config/adapter.py +1 -2
  3. dfm_python/config/results.py +53 -1
  4. dfm_python/config/schema.py +8 -13
  5. dfm_python/data/__init__.py +2 -0
  6. dfm_python/data/dataloader.py +3 -3
  7. dfm_python/data/utils.py +2 -3
  8. dfm_python/encoder/vae.py +15 -0
  9. dfm_python/lightning/__init__.py +2 -5
  10. dfm_python/lightning/data_module.py +116 -31
  11. dfm_python/lightning/scaling.py +32 -26
  12. dfm_python/logger/inference_logger.py +1 -1
  13. dfm_python/logger/logger.py +23 -10
  14. dfm_python/logger/train_logger.py +1 -1
  15. dfm_python/models/__init__.py +2 -0
  16. dfm_python/models/base.py +481 -152
  17. dfm_python/models/ddfm.py +572 -1433
  18. dfm_python/models/dfm.py +204 -45
  19. dfm_python/models/mcmc.py +491 -0
  20. dfm_python/models/utils.py +467 -0
  21. dfm_python/ssm/em.py +534 -53
  22. dfm_python/ssm/kalman.py +336 -30
  23. dfm_python/ssm/utils.py +65 -17
  24. dfm_python/trainer/__init__.py +10 -3
  25. dfm_python/trainer/ddfm.py +5 -4
  26. dfm_python/trainer/dfm.py +3 -3
  27. dfm_python/utils/__init__.py +9 -32
  28. dfm_python/utils/data.py +63 -3
  29. dfm_python/utils/helpers.py +1 -1
  30. dfm_python/utils/statespace.py +4 -5
  31. dfm_python/utils/time.py +3 -3
  32. dfm_python-0.5.0.dist-info/METADATA +393 -0
  33. dfm_python-0.5.0.dist-info/RECORD +44 -0
  34. dfm_python/nowcast/__init__.py +0 -37
  35. dfm_python/nowcast/dataview.py +0 -143
  36. dfm_python/nowcast/helpers.py +0 -356
  37. dfm_python/nowcast/nowcast.py +0 -1263
  38. dfm_python/nowcast/splitters.py +0 -441
  39. dfm_python/nowcast/transformers.py +0 -330
  40. dfm_python/nowcast/utils.py +0 -285
  41. dfm_python-0.4.51.dist-info/METADATA +0 -1299
  42. dfm_python-0.4.51.dist-info/RECORD +0 -49
  43. {dfm_python-0.4.51.dist-info → dfm_python-0.5.0.dist-info}/WHEEL +0 -0
  44. {dfm_python-0.4.51.dist-info → dfm_python-0.5.0.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.4.51"
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. Nowcasting: Nowcast, result classes, para_const
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', 'Nowcast',
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)
@@ -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
@@ -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,57 @@ 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
+ >>> model = DFM()
317
+ >>> trainer.fit(model, data_module)
318
+ >>> # Get nowcast with full result
319
+ >>> result = model.nowcast('gdp', view_date='2024-01-15', return_result=True)
320
+ >>> print(f"Nowcast: {result.nowcast_value}")
321
+ >>> print(f"Available data: {result.data_availability['n_available']}")
322
+ """
323
+ target_series: str
324
+ target_period: datetime
325
+ view_date: datetime
326
+ nowcast_value: float
327
+ confidence_interval: Optional[Tuple[float, float]] = None # (lower, upper)
328
+ factors_at_view: Optional[np.ndarray] = None # Factor values at view_date
329
+ dfm_result: Optional[BaseResult] = None # Full DFM/DDFM result for this view
330
+ data_availability: Optional[Dict[str, int]] = None # n_available, n_missing
331
+
332
+
281
333
  @dataclass
282
334
  class FitParams:
283
335
  """Parameter overrides for DFM model fitting.
@@ -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)
@@ -660,6 +655,7 @@ class DFMConfig(BaseModelConfig):
660
655
  'nan_method': data.get('nan_method', 2),
661
656
  'nan_k': data.get('nan_k', 3),
662
657
  'clock': data.get('clock', 'm'),
658
+ 'scaler': data.get('scaler', 'standard'), # Unified scaler for all series (default: 'standard')
663
659
  }
664
660
 
665
661
  @classmethod
@@ -716,11 +712,11 @@ class DDFMConfig(BaseModelConfig):
716
712
  # ========================================================================
717
713
  encoder_layers: Optional[List[int]] = None # Hidden layer dimensions for encoder (default: [64, 32])
718
714
  num_factors: Optional[int] = None # Number of factors (inferred from config if None)
719
- activation: str = 'tanh' # Activation function ('tanh', 'relu', 'sigmoid', default: 'tanh')
715
+ activation: str = 'relu' # Activation function ('tanh', 'relu', 'sigmoid', default: 'relu' to match original DDFM)
720
716
  use_batch_norm: bool = True # Use batch normalization in encoder (default: True)
721
717
  learning_rate: float = 0.001 # Learning rate for Adam optimizer (default: 0.001)
722
718
  epochs: int = 100 # Number of training epochs (default: 100)
723
- batch_size: int = 32 # Batch size for training (default: 32)
719
+ batch_size: int = 100 # Batch size for training (default: 100 to match original DDFM)
724
720
  factor_order: int = 1 # VAR lag order for factor dynamics (1 or 2, default: 1)
725
721
  use_idiosyncratic: bool = True # Model idio components with AR(1) dynamics (default: True)
726
722
  min_obs_idio: int = 5 # Minimum observations for idio AR(1) estimation (default: 5)
@@ -750,15 +746,15 @@ class DDFMConfig(BaseModelConfig):
750
746
  def _extract_ddfm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
751
747
  """Extract DDFM-specific parameters from config dict."""
752
748
  base_params = cls._extract_base(data)
753
- # Handle both direct keys and ddfm_ prefix format (for backward compatibility)
749
+ # Handle both direct keys and ddfm_ prefix format
754
750
  base_params.update({
755
751
  'encoder_layers': data.get('encoder_layers') or data.get('ddfm_encoder_layers', None),
756
752
  'num_factors': data.get('num_factors') or data.get('ddfm_num_factors', None),
757
- 'activation': data.get('activation') or data.get('ddfm_activation', 'tanh'),
753
+ 'activation': data.get('activation') or data.get('ddfm_activation', 'relu'),
758
754
  'use_batch_norm': data.get('use_batch_norm', data.get('ddfm_use_batch_norm', True)),
759
755
  'learning_rate': data.get('learning_rate', data.get('ddfm_learning_rate', 0.001)),
760
756
  'epochs': data.get('epochs', data.get('ddfm_epochs', 100)),
761
- 'batch_size': data.get('batch_size', data.get('ddfm_batch_size', 32)),
757
+ 'batch_size': data.get('batch_size', data.get('ddfm_batch_size', 100)),
762
758
  'factor_order': data.get('factor_order', data.get('ddfm_factor_order', 1)),
763
759
  'use_idiosyncratic': data.get('use_idiosyncratic', data.get('ddfm_use_idiosyncratic', True)),
764
760
  'min_obs_idio': data.get('min_obs_idio', data.get('ddfm_min_obs_idio', 5)),
@@ -832,8 +828,7 @@ class DDFMConfig(BaseModelConfig):
832
828
  transformation=series_cfg.get('transformation', 'lin'),
833
829
  blocks=series_blocks,
834
830
  units=series_cfg.get('units', None), # Optional, for display only
835
- release_date=series_cfg.get('release_date', None), # Optional, for nowcasting
836
- scaler=series_cfg.get('scaler', None) # Optional, for per-series scaling
831
+ release_date=series_cfg.get('release_date', None) # Optional, for nowcasting
837
832
  ))
838
833
 
839
834
  # Convert blocks_dict to dict of block properties
@@ -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
@@ -101,7 +101,7 @@ def create_dfm_dataloader(
101
101
 
102
102
  def create_ddfm_dataloader(
103
103
  dataset: DDFMDataset,
104
- batch_size: int = 32,
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 32
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 (for backward compatibility)
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 = safe_get_attr(config, 'clock', 'm')
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
 
@@ -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', # New module class
35
+ 'KalmanFilter',
37
36
  'KalmanFilterState',
38
37
  # EM algorithm
39
- 'EMAlgorithm', # New module class
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 (can be any scaler, StandardScaler directly, or TabularToSeriesAdaptor for backward compatibility).
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 TabularToSeriesAdaptor (backward compatibility only)
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 ColumnTransformer (backward compatibility only - not recommended for new code)
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
- return np.where(wx == 0, 1.0, wx)
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
- mx = np.mean(data, axis=0)
217
- wx = np.std(data, axis=0)
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**: This package expects preprocessed data. All preprocessing (imputation,
265
- scaling, feature engineering, etc.) must be done by the user before passing data to
266
- this DataModule. Users are responsible for creating their own preprocessing pipelines
267
- using sktime or other tools.
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
- - Provide already-preprocessed data (standardized, no missing values, feature-engineered)
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
- Preprocessed data array or DataFrame. Data must be:
315
- - Standardized/scaled (mean=0, std=1 recommended for DFM/DDFM)
316
- - No missing values (NaN-free)
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**: Data must be fully preprocessed by the user before passing to this DataModule.
421
- All preprocessing (imputation, scaling, feature engineering, etc.) is the user's responsibility.
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
- - Provide preprocessed data (standardized, no missing values)
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
- # Ensure output is pandas DataFrame
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 (works with TransformerPipeline, StandardScaler directly, or TabularToSeriesAdaptor for backward compatibility)
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: