dfm-python 0.5.56__py3-none-any.whl → 0.5.57__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/config/__init__.py +3 -10
- dfm_python/config/adapter.py +63 -7
- dfm_python/config/constants.py +5 -4
- dfm_python/config/schema/__init__.py +2 -6
- dfm_python/config/schema/block.py +61 -3
- dfm_python/config/schema/model.py +11 -2
- dfm_python/config/schema/params.py +207 -22
- dfm_python/config/schema/results.py +8 -74
- dfm_python/config/types.py +3 -11
- dfm_python/dataset/ddfm_dataset.py +35 -13
- dfm_python/dataset/dfm_dataset.py +9 -1
- dfm_python/functional/__init__.py +7 -9
- dfm_python/functional/dfm_block.py +65 -14
- dfm_python/functional/em.py +580 -223
- dfm_python/logger/__init__.py +6 -12
- dfm_python/logger/inference_logger.py +3 -17
- dfm_python/logger/logger.py +129 -5
- dfm_python/logger/train_logger.py +26 -34
- dfm_python/models/__init__.py +2 -2
- dfm_python/models/base.py +8 -29
- dfm_python/models/ddfm.py +103 -3
- dfm_python/models/dfm.py +486 -219
- dfm_python/numeric/estimator.py +146 -26
- dfm_python/numeric/stability.py +55 -151
- dfm_python/numeric/tent.py +16 -6
- dfm_python/numeric/validator.py +1 -2
- dfm_python/ssm/__init__.py +13 -3
- dfm_python/ssm/kalman.py +272 -141
- dfm_python/utils/checkpoint.py +1 -1
- dfm_python/utils/misc.py +4 -10
- dfm_python/utils/validation.py +4 -71
- {dfm_python-0.5.56.dist-info → dfm_python-0.5.57.dist-info}/METADATA +4 -5
- dfm_python-0.5.57.dist-info/RECORD +52 -0
- dfm_python/functional/irf.py +0 -305
- dfm_python/functional/krylov.py +0 -124
- dfm_python/ssm/arma.py +0 -0
- dfm_python/ssm/companion.py +0 -995
- dfm_python/ssm/structural.py +0 -298
- dfm_python-0.5.56.dist-info/RECORD +0 -58
- dfm_python-0.5.56.dist-info/licenses/LICENSE +0 -21
- {dfm_python-0.5.56.dist-info → dfm_python-0.5.57.dist-info}/WHEEL +0 -0
dfm_python/config/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ Note: Series are specified via frequency dict mapping column names to frequencie
|
|
|
10
10
|
from .schema import (
|
|
11
11
|
BaseModelConfig, DFMConfig, DDFMConfig,
|
|
12
12
|
BaseResult, DFMResult, DDFMResult,
|
|
13
|
-
|
|
13
|
+
DFMStateSpaceParams, DDFMStateSpaceParams,
|
|
14
14
|
)
|
|
15
15
|
# DEFAULT_BLOCK_NAME is imported lazily where needed to avoid circular imports
|
|
16
16
|
from .constants import (
|
|
@@ -46,8 +46,6 @@ from .types import (
|
|
|
46
46
|
ForecastResult,
|
|
47
47
|
CoefficientMatrix,
|
|
48
48
|
CovarianceMatrix,
|
|
49
|
-
IRFArray,
|
|
50
|
-
CompanionMatrix,
|
|
51
49
|
SeriesID,
|
|
52
50
|
Frequency,
|
|
53
51
|
DatasetName,
|
|
@@ -58,7 +56,6 @@ from .types import (
|
|
|
58
56
|
ResultDict,
|
|
59
57
|
ForecastDict,
|
|
60
58
|
MetricsDict,
|
|
61
|
-
IRFResultDict,
|
|
62
59
|
CheckpointDict,
|
|
63
60
|
ConfigDict,
|
|
64
61
|
Device,
|
|
@@ -107,9 +104,8 @@ __all__ = [
|
|
|
107
104
|
# 'DEFAULT_BLOCK_NAME', # Removed to avoid circular import - import directly from functional.dfm_block
|
|
108
105
|
# Model-specific configs (from schema.py)
|
|
109
106
|
'DFMConfig', 'DDFMConfig',
|
|
110
|
-
#
|
|
111
|
-
'
|
|
112
|
-
'FitParams', # Backward compatibility alias
|
|
107
|
+
# State-space parameters
|
|
108
|
+
'DFMStateSpaceParams', 'DDFMStateSpaceParams',
|
|
113
109
|
# Model-specific results (from results.py)
|
|
114
110
|
'DFMResult', 'DDFMResult',
|
|
115
111
|
# Utilities
|
|
@@ -139,8 +135,6 @@ __all__ = [
|
|
|
139
135
|
'ForecastResult',
|
|
140
136
|
'CoefficientMatrix',
|
|
141
137
|
'CovarianceMatrix',
|
|
142
|
-
'IRFArray',
|
|
143
|
-
'CompanionMatrix',
|
|
144
138
|
'SeriesID',
|
|
145
139
|
'Frequency',
|
|
146
140
|
'DatasetName',
|
|
@@ -151,7 +145,6 @@ __all__ = [
|
|
|
151
145
|
'ResultDict',
|
|
152
146
|
'ForecastDict',
|
|
153
147
|
'MetricsDict',
|
|
154
|
-
'IRFResultDict',
|
|
155
148
|
'CheckpointDict',
|
|
156
149
|
'ConfigDict',
|
|
157
150
|
'Device',
|
dfm_python/config/adapter.py
CHANGED
|
@@ -118,7 +118,12 @@ def _extract_frequency_dict(
|
|
|
118
118
|
data: Dict[str, Any],
|
|
119
119
|
clock: str = DEFAULT_CLOCK_FREQUENCY
|
|
120
120
|
) -> Optional[Dict[str, str]]:
|
|
121
|
-
"""Extract frequency dict from data, handling
|
|
121
|
+
"""Extract frequency dict from data, handling multiple formats.
|
|
122
|
+
|
|
123
|
+
Supports:
|
|
124
|
+
1. Grouped format: {'w': [series1, series2, ...], 'm': [series3, ...]}
|
|
125
|
+
2. Individual format: {'series1': 'w', 'series2': 'm', ...}
|
|
126
|
+
3. Legacy format: {'series': [{'series_id': ..., 'frequency': ...}, ...]}
|
|
122
127
|
|
|
123
128
|
Parameters
|
|
124
129
|
----------
|
|
@@ -132,12 +137,63 @@ def _extract_frequency_dict(
|
|
|
132
137
|
Optional[Dict[str, str]]
|
|
133
138
|
Frequency dict mapping column names to frequencies, or None if not found
|
|
134
139
|
"""
|
|
135
|
-
if 'frequency' in data:
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
if 'frequency' not in data:
|
|
141
|
+
# Legacy: check for 'series' key
|
|
142
|
+
if 'series' in data:
|
|
143
|
+
return _convert_series_to_frequency_dict(data['series'], clock)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
freq_data = data['frequency']
|
|
147
|
+
|
|
148
|
+
# Check if it's in grouped format: {'w': [...], 'm': [...]}
|
|
149
|
+
if _is_dict_like(freq_data):
|
|
150
|
+
from .constants import VALID_FREQUENCIES
|
|
151
|
+
|
|
152
|
+
# Check if all keys are valid frequency codes (grouped format)
|
|
153
|
+
keys_are_frequencies = all(
|
|
154
|
+
isinstance(k, str) and k in VALID_FREQUENCIES
|
|
155
|
+
for k in freq_data.keys()
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Check if values are lists (grouped format)
|
|
159
|
+
values_are_lists = all(
|
|
160
|
+
isinstance(v, (list, tuple)) for v in freq_data.values()
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if keys_are_frequencies and values_are_lists:
|
|
164
|
+
# Grouped format: convert to individual format
|
|
165
|
+
result = {}
|
|
166
|
+
for freq, series_list in freq_data.items():
|
|
167
|
+
if not isinstance(series_list, (list, tuple)):
|
|
168
|
+
_raise_config_error(
|
|
169
|
+
f"Frequency '{freq}' must map to a list of series names, got {type(series_list)}"
|
|
170
|
+
)
|
|
171
|
+
for series_name in series_list:
|
|
172
|
+
if not isinstance(series_name, str):
|
|
173
|
+
_raise_config_error(
|
|
174
|
+
f"Series names must be strings, got {type(series_name)} in frequency '{freq}'"
|
|
175
|
+
)
|
|
176
|
+
if series_name in result:
|
|
177
|
+
_raise_config_error(
|
|
178
|
+
f"Series '{series_name}' appears in multiple frequency groups"
|
|
179
|
+
)
|
|
180
|
+
result[series_name] = freq
|
|
181
|
+
return result
|
|
182
|
+
else:
|
|
183
|
+
# Individual format: already in correct format, validate structure
|
|
184
|
+
result = {}
|
|
185
|
+
for series_name, freq in freq_data.items():
|
|
186
|
+
if not isinstance(series_name, str):
|
|
187
|
+
_raise_config_error(
|
|
188
|
+
f"Frequency dict keys must be strings (series names), got {type(series_name)}"
|
|
189
|
+
)
|
|
190
|
+
if not isinstance(freq, str):
|
|
191
|
+
_raise_config_error(
|
|
192
|
+
f"Frequency dict values must be strings (frequency codes), got {type(freq)} for series '{series_name}'"
|
|
193
|
+
)
|
|
194
|
+
result[series_name] = freq
|
|
195
|
+
return result
|
|
196
|
+
|
|
141
197
|
return None
|
|
142
198
|
|
|
143
199
|
|
dfm_python/config/constants.py
CHANGED
|
@@ -23,7 +23,7 @@ DEFAULT_MIN_DELTA = 1e-6 # Minimum change for improvement
|
|
|
23
23
|
# ============================================================================
|
|
24
24
|
|
|
25
25
|
# Minimum eigenvalues and variances
|
|
26
|
-
MIN_EIGENVALUE = 1e-
|
|
26
|
+
MIN_EIGENVALUE = 1e-6 # Minimum eigenvalue for positive definite matrices (increased from 1e-8 for better conditioning)
|
|
27
27
|
MIN_DIAGONAL_VARIANCE = 1e-8 # Minimum variance for diagonal elements
|
|
28
28
|
MIN_OBSERVATION_NOISE = 1e-4 # Minimum observation noise for measurement error (used in EM updates)
|
|
29
29
|
DEFAULT_DDFM_OBSERVATION_NOISE = 1e-15 # Default observation noise for DDFM state-space (matches original TensorFlow)
|
|
@@ -33,7 +33,7 @@ MIN_STD_FOR_SCALE_CHECK = 1e-10 # Minimum standard deviation for scale ratio co
|
|
|
33
33
|
DEFAULT_VARIANCE_FALLBACK = 1.0 # Default variance fallback value for numerical stability
|
|
34
34
|
|
|
35
35
|
# Maximum eigenvalues
|
|
36
|
-
MAX_EIGENVALUE =
|
|
36
|
+
MAX_EIGENVALUE = 1e3 # Maximum eigenvalue cap (reduced to 1e3 to improve condition number: 1e3/1e-6 = 1e9)
|
|
37
37
|
|
|
38
38
|
# Eigenvalue stability thresholds
|
|
39
39
|
DEFAULT_EIGENVALUE_MAX_MAGNITUDE = 1.0 # Default maximum eigenvalue magnitude for stability checks
|
|
@@ -217,6 +217,7 @@ DEFAULT_START_DATE = datetime(2000, 1, 1)
|
|
|
217
217
|
# Default window size for DDFM
|
|
218
218
|
DEFAULT_WINDOW_SIZE = 100
|
|
219
219
|
DEFAULT_TENT_KERNEL_SIZE = 5 # Default tent kernel size for slower-frequency series aggregation
|
|
220
|
+
DEFAULT_TENT_KERNEL_REGULARIZATION_MULTIPLIER = 100.0 # Regularization multiplier for tent kernel series (handles ill-conditioning)
|
|
220
221
|
# Note: DEFAULT_BATCH_SIZE is defined above (line 88) as 32 for general neural networks
|
|
221
222
|
# Use DEFAULT_DDFM_WINDOW_SIZE (100) for DDFM-specific window size (time-step batch size)
|
|
222
223
|
|
|
@@ -284,7 +285,7 @@ DEFAULT_PROGRESS_LOG_INTERVAL = 5 # Default progress logging interval for EM al
|
|
|
284
285
|
# Precision Defaults
|
|
285
286
|
# ============================================================================
|
|
286
287
|
|
|
287
|
-
DEFAULT_DTYPE = np.
|
|
288
|
+
DEFAULT_DTYPE = np.float64 # Default numpy dtype for arrays (float64 for better numerical stability)
|
|
288
289
|
|
|
289
290
|
# PyTorch dtype (matches DEFAULT_DTYPE)
|
|
290
291
|
try:
|
|
@@ -518,7 +519,6 @@ __all__ = [
|
|
|
518
519
|
'MIN_VARIABLES',
|
|
519
520
|
'MIN_SHAPE_FOR_AR2',
|
|
520
521
|
'MIN_DDFM_TIME_STEPS',
|
|
521
|
-
'MIN_SHAPE_FOR_AR2',
|
|
522
522
|
'MIN_DDFM_DATASET_SIZE_WARNING',
|
|
523
523
|
'MIN_ITER_FOR_DELTA_COMPUTATION',
|
|
524
524
|
'DEFAULT_MIN_ITER_FOR_CONVERGENCE_CHECK',
|
|
@@ -546,6 +546,7 @@ __all__ = [
|
|
|
546
546
|
'DEFAULT_LOG_INTERVAL',
|
|
547
547
|
'DEFAULT_PROGRESS_LOG_INTERVAL',
|
|
548
548
|
'DEFAULT_TENT_KERNEL_SIZE',
|
|
549
|
+
'DEFAULT_TENT_KERNEL_REGULARIZATION_MULTIPLIER',
|
|
549
550
|
'MAX_ERROR_ITEMS',
|
|
550
551
|
# IRF
|
|
551
552
|
'DEFAULT_IRF_HORIZON',
|
|
@@ -9,15 +9,11 @@ Note: Series are specified via frequency dict mapping column names to frequencie
|
|
|
9
9
|
|
|
10
10
|
from .model import BaseModelConfig, DFMConfig, DDFMConfig
|
|
11
11
|
from .results import BaseResult, DFMResult, DDFMResult
|
|
12
|
-
from .params import
|
|
13
|
-
|
|
14
|
-
# Backward compatibility alias
|
|
15
|
-
FitParams = DFMFitParams
|
|
12
|
+
from .params import DFMStateSpaceParams, DDFMStateSpaceParams
|
|
16
13
|
|
|
17
14
|
__all__ = [
|
|
18
15
|
'BaseModelConfig', 'DFMConfig', 'DDFMConfig',
|
|
19
16
|
'BaseResult', 'DFMResult', 'DDFMResult',
|
|
20
|
-
'
|
|
21
|
-
'FitParams', # Backward compatibility alias
|
|
17
|
+
'DFMStateSpaceParams', 'DDFMStateSpaceParams',
|
|
22
18
|
]
|
|
23
19
|
|
|
@@ -5,8 +5,8 @@ parameters in EM algorithm, replacing long conditional checks with a single
|
|
|
5
5
|
optional parameter.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from typing import Optional, Dict
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Optional, Dict, List
|
|
10
10
|
import numpy as np
|
|
11
11
|
|
|
12
12
|
|
|
@@ -26,7 +26,10 @@ class BlockStructure:
|
|
|
26
26
|
p : int
|
|
27
27
|
VAR lag order
|
|
28
28
|
p_plus_one : int
|
|
29
|
-
|
|
29
|
+
State dimension per factor. This is equal to max_lag_size = max(p + 1, tent_kernel_size).
|
|
30
|
+
The name "p_plus_one" is used for backward compatibility with the EM algorithm interface.
|
|
31
|
+
In the model code, this is called "max_lag_size" to more clearly indicate it accounts
|
|
32
|
+
for tent kernel size when tent_kernel_size > p + 1.
|
|
30
33
|
n_clock_freq : int
|
|
31
34
|
Number of clock-frequency series
|
|
32
35
|
idio_indicator : np.ndarray
|
|
@@ -39,6 +42,30 @@ class BlockStructure:
|
|
|
39
42
|
Number of slower-frequency series
|
|
40
43
|
tent_weights_dict : dict, optional
|
|
41
44
|
Dictionary mapping frequency pairs to tent weights
|
|
45
|
+
|
|
46
|
+
Cached indices (computed once, reused across EM iterations):
|
|
47
|
+
_cached_unique_blocks : List[np.ndarray], optional
|
|
48
|
+
Unique block patterns (cached)
|
|
49
|
+
_cached_unique_indices : List[int], optional
|
|
50
|
+
Indices of unique block patterns (cached)
|
|
51
|
+
_cached_bl_idxM : List[np.ndarray], optional
|
|
52
|
+
Clock-frequency factor loadings indices per unique block pattern (cached)
|
|
53
|
+
_cached_bl_idxQ : List[np.ndarray], optional
|
|
54
|
+
Slower-frequency factor loadings indices per unique block pattern (cached)
|
|
55
|
+
_cached_R_con : np.ndarray, optional
|
|
56
|
+
Block diagonal constraint matrix (cached)
|
|
57
|
+
_cached_q_con : np.ndarray, optional
|
|
58
|
+
Constraint vector (cached)
|
|
59
|
+
_cached_total_factor_dim : int, optional
|
|
60
|
+
Total factor state dimension (cached)
|
|
61
|
+
_cached_idio_indicator_M : np.ndarray, optional
|
|
62
|
+
Clock-frequency idiosyncratic component indicator (cached)
|
|
63
|
+
_cached_n_idio_M : int, optional
|
|
64
|
+
Number of clock-frequency idiosyncratic components (cached)
|
|
65
|
+
_cached_c_idio_indicator : np.ndarray, optional
|
|
66
|
+
Cumulative sum of idiosyncratic component indicator (cached)
|
|
67
|
+
_cached_rp1 : int, optional
|
|
68
|
+
Start of idiosyncratic components in state space (cached)
|
|
42
69
|
"""
|
|
43
70
|
blocks: np.ndarray
|
|
44
71
|
r: np.ndarray
|
|
@@ -51,6 +78,19 @@ class BlockStructure:
|
|
|
51
78
|
n_slower_freq: Optional[int] = None
|
|
52
79
|
tent_weights_dict: Optional[Dict[str, np.ndarray]] = None
|
|
53
80
|
|
|
81
|
+
# Cached indices (computed once, reused across EM iterations)
|
|
82
|
+
_cached_unique_blocks: Optional[List[np.ndarray]] = field(default=None, init=False, repr=False)
|
|
83
|
+
_cached_unique_indices: Optional[List[int]] = field(default=None, init=False, repr=False)
|
|
84
|
+
_cached_bl_idxM: Optional[List[np.ndarray]] = field(default=None, init=False, repr=False)
|
|
85
|
+
_cached_bl_idxQ: Optional[List[np.ndarray]] = field(default=None, init=False, repr=False)
|
|
86
|
+
_cached_R_con: Optional[np.ndarray] = field(default=None, init=False, repr=False)
|
|
87
|
+
_cached_q_con: Optional[np.ndarray] = field(default=None, init=False, repr=False)
|
|
88
|
+
_cached_total_factor_dim: Optional[int] = field(default=None, init=False, repr=False)
|
|
89
|
+
_cached_idio_indicator_M: Optional[np.ndarray] = field(default=None, init=False, repr=False)
|
|
90
|
+
_cached_n_idio_M: Optional[int] = field(default=None, init=False, repr=False)
|
|
91
|
+
_cached_c_idio_indicator: Optional[np.ndarray] = field(default=None, init=False, repr=False)
|
|
92
|
+
_cached_rp1: Optional[int] = field(default=None, init=False, repr=False)
|
|
93
|
+
|
|
54
94
|
def is_valid(self) -> bool:
|
|
55
95
|
"""Check if block structure is valid (all required fields are not None)."""
|
|
56
96
|
return (
|
|
@@ -61,4 +101,22 @@ class BlockStructure:
|
|
|
61
101
|
and self.n_clock_freq is not None
|
|
62
102
|
and self.idio_indicator is not None
|
|
63
103
|
)
|
|
104
|
+
|
|
105
|
+
def has_cached_indices(self) -> bool:
|
|
106
|
+
"""Check if block structure indices are cached."""
|
|
107
|
+
return self._cached_unique_blocks is not None
|
|
108
|
+
|
|
109
|
+
def clear_cache(self) -> None:
|
|
110
|
+
"""Clear all cached indices to ensure clean state for new training runs."""
|
|
111
|
+
self._cached_unique_blocks = None
|
|
112
|
+
self._cached_unique_indices = None
|
|
113
|
+
self._cached_bl_idxM = None
|
|
114
|
+
self._cached_bl_idxQ = None
|
|
115
|
+
self._cached_R_con = None
|
|
116
|
+
self._cached_q_con = None
|
|
117
|
+
self._cached_total_factor_dim = None
|
|
118
|
+
self._cached_idio_indicator_M = None
|
|
119
|
+
self._cached_n_idio_M = None
|
|
120
|
+
self._cached_c_idio_indicator = None
|
|
121
|
+
self._cached_rp1 = None
|
|
64
122
|
|
|
@@ -91,7 +91,9 @@ class BaseModelConfig:
|
|
|
91
91
|
- Data preprocessing (missing data handling)
|
|
92
92
|
|
|
93
93
|
Series Configuration:
|
|
94
|
-
- Provide `frequency` dict
|
|
94
|
+
- Provide `frequency` dict in one of two formats:
|
|
95
|
+
1. Grouped format: {'w': [series1, series2, ...], 'm': [series3, ...]} (recommended for large configs)
|
|
96
|
+
2. Individual format: {'series1': 'w', 'series2': 'm', ...} (backward compatible)
|
|
95
97
|
- If `frequency` is None, all columns will use `clock` frequency
|
|
96
98
|
- If a column is missing from `frequency` dict, it will use `clock` frequency
|
|
97
99
|
- When data is loaded, missing columns in `frequency` dict are automatically added with `clock` frequency
|
|
@@ -103,7 +105,14 @@ class BaseModelConfig:
|
|
|
103
105
|
|
|
104
106
|
Examples
|
|
105
107
|
--------
|
|
106
|
-
>>> # With
|
|
108
|
+
>>> # With grouped frequency mapping (recommended for large configs)
|
|
109
|
+
>>> config = DFMConfig(
|
|
110
|
+
... frequency={'q': ['gdp'], 'm': ['unemployment', 'interest_rate']},
|
|
111
|
+
... clock='m',
|
|
112
|
+
... blocks={...}
|
|
113
|
+
... )
|
|
114
|
+
>>>
|
|
115
|
+
>>> # With individual frequency mapping (backward compatible)
|
|
107
116
|
>>> config = DFMConfig(
|
|
108
117
|
... frequency={'gdp': 'q', 'unemployment': 'm', 'interest_rate': 'm'},
|
|
109
118
|
... clock='m',
|
|
@@ -1,37 +1,199 @@
|
|
|
1
1
|
"""Model parameters for DFM and DDFM.
|
|
2
2
|
|
|
3
|
-
This module defines dataclasses for storing
|
|
3
|
+
This module defines dataclasses for storing state-space parameters and model structure.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
import numpy as np
|
|
8
|
-
from typing import Optional
|
|
8
|
+
from typing import Optional, Any
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
12
|
-
class
|
|
13
|
-
"""
|
|
12
|
+
class DFMStateSpaceParams:
|
|
13
|
+
"""State-space model parameters for DFM (fitted during model training).
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
These parameters are computed during DFM.fit() and represent the fitted
|
|
16
|
+
state-space model structure. Contains only the state-space parameters,
|
|
17
|
+
not training metadata (loglik, num_iter, converged) which are stored
|
|
18
|
+
separately in checkpoint dicts.
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
A : np.ndarray
|
|
23
|
+
Transition matrix (m x m) - VAR dynamics for factors
|
|
24
|
+
C : np.ndarray
|
|
25
|
+
Observation matrix (N x m) - factor loadings
|
|
26
|
+
Q : np.ndarray
|
|
27
|
+
Process noise covariance (m x m) - innovation covariance
|
|
28
|
+
R : np.ndarray
|
|
29
|
+
Observation noise covariance (N x N) - typically diagonal
|
|
30
|
+
Z_0 : np.ndarray
|
|
31
|
+
Initial state mean (m,) - initial factor values
|
|
32
|
+
V_0 : np.ndarray
|
|
33
|
+
Initial state covariance (m x m) - initial uncertainty
|
|
34
|
+
"""
|
|
35
|
+
A: np.ndarray # Transition matrix (m x m)
|
|
36
|
+
C: np.ndarray # Observation matrix (N x m)
|
|
37
|
+
Q: np.ndarray # Process noise covariance (m x m)
|
|
38
|
+
R: np.ndarray # Observation noise covariance (N x N)
|
|
39
|
+
Z_0: np.ndarray # Initial state mean (m,)
|
|
40
|
+
V_0: np.ndarray # Initial state covariance (m x m)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_model(cls, model: Any) -> 'DFMStateSpaceParams':
|
|
44
|
+
"""Create DFMStateSpaceParams from DFM model instance.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
model : DFM
|
|
49
|
+
DFM model instance with fitted parameters
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
DFMStateSpaceParams
|
|
54
|
+
State-space parameters dataclass
|
|
55
|
+
"""
|
|
56
|
+
# Get parameters from model (stored after fit)
|
|
57
|
+
# First check if training_state is already DFMStateSpaceParams (new format)
|
|
58
|
+
if hasattr(model, 'training_state') and model.training_state is not None:
|
|
59
|
+
if isinstance(model.training_state, cls):
|
|
60
|
+
# Already a DFMStateSpaceParams instance
|
|
61
|
+
return model.training_state
|
|
62
|
+
# Backward compatibility: If old format training_state (has A, C, Q, R, Z_0, V_0 attributes)
|
|
63
|
+
if hasattr(model.training_state, 'A'):
|
|
64
|
+
return cls(
|
|
65
|
+
A=model.training_state.A,
|
|
66
|
+
C=model.training_state.C,
|
|
67
|
+
Q=model.training_state.Q,
|
|
68
|
+
R=model.training_state.R,
|
|
69
|
+
Z_0=model.training_state.Z_0,
|
|
70
|
+
V_0=model.training_state.V_0
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Get from model attributes (after _update_parameters is called)
|
|
74
|
+
return cls(
|
|
75
|
+
A=getattr(model, 'A', None),
|
|
76
|
+
C=getattr(model, 'C', None),
|
|
77
|
+
Q=getattr(model, 'Q', None),
|
|
78
|
+
R=getattr(model, 'R', None),
|
|
79
|
+
Z_0=getattr(model, 'Z_0', None),
|
|
80
|
+
V_0=getattr(model, 'V_0', None)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def apply_to_model(self, model: Any) -> None:
|
|
84
|
+
"""Apply state-space parameters to DFM model instance.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
model : DFM
|
|
89
|
+
DFM model instance to update
|
|
90
|
+
"""
|
|
91
|
+
model._update_parameters(
|
|
92
|
+
self.A, self.C, self.Q, self.R, self.Z_0, self.V_0
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class DFMModelState:
|
|
98
|
+
"""DFM model structure and mixed-frequency parameters.
|
|
99
|
+
|
|
100
|
+
Consolidates model structure and mixed-frequency state for checkpointing.
|
|
101
|
+
This allows save/load methods to be simplified by using a single dataclass.
|
|
19
102
|
|
|
20
103
|
Attributes
|
|
21
104
|
----------
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
105
|
+
num_factors : int
|
|
106
|
+
Number of factors
|
|
107
|
+
r : np.ndarray
|
|
108
|
+
Number of factors per block
|
|
109
|
+
p : int
|
|
110
|
+
AR lag order
|
|
111
|
+
blocks : np.ndarray
|
|
112
|
+
Block structure array
|
|
113
|
+
mixed_freq : bool, optional
|
|
114
|
+
Whether mixed-frequency data is used
|
|
115
|
+
constraint_matrix : np.ndarray, optional
|
|
116
|
+
Constraint matrix for tent kernel aggregation
|
|
117
|
+
constraint_vector : np.ndarray, optional
|
|
118
|
+
Constraint vector for tent kernel aggregation
|
|
119
|
+
n_slower_freq : int
|
|
120
|
+
Number of slower-frequency series
|
|
121
|
+
n_clock_freq : int, optional
|
|
122
|
+
Number of clock-frequency series
|
|
123
|
+
tent_weights_dict : dict, optional
|
|
124
|
+
Dictionary of tent weights by frequency
|
|
125
|
+
frequencies : np.ndarray, optional
|
|
126
|
+
Frequency array for each series
|
|
127
|
+
idio_indicator : np.ndarray, optional
|
|
128
|
+
Indicator for idiosyncratic components
|
|
129
|
+
max_lag_size : int, optional
|
|
130
|
+
Maximum lag size for state dimension
|
|
31
131
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
132
|
+
num_factors: int
|
|
133
|
+
r: np.ndarray
|
|
134
|
+
p: int
|
|
135
|
+
blocks: np.ndarray
|
|
136
|
+
mixed_freq: Optional[bool] = None
|
|
137
|
+
constraint_matrix: Optional[np.ndarray] = None
|
|
138
|
+
constraint_vector: Optional[np.ndarray] = None
|
|
139
|
+
n_slower_freq: int = 0
|
|
140
|
+
n_clock_freq: Optional[int] = None
|
|
141
|
+
tent_weights_dict: Optional[dict] = None
|
|
142
|
+
frequencies: Optional[np.ndarray] = None
|
|
143
|
+
idio_indicator: Optional[np.ndarray] = None
|
|
144
|
+
max_lag_size: Optional[int] = None
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_model(cls, model: Any) -> 'DFMModelState':
|
|
148
|
+
"""Create DFMModelState from DFM model instance.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
model : DFM
|
|
153
|
+
DFM model instance
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
DFMModelState
|
|
158
|
+
Model state dataclass
|
|
159
|
+
"""
|
|
160
|
+
return cls(
|
|
161
|
+
num_factors=model.num_factors,
|
|
162
|
+
r=model.r,
|
|
163
|
+
p=model.p,
|
|
164
|
+
blocks=model.blocks,
|
|
165
|
+
mixed_freq=getattr(model, '_mixed_freq', None),
|
|
166
|
+
constraint_matrix=getattr(model, '_constraint_matrix', None),
|
|
167
|
+
constraint_vector=getattr(model, '_constraint_vector', None),
|
|
168
|
+
n_slower_freq=getattr(model, '_n_slower_freq', 0),
|
|
169
|
+
n_clock_freq=getattr(model, '_n_clock_freq', None),
|
|
170
|
+
tent_weights_dict=getattr(model, '_tent_weights_dict', None),
|
|
171
|
+
frequencies=getattr(model, '_frequencies', None),
|
|
172
|
+
idio_indicator=getattr(model, '_idio_indicator', None),
|
|
173
|
+
max_lag_size=getattr(model, '_max_lag_size', None)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def apply_to_model(self, model: Any) -> None:
|
|
177
|
+
"""Apply this state to DFM model instance.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
model : DFM
|
|
182
|
+
DFM model instance to update
|
|
183
|
+
"""
|
|
184
|
+
model.num_factors = self.num_factors
|
|
185
|
+
model.r = self.r
|
|
186
|
+
model.p = self.p
|
|
187
|
+
model.blocks = self.blocks
|
|
188
|
+
model._mixed_freq = self.mixed_freq
|
|
189
|
+
model._constraint_matrix = self.constraint_matrix
|
|
190
|
+
model._constraint_vector = self.constraint_vector
|
|
191
|
+
model._n_slower_freq = self.n_slower_freq
|
|
192
|
+
model._n_clock_freq = self.n_clock_freq
|
|
193
|
+
model._tent_weights_dict = self.tent_weights_dict
|
|
194
|
+
model._frequencies = self.frequencies
|
|
195
|
+
model._idio_indicator = self.idio_indicator
|
|
196
|
+
model._max_lag_size = self.max_lag_size
|
|
35
197
|
|
|
36
198
|
@dataclass
|
|
37
199
|
class DDFMTrainingState:
|
|
@@ -62,13 +224,36 @@ class DDFMTrainingState:
|
|
|
62
224
|
eps: Optional[np.ndarray] = None
|
|
63
225
|
factors: Optional[np.ndarray] = None
|
|
64
226
|
last_neurons: Optional[np.ndarray] = None
|
|
227
|
+
|
|
228
|
+
def sync_from_model(self, model: Any) -> 'DDFMTrainingState':
|
|
229
|
+
"""Update this dataclass from model instance attributes.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
model : DDFM
|
|
234
|
+
DDFM model instance
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
DDFMTrainingState
|
|
239
|
+
Self (for chaining)
|
|
240
|
+
"""
|
|
241
|
+
if hasattr(model, '_has_factors') and model._has_factors:
|
|
242
|
+
self.factors = getattr(model, 'factors', None)
|
|
243
|
+
self.eps = getattr(model, 'eps', None)
|
|
244
|
+
self.last_neurons = getattr(model, 'last_neurons', None)
|
|
245
|
+
self.num_iter = getattr(model, '_num_iter', 0)
|
|
246
|
+
self.loss_now = getattr(model, 'loss_now', None)
|
|
247
|
+
self.converged = getattr(model, '_converged', False)
|
|
248
|
+
return self
|
|
65
249
|
|
|
66
250
|
@dataclass
|
|
67
|
-
class
|
|
68
|
-
"""State-space model parameters for DDFM (
|
|
251
|
+
class DDFMStateSpaceParams:
|
|
252
|
+
"""State-space model parameters for DDFM (fitted during model training).
|
|
69
253
|
|
|
70
254
|
These parameters are computed during DDFM.build_state_space() and represent
|
|
71
|
-
the fitted state-space model structure.
|
|
255
|
+
the fitted state-space model structure. Similar to DFMStateSpaceParams, this contains
|
|
256
|
+
the fitted state-space parameters but for DDFM. Naming follows original paper conventions.
|
|
72
257
|
|
|
73
258
|
Parameters
|
|
74
259
|
----------
|