dfm-python 0.5.0__py3-none-any.whl → 0.5.2__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/config/results.py +6 -4
- dfm_python/config/schema.py +7 -2
- dfm_python/lightning/scaling.py +11 -5
- dfm_python/models/base.py +103 -186
- dfm_python/models/ddfm.py +138 -180
- dfm_python/models/dfm.py +117 -6
- dfm_python/models/utils.py +62 -59
- dfm_python/ssm/em.py +1 -1
- dfm_python/utils/data.py +5 -0
- dfm_python/utils/statespace.py +6 -2
- {dfm_python-0.5.0.dist-info → dfm_python-0.5.2.dist-info}/METADATA +49 -26
- {dfm_python-0.5.0.dist-info → dfm_python-0.5.2.dist-info}/RECORD +15 -15
- {dfm_python-0.5.0.dist-info → dfm_python-0.5.2.dist-info}/WHEEL +0 -0
- {dfm_python-0.5.0.dist-info → dfm_python-0.5.2.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/config/results.py
CHANGED
|
@@ -313,12 +313,14 @@ class NowcastResult:
|
|
|
313
313
|
Examples
|
|
314
314
|
--------
|
|
315
315
|
>>> from dfm_python import DFM
|
|
316
|
+
>>> import numpy as np
|
|
316
317
|
>>> model = DFM()
|
|
317
318
|
>>> trainer.fit(model, data_module)
|
|
318
|
-
>>> #
|
|
319
|
-
>>>
|
|
320
|
-
>>>
|
|
321
|
-
>>>
|
|
319
|
+
>>> # Update state with new data, then predict
|
|
320
|
+
>>> X_std = np.random.randn(10, 5) # Standardized data
|
|
321
|
+
>>> model.update(X_std)
|
|
322
|
+
>>> forecast = model.predict(horizon=1)
|
|
323
|
+
>>> print(f"Forecast: {forecast[0, 0]}")
|
|
322
324
|
"""
|
|
323
325
|
target_series: str
|
|
324
326
|
target_period: datetime
|
dfm_python/config/schema.py
CHANGED
|
@@ -398,6 +398,11 @@ class BaseModelConfig:
|
|
|
398
398
|
f"Block '{block_name}' validation failed: AR lag must be at least 1, got {ar_lag}. "
|
|
399
399
|
f"Please set ar_lag >= 1 for block '{block_name}'."
|
|
400
400
|
)
|
|
401
|
+
if ar_lag > 2:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"Block '{block_name}' validation failed: AR lag must be at most 2 (maximum supported VAR order is VAR(2)), got {ar_lag}. "
|
|
404
|
+
f"Please set ar_lag to 1 (VAR(1)) or 2 (VAR(2)) for block '{block_name}'."
|
|
405
|
+
)
|
|
401
406
|
|
|
402
407
|
# Auto-generate series_id if not provided and convert blocks to indices
|
|
403
408
|
n_blocks = len(self.block_names)
|
|
@@ -608,7 +613,7 @@ class DFMConfig(BaseModelConfig):
|
|
|
608
613
|
# ========================================================================
|
|
609
614
|
# EM Algorithm Parameters (HOW - controls the algorithm)
|
|
610
615
|
# ========================================================================
|
|
611
|
-
ar_lag: int = 1 # Number of lags in AR transition equation (lookback window)
|
|
616
|
+
ar_lag: int = 1 # Number of lags in AR transition equation (lookback window). Must be 1 or 2 (maximum supported order is VAR(2))
|
|
612
617
|
threshold: float = 1e-5 # EM convergence threshold
|
|
613
618
|
max_iter: int = 5000 # Maximum EM iterations
|
|
614
619
|
|
|
@@ -717,7 +722,7 @@ class DDFMConfig(BaseModelConfig):
|
|
|
717
722
|
learning_rate: float = 0.001 # Learning rate for Adam optimizer (default: 0.001)
|
|
718
723
|
epochs: int = 100 # Number of training epochs (default: 100)
|
|
719
724
|
batch_size: int = 100 # Batch size for training (default: 100 to match original DDFM)
|
|
720
|
-
factor_order: int = 1 # VAR lag order for factor dynamics
|
|
725
|
+
factor_order: int = 1 # VAR lag order for factor dynamics. Must be 1 or 2 (maximum supported order is VAR(2), default: 1)
|
|
721
726
|
use_idiosyncratic: bool = True # Model idio components with AR(1) dynamics (default: True)
|
|
722
727
|
min_obs_idio: int = 5 # Minimum observations for idio AR(1) estimation (default: 5)
|
|
723
728
|
|
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
|
@@ -16,16 +16,10 @@ import torch
|
|
|
16
16
|
import pytorch_lightning as pl
|
|
17
17
|
import pandas as pd
|
|
18
18
|
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from ..config.results import NowcastResult
|
|
21
|
-
|
|
22
19
|
from ..config import DFMConfig, make_config_source, ConfigSource, MergedConfigSource
|
|
23
20
|
from ..config.results import BaseResult
|
|
24
21
|
from ..config.schema import SeriesConfig, DEFAULT_BLOCK_NAME
|
|
25
22
|
from ..logger import get_logger
|
|
26
|
-
from .utils import create_nowcast_result
|
|
27
|
-
from ..utils.time import parse_timestamp, get_latest_time
|
|
28
|
-
from ..utils.data import create_data_view
|
|
29
23
|
|
|
30
24
|
_logger = get_logger(__name__)
|
|
31
25
|
|
|
@@ -194,28 +188,6 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
194
188
|
|
|
195
189
|
return config
|
|
196
190
|
|
|
197
|
-
def _get_data_from_datamodule(self) -> Tuple[Any, Optional[np.ndarray], Optional[np.ndarray]]:
|
|
198
|
-
"""Get processed data and standardization parameters from DataModule.
|
|
199
|
-
|
|
200
|
-
Returns
|
|
201
|
-
-------
|
|
202
|
-
X_torch : torch.Tensor
|
|
203
|
-
Processed data tensor (T x N)
|
|
204
|
-
Mx : np.ndarray or None
|
|
205
|
-
Mean values for unstandardization (N,)
|
|
206
|
-
Wx : np.ndarray or None
|
|
207
|
-
Standard deviation values for unstandardization (N,)
|
|
208
|
-
|
|
209
|
-
Raises
|
|
210
|
-
------
|
|
211
|
-
ValueError
|
|
212
|
-
If DataModule is not available
|
|
213
|
-
"""
|
|
214
|
-
data_module = self._get_datamodule()
|
|
215
|
-
X_torch = data_module.get_processed_data()
|
|
216
|
-
Mx, Wx = data_module.get_std_params()
|
|
217
|
-
return X_torch, Mx, Wx
|
|
218
|
-
|
|
219
191
|
def _get_datamodule(self):
|
|
220
192
|
"""Get DataModule from model or trainer.
|
|
221
193
|
|
|
@@ -250,7 +222,7 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
250
222
|
) -> np.ndarray:
|
|
251
223
|
"""Forecast factors using VAR dynamics.
|
|
252
224
|
|
|
253
|
-
Supports VAR(1) and VAR(2) factor dynamics.
|
|
225
|
+
Supports VAR(1) and VAR(2) factor dynamics (maximum supported order is VAR(2)).
|
|
254
226
|
|
|
255
227
|
Parameters
|
|
256
228
|
----------
|
|
@@ -259,7 +231,7 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
259
231
|
A : np.ndarray
|
|
260
232
|
Transition matrix. For VAR(1): (m x m), for VAR(2): (m x 2m)
|
|
261
233
|
p : int
|
|
262
|
-
VAR order
|
|
234
|
+
VAR order. Must be 1 or 2 (maximum supported order is VAR(2))
|
|
263
235
|
horizon : int
|
|
264
236
|
Number of periods to forecast
|
|
265
237
|
Z_prev : np.ndarray, optional
|
|
@@ -297,7 +269,7 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
297
269
|
else:
|
|
298
270
|
raise ValueError(
|
|
299
271
|
f"{self.__class__.__name__} prediction failed: unsupported VAR order {p}. "
|
|
300
|
-
f"
|
|
272
|
+
f"Maximum supported VAR order is VAR(2). Please use factor_order=1 (VAR(1)) or factor_order=2 (VAR(2))"
|
|
301
273
|
)
|
|
302
274
|
return Z_forecast
|
|
303
275
|
|
|
@@ -428,13 +400,22 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
428
400
|
Dict[str, torch.Tensor]
|
|
429
401
|
Dictionary of torch tensors: A, C, Q, R, Z_0, V_0
|
|
430
402
|
"""
|
|
403
|
+
# Ensure V_0 is always 2D (defensive check for edge cases)
|
|
404
|
+
V_0 = result.V_0
|
|
405
|
+
if isinstance(V_0, np.ndarray):
|
|
406
|
+
if V_0.ndim == 0:
|
|
407
|
+
V_0 = np.atleast_2d(V_0)
|
|
408
|
+
elif V_0.ndim == 1:
|
|
409
|
+
# If 1D, reshape to (m x m) - assume it's a single value for m=1 case
|
|
410
|
+
V_0 = np.atleast_2d(V_0).T if V_0.shape[0] == 1 else np.atleast_2d(V_0)
|
|
411
|
+
|
|
431
412
|
return {
|
|
432
413
|
'A': torch.tensor(result.A, dtype=torch.float32),
|
|
433
414
|
'C': torch.tensor(C if C is not None else result.C, dtype=torch.float32),
|
|
434
415
|
'Q': torch.tensor(result.Q, dtype=torch.float32),
|
|
435
416
|
'R': torch.tensor(R if R is not None else result.R, dtype=torch.float32),
|
|
436
417
|
'Z_0': torch.tensor(result.Z_0, dtype=torch.float32),
|
|
437
|
-
'V_0': torch.tensor(
|
|
418
|
+
'V_0': torch.tensor(V_0, dtype=torch.float32)
|
|
438
419
|
}
|
|
439
420
|
|
|
440
421
|
def _update_factor_state_dfm(
|
|
@@ -494,33 +475,66 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
494
475
|
"""
|
|
495
476
|
if not hasattr(self, 'encoder') or self.encoder is None:
|
|
496
477
|
_logger.warning(
|
|
497
|
-
f"{self.__class__.__name__}
|
|
498
|
-
"
|
|
478
|
+
f"{self.__class__.__name__} update(): Encoder not available, "
|
|
479
|
+
"cannot update factor state"
|
|
499
480
|
)
|
|
500
481
|
return None
|
|
501
482
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
483
|
+
try:
|
|
484
|
+
# Extract factors using encoder
|
|
485
|
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
486
|
+
self.encoder.eval()
|
|
487
|
+
X_tensor = torch.tensor(X_recent_std, device=device, dtype=torch.float32)
|
|
488
|
+
with torch.no_grad():
|
|
489
|
+
factors_raw = self.encoder(X_tensor).cpu().numpy() # (T x m)
|
|
490
|
+
|
|
491
|
+
# Validate factors shape
|
|
492
|
+
if factors_raw.ndim != 2 or factors_raw.shape[0] == 0:
|
|
493
|
+
_logger.warning(
|
|
494
|
+
f"{self.__class__.__name__} update(): Invalid factors shape {factors_raw.shape}, "
|
|
495
|
+
"cannot update factor state"
|
|
496
|
+
)
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
# For DDFM: C = I (identity), R = small noise
|
|
500
|
+
m = factors_raw.shape[1]
|
|
501
|
+
C = np.eye(m)
|
|
502
|
+
R = np.eye(m) * 1e-8
|
|
503
|
+
|
|
504
|
+
# Convert to torch: (m x T) format for Kalman filter
|
|
505
|
+
Y = torch.tensor(factors_raw.T, dtype=torch.float32)
|
|
506
|
+
params = self._result_to_torch_params(result, C=C, R=R)
|
|
507
|
+
|
|
508
|
+
# Run Kalman smoother
|
|
509
|
+
kf = self._get_kalman_filter(kalman_filter)
|
|
510
|
+
kf_result = kf(Y, params['A'], params['C'], params['Q'],
|
|
511
|
+
params['R'], params['Z_0'], params['V_0'])
|
|
512
|
+
|
|
513
|
+
# Handle return value - Kalman filter returns (zsmooth, Vsmooth, VVsmooth, loglik)
|
|
514
|
+
if isinstance(kf_result, tuple) and len(kf_result) >= 1:
|
|
515
|
+
zsmooth = kf_result[0]
|
|
516
|
+
else:
|
|
517
|
+
zsmooth = kf_result
|
|
518
|
+
|
|
519
|
+
# zsmooth is (m x (T+1)), transpose to ((T+1) x m), then get last row
|
|
520
|
+
if not isinstance(zsmooth, torch.Tensor):
|
|
521
|
+
zsmooth = torch.tensor(zsmooth, dtype=torch.float32)
|
|
522
|
+
|
|
523
|
+
zsmooth_T = zsmooth.T # ((T+1) x m)
|
|
524
|
+
if zsmooth_T.ndim == 2 and zsmooth_T.shape[0] > 0:
|
|
525
|
+
return zsmooth_T[-1, :].cpu().numpy()
|
|
526
|
+
else:
|
|
527
|
+
_logger.warning(
|
|
528
|
+
f"{self.__class__.__name__} update(): Invalid zsmooth shape {zsmooth_T.shape}, "
|
|
529
|
+
"cannot extract last state"
|
|
530
|
+
)
|
|
531
|
+
return None
|
|
532
|
+
|
|
533
|
+
except Exception as e:
|
|
534
|
+
_logger.warning(
|
|
535
|
+
f"{self.__class__.__name__} update(): Failed to update factor state: {e}"
|
|
536
|
+
)
|
|
537
|
+
return None
|
|
524
538
|
|
|
525
539
|
@property
|
|
526
540
|
def result(self) -> BaseResult:
|
|
@@ -576,149 +590,52 @@ class BaseFactorModel(pl.LightningModule):
|
|
|
576
590
|
"""
|
|
577
591
|
pass
|
|
578
592
|
|
|
579
|
-
|
|
593
|
+
@abstractmethod
|
|
594
|
+
def update(
|
|
580
595
|
self,
|
|
581
|
-
|
|
582
|
-
view_date: Optional[Union[datetime, str]] = None,
|
|
583
|
-
target_period: Optional[Union[datetime, str]] = None,
|
|
596
|
+
X_std: np.ndarray,
|
|
584
597
|
*,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
This method performs nowcasting by:
|
|
591
|
-
1. Creating a data view with masking based on release dates
|
|
592
|
-
2. Re-running Kalman filter with masked data to update factor state
|
|
593
|
-
3. Calling predict() with the updated state
|
|
598
|
+
history: Optional[int] = None,
|
|
599
|
+
kalman_filter: Optional[Any] = None,
|
|
600
|
+
scaler: Optional[Any] = None
|
|
601
|
+
) -> 'BaseFactorModel':
|
|
602
|
+
"""Update factor state with standardized data.
|
|
594
603
|
|
|
595
|
-
This
|
|
604
|
+
This method permanently updates the last factor state (result.Z[-1, :])
|
|
605
|
+
using the provided standardized data. Users should handle all preprocessing
|
|
606
|
+
(masking, imputation, standardization) before calling this method.
|
|
596
607
|
|
|
597
608
|
Parameters
|
|
598
609
|
----------
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
+
X_std : np.ndarray
|
|
611
|
+
Standardized data array (T x N), where T is number of time periods
|
|
612
|
+
and N is number of series. Data should already be standardized using
|
|
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.
|
|
619
|
+
kalman_filter : Any, optional
|
|
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.
|
|
610
624
|
|
|
611
625
|
Returns
|
|
612
626
|
-------
|
|
613
|
-
|
|
614
|
-
|
|
627
|
+
BaseFactorModel
|
|
628
|
+
Self for method chaining
|
|
615
629
|
|
|
616
630
|
Examples
|
|
617
631
|
--------
|
|
618
|
-
>>>
|
|
619
|
-
>>>
|
|
620
|
-
>>> #
|
|
621
|
-
>>>
|
|
622
|
-
>>>
|
|
623
|
-
>>> result = model.nowcast('gdp', view_date='2024-01-15', return_result=True)
|
|
632
|
+
>>> # Update state with new data, then predict
|
|
633
|
+
>>> model.update(X_std).predict(horizon=1)
|
|
634
|
+
>>> # Or update with only recent 12 periods
|
|
635
|
+
>>> model.update(X_std, history=12)
|
|
636
|
+
>>> forecast = model.predict(horizon=6)
|
|
624
637
|
"""
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
# Get DataModule
|
|
628
|
-
data_module = self._get_datamodule()
|
|
629
|
-
raw_data = data_module.data
|
|
630
|
-
time_index = data_module.time_index
|
|
631
|
-
|
|
632
|
-
# Convert to numpy
|
|
633
|
-
X = raw_data.to_numpy() if hasattr(raw_data, 'to_numpy') else np.asarray(raw_data)
|
|
634
|
-
|
|
635
|
-
# Parse view_date
|
|
636
|
-
if view_date is None:
|
|
637
|
-
view_date_dt = get_latest_time(time_index)
|
|
638
|
-
elif isinstance(view_date, str):
|
|
639
|
-
view_date_dt = parse_timestamp(view_date)
|
|
640
|
-
else:
|
|
641
|
-
view_date_dt = view_date
|
|
642
|
-
|
|
643
|
-
# Create data view with masking
|
|
644
|
-
X_view, Time_view, _ = create_data_view(
|
|
645
|
-
X=X,
|
|
646
|
-
Time=time_index,
|
|
647
|
-
Z=None,
|
|
648
|
-
config=self.config,
|
|
649
|
-
view_date=view_date_dt,
|
|
650
|
-
X_frame=raw_data if isinstance(raw_data, pd.DataFrame) else None
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
# Standardize masked data
|
|
654
|
-
result = self._result
|
|
655
|
-
X_view_std = self._standardize_data(X_view, result.Mx, result.Wx)
|
|
656
|
-
X_view_std = np.where(np.isfinite(X_view_std), X_view_std, np.nan)
|
|
657
|
-
|
|
658
|
-
# Update factor state using masked data (model-specific)
|
|
659
|
-
if hasattr(self, 'encoder') and self.encoder is not None:
|
|
660
|
-
# DDFM: Extract factors via encoder, then use Kalman filter
|
|
661
|
-
Z_last_updated = self._update_factor_state_ddfm(X_view_std, result, None)
|
|
662
|
-
else:
|
|
663
|
-
# DFM: Use Kalman filter directly on standardized data
|
|
664
|
-
Z_last_updated = self._update_factor_state_dfm(
|
|
665
|
-
X_view_std, result, getattr(self, 'kalman', None)
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
if Z_last_updated is None:
|
|
669
|
-
Z_last_updated = result.Z[-1, :]
|
|
670
|
-
_logger.warning(
|
|
671
|
-
f"{self.__class__.__name__} nowcast(): Failed to update factor state, "
|
|
672
|
-
f"using training state instead"
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
# Temporarily update result.Z[-1, :] with updated state
|
|
676
|
-
original_Z_last = result.Z[-1, :].copy()
|
|
677
|
-
result.Z[-1, :] = Z_last_updated
|
|
678
|
-
|
|
679
|
-
try:
|
|
680
|
-
# Call predict() with updated state
|
|
681
|
-
X_forecast = self.predict(horizon=horizon, return_series=True, return_factors=False)
|
|
682
|
-
|
|
683
|
-
# Extract nowcast value for target series
|
|
684
|
-
series_ids = [s.series_id for s in self.config.series if s.series_id]
|
|
685
|
-
try:
|
|
686
|
-
target_idx = series_ids.index(target_series)
|
|
687
|
-
except ValueError:
|
|
688
|
-
available = ', '.join(str(sid) for sid in series_ids[:5])
|
|
689
|
-
raise ValueError(
|
|
690
|
-
f"{self.__class__.__name__} nowcast failed: target_series '{target_series}' not found. "
|
|
691
|
-
f"Available: {available}..."
|
|
692
|
-
)
|
|
693
|
-
|
|
694
|
-
# Get nowcast value (first period of forecast)
|
|
695
|
-
if isinstance(X_forecast, np.ndarray):
|
|
696
|
-
nowcast_value = float(X_forecast[0, target_idx])
|
|
697
|
-
else:
|
|
698
|
-
nowcast_value = float(X_forecast[0][target_idx])
|
|
699
|
-
|
|
700
|
-
if return_result:
|
|
701
|
-
# Calculate data availability
|
|
702
|
-
data_availability = {
|
|
703
|
-
'n_available': int(np.sum(np.isfinite(X_view))),
|
|
704
|
-
'n_missing': int(np.sum(np.isnan(X_view)))
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
# Create NowcastResult using utility function
|
|
708
|
-
return create_nowcast_result(
|
|
709
|
-
target_series=target_series,
|
|
710
|
-
target_period=target_period,
|
|
711
|
-
view_date=view_date_dt,
|
|
712
|
-
nowcast_value=nowcast_value,
|
|
713
|
-
factors_at_view=Z_last_updated,
|
|
714
|
-
data_availability=data_availability,
|
|
715
|
-
dfm_result=result
|
|
716
|
-
)
|
|
717
|
-
else:
|
|
718
|
-
return nowcast_value
|
|
719
|
-
finally:
|
|
720
|
-
# Restore original state
|
|
721
|
-
result.Z[-1, :] = original_Z_last
|
|
638
|
+
pass
|
|
722
639
|
|
|
723
640
|
def get_result(self) -> BaseResult:
|
|
724
641
|
"""Extract result from trained model.
|