dfm-python 0.5.1__py3-none-any.whl → 0.5.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 +1 -1
- dfm_python/encoder/vae.py +16 -2
- dfm_python/lightning/scaling.py +11 -5
- dfm_python/models/base.py +15 -41
- dfm_python/models/ddfm.py +175 -10
- dfm_python/models/dfm.py +273 -53
- dfm_python/models/mcmc.py +5 -4
- dfm_python/ssm/em.py +1 -1
- {dfm_python-0.5.1.dist-info → dfm_python-0.5.3.dist-info}/METADATA +23 -23
- {dfm_python-0.5.1.dist-info → dfm_python-0.5.3.dist-info}/RECORD +12 -12
- {dfm_python-0.5.1.dist-info → dfm_python-0.5.3.dist-info}/WHEEL +0 -0
- {dfm_python-0.5.1.dist-info → dfm_python-0.5.3.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -65,7 +65,7 @@ Note: DFMConfig and SeriesConfig are internal implementation details.
|
|
|
65
65
|
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
|
-
__version__ = "0.5.
|
|
68
|
+
__version__ = "0.5.2"
|
|
69
69
|
|
|
70
70
|
# ============================================================================
|
|
71
71
|
# PUBLIC API DEFINITION
|
dfm_python/encoder/vae.py
CHANGED
|
@@ -82,13 +82,27 @@ if _has_torch:
|
|
|
82
82
|
# Build layers
|
|
83
83
|
prev_dim = input_dim
|
|
84
84
|
for hidden_dim in hidden_dims:
|
|
85
|
-
|
|
85
|
+
layer = nn.Linear(prev_dim, hidden_dim)
|
|
86
|
+
# Initialize weights using Xavier/Kaiming initialization for better training stability
|
|
87
|
+
# Use Kaiming for ReLU, Xavier for tanh/sigmoid
|
|
88
|
+
if activation == 'relu':
|
|
89
|
+
nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='relu')
|
|
90
|
+
else:
|
|
91
|
+
nn.init.xavier_normal_(layer.weight, gain=1.0)
|
|
92
|
+
# Initialize bias to small values
|
|
93
|
+
if layer.bias is not None:
|
|
94
|
+
nn.init.constant_(layer.bias, 0.0)
|
|
95
|
+
self.layers.append(layer)
|
|
86
96
|
if use_batch_norm:
|
|
87
97
|
self.batch_norms.append(nn.BatchNorm1d(hidden_dim))
|
|
88
98
|
prev_dim = hidden_dim
|
|
89
99
|
|
|
90
|
-
# Output layer (no activation
|
|
100
|
+
# Output layer (linear, no activation)
|
|
101
|
+
# Use smaller initialization for output layer to prevent large initial factors
|
|
91
102
|
self.output_layer = nn.Linear(prev_dim, output_dim)
|
|
103
|
+
nn.init.xavier_normal_(self.output_layer.weight, gain=0.1) # Smaller gain for output
|
|
104
|
+
if self.output_layer.bias is not None:
|
|
105
|
+
nn.init.constant_(self.output_layer.bias, 0.0)
|
|
92
106
|
|
|
93
107
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
94
108
|
"""Forward pass through encoder.
|
dfm_python/lightning/scaling.py
CHANGED
|
@@ -124,7 +124,10 @@ class NoScalingStrategy:
|
|
|
124
124
|
# Scaler Factory
|
|
125
125
|
# ============================================================================
|
|
126
126
|
|
|
127
|
-
def _create_scaler_transformer(
|
|
127
|
+
def _create_scaler_transformer(
|
|
128
|
+
scaler_type: Optional[str],
|
|
129
|
+
use_robust_as_default: bool = False
|
|
130
|
+
) -> Any:
|
|
128
131
|
"""Create a raw sklearn scaler for unified scaling.
|
|
129
132
|
|
|
130
133
|
Returns a raw sklearn scaler that can be used directly in TransformerPipeline.
|
|
@@ -160,10 +163,13 @@ def _create_scaler_transformer(scaler_type: Optional[str]) -> Any:
|
|
|
160
163
|
)
|
|
161
164
|
|
|
162
165
|
if scaler_type is None or scaler_type == 'none':
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
if use_robust_as_default:
|
|
167
|
+
scaler_type_lower = 'robust'
|
|
168
|
+
else:
|
|
169
|
+
# Passthrough: no scaling
|
|
170
|
+
return FunctionTransformer(func=lambda x: x, inverse_func=lambda x: x)
|
|
171
|
+
else:
|
|
172
|
+
scaler_type_lower = scaler_type.lower()
|
|
167
173
|
|
|
168
174
|
if scaler_type_lower == 'standard':
|
|
169
175
|
return StandardScaler()
|
dfm_python/models/base.py
CHANGED
|
@@ -590,11 +590,14 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
590
590
|
"""
|
|
591
591
|
pass
|
|
592
592
|
|
|
593
|
+
@abstractmethod
|
|
593
594
|
def update(
|
|
594
595
|
self,
|
|
595
596
|
X_std: np.ndarray,
|
|
596
597
|
*,
|
|
597
|
-
|
|
598
|
+
history: Optional[int] = None,
|
|
599
|
+
kalman_filter: Optional[Any] = None,
|
|
600
|
+
scaler: Optional[Any] = None
|
|
598
601
|
) -> 'BaseFactorModel':
|
|
599
602
|
"""Update factor state with standardized data.
|
|
600
603
|
|
|
@@ -608,8 +611,16 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
608
611
|
Standardized data array (T x N), where T is number of time periods
|
|
609
612
|
and N is number of series. Data should already be standardized using
|
|
610
613
|
result.Mx and result.Wx.
|
|
614
|
+
history : int, optional
|
|
615
|
+
Number of recent periods to use for factor state update. If None, uses
|
|
616
|
+
all provided data (default). If specified (e.g., 60), uses only the most
|
|
617
|
+
recent N periods. Initial state (Z_0, V_0) is always estimated from
|
|
618
|
+
full training data, but the update uses only recent history for efficiency.
|
|
611
619
|
kalman_filter : Any, optional
|
|
612
620
|
Kalman filter instance. If None, uses default or model's kalman filter.
|
|
621
|
+
scaler : Any, optional
|
|
622
|
+
If provided, attach/replace the model's scaler (e.g., refit on new regime).
|
|
623
|
+
When None, keep the existing scaler from training.
|
|
613
624
|
|
|
614
625
|
Returns
|
|
615
626
|
-------
|
|
@@ -620,48 +631,11 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
620
631
|
--------
|
|
621
632
|
>>> # Update state with new data, then predict
|
|
622
633
|
>>> model.update(X_std).predict(horizon=1)
|
|
623
|
-
>>> # Or update
|
|
624
|
-
>>> model.update(X_std)
|
|
634
|
+
>>> # Or update with only recent 12 periods
|
|
635
|
+
>>> model.update(X_std, history=12)
|
|
625
636
|
>>> forecast = model.predict(horizon=6)
|
|
626
637
|
"""
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
result = self.result # Use property which ensures non-None after _check_trained()
|
|
630
|
-
|
|
631
|
-
# Validate input shape
|
|
632
|
-
if not isinstance(X_std, np.ndarray):
|
|
633
|
-
X_std = np.asarray(X_std)
|
|
634
|
-
if X_std.ndim != 2:
|
|
635
|
-
raise ValueError(
|
|
636
|
-
f"{self.__class__.__name__} update(): X_std must be 2D array (T x N), "
|
|
637
|
-
f"got shape {X_std.shape}"
|
|
638
|
-
)
|
|
639
|
-
|
|
640
|
-
# Handle NaN/Inf values
|
|
641
|
-
X_std = np.where(np.isfinite(X_std), X_std, np.nan)
|
|
642
|
-
|
|
643
|
-
# Update factor state (model-specific)
|
|
644
|
-
if hasattr(self, 'encoder') and self.encoder is not None:
|
|
645
|
-
# DDFM: Extract factors via encoder, then use Kalman filter
|
|
646
|
-
Z_last_updated = self._update_factor_state_ddfm(
|
|
647
|
-
X_std, result, kalman_filter
|
|
648
|
-
)
|
|
649
|
-
else:
|
|
650
|
-
# DFM: Use Kalman filter directly on standardized data
|
|
651
|
-
Z_last_updated = self._update_factor_state_dfm(
|
|
652
|
-
X_std, result, kalman_filter or getattr(self, 'kalman', None)
|
|
653
|
-
)
|
|
654
|
-
|
|
655
|
-
# Update result.Z[-1, :] permanently
|
|
656
|
-
if Z_last_updated is not None:
|
|
657
|
-
result.Z[-1, :] = Z_last_updated
|
|
658
|
-
else:
|
|
659
|
-
_logger.warning(
|
|
660
|
-
f"{self.__class__.__name__} update(): Failed to update factor state, "
|
|
661
|
-
f"keeping current state"
|
|
662
|
-
)
|
|
663
|
-
|
|
664
|
-
return self
|
|
638
|
+
pass
|
|
665
639
|
|
|
666
640
|
def get_result(self) -> BaseResult:
|
|
667
641
|
"""Extract result from trained model.
|
dfm_python/models/ddfm.py
CHANGED
|
@@ -308,6 +308,10 @@ class DDFM(BaseFactorModel):
|
|
|
308
308
|
decay_learning_rate: bool = True,
|
|
309
309
|
min_obs_pretrain: int = 50,
|
|
310
310
|
mult_epoch_pretrain: int = 1,
|
|
311
|
+
loss_function: str = 'mse',
|
|
312
|
+
huber_delta: float = 1.0,
|
|
313
|
+
weight_decay: float = 0.0,
|
|
314
|
+
grad_clip_val: float = 1.0,
|
|
311
315
|
**kwargs
|
|
312
316
|
):
|
|
313
317
|
"""Initialize DDFM instance.
|
|
@@ -349,6 +353,19 @@ class DDFM(BaseFactorModel):
|
|
|
349
353
|
mult_epoch_pretrain : int, default 1
|
|
350
354
|
Multiplier for number of epochs during pre-training
|
|
351
355
|
Display progress every 'disp' iterations
|
|
356
|
+
loss_function : str, default 'mse'
|
|
357
|
+
Loss function for training ('mse', 'huber').
|
|
358
|
+
'mse': Mean squared error (default, matches original DDFM)
|
|
359
|
+
'huber': Huber loss (more robust to outliers)
|
|
360
|
+
huber_delta : float, default 1.0
|
|
361
|
+
Delta parameter for Huber loss (only used if loss_function='huber').
|
|
362
|
+
Controls the transition point between quadratic and linear regions.
|
|
363
|
+
weight_decay : float, default 0.0
|
|
364
|
+
Weight decay (L2 regularization) for optimizer. Helps prevent overfitting to linear features.
|
|
365
|
+
Recommended: 1e-5 to 1e-3 for deeper encoders or when encoder collapses to linear behavior.
|
|
366
|
+
grad_clip_val : float, default 1.0
|
|
367
|
+
Maximum gradient norm for gradient clipping. Prevents training instability.
|
|
368
|
+
Set to 0.0 to disable gradient clipping.
|
|
352
369
|
seed : int, optional
|
|
353
370
|
Random seed for reproducibility
|
|
354
371
|
**kwargs
|
|
@@ -383,6 +400,22 @@ class DDFM(BaseFactorModel):
|
|
|
383
400
|
self.decay_learning_rate = decay_learning_rate
|
|
384
401
|
self.min_obs_pretrain = min_obs_pretrain
|
|
385
402
|
self.mult_epoch_pretrain = mult_epoch_pretrain
|
|
403
|
+
self.loss_function = loss_function.lower()
|
|
404
|
+
self.huber_delta = huber_delta
|
|
405
|
+
self.weight_decay = weight_decay
|
|
406
|
+
self.grad_clip_val = grad_clip_val
|
|
407
|
+
|
|
408
|
+
# Validate loss function
|
|
409
|
+
if self.loss_function not in ['mse', 'huber']:
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"DDFM initialization failed: loss_function must be 'mse' or 'huber', got '{loss_function}'"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Validate gradient clipping value
|
|
415
|
+
if self.grad_clip_val < 0.0:
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"DDFM initialization failed: grad_clip_val must be >= 0.0, got {grad_clip_val}"
|
|
418
|
+
)
|
|
386
419
|
|
|
387
420
|
# Determine number of factors
|
|
388
421
|
# DDFM does not use block structure - num_factors is specified directly
|
|
@@ -586,7 +619,9 @@ class DDFM(BaseFactorModel):
|
|
|
586
619
|
|
|
587
620
|
# Clip input data to prevent extreme values that cause NaN
|
|
588
621
|
# Clip to reasonable range: -10 to 10 standard deviations
|
|
589
|
-
|
|
622
|
+
# For deeper networks, use slightly tighter clipping to improve stability
|
|
623
|
+
clip_range = 8.0 if len(self.encoder_layers) > 2 else 10.0
|
|
624
|
+
data_clipped = torch.clamp(data, min=-clip_range, max=clip_range)
|
|
590
625
|
|
|
591
626
|
# Forward pass
|
|
592
627
|
reconstructed = self.forward(data_clipped)
|
|
@@ -603,8 +638,23 @@ class DDFM(BaseFactorModel):
|
|
|
603
638
|
# Compute loss with missing data masking
|
|
604
639
|
mask = torch.isfinite(target)
|
|
605
640
|
target_clean = torch.where(mask, target, torch.zeros_like(target))
|
|
606
|
-
|
|
607
|
-
|
|
641
|
+
|
|
642
|
+
# Use specified loss function
|
|
643
|
+
if self.loss_function == 'huber':
|
|
644
|
+
# Huber loss: more robust to outliers
|
|
645
|
+
# L_delta(a) = 0.5 * a^2 if |a| <= delta, else delta * (|a| - 0.5 * delta)
|
|
646
|
+
diff = target_clean - reconstructed
|
|
647
|
+
abs_diff = torch.abs(diff)
|
|
648
|
+
huber_loss = torch.where(
|
|
649
|
+
abs_diff <= self.huber_delta,
|
|
650
|
+
0.5 * diff ** 2,
|
|
651
|
+
self.huber_delta * (abs_diff - 0.5 * self.huber_delta)
|
|
652
|
+
)
|
|
653
|
+
loss = torch.sum(huber_loss * mask) / (torch.sum(mask) + 1e-8)
|
|
654
|
+
else:
|
|
655
|
+
# MSE loss (default)
|
|
656
|
+
squared_diff = (target_clean - reconstructed) ** 2
|
|
657
|
+
loss = torch.sum(squared_diff * mask) / (torch.sum(mask) + 1e-8)
|
|
608
658
|
|
|
609
659
|
# Handle NaN/Inf in loss
|
|
610
660
|
if not torch.isfinite(loss):
|
|
@@ -614,6 +664,9 @@ class DDFM(BaseFactorModel):
|
|
|
614
664
|
# Log metrics
|
|
615
665
|
self.log('train_loss', loss, on_step=True, on_epoch=True, prog_bar=True)
|
|
616
666
|
|
|
667
|
+
# Note: Gradient clipping is handled automatically by Lightning trainer if gradient_clip_val is set
|
|
668
|
+
# The grad_clip_val parameter is used in pre_train() and MCMC training for manual training loops
|
|
669
|
+
|
|
617
670
|
return loss
|
|
618
671
|
|
|
619
672
|
|
|
@@ -697,7 +750,8 @@ class DDFM(BaseFactorModel):
|
|
|
697
750
|
|
|
698
751
|
optimizer = torch.optim.Adam(
|
|
699
752
|
list(self.encoder.parameters()) + list(self.decoder.parameters()),
|
|
700
|
-
lr=self.learning_rate
|
|
753
|
+
lr=self.learning_rate,
|
|
754
|
+
weight_decay=self.weight_decay
|
|
701
755
|
)
|
|
702
756
|
|
|
703
757
|
if self.decay_learning_rate:
|
|
@@ -750,7 +804,8 @@ class DDFM(BaseFactorModel):
|
|
|
750
804
|
|
|
751
805
|
optimizer = torch.optim.Adam(
|
|
752
806
|
list(self.encoder.parameters()) + list(self.decoder.parameters()),
|
|
753
|
-
lr=lr
|
|
807
|
+
lr=lr,
|
|
808
|
+
weight_decay=self.weight_decay
|
|
754
809
|
)
|
|
755
810
|
|
|
756
811
|
return optimizer
|
|
@@ -842,7 +897,8 @@ class DDFM(BaseFactorModel):
|
|
|
842
897
|
# Create optimizer for pre-training
|
|
843
898
|
optimizer = torch.optim.Adam(
|
|
844
899
|
list(self.encoder.parameters()) + list(self.decoder.parameters()),
|
|
845
|
-
lr=self.learning_rate
|
|
900
|
+
lr=self.learning_rate,
|
|
901
|
+
weight_decay=self.weight_decay
|
|
846
902
|
)
|
|
847
903
|
|
|
848
904
|
# Pre-train for epochs * mult_epoch_pretrain
|
|
@@ -889,10 +945,11 @@ class DDFM(BaseFactorModel):
|
|
|
889
945
|
loss.backward()
|
|
890
946
|
|
|
891
947
|
# Gradient clipping for stability
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
948
|
+
if self.grad_clip_val > 0.0:
|
|
949
|
+
torch.nn.utils.clip_grad_norm_(
|
|
950
|
+
list(self.encoder.parameters()) + list(self.decoder.parameters()),
|
|
951
|
+
max_norm=self.grad_clip_val
|
|
952
|
+
)
|
|
896
953
|
|
|
897
954
|
optimizer.step()
|
|
898
955
|
|
|
@@ -1388,12 +1445,120 @@ class DDFM(BaseFactorModel):
|
|
|
1388
1445
|
Mx=Mx
|
|
1389
1446
|
)
|
|
1390
1447
|
|
|
1448
|
+
# Convert to numpy (handles torch inputs) and attempt scaler inverse_transform if available
|
|
1449
|
+
X_forecast = np.asarray(
|
|
1450
|
+
X_forecast.detach().cpu().numpy() if hasattr(X_forecast, "detach") else X_forecast
|
|
1451
|
+
)
|
|
1452
|
+
scaler = getattr(self, "scaler", None)
|
|
1453
|
+
if scaler is not None and hasattr(scaler, "inverse_transform"):
|
|
1454
|
+
try:
|
|
1455
|
+
X_forecast = scaler.inverse_transform(X_forecast)
|
|
1456
|
+
except Exception as e:
|
|
1457
|
+
_logger.warning(
|
|
1458
|
+
f"DDFM prediction: scaler.inverse_transform failed, returning unstandardized values. "
|
|
1459
|
+
f"error={e}"
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1391
1462
|
if return_series and return_factors:
|
|
1392
1463
|
return X_forecast, Z_forecast
|
|
1393
1464
|
if return_series:
|
|
1394
1465
|
return X_forecast
|
|
1395
1466
|
return Z_forecast
|
|
1396
1467
|
|
|
1468
|
+
def update(
|
|
1469
|
+
self,
|
|
1470
|
+
X_std: np.ndarray,
|
|
1471
|
+
*,
|
|
1472
|
+
history: Optional[int] = None,
|
|
1473
|
+
kalman_filter: Optional[Any] = None,
|
|
1474
|
+
scaler: Optional[Any] = None
|
|
1475
|
+
) -> 'DDFM':
|
|
1476
|
+
"""Update factor state with standardized data.
|
|
1477
|
+
|
|
1478
|
+
This method permanently updates the last factor state (result.Z[-1, :])
|
|
1479
|
+
using the provided standardized data. Users should handle all preprocessing
|
|
1480
|
+
(masking, imputation, standardization) before calling this method.
|
|
1481
|
+
|
|
1482
|
+
Parameters
|
|
1483
|
+
----------
|
|
1484
|
+
X_std : np.ndarray
|
|
1485
|
+
Standardized data array (T x N), where T is number of time periods
|
|
1486
|
+
and N is number of series. Data should already be standardized using
|
|
1487
|
+
result.Mx and result.Wx.
|
|
1488
|
+
history : int, optional
|
|
1489
|
+
Number of recent periods to use for factor state update. If None, uses
|
|
1490
|
+
all provided data (default). If specified (e.g., 60), uses only the most
|
|
1491
|
+
recent N periods. Initial state (Z_0, V_0) is always estimated from
|
|
1492
|
+
full training data, but the update uses only recent history for efficiency.
|
|
1493
|
+
kalman_filter : Any, optional
|
|
1494
|
+
Kalman filter instance. If None, uses default or model's kalman filter.
|
|
1495
|
+
|
|
1496
|
+
Returns
|
|
1497
|
+
-------
|
|
1498
|
+
DDFM
|
|
1499
|
+
Self for method chaining
|
|
1500
|
+
|
|
1501
|
+
Examples
|
|
1502
|
+
--------
|
|
1503
|
+
>>> # Update state with new data, then predict
|
|
1504
|
+
>>> model.update(X_std).predict(horizon=1)
|
|
1505
|
+
>>> # Or update with only recent 12 periods
|
|
1506
|
+
>>> model.update(X_std, history=12)
|
|
1507
|
+
>>> forecast = model.predict(horizon=6)
|
|
1508
|
+
"""
|
|
1509
|
+
self._check_trained()
|
|
1510
|
+
|
|
1511
|
+
# Optionally replace scaler (e.g., if refit on new regime)
|
|
1512
|
+
if scaler is not None:
|
|
1513
|
+
self.scaler = scaler
|
|
1514
|
+
|
|
1515
|
+
result = self.result # Use property which ensures non-None after _check_trained()
|
|
1516
|
+
|
|
1517
|
+
# Validate input shape
|
|
1518
|
+
if not isinstance(X_std, np.ndarray):
|
|
1519
|
+
X_std = np.asarray(X_std)
|
|
1520
|
+
if X_std.ndim != 2:
|
|
1521
|
+
raise ValueError(
|
|
1522
|
+
f"DDFM update(): X_std must be 2D array (T x N), "
|
|
1523
|
+
f"got shape {X_std.shape}"
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
# Handle NaN/Inf values
|
|
1527
|
+
X_std = np.where(np.isfinite(X_std), X_std, np.nan)
|
|
1528
|
+
|
|
1529
|
+
# Filter to recent history if specified
|
|
1530
|
+
# Note: Initial state (Z_0, V_0) from result is estimated from full training data,
|
|
1531
|
+
# but we use only recent history for the update step
|
|
1532
|
+
if history is not None and history > 0:
|
|
1533
|
+
if X_std.shape[0] > history:
|
|
1534
|
+
X_recent = X_std[-history:, :]
|
|
1535
|
+
_logger.debug(
|
|
1536
|
+
f"DDFM update(): Using {history} most recent periods out of {X_std.shape[0]} total periods"
|
|
1537
|
+
)
|
|
1538
|
+
else:
|
|
1539
|
+
X_recent = X_std
|
|
1540
|
+
_logger.debug(
|
|
1541
|
+
f"DDFM update(): history={history} specified but data has only {X_std.shape[0]} periods, using all data"
|
|
1542
|
+
)
|
|
1543
|
+
else:
|
|
1544
|
+
X_recent = X_std
|
|
1545
|
+
|
|
1546
|
+
# Update factor state using encoder to extract factors, then Kalman filter
|
|
1547
|
+
Z_last_updated = self._update_factor_state_ddfm(
|
|
1548
|
+
X_recent, result, kalman_filter
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
# Update result.Z[-1, :] permanently
|
|
1552
|
+
if Z_last_updated is not None:
|
|
1553
|
+
result.Z[-1, :] = Z_last_updated
|
|
1554
|
+
else:
|
|
1555
|
+
_logger.warning(
|
|
1556
|
+
f"DDFM update(): Failed to update factor state, "
|
|
1557
|
+
f"keeping current state"
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
return self
|
|
1561
|
+
|
|
1397
1562
|
@property
|
|
1398
1563
|
def result(self) -> DDFMResult:
|
|
1399
1564
|
"""Get model result from training state.
|
dfm_python/models/dfm.py
CHANGED
|
@@ -23,11 +23,15 @@ from ..config import (
|
|
|
23
23
|
MergedConfigSource,
|
|
24
24
|
)
|
|
25
25
|
from ..config.results import DFMResult, FitParams
|
|
26
|
+
from ..config.utils import get_agg_structure, get_tent_weights, FREQUENCY_HIERARCHY, TENT_WEIGHTS_LOOKUP
|
|
26
27
|
from ..logger import get_logger
|
|
27
28
|
from ..ssm.em import EMAlgorithm, EMStepParams
|
|
28
29
|
from ..ssm.kalman import KalmanFilter
|
|
29
30
|
from .base import BaseFactorModel
|
|
30
31
|
|
|
32
|
+
# Frequency to integer mapping for tensor conversion
|
|
33
|
+
_FREQ_TO_INT = {'d': 1, 'w': 2, 'm': 3, 'q': 4, 'sa': 5, 'a': 6}
|
|
34
|
+
|
|
31
35
|
if TYPE_CHECKING:
|
|
32
36
|
from omegaconf import DictConfig
|
|
33
37
|
from ..lightning import DFMDataModule
|
|
@@ -89,6 +93,8 @@ class DFM(BaseFactorModel):
|
|
|
89
93
|
max_iter: int = 100,
|
|
90
94
|
nan_method: int = 2,
|
|
91
95
|
nan_k: int = 3,
|
|
96
|
+
tent_weights_dict: Optional[dict] = None,
|
|
97
|
+
mixed_freq: bool = False,
|
|
92
98
|
**kwargs
|
|
93
99
|
):
|
|
94
100
|
"""Initialize DFM instance.
|
|
@@ -107,6 +113,12 @@ class DFM(BaseFactorModel):
|
|
|
107
113
|
Missing data handling method
|
|
108
114
|
nan_k : int, default 3
|
|
109
115
|
Spline interpolation order
|
|
116
|
+
tent_weights_dict : dict, optional
|
|
117
|
+
[DEPRECATED] Optional tent weights to control aggregation. Use `mixed_freq` parameter instead.
|
|
118
|
+
Kept for backward compatibility only.
|
|
119
|
+
mixed_freq : bool, default False
|
|
120
|
+
If True, use tent kernels for mixed-frequency data. If False, treat all series as clock frequency.
|
|
121
|
+
When True, raises ValueError if any frequency pair is not in TENT_WEIGHTS_LOOKUP.
|
|
110
122
|
**kwargs
|
|
111
123
|
Additional arguments passed to BaseFactorModel
|
|
112
124
|
"""
|
|
@@ -119,6 +131,20 @@ class DFM(BaseFactorModel):
|
|
|
119
131
|
self.max_iter = max_iter
|
|
120
132
|
self.nan_method = nan_method
|
|
121
133
|
self.nan_k = nan_k
|
|
134
|
+
self.mixed_freq = mixed_freq
|
|
135
|
+
|
|
136
|
+
# Legacy tent_weights_dict parameter (deprecated - use mixed_freq instead)
|
|
137
|
+
# Only used when mixed_freq=False for backward compatibility
|
|
138
|
+
self._legacy_tent_weights_dict = tent_weights_dict
|
|
139
|
+
|
|
140
|
+
# Mixed frequency parameters (set during initialize_from_data)
|
|
141
|
+
self._em_R_mat = None
|
|
142
|
+
self._em_q = None
|
|
143
|
+
self._em_nQ = 0
|
|
144
|
+
self._em_tent_weights_dict = None
|
|
145
|
+
self._em_frequencies = None
|
|
146
|
+
self._em_i_idio = None
|
|
147
|
+
self._em_idio_chain_lengths = None
|
|
122
148
|
|
|
123
149
|
# Determine number of factors
|
|
124
150
|
if num_factors is None:
|
|
@@ -178,6 +204,53 @@ class DFM(BaseFactorModel):
|
|
|
178
204
|
# Parameters will be initialized during fit_em() or first training step
|
|
179
205
|
pass
|
|
180
206
|
|
|
207
|
+
def _create_em_step_params(
|
|
208
|
+
self,
|
|
209
|
+
y: torch.Tensor,
|
|
210
|
+
device: torch.device,
|
|
211
|
+
dtype: torch.dtype
|
|
212
|
+
) -> EMStepParams:
|
|
213
|
+
"""Create EM step parameters using stored mixed frequency parameters.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
y : torch.Tensor
|
|
218
|
+
Data tensor (N x T)
|
|
219
|
+
device : torch.device
|
|
220
|
+
Device for tensors
|
|
221
|
+
dtype : torch.dtype
|
|
222
|
+
Data type for tensors
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
EMStepParams
|
|
227
|
+
EM step parameters
|
|
228
|
+
"""
|
|
229
|
+
clock = getattr(self.config, 'clock', 'm')
|
|
230
|
+
N = y.shape[0]
|
|
231
|
+
|
|
232
|
+
return EMStepParams(
|
|
233
|
+
y=y,
|
|
234
|
+
A=self.A,
|
|
235
|
+
C=self.C,
|
|
236
|
+
Q=self.Q,
|
|
237
|
+
R=self.R,
|
|
238
|
+
Z_0=self.Z_0,
|
|
239
|
+
V_0=self.V_0,
|
|
240
|
+
r=self.r.to(device),
|
|
241
|
+
p=self.p,
|
|
242
|
+
R_mat=self._em_R_mat,
|
|
243
|
+
q=self._em_q,
|
|
244
|
+
nQ=self._em_nQ,
|
|
245
|
+
i_idio=self._em_i_idio if self._em_i_idio is not None else torch.ones(N, device=device, dtype=dtype),
|
|
246
|
+
blocks=self.blocks.to(device),
|
|
247
|
+
tent_weights_dict=self._em_tent_weights_dict,
|
|
248
|
+
clock=clock,
|
|
249
|
+
frequencies=self._em_frequencies,
|
|
250
|
+
idio_chain_lengths=self._em_idio_chain_lengths if self._em_idio_chain_lengths is not None else torch.zeros(N, device=device, dtype=dtype),
|
|
251
|
+
config=self.config
|
|
252
|
+
)
|
|
253
|
+
|
|
181
254
|
def initialize_from_data(self, X: torch.Tensor) -> None:
|
|
182
255
|
"""Initialize parameters from data using PCA and OLS.
|
|
183
256
|
|
|
@@ -187,22 +260,100 @@ class DFM(BaseFactorModel):
|
|
|
187
260
|
Standardized data (T x N)
|
|
188
261
|
"""
|
|
189
262
|
opt_nan = {'method': self.nan_method, 'k': self.nan_k}
|
|
263
|
+
clock = getattr(self.config, 'clock', 'm')
|
|
190
264
|
|
|
191
|
-
#
|
|
265
|
+
# Handle mixed_freq parameter
|
|
266
|
+
if self.mixed_freq:
|
|
267
|
+
# Use tent kernels for mixed-frequency data
|
|
268
|
+
agg_structure = get_agg_structure(self.config, clock=clock)
|
|
269
|
+
|
|
270
|
+
# Validate that all required frequency pairs are in TENT_WEIGHTS_LOOKUP
|
|
271
|
+
frequencies_list = [s.frequency for s in self.config.series]
|
|
272
|
+
frequencies_set = set(frequencies_list)
|
|
273
|
+
clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, 3)
|
|
274
|
+
|
|
275
|
+
missing_pairs = []
|
|
276
|
+
for freq in frequencies_set:
|
|
277
|
+
freq_hierarchy = FREQUENCY_HIERARCHY.get(freq, 3)
|
|
278
|
+
if freq_hierarchy > clock_hierarchy:
|
|
279
|
+
# This frequency is slower than clock, needs tent kernel
|
|
280
|
+
tent_w = get_tent_weights(freq, clock)
|
|
281
|
+
if tent_w is None:
|
|
282
|
+
missing_pairs.append((freq, clock))
|
|
283
|
+
|
|
284
|
+
if missing_pairs:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"mixed_freq=True but the following frequency pairs are not in TENT_WEIGHTS_LOOKUP: {missing_pairs}. "
|
|
287
|
+
f"Available pairs: {list(TENT_WEIGHTS_LOOKUP.keys())}. "
|
|
288
|
+
f"Either add the missing pairs to TENT_WEIGHTS_LOOKUP or set mixed_freq=False."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Convert tent_weights to torch tensors
|
|
292
|
+
tent_weights_dict = {k: torch.tensor(v, dtype=torch.float32, device=X.device)
|
|
293
|
+
for k, v in agg_structure['tent_weights'].items()}
|
|
294
|
+
|
|
295
|
+
# Get R_mat and q from first structure (if any)
|
|
296
|
+
R_mat = None
|
|
297
|
+
q = None
|
|
298
|
+
if agg_structure['structures']:
|
|
299
|
+
first_structure = list(agg_structure['structures'].values())[0]
|
|
300
|
+
R_mat = torch.tensor(first_structure[0], dtype=torch.float32, device=X.device)
|
|
301
|
+
q = torch.tensor(first_structure[1], dtype=torch.float32, device=X.device)
|
|
302
|
+
|
|
303
|
+
# Create frequencies array
|
|
304
|
+
frequencies_array = np.array(frequencies_list, dtype=object)
|
|
305
|
+
|
|
306
|
+
# Count slower-frequency series
|
|
307
|
+
nQ = sum(1 for freq in frequencies_list
|
|
308
|
+
if FREQUENCY_HIERARCHY.get(freq, 3) > clock_hierarchy)
|
|
309
|
+
|
|
310
|
+
# Compute i_idio (1 for clock frequency, 0 for slower frequencies)
|
|
311
|
+
i_idio = torch.tensor([1 if freq == clock else 0 for freq in frequencies_list],
|
|
312
|
+
dtype=torch.float32, device=X.device)
|
|
313
|
+
else:
|
|
314
|
+
# Treat all series as clock frequency (unified frequency)
|
|
315
|
+
tent_weights_dict = self._legacy_tent_weights_dict
|
|
316
|
+
R_mat = None
|
|
317
|
+
q = None
|
|
318
|
+
frequencies_array = None
|
|
319
|
+
nQ = 0
|
|
320
|
+
i_idio = torch.ones(X.shape[1], dtype=torch.float32, device=X.device)
|
|
321
|
+
|
|
322
|
+
# Convert frequencies to torch tensor
|
|
323
|
+
frequencies_tensor = None
|
|
324
|
+
if frequencies_array is not None:
|
|
325
|
+
frequencies_tensor = torch.tensor(
|
|
326
|
+
[_FREQ_TO_INT.get(f, 3) for f in frequencies_array],
|
|
327
|
+
dtype=torch.int32,
|
|
328
|
+
device=X.device
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Store for reuse in EM steps
|
|
332
|
+
self._em_R_mat = R_mat
|
|
333
|
+
self._em_q = q
|
|
334
|
+
self._em_nQ = nQ
|
|
335
|
+
self._em_tent_weights_dict = tent_weights_dict
|
|
336
|
+
self._em_frequencies = frequencies_tensor
|
|
337
|
+
self._em_i_idio = i_idio
|
|
338
|
+
|
|
339
|
+
# Idiosyncratic chain lengths (currently unused, set to zeros)
|
|
340
|
+
self._em_idio_chain_lengths = torch.zeros(X.shape[1], dtype=torch.int32, device=X.device)
|
|
341
|
+
|
|
342
|
+
# Initialize parameters using EM algorithm
|
|
192
343
|
A, C, Q, R, Z_0, V_0 = self.em.initialize_parameters(
|
|
193
344
|
X,
|
|
194
345
|
r=self.r.to(X.device),
|
|
195
346
|
p=self.p,
|
|
196
347
|
blocks=self.blocks.to(X.device),
|
|
197
348
|
opt_nan=opt_nan,
|
|
198
|
-
R_mat=
|
|
199
|
-
q=
|
|
200
|
-
nQ=
|
|
201
|
-
i_idio=
|
|
202
|
-
clock=
|
|
203
|
-
tent_weights_dict=
|
|
204
|
-
frequencies=
|
|
205
|
-
idio_chain_lengths=
|
|
349
|
+
R_mat=R_mat,
|
|
350
|
+
q=q,
|
|
351
|
+
nQ=nQ,
|
|
352
|
+
i_idio=i_idio,
|
|
353
|
+
clock=clock,
|
|
354
|
+
tent_weights_dict=tent_weights_dict,
|
|
355
|
+
frequencies=frequencies_tensor,
|
|
356
|
+
idio_chain_lengths=self._em_idio_chain_lengths,
|
|
206
357
|
config=self.config
|
|
207
358
|
)
|
|
208
359
|
|
|
@@ -264,28 +415,8 @@ class DFM(BaseFactorModel):
|
|
|
264
415
|
# EM expects y as (N x T), but data is (T x N)
|
|
265
416
|
y = data.T # (N x T)
|
|
266
417
|
|
|
267
|
-
# Create EM step parameters
|
|
268
|
-
em_params =
|
|
269
|
-
y=y,
|
|
270
|
-
A=self.A,
|
|
271
|
-
C=self.C,
|
|
272
|
-
Q=self.Q,
|
|
273
|
-
R=self.R,
|
|
274
|
-
Z_0=self.Z_0,
|
|
275
|
-
V_0=self.V_0,
|
|
276
|
-
r=self.r.to(y.device),
|
|
277
|
-
p=self.p,
|
|
278
|
-
R_mat=None,
|
|
279
|
-
q=None,
|
|
280
|
-
nQ=0,
|
|
281
|
-
i_idio=torch.ones(y.shape[0], device=y.device, dtype=y.dtype),
|
|
282
|
-
blocks=self.blocks.to(y.device),
|
|
283
|
-
tent_weights_dict={},
|
|
284
|
-
clock=getattr(self.config, 'clock', 'm'),
|
|
285
|
-
frequencies=None,
|
|
286
|
-
idio_chain_lengths=torch.zeros(y.shape[0], device=y.device, dtype=y.dtype),
|
|
287
|
-
config=self.config
|
|
288
|
-
)
|
|
418
|
+
# Create EM step parameters using stored mixed frequency parameters
|
|
419
|
+
em_params = self._create_em_step_params(y, y.device, y.dtype)
|
|
289
420
|
|
|
290
421
|
# Perform EM step - use self.em(...) instead of em_step(...)
|
|
291
422
|
C_new, R_new, A_new, Q_new, Z_0_new, V_0_new, loglik = self.em(em_params)
|
|
@@ -387,28 +518,8 @@ class DFM(BaseFactorModel):
|
|
|
387
518
|
|
|
388
519
|
# EM loop
|
|
389
520
|
while num_iter < self.max_iter and not converged:
|
|
390
|
-
# Create EM step parameters
|
|
391
|
-
em_params =
|
|
392
|
-
y=y,
|
|
393
|
-
A=self.A,
|
|
394
|
-
C=self.C,
|
|
395
|
-
Q=self.Q,
|
|
396
|
-
R=self.R,
|
|
397
|
-
Z_0=self.Z_0,
|
|
398
|
-
V_0=self.V_0,
|
|
399
|
-
r=self.r.to(device),
|
|
400
|
-
p=self.p,
|
|
401
|
-
R_mat=None,
|
|
402
|
-
q=None,
|
|
403
|
-
nQ=0,
|
|
404
|
-
i_idio=torch.ones(y.shape[0], device=device, dtype=dtype),
|
|
405
|
-
blocks=self.blocks.to(device),
|
|
406
|
-
tent_weights_dict={},
|
|
407
|
-
clock=getattr(self.config, 'clock', 'm'),
|
|
408
|
-
frequencies=None,
|
|
409
|
-
idio_chain_lengths=torch.zeros(y.shape[0], device=device, dtype=dtype),
|
|
410
|
-
config=self.config
|
|
411
|
-
)
|
|
521
|
+
# Create EM step parameters using stored mixed frequency parameters
|
|
522
|
+
em_params = self._create_em_step_params(y, device, dtype)
|
|
412
523
|
|
|
413
524
|
# Perform EM step - use self.em(...) instead of em_step(...)
|
|
414
525
|
C_new, R_new, A_new, Q_new, Z_0_new, V_0_new, loglik = self.em(em_params)
|
|
@@ -832,6 +943,21 @@ class DFM(BaseFactorModel):
|
|
|
832
943
|
f"(max deviation: {max_deviation:.1f} std devs). "
|
|
833
944
|
f"Possible numerical instability."
|
|
834
945
|
)
|
|
946
|
+
|
|
947
|
+
# Convert to numpy (handles torch inputs) and attempt scaler inverse_transform if available
|
|
948
|
+
X_forecast = np.asarray(
|
|
949
|
+
X_forecast.detach().cpu().numpy() if hasattr(X_forecast, "detach") else X_forecast
|
|
950
|
+
)
|
|
951
|
+
scaler = getattr(self, "scaler", None)
|
|
952
|
+
if scaler is not None and hasattr(scaler, "inverse_transform"):
|
|
953
|
+
try:
|
|
954
|
+
X_forecast = scaler.inverse_transform(X_forecast)
|
|
955
|
+
except Exception as e:
|
|
956
|
+
_logger.warning(
|
|
957
|
+
f"DFM prediction: scaler.inverse_transform failed, returning unstandardized values. "
|
|
958
|
+
f"error={e}"
|
|
959
|
+
)
|
|
960
|
+
|
|
835
961
|
if return_factors and np.any(~np.isfinite(Z_forecast)):
|
|
836
962
|
nan_count = np.sum(~np.isfinite(Z_forecast))
|
|
837
963
|
raise ValueError(
|
|
@@ -846,6 +972,100 @@ class DFM(BaseFactorModel):
|
|
|
846
972
|
return X_forecast
|
|
847
973
|
return Z_forecast
|
|
848
974
|
|
|
975
|
+
def update(
|
|
976
|
+
self,
|
|
977
|
+
X_std: np.ndarray,
|
|
978
|
+
*,
|
|
979
|
+
history: Optional[int] = None,
|
|
980
|
+
kalman_filter: Optional[Any] = None,
|
|
981
|
+
scaler: Optional[Any] = None
|
|
982
|
+
) -> 'DFM':
|
|
983
|
+
"""Update factor state with standardized data.
|
|
984
|
+
|
|
985
|
+
This method permanently updates the last factor state (result.Z[-1, :])
|
|
986
|
+
using the provided standardized data. Users should handle all preprocessing
|
|
987
|
+
(masking, imputation, standardization) before calling this method.
|
|
988
|
+
|
|
989
|
+
Parameters
|
|
990
|
+
----------
|
|
991
|
+
X_std : np.ndarray
|
|
992
|
+
Standardized data array (T x N), where T is number of time periods
|
|
993
|
+
and N is number of series. Data should already be standardized using
|
|
994
|
+
result.Mx and result.Wx.
|
|
995
|
+
history : int, optional
|
|
996
|
+
Number of recent periods to use for factor state update. If None, uses
|
|
997
|
+
all provided data (default). If specified (e.g., 60), uses only the most
|
|
998
|
+
recent N periods. Initial state (Z_0, V_0) is always estimated from
|
|
999
|
+
full training data, but the update uses only recent history for efficiency.
|
|
1000
|
+
kalman_filter : Any, optional
|
|
1001
|
+
Kalman filter instance. If None, uses default or model's kalman filter.
|
|
1002
|
+
|
|
1003
|
+
Returns
|
|
1004
|
+
-------
|
|
1005
|
+
DFM
|
|
1006
|
+
Self for method chaining
|
|
1007
|
+
|
|
1008
|
+
Examples
|
|
1009
|
+
--------
|
|
1010
|
+
>>> # Update state with new data, then predict
|
|
1011
|
+
>>> model.update(X_std).predict(horizon=1)
|
|
1012
|
+
>>> # Or update with only recent 12 periods
|
|
1013
|
+
>>> model.update(X_std, history=12)
|
|
1014
|
+
>>> forecast = model.predict(horizon=6)
|
|
1015
|
+
"""
|
|
1016
|
+
self._check_trained()
|
|
1017
|
+
|
|
1018
|
+
# Optionally replace scaler (e.g., if refit on new regime)
|
|
1019
|
+
if scaler is not None:
|
|
1020
|
+
self.scaler = scaler
|
|
1021
|
+
|
|
1022
|
+
result = self.result # Use property which ensures non-None after _check_trained()
|
|
1023
|
+
|
|
1024
|
+
# Validate input shape
|
|
1025
|
+
if not isinstance(X_std, np.ndarray):
|
|
1026
|
+
X_std = np.asarray(X_std)
|
|
1027
|
+
if X_std.ndim != 2:
|
|
1028
|
+
raise ValueError(
|
|
1029
|
+
f"DFM update(): X_std must be 2D array (T x N), "
|
|
1030
|
+
f"got shape {X_std.shape}"
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# Handle NaN/Inf values
|
|
1034
|
+
X_std = np.where(np.isfinite(X_std), X_std, np.nan)
|
|
1035
|
+
|
|
1036
|
+
# Filter to recent history if specified
|
|
1037
|
+
# Note: Initial state (Z_0, V_0) from result is estimated from full training data,
|
|
1038
|
+
# but we use only recent history for the update step
|
|
1039
|
+
if history is not None and history > 0:
|
|
1040
|
+
if X_std.shape[0] > history:
|
|
1041
|
+
X_recent = X_std[-history:, :]
|
|
1042
|
+
_logger.debug(
|
|
1043
|
+
f"DFM update(): Using {history} most recent periods out of {X_std.shape[0]} total periods"
|
|
1044
|
+
)
|
|
1045
|
+
else:
|
|
1046
|
+
X_recent = X_std
|
|
1047
|
+
_logger.debug(
|
|
1048
|
+
f"DFM update(): history={history} specified but data has only {X_std.shape[0]} periods, using all data"
|
|
1049
|
+
)
|
|
1050
|
+
else:
|
|
1051
|
+
X_recent = X_std
|
|
1052
|
+
|
|
1053
|
+
# Update factor state using Kalman filter directly on standardized data
|
|
1054
|
+
Z_last_updated = self._update_factor_state_dfm(
|
|
1055
|
+
X_recent, result, kalman_filter or getattr(self, 'kalman', None)
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# Update result.Z[-1, :] permanently
|
|
1059
|
+
if Z_last_updated is not None:
|
|
1060
|
+
result.Z[-1, :] = Z_last_updated
|
|
1061
|
+
else:
|
|
1062
|
+
_logger.warning(
|
|
1063
|
+
f"DFM update(): Failed to update factor state, "
|
|
1064
|
+
f"keeping current state"
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
return self
|
|
1068
|
+
|
|
849
1069
|
@property
|
|
850
1070
|
def result(self) -> DFMResult:
|
|
851
1071
|
"""Get model result from training state.
|
dfm_python/models/mcmc.py
CHANGED
|
@@ -347,10 +347,11 @@ class DDFMMCMCTrainer:
|
|
|
347
347
|
loss = torch.sum(squared_diff) / (torch.sum(mask) + 1e-8)
|
|
348
348
|
loss.backward()
|
|
349
349
|
# Gradient clipping to prevent NaN and improve stability
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
350
|
+
if self.model.grad_clip_val > 0.0:
|
|
351
|
+
torch.nn.utils.clip_grad_norm_(
|
|
352
|
+
list(self.model.encoder.parameters()) + list(self.model.decoder.parameters()),
|
|
353
|
+
max_norm=self.model.grad_clip_val
|
|
354
|
+
)
|
|
354
355
|
optimizer.step()
|
|
355
356
|
# Extract factors from this sample
|
|
356
357
|
x_sample_tensor = torch.tensor(x_sim_den[i, :, :], device=device, dtype=dtype)
|
dfm_python/ssm/em.py
CHANGED
|
@@ -570,7 +570,7 @@ class EMAlgorithm(nn.Module):
|
|
|
570
570
|
resNaN = x_clean.clone()
|
|
571
571
|
resNaN[indNaN] = torch.nan
|
|
572
572
|
|
|
573
|
-
# Determine tent kernel size (pC) for
|
|
573
|
+
# Determine tent kernel size (pC) for slower-frequency aggregation
|
|
574
574
|
pC = 5 # Default: quarterly to monthly uses 5 periods [1,2,3,2,1]
|
|
575
575
|
if R_mat is not None:
|
|
576
576
|
pC = R_mat.shape[1]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Dynamic Factor Model (DFM) estimation and nowcasting 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
|
|
7
7
|
Project-URL: Repository, https://github.com/yourusername/dfm-python
|
|
8
8
|
Project-URL: Issues, https://github.com/yourusername/dfm-python/issues
|
|
9
|
-
Author:
|
|
9
|
+
Author: Minkey Chang
|
|
10
10
|
License: MIT
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: dfm,dynamic-factor-model,econometrics,forecasting,nowcasting,time-series
|
|
@@ -59,25 +59,25 @@ Description-Content-Type: text/markdown
|
|
|
59
59
|
[](https://pypi.org/project/dfm-python/)
|
|
60
60
|
[](https://www.python.org/downloads/)
|
|
61
61
|
|
|
62
|
-
A comprehensive Python implementation of **Dynamic Factor Models (DFM)** and **Deep Dynamic Factor Models (DDFM)** for forecasting high-dimensional time series. Built with PyTorch Lightning
|
|
62
|
+
A comprehensive Python implementation of **Dynamic Factor Models (DFM)** and **Deep Dynamic Factor Models (DDFM)** for forecasting high-dimensional time series. Built with PyTorch Lightning.
|
|
63
63
|
|
|
64
64
|
## Features
|
|
65
65
|
|
|
66
66
|
### Core Capabilities
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
67
|
+
- **Mixed-frequency data**: Monthly, quarterly, semi-annual, annual series in one model
|
|
68
|
+
- **Clock-based framework**: All factors evolve at a common clock frequency
|
|
69
|
+
- **Block structure**: Flexible factor organization (global + sector-specific factors)
|
|
70
|
+
- **Idiosyncratic components**: Per-series state augmentation for better fit
|
|
71
|
+
- **Preprocessed data**: Users handle preprocessing (imputation, scaling) using sktime or other tools
|
|
72
|
+
- **Forecasting**: Generate predictions for any horizon
|
|
73
|
+
- **Nowcasting**: Estimate current period values using incomplete data
|
|
74
|
+
- **Deep DFM (DDFM)**: Nonlinear encoder with PyTorch for capturing complex factor structures
|
|
75
75
|
|
|
76
76
|
### Technical Features
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
77
|
+
- **PyTorch Lightning**: Standard training interface with DataModule and Trainer
|
|
78
|
+
- **Multiple configuration methods**: YAML files, Python dictionaries, or Hydra
|
|
79
|
+
- **Advanced numerical stability**: Adaptive regularization, spectral radius capping, variance floors
|
|
80
|
+
- **Error handling**: Comprehensive error handling, extensive testing, well-documented
|
|
81
81
|
|
|
82
82
|
## Installation
|
|
83
83
|
|
|
@@ -391,17 +391,17 @@ pytest src/test/test_trainer.py -v
|
|
|
391
391
|
## Project Status
|
|
392
392
|
|
|
393
393
|
**Version**: 0.5.1
|
|
394
|
-
**Status**: Stable
|
|
395
|
-
**Python**: 3.10+
|
|
394
|
+
**Status**: Stable
|
|
395
|
+
**Python**: 3.10+
|
|
396
396
|
|
|
397
397
|
### What's New in 0.5.1
|
|
398
398
|
|
|
399
|
-
-
|
|
400
|
-
-
|
|
401
|
-
-
|
|
402
|
-
-
|
|
403
|
-
-
|
|
404
|
-
-
|
|
399
|
+
- **New `update()` method**: Replaces legacy `nowcast()` with flexible `update().predict()` pattern
|
|
400
|
+
- **Improved API**: Users now control all preprocessing (masking, imputation, standardization)
|
|
401
|
+
- **Code cleanup**: Removed legacy code and overengineering (~200+ lines removed)
|
|
402
|
+
- **Bug fixes**: Fixed DDFM `update()` method for single-factor models
|
|
403
|
+
- **Updated tutorials**: All 4 tutorials rewritten with new pattern and VAR(1) configuration
|
|
404
|
+
- **VAR(1) only**: Simplified to VAR(1) factor dynamics throughout
|
|
405
405
|
|
|
406
406
|
## License
|
|
407
407
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
dfm_python/__init__.py,sha256=
|
|
1
|
+
dfm_python/__init__.py,sha256=NU9ZxIsniiU9q1IVL8AuGVej9_ROM7nyIFD5dh4MfT4,5683
|
|
2
2
|
dfm_python/config/__init__.py,sha256=NrTwjNzQRcYlk5Ht0hTF8o-6CCl5e4S_JxQAp1mNyCs,1507
|
|
3
3
|
dfm_python/config/adapter.py,sha256=X-mjWlg6ebWp-VAxjAUssgHnmr8AXOvSxhTTVSHOIIk,27475
|
|
4
4
|
dfm_python/config/results.py,sha256=hzXL6MwaIxzA4uzouh6Bc_57lbpedphN9ErTMgfkpeE,15967
|
|
@@ -11,22 +11,22 @@ dfm_python/data/utils.py,sha256=5aJFrQ09I3oda4KNut1dXVt-9GYMGouT5lDUpfUEb8Q,1330
|
|
|
11
11
|
dfm_python/encoder/__init__.py,sha256=ljPTXq04cpOsGXNnma2EJ447kPFlytT6_mToLyKok7w,850
|
|
12
12
|
dfm_python/encoder/base.py,sha256=MOSn_bsQP-GVonGprJymQq-_Q6tmCQex8VOWUJjyxNA,3029
|
|
13
13
|
dfm_python/encoder/pca.py,sha256=FsvCqXD0AuASxymV2-5oAM2QVv_syhSyL3CmxVOXxgM,10584
|
|
14
|
-
dfm_python/encoder/vae.py,sha256=
|
|
14
|
+
dfm_python/encoder/vae.py,sha256=dmM243XrLedzwZ-4CdI3W8A9LH591JnlBjd72eHvXvs,22122
|
|
15
15
|
dfm_python/lightning/__init__.py,sha256=_2DFZnMX3BOybuZZ76VJJ8gmMLZ7tFyO3yyN-A4_ECI,1247
|
|
16
16
|
dfm_python/lightning/data_module.py,sha256=cZuBdGbk6meaeiw9ZPGkcsUW4v25ZP_km-u0Pkoh5bg,34815
|
|
17
|
-
dfm_python/lightning/scaling.py,sha256
|
|
17
|
+
dfm_python/lightning/scaling.py,sha256=-8zYrBm3y4_Ke0ODh1-SJnC0pWef3ZJT6GRv8owo2o8,16997
|
|
18
18
|
dfm_python/logger/__init__.py,sha256=2GwtfOG2ttbtJ5CrO6338R6GIao3YuY0HLiBeCoC0Ns,1066
|
|
19
19
|
dfm_python/logger/inference_logger.py,sha256=TmXiWl703boJ3mIR-WgGpy6hfeF8a68OW9vrZppTTCE,10091
|
|
20
20
|
dfm_python/logger/logger.py,sha256=1YzsUnvAnAK9CTcfttdSIMtgFvylO13LjQPMEsSs9xc,5187
|
|
21
21
|
dfm_python/logger/train_logger.py,sha256=9VYhzwNYH1Rn7IQipKTY5wmVJcX1n6HDgnUZH0hChPY,17732
|
|
22
22
|
dfm_python/models/__init__.py,sha256=3JsYZOqDGkl1eG8Zrc4-5gf7Sw47PzzDg2XBhsudB04,724
|
|
23
|
-
dfm_python/models/base.py,sha256
|
|
24
|
-
dfm_python/models/ddfm.py,sha256=
|
|
25
|
-
dfm_python/models/dfm.py,sha256=
|
|
26
|
-
dfm_python/models/mcmc.py,sha256=
|
|
23
|
+
dfm_python/models/base.py,sha256=WIx1d38bsaRkpfA4iYVG0_x7R1N8zVfi922LAlXqsEg,29261
|
|
24
|
+
dfm_python/models/ddfm.py,sha256=oMHZTUMu7Dy1_8O1IK33prx_B15s0ex83Hq0lWP-91w,66657
|
|
25
|
+
dfm_python/models/dfm.py,sha256=AK4gdwi5a_oEX8FqovyE19bCWN2GpL6SWiEx6-HsGqw,43318
|
|
26
|
+
dfm_python/models/mcmc.py,sha256=IyQYyfNV5ou48Q3SfStPt3k07N74lSQE_u8W8QX93ro,24070
|
|
27
27
|
dfm_python/models/utils.py,sha256=LAtvBqUYxnnl4dIaWl6MC-NHSuoyeNkHZppfWB1lCxQ,20193
|
|
28
28
|
dfm_python/ssm/__init__.py,sha256=dFpt8_3jb_foISDNhma_JUAzch0VrKZcaeWP9zwpwoM,1161
|
|
29
|
-
dfm_python/ssm/em.py,sha256=
|
|
29
|
+
dfm_python/ssm/em.py,sha256=sUiwuBVLTQBNHlBJ9YwemVY0wJYMTUqkuhyjPohENNw,63429
|
|
30
30
|
dfm_python/ssm/kalman.py,sha256=KC6Q71VxM2NSdBVB5107QsNUm-_kE59MiyqrzImWz7E,45931
|
|
31
31
|
dfm_python/ssm/utils.py,sha256=SYsu4E2zHAXXLfR2iQWNX-ywuzmwZj5kT4owHlyfvjU,13637
|
|
32
32
|
dfm_python/trainer/__init__.py,sha256=gc8jRjM-y2XN911LldwYUYHAAuFEvzUJJvXhG6B3Sv4,25654
|
|
@@ -38,7 +38,7 @@ dfm_python/utils/diagnostics.py,sha256=UwrJfnn8VACATfQEkTcWw9eEJlTik-VCgTZ1dJABZ
|
|
|
38
38
|
dfm_python/utils/helpers.py,sha256=iOVwkP5pti-q-HKOAkp6bUPAp7BPV7BBgh8jL76g9go,13049
|
|
39
39
|
dfm_python/utils/statespace.py,sha256=P-GQiCP9HzI2d9PgYst4nmRuocEXr9gG1yzGYsYWkMg,44159
|
|
40
40
|
dfm_python/utils/time.py,sha256=o6NRPuk4Ynk8kPaUnzR6xq3pPAO6aLUTR9N_3hn_N88,35651
|
|
41
|
-
dfm_python-0.5.
|
|
42
|
-
dfm_python-0.5.
|
|
43
|
-
dfm_python-0.5.
|
|
44
|
-
dfm_python-0.5.
|
|
41
|
+
dfm_python-0.5.3.dist-info/METADATA,sha256=6yDzvYrImsw5VC1FF2XyRGy7HLxlAs4c-jKU5cKMsw0,12007
|
|
42
|
+
dfm_python-0.5.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
43
|
+
dfm_python-0.5.3.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
|
|
44
|
+
dfm_python-0.5.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|