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.
- dfm_python/__init__.py +9 -11
- dfm_python/config/__init__.py +8 -13
- dfm_python/config/adapter.py +12 -210
- dfm_python/config/results.py +18 -10
- dfm_python/config/schema.py +31 -66
- dfm_python/config/utils.py +13 -13
- dfm_python/data/__init__.py +5 -29
- dfm_python/data/dataloader.py +51 -8
- dfm_python/data/utils.py +28 -30
- dfm_python/encoder/vae.py +3 -2
- dfm_python/lightning/__init__.py +27 -14
- dfm_python/lightning/data_module.py +348 -220
- dfm_python/lightning/scaling.py +432 -0
- dfm_python/models/__init__.py +3 -3
- dfm_python/models/base.py +335 -6
- dfm_python/models/ddfm.py +1137 -321
- dfm_python/models/dfm.py +125 -586
- dfm_python/models/estimation.py +13 -14
- dfm_python/nowcast/__init__.py +17 -8
- dfm_python/nowcast/dataview.py +7 -7
- dfm_python/nowcast/helpers.py +6 -6
- dfm_python/nowcast/nowcast.py +274 -26
- dfm_python/nowcast/splitters.py +431 -0
- dfm_python/nowcast/transformers.py +330 -0
- dfm_python/nowcast/utils.py +11 -11
- dfm_python/ssm/kalman.py +13 -27
- dfm_python/ssm/utils.py +3 -1
- dfm_python/trainer/__init__.py +22 -22
- dfm_python/trainer/ddfm.py +7 -7
- dfm_python/trainer/dfm.py +7 -7
- dfm_python/utils/__init__.py +15 -15
- dfm_python/utils/data.py +17 -20
- dfm_python/utils/diagnostics.py +28 -23
- dfm_python/utils/helpers.py +7 -7
- dfm_python/utils/statespace.py +41 -71
- dfm_python/utils/time.py +148 -249
- {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/METADATA +24 -64
- dfm_python-0.4.3.dist-info/RECORD +52 -0
- dfm_python/data/transformation.py +0 -313
- dfm_python/lightning/ddfm_module.py +0 -580
- dfm_python/lightning/dfm_module.py +0 -466
- dfm_python-0.4.2.dist-info/RECORD +0 -52
- {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/WHEEL +0 -0
- {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/licenses/LICENSE +0 -0
dfm_python/config/schema.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
1037
|
-
"""Create
|
|
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
|
|
1023
|
+
return cls.from_dict(cfg)
|
|
1047
1024
|
|
|
1048
1025
|
DFMConfig.from_dict = classmethod(_dfm_from_dict)
|
|
1049
|
-
DFMConfig.from_hydra = classmethod(
|
|
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.
|
|
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(
|
|
1086
|
+
DDFMConfig.from_hydra = classmethod(_from_hydra)
|
|
1122
1087
|
|
|
1123
1088
|
|
dfm_python/config/utils.py
CHANGED
|
@@ -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,
|
|
7
|
-
4. Aggregation structure:
|
|
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
|
|
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
|
-
>>>
|
|
319
|
+
>>> get_tent_weights('q', 'm')
|
|
320
320
|
array([1, 2, 3, 2, 1]) # Quarterly -> monthly
|
|
321
321
|
|
|
322
|
-
>>>
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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)
|
dfm_python/data/__init__.py
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
|
-
"""Data loading
|
|
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
|
-
|
|
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
|
|
dfm_python/data/dataloader.py
CHANGED
|
@@ -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)
|
|
90
|
+
batch_size = len(dataset)
|
|
48
91
|
|
|
49
|
-
return
|
|
92
|
+
return _create_dataloader(
|
|
50
93
|
dataset,
|
|
51
94
|
batch_size=batch_size,
|
|
52
|
-
shuffle=False,
|
|
95
|
+
shuffle=False,
|
|
53
96
|
num_workers=num_workers,
|
|
54
97
|
pin_memory=pin_memory,
|
|
55
|
-
|
|
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
|
|
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
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
67
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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].
|
|
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
|
|
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]
|
|
143
|
-
# If that fails, try
|
|
144
|
-
if time_series.
|
|
145
|
-
|
|
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
|
|
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
|
|
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,
|
|
244
|
-
mask = mask.
|
|
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,
|
|
258
|
-
mask = mask.
|
|
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 =
|
|
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
|
|
318
|
-
series_id =
|
|
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
|
|
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
|
|
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
|
-------
|