dfm-python 0.2.8__py3-none-any.whl → 0.2.9__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 +6 -29
- dfm_python/api.py +38 -19
- dfm_python/config.py +13 -14
- dfm_python/config_sources.py +354 -59
- dfm_python/core/helpers.py +76 -0
- dfm_python/data.py +1 -51
- dfm_python/dfm.py +4 -1
- dfm_python/news.py +14 -22
- dfm_python/utils.py +4 -7
- {dfm_python-0.2.8.dist-info → dfm_python-0.2.9.dist-info}/METADATA +4 -10
- dfm_python-0.2.9.dist-info/RECORD +18 -0
- dfm_python-0.2.8.dist-info/RECORD +0 -18
- {dfm_python-0.2.8.dist-info → dfm_python-0.2.9.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.8.dist-info → dfm_python-0.2.9.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -50,22 +50,17 @@ Example (Low-level API - For advanced usage):
|
|
|
50
50
|
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
__version__ = "0.2.
|
|
53
|
+
__version__ = "0.2.9"
|
|
54
54
|
|
|
55
|
-
from .config import DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME
|
|
56
55
|
from .config import (
|
|
57
|
-
|
|
58
|
-
YamlSource,
|
|
59
|
-
|
|
60
|
-
SpecCSVSource,
|
|
61
|
-
HydraSource,
|
|
62
|
-
MergedConfigSource,
|
|
63
|
-
make_config_source,
|
|
56
|
+
DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME,
|
|
57
|
+
ConfigSource, YamlSource, DictSource, SpecCSVSource, HydraSource,
|
|
58
|
+
MergedConfigSource, make_config_source,
|
|
64
59
|
)
|
|
65
60
|
from .data import transform_data
|
|
66
61
|
from .dfm import DFMResult
|
|
67
62
|
from .core import calculate_rmse, diagnose_series, print_series_diagnosis
|
|
68
|
-
from .dfm import
|
|
63
|
+
from .dfm import DFMCore # Core DFM class from dfm.py
|
|
69
64
|
from .kalman import run_kf, skf, fis, miss_data
|
|
70
65
|
from .news import update_nowcast, news_dfm, para_const
|
|
71
66
|
|
|
@@ -100,27 +95,9 @@ def get_original_data():
|
|
|
100
95
|
"""Get original (untransformed) data matrix."""
|
|
101
96
|
return _dfm_instance.original_data
|
|
102
97
|
|
|
103
|
-
# For direct attribute access, we'll use __getattr__ but handle config module conflict
|
|
104
|
-
|
|
105
|
-
def __getattr__(name):
|
|
106
|
-
"""Allow access to DFM instance properties at module level."""
|
|
107
|
-
# Handle special cases that conflict with submodules
|
|
108
|
-
if name == 'config' and _dfm_instance.config is not None:
|
|
109
|
-
return _dfm_instance.config
|
|
110
|
-
elif name == 'data':
|
|
111
|
-
return _dfm_instance.data
|
|
112
|
-
elif name == 'time':
|
|
113
|
-
return _dfm_instance.time
|
|
114
|
-
elif name == 'result':
|
|
115
|
-
return _dfm_instance.result
|
|
116
|
-
elif name == 'original_data':
|
|
117
|
-
return _dfm_instance.original_data
|
|
118
|
-
# For other attributes, try normal import
|
|
119
|
-
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
120
|
-
|
|
121
98
|
__all__ = [
|
|
122
99
|
# Core classes
|
|
123
|
-
'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params', 'DFM',
|
|
100
|
+
'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params', 'DFM', 'DFMCore',
|
|
124
101
|
# Constants
|
|
125
102
|
'DEFAULT_GLOBAL_BLOCK_NAME',
|
|
126
103
|
# Config sources
|
dfm_python/api.py
CHANGED
|
@@ -29,11 +29,11 @@ from .config import (
|
|
|
29
29
|
ConfigSource,
|
|
30
30
|
MergedConfigSource,
|
|
31
31
|
)
|
|
32
|
-
from .dfm import
|
|
32
|
+
from .dfm import DFMCore, DFMResult
|
|
33
33
|
from .core.helpers import safe_get_method, safe_get_attr
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class DFM(
|
|
36
|
+
class DFM(DFMCore):
|
|
37
37
|
"""High-level API for Dynamic Factor Model estimation.
|
|
38
38
|
|
|
39
39
|
This class provides a simple, object-oriented interface for DFM operations.
|
|
@@ -368,29 +368,48 @@ def from_yaml(yaml_path: Union[str, Path]) -> DFM:
|
|
|
368
368
|
return _dfm_instance.load_config(yaml=yaml_path)
|
|
369
369
|
|
|
370
370
|
|
|
371
|
-
def from_spec(
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
def from_spec(
|
|
372
|
+
csv_path: Union[str, Path],
|
|
373
|
+
output_dir: Optional[Union[str, Path]] = None,
|
|
374
|
+
series_filename: Optional[str] = None,
|
|
375
|
+
blocks_filename: Optional[str] = None
|
|
376
|
+
) -> Tuple[Path, Path]:
|
|
377
|
+
"""Convert spec CSV file to YAML configuration files.
|
|
374
378
|
|
|
375
|
-
|
|
376
|
-
|
|
379
|
+
This function reads a spec CSV file and generates two YAML files:
|
|
380
|
+
- config/series/{basename}.yaml - series definitions
|
|
381
|
+
- config/blocks/{basename}.yaml - block definitions
|
|
377
382
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
csv_path : str or Path
|
|
386
|
+
Path to the spec CSV file
|
|
387
|
+
output_dir : str or Path, optional
|
|
388
|
+
Output directory for YAML files. Defaults to config/ directory relative to CSV.
|
|
389
|
+
series_filename : str, optional
|
|
390
|
+
Custom filename for series YAML (without .yaml extension).
|
|
391
|
+
Defaults to CSV basename.
|
|
392
|
+
blocks_filename : str, optional
|
|
393
|
+
Custom filename for blocks YAML (without .yaml extension).
|
|
394
|
+
Defaults to CSV basename.
|
|
395
|
+
|
|
396
|
+
Returns
|
|
397
|
+
-------
|
|
398
|
+
Tuple[Path, Path]
|
|
399
|
+
Paths to generated series YAML and blocks YAML files
|
|
400
|
+
|
|
401
|
+
Examples
|
|
402
|
+
--------
|
|
403
|
+
>>> series_path, blocks_path = from_spec('data/sample_spec.csv')
|
|
404
|
+
>>> # Creates config/series/sample_spec.yaml and config/blocks/sample_spec.yaml
|
|
405
|
+
"""
|
|
406
|
+
from .config_sources import from_spec as _from_spec
|
|
407
|
+
return _from_spec(csv_path, output_dir, series_filename, blocks_filename)
|
|
389
408
|
|
|
390
409
|
|
|
391
410
|
def from_spec_df(spec_df: pd.DataFrame, params: Optional[Params] = None) -> DFM:
|
|
392
411
|
"""Load configuration from spec DataFrame (convenience constructor)."""
|
|
393
|
-
from .
|
|
412
|
+
from .config_sources import _load_config_from_dataframe
|
|
394
413
|
|
|
395
414
|
if params is None:
|
|
396
415
|
params = Params()
|
dfm_python/config.py
CHANGED
|
@@ -21,6 +21,7 @@ from config_sources for backward compatibility.
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
import numpy as np
|
|
24
|
+
import warnings
|
|
24
25
|
from typing import List, Optional, Dict, Any, Union
|
|
25
26
|
from dataclasses import dataclass, field
|
|
26
27
|
|
|
@@ -102,10 +103,14 @@ class SeriesConfig:
|
|
|
102
103
|
Unique identifier (auto-generated if None)
|
|
103
104
|
series_name : str, optional
|
|
104
105
|
Human-readable name (defaults to series_id if None)
|
|
105
|
-
units : str
|
|
106
|
-
Units of measurement
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
units : str, optional
|
|
107
|
+
Units of measurement (optional metadata for display purposes only).
|
|
108
|
+
Used in news decomposition output for readability. Not used in model estimation.
|
|
109
|
+
release_date : int, optional
|
|
110
|
+
Release date information for pseudo real-time nowcasting.
|
|
111
|
+
- Positive value (1-31): Day of month when data is released
|
|
112
|
+
- Negative value: Days before end of previous month when data is released
|
|
113
|
+
Example: 25 = released on 25th of each month, -5 = released 5 days before end of previous month
|
|
109
114
|
"""
|
|
110
115
|
# Required fields (no defaults)
|
|
111
116
|
frequency: str
|
|
@@ -114,9 +119,8 @@ class SeriesConfig:
|
|
|
114
119
|
# Optional fields (with defaults - must come after required fields)
|
|
115
120
|
series_id: Optional[str] = None # Auto-generated if None: "series_0", "series_1", etc.
|
|
116
121
|
series_name: Optional[str] = None # Optional metadata for display
|
|
117
|
-
units: str =
|
|
118
|
-
|
|
119
|
-
aggregate: Optional[str] = None # Aggregation method (deprecated: higher frequencies than clock are not supported)
|
|
122
|
+
units: Optional[str] = None # Optional metadata for display only (used in news.py output)
|
|
123
|
+
release_date: Optional[int] = None # Release date for pseudo real-time nowcasting
|
|
120
124
|
|
|
121
125
|
def __post_init__(self):
|
|
122
126
|
"""Validate fields after initialization."""
|
|
@@ -716,9 +720,7 @@ class DFMConfig:
|
|
|
716
720
|
blocks=series_blocks,
|
|
717
721
|
series_id=get_list_value('SeriesID', i, None),
|
|
718
722
|
series_name=get_list_value('SeriesName', i, None),
|
|
719
|
-
units=
|
|
720
|
-
category=str(get_list_value('Category', i, '')),
|
|
721
|
-
aggregate=get_list_value('Aggregate', i, None)
|
|
723
|
+
units=get_list_value('Units', i, None) # Optional, for display only
|
|
722
724
|
))
|
|
723
725
|
|
|
724
726
|
# Convert block_names to blocks dict
|
|
@@ -805,9 +807,7 @@ class DFMConfig:
|
|
|
805
807
|
frequency=series_cfg.get('frequency', 'm'),
|
|
806
808
|
transformation=series_cfg.get('transformation', 'lin'),
|
|
807
809
|
blocks=series_blocks,
|
|
808
|
-
units=series_cfg.get('units',
|
|
809
|
-
category=series_cfg.get('category', ''),
|
|
810
|
-
aggregate=series_cfg.get('aggregate', None) # Deprecated
|
|
810
|
+
units=series_cfg.get('units', None) # Optional, for display only
|
|
811
811
|
))
|
|
812
812
|
|
|
813
813
|
# Convert blocks_dict to BlockConfig dict
|
|
@@ -1031,7 +1031,6 @@ def validate_transformation(transformation: str) -> str:
|
|
|
1031
1031
|
>>> validate_transformation('unknown') # Issues warning but returns value
|
|
1032
1032
|
'unknown'
|
|
1033
1033
|
"""
|
|
1034
|
-
import warnings
|
|
1035
1034
|
if transformation not in _VALID_TRANSFORMATIONS:
|
|
1036
1035
|
warnings.warn(f"Unknown transformation code: {transformation}. Will use untransformed data.")
|
|
1037
1036
|
return transformation
|
dfm_python/config_sources.py
CHANGED
|
@@ -10,11 +10,62 @@ This module provides adapters for loading DFMConfig from various sources:
|
|
|
10
10
|
All adapters implement the ConfigSource protocol and return DFMConfig objects.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import warnings
|
|
14
|
+
from typing import Protocol, Optional, Dict, Any, Union, Tuple
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from dataclasses import is_dataclass, asdict
|
|
16
17
|
|
|
17
18
|
from .config import DFMConfig, SeriesConfig, BlockConfig, DEFAULT_GLOBAL_BLOCK_NAME
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_config_from_defaults(cfg, root_config_dir, config_type: str) -> Optional[dict]:
|
|
23
|
+
"""Load config from defaults or direct path (helper for series/blocks loading).
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
cfg : OmegaConf.DictConfig
|
|
28
|
+
Main config object
|
|
29
|
+
root_config_dir : Path
|
|
30
|
+
Root config directory (contains series/ and blocks/ subdirectories)
|
|
31
|
+
config_type : str
|
|
32
|
+
Type of config to load: 'series' or 'blocks'
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
Optional[dict]
|
|
37
|
+
Loaded config dict or None if not found
|
|
38
|
+
"""
|
|
39
|
+
from omegaconf import OmegaConf
|
|
40
|
+
|
|
41
|
+
config_dict = None
|
|
42
|
+
config_loaded = False
|
|
43
|
+
|
|
44
|
+
# Try loading from defaults
|
|
45
|
+
if 'defaults' in cfg:
|
|
46
|
+
defaults_list = cfg.defaults
|
|
47
|
+
for default_item in defaults_list:
|
|
48
|
+
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
49
|
+
|
|
50
|
+
# Handle dict format: {'series': 'default'} or {'blocks': 'default'}
|
|
51
|
+
if isinstance(default_dict, dict) and config_type in default_dict:
|
|
52
|
+
config_name = default_dict[config_type]
|
|
53
|
+
config_path = root_config_dir / config_type / f'{config_name}.yaml'
|
|
54
|
+
if config_path.exists():
|
|
55
|
+
config_cfg = OmegaConf.load(config_path)
|
|
56
|
+
config_dict = OmegaConf.to_container(config_cfg, resolve=True)
|
|
57
|
+
config_loaded = True
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
# If not loaded from defaults, try direct path
|
|
61
|
+
if not config_loaded:
|
|
62
|
+
config_path = root_config_dir / config_type / 'default.yaml'
|
|
63
|
+
if config_path.exists():
|
|
64
|
+
config_cfg = OmegaConf.load(config_path)
|
|
65
|
+
config_dict = OmegaConf.to_container(config_cfg, resolve=True)
|
|
66
|
+
config_loaded = True
|
|
67
|
+
|
|
68
|
+
return config_dict if config_loaded else None
|
|
18
69
|
|
|
19
70
|
|
|
20
71
|
class ConfigSource(Protocol):
|
|
@@ -55,6 +106,14 @@ class YamlSource:
|
|
|
55
106
|
raise FileNotFoundError(f"Configuration file not found: {configfile}")
|
|
56
107
|
|
|
57
108
|
config_dir = configfile.parent
|
|
109
|
+
# If config file is in a subdirectory (e.g., experiment/), find the root config directory
|
|
110
|
+
# Look for series/ or blocks/ directories to identify config root
|
|
111
|
+
root_config_dir = config_dir
|
|
112
|
+
while root_config_dir.parent != root_config_dir: # Not at filesystem root
|
|
113
|
+
if (root_config_dir / 'series').exists() or (root_config_dir / 'blocks').exists():
|
|
114
|
+
break
|
|
115
|
+
root_config_dir = root_config_dir.parent
|
|
116
|
+
|
|
58
117
|
cfg = OmegaConf.load(configfile)
|
|
59
118
|
cfg_dict = OmegaConf.to_container(cfg, resolve=True)
|
|
60
119
|
|
|
@@ -64,44 +123,28 @@ class YamlSource:
|
|
|
64
123
|
|
|
65
124
|
# Load series from config/series/default.yaml
|
|
66
125
|
series_list = []
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if 'defaults' in cfg:
|
|
71
|
-
defaults_list = cfg.defaults
|
|
72
|
-
for default_item in defaults_list:
|
|
73
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
74
|
-
|
|
75
|
-
# Handle dict format: {'series': 'default'}
|
|
76
|
-
if isinstance(default_dict, dict) and 'series' in default_dict:
|
|
77
|
-
series_config_name = default_dict['series']
|
|
78
|
-
series_config_path = config_dir / 'series' / f'{series_config_name}.yaml'
|
|
79
|
-
if series_config_path.exists():
|
|
80
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
81
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
82
|
-
series_loaded = True
|
|
83
|
-
break
|
|
84
|
-
|
|
85
|
-
# If not loaded from defaults, try direct path
|
|
86
|
-
if not series_loaded:
|
|
87
|
-
series_config_path = config_dir / 'series' / 'default.yaml'
|
|
88
|
-
if series_config_path.exists():
|
|
89
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
90
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
91
|
-
series_loaded = True
|
|
126
|
+
series_dict = _load_config_from_defaults(cfg, root_config_dir, 'series')
|
|
127
|
+
series_loaded = series_dict is not None
|
|
92
128
|
|
|
93
129
|
# Convert series dict to SeriesConfig objects
|
|
94
130
|
if series_loaded and series_dict is not None:
|
|
95
131
|
for series_id, series_data in series_dict.items():
|
|
96
132
|
if isinstance(series_data, dict):
|
|
133
|
+
# Parse release_date if available
|
|
134
|
+
release_date = series_data.get('release', series_data.get('release_date', None))
|
|
135
|
+
if release_date is not None:
|
|
136
|
+
try:
|
|
137
|
+
release_date = int(release_date)
|
|
138
|
+
except (ValueError, TypeError):
|
|
139
|
+
release_date = None
|
|
140
|
+
|
|
97
141
|
series_list.append(SeriesConfig(
|
|
98
142
|
series_id=series_id,
|
|
99
143
|
series_name=series_data.get('series_name', series_id),
|
|
100
144
|
frequency=series_data.get('frequency', 'm'),
|
|
101
145
|
transformation=series_data.get('transformation', 'lin'),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
blocks=series_data.get('blocks', [])
|
|
146
|
+
blocks=series_data.get('blocks', []),
|
|
147
|
+
release_date=release_date
|
|
105
148
|
))
|
|
106
149
|
|
|
107
150
|
# If no series loaded from separate files, try to get from main config
|
|
@@ -118,31 +161,8 @@ class YamlSource:
|
|
|
118
161
|
|
|
119
162
|
# Load blocks from config/blocks/default.yaml
|
|
120
163
|
blocks_dict = {}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if 'defaults' in cfg:
|
|
125
|
-
defaults_list = cfg.defaults
|
|
126
|
-
for default_item in defaults_list:
|
|
127
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
128
|
-
|
|
129
|
-
# Handle dict format: {'blocks': 'default'}
|
|
130
|
-
if isinstance(default_dict, dict) and 'blocks' in default_dict:
|
|
131
|
-
blocks_config_name = default_dict['blocks']
|
|
132
|
-
blocks_config_path = config_dir / 'blocks' / f'{blocks_config_name}.yaml'
|
|
133
|
-
if blocks_config_path.exists():
|
|
134
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
135
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
136
|
-
blocks_loaded = True
|
|
137
|
-
break
|
|
138
|
-
|
|
139
|
-
# If not loaded from defaults, try direct path
|
|
140
|
-
if not blocks_loaded:
|
|
141
|
-
blocks_config_path = config_dir / 'blocks' / 'default.yaml'
|
|
142
|
-
if blocks_config_path.exists():
|
|
143
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
144
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
145
|
-
blocks_loaded = True
|
|
164
|
+
blocks_dict_raw = _load_config_from_defaults(cfg, root_config_dir, 'blocks')
|
|
165
|
+
blocks_loaded = blocks_dict_raw is not None
|
|
146
166
|
|
|
147
167
|
# Convert blocks dict to BlockConfig objects
|
|
148
168
|
if blocks_loaded and blocks_dict_raw is not None:
|
|
@@ -241,9 +261,6 @@ class SpecCSVSource:
|
|
|
241
261
|
|
|
242
262
|
def load(self) -> DFMConfig:
|
|
243
263
|
"""Load series definitions from spec CSV."""
|
|
244
|
-
import pandas as pd
|
|
245
|
-
from .data import _load_config_from_dataframe
|
|
246
|
-
|
|
247
264
|
specfile = Path(self.spec_path)
|
|
248
265
|
if not specfile.exists():
|
|
249
266
|
raise FileNotFoundError(f"Spec file not found: {specfile}")
|
|
@@ -371,6 +388,190 @@ class MergedConfigSource:
|
|
|
371
388
|
)
|
|
372
389
|
|
|
373
390
|
|
|
391
|
+
def from_spec(
|
|
392
|
+
csv_path: Union[str, Path],
|
|
393
|
+
output_dir: Optional[Union[str, Path]] = None,
|
|
394
|
+
series_filename: Optional[str] = None,
|
|
395
|
+
blocks_filename: Optional[str] = None
|
|
396
|
+
) -> Tuple[Path, Path]:
|
|
397
|
+
"""Convert spec CSV file to YAML configuration files.
|
|
398
|
+
|
|
399
|
+
This function reads a spec CSV file and generates two YAML files:
|
|
400
|
+
- config/series/{basename}.yaml - series definitions
|
|
401
|
+
- config/blocks/{basename}.yaml - block definitions
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
csv_path : str or Path
|
|
406
|
+
Path to the spec CSV file
|
|
407
|
+
output_dir : str or Path, optional
|
|
408
|
+
Output directory for YAML files. Defaults to config/ directory relative to CSV.
|
|
409
|
+
series_filename : str, optional
|
|
410
|
+
Custom filename for series YAML (without .yaml extension).
|
|
411
|
+
Defaults to CSV basename.
|
|
412
|
+
blocks_filename : str, optional
|
|
413
|
+
Custom filename for blocks YAML (without .yaml extension).
|
|
414
|
+
Defaults to CSV basename.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
Tuple[Path, Path]
|
|
419
|
+
Paths to generated series YAML and blocks YAML files
|
|
420
|
+
|
|
421
|
+
Examples
|
|
422
|
+
--------
|
|
423
|
+
>>> series_path, blocks_path = from_spec('data/sample_spec.csv')
|
|
424
|
+
>>> # Creates config/series/sample_spec.yaml and config/blocks/sample_spec.yaml
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
csv_path = Path(csv_path)
|
|
428
|
+
if not csv_path.exists():
|
|
429
|
+
raise FileNotFoundError(f"CSV file not found: {csv_path}")
|
|
430
|
+
|
|
431
|
+
# Determine output directory
|
|
432
|
+
if output_dir is None:
|
|
433
|
+
# Default: config/ directory relative to CSV file's parent
|
|
434
|
+
output_dir = csv_path.parent.parent / 'config'
|
|
435
|
+
else:
|
|
436
|
+
output_dir = Path(output_dir)
|
|
437
|
+
|
|
438
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
439
|
+
|
|
440
|
+
# Determine filenames
|
|
441
|
+
csv_basename = csv_path.stem
|
|
442
|
+
series_filename = series_filename or csv_basename
|
|
443
|
+
blocks_filename = blocks_filename or csv_basename
|
|
444
|
+
|
|
445
|
+
# Read CSV and convert to DFMConfig
|
|
446
|
+
try:
|
|
447
|
+
df = pd.read_csv(csv_path)
|
|
448
|
+
except Exception as e:
|
|
449
|
+
raise ValueError(f"Failed to read CSV file {csv_path}: {e}")
|
|
450
|
+
|
|
451
|
+
config = _load_config_from_dataframe(df)
|
|
452
|
+
|
|
453
|
+
# Prepare series YAML content
|
|
454
|
+
series_dir = output_dir / 'series'
|
|
455
|
+
series_dir.mkdir(parents=True, exist_ok=True)
|
|
456
|
+
series_path = series_dir / f'{series_filename}.yaml'
|
|
457
|
+
|
|
458
|
+
series_dict = {}
|
|
459
|
+
# Get block names in order (from config.block_names)
|
|
460
|
+
block_names = config.block_names
|
|
461
|
+
|
|
462
|
+
for series in config.series:
|
|
463
|
+
# Convert blocks from indices to block names if needed
|
|
464
|
+
if isinstance(series.blocks, list) and len(series.blocks) > 0:
|
|
465
|
+
if isinstance(series.blocks[0], int):
|
|
466
|
+
# Blocks are indices - convert to block names
|
|
467
|
+
blocks_names = [block_names[i] for i, val in enumerate(series.blocks) if val != 0]
|
|
468
|
+
else:
|
|
469
|
+
# Blocks are already names
|
|
470
|
+
blocks_names = list(series.blocks)
|
|
471
|
+
else:
|
|
472
|
+
blocks_names = ['Block_Global'] # Default
|
|
473
|
+
|
|
474
|
+
series_entry = {
|
|
475
|
+
'series_name': series.series_name,
|
|
476
|
+
'frequency': series.frequency,
|
|
477
|
+
'transformation': series.transformation,
|
|
478
|
+
'blocks': blocks_names
|
|
479
|
+
}
|
|
480
|
+
if series.release_date is not None:
|
|
481
|
+
series_entry['release'] = series.release_date
|
|
482
|
+
series_dict[series.series_id] = series_entry
|
|
483
|
+
|
|
484
|
+
# Prepare blocks YAML content
|
|
485
|
+
blocks_dir = output_dir / 'blocks'
|
|
486
|
+
blocks_dir.mkdir(parents=True, exist_ok=True)
|
|
487
|
+
blocks_path = blocks_dir / f'{blocks_filename}.yaml'
|
|
488
|
+
|
|
489
|
+
blocks_dict = {}
|
|
490
|
+
for block_name, block_config in config.blocks.items():
|
|
491
|
+
block_entry = {
|
|
492
|
+
'factors': block_config.factors,
|
|
493
|
+
'ar_lag': block_config.ar_lag,
|
|
494
|
+
'clock': block_config.clock
|
|
495
|
+
}
|
|
496
|
+
if block_config.notes:
|
|
497
|
+
block_entry['notes'] = block_config.notes
|
|
498
|
+
blocks_dict[block_name] = block_entry
|
|
499
|
+
|
|
500
|
+
# Write YAML files
|
|
501
|
+
try:
|
|
502
|
+
try:
|
|
503
|
+
import yaml
|
|
504
|
+
yaml_lib = yaml
|
|
505
|
+
yaml_kwargs = {'default_flow_style': False, 'sort_keys': False, 'allow_unicode': True}
|
|
506
|
+
except ImportError:
|
|
507
|
+
try:
|
|
508
|
+
from omegaconf import OmegaConf
|
|
509
|
+
yaml_lib = OmegaConf
|
|
510
|
+
yaml_kwargs = {}
|
|
511
|
+
except ImportError:
|
|
512
|
+
raise ImportError(
|
|
513
|
+
"Either PyYAML or omegaconf is required for YAML generation. "
|
|
514
|
+
"Install with: pip install pyyaml or pip install omegaconf"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Write series YAML
|
|
518
|
+
if hasattr(yaml_lib, 'dump'):
|
|
519
|
+
# PyYAML - add blank lines between series for better readability
|
|
520
|
+
import io
|
|
521
|
+
stream = io.StringIO()
|
|
522
|
+
yaml_lib.dump(series_dict, stream, **yaml_kwargs)
|
|
523
|
+
yaml_content = stream.getvalue()
|
|
524
|
+
|
|
525
|
+
# Add blank line before each series key (except the first one)
|
|
526
|
+
lines = yaml_content.split('\n')
|
|
527
|
+
formatted_lines = []
|
|
528
|
+
for i, line in enumerate(lines):
|
|
529
|
+
# If line starts a new series (starts with a key and colon, not indented)
|
|
530
|
+
if line and not line.startswith(' ') and ':' in line and not line.strip().startswith('#'):
|
|
531
|
+
# Add blank line before this line (except for the first series)
|
|
532
|
+
if i > 0 and formatted_lines and formatted_lines[-1].strip():
|
|
533
|
+
formatted_lines.append('')
|
|
534
|
+
formatted_lines.append(line)
|
|
535
|
+
|
|
536
|
+
with open(series_path, 'w', encoding='utf-8') as f:
|
|
537
|
+
f.write('\n'.join(formatted_lines))
|
|
538
|
+
else:
|
|
539
|
+
# OmegaConf
|
|
540
|
+
series_cfg = OmegaConf.create(series_dict)
|
|
541
|
+
OmegaConf.save(series_cfg, series_path)
|
|
542
|
+
|
|
543
|
+
# Write blocks YAML
|
|
544
|
+
if hasattr(yaml_lib, 'dump'):
|
|
545
|
+
# PyYAML - add blank lines between blocks for better readability
|
|
546
|
+
import io
|
|
547
|
+
stream = io.StringIO()
|
|
548
|
+
yaml_lib.dump(blocks_dict, stream, **yaml_kwargs)
|
|
549
|
+
yaml_content = stream.getvalue()
|
|
550
|
+
|
|
551
|
+
# Add blank line before each block key (except the first one)
|
|
552
|
+
lines = yaml_content.split('\n')
|
|
553
|
+
formatted_lines = []
|
|
554
|
+
for i, line in enumerate(lines):
|
|
555
|
+
# If line starts a new block (starts with a key and colon, not indented)
|
|
556
|
+
if line and not line.startswith(' ') and ':' in line and not line.strip().startswith('#'):
|
|
557
|
+
# Add blank line before this line (except for the first block)
|
|
558
|
+
if i > 0 and formatted_lines and formatted_lines[-1].strip():
|
|
559
|
+
formatted_lines.append('')
|
|
560
|
+
formatted_lines.append(line)
|
|
561
|
+
|
|
562
|
+
with open(blocks_path, 'w', encoding='utf-8') as f:
|
|
563
|
+
f.write('\n'.join(formatted_lines))
|
|
564
|
+
else:
|
|
565
|
+
# OmegaConf
|
|
566
|
+
blocks_cfg = OmegaConf.create(blocks_dict)
|
|
567
|
+
OmegaConf.save(blocks_cfg, blocks_path)
|
|
568
|
+
|
|
569
|
+
except Exception as e:
|
|
570
|
+
raise RuntimeError(f"Failed to write YAML files: {e}")
|
|
571
|
+
|
|
572
|
+
return series_path, blocks_path
|
|
573
|
+
|
|
574
|
+
|
|
374
575
|
def make_config_source(
|
|
375
576
|
source: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
376
577
|
*,
|
|
@@ -503,6 +704,101 @@ def make_config_source(
|
|
|
503
704
|
)
|
|
504
705
|
|
|
505
706
|
|
|
707
|
+
# ============================================================================
|
|
708
|
+
# Internal helper: Load config from DataFrame
|
|
709
|
+
# ============================================================================
|
|
710
|
+
|
|
711
|
+
def _load_config_from_dataframe(df: pd.DataFrame) -> DFMConfig:
|
|
712
|
+
"""Load configuration from DataFrame (internal helper).
|
|
713
|
+
|
|
714
|
+
This function converts a DataFrame with series specifications into a DFMConfig.
|
|
715
|
+
It's used internally by SpecCSVSource and other config loaders.
|
|
716
|
+
|
|
717
|
+
Parameters
|
|
718
|
+
----------
|
|
719
|
+
df : pd.DataFrame
|
|
720
|
+
DataFrame with columns: series_id, series_name, frequency, transformation, blocks
|
|
721
|
+
Optional columns: Release (release date)
|
|
722
|
+
Block_* columns: Binary indicators for block membership
|
|
723
|
+
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
DFMConfig
|
|
727
|
+
Model configuration object
|
|
728
|
+
"""
|
|
729
|
+
series_list = []
|
|
730
|
+
block_names_set = set()
|
|
731
|
+
|
|
732
|
+
for _, row in df.iterrows():
|
|
733
|
+
# Parse blocks from Block_* columns or 'blocks' column
|
|
734
|
+
blocks = []
|
|
735
|
+
|
|
736
|
+
# First, try to find Block_* columns (CSV format)
|
|
737
|
+
block_cols = [col for col in df.columns
|
|
738
|
+
if (col.startswith('Block_') or col.startswith('Block-'))
|
|
739
|
+
and col not in ['blocks']]
|
|
740
|
+
|
|
741
|
+
if block_cols:
|
|
742
|
+
# CSV format: Block_Global, Block_Consumption, etc. columns with 1/0 values
|
|
743
|
+
for block_col in block_cols:
|
|
744
|
+
block_value = row.get(block_col, 0)
|
|
745
|
+
# Handle various types: int, float, numpy int/float, bool, string "1"/"0"
|
|
746
|
+
# Check numpy types first (numpy.int64, numpy.float64, etc.)
|
|
747
|
+
if hasattr(block_value, 'item'):
|
|
748
|
+
# numpy scalar - convert to Python native type
|
|
749
|
+
block_value = block_value.item()
|
|
750
|
+
|
|
751
|
+
if isinstance(block_value, (int, float)) and block_value != 0:
|
|
752
|
+
blocks.append(block_col)
|
|
753
|
+
elif isinstance(block_value, bool) and block_value:
|
|
754
|
+
blocks.append(block_col)
|
|
755
|
+
elif isinstance(block_value, str) and block_value.strip() in ['1', 'True', 'true']:
|
|
756
|
+
blocks.append(block_col)
|
|
757
|
+
else:
|
|
758
|
+
# Fallback: try 'blocks' column (comma-separated string or list)
|
|
759
|
+
blocks_str = row.get('blocks', 'Block_Global')
|
|
760
|
+
if isinstance(blocks_str, str):
|
|
761
|
+
blocks = [b.strip() for b in blocks_str.split(',')]
|
|
762
|
+
elif isinstance(blocks_str, list):
|
|
763
|
+
blocks = blocks_str
|
|
764
|
+
else:
|
|
765
|
+
blocks = ['Block_Global']
|
|
766
|
+
|
|
767
|
+
# Ensure at least Block_Global
|
|
768
|
+
if not blocks:
|
|
769
|
+
blocks = ['Block_Global']
|
|
770
|
+
|
|
771
|
+
# Track block names
|
|
772
|
+
for block in blocks:
|
|
773
|
+
block_names_set.add(block)
|
|
774
|
+
|
|
775
|
+
# Parse release_date if available
|
|
776
|
+
release_date = row.get('Release', None)
|
|
777
|
+
if release_date is not None:
|
|
778
|
+
try:
|
|
779
|
+
release_date = int(release_date)
|
|
780
|
+
except (ValueError, TypeError):
|
|
781
|
+
release_date = None
|
|
782
|
+
|
|
783
|
+
series_list.append(SeriesConfig(
|
|
784
|
+
series_id=row.get('series_id', f"series_{len(series_list)}"),
|
|
785
|
+
series_name=row.get('series_name', row.get('series_id', f"Series {len(series_list)}")),
|
|
786
|
+
frequency=row.get('frequency', 'm'),
|
|
787
|
+
transformation=row.get('transformation', 'lin'),
|
|
788
|
+
blocks=blocks,
|
|
789
|
+
release_date=release_date
|
|
790
|
+
))
|
|
791
|
+
|
|
792
|
+
# Create default blocks if none specified
|
|
793
|
+
block_names = sorted(block_names_set) if block_names_set else ['Block_Global']
|
|
794
|
+
blocks = {}
|
|
795
|
+
for block_name in block_names:
|
|
796
|
+
blocks[block_name] = BlockConfig(factors=1, ar_lag=1, clock='m')
|
|
797
|
+
|
|
798
|
+
# block_names is derived automatically in DFMConfig.__post_init__ from blocks dict
|
|
799
|
+
return DFMConfig(series=series_list, blocks=blocks)
|
|
800
|
+
|
|
801
|
+
|
|
506
802
|
# ============================================================================
|
|
507
803
|
# Hydra ConfigStore Registration (optional - only if Hydra is available)
|
|
508
804
|
# ============================================================================
|
|
@@ -527,10 +823,9 @@ if HYDRA_AVAILABLE and ConfigStore is not None:
|
|
|
527
823
|
series_id: str
|
|
528
824
|
series_name: str
|
|
529
825
|
frequency: str
|
|
530
|
-
units: str
|
|
531
826
|
transformation: str
|
|
532
|
-
category: str
|
|
533
827
|
blocks: ListType[int]
|
|
828
|
+
units: Optional[str] = None # Optional, for display only
|
|
534
829
|
|
|
535
830
|
@schema_dataclass
|
|
536
831
|
class DFMConfigSchema:
|
dfm_python/core/helpers.py
CHANGED
|
@@ -996,3 +996,79 @@ def validate_params(
|
|
|
996
996
|
V_0 = np.eye(V_0.shape[0]) * default_idio_init_covariance
|
|
997
997
|
|
|
998
998
|
return A, Q, R, C, Z_0, V_0
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
# ============================================================================
|
|
1002
|
+
# Legacy config compatibility helpers
|
|
1003
|
+
# ============================================================================
|
|
1004
|
+
|
|
1005
|
+
def _get_frequencies_from_config(config: Any) -> list:
|
|
1006
|
+
"""Get frequencies from config, supporting both new and legacy formats.
|
|
1007
|
+
|
|
1008
|
+
This function provides a unified interface for accessing frequencies
|
|
1009
|
+
from DFMConfig objects, handling both:
|
|
1010
|
+
- New format: config.get_frequencies() method
|
|
1011
|
+
- Legacy format: config.Frequency attribute
|
|
1012
|
+
|
|
1013
|
+
Parameters
|
|
1014
|
+
----------
|
|
1015
|
+
config : Any
|
|
1016
|
+
Configuration object (DFMConfig or legacy format)
|
|
1017
|
+
|
|
1018
|
+
Returns
|
|
1019
|
+
-------
|
|
1020
|
+
list
|
|
1021
|
+
List of frequency codes for each series
|
|
1022
|
+
|
|
1023
|
+
Notes
|
|
1024
|
+
-----
|
|
1025
|
+
- Maintains backward compatibility with legacy configuration formats
|
|
1026
|
+
- Returns empty list if neither format is available
|
|
1027
|
+
"""
|
|
1028
|
+
if hasattr(config, 'get_frequencies'):
|
|
1029
|
+
return config.get_frequencies()
|
|
1030
|
+
elif hasattr(config, 'Frequency'):
|
|
1031
|
+
# Legacy format support: Frequency attribute (for backward compatibility)
|
|
1032
|
+
freq_attr = config.Frequency
|
|
1033
|
+
if hasattr(freq_attr, '__iter__') and not isinstance(freq_attr, str):
|
|
1034
|
+
return list(freq_attr)
|
|
1035
|
+
else:
|
|
1036
|
+
return [freq_attr] if freq_attr is not None else []
|
|
1037
|
+
else:
|
|
1038
|
+
return []
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _get_units_from_config(config: Any, series_index: int) -> Optional[str]:
|
|
1042
|
+
"""Get units for a series from config, supporting both new and legacy formats.
|
|
1043
|
+
|
|
1044
|
+
This function provides a unified interface for accessing units from config,
|
|
1045
|
+
handling both:
|
|
1046
|
+
- New format: config.series[series_index].units
|
|
1047
|
+
- Legacy format: config.UnitsTransformed[series_index]
|
|
1048
|
+
|
|
1049
|
+
Parameters
|
|
1050
|
+
----------
|
|
1051
|
+
config : Any
|
|
1052
|
+
Configuration object (DFMConfig or legacy format)
|
|
1053
|
+
series_index : int
|
|
1054
|
+
Index of the series (0-based)
|
|
1055
|
+
|
|
1056
|
+
Returns
|
|
1057
|
+
-------
|
|
1058
|
+
Optional[str]
|
|
1059
|
+
Units string or None if not available
|
|
1060
|
+
|
|
1061
|
+
Notes
|
|
1062
|
+
-----
|
|
1063
|
+
- Maintains backward compatibility with legacy configuration formats
|
|
1064
|
+
- Returns None if units are not available for the series
|
|
1065
|
+
"""
|
|
1066
|
+
# New format: SeriesConfig.units
|
|
1067
|
+
if hasattr(config, 'series') and series_index < len(config.series):
|
|
1068
|
+
return config.series[series_index].units
|
|
1069
|
+
|
|
1070
|
+
# Legacy format: UnitsTransformed attribute
|
|
1071
|
+
if hasattr(config, 'UnitsTransformed') and series_index < len(config.UnitsTransformed):
|
|
1072
|
+
return config.UnitsTransformed[series_index]
|
|
1073
|
+
|
|
1074
|
+
return None
|
dfm_python/data.py
CHANGED
|
@@ -5,6 +5,7 @@ for Dynamic Factor Model estimation.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import warnings
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import List, Optional, Tuple, Union
|
|
10
11
|
|
|
@@ -408,7 +409,6 @@ def load_data(datafile: Union[str, Path], config: DFMConfig,
|
|
|
408
409
|
if len(warnings_list) > 5:
|
|
409
410
|
logger.warning(f"... and {len(warnings_list) - 5} more series with T < N")
|
|
410
411
|
|
|
411
|
-
import warnings
|
|
412
412
|
warnings.warn(
|
|
413
413
|
f"Insufficient data: {len(warnings_list)} series have T < N (time periods < number of series). "
|
|
414
414
|
f"This may cause numerical issues. Suggested fix: increase sample size or reduce number of series. "
|
|
@@ -434,7 +434,6 @@ def load_data(datafile: Union[str, Path], config: DFMConfig,
|
|
|
434
434
|
if len(extreme_missing_series) > 5:
|
|
435
435
|
logger.warning(f"... and {len(extreme_missing_series) - 5} more series with >90% missing data")
|
|
436
436
|
|
|
437
|
-
import warnings
|
|
438
437
|
warnings.warn(
|
|
439
438
|
f"Extreme missing data detected: {len(extreme_missing_series)} series have >90% missing values. "
|
|
440
439
|
f"Estimation may be unreliable. Consider removing these series or increasing data coverage. "
|
|
@@ -446,55 +445,6 @@ def load_data(datafile: Union[str, Path], config: DFMConfig,
|
|
|
446
445
|
return X, Time, Z
|
|
447
446
|
|
|
448
447
|
|
|
449
|
-
def _load_config_from_dataframe(df: pd.DataFrame) -> DFMConfig:
|
|
450
|
-
"""Load configuration from DataFrame (internal helper).
|
|
451
|
-
|
|
452
|
-
This function converts a DataFrame with series specifications into a DFMConfig.
|
|
453
|
-
It's used internally by SpecCSVSource and other config loaders.
|
|
454
|
-
|
|
455
|
-
Parameters
|
|
456
|
-
----------
|
|
457
|
-
df : pd.DataFrame
|
|
458
|
-
DataFrame with columns: series_id, series_name, frequency, transformation, blocks
|
|
459
|
-
|
|
460
|
-
Returns
|
|
461
|
-
-------
|
|
462
|
-
DFMConfig
|
|
463
|
-
Model configuration object
|
|
464
|
-
"""
|
|
465
|
-
series_list = []
|
|
466
|
-
block_names_set = set()
|
|
467
|
-
|
|
468
|
-
for _, row in df.iterrows():
|
|
469
|
-
# Parse blocks (can be comma-separated string or list)
|
|
470
|
-
blocks_str = row.get('blocks', 'Block_Global')
|
|
471
|
-
if isinstance(blocks_str, str):
|
|
472
|
-
blocks = [b.strip() for b in blocks_str.split(',')]
|
|
473
|
-
else:
|
|
474
|
-
blocks = blocks_str if isinstance(blocks_str, list) else ['Block_Global']
|
|
475
|
-
|
|
476
|
-
# Track block names
|
|
477
|
-
for block in blocks:
|
|
478
|
-
block_names_set.add(block)
|
|
479
|
-
|
|
480
|
-
series_list.append(SeriesConfig(
|
|
481
|
-
series_id=row.get('series_id', f"series_{len(series_list)}"),
|
|
482
|
-
series_name=row.get('series_name', row.get('series_id', f"Series {len(series_list)}")),
|
|
483
|
-
frequency=row.get('frequency', 'm'),
|
|
484
|
-
transformation=row.get('transformation', 'lin'),
|
|
485
|
-
blocks=blocks
|
|
486
|
-
))
|
|
487
|
-
|
|
488
|
-
# Create default blocks if none specified
|
|
489
|
-
block_names = sorted(block_names_set) if block_names_set else ['Block_Global']
|
|
490
|
-
blocks = {}
|
|
491
|
-
for block_name in block_names:
|
|
492
|
-
blocks[block_name] = BlockConfig(factors=1, ar_lag=1, clock='m')
|
|
493
|
-
|
|
494
|
-
# block_names is derived automatically in DFMConfig.__post_init__ from blocks dict
|
|
495
|
-
return DFMConfig(series=series_list, blocks=blocks)
|
|
496
|
-
|
|
497
|
-
|
|
498
448
|
def rem_nans_spline(X: np.ndarray, method: int = 2, k: int = 3) -> Tuple[np.ndarray, np.ndarray]:
|
|
499
449
|
"""Treat NaNs in dataset for DFM estimation using standard interpolation methods.
|
|
500
450
|
|
dfm_python/dfm.py
CHANGED
|
@@ -301,11 +301,14 @@ class EMAlgorithmParams:
|
|
|
301
301
|
# Core functions are imported directly from core modules - no proxy functions needed
|
|
302
302
|
|
|
303
303
|
|
|
304
|
-
class
|
|
304
|
+
class DFMCore:
|
|
305
305
|
"""Core Dynamic Factor Model class.
|
|
306
306
|
|
|
307
307
|
This class provides the main DFM estimation functionality. The core algorithm
|
|
308
308
|
is implemented in the `fit()` method, which performs EM estimation.
|
|
309
|
+
|
|
310
|
+
Note: This is the core implementation class. For high-level API with
|
|
311
|
+
convenience methods, use the `DFM` class from `api.py` which extends this class.
|
|
309
312
|
"""
|
|
310
313
|
|
|
311
314
|
def __init__(self):
|
dfm_python/news.py
CHANGED
|
@@ -18,6 +18,7 @@ import numpy as np
|
|
|
18
18
|
from scipy.linalg import pinv, inv
|
|
19
19
|
from typing import Tuple, Optional, Dict, Union, List, Any, Callable
|
|
20
20
|
import pandas as pd
|
|
21
|
+
import warnings
|
|
21
22
|
import logging
|
|
22
23
|
|
|
23
24
|
from .kalman import skf, fis, miss_data
|
|
@@ -61,7 +62,6 @@ def _check_config_consistency(saved_config: Any, current_config: DFMConfig) -> N
|
|
|
61
62
|
else:
|
|
62
63
|
current_ids = []
|
|
63
64
|
if saved_ids != current_ids:
|
|
64
|
-
import warnings
|
|
65
65
|
warnings.warn(
|
|
66
66
|
"Config used in estimation differs from current config. "
|
|
67
67
|
"Results may be inconsistent. Consider re-estimating.",
|
|
@@ -552,12 +552,9 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
|
|
|
552
552
|
# Validate period format
|
|
553
553
|
try:
|
|
554
554
|
i_series = series_ids.index(series)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
frequencies = config.Frequency
|
|
559
|
-
else:
|
|
560
|
-
frequencies = []
|
|
555
|
+
# Get frequencies using unified helper function (supports both new and legacy formats)
|
|
556
|
+
from .core.helpers import _get_frequencies_from_config
|
|
557
|
+
frequencies = _get_frequencies_from_config(config)
|
|
561
558
|
freq = frequencies[i_series] if i_series < len(frequencies) else 'm'
|
|
562
559
|
if freq == 'm':
|
|
563
560
|
# Format: YYYYmMM (e.g., "2016m12")
|
|
@@ -650,12 +647,9 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
|
|
|
650
647
|
else:
|
|
651
648
|
series_names = []
|
|
652
649
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
frequencies = config.Frequency
|
|
657
|
-
else:
|
|
658
|
-
frequencies = []
|
|
650
|
+
# Get frequencies using unified helper function (supports both new and legacy formats)
|
|
651
|
+
from .core.helpers import _get_frequencies_from_config
|
|
652
|
+
frequencies = _get_frequencies_from_config(config)
|
|
659
653
|
series_name = series_names[i_series] if i_series < len(series_names) else series
|
|
660
654
|
freq = frequencies[i_series] if i_series < len(frequencies) else 'm'
|
|
661
655
|
|
|
@@ -705,14 +699,13 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
|
|
|
705
699
|
else:
|
|
706
700
|
period_str = pd.Timestamp(Time[t_nowcast]).strftime('%Y-%m')
|
|
707
701
|
|
|
708
|
-
# Get units
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
else
|
|
714
|
-
|
|
715
|
-
print(f'Nowcast for {series_name} ({units}), {period_str}')
|
|
702
|
+
# Get units using unified helper function (supports both new and legacy formats)
|
|
703
|
+
from .core.helpers import _get_units_from_config
|
|
704
|
+
units = _get_units_from_config(config, i_series)
|
|
705
|
+
|
|
706
|
+
# Format units for display
|
|
707
|
+
units_str = f" ({units})" if units else ""
|
|
708
|
+
print(f'Nowcast for {series_name}{units_str}, {period_str}')
|
|
716
709
|
|
|
717
710
|
if len(forecast) == 0 or np.all(np.isnan(forecast)):
|
|
718
711
|
print('\n No forecast was made.\n')
|
|
@@ -774,7 +767,6 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
|
|
|
774
767
|
Res=Res
|
|
775
768
|
)
|
|
776
769
|
except Exception as e:
|
|
777
|
-
import warnings
|
|
778
770
|
warnings.warn(
|
|
779
771
|
f"save_callback failed: {e}. Continuing without saving.",
|
|
780
772
|
UserWarning
|
dfm_python/utils.py
CHANGED
|
@@ -224,13 +224,10 @@ def get_aggregation_structure(
|
|
|
224
224
|
>>> print(agg_info['tent_weights'])
|
|
225
225
|
{'q': array([1, 2, 3, 2, 1]), 'sa': array([1, 2, 3, 4, 3, 2, 1])}
|
|
226
226
|
"""
|
|
227
|
-
# Get frequencies using
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
frequencies = set(config.Frequency)
|
|
232
|
-
else:
|
|
233
|
-
frequencies = set()
|
|
227
|
+
# Get frequencies using unified helper function (supports both new and legacy formats)
|
|
228
|
+
from .core.helpers import _get_frequencies_from_config
|
|
229
|
+
frequencies_list = _get_frequencies_from_config(config)
|
|
230
|
+
frequencies = set(frequencies_list)
|
|
234
231
|
structures = {}
|
|
235
232
|
tent_weights = {}
|
|
236
233
|
n_periods_map = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
|
@@ -25,18 +25,12 @@ Requires-Dist: numpy>=1.24.0
|
|
|
25
25
|
Requires-Dist: pandas>=2.0.0
|
|
26
26
|
Requires-Dist: scipy>=1.10.0
|
|
27
27
|
Provides-Extra: all
|
|
28
|
-
Requires-Dist: httpx>=0.24.0; extra == 'all'
|
|
29
28
|
Requires-Dist: hydra-core>=1.3.0; extra == 'all'
|
|
30
29
|
Requires-Dist: omegaconf>=2.3.0; extra == 'all'
|
|
31
|
-
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
|
|
32
|
-
Requires-Dist: pydantic-settings>=2.11.0; extra == 'all'
|
|
33
30
|
Requires-Dist: pytest>=7.0.0; extra == 'all'
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Provides-Extra:
|
|
36
|
-
Requires-Dist:
|
|
37
|
-
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'database'
|
|
38
|
-
Requires-Dist: pydantic-settings>=2.11.0; extra == 'database'
|
|
39
|
-
Requires-Dist: supabase>=2.0.0; extra == 'database'
|
|
31
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'all'
|
|
32
|
+
Provides-Extra: db
|
|
33
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == 'db'
|
|
40
34
|
Provides-Extra: dev
|
|
41
35
|
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
42
36
|
Provides-Extra: hydra
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
dfm_python/__init__.py,sha256=ygVrK-7Q8VPyOIPMib15hVu5AFQO-ILSCgFR8fsXcyU,4844
|
|
2
|
+
dfm_python/api.py,sha256=d18qOn1XG5F7gTuoFWxQ_6xhGff9AjGbym5Ji3m6Qfw,16989
|
|
3
|
+
dfm_python/config.py,sha256=-SkouUFq38-rQKfyFcg1nI4_YkNbwGCwogNQiyOOT8A,47193
|
|
4
|
+
dfm_python/config_sources.py,sha256=0e5_yOWUlMZA7bEgSAJF_ZFqr-vKTlhsqpl84GB2ySY,33193
|
|
5
|
+
dfm_python/data.py,sha256=22M9zn2fS1yaZvJe94k2BgOkH9LFwVKw_WltnVu-qFs,20380
|
|
6
|
+
dfm_python/dfm.py,sha256=ANG_-7qCizp8VS5tdR_aJoBtPhvRR4QGJjvntEpA6KY,35109
|
|
7
|
+
dfm_python/kalman.py,sha256=EXhloLbpzy7GKG9KFrSZt9pRp0y3CZyvycPVbvUWxq8,18074
|
|
8
|
+
dfm_python/news.py,sha256=abt6X7q-9b6TL0aRcCjhLUGuBOEnjo-pfK4kXgkLo8A,32503
|
|
9
|
+
dfm_python/utils.py,sha256=cRb3afQzpbh_RqoScaq25jH08bn1qImz_MADdFWRteY,15674
|
|
10
|
+
dfm_python/core/__init__.py,sha256=oDrhAoc1W_ODohgufQ-gN0uri8-vZI0EXsF0dysth_A,331
|
|
11
|
+
dfm_python/core/diagnostics.py,sha256=1ninWcQlWhJmfUZq9kky7IQoRf_6fI_IlVzzSb7apRQ,28085
|
|
12
|
+
dfm_python/core/em.py,sha256=jbtsFHynMDtblZTtlzgodX04e86BBB9hPn20MMnV3w0,72705
|
|
13
|
+
dfm_python/core/helpers.py,sha256=NMM04OdSbrG73QDciuue8x0XvxleLYWr-OBzvG1wQ8c,34109
|
|
14
|
+
dfm_python/core/numeric.py,sha256=IcIwcKtMhIoJ5-7Mz6UNYwTUtEa5W_ZMnQtF87v0SSc,26431
|
|
15
|
+
dfm_python-0.2.9.dist-info/METADATA,sha256=0sOxEYOl7hJhj3vGe9CTLoj8D59Q1bbuqvkOjnresfQ,28264
|
|
16
|
+
dfm_python-0.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
dfm_python-0.2.9.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
|
|
18
|
+
dfm_python-0.2.9.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
dfm_python/__init__.py,sha256=oz0p9JreUg6D9SfU2vUAFMRwf7JwHaa3dQDy87YHtIw,5589
|
|
2
|
-
dfm_python/api.py,sha256=-mmV7aaXOAqVW9HPhTEut7T6wN1EyW0rS3J0y-z-MRs,16221
|
|
3
|
-
dfm_python/config.py,sha256=bBiKfoETohNVJ3brRbY1330C1nQROybCR3co9iagIKs,46978
|
|
4
|
-
dfm_python/config_sources.py,sha256=6LTK7k5WJpnayMmtxtYwZ9SQi_hGXoz-Vg4sRGTabI0,21798
|
|
5
|
-
dfm_python/data.py,sha256=GpvI4dZ48-uUI_rbsQhCq2WcbhyHJ-foER7-baTyHo8,22223
|
|
6
|
-
dfm_python/dfm.py,sha256=9k_0RyxtsbZZrfqhAKmxX4CQy41CCoPolbe_oi8a_K8,34942
|
|
7
|
-
dfm_python/kalman.py,sha256=EXhloLbpzy7GKG9KFrSZt9pRp0y3CZyvycPVbvUWxq8,18074
|
|
8
|
-
dfm_python/news.py,sha256=iXbJnsvH5FgKToPzKnqIpTosrZu-QHXBNHjsqwMYrHo,32583
|
|
9
|
-
dfm_python/utils.py,sha256=qOAJm2KG71yP2a29iJVDGOC-ZUS_HPL5oAo16EgNXA4,15709
|
|
10
|
-
dfm_python/core/__init__.py,sha256=oDrhAoc1W_ODohgufQ-gN0uri8-vZI0EXsF0dysth_A,331
|
|
11
|
-
dfm_python/core/diagnostics.py,sha256=1ninWcQlWhJmfUZq9kky7IQoRf_6fI_IlVzzSb7apRQ,28085
|
|
12
|
-
dfm_python/core/em.py,sha256=jbtsFHynMDtblZTtlzgodX04e86BBB9hPn20MMnV3w0,72705
|
|
13
|
-
dfm_python/core/helpers.py,sha256=XJvWWEZlUqozBfFNcENKgbOOFkwlm-tGwO-u0LnAf-E,31541
|
|
14
|
-
dfm_python/core/numeric.py,sha256=IcIwcKtMhIoJ5-7Mz6UNYwTUtEa5W_ZMnQtF87v0SSc,26431
|
|
15
|
-
dfm_python-0.2.8.dist-info/METADATA,sha256=wY69TZa4fK9I4zY4t7hQUJksWuMCyxtGUS0Nr_C3SPk,28599
|
|
16
|
-
dfm_python-0.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
-
dfm_python-0.2.8.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
|
|
18
|
-
dfm_python-0.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|