dfm-python 0.4.9__py3-none-any.whl → 0.5.0__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 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.4.9"
68
+ __version__ = "0.5.0"
69
69
 
70
70
  # ============================================================================
71
71
  # PUBLIC API DEFINITION
@@ -107,8 +107,6 @@ from .lightning import (
107
107
  EMAlgorithm, # Module class
108
108
  )
109
109
 
110
- # Nowcasting removed - use src.nowcast instead
111
-
112
110
  # Model implementations
113
111
  from .models.base import BaseFactorModel
114
112
  from .models.dfm import DFM
@@ -6,8 +6,9 @@ This module contains dataclasses for storing DFM estimation results and paramete
6
6
  import numpy as np
7
7
  import warnings
8
8
  from dataclasses import dataclass
9
- from typing import Optional, List, Dict, Any
9
+ from typing import Optional, List, Dict, Any, Tuple
10
10
  from abc import ABC
11
+ from datetime import datetime
11
12
 
12
13
  from .schema import DFMConfig
13
14
 
@@ -278,6 +279,57 @@ class DDFMResult(BaseResult):
278
279
  use_idiosyncratic: Optional[bool] = None # Whether idio components were used
279
280
 
280
281
 
282
+ @dataclass
283
+ class NowcastResult:
284
+ """Result from a single nowcast calculation.
285
+
286
+ This dataclass contains all information from a nowcast operation,
287
+ including the nowcast value, metadata about the data view, and
288
+ optional diagnostic information.
289
+
290
+ Attributes
291
+ ----------
292
+ target_series : str
293
+ Target series ID that was nowcasted.
294
+ target_period : datetime
295
+ Target period for the nowcast (the period being estimated).
296
+ view_date : datetime
297
+ View date (when data is available). This determines which
298
+ data points are masked/unmasked in the nowcast calculation.
299
+ nowcast_value : float
300
+ The calculated nowcast value for the target series.
301
+ confidence_interval : Tuple[float, float], optional
302
+ Confidence interval (lower, upper) for the nowcast, if available.
303
+ factors_at_view : np.ndarray, optional
304
+ Factor values at the view_date (m,). These are the updated
305
+ factor states after applying the data view masking.
306
+ dfm_result : BaseResult, optional
307
+ Full DFM/DDFM result for this view. Can be used for further
308
+ analysis or diagnostics.
309
+ data_availability : Dict[str, int], optional
310
+ Dictionary with keys 'n_available' and 'n_missing' indicating
311
+ how many data points were available vs missing in the data view.
312
+
313
+ Examples
314
+ --------
315
+ >>> from dfm_python import DFM
316
+ >>> model = DFM()
317
+ >>> trainer.fit(model, data_module)
318
+ >>> # Get nowcast with full result
319
+ >>> result = model.nowcast('gdp', view_date='2024-01-15', return_result=True)
320
+ >>> print(f"Nowcast: {result.nowcast_value}")
321
+ >>> print(f"Available data: {result.data_availability['n_available']}")
322
+ """
323
+ target_series: str
324
+ target_period: datetime
325
+ view_date: datetime
326
+ nowcast_value: float
327
+ confidence_interval: Optional[Tuple[float, float]] = None # (lower, upper)
328
+ factors_at_view: Optional[np.ndarray] = None # Factor values at view_date
329
+ dfm_result: Optional[BaseResult] = None # Full DFM/DDFM result for this view
330
+ data_availability: Optional[Dict[str, int]] = None # n_available, n_missing
331
+
332
+
281
333
  @dataclass
282
334
  class FitParams:
283
335
  """Parameter overrides for DFM model fitting.
@@ -4,6 +4,7 @@ This module provides:
4
4
  - Dataset classes: DFMDataset (linear DFM), DDFMDataset (deep DDFM)
5
5
  - DataLoader factories: create_dfm_dataloader, create_ddfm_dataloader
6
6
  - Data reading: read_data, load_data
7
+ - Nowcast data classes moved to src.nowcasting in main project
7
8
 
8
9
  Note: Transformation functions are application-specific and should be provided
9
10
  by users in their preprocessing pipelines. The package does not include
@@ -13,6 +14,7 @@ transformation utilities to remain generic.
13
14
  from .dataset import DFMDataset, DDFMDataset
14
15
  from .dataloader import create_dfm_dataloader, create_ddfm_dataloader
15
16
  from .utils import read_data, load_data
17
+ # Nowcast data classes moved to src.nowcasting in main project
16
18
 
17
19
  __all__ = [
18
20
  # Datasets
dfm_python/data/utils.py CHANGED
@@ -17,7 +17,6 @@ from ..config import DFMConfig
17
17
  from ..config.utils import FREQUENCY_HIERARCHY
18
18
  from ..utils.time import TimeIndex, parse_timestamp
19
19
  from ..utils.helpers import (
20
- safe_get_attr,
21
20
  get_frequencies,
22
21
  get_series_ids,
23
22
  )
@@ -267,7 +266,7 @@ def load_data(
267
266
  _logger.info(f"Loaded data: {X.shape[0]} time periods, {X.shape[1]} series (raw, not transformed)")
268
267
 
269
268
  # Validate data quality
270
- clock = safe_get_attr(config, 'clock', 'm')
269
+ clock = getattr(config, 'clock', 'm')
271
270
  clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, 3)
272
271
 
273
272
  frequencies = get_frequencies(config)
@@ -182,34 +182,76 @@ def _get_scale(scaler: Any, data: np.ndarray) -> Optional[np.ndarray]:
182
182
  def _normalize_wx(wx: np.ndarray) -> np.ndarray:
183
183
  """Normalize Wx to avoid division by zero.
184
184
 
185
+ This function replaces zero or NaN values in Wx with 1.0 to prevent
186
+ division by zero during standardization/unstandardization.
187
+
185
188
  Parameters
186
189
  ----------
187
190
  wx : np.ndarray
188
- Scale values (N,)
191
+ Scale values (N,), may contain zeros or NaN
189
192
 
190
193
  Returns
191
194
  -------
192
195
  np.ndarray
193
- Normalized scale values with zeros replaced by 1.0
196
+ Normalized scale values with zeros and NaN replaced by 1.0
194
197
  """
195
- return np.where(wx == 0, 1.0, wx)
198
+ # Replace both zero and NaN with 1.0
199
+ return np.where((wx == 0) | np.isnan(wx), 1.0, wx)
196
200
 
197
201
 
198
202
  def _compute_mx_wx(data: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
199
203
  """Compute Mx and Wx from data as fallback.
200
204
 
205
+ This function computes mean (Mx) and standard deviation (Wx) from data,
206
+ handling missing values (NaN) by using nan-aware functions. If NaN values
207
+ are detected, a warning is issued recommending imputation.
208
+
201
209
  Parameters
202
210
  ----------
203
211
  data : np.ndarray
204
- Processed data array (T x N)
212
+ Processed data array (T x N), may contain NaN values
205
213
 
206
214
  Returns
207
215
  -------
208
216
  Tuple[np.ndarray, np.ndarray]
209
- (Mx, Wx) tuple where Mx is mean and Wx is normalized std
217
+ (Mx, Wx) tuple where Mx is mean and Wx is normalized std.
218
+ NaN values are handled using nanmean and nanstd.
219
+
220
+ Notes
221
+ -----
222
+ If data contains NaN values, this function will:
223
+ 1. Use np.nanmean() and np.nanstd() to compute statistics ignoring NaN
224
+ 2. Issue a warning recommending data imputation for better results
225
+ 3. Continue processing with available data
210
226
  """
211
- mx = np.mean(data, axis=0)
212
- wx = np.std(data, axis=0)
227
+ # Check for NaN values
228
+ has_nan = np.any(np.isnan(data))
229
+ if has_nan:
230
+ nan_count = np.sum(np.isnan(data))
231
+ nan_pct = 100.0 * nan_count / data.size
232
+ _logger.warning(
233
+ f"Data contains {nan_count} NaN values ({nan_pct:.1f}%). "
234
+ f"Using nanmean/nanstd to compute standardization parameters. "
235
+ f"For better results, consider imputing missing values before creating DataModule. "
236
+ f"Suggested approaches: forward-fill, backward-fill, or interpolation."
237
+ )
238
+
239
+ # Use nan-aware functions to handle missing values
240
+ mx = np.nanmean(data, axis=0)
241
+ wx = np.nanstd(data, axis=0)
242
+
243
+ # Check if any series have all NaN values (would result in NaN std)
244
+ nan_std_mask = np.isnan(wx)
245
+ if np.any(nan_std_mask):
246
+ n_nan_std = np.sum(nan_std_mask)
247
+ _logger.warning(
248
+ f"{n_nan_std} series have all NaN values, resulting in NaN standard deviation. "
249
+ f"These will be normalized to 1.0 to avoid division by zero. "
250
+ f"Consider imputing or removing these series."
251
+ )
252
+ # Replace NaN std with 1.0 (will be normalized by _normalize_wx anyway)
253
+ wx = np.where(nan_std_mask, 1.0, wx)
254
+
213
255
  wx = _normalize_wx(wx)
214
256
  return mx, wx
215
257