dfm-python 0.1.8__py3-none-any.whl → 0.2.1__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 +69 -31
- dfm_python/api.py +420 -0
- dfm_python/config.py +763 -149
- dfm_python/core/__init__.py +59 -0
- dfm_python/core/diagnostics.py +274 -0
- dfm_python/core/em.py +1022 -0
- dfm_python/core/grouping.py +54 -0
- dfm_python/core/helpers.py +56 -0
- dfm_python/core/numeric.py +346 -0
- dfm_python/core/results.py +70 -0
- dfm_python/data_loader.py +187 -225
- dfm_python/dfm.py +497 -3435
- dfm_python/kalman.py +22 -0
- dfm_python/news.py +62 -12
- dfm_python/utils/aggregation.py +7 -1
- dfm_python/utils/data_utils.py +25 -4
- dfm_python-0.2.1.dist-info/METADATA +310 -0
- dfm_python-0.2.1.dist-info/RECORD +21 -0
- dfm_python/dfm_api.py +0 -328
- dfm_python-0.1.8.dist-info/METADATA +0 -343
- dfm_python-0.1.8.dist-info/RECORD +0 -14
- {dfm_python-0.1.8.dist-info → dfm_python-0.2.1.dist-info}/WHEEL +0 -0
- {dfm_python-0.1.8.dist-info → dfm_python-0.2.1.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -27,11 +27,14 @@ Example (High-level API - Recommended):
|
|
|
27
27
|
>>> import dfm_python as dfm
|
|
28
28
|
>>> dfm.load_config('config/default.yaml')
|
|
29
29
|
>>> dfm.load_data('data/sample_data.csv')
|
|
30
|
-
>>> dfm.train()
|
|
31
|
-
>>>
|
|
30
|
+
>>> dfm.train(max_iter=1)
|
|
31
|
+
>>> Xf, Zf = dfm.predict(horizon=6)
|
|
32
|
+
>>> dfm.plot(kind='factor', factor_index=0, forecast_horizon=6, save_path='outputs/factor_forecast.png')
|
|
33
|
+
>>> factors = dfm.get_result().Z # Access factors via result
|
|
32
34
|
|
|
33
35
|
Example (Low-level API - For advanced usage):
|
|
34
|
-
>>> from dfm_python import
|
|
36
|
+
>>> from dfm_python import DFM, DFMConfig, SeriesConfig
|
|
37
|
+
>>> from dfm_python.data_loader import load_data
|
|
35
38
|
>>> # Option 1: Load from YAML
|
|
36
39
|
>>> config = load_config('config.yaml')
|
|
37
40
|
>>> # Option 2: Create directly
|
|
@@ -40,41 +43,61 @@ Example (Low-level API - For advanced usage):
|
|
|
40
43
|
... block_names=['Global']
|
|
41
44
|
... )
|
|
42
45
|
>>> X, Time, _ = load_data('data.csv', config) # or use database adapter
|
|
43
|
-
>>>
|
|
46
|
+
>>> model = DFM()
|
|
47
|
+
>>> result = model.fit(X, config)
|
|
44
48
|
>>> factors = result.Z # Extract estimated factors
|
|
45
49
|
|
|
46
|
-
For detailed documentation, see the README.md file.
|
|
50
|
+
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
47
51
|
"""
|
|
48
52
|
|
|
49
|
-
__version__ = "0.1
|
|
50
|
-
|
|
51
|
-
from .config import DFMConfig, SeriesConfig, BlockConfig
|
|
52
|
-
from .
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
__version__ = "0.2.1"
|
|
54
|
+
|
|
55
|
+
from .config import DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME
|
|
56
|
+
from .config import (
|
|
57
|
+
ConfigSource,
|
|
58
|
+
YamlSource,
|
|
59
|
+
DictSource,
|
|
60
|
+
SpecCSVSource,
|
|
61
|
+
HydraSource,
|
|
62
|
+
MergedConfigSource,
|
|
63
|
+
make_config_source,
|
|
57
64
|
)
|
|
58
|
-
from .
|
|
65
|
+
from .data_loader import transform_data
|
|
66
|
+
from .dfm import DFMResult, calculate_rmse, diagnose_series, print_series_diagnosis
|
|
67
|
+
from .dfm import DFM as _DFMCore # Core DFM class from dfm.py
|
|
59
68
|
from .kalman import run_kf, skf, fis, miss_data
|
|
60
69
|
from .news import update_nowcast, news_dfm, para_const
|
|
61
70
|
|
|
62
|
-
# Import high-level API
|
|
63
|
-
from .
|
|
71
|
+
# Import high-level API (extends core DFM with convenience methods)
|
|
72
|
+
from .api import DFM, _dfm_instance, from_yaml, from_spec, from_spec_df, from_dict
|
|
64
73
|
|
|
65
74
|
# Module-level API: expose singleton instance methods and properties
|
|
66
75
|
# This allows: import dfm_python as dfm; dfm.load_config(...); dfm.result
|
|
67
|
-
def load_config(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
def load_config(
|
|
77
|
+
source=None,
|
|
78
|
+
*,
|
|
79
|
+
yaml=None,
|
|
80
|
+
mapping=None,
|
|
81
|
+
spec=None,
|
|
82
|
+
hydra=None,
|
|
83
|
+
base=None,
|
|
84
|
+
override=None,
|
|
85
|
+
config=None,
|
|
86
|
+
):
|
|
87
|
+
"""Load configuration (module-level convenience function).
|
|
88
|
+
|
|
89
|
+
See DFM.load_config() for parameter documentation.
|
|
90
|
+
"""
|
|
91
|
+
return _dfm_instance.load_config(
|
|
92
|
+
source=source,
|
|
93
|
+
yaml=yaml,
|
|
94
|
+
mapping=mapping,
|
|
95
|
+
spec=spec,
|
|
96
|
+
hydra=hydra,
|
|
97
|
+
base=base,
|
|
98
|
+
override=override,
|
|
99
|
+
config=config,
|
|
100
|
+
)
|
|
78
101
|
|
|
79
102
|
def load_data(data_path=None, data=None, **kwargs):
|
|
80
103
|
"""Load data (module-level convenience function)."""
|
|
@@ -84,6 +107,14 @@ def train(threshold=None, max_iter=None, **kwargs):
|
|
|
84
107
|
"""Train the model (module-level convenience function)."""
|
|
85
108
|
return _dfm_instance.train(threshold=threshold, max_iter=max_iter, **kwargs)
|
|
86
109
|
|
|
110
|
+
def predict(horizon=12, **kwargs):
|
|
111
|
+
"""Forecast using the trained model (module-level convenience function)."""
|
|
112
|
+
return _dfm_instance.predict(horizon=horizon, **kwargs)
|
|
113
|
+
|
|
114
|
+
def plot(**kwargs):
|
|
115
|
+
"""Plot common visualizations (module-level convenience function)."""
|
|
116
|
+
return _dfm_instance.plot(**kwargs)
|
|
117
|
+
|
|
87
118
|
def reset():
|
|
88
119
|
"""Reset state (module-level convenience function)."""
|
|
89
120
|
return _dfm_instance.reset()
|
|
@@ -132,14 +163,21 @@ def __getattr__(name):
|
|
|
132
163
|
|
|
133
164
|
__all__ = [
|
|
134
165
|
# Core classes
|
|
135
|
-
'DFMConfig', 'SeriesConfig', 'BlockConfig', 'DFM',
|
|
166
|
+
'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params', 'DFM',
|
|
167
|
+
# Constants
|
|
168
|
+
'DEFAULT_GLOBAL_BLOCK_NAME',
|
|
169
|
+
# Config sources
|
|
170
|
+
'ConfigSource', 'YamlSource', 'DictSource', 'SpecCSVSource', 'HydraSource',
|
|
171
|
+
'MergedConfigSource', 'make_config_source',
|
|
136
172
|
# High-level API (module-level - recommended)
|
|
137
|
-
'load_config',
|
|
138
|
-
'load_data', 'train', 'reset',
|
|
173
|
+
'load_config',
|
|
174
|
+
'load_data', 'train', 'predict', 'plot', 'reset',
|
|
139
175
|
'get_config', 'get_data', 'get_time', 'get_result', 'get_original_data',
|
|
176
|
+
# Convenience constructors (cleaner API)
|
|
177
|
+
'from_yaml', 'from_spec', 'from_spec_df', 'from_dict',
|
|
140
178
|
# Low-level API (functional interface - advanced usage)
|
|
141
179
|
'transform_data',
|
|
142
|
-
'DFMResult', '
|
|
180
|
+
'DFMResult', 'calculate_rmse', 'diagnose_series', 'print_series_diagnosis',
|
|
143
181
|
'run_kf', 'skf', 'fis', 'miss_data',
|
|
144
182
|
'update_nowcast', 'news_dfm', 'para_const',
|
|
145
183
|
]
|
dfm_python/api.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""High-level API for Dynamic Factor Model (convenience layer).
|
|
2
|
+
|
|
3
|
+
This module provides an object-oriented interface for DFM estimation
|
|
4
|
+
on top of the core `DFM` in `dfm.py`. Use this for a simpler, more
|
|
5
|
+
intuitive workflow:
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> import dfm_python as dfm
|
|
9
|
+
>>> dfm.load_config('config/default.yaml')
|
|
10
|
+
>>> dfm.load_data('data/sample_data.csv')
|
|
11
|
+
>>> dfm.train(max_iter=1)
|
|
12
|
+
>>> # Forecast and plot
|
|
13
|
+
>>> Xf, Zf = dfm.predict(horizon=6)
|
|
14
|
+
>>> dfm.plot(kind='factor', factor_index=0, forecast_horizon=6, save_path='outputs/factor_forecast.png')
|
|
15
|
+
>>> # Access results
|
|
16
|
+
>>> result = dfm.get_result()
|
|
17
|
+
>>> factors = result.Z
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Optional, Union, Dict, Any, Tuple
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
from .config import DFMConfig, Params
|
|
26
|
+
from .data_loader import load_data as _load_data
|
|
27
|
+
from .config import (
|
|
28
|
+
make_config_source,
|
|
29
|
+
ConfigSource,
|
|
30
|
+
MergedConfigSource,
|
|
31
|
+
)
|
|
32
|
+
from .dfm import DFM as _DFMCore, DFMResult
|
|
33
|
+
from .core.helpers import safe_get_method, safe_get_attr
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DFM(_DFMCore):
|
|
37
|
+
"""High-level API for Dynamic Factor Model estimation.
|
|
38
|
+
|
|
39
|
+
This class provides a simple, object-oriented interface for DFM operations.
|
|
40
|
+
It maintains state (config, data, results) and provides convenient methods
|
|
41
|
+
for loading configuration, data, training, prediction, and plotting.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> import dfm_python as dfm
|
|
45
|
+
>>> dfm.load_config('config/default.yaml')
|
|
46
|
+
>>> dfm.load_data('data/sample_data.csv')
|
|
47
|
+
>>> dfm.train(max_iter=1)
|
|
48
|
+
>>> Xf, Zf = dfm.predict(horizon=6)
|
|
49
|
+
>>> dfm.plot(kind='factor', factor_index=0, forecast_horizon=6, save_path='outputs/factor_forecast.png')
|
|
50
|
+
>>> print(dfm.get_result().converged)
|
|
51
|
+
>>> factors = dfm.get_result().Z
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
"""Initialize DFM instance with empty state."""
|
|
56
|
+
super().__init__()
|
|
57
|
+
self._time: Optional[np.ndarray] = None
|
|
58
|
+
self._original_data: Optional[np.ndarray] = None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def data(self) -> Optional[np.ndarray]:
|
|
62
|
+
"""Get current data matrix (T x N)."""
|
|
63
|
+
return self._data
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def time(self) -> Optional[np.ndarray]:
|
|
67
|
+
"""Get time index for data."""
|
|
68
|
+
return self._time
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def original_data(self) -> Optional[np.ndarray]:
|
|
72
|
+
"""Get original (untransformed) data matrix."""
|
|
73
|
+
return self._original_data
|
|
74
|
+
|
|
75
|
+
def load_config(
|
|
76
|
+
self,
|
|
77
|
+
source: Optional[Union[str, Path, Dict[str, Any], DFMConfig, ConfigSource, Any]] = None,
|
|
78
|
+
*,
|
|
79
|
+
yaml: Optional[Union[str, Path]] = None,
|
|
80
|
+
mapping: Optional[Union[Dict[str, Any], Any]] = None,
|
|
81
|
+
spec: Optional[Union[str, Path]] = None,
|
|
82
|
+
hydra: Optional[Union[Dict[str, Any], 'DictConfig']] = None,
|
|
83
|
+
base: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
84
|
+
override: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
85
|
+
config: Optional[Any] = None,
|
|
86
|
+
) -> 'DFM':
|
|
87
|
+
"""Load configuration from various sources.
|
|
88
|
+
|
|
89
|
+
Unified interface for YAML files, dictionaries, spec CSV, or Hydra configs.
|
|
90
|
+
Supports merging configurations (e.g., YAML base + Spec CSV series).
|
|
91
|
+
"""
|
|
92
|
+
# Handle direct DFMConfig
|
|
93
|
+
if isinstance(source, DFMConfig):
|
|
94
|
+
self._config = source
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
# Support (mapping + spec) convenience: treat as base+override
|
|
98
|
+
if (mapping is not None or config is not None) and spec is not None:
|
|
99
|
+
base_source = make_config_source(mapping=mapping if mapping is not None else config)
|
|
100
|
+
override_source = make_config_source(spec=spec)
|
|
101
|
+
merged_source = MergedConfigSource(base_source, override_source)
|
|
102
|
+
self._config = merged_source.load()
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
# Handle merging
|
|
106
|
+
if base is not None or override is not None:
|
|
107
|
+
if base is None or override is None:
|
|
108
|
+
raise ValueError("Both 'base' and 'override' must be provided for merging.")
|
|
109
|
+
base_source = make_config_source(base)
|
|
110
|
+
override_source = make_config_source(override)
|
|
111
|
+
merged_source = MergedConfigSource(base_source, override_source)
|
|
112
|
+
self._config = merged_source.load()
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
# Create appropriate source adapter
|
|
116
|
+
source_adapter = make_config_source(
|
|
117
|
+
source=source,
|
|
118
|
+
yaml=yaml,
|
|
119
|
+
mapping=mapping if mapping is not None else config,
|
|
120
|
+
spec=spec,
|
|
121
|
+
hydra=hydra,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self._config = source_adapter.load()
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def load_data(self,
|
|
128
|
+
data_path: Optional[Union[str, Path]] = None,
|
|
129
|
+
data: Optional[np.ndarray] = None,
|
|
130
|
+
**kwargs) -> 'DFM':
|
|
131
|
+
"""Load data from file or use provided array."""
|
|
132
|
+
if self._config is None:
|
|
133
|
+
raise ValueError("Configuration must be loaded before loading data. "
|
|
134
|
+
"Call load_config() first.")
|
|
135
|
+
|
|
136
|
+
if data_path is not None:
|
|
137
|
+
self._data, self._time, self._original_data = _load_data(
|
|
138
|
+
data_path, self._config, **kwargs
|
|
139
|
+
)
|
|
140
|
+
elif data is not None:
|
|
141
|
+
# If data is provided directly, we still need a time index
|
|
142
|
+
self._data = data
|
|
143
|
+
if 'time' in kwargs:
|
|
144
|
+
self._time = kwargs['time']
|
|
145
|
+
else:
|
|
146
|
+
# Generate default monthly-end index
|
|
147
|
+
self._time = pd.date_range(
|
|
148
|
+
start='2000-01-01',
|
|
149
|
+
periods=len(data),
|
|
150
|
+
freq='ME'
|
|
151
|
+
)
|
|
152
|
+
self._original_data = data
|
|
153
|
+
else:
|
|
154
|
+
raise ValueError("Either data_path or data must be provided.")
|
|
155
|
+
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
def train(self,
|
|
159
|
+
threshold: Optional[float] = None,
|
|
160
|
+
max_iter: Optional[int] = None,
|
|
161
|
+
**kwargs) -> 'DFM':
|
|
162
|
+
"""Train the DFM model using EM algorithm."""
|
|
163
|
+
if self._config is None:
|
|
164
|
+
raise ValueError("Configuration must be loaded before training. "
|
|
165
|
+
"Call load_config() first.")
|
|
166
|
+
if self._data is None:
|
|
167
|
+
raise ValueError("Data must be loaded before training. "
|
|
168
|
+
"Call load_data() first.")
|
|
169
|
+
|
|
170
|
+
self._result = self.fit(
|
|
171
|
+
self._data,
|
|
172
|
+
self._config,
|
|
173
|
+
threshold=threshold,
|
|
174
|
+
max_iter=max_iter,
|
|
175
|
+
**kwargs
|
|
176
|
+
)
|
|
177
|
+
# Attach metadata for convenient OOP access
|
|
178
|
+
if self._time is not None:
|
|
179
|
+
self._result.time_index = self._time
|
|
180
|
+
series_ids = safe_get_method(self._config, 'get_series_ids')
|
|
181
|
+
if series_ids is not None:
|
|
182
|
+
self._result.series_ids = series_ids
|
|
183
|
+
block_names = safe_get_attr(self._config, 'block_names')
|
|
184
|
+
if block_names is not None:
|
|
185
|
+
self._result.block_names = block_names
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def reset(self) -> 'DFM':
|
|
189
|
+
"""Reset all state (config, data, results)."""
|
|
190
|
+
super().__init__()
|
|
191
|
+
self._time = None
|
|
192
|
+
self._original_data = None
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------
|
|
196
|
+
# Inference API
|
|
197
|
+
# ---------------------------------------------------------------------
|
|
198
|
+
def predict(self, horizon: int = 12, *, return_series: bool = True, return_factors: bool = True) -> Union[Tuple[np.ndarray, np.ndarray], np.ndarray]:
|
|
199
|
+
"""Forecast series and/or factors for a given horizon using the trained model.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
horizon : int
|
|
204
|
+
Number of periods ahead to forecast (default: 12)
|
|
205
|
+
return_series : bool
|
|
206
|
+
Whether to return forecasted series (default: True)
|
|
207
|
+
return_factors : bool
|
|
208
|
+
Whether to return forecasted factors (default: True)
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
If both return_series and return_factors are True:
|
|
213
|
+
Tuple[np.ndarray, np.ndarray]
|
|
214
|
+
(X_forecast, Z_forecast) where X_forecast is (horizon x N) and Z_forecast is (horizon x m)
|
|
215
|
+
If only return_series is True:
|
|
216
|
+
np.ndarray
|
|
217
|
+
X_forecast (horizon x N)
|
|
218
|
+
If only return_factors is True:
|
|
219
|
+
np.ndarray
|
|
220
|
+
Z_forecast (horizon x m)
|
|
221
|
+
"""
|
|
222
|
+
if self._result is None:
|
|
223
|
+
raise ValueError("Model must be trained before calling predict(). Call train() first.")
|
|
224
|
+
if horizon <= 0:
|
|
225
|
+
raise ValueError("horizon must be a positive integer.")
|
|
226
|
+
|
|
227
|
+
A = self._result.A
|
|
228
|
+
C = self._result.C
|
|
229
|
+
Wx = self._result.Wx
|
|
230
|
+
Mx = self._result.Mx
|
|
231
|
+
Z_last = self._result.Z[-1, :]
|
|
232
|
+
|
|
233
|
+
# Forecast latent factors: Z_{t+h} = A^h Z_t (deterministic)
|
|
234
|
+
Z_forecast = np.zeros((horizon, Z_last.shape[0]))
|
|
235
|
+
Z_forecast[0, :] = A @ Z_last
|
|
236
|
+
for h in range(1, horizon):
|
|
237
|
+
Z_forecast[h, :] = A @ Z_forecast[h - 1, :]
|
|
238
|
+
|
|
239
|
+
# Forecast observed standardized series then denormalize
|
|
240
|
+
X_forecast_std = Z_forecast @ C.T
|
|
241
|
+
X_forecast = X_forecast_std * Wx + Mx
|
|
242
|
+
|
|
243
|
+
if return_series and return_factors:
|
|
244
|
+
return X_forecast, Z_forecast
|
|
245
|
+
if return_series:
|
|
246
|
+
return X_forecast
|
|
247
|
+
return Z_forecast
|
|
248
|
+
|
|
249
|
+
def plot(self, *, kind: str = 'factor', factor_index: int = 0, forecast_horizon: Optional[int] = None,
|
|
250
|
+
save_path: Optional[Union[str, Path]] = None, show: bool = False):
|
|
251
|
+
"""Quick plotting utility for common visualizations."""
|
|
252
|
+
if self._result is None:
|
|
253
|
+
raise ValueError("No results to plot. Train the model first.")
|
|
254
|
+
try:
|
|
255
|
+
import matplotlib.pyplot as plt # Local import to avoid hard dependency at import time
|
|
256
|
+
except Exception as e:
|
|
257
|
+
raise RuntimeError(f"matplotlib is required for plotting: {e}")
|
|
258
|
+
|
|
259
|
+
Z = self._result.Z
|
|
260
|
+
if factor_index < 0 or factor_index >= Z.shape[1]:
|
|
261
|
+
raise ValueError(f"factor_index out of range: {factor_index} (num factors: {Z.shape[1]})")
|
|
262
|
+
|
|
263
|
+
# Build time axis
|
|
264
|
+
if self._time is not None:
|
|
265
|
+
time_hist = pd.to_datetime(self._time)
|
|
266
|
+
else:
|
|
267
|
+
time_hist = pd.date_range(start='2000-01-01', periods=Z.shape[0], freq='ME')
|
|
268
|
+
|
|
269
|
+
plt.figure(figsize=(12, 4))
|
|
270
|
+
plt.plot(time_hist, Z[:, factor_index], 'b-', linewidth=2, label='Historical factor')
|
|
271
|
+
|
|
272
|
+
if forecast_horizon is not None and forecast_horizon > 0:
|
|
273
|
+
Z_fore = self.predict(forecast_horizon, return_series=False, return_factors=True)
|
|
274
|
+
# Build forecast dates
|
|
275
|
+
last_date = time_hist[-1]
|
|
276
|
+
# Use same monthly-end spacing; if freq missing, fallback to monthly-end
|
|
277
|
+
try:
|
|
278
|
+
forecast_dates = pd.date_range(start=last_date + pd.tseries.frequencies.to_offset('ME'),
|
|
279
|
+
periods=forecast_horizon, freq='ME')
|
|
280
|
+
except Exception:
|
|
281
|
+
forecast_dates = pd.date_range(start=last_date + pd.Timedelta(days=30),
|
|
282
|
+
periods=forecast_horizon, freq='ME')
|
|
283
|
+
plt.plot(forecast_dates, Z_fore[:, factor_index], 'r--', linewidth=2, label='Forecast factor')
|
|
284
|
+
plt.axvline(x=last_date, color='gray', linestyle=':', linewidth=1, label='Forecast start')
|
|
285
|
+
|
|
286
|
+
title_block = None
|
|
287
|
+
if hasattr(self._result, 'block_names') and self._result.block_names:
|
|
288
|
+
title_block = self._result.block_names[0] if factor_index == 0 else None
|
|
289
|
+
plt.title(f"Factor {factor_index}" + (f" ({title_block})" if title_block else ""))
|
|
290
|
+
plt.xlabel("Date")
|
|
291
|
+
plt.ylabel("Factor value")
|
|
292
|
+
plt.grid(True, alpha=0.3)
|
|
293
|
+
plt.legend()
|
|
294
|
+
plt.tight_layout()
|
|
295
|
+
|
|
296
|
+
out_path = None
|
|
297
|
+
if save_path is not None:
|
|
298
|
+
save_path = Path(save_path)
|
|
299
|
+
save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
300
|
+
plt.savefig(str(save_path), dpi=150, bbox_inches='tight')
|
|
301
|
+
out_path = save_path
|
|
302
|
+
if show:
|
|
303
|
+
plt.show()
|
|
304
|
+
else:
|
|
305
|
+
plt.close()
|
|
306
|
+
return out_path
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# Create a singleton instance for module-level usage
|
|
310
|
+
_dfm_instance = DFM()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Module-level convenience functions that delegate to the singleton
|
|
314
|
+
def load_config(
|
|
315
|
+
source: Optional[Union[str, Path, Dict[str, Any], DFMConfig, ConfigSource]] = None,
|
|
316
|
+
*,
|
|
317
|
+
yaml: Optional[Union[str, Path]] = None,
|
|
318
|
+
mapping: Optional[Dict[str, Any]] = None,
|
|
319
|
+
spec: Optional[Union[str, Path]] = None,
|
|
320
|
+
hydra: Optional[Union[Dict[str, Any], 'DictConfig']] = None,
|
|
321
|
+
base: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
322
|
+
override: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
323
|
+
) -> DFM:
|
|
324
|
+
"""Load configuration (module-level convenience function)."""
|
|
325
|
+
return _dfm_instance.load_config(
|
|
326
|
+
source=source,
|
|
327
|
+
yaml=yaml,
|
|
328
|
+
mapping=mapping,
|
|
329
|
+
spec=spec,
|
|
330
|
+
hydra=hydra,
|
|
331
|
+
base=base,
|
|
332
|
+
override=override,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def load_data(data_path: Optional[Union[str, Path]] = None,
|
|
337
|
+
data: Optional[np.ndarray] = None,
|
|
338
|
+
**kwargs) -> DFM:
|
|
339
|
+
"""Load data (module-level convenience function)."""
|
|
340
|
+
return _dfm_instance.load_data(data_path=data_path, data=data, **kwargs)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def train(threshold: Optional[float] = None,
|
|
344
|
+
max_iter: Optional[int] = None,
|
|
345
|
+
**kwargs) -> DFM:
|
|
346
|
+
"""Train the model (module-level convenience function)."""
|
|
347
|
+
return _dfm_instance.train(threshold=threshold, max_iter=max_iter, **kwargs)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def predict(horizon: int = 12, **kwargs):
|
|
351
|
+
"""Forecast using the trained model (module-level convenience function)."""
|
|
352
|
+
return _dfm_instance.predict(horizon=horizon, **kwargs)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def plot(**kwargs):
|
|
356
|
+
"""Plot common visualizations (module-level convenience function)."""
|
|
357
|
+
return _dfm_instance.plot(**kwargs)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def reset() -> DFM:
|
|
361
|
+
"""Reset state (module-level convenience function)."""
|
|
362
|
+
return _dfm_instance.reset()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# Convenience constructors for cleaner API
|
|
366
|
+
def from_yaml(yaml_path: Union[str, Path]) -> DFM:
|
|
367
|
+
"""Load configuration from YAML file (convenience constructor)."""
|
|
368
|
+
return _dfm_instance.load_config(yaml=yaml_path)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def from_spec(spec_path: Union[str, Path], params: Optional[Params] = None) -> DFM:
|
|
372
|
+
"""Load configuration from spec CSV file (convenience constructor)."""
|
|
373
|
+
from .config import SpecCSVSource
|
|
374
|
+
|
|
375
|
+
if params is None:
|
|
376
|
+
params = Params()
|
|
377
|
+
|
|
378
|
+
# Load config from spec CSV using ConfigSource pattern
|
|
379
|
+
spec_source = SpecCSVSource(spec_path)
|
|
380
|
+
config = spec_source.load()
|
|
381
|
+
|
|
382
|
+
# Override estimation parameters from params
|
|
383
|
+
for key, value in params.__dict__.items():
|
|
384
|
+
if hasattr(config, key):
|
|
385
|
+
setattr(config, key, value)
|
|
386
|
+
|
|
387
|
+
_dfm_instance._config = config
|
|
388
|
+
return _dfm_instance
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def from_spec_df(spec_df: pd.DataFrame, params: Optional[Params] = None) -> DFM:
|
|
392
|
+
"""Load configuration from spec DataFrame (convenience constructor)."""
|
|
393
|
+
from .data_loader import _load_config_from_dataframe
|
|
394
|
+
|
|
395
|
+
if params is None:
|
|
396
|
+
params = Params()
|
|
397
|
+
|
|
398
|
+
# Load config from DataFrame (creates DFMConfig with series/blocks from spec)
|
|
399
|
+
config = _load_config_from_dataframe(spec_df)
|
|
400
|
+
|
|
401
|
+
# Override estimation parameters from params
|
|
402
|
+
for key, value in params.__dict__.items():
|
|
403
|
+
if hasattr(config, key):
|
|
404
|
+
setattr(config, key, value)
|
|
405
|
+
|
|
406
|
+
_dfm_instance._config = config
|
|
407
|
+
return _dfm_instance
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def from_dict(mapping: Dict[str, Any]) -> DFM:
|
|
411
|
+
"""Load configuration from dictionary (convenience constructor)."""
|
|
412
|
+
return _dfm_instance.load_config(mapping=mapping)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# Expose singleton instance for direct access
|
|
416
|
+
# Users can access: dfm.config, dfm.data, dfm.result, etc.
|
|
417
|
+
__all__ = ['DFM', 'load_config', 'load_data', 'train', 'reset',
|
|
418
|
+
'from_yaml', 'from_spec', 'from_spec_df', 'from_dict']
|
|
419
|
+
|
|
420
|
+
|