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 +35 -3
- dfm_python/config.py +70 -23
- dfm_python/data_loader.py +59 -10
- dfm_python/dfm.py +236 -293
- dfm_python/kalman.py +68 -16
- dfm_python/news.py +25 -3
- dfm_python/utils/__init__.py +17 -2
- dfm_python/utils/aggregation.py +258 -0
- dfm_python/utils/data_utils.py +11 -1
- dfm_python-0.1.3.dist-info/METADATA +1010 -0
- dfm_python-0.1.3.dist-info/RECORD +13 -0
- dfm_python-0.1.2.dist-info/METADATA +0 -395
- dfm_python-0.1.2.dist-info/RECORD +0 -12
- {dfm_python-0.1.2.dist-info → dfm_python-0.1.3.dist-info}/WHEEL +0 -0
- {dfm_python-0.1.2.dist-info → dfm_python-0.1.3.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Dynamic Factor Model (DFM) package for Python.
|
|
2
2
|
|
|
3
|
-
|
|
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',
|
|
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(
|
|
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)}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
model_dict = cfg_dict
|
|
60
75
|
elif 'model' in cfg_dict:
|
|
61
76
|
# Nested under 'model' key (from @package model:)
|
|
62
|
-
|
|
77
|
+
model_dict = cfg_dict['model']
|
|
63
78
|
else:
|
|
64
79
|
# Try to construct from top-level keys
|
|
65
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|