dfm-python 0.5.57__tar.gz → 0.5.58__tar.gz
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-0.5.57 → dfm_python-0.5.58}/PKG-INFO +1 -1
- {dfm_python-0.5.57 → dfm_python-0.5.58}/pyproject.toml +1 -1
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/__init__.py +0 -1
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/__init__.py +1 -3
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/adapter.py +5 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/constants.py +0 -17
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/schema/block.py +2 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/schema/model.py +20 -3
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/dataset/ddfm_dataset.py +73 -7
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/dataset/dfm_dataset.py +71 -13
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/encoder/pca.py +59 -9
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/functional/dfm_block.py +20 -5
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/functional/em.py +69 -47
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/models/ddfm.py +157 -28
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/models/dfm.py +175 -30
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/numeric/builder.py +1 -5
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/numeric/estimator.py +71 -2
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/numeric/stability.py +44 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/numeric/tent.py +59 -38
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/numeric/validator.py +85 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/ssm/kalman.py +144 -80
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/__init__.py +2 -5
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/helper.py +64 -8
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/misc.py +62 -9
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/schema/test_model.py +7 -1
- dfm_python-0.5.58/src/test/config/test_tent_weights_config.py +240 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/dataset/test_process.py +2 -2
- dfm_python-0.5.58/src/test/functional/test_dfm_block.py +140 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/functional/test_em.py +6 -6
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/models/test_ddfm.py +81 -6
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/models/test_dfm.py +6 -6
- dfm_python-0.5.58/src/test/models/test_dfm_fixes.py +261 -0
- dfm_python-0.5.58/src/test/models/test_dfm_result_caching.py +309 -0
- dfm_python-0.5.58/src/test/models/test_dfm_save_load.py +261 -0
- dfm_python-0.5.58/src/test/models/test_dfm_save_load_integration.py +136 -0
- dfm_python-0.5.58/src/test/models/test_dfm_save_load_mock.py +340 -0
- dfm_python-0.5.58/src/test/models/test_dfm_training_pipeline_save.py +246 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/numeric/test_builder.py +3 -3
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/numeric/test_tent.py +28 -50
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/ssm/test_kalman.py +40 -0
- dfm_python-0.5.58/src/test/ssm/test_kalman_progress.py +161 -0
- dfm_python-0.5.58/src/test/ssm/test_progress_bar_exact_scenario.py +226 -0
- dfm_python-0.5.58/src/test/ssm/test_progress_bar_spam_fix.py +297 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/test_helper.py +21 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/test_misc.py +51 -1
- dfm_python-0.5.57/src/test/functional/test_dfm_block.py +0 -219
- {dfm_python-0.5.57 → dfm_python-0.5.58}/.git +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/.gitignore +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/.python-version +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/MANIFEST.in +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/README.md +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/config/ddfm_exchange.yaml +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/config/ddfm_finance.yaml +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/config/ddfm_macro.yaml +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/config/dfm_finance.yaml +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/config/dfm_macro.yaml +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/data/exchange_rate.csv +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/data/finance.csv +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/data/macro.csv +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/schema/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/schema/params.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/schema/results.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/config/types.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/dataset/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/dataset/time.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/decoder/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/decoder/base.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/decoder/linear.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/decoder/mlp.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/encoder/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/encoder/base.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/encoder/simple_autoencoder.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/functional/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/logger/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/logger/inference_logger.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/logger/logger.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/logger/train_logger.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/models/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/models/base.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/numeric/statistic.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/ssm/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/checkpoint.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/errors.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/loss.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/metric.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/dfm_python/utils/validation.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/schema/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/schema/test_block.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/schema/test_results.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/test_adapter.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/test_constants.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/config/test_types.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/conftest.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/dataset/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/dataset/test_dataloader.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/dataset/test_dataset.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/decoder/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/decoder/test_linear.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/decoder/test_mlp.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/encoder/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/encoder/test_base.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/encoder/test_pca.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/encoder/test_simple_encoder.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/functional/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/logger/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/logger/test_inference_logger.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/logger/test_logger.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/logger/test_train_logger.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/models/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/models/test_base.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/models/test_ddfm_batchnorm_placement.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/models/test_tensorflow_batchnorm_comparison.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/numeric/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/numeric/test_estimator.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/numeric/test_stability.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/numeric/test_validator.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/ssm/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/ssm/test_arma.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/ssm/test_companion.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/ssm/test_structural.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/__init__.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/test_common.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/test_errors.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/test_metric.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/test_tensor_utils.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/src/test/utils/test_validation.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/tutorial/tutorial_exchange_ddfm.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/tutorial/tutorial_finance_ddfm.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/tutorial/tutorial_finance_dfm.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/tutorial/tutorial_illness_ddfm.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/tutorial/tutorial_macro_ddfm.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/tutorial/tutorial_macro_dfm.py +0 -0
- {dfm_python-0.5.57 → dfm_python-0.5.58}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.58
|
|
4
4
|
Summary: Dynamic Factor Model (DFM) estimation and forecasting in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/dfm-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/yourusername/dfm-python#readme
|
|
@@ -86,9 +86,8 @@ except ImportError:
|
|
|
86
86
|
compute_idio_lengths = None
|
|
87
87
|
|
|
88
88
|
try:
|
|
89
|
-
from ..numeric.tent import
|
|
89
|
+
from ..numeric.tent import get_agg_structure, group_by_freq
|
|
90
90
|
except ImportError:
|
|
91
|
-
get_tent_weights = None
|
|
92
91
|
get_agg_structure = None
|
|
93
92
|
group_by_freq = None
|
|
94
93
|
from .constants import FREQUENCY_HIERARCHY, PERIODS_PER_YEAR
|
|
@@ -119,7 +118,6 @@ __all__ = [
|
|
|
119
118
|
'PERIODS_PER_YEAR',
|
|
120
119
|
'get_periods_per_year',
|
|
121
120
|
'compute_idio_lengths',
|
|
122
|
-
'get_tent_weights',
|
|
123
121
|
'get_agg_structure',
|
|
124
122
|
'group_by_freq',
|
|
125
123
|
# Type definitions (from types.py)
|
|
@@ -552,6 +552,11 @@ def detect_config_type(data: Dict[str, Any]) -> str:
|
|
|
552
552
|
if model_type == MODEL_TYPE_DFM:
|
|
553
553
|
return MODEL_TYPE_DFM
|
|
554
554
|
|
|
555
|
+
# Priority check: blocks parameter indicates DFM (highest priority)
|
|
556
|
+
# DFM is the only model type that uses blocks structure
|
|
557
|
+
if 'blocks' in data and data.get('blocks'):
|
|
558
|
+
return MODEL_TYPE_DFM
|
|
559
|
+
|
|
555
560
|
# Check for model-specific parameters
|
|
556
561
|
keys = set(data.keys())
|
|
557
562
|
if keys & _KDFM_PARAMS:
|
|
@@ -387,22 +387,6 @@ FREQUENCY_HIERARCHY: Dict[str, int] = {
|
|
|
387
387
|
# For frequency gaps larger than this, the missing data approach is used instead
|
|
388
388
|
MAX_TENT_SIZE: int = 12
|
|
389
389
|
|
|
390
|
-
# Deterministic tent weights lookup for supported frequency pairs
|
|
391
|
-
# Format: (slower_freq, faster_freq) -> tent_weights_array
|
|
392
|
-
# These weights define how slower-frequency series aggregate clock-frequency factors
|
|
393
|
-
TENT_WEIGHTS_LOOKUP: Dict[Tuple[str, str], np.ndarray] = {
|
|
394
|
-
('q', 'm'): np.array([1, 2, 3, 2, 1]), # 5 periods: quarterly -> monthly
|
|
395
|
-
('sa', 'm'): np.array([1, 2, 3, 4, 3, 2, 1]), # 7 periods: semi-annual -> monthly
|
|
396
|
-
('a', 'm'): np.array([1, 2, 3, 4, 5, 4, 3, 2, 1]), # 9 periods: annual -> monthly
|
|
397
|
-
('m', 'w'): np.array([1, 2, 3, 2, 1]), # 5 periods: monthly -> weekly
|
|
398
|
-
('q', 'w'): np.array([1, 2, 3, 4, 5, 4, 3, 2, 1]), # 9 periods: quarterly -> weekly
|
|
399
|
-
('sa', 'w'): np.array([1, 2, 3, 4, 3, 2, 1]), # 7 periods: semi-annual -> weekly
|
|
400
|
-
('a', 'w'): np.array([1, 2, 3, 4, 5, 4, 3, 2, 1]), # 9 periods: annual -> weekly
|
|
401
|
-
('sa', 'q'): np.array([1, 2, 1]), # 3 periods: semi-annual -> quarterly
|
|
402
|
-
('a', 'q'): np.array([1, 2, 3, 2, 1]), # 5 periods: annual -> quarterly
|
|
403
|
-
('a', 'sa'): np.array([1, 2, 1]), # 3 periods: annual -> semi-annual
|
|
404
|
-
}
|
|
405
|
-
|
|
406
390
|
# ============================================================================
|
|
407
391
|
# Export all constants
|
|
408
392
|
# ============================================================================
|
|
@@ -582,6 +566,5 @@ __all__ = [
|
|
|
582
566
|
# Frequency hierarchy and tent kernels
|
|
583
567
|
'FREQUENCY_HIERARCHY',
|
|
584
568
|
'MAX_TENT_SIZE',
|
|
585
|
-
'TENT_WEIGHTS_LOOKUP',
|
|
586
569
|
]
|
|
587
570
|
|
|
@@ -90,6 +90,7 @@ class BlockStructure:
|
|
|
90
90
|
_cached_n_idio_M: Optional[int] = field(default=None, init=False, repr=False)
|
|
91
91
|
_cached_c_idio_indicator: Optional[np.ndarray] = field(default=None, init=False, repr=False)
|
|
92
92
|
_cached_rp1: Optional[int] = field(default=None, init=False, repr=False)
|
|
93
|
+
_cached_N: Optional[int] = field(default=None, init=False, repr=False) # N value used for caching
|
|
93
94
|
|
|
94
95
|
def is_valid(self) -> bool:
|
|
95
96
|
"""Check if block structure is valid (all required fields are not None)."""
|
|
@@ -119,4 +120,5 @@ class BlockStructure:
|
|
|
119
120
|
self._cached_n_idio_M = None
|
|
120
121
|
self._cached_c_idio_indicator = None
|
|
121
122
|
self._cached_rp1 = None
|
|
123
|
+
self._cached_N = None
|
|
122
124
|
|
|
@@ -286,12 +286,19 @@ class DFMConfig(BaseModelConfig):
|
|
|
286
286
|
# Damped Updates: If provided (not None), automatically enables damping and always warns
|
|
287
287
|
damping_factor: Optional[float] = None # Damping factor (0.8 = 80% new, 20% old). If None, no damping. If provided, damping enabled and warnings always shown.
|
|
288
288
|
|
|
289
|
-
# Idiosyncratic Component Parameters
|
|
289
|
+
# Idiosyncratic Component Parameters
|
|
290
290
|
idio_rho0: float = DEFAULT_IDIO_RHO0 # Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
291
291
|
idio_min_var: float = MIN_DIAGONAL_VARIANCE # Minimum variance for idiosyncratic innovation covariance (defaults to MIN_DIAGONAL_VARIANCE)
|
|
292
|
-
# Note: augment_idio and augment_idio_slow are auto-detected
|
|
292
|
+
# Note: augment_idio and augment_idio_slow are INTERNALLY auto-detected (not configurable)
|
|
293
293
|
# - If all series use clock frequency: augment_idio=False, augment_idio_slow=False
|
|
294
|
-
# - If mixed frequencies detected: augment_idio=True, augment_idio_slow=True (
|
|
294
|
+
# - If mixed frequencies detected: augment_idio=True, augment_idio_slow=True (required for tent kernel)
|
|
295
|
+
# These are properties, not config fields - automatically determined from frequency configuration
|
|
296
|
+
|
|
297
|
+
# Kalman Filter Type
|
|
298
|
+
use_cholesky_filter: bool = False # Use CholeskyKalmanFilter for better numerical stability (default: False)
|
|
299
|
+
|
|
300
|
+
# Tent Kernel Weights (for mixed-frequency aggregation)
|
|
301
|
+
tent_weights: Optional[Union[Dict[str, List[float]], Dict[str, np.ndarray]]] = None # Required for mixed-frequency data: tent weights for frequency pairs. Format: {'freq_pair': [weights]} or {'freq': [weights]}. Example: {'m:w': [1, 2, 1]} or {'m': [1, 2, 1]}. Must be specified in config for all slower-frequency pairs.
|
|
295
302
|
|
|
296
303
|
def __post_init__(self):
|
|
297
304
|
"""Validate blocks structure and derive block properties."""
|
|
@@ -446,6 +453,8 @@ class DFMConfig(BaseModelConfig):
|
|
|
446
453
|
'data_clip': data.get('data_clip', None),
|
|
447
454
|
'regularization': data.get('regularization', None),
|
|
448
455
|
'damping_factor': data.get('damping_factor', None),
|
|
456
|
+
'use_cholesky_filter': data.get('use_cholesky_filter', False),
|
|
457
|
+
'tent_weights': data.get('tent_weights', None),
|
|
449
458
|
}
|
|
450
459
|
|
|
451
460
|
result = base_params.copy()
|
|
@@ -524,6 +533,11 @@ class DDFMConfig(BaseModelConfig):
|
|
|
524
533
|
seed: Optional[int] = None # Random seed for reproducibility
|
|
525
534
|
lags_input: int = 0 # Number of lags of inputs on encoder (default 0, matching original TensorFlow DDFM)
|
|
526
535
|
|
|
536
|
+
# Interpolation parameters (for handling missing values)
|
|
537
|
+
interpolation_method: str = 'linear' # Interpolation method: 'linear', 'spline', 'cubic', etc. (default: 'linear' for stability)
|
|
538
|
+
interpolation_limit: Optional[int] = 10 # Maximum consecutive NaNs to interpolate (default: 10, prevents extreme extrapolation)
|
|
539
|
+
interpolation_limit_direction: str = 'both' # Direction to fill: 'forward', 'backward', or 'both' (default: 'both')
|
|
540
|
+
|
|
527
541
|
|
|
528
542
|
# ========================================================================
|
|
529
543
|
# Factory Methods
|
|
@@ -550,6 +564,9 @@ class DDFMConfig(BaseModelConfig):
|
|
|
550
564
|
'disp': DEFAULT_DISP,
|
|
551
565
|
'seed': None,
|
|
552
566
|
'lags_input': 0, # Number of lags (default 0, matching original TensorFlow)
|
|
567
|
+
'interpolation_method': 'linear', # Interpolation method (default: 'linear' for stability)
|
|
568
|
+
'interpolation_limit': 10, # Maximum consecutive NaNs to interpolate (default: 10)
|
|
569
|
+
'interpolation_limit_direction': 'both', # Direction to fill (default: 'both')
|
|
553
570
|
})
|
|
554
571
|
# Map 'epochs' from config to 'n_mc_samples' for clarity (backward compatibility)
|
|
555
572
|
# Always remove 'epochs' if present (even if n_mc_samples is also present)
|
|
@@ -21,7 +21,7 @@ from ..config.constants import DEFAULT_TORCH_DTYPE
|
|
|
21
21
|
class DDFMDataset(Dataset):
|
|
22
22
|
"""Dataset for DDFM training.
|
|
23
23
|
|
|
24
|
-
Scales target series if scaler provided. Feature series
|
|
24
|
+
Scales target series if scaler provided. Feature series can also be scaled if provided.
|
|
25
25
|
|
|
26
26
|
Parameters
|
|
27
27
|
----------
|
|
@@ -33,6 +33,8 @@ class DDFMDataset(Dataset):
|
|
|
33
33
|
Target series column names to scale. If None or empty, all columns will be used as targets.
|
|
34
34
|
target_scaler : StandardScaler | RobustScaler | MinMaxScaler, optional
|
|
35
35
|
Scaler instance to scale target series. If None, no scaling.
|
|
36
|
+
feature_scaler : StandardScaler | RobustScaler | MinMaxScaler, optional
|
|
37
|
+
Scaler instance to scale feature series. If None, no scaling.
|
|
36
38
|
"""
|
|
37
39
|
|
|
38
40
|
def __init__(
|
|
@@ -41,6 +43,7 @@ class DDFMDataset(Dataset):
|
|
|
41
43
|
time_idx: str,
|
|
42
44
|
target_series: Optional[List[str]] = None,
|
|
43
45
|
target_scaler: Optional[Union[StandardScaler, RobustScaler, MinMaxScaler]] = None,
|
|
46
|
+
feature_scaler: Optional[Union[StandardScaler, RobustScaler, MinMaxScaler]] = None,
|
|
44
47
|
):
|
|
45
48
|
if _has_polars and isinstance(data, pl.DataFrame):
|
|
46
49
|
data = data.to_pandas()
|
|
@@ -70,10 +73,31 @@ class DDFMDataset(Dataset):
|
|
|
70
73
|
self.target_series = target_series_list
|
|
71
74
|
self.data_original = data.copy()
|
|
72
75
|
self.target_scaler = target_scaler
|
|
76
|
+
self.feature_scaler = feature_scaler
|
|
73
77
|
|
|
74
78
|
y = data[target_series_list]
|
|
75
79
|
X = data.drop(columns=target_series_list)
|
|
76
80
|
|
|
81
|
+
# Optionally scale feature series to avoid scale dominance in encoder input
|
|
82
|
+
if feature_scaler is not None and not X.empty:
|
|
83
|
+
x_mean = X.mean().abs().max()
|
|
84
|
+
x_std = (X.std() - 1.0).abs().max()
|
|
85
|
+
x_is_already_scaled = x_mean < 0.1 and x_std < 0.1
|
|
86
|
+
x_scaler_is_fitted = (
|
|
87
|
+
hasattr(feature_scaler, 'mean_') and
|
|
88
|
+
feature_scaler.mean_ is not None and
|
|
89
|
+
hasattr(feature_scaler, 'scale_') and
|
|
90
|
+
feature_scaler.scale_ is not None
|
|
91
|
+
)
|
|
92
|
+
if not x_is_already_scaled:
|
|
93
|
+
if not x_scaler_is_fitted:
|
|
94
|
+
self.feature_scaler.fit(X.values)
|
|
95
|
+
X_scaled = self.feature_scaler.transform(X.values)
|
|
96
|
+
X = pd.DataFrame(X_scaled, index=X.index, columns=X.columns)
|
|
97
|
+
else:
|
|
98
|
+
if not x_scaler_is_fitted:
|
|
99
|
+
self.feature_scaler.fit(X.values)
|
|
100
|
+
|
|
77
101
|
# Check if data is already standardized (mean≈0, std≈1) to avoid double scaling
|
|
78
102
|
# If data is already scaled, we still need target_scaler for inverse transformation
|
|
79
103
|
# but we should NOT scale again
|
|
@@ -83,17 +107,58 @@ class DDFMDataset(Dataset):
|
|
|
83
107
|
y_std = (y.std() - 1.0).abs().max()
|
|
84
108
|
is_already_scaled = y_mean < 0.1 and y_std < 0.1
|
|
85
109
|
|
|
110
|
+
# Check if scaler is already fitted
|
|
111
|
+
scaler_is_fitted = (
|
|
112
|
+
hasattr(target_scaler, 'mean_') and
|
|
113
|
+
target_scaler.mean_ is not None and
|
|
114
|
+
hasattr(target_scaler, 'scale_') and
|
|
115
|
+
target_scaler.scale_ is not None
|
|
116
|
+
)
|
|
117
|
+
|
|
86
118
|
if is_already_scaled:
|
|
87
119
|
# Data is already standardized - don't scale again
|
|
88
|
-
#
|
|
120
|
+
# IMPORTANT: If scaler is already fitted, do NOT refit it on already-scaled data
|
|
89
121
|
# The scaler should have been fitted on original unscaled data by the caller
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
# Refitting on already-scaled data would break inverse transformation
|
|
123
|
+
if not scaler_is_fitted:
|
|
124
|
+
# Only fit if scaler is not already fitted
|
|
125
|
+
# This assumes caller passed unscaled data (edge case)
|
|
126
|
+
import warnings
|
|
127
|
+
warnings.warn(
|
|
128
|
+
"DDFMDataset: Data appears already scaled but scaler is not fitted. "
|
|
129
|
+
"Fitting scaler on already-scaled data may break inverse transformation. "
|
|
130
|
+
"Ensure scaler is fitted on original unscaled data before passing to DDFMDataset.",
|
|
131
|
+
UserWarning
|
|
132
|
+
)
|
|
92
133
|
self.target_scaler.fit(y.values)
|
|
93
134
|
# Don't transform - data is already scaled
|
|
94
135
|
else:
|
|
95
136
|
# Data is not standardized - apply scaling
|
|
96
|
-
|
|
137
|
+
# If scaler is already fitted, DO NOT refit (preserve training scaler).
|
|
138
|
+
if scaler_is_fitted:
|
|
139
|
+
# Check if scaler was fitted on data with similar scale
|
|
140
|
+
# If mean/scale are very different, warn user but keep scaler.
|
|
141
|
+
scaler_mean = np.abs(target_scaler.mean_)
|
|
142
|
+
scaler_scale = target_scaler.scale_
|
|
143
|
+
data_mean = np.abs(y.mean().values)
|
|
144
|
+
data_std = y.std().values
|
|
145
|
+
|
|
146
|
+
# Check for significant mismatch (more than 2x difference)
|
|
147
|
+
mean_mismatch = np.any((data_mean > 2 * scaler_mean) | (scaler_mean > 2 * data_mean))
|
|
148
|
+
scale_mismatch = np.any((data_std > 2 * scaler_scale) | (scaler_scale > 2 * data_std))
|
|
149
|
+
|
|
150
|
+
if mean_mismatch or scale_mismatch:
|
|
151
|
+
import warnings
|
|
152
|
+
warnings.warn(
|
|
153
|
+
"DDFMDataset: Scaler appears to be fitted on data with different scale. "
|
|
154
|
+
"Using existing scaler to preserve inverse transformation. "
|
|
155
|
+
"Ensure scaler consistency across train/test.",
|
|
156
|
+
UserWarning
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
# Scaler not fitted - fit on current data
|
|
160
|
+
self.target_scaler.fit(y.values)
|
|
161
|
+
|
|
97
162
|
y_scaled = self.target_scaler.transform(y.values)
|
|
98
163
|
y = pd.DataFrame(y_scaled, index=y.index, columns=y.columns)
|
|
99
164
|
|
|
@@ -184,13 +249,14 @@ class DDFMDataset(Dataset):
|
|
|
184
249
|
Returns
|
|
185
250
|
-------
|
|
186
251
|
DDFMDataset
|
|
187
|
-
New dataset with same time_idx, target_series, target_scaler.
|
|
252
|
+
New dataset with same time_idx, target_series, target_scaler, feature_scaler.
|
|
188
253
|
"""
|
|
189
254
|
return cls(
|
|
190
255
|
data=new_data,
|
|
191
256
|
time_idx=dataset.time_idx,
|
|
192
257
|
target_series=dataset.target_series,
|
|
193
|
-
target_scaler=dataset.target_scaler
|
|
258
|
+
target_scaler=dataset.target_scaler,
|
|
259
|
+
feature_scaler=getattr(dataset, 'feature_scaler', None)
|
|
194
260
|
)
|
|
195
261
|
|
|
196
262
|
def create_autoencoder_dataset(
|
|
@@ -14,10 +14,9 @@ if TYPE_CHECKING:
|
|
|
14
14
|
|
|
15
15
|
from ..logger import get_logger
|
|
16
16
|
from ..config import DFMConfig
|
|
17
|
-
from ..numeric.tent import
|
|
17
|
+
from ..numeric.tent import generate_R_mat
|
|
18
18
|
from ..config.constants import (
|
|
19
19
|
FREQUENCY_HIERARCHY,
|
|
20
|
-
TENT_WEIGHTS_LOOKUP,
|
|
21
20
|
DEFAULT_HIERARCHY_VALUE,
|
|
22
21
|
DEFAULT_CLOCK_FREQUENCY,
|
|
23
22
|
DEFAULT_NAN_METHOD,
|
|
@@ -368,26 +367,72 @@ class DFMDataset:
|
|
|
368
367
|
self.config._update_idio_flags_from_frequencies(frequencies_list)
|
|
369
368
|
clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, DEFAULT_HIERARCHY_VALUE)
|
|
370
369
|
|
|
371
|
-
# Validate frequency pairs
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
370
|
+
# Validate frequency pairs (tent weights must be in config)
|
|
371
|
+
config_tent_weights = getattr(self.config, 'tent_weights', None)
|
|
372
|
+
missing_pairs = []
|
|
373
|
+
for freq in frequencies_set:
|
|
374
|
+
if FREQUENCY_HIERARCHY.get(freq, DEFAULT_HIERARCHY_VALUE) > clock_hierarchy:
|
|
375
|
+
# Check if tent weights available in config
|
|
376
|
+
has_config_weights = False
|
|
377
|
+
if config_tent_weights is not None:
|
|
378
|
+
pair_key = f"{freq}:{clock}"
|
|
379
|
+
has_config_weights = (pair_key in config_tent_weights) or (freq in config_tent_weights)
|
|
380
|
+
|
|
381
|
+
if not has_config_weights:
|
|
382
|
+
missing_pairs.append((freq, clock))
|
|
383
|
+
|
|
376
384
|
if missing_pairs:
|
|
377
385
|
raise DataValidationError(
|
|
378
|
-
f"Mixed-frequency data detected but
|
|
379
|
-
f"
|
|
380
|
-
f"
|
|
386
|
+
f"Mixed-frequency data detected but no tent weights found in config for frequency pairs: {missing_pairs}. "
|
|
387
|
+
f"Please specify tent_weights in config (e.g., tent_weights: {{'m:w': [1, 2, 1]}}). "
|
|
388
|
+
f"Alternatively, ensure all series use clock frequency.",
|
|
381
389
|
details=f"Frequency pairs without tent weights: {missing_pairs}"
|
|
382
390
|
)
|
|
383
391
|
|
|
384
|
-
# Get aggregation structure
|
|
392
|
+
# Get aggregation structure (tent weights must be in config)
|
|
385
393
|
tent_weights_dict = {}
|
|
394
|
+
config_tent_weights = getattr(self.config, 'tent_weights', None)
|
|
395
|
+
|
|
396
|
+
if config_tent_weights is None:
|
|
397
|
+
slower_freqs = [freq for freq in frequencies_set
|
|
398
|
+
if FREQUENCY_HIERARCHY.get(freq, DEFAULT_HIERARCHY_VALUE) > clock_hierarchy]
|
|
399
|
+
if slower_freqs:
|
|
400
|
+
raise DataValidationError(
|
|
401
|
+
f"Mixed-frequency data detected (slower frequencies: {slower_freqs}) but no tent_weights "
|
|
402
|
+
f"specified in config. Please specify tent_weights in config for all frequency pairs.",
|
|
403
|
+
details=f"Slower frequencies: {slower_freqs}, clock: {clock}"
|
|
404
|
+
)
|
|
405
|
+
|
|
386
406
|
for freq in frequencies_set:
|
|
387
407
|
if FREQUENCY_HIERARCHY.get(freq, DEFAULT_HIERARCHY_VALUE) > clock_hierarchy:
|
|
388
|
-
tent_w =
|
|
408
|
+
tent_w = None
|
|
409
|
+
|
|
410
|
+
# Get tent weights from config (required)
|
|
411
|
+
if config_tent_weights is not None:
|
|
412
|
+
# Try both formats: 'freq:clock' (e.g., 'm:w') or just 'freq' (e.g., 'm')
|
|
413
|
+
pair_key = f"{freq}:{clock}"
|
|
414
|
+
if pair_key in config_tent_weights:
|
|
415
|
+
tent_w = config_tent_weights[pair_key]
|
|
416
|
+
elif freq in config_tent_weights:
|
|
417
|
+
tent_w = config_tent_weights[freq]
|
|
418
|
+
|
|
419
|
+
# Tent weights must be in config
|
|
420
|
+
if tent_w is None:
|
|
421
|
+
pair_key = f"{freq}:{clock}"
|
|
422
|
+
raise DataValidationError(
|
|
423
|
+
f"Tent weights not found in config for frequency pair ({freq}, {clock}). "
|
|
424
|
+
f"Please specify tent_weights in config, e.g., tent_weights: {{'{pair_key}': [1, 2, 1]}}",
|
|
425
|
+
details=f"Frequency pair: ({freq}, {clock})"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Convert to numpy array if found
|
|
389
429
|
if tent_w is not None:
|
|
390
|
-
|
|
430
|
+
if isinstance(tent_w, list):
|
|
431
|
+
tent_weights_dict[freq] = np.array(tent_w, dtype=np.float32)
|
|
432
|
+
elif isinstance(tent_w, np.ndarray):
|
|
433
|
+
tent_weights_dict[freq] = tent_w.astype(np.float32)
|
|
434
|
+
else:
|
|
435
|
+
tent_weights_dict[freq] = np.array(tent_w, dtype=np.float32)
|
|
391
436
|
|
|
392
437
|
# Validate: DFM supports only clock + one slower frequency
|
|
393
438
|
if len(tent_weights_dict) > 1:
|
|
@@ -398,6 +443,19 @@ class DFMDataset:
|
|
|
398
443
|
details=f"Slower frequencies detected: {slower_freqs}, clock: {clock}"
|
|
399
444
|
)
|
|
400
445
|
|
|
446
|
+
# Warn if slower frequency detected but no tent weights found
|
|
447
|
+
slower_freqs_detected = [freq for freq in frequencies_set
|
|
448
|
+
if FREQUENCY_HIERARCHY.get(freq, DEFAULT_HIERARCHY_VALUE) > clock_hierarchy]
|
|
449
|
+
if slower_freqs_detected and not tent_weights_dict:
|
|
450
|
+
missing_pairs = [(freq, clock) for freq in slower_freqs_detected]
|
|
451
|
+
from ..logger import get_logger
|
|
452
|
+
_logger = get_logger(__name__)
|
|
453
|
+
_logger.warning(
|
|
454
|
+
f"Mixed-frequency data detected but no tent weights found in config for pairs: {missing_pairs}. "
|
|
455
|
+
f"Tent weights must be specified in config. "
|
|
456
|
+
f"This may cause errors in model initialization."
|
|
457
|
+
)
|
|
458
|
+
|
|
401
459
|
# Generate constraint matrices if needed
|
|
402
460
|
R_mat = None
|
|
403
461
|
q = None
|
|
@@ -153,26 +153,76 @@ class PCAEncoder(BaseEncoder):
|
|
|
153
153
|
|
|
154
154
|
# Use SVD for efficient low-rank PCA (NumPy equivalent of torch.pca_lowrank)
|
|
155
155
|
try:
|
|
156
|
+
# Check for NaN/Inf values before SVD
|
|
157
|
+
if np.any(~np.isfinite(X_centered)):
|
|
158
|
+
raise ValueError("Input contains non-finite values")
|
|
159
|
+
|
|
160
|
+
# Check for zero variance columns (can cause SVD issues)
|
|
161
|
+
col_stds = np.std(X_centered, axis=0)
|
|
162
|
+
if np.any(col_stds < 1e-10):
|
|
163
|
+
_logger.warning(
|
|
164
|
+
f"PCA: Some columns have near-zero variance (min_std={col_stds.min():.2e}). "
|
|
165
|
+
"This may cause SVD convergence issues. Consider removing constant columns."
|
|
166
|
+
)
|
|
167
|
+
|
|
156
168
|
# Use truncated SVD for efficiency
|
|
157
|
-
|
|
169
|
+
# Use hermitian=False explicitly for clarity
|
|
170
|
+
U, S, Vt = np.linalg.svd(X_centered, full_matrices=False, hermitian=False)
|
|
171
|
+
|
|
172
|
+
# Validate SVD results
|
|
173
|
+
if not np.all(np.isfinite(S)):
|
|
174
|
+
raise ValueError("SVD produced non-finite singular values")
|
|
175
|
+
|
|
158
176
|
# Take top n_components
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
177
|
+
n_components_actual = min(self.n_components, len(S))
|
|
178
|
+
if n_components_actual < self.n_components:
|
|
179
|
+
_logger.warning(
|
|
180
|
+
f"PCA: Requested {self.n_components} components but only {len(S)} available. "
|
|
181
|
+
f"Using {n_components_actual} components."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
U = U[:, :n_components_actual]
|
|
185
|
+
S = S[:n_components_actual]
|
|
186
|
+
Vt = Vt[:n_components_actual, :]
|
|
162
187
|
|
|
163
188
|
# Vt contains the principal components (eigenvectors) as rows
|
|
164
189
|
# Transpose to get (N x n_components)
|
|
165
190
|
self.eigenvectors = Vt.T
|
|
166
191
|
# Convert singular values to eigenvalues
|
|
167
192
|
self.eigenvalues = S ** 2
|
|
193
|
+
|
|
168
194
|
except (ValueError, np.linalg.LinAlgError) as e:
|
|
169
195
|
# Fallback: compute covariance and use eigendecomposition
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
self.cov_matrix, self.n_components, block_idx=self.block_idx
|
|
196
|
+
error_msg = str(e) if str(e) else type(e).__name__
|
|
197
|
+
_logger.warning(
|
|
198
|
+
f"PCA SVD failed ({error_msg}), falling back to eigendecomposition. "
|
|
199
|
+
"This may be due to numerical instability or ill-conditioned data."
|
|
175
200
|
)
|
|
201
|
+
|
|
202
|
+
# Clean data for covariance computation
|
|
203
|
+
X_centered_clean = np.nan_to_num(X_centered, nan=0.0, posinf=0.0, neginf=0.0)
|
|
204
|
+
T = X_centered_clean.shape[0]
|
|
205
|
+
|
|
206
|
+
# Compute covariance with regularization for stability
|
|
207
|
+
cov_raw = (X_centered_clean.T @ X_centered_clean) / max(T - 1, 1)
|
|
208
|
+
|
|
209
|
+
# Add small regularization to diagonal for numerical stability
|
|
210
|
+
reg = 1e-8 * np.eye(cov_raw.shape[0], dtype=cov_raw.dtype)
|
|
211
|
+
self.cov_matrix = cov_raw + reg
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
self.eigenvalues, self.eigenvectors = compute_principal_components(
|
|
215
|
+
self.cov_matrix, self.n_components, block_idx=self.block_idx
|
|
216
|
+
)
|
|
217
|
+
except (ValueError, np.linalg.LinAlgError) as e2:
|
|
218
|
+
# If eigendecomposition also fails, raise with helpful message
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
f"PCA failed: Both SVD and eigendecomposition failed. "
|
|
221
|
+
f"Original error: {error_msg}, Eigendecomposition error: {str(e2)}. "
|
|
222
|
+
"This may indicate severely ill-conditioned data. "
|
|
223
|
+
"Consider: (1) Removing constant/near-constant columns, "
|
|
224
|
+
"(2) Checking for outliers, (3) Using fewer components."
|
|
225
|
+
) from e2
|
|
176
226
|
|
|
177
227
|
return self
|
|
178
228
|
|
|
@@ -8,8 +8,8 @@ This module provides functions for initializing DFM blocks, including:
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
|
-
from typing import Optional, Tuple, Dict, Any, List
|
|
12
|
-
from ..numeric.tent import
|
|
11
|
+
from typing import Optional, Tuple, Dict, Any, List, Callable
|
|
12
|
+
from ..numeric.tent import generate_tent_weights
|
|
13
13
|
from ..numeric.stability import create_scaled_identity
|
|
14
14
|
from ..logger import get_logger
|
|
15
15
|
from ..config.constants import (
|
|
@@ -176,7 +176,8 @@ def initialize_block_loadings(
|
|
|
176
176
|
N: int,
|
|
177
177
|
max_lag_size: int,
|
|
178
178
|
matrix_regularization: Optional[float] = None,
|
|
179
|
-
dtype: type = np.float32
|
|
179
|
+
dtype: type = np.float32,
|
|
180
|
+
impute_func: Optional[callable] = None
|
|
180
181
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
181
182
|
"""Initialize loadings for a block (clock frequency PCA + slower frequency constrained OLS).
|
|
182
183
|
|
|
@@ -210,6 +211,9 @@ def initialize_block_loadings(
|
|
|
210
211
|
Regularization for matrix operations
|
|
211
212
|
dtype : type, default np.float32
|
|
212
213
|
Data type
|
|
214
|
+
impute_func : Optional[Callable[[np.ndarray], np.ndarray]], optional
|
|
215
|
+
Function to impute NaN values for initialization fallback.
|
|
216
|
+
Only used when insufficient non-NaN observations for regression.
|
|
213
217
|
|
|
214
218
|
Returns
|
|
215
219
|
-------
|
|
@@ -310,10 +314,17 @@ def initialize_block_loadings(
|
|
|
310
314
|
series_data = data_with_nans[tent_kernel_size:, series_idx_int]
|
|
311
315
|
non_nan_mask = ~np.isnan(series_data)
|
|
312
316
|
|
|
313
|
-
# Use
|
|
317
|
+
# Use imputed data if insufficient non-NaN values (following FRBNY pattern)
|
|
314
318
|
min_required = slower_freq_factors.shape[1] + 2
|
|
315
319
|
if np.sum(non_nan_mask) < min_required:
|
|
316
|
-
|
|
320
|
+
# Try data_for_extraction first (may already be imputed)
|
|
321
|
+
series_data_attempt = data_for_extraction[tent_kernel_size:, series_idx_int]
|
|
322
|
+
if np.sum(~np.isnan(series_data_attempt)) < min_required and impute_func is not None:
|
|
323
|
+
# Fallback: use imputation function (only for initialization)
|
|
324
|
+
series_data = impute_func(series_data)
|
|
325
|
+
_logger.debug(f" Using imputed data for series {series_idx_int} (insufficient observations)")
|
|
326
|
+
else:
|
|
327
|
+
series_data = series_data_attempt
|
|
317
328
|
non_nan_mask = np.ones(len(series_data), dtype=bool)
|
|
318
329
|
|
|
319
330
|
slower_freq_factors_clean = slower_freq_factors[tent_kernel_size:][non_nan_mask, :]
|
|
@@ -341,6 +352,10 @@ def initialize_block_loadings(
|
|
|
341
352
|
regularization=reg,
|
|
342
353
|
dtype=dtype
|
|
343
354
|
)
|
|
355
|
+
# Validate loadings are finite
|
|
356
|
+
if np.any(~np.isfinite(loadings_constrained)):
|
|
357
|
+
_logger.warning(f"Computed loadings contain non-finite values for series {series_idx_int}. Skipping.")
|
|
358
|
+
continue
|
|
344
359
|
C_i[series_idx_int, :num_factors * tent_kernel_size] = loadings_constrained
|
|
345
360
|
except (np.linalg.LinAlgError, ValueError) as e:
|
|
346
361
|
_logger.warning(f"Failed to compute constrained loadings for series {series_idx_int}: {e}. Skipping.")
|