dfm-python 0.4.2__py3-none-any.whl → 0.4.3__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 +9 -11
  2. dfm_python/config/__init__.py +8 -13
  3. dfm_python/config/adapter.py +12 -210
  4. dfm_python/config/results.py +18 -10
  5. dfm_python/config/schema.py +31 -66
  6. dfm_python/config/utils.py +13 -13
  7. dfm_python/data/__init__.py +5 -29
  8. dfm_python/data/dataloader.py +51 -8
  9. dfm_python/data/utils.py +28 -30
  10. dfm_python/encoder/vae.py +3 -2
  11. dfm_python/lightning/__init__.py +27 -14
  12. dfm_python/lightning/data_module.py +348 -220
  13. dfm_python/lightning/scaling.py +432 -0
  14. dfm_python/models/__init__.py +3 -3
  15. dfm_python/models/base.py +335 -6
  16. dfm_python/models/ddfm.py +1137 -321
  17. dfm_python/models/dfm.py +125 -586
  18. dfm_python/models/estimation.py +13 -14
  19. dfm_python/nowcast/__init__.py +17 -8
  20. dfm_python/nowcast/dataview.py +7 -7
  21. dfm_python/nowcast/helpers.py +6 -6
  22. dfm_python/nowcast/nowcast.py +274 -26
  23. dfm_python/nowcast/splitters.py +431 -0
  24. dfm_python/nowcast/transformers.py +330 -0
  25. dfm_python/nowcast/utils.py +11 -11
  26. dfm_python/ssm/kalman.py +13 -27
  27. dfm_python/ssm/utils.py +3 -1
  28. dfm_python/trainer/__init__.py +22 -22
  29. dfm_python/trainer/ddfm.py +7 -7
  30. dfm_python/trainer/dfm.py +7 -7
  31. dfm_python/utils/__init__.py +15 -15
  32. dfm_python/utils/data.py +17 -20
  33. dfm_python/utils/diagnostics.py +28 -23
  34. dfm_python/utils/helpers.py +7 -7
  35. dfm_python/utils/statespace.py +41 -71
  36. dfm_python/utils/time.py +148 -249
  37. {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/METADATA +24 -64
  38. dfm_python-0.4.3.dist-info/RECORD +52 -0
  39. dfm_python/data/transformation.py +0 -313
  40. dfm_python/lightning/ddfm_module.py +0 -580
  41. dfm_python/lightning/dfm_module.py +0 -466
  42. dfm_python-0.4.2.dist-info/RECORD +0 -52
  43. {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/WHEEL +0 -0
  44. {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/licenses/LICENSE +0 -0
@@ -19,7 +19,7 @@ Blocks are defined as Dict[str, Dict[str, Any]] where each block is a dict with:
19
19
  - ar_lag: int (AR lag order)
20
20
  - clock: str (block clock frequency)
21
21
 
22
- For loading configurations from files (YAML, Spec CSV) or other sources,
22
+ For loading configurations from files (YAML) or other sources,
23
23
  see the config.adapter module which provides source adapters.
24
24
  """
25
25
 
@@ -89,7 +89,7 @@ def _parse_blocks_dict(blocks_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]
89
89
  return blocks_dict
90
90
 
91
91
 
92
- def _infer_blocks_from_series(
92
+ def _infer_blocks(
93
93
  series_list: List['SeriesConfig'],
94
94
  data: Dict[str, Any]
95
95
  ) -> Dict[str, Dict[str, Any]]:
@@ -200,6 +200,11 @@ 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').
203
208
  """
204
209
  # Required fields (no defaults)
205
210
  frequency: str
@@ -210,6 +215,7 @@ class SeriesConfig:
210
215
  series_name: Optional[str] = None # Optional metadata for display
211
216
  units: Optional[str] = None # Optional metadata for display only (used in news.py output)
212
217
  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
213
219
 
214
220
  def __post_init__(self):
215
221
  """Validate fields after initialization."""
@@ -648,7 +654,7 @@ class DFMConfig(BaseModelConfig):
648
654
  # ========================================================================
649
655
 
650
656
  @classmethod
651
- def _extract_base_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
657
+ def _extract_base(cls, data: Dict[str, Any]) -> Dict[str, Any]:
652
658
  """Extract shared base parameters from config dict."""
653
659
  return {
654
660
  'nan_method': data.get('nan_method', 2),
@@ -659,7 +665,7 @@ class DFMConfig(BaseModelConfig):
659
665
  @classmethod
660
666
  def _extract_dfm_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
661
667
  """Extract DFM-specific parameters from config dict."""
662
- base_params = cls._extract_base_params(data)
668
+ base_params = cls._extract_base(data)
663
669
  base_params.update({
664
670
  'ar_lag': data.get('ar_lag', 1),
665
671
  'threshold': data.get('threshold', 1e-5),
@@ -731,50 +737,19 @@ class DDFMConfig(BaseModelConfig):
731
737
  # ========================================================================
732
738
 
733
739
  @classmethod
734
- def _extract_base_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
735
- """Extract shared base parameters from config dict."""
736
- return {
737
- 'nan_method': data.get('nan_method', 2),
738
- 'nan_k': data.get('nan_k', 3),
739
- 'clock': data.get('clock', 'm'),
740
- }
740
+ def _extract_base(cls, data: Dict[str, Any]) -> Dict[str, Any]:
741
+ """Extract shared base parameters from config dict (delegates to DFMConfig)."""
742
+ return DFMConfig._extract_base(data)
741
743
 
742
744
  @classmethod
743
745
  def _extract_dfm_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
744
- """Extract DFM-specific parameters from config dict."""
745
- base_params = cls._extract_base_params(data)
746
- base_params.update({
747
- 'ar_lag': data.get('ar_lag', 1),
748
- 'threshold': data.get('threshold', 1e-5),
749
- 'max_iter': data.get('max_iter', 5000),
750
- # Numerical stability parameters
751
- 'clip_ar_coefficients': data.get('clip_ar_coefficients', True),
752
- 'ar_clip_min': data.get('ar_clip_min', -0.99),
753
- 'ar_clip_max': data.get('ar_clip_max', 0.99),
754
- 'warn_on_ar_clip': data.get('warn_on_ar_clip', True),
755
- 'clip_data_values': data.get('clip_data_values', True),
756
- 'data_clip_threshold': data.get('data_clip_threshold', 100.0),
757
- 'warn_on_data_clip': data.get('warn_on_data_clip', True),
758
- 'use_regularization': data.get('use_regularization', True),
759
- 'regularization_scale': data.get('regularization_scale', 1e-5),
760
- 'min_eigenvalue': data.get('min_eigenvalue', 1e-8),
761
- 'max_eigenvalue': data.get('max_eigenvalue', 1e6),
762
- 'warn_on_regularization': data.get('warn_on_regularization', True),
763
- 'use_damped_updates': data.get('use_damped_updates', True),
764
- 'damping_factor': data.get('damping_factor', 0.8),
765
- 'warn_on_damped_update': data.get('warn_on_damped_update', True),
766
- # Idiosyncratic component augmentation
767
- 'augment_idio': data.get('augment_idio', True),
768
- 'augment_idio_slow': data.get('augment_idio_slow', True),
769
- 'idio_rho0': data.get('idio_rho0', 0.1),
770
- 'idio_min_var': data.get('idio_min_var', 1e-8),
771
- })
772
- return base_params
746
+ """Extract DFM-specific parameters from config dict (delegates to DFMConfig)."""
747
+ return DFMConfig._extract_dfm_params(data)
773
748
 
774
749
  @classmethod
775
- def _extract_ddfm_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
750
+ def _extract_ddfm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
776
751
  """Extract DDFM-specific parameters from config dict."""
777
- base_params = cls._extract_base_params(data)
752
+ base_params = cls._extract_base(data)
778
753
  # Handle both direct keys and ddfm_ prefix format (for backward compatibility)
779
754
  base_params.update({
780
755
  'encoder_layers': data.get('encoder_layers') or data.get('ddfm_encoder_layers', None),
@@ -810,7 +785,7 @@ class DDFMConfig(BaseModelConfig):
810
785
  factors_per_block = data.get('factors_per_block', None)
811
786
 
812
787
  # If block_names is still empty, try to infer from series blocks
813
- if not block_names and 'series' in data:
788
+ if not block_names and 'series' in data and isinstance(data['series'], dict):
814
789
  # Collect all unique block names from series
815
790
  all_blocks = set()
816
791
  for series_cfg in data['series'].values():
@@ -856,7 +831,9 @@ class DDFMConfig(BaseModelConfig):
856
831
  frequency=series_cfg.get('frequency', 'm'),
857
832
  transformation=series_cfg.get('transformation', 'lin'),
858
833
  blocks=series_blocks,
859
- units=series_cfg.get('units', None) # Optional, for display only
834
+ 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
860
837
  ))
861
838
 
862
839
  # Convert blocks_dict to dict of block properties
@@ -893,7 +870,7 @@ class DDFMConfig(BaseModelConfig):
893
870
  return DDFMConfig(
894
871
  series=series_list,
895
872
  blocks=blocks_dict_final,
896
- **DDFMConfig._extract_ddfm_params(data)
873
+ **DDFMConfig._extract_ddfm(data)
897
874
  )
898
875
  else:
899
876
  return DFMConfig(
@@ -930,7 +907,7 @@ class DDFMConfig(BaseModelConfig):
930
907
  raise ValueError(f"blocks must be a dict, got {type(blocks_data)}")
931
908
  else:
932
909
  # If no blocks provided, infer from series using helper
933
- blocks_dict = _infer_blocks_from_series(series_list, data)
910
+ blocks_dict = _infer_blocks(series_list, data)
934
911
 
935
912
  # Determine config type using helper function
936
913
  config_type = _detect_config_type(data)
@@ -939,7 +916,7 @@ class DDFMConfig(BaseModelConfig):
939
916
  return DDFMConfig(
940
917
  series=series_list,
941
918
  blocks=blocks_dict,
942
- **DDFMConfig._extract_ddfm_params(data)
919
+ **DDFMConfig._extract_ddfm(data)
943
920
  )
944
921
  else:
945
922
  return DFMConfig(
@@ -1016,7 +993,7 @@ def _dfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig'
1016
993
  return DDFMConfig(
1017
994
  series=series_list,
1018
995
  blocks=blocks_dict,
1019
- **DDFMConfig._extract_ddfm_params(data)
996
+ **DDFMConfig._extract_ddfm(data)
1020
997
  )
1021
998
  else:
1022
999
  return DFMConfig(
@@ -1033,8 +1010,8 @@ def _dfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig'
1033
1010
  else:
1034
1011
  return DFMConfig(**data)
1035
1012
 
1036
- def _dfm_from_hydra(cls, cfg: Any) -> 'DFMConfig':
1037
- """Create DFMConfig from Hydra config."""
1013
+ def _from_hydra(cls, cfg: Any) -> Union['DFMConfig', 'DDFMConfig']:
1014
+ """Create config from Hydra DictConfig (auto-detects DFM/DDFM)."""
1038
1015
  try:
1039
1016
  from omegaconf import DictConfig, OmegaConf
1040
1017
  if isinstance(cfg, DictConfig):
@@ -1043,10 +1020,10 @@ def _dfm_from_hydra(cls, cfg: Any) -> 'DFMConfig':
1043
1020
  pass
1044
1021
  if not isinstance(cfg, dict):
1045
1022
  raise TypeError("from_hydra expects a DictConfig or dict.")
1046
- return DFMConfig.from_dict(cfg)
1023
+ return cls.from_dict(cfg)
1047
1024
 
1048
1025
  DFMConfig.from_dict = classmethod(_dfm_from_dict)
1049
- DFMConfig.from_hydra = classmethod(_dfm_from_hydra)
1026
+ DFMConfig.from_hydra = classmethod(_from_hydra)
1050
1027
 
1051
1028
  # Add factory methods to DDFMConfig class
1052
1029
  def _ddfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig']:
@@ -1083,7 +1060,7 @@ def _ddfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig
1083
1060
  return DDFMConfig(
1084
1061
  series=series_list,
1085
1062
  blocks=blocks_dict,
1086
- **DDFMConfig._extract_ddfm_params(data)
1063
+ **DDFMConfig._extract_ddfm(data)
1087
1064
  )
1088
1065
  else:
1089
1066
  return DFMConfig(
@@ -1105,19 +1082,7 @@ def _ddfm_from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig
1105
1082
  else:
1106
1083
  return DFMConfig(**data)
1107
1084
 
1108
- def _ddfm_from_hydra(cls, cfg: Any) -> 'DDFMConfig':
1109
- """Create DDFMConfig from Hydra config."""
1110
- try:
1111
- from omegaconf import DictConfig, OmegaConf
1112
- if isinstance(cfg, DictConfig):
1113
- cfg = OmegaConf.to_container(cfg, resolve=True)
1114
- except Exception:
1115
- pass
1116
- if not isinstance(cfg, dict):
1117
- raise TypeError("from_hydra expects a DictConfig or dict.")
1118
- return DDFMConfig.from_dict(cfg)
1119
-
1120
1085
  DDFMConfig.from_dict = classmethod(_ddfm_from_dict)
1121
- DDFMConfig.from_hydra = classmethod(_ddfm_from_hydra)
1086
+ DDFMConfig.from_hydra = classmethod(_from_hydra)
1122
1087
 
1123
1088
 
@@ -3,8 +3,8 @@
3
3
  This module provides:
4
4
  1. Validation functions: validate_frequency, validate_transformation
5
5
  2. Frequency hierarchy and period calculations: FREQUENCY_HIERARCHY, PERIODS_PER_YEAR, get_periods_per_year, get_annual_factor
6
- 3. Tent kernel utilities: generate_tent_weights, generate_R_mat, get_tent_weights_for_pair
7
- 4. Aggregation structure: get_aggregation_structure, group_series_by_frequency, compute_idio_chain_lengths
6
+ 3. Tent kernel utilities: generate_tent_weights, generate_R_mat, get_tent_weights
7
+ 4. Aggregation structure: get_agg_structure, group_by_freq, compute_idio_lengths
8
8
  5. Transformation mappings: TRANSFORM_UNITS_MAP
9
9
  """
10
10
 
@@ -299,7 +299,7 @@ def generate_R_mat(tent_weights: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
299
299
  return R_mat, q
300
300
 
301
301
 
302
- def get_tent_weights_for_pair(slower_freq: str, faster_freq: str) -> Optional[np.ndarray]:
302
+ def get_tent_weights(slower_freq: str, faster_freq: str) -> Optional[np.ndarray]:
303
303
  """Get deterministic tent weights for a frequency pair.
304
304
 
305
305
  Parameters:
@@ -316,16 +316,16 @@ def get_tent_weights_for_pair(slower_freq: str, faster_freq: str) -> Optional[np
316
316
 
317
317
  Examples:
318
318
  --------
319
- >>> get_tent_weights_for_pair('q', 'm')
319
+ >>> get_tent_weights('q', 'm')
320
320
  array([1, 2, 3, 2, 1]) # Quarterly -> monthly
321
321
 
322
- >>> get_tent_weights_for_pair('m', 'd')
322
+ >>> get_tent_weights('m', 'd')
323
323
  None # Not supported (too large gap)
324
324
  """
325
325
  return TENT_WEIGHTS_LOOKUP.get((slower_freq, faster_freq))
326
326
 
327
327
 
328
- def get_aggregation_structure(
328
+ def get_agg_structure(
329
329
  config: 'DFMConfig',
330
330
  clock: str = 'm'
331
331
  ) -> Dict[str, Any]:
@@ -366,7 +366,7 @@ def get_aggregation_structure(
366
366
  --------
367
367
  >>> from dfm_python import load_config
368
368
  >>> config = load_config('config.yaml')
369
- >>> agg_info = get_aggregation_structure(config, clock='m')
369
+ >>> agg_info = get_agg_structure(config, clock='m')
370
370
  >>> # Check which frequencies need tent kernels
371
371
  >>> print(agg_info['tent_weights'])
372
372
  {'q': array([1, 2, 3, 2, 1]), 'sa': array([1, 2, 3, 4, 3, 2, 1])}
@@ -382,7 +382,7 @@ def get_aggregation_structure(
382
382
  for freq in frequencies:
383
383
  if FREQUENCY_HIERARCHY.get(freq, 999) > FREQUENCY_HIERARCHY.get(clock, 0):
384
384
  # This frequency is slower than clock, check if tent kernel is available
385
- tent_w = get_tent_weights_for_pair(freq, clock)
385
+ tent_w = get_tent_weights(freq, clock)
386
386
  if tent_w is not None and len(tent_w) <= MAX_TENT_SIZE:
387
387
  # Tent kernel available and within size limit
388
388
  tent_weights[freq] = tent_w
@@ -400,7 +400,7 @@ def get_aggregation_structure(
400
400
  }
401
401
 
402
402
 
403
- def group_series_by_frequency(
403
+ def group_by_freq(
404
404
  idx_i: np.ndarray,
405
405
  frequencies: np.ndarray,
406
406
  clock: str
@@ -477,7 +477,7 @@ def group_series_by_frequency(
477
477
  return {freq: np.array(indices, dtype=int) for freq, indices in freq_groups.items()}
478
478
 
479
479
 
480
- def compute_idio_chain_lengths(
480
+ def compute_idio_lengths(
481
481
  config: 'DFMConfig',
482
482
  clock: str,
483
483
  tent_weights_dict: Optional[Dict[str, np.ndarray]] = None
@@ -496,7 +496,7 @@ def compute_idio_chain_lengths(
496
496
  Clock frequency ('d', 'w', 'm', 'q', 'sa', 'a')
497
497
  tent_weights_dict : Dict[str, np.ndarray], optional
498
498
  Dictionary mapping frequency strings to tent weight arrays.
499
- If None, will be computed from config using get_aggregation_structure.
499
+ If None, will be computed from config using get_agg_structure.
500
500
 
501
501
  Returns
502
502
  -------
@@ -510,7 +510,7 @@ def compute_idio_chain_lengths(
510
510
  --------
511
511
  >>> from dfm_python import load_config
512
512
  >>> config = load_config('config.yaml')
513
- >>> lengths = compute_idio_chain_lengths(config, clock='m')
513
+ >>> lengths = compute_idio_lengths(config, clock='m')
514
514
  >>> # Returns array with 1 for monthly series, 5 for quarterly, etc.
515
515
  """
516
516
  if not config.augment_idio:
@@ -523,7 +523,7 @@ def compute_idio_chain_lengths(
523
523
 
524
524
  # Get tent weights if not provided
525
525
  if tent_weights_dict is None:
526
- agg_structure = get_aggregation_structure(config, clock=clock)
526
+ agg_structure = get_agg_structure(config, clock=clock)
527
527
  tent_weights_dict = agg_structure.get('tent_weights', {})
528
528
 
529
529
  lengths = np.zeros(len(config.series), dtype=int)
@@ -1,29 +1,18 @@
1
- """Data loading, transformation, and dataset utilities for DFM.
1
+ """Data loading and dataset utilities for DFM.
2
2
 
3
3
  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
- - Transformation functions: pch_transform, pc1_transform, etc.
7
+
8
+ Note: Transformation functions are application-specific and should be provided
9
+ by users in their preprocessing pipelines. The package does not include
10
+ transformation utilities to remain generic.
8
11
  """
9
12
 
10
13
  from .dataset import DFMDataset, DDFMDataset
11
14
  from .dataloader import create_dfm_dataloader, create_ddfm_dataloader
12
15
  from .utils import read_data, load_data
13
- from .transformation import (
14
- pch_transform,
15
- pc1_transform,
16
- pca_transform,
17
- cch_transform,
18
- cca_transform,
19
- log_transform,
20
- identity_transform,
21
- make_pch_transformer,
22
- make_pc1_transformer,
23
- make_pca_transformer,
24
- make_cch_transformer,
25
- make_cca_transformer,
26
- )
27
16
 
28
17
  __all__ = [
29
18
  # Datasets
@@ -35,18 +24,5 @@ __all__ = [
35
24
  # Data reading
36
25
  'read_data',
37
26
  'load_data',
38
- # Transformations
39
- 'pch_transform',
40
- 'pc1_transform',
41
- 'pca_transform',
42
- 'cch_transform',
43
- 'cca_transform',
44
- 'log_transform',
45
- 'identity_transform',
46
- 'make_pch_transformer',
47
- 'make_pc1_transformer',
48
- 'make_pca_transformer',
49
- 'make_cch_transformer',
50
- 'make_cca_transformer',
51
27
  ]
52
28
 
@@ -7,13 +7,56 @@ DDFM uses a standard DataLoader with batching for neural network training.
7
7
 
8
8
  import torch
9
9
  from torch.utils.data import DataLoader
10
- from typing import Optional
10
+ from typing import Optional, Union
11
11
  from .dataset import DFMDataset, DDFMDataset
12
12
  from ..logger import get_logger
13
13
 
14
14
  _logger = get_logger(__name__)
15
15
 
16
16
 
17
+ def _create_dataloader(
18
+ dataset: Union[DFMDataset, DDFMDataset],
19
+ batch_size: int,
20
+ shuffle: bool,
21
+ num_workers: int,
22
+ pin_memory: bool,
23
+ auto_pin_memory: bool = False
24
+ ) -> DataLoader:
25
+ """Create DataLoader (shared helper).
26
+
27
+ Parameters
28
+ ----------
29
+ dataset : DFMDataset or DDFMDataset
30
+ Dataset instance
31
+ batch_size : int
32
+ Batch size
33
+ shuffle : bool
34
+ Whether to shuffle data
35
+ num_workers : int
36
+ Number of worker processes
37
+ pin_memory : bool
38
+ Whether to pin memory for faster GPU transfer
39
+ auto_pin_memory : bool, default False
40
+ If True, automatically disable pin_memory if CUDA is not available
41
+
42
+ Returns
43
+ -------
44
+ DataLoader
45
+ PyTorch DataLoader
46
+ """
47
+ if auto_pin_memory and not torch.cuda.is_available():
48
+ pin_memory = False
49
+
50
+ return DataLoader(
51
+ dataset,
52
+ batch_size=batch_size,
53
+ shuffle=shuffle,
54
+ num_workers=num_workers,
55
+ pin_memory=pin_memory,
56
+ drop_last=False
57
+ )
58
+
59
+
17
60
  def create_dfm_dataloader(
18
61
  dataset: DFMDataset,
19
62
  batch_size: Optional[int] = None,
@@ -44,15 +87,15 @@ def create_dfm_dataloader(
44
87
  Configured DataLoader for DFM
45
88
  """
46
89
  if batch_size is None:
47
- batch_size = len(dataset) # Typically 1 for full sequence
90
+ batch_size = len(dataset)
48
91
 
49
- return DataLoader(
92
+ return _create_dataloader(
50
93
  dataset,
51
94
  batch_size=batch_size,
52
- shuffle=False, # No shuffle for full sequence
95
+ shuffle=False,
53
96
  num_workers=num_workers,
54
97
  pin_memory=pin_memory,
55
- drop_last=False
98
+ auto_pin_memory=False
56
99
  )
57
100
 
58
101
 
@@ -86,12 +129,12 @@ def create_ddfm_dataloader(
86
129
  DataLoader
87
130
  Configured DataLoader for DDFM
88
131
  """
89
- return DataLoader(
132
+ return _create_dataloader(
90
133
  dataset,
91
134
  batch_size=batch_size,
92
135
  shuffle=shuffle,
93
136
  num_workers=num_workers,
94
- pin_memory=pin_memory if torch.cuda.is_available() else False,
95
- drop_last=False
137
+ pin_memory=pin_memory,
138
+ auto_pin_memory=True
96
139
  )
97
140
 
dfm_python/data/utils.py CHANGED
@@ -11,14 +11,14 @@ from typing import List, Optional, Tuple, Union
11
11
  from datetime import datetime
12
12
 
13
13
  import numpy as np
14
- import polars as pl
14
+ import pandas as pd
15
15
 
16
16
  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
20
  safe_get_attr,
21
- get_frequencies_from_config,
21
+ get_frequencies,
22
22
  get_series_ids,
23
23
  )
24
24
  from ..config.utils import get_periods_per_year
@@ -63,9 +63,8 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, TimeIndex, List[s
63
63
 
64
64
  # Read data file
65
65
  try:
66
- # Use infer_schema_length=None to infer all rows, and try_parse_dates=False
67
- # to avoid parsing issues with mixed numeric/string columns
68
- df = pl.read_csv(datafile, infer_schema_length=None, try_parse_dates=False)
66
+ # Use pandas read_csv with low_memory=False to infer all columns properly
67
+ df = pd.read_csv(datafile, low_memory=False)
69
68
  except Exception as e:
70
69
  raise ValueError(f"Failed to read data file {datafile}: {e}")
71
70
 
@@ -87,7 +86,7 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, TimeIndex, List[s
87
86
  # Skip this check if first column is integer (likely date_id) - treat as standard format
88
87
  if not is_date_first:
89
88
  first_col_type = df[first_col].dtype
90
- is_integer_id = first_col_type in [pl.Int64, pl.Int32, pl.Int16, pl.Int8, pl.UInt64, pl.UInt32, pl.UInt16, pl.UInt8]
89
+ is_integer_id = pd.api.types.is_integer_dtype(first_col_type)
91
90
 
92
91
  # Only check for long format if first column is not an integer ID
93
92
  if not is_integer_id:
@@ -95,16 +94,16 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, TimeIndex, List[s
95
94
  date_cols = []
96
95
  for col in df.columns:
97
96
  try:
98
- parse_timestamp(str(df[col][0]))
97
+ parse_timestamp(str(df[col].iloc[0]))
99
98
  date_cols.append(col)
100
- except (ValueError, TypeError):
99
+ except (ValueError, TypeError, IndexError):
101
100
  pass
102
101
 
103
102
  if len(date_cols) > 0:
104
103
  # Long format: transpose and use first date column as index
105
104
  first_date_col = date_cols[0]
106
- date_col_idx = df.columns.index(first_date_col)
107
- date_cols_all = df.columns[date_col_idx:]
105
+ date_col_idx = df.columns.get_loc(first_date_col)
106
+ date_cols_all = df.columns[date_col_idx:].tolist()
108
107
 
109
108
  # Extract dates from column names (they are dates in long format)
110
109
  dates = []
@@ -117,10 +116,10 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, TimeIndex, List[s
117
116
 
118
117
  # Transpose: rows become series, columns become time
119
118
  # Select date columns and transpose
120
- date_data = df.select(date_cols_all)
119
+ date_data = df[date_cols_all]
121
120
  Z = date_data.to_numpy().T.astype(float)
122
121
  Time = TimeIndex(dates)
123
- mnemonics = df[first_col].to_list() if first_col in df.columns else [f"series_{i}" for i in range(len(df))]
122
+ mnemonics = df[first_col].tolist() if first_col in df.columns else [f"series_{i}" for i in range(len(df))]
124
123
 
125
124
  return Z, Time, mnemonics
126
125
 
@@ -129,31 +128,30 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, TimeIndex, List[s
129
128
  try:
130
129
  # Check if first column is integer (date_id format)
131
130
  first_col_type = df[first_col].dtype
132
- if first_col_type in [pl.Int64, pl.Int32, pl.Int16, pl.Int8, pl.UInt64, pl.UInt32, pl.UInt16, pl.UInt8]:
131
+ if pd.api.types.is_integer_dtype(first_col_type):
133
132
  # Integer date_id: use as sequential index, generate synthetic dates
134
133
  n_periods = len(df)
135
134
  from datetime import timedelta
136
135
  # Start from a default date and increment by day
137
136
  start_date = datetime(2000, 1, 1)
138
- dates = [start_date + timedelta(days=int(df[first_col][i])) for i in range(n_periods)]
137
+ dates = [start_date + timedelta(days=int(df[first_col].iloc[i])) for i in range(n_periods)]
139
138
  Time = TimeIndex(dates)
140
139
  else:
141
- # Try to parse as date
142
- time_series = df[first_col].cast(pl.Utf8).str.strptime(pl.Datetime, "%Y-%m-%d", strict=False)
143
- # If that fails, try other formats
144
- if time_series.null_count() > 0:
145
- # Try parsing as string first
146
- time_series = df[first_col].str.strptime(pl.Datetime, strict=False)
140
+ # Try to parse as date using pandas
141
+ time_series = pd.to_datetime(df[first_col], errors='coerce', format='%Y-%m-%d')
142
+ # If that fails, try without format
143
+ if time_series.isna().any():
144
+ time_series = pd.to_datetime(df[first_col], errors='coerce')
147
145
  Time = TimeIndex(time_series)
148
146
  except (ValueError, TypeError) as e:
149
147
  # If date parsing fails, treat first column as integer date_id
150
148
  try:
151
149
  first_col_type = df[first_col].dtype
152
- if first_col_type in [pl.Int64, pl.Int32, pl.Int16, pl.Int8, pl.UInt64, pl.UInt32, pl.UInt16, pl.UInt8]:
150
+ if pd.api.types.is_integer_dtype(first_col_type):
153
151
  n_periods = len(df)
154
152
  from datetime import timedelta
155
153
  start_date = datetime(2000, 1, 1)
156
- dates = [start_date + timedelta(days=int(df[first_col][i])) for i in range(n_periods)]
154
+ dates = [start_date + timedelta(days=int(df[first_col].iloc[i])) for i in range(n_periods)]
157
155
  Time = TimeIndex(dates)
158
156
  else:
159
157
  raise ValueError(f"Failed to parse date column '{first_col}': {e}")
@@ -162,7 +160,7 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, TimeIndex, List[s
162
160
 
163
161
  # Extract series data (all columns except first)
164
162
  series_cols = [col for col in df.columns if col != first_col]
165
- series_data = df.select(series_cols)
163
+ series_data = df[series_cols]
166
164
  Z = series_data.to_numpy().astype(float)
167
165
  mnemonics = series_cols
168
166
 
@@ -240,8 +238,8 @@ def load_data(
240
238
  if isinstance(sample_start, str):
241
239
  sample_start = parse_timestamp(sample_start)
242
240
  mask = Time >= sample_start
243
- if isinstance(mask, pl.Series):
244
- mask = mask.to_numpy()
241
+ if isinstance(mask, pd.Series):
242
+ mask = mask.values
245
243
  elif not isinstance(mask, np.ndarray):
246
244
  # Convert to numpy array if needed
247
245
  mask = np.asarray(mask)
@@ -254,8 +252,8 @@ def load_data(
254
252
  if isinstance(sample_end, str):
255
253
  sample_end = parse_timestamp(sample_end)
256
254
  mask = Time <= sample_end
257
- if isinstance(mask, pl.Series):
258
- mask = mask.to_numpy()
255
+ if isinstance(mask, pd.Series):
256
+ mask = mask.values
259
257
  elif not isinstance(mask, np.ndarray):
260
258
  # Convert to numpy array if needed
261
259
  mask = np.asarray(mask)
@@ -272,7 +270,7 @@ def load_data(
272
270
  clock = safe_get_attr(config, 'clock', 'm')
273
271
  clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, 3)
274
272
 
275
- frequencies = get_frequencies_from_config(config)
273
+ frequencies = get_frequencies(config)
276
274
  series_ids = get_series_ids(config)
277
275
  warnings_list = []
278
276
 
@@ -314,8 +312,8 @@ def load_data(
314
312
  extreme_missing_series = []
315
313
  for i, ratio in enumerate(missing_ratios):
316
314
  if ratio > 0.9:
317
- from ..utils.helpers import get_series_id_by_index
318
- series_id = get_series_id_by_index(config, i)
315
+ from ..utils.helpers import get_series_id
316
+ series_id = get_series_id(config, i)
319
317
  extreme_missing_series.append((series_id, ratio))
320
318
 
321
319
  if len(extreme_missing_series) > 0:
dfm_python/encoder/vae.py CHANGED
@@ -173,7 +173,7 @@ if _has_torch:
173
173
 
174
174
  Note: VAE encoders are typically trained via autoencoder training
175
175
  (encoder + decoder) before being used for factor extraction.
176
- This method is a placeholder for the BaseEncoder interface.
176
+ This method satisfies the BaseEncoder interface but does nothing.
177
177
 
178
178
  Parameters
179
179
  ----------
@@ -476,7 +476,8 @@ def convert_decoder_to_numpy(
476
476
  has_bias : bool
477
477
  Whether the decoder has a bias term
478
478
  factor_order : int
479
- Lag order for common factors (1 for VAR(1), 2 for VAR(2))
479
+ Lag order for common factors. Only VAR(1) and VAR(2) are supported.
480
+ Higher orders will raise NotImplementedError.
480
481
 
481
482
  Returns
482
483
  -------