dfm-python 0.1.2__py3-none-any.whl → 0.1.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 CHANGED
@@ -1,6 +1,38 @@
1
- """Core nowcasting modules for DFM estimation and forecasting."""
1
+ """Dynamic Factor Model (DFM) package for Python.
2
2
 
3
- from .config import DFMConfig, AppConfig
3
+ This package implements a comprehensive Dynamic Factor Model framework with support for:
4
+ - Mixed-frequency time series data (daily, weekly, monthly, quarterly, semi-annual, annual)
5
+ - Clock-based synchronization of latent factors
6
+ - Tent kernel aggregation for low-to-high frequency mapping
7
+ - Expectation-Maximization (EM) algorithm for parameter estimation
8
+ - Kalman filtering and smoothing for factor extraction
9
+ - News decomposition for nowcasting
10
+
11
+ The package follows the FRBNY (Federal Reserve Bank of New York) approach to mixed-frequency
12
+ DFMs, where all latent factors (global and block-level) are synchronized to a common "clock"
13
+ frequency, typically monthly. Lower-frequency observed variables are mapped to higher-frequency
14
+ latent states using deterministic tent kernels in the observation equation.
15
+
16
+ Key Features:
17
+ - Unified configuration system (YAML/CSV)
18
+ - Flexible block structure for factor modeling
19
+ - Robust handling of missing data
20
+ - Comprehensive transformation support
21
+ - News decomposition for forecast updates
22
+
23
+ Example:
24
+ >>> from dfm_python import load_config, load_data, dfm
25
+ >>> config = load_config('config.yaml')
26
+ >>> X, Time, _ = load_data('data.csv', config)
27
+ >>> result = dfm(X, config)
28
+ >>> factors = result.Z # Extract estimated factors
29
+
30
+ For detailed documentation, see the README.md file.
31
+ """
32
+
33
+ __version__ = "0.1.3"
34
+
35
+ from .config import DFMConfig
4
36
  from .data_loader import (
5
37
  load_config, load_config_from_yaml, load_config_from_csv, load_data, transform_data
6
38
  )
@@ -14,7 +46,7 @@ ModelSpec = DFMConfig # Deprecated: use DFMConfig
14
46
  load_spec = load_config # Deprecated: use load_config
15
47
 
16
48
  __all__ = [
17
- 'DFMConfig', 'AppConfig',
49
+ 'DFMConfig',
18
50
  # Backward compatibility
19
51
  'ModelConfig', # Alias for DFMConfig
20
52
  'load_config', 'load_config_from_yaml', 'load_config_from_csv',
dfm_python/config.py CHANGED
@@ -1,4 +1,15 @@
1
- """Configuration models for DFM nowcasting using OmegaConf and Hydra."""
1
+ """Configuration models for DFM nowcasting using OmegaConf and Hydra.
2
+
3
+ This module defines the configuration structure for Dynamic Factor Models,
4
+ providing a unified interface for specifying:
5
+ - Series definitions (frequency, transformation, block loadings)
6
+ - Block structure and factor dimensions
7
+ - Estimation parameters (EM algorithm settings, missing data handling)
8
+ - Clock frequency for mixed-frequency synchronization
9
+
10
+ The configuration system supports both YAML and CSV formats, with validation
11
+ and type checking to ensure model specifications are correct before estimation.
12
+ """
2
13
 
3
14
  import numpy as np
4
15
  from typing import List, Optional, Dict, Any
@@ -106,6 +117,7 @@ class DFMConfig:
106
117
  max_iter: int = 5000 # Maximum EM iterations
107
118
  nan_method: int = 2 # Missing data handling method (1-5)
108
119
  nan_k: int = 3 # Spline parameter for NaN interpolation
120
+ clock: str = 'm' # Base frequency for nowcasting (global clock): 'd', 'w', 'm', 'q', 'sa', 'a' (defaults to 'm' for monthly)
109
121
 
110
122
  # ========================================================================
111
123
  # Internal cache (not user-configurable)
@@ -113,9 +125,26 @@ class DFMConfig:
113
125
  _cached_blocks: Optional[np.ndarray] = field(default=None, init=False, repr=False)
114
126
 
115
127
  def __post_init__(self):
116
- """Validate blocks structure and consistency."""
128
+ """Validate blocks structure and consistency.
129
+
130
+ This method performs comprehensive validation of the DFM configuration:
131
+ - Ensures at least one series is specified
132
+ - Validates block structure consistency across all series
133
+ - Ensures all series load on the global block
134
+ - Validates factor dimensions match block structure
135
+ - Validates clock frequency
136
+
137
+ Raises
138
+ ------
139
+ ValueError
140
+ If any validation check fails, with a descriptive error message
141
+ indicating what needs to be fixed.
142
+ """
117
143
  if not self.series:
118
- raise ValueError("At least one series must be specified")
144
+ raise ValueError(
145
+ "DFM configuration must contain at least one series. "
146
+ "Please add series definitions to your configuration."
147
+ )
119
148
 
120
149
  # Auto-generate series_id if not provided
121
150
  for i, s in enumerate(self.series):
@@ -132,16 +161,19 @@ class DFMConfig:
132
161
  for i, s in enumerate(self.series):
133
162
  if len(s.blocks) != n_blocks:
134
163
  raise ValueError(
135
- f"Series {i} ({s.series_id}) has {len(s.blocks)} blocks, "
136
- f"but expected {n_blocks} (from block_names)"
164
+ f"Series {i} ('{s.series_id}') has {len(s.blocks)} block loadings, "
165
+ f"but expected {n_blocks} (from block_names: {self.block_names}). "
166
+ f"Each series must specify a loading (0 or 1) for each block."
137
167
  )
138
168
 
139
169
  # Check first column (global block) is all 1s
140
170
  for i, s in enumerate(self.series):
141
171
  if s.blocks[0] != 1:
142
172
  raise ValueError(
143
- f"Series {i} ({s.series_id}) must load on global block "
144
- f"(first block must be 1)"
173
+ f"Series {i} ('{s.series_id}') must load on the global block "
174
+ f"(first block '{self.block_names[0]}'). "
175
+ f"All series must have blocks[0] = 1. "
176
+ f"Current value: {s.blocks[0]}"
145
177
  )
146
178
 
147
179
  # Validate factors_per_block if provided
@@ -149,10 +181,21 @@ class DFMConfig:
149
181
  if len(self.factors_per_block) != n_blocks:
150
182
  raise ValueError(
151
183
  f"factors_per_block length ({len(self.factors_per_block)}) must match "
152
- f"number of blocks ({n_blocks})"
184
+ f"number of blocks ({n_blocks}). "
185
+ f"Block names: {self.block_names}. "
186
+ f"Please provide one factor count per block."
153
187
  )
154
188
  if any(f < 1 for f in self.factors_per_block):
155
- raise ValueError("factors_per_block must contain positive integers (>= 1)")
189
+ invalid_blocks = [i for i, f in enumerate(self.factors_per_block) if f < 1]
190
+ raise ValueError(
191
+ f"factors_per_block must contain positive integers (>= 1). "
192
+ f"Invalid values found at block indices {invalid_blocks}: "
193
+ f"{[self.factors_per_block[i] for i in invalid_blocks]}. "
194
+ f"Each block must have at least one factor."
195
+ )
196
+
197
+ # Validate clock
198
+ self.clock = validate_frequency(self.clock)
156
199
 
157
200
  # Convenience properties for backward compatibility
158
201
  @property
@@ -250,18 +293,19 @@ class DFMConfig:
250
293
  category=str(cat_val) if cat_val is not None else ''
251
294
  ))
252
295
 
253
- # Extract estimation parameters if provided
254
- return cls(
255
- series=series_list,
256
- block_names=data.get('BlockNames', data.get('block_names', [])),
257
- factors_per_block=data.get('factors_per_block', None),
258
- # Estimation parameters
259
- ar_lag=data.get('ar_lag', 1),
260
- threshold=data.get('threshold', 1e-5),
261
- max_iter=data.get('max_iter', 5000),
262
- nan_method=data.get('nan_method', 2),
263
- nan_k=data.get('nan_k', 3)
264
- )
296
+ # Extract estimation parameters if provided
297
+ return cls(
298
+ series=series_list,
299
+ block_names=data.get('BlockNames', data.get('block_names', [])),
300
+ factors_per_block=data.get('factors_per_block', None),
301
+ # Estimation parameters
302
+ ar_lag=data.get('ar_lag', 1),
303
+ threshold=data.get('threshold', 1e-5),
304
+ max_iter=data.get('max_iter', 5000),
305
+ nan_method=data.get('nan_method', 2),
306
+ nan_k=data.get('nan_k', 3),
307
+ clock=data.get('clock', 'm')
308
+ )
265
309
 
266
310
  @classmethod
267
311
  def from_dict(cls, data: Dict[str, Any]) -> 'DFMConfig':
@@ -331,7 +375,8 @@ class DFMConfig:
331
375
  threshold=data.get('threshold', 1e-5),
332
376
  max_iter=data.get('max_iter', 5000),
333
377
  nan_method=data.get('nan_method', 2),
334
- nan_k=data.get('nan_k', 3)
378
+ nan_k=data.get('nan_k', 3),
379
+ clock=data.get('clock', 'm')
335
380
  )
336
381
 
337
382
  # New format with series list
@@ -350,7 +395,8 @@ class DFMConfig:
350
395
  threshold=data.get('threshold', 1e-5),
351
396
  max_iter=data.get('max_iter', 5000),
352
397
  nan_method=data.get('nan_method', 2),
353
- nan_k=data.get('nan_k', 3)
398
+ nan_k=data.get('nan_k', 3),
399
+ clock=data.get('clock', 'm')
354
400
  )
355
401
 
356
402
  # Direct instantiation (shouldn't happen often, but handle it)
@@ -415,6 +461,7 @@ if HYDRA_AVAILABLE and ConfigStore is not None:
415
461
  max_iter: int = 5000
416
462
  nan_method: int = 2
417
463
  nan_k: int = 3
464
+ clock: str = 'm'
418
465
 
419
466
  # Register schemas in config groups (following Hydra docs pattern)
420
467
  # These can be referenced in YAML defaults lists for validation
dfm_python/data_loader.py CHANGED
@@ -1,4 +1,14 @@
1
- """Data loading, specification parsing, and transformation functions."""
1
+ """Data loading and transformation utilities for DFM estimation.
2
+
3
+ This module provides comprehensive data handling for Dynamic Factor Models:
4
+ - Configuration loading from YAML and CSV files
5
+ - Data loading from CSV with automatic date parsing
6
+ - Time series transformations (differences, percent changes, etc.)
7
+ - Data sorting and alignment with configuration
8
+
9
+ The module supports flexible configuration formats and handles common data
10
+ issues such as missing dates, inconsistent formats, and transformation errors.
11
+ """
2
12
 
3
13
  import numpy as np
4
14
  import pandas as pd
@@ -9,7 +19,8 @@ import warnings
9
19
  import logging
10
20
 
11
21
  from .config import DFMConfig
12
- # Backward compatibility
22
+
23
+ # Backward compatibility alias (deprecated - use DFMConfig)
13
24
  ModelConfig = DFMConfig
14
25
 
15
26
  logger = logging.getLogger(__name__)
@@ -53,16 +64,34 @@ def load_config_from_yaml(configfile: Union[str, Path]) -> DFMConfig:
53
64
  cfg = OmegaConf.load(configfile)
54
65
  cfg_dict = OmegaConf.to_container(cfg, resolve=True)
55
66
 
67
+ # Extract model config and dfm parameters
68
+ model_dict = None
69
+ dfm_params = {}
70
+
56
71
  # Handle nested structure (if config has @package model: directive)
57
72
  if 'series' in cfg_dict and 'block_names' in cfg_dict:
58
73
  # Direct model config structure
59
- return DFMConfig.from_dict(cfg_dict)
74
+ model_dict = cfg_dict
60
75
  elif 'model' in cfg_dict:
61
76
  # Nested under 'model' key (from @package model:)
62
- return DFMConfig.from_dict(cfg_dict['model'])
77
+ model_dict = cfg_dict['model']
63
78
  else:
64
79
  # Try to construct from top-level keys
65
- return DFMConfig.from_dict(cfg_dict)
80
+ model_dict = cfg_dict
81
+
82
+ # Extract dfm parameters (estimation parameters like clock)
83
+ if 'dfm' in cfg_dict:
84
+ dfm_params = cfg_dict['dfm']
85
+
86
+ # Merge dfm parameters into model config
87
+ if dfm_params:
88
+ if isinstance(model_dict, dict):
89
+ model_dict = {**model_dict, **dfm_params}
90
+ else:
91
+ # If model_dict is not a dict, create new dict
92
+ model_dict = {'series': model_dict, **dfm_params} if model_dict else dfm_params
93
+
94
+ return DFMConfig.from_dict(model_dict)
66
95
 
67
96
 
68
97
  def load_config_from_csv(configfile: Union[str, Path]) -> DFMConfig:
@@ -291,16 +320,36 @@ def _transform_series(Z: np.ndarray, formula: str, freq: str, step: int) -> np.n
291
320
 
292
321
 
293
322
  def transform_data(Z: np.ndarray, Time: pd.DatetimeIndex, config: DFMConfig) -> Tuple[np.ndarray, pd.DatetimeIndex, np.ndarray]:
294
- """Transform each data series based on specification."""
323
+ """Transform each data series based on specification.
324
+
325
+ Handles all frequencies: daily (d), weekly (w), monthly (m),
326
+ quarterly (q), semi-annual (sa), annual (a).
327
+ """
295
328
  T, N = Z.shape
296
329
  X = np.full((T, N), np.nan)
297
330
 
331
+ # Frequency to step mapping (step = number of base periods per observation)
332
+ # Base frequency is monthly, so step is months per observation
333
+ freq_to_step = {
334
+ 'd': 1, # Daily: 1 month per observation (aggregated to monthly)
335
+ 'w': 1, # Weekly: 1 month per observation (aggregated to monthly)
336
+ 'm': 1, # Monthly: 1 month per observation
337
+ 'q': 3, # Quarterly: 3 months per observation
338
+ 'sa': 6, # Semi-annual: 6 months per observation
339
+ 'a': 12, # Annual: 12 months per observation
340
+ }
341
+
298
342
  for i in range(N):
299
- step = 3 if config.Frequency[i] == 'q' else 1
300
- X[:, i] = _transform_series(Z[:, i], config.Transformation[i], config.Frequency[i], step)
343
+ freq = config.Frequency[i]
344
+ step = freq_to_step.get(freq, 1) # Default to 1 if unknown frequency
345
+ X[:, i] = _transform_series(Z[:, i], config.Transformation[i], freq, step)
346
+
347
+ # Drop initial observations based on highest frequency in data
348
+ # Find maximum step (longest period) to determine drop period
349
+ max_step = max([freq_to_step.get(f, 1) for f in config.Frequency])
350
+ # Drop period should be at least max_step + 1 to handle transformations
351
+ drop = max(4, max_step + 1) # At least 4 for quarterly, more for annual
301
352
 
302
- # Drop first quarter of observations (4 months) since transformations cause missing values
303
- drop = 4
304
353
  if T > drop:
305
354
  return X[drop:], Time[drop:], Z[drop:]
306
355
  return X, Time, Z