pymc-extras 0.3.1__py3-none-any.whl → 0.4.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.
- pymc_extras/distributions/__init__.py +5 -5
- pymc_extras/distributions/histogram_utils.py +1 -1
- pymc_extras/inference/__init__.py +1 -1
- pymc_extras/inference/laplace_approx/find_map.py +12 -5
- pymc_extras/inference/laplace_approx/idata.py +4 -3
- pymc_extras/inference/laplace_approx/laplace.py +6 -4
- pymc_extras/inference/pathfinder/pathfinder.py +1 -2
- pymc_extras/printing.py +1 -1
- pymc_extras/statespace/__init__.py +4 -4
- pymc_extras/statespace/core/__init__.py +1 -1
- pymc_extras/statespace/core/representation.py +8 -8
- pymc_extras/statespace/core/statespace.py +94 -23
- pymc_extras/statespace/filters/__init__.py +3 -3
- pymc_extras/statespace/filters/kalman_filter.py +16 -11
- pymc_extras/statespace/models/SARIMAX.py +138 -74
- pymc_extras/statespace/models/VARMAX.py +248 -57
- pymc_extras/statespace/models/__init__.py +2 -2
- pymc_extras/statespace/models/structural/__init__.py +21 -0
- pymc_extras/statespace/models/structural/components/__init__.py +0 -0
- pymc_extras/statespace/models/structural/components/autoregressive.py +213 -0
- pymc_extras/statespace/models/structural/components/cycle.py +325 -0
- pymc_extras/statespace/models/structural/components/level_trend.py +289 -0
- pymc_extras/statespace/models/structural/components/measurement_error.py +154 -0
- pymc_extras/statespace/models/structural/components/regression.py +257 -0
- pymc_extras/statespace/models/structural/components/seasonality.py +628 -0
- pymc_extras/statespace/models/structural/core.py +919 -0
- pymc_extras/statespace/models/structural/utils.py +16 -0
- pymc_extras/statespace/models/utilities.py +285 -0
- pymc_extras/statespace/utils/constants.py +21 -18
- pymc_extras/statespace/utils/data_tools.py +4 -3
- {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/METADATA +5 -4
- {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/RECORD +34 -25
- pymc_extras/statespace/models/structural.py +0 -1679
- {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/WHEEL +0 -0
- {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
import functools as ft
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from itertools import pairwise
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import xarray as xr
|
|
10
|
+
|
|
11
|
+
from pytensor import Mode, Variable, config
|
|
12
|
+
from pytensor import tensor as pt
|
|
13
|
+
|
|
14
|
+
from pymc_extras.statespace.core import PyMCStateSpace, PytensorRepresentation
|
|
15
|
+
from pymc_extras.statespace.models.utilities import (
|
|
16
|
+
add_tensors_by_dim_labels,
|
|
17
|
+
conform_time_varying_and_time_invariant_matrices,
|
|
18
|
+
join_tensors_by_dim_labels,
|
|
19
|
+
make_default_coords,
|
|
20
|
+
)
|
|
21
|
+
from pymc_extras.statespace.utils.constants import (
|
|
22
|
+
ALL_STATE_AUX_DIM,
|
|
23
|
+
ALL_STATE_DIM,
|
|
24
|
+
LONG_MATRIX_NAMES,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_log = logging.getLogger(__name__)
|
|
28
|
+
floatX = config.floatX
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StructuralTimeSeries(PyMCStateSpace):
|
|
32
|
+
r"""
|
|
33
|
+
Structural Time Series Model
|
|
34
|
+
|
|
35
|
+
The structural time series model, named by [1] and presented in statespace form in [2], is a framework for
|
|
36
|
+
decomposing a univariate time series into level, trend, seasonal, and cycle components. It also admits the
|
|
37
|
+
possibility of exogenous regressors. Unlike the SARIMAX framework, the time series is not assumed to be stationary.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
ssm : PytensorRepresentation
|
|
42
|
+
The state space representation containing system matrices.
|
|
43
|
+
name : str
|
|
44
|
+
Name of the model. If None, defaults to "StructuralTimeSeries".
|
|
45
|
+
state_names : list[str]
|
|
46
|
+
Names of the hidden states in the model.
|
|
47
|
+
observed_state_names : list[str]
|
|
48
|
+
Names of the observed variables.
|
|
49
|
+
data_names : list[str]
|
|
50
|
+
Names of data variables expected by the model.
|
|
51
|
+
shock_names : list[str]
|
|
52
|
+
Names of innovation/shock processes.
|
|
53
|
+
param_names : list[str]
|
|
54
|
+
Names of model parameters.
|
|
55
|
+
exog_names : list[str]
|
|
56
|
+
Names of exogenous variables.
|
|
57
|
+
param_dims : dict[str, tuple[int]]
|
|
58
|
+
Dimension specifications for parameters.
|
|
59
|
+
coords : dict[str, Sequence]
|
|
60
|
+
Coordinate specifications for the model.
|
|
61
|
+
param_info : dict[str, dict[str, Any]]
|
|
62
|
+
Information about parameters including shapes and constraints.
|
|
63
|
+
data_info : dict[str, dict[str, Any]]
|
|
64
|
+
Information about data variables.
|
|
65
|
+
component_info : dict[str, dict[str, Any]]
|
|
66
|
+
Information about model components.
|
|
67
|
+
measurement_error : bool
|
|
68
|
+
Whether the model includes measurement error.
|
|
69
|
+
name_to_variable : dict[str, Variable]
|
|
70
|
+
Mapping from parameter names to PyTensor variables.
|
|
71
|
+
name_to_data : dict[str, Variable] | None, optional
|
|
72
|
+
Mapping from data names to PyTensor variables. Default is None.
|
|
73
|
+
verbose : bool, optional
|
|
74
|
+
Whether to print model information. Default is True.
|
|
75
|
+
filter_type : str, optional
|
|
76
|
+
Type of Kalman filter to use. Default is "standard".
|
|
77
|
+
mode : str | Mode | None, optional
|
|
78
|
+
PyTensor compilation mode. Default is None.
|
|
79
|
+
|
|
80
|
+
Notes
|
|
81
|
+
-----
|
|
82
|
+
The structural time series model decomposes a time series into interpretable components:
|
|
83
|
+
|
|
84
|
+
.. math::
|
|
85
|
+
|
|
86
|
+
y_t = \mu_t + \nu_t + \cdots + \gamma_t + c_t + \xi_t + \varepsilon_t
|
|
87
|
+
|
|
88
|
+
Where:
|
|
89
|
+
- :math:`\mu_t` is the level component
|
|
90
|
+
- :math:`\nu_t` is the slope/trend component
|
|
91
|
+
- :math:`\cdots` represents higher-order trend components
|
|
92
|
+
- :math:`\gamma_t` is the seasonal component
|
|
93
|
+
- :math:`c_t` is the cycle component
|
|
94
|
+
- :math:`\xi_t` is the autoregressive component
|
|
95
|
+
- :math:`\varepsilon_t` is the measurement error
|
|
96
|
+
|
|
97
|
+
The model is built by combining individual components (e.g., LevelTrendComponent,
|
|
98
|
+
TimeSeasonality, CycleComponent) using the addition operator. Each component
|
|
99
|
+
contributes to the overall state space representation.
|
|
100
|
+
|
|
101
|
+
Examples
|
|
102
|
+
--------
|
|
103
|
+
Create a model with trend and seasonal components:
|
|
104
|
+
|
|
105
|
+
.. code:: python
|
|
106
|
+
|
|
107
|
+
from pymc_extras.statespace import structural as st
|
|
108
|
+
import pymc as pm
|
|
109
|
+
import pytensor.tensor as pt
|
|
110
|
+
|
|
111
|
+
trend = st.LevelTrendComponent(order=2 innovations_order=1)
|
|
112
|
+
seasonal = st.TimeSeasonality(season_length=12, innovations=True)
|
|
113
|
+
error = st.MeasurementError()
|
|
114
|
+
|
|
115
|
+
ss_mod = (trend + seasonal + error).build()
|
|
116
|
+
|
|
117
|
+
with pm.Model(coords=ss_mod.coords) as model:
|
|
118
|
+
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
|
|
119
|
+
|
|
120
|
+
initial_trend = pm.Normal('initial_trend', sigma=10, dims=ss_mod.param_dims['initial_trend'])
|
|
121
|
+
sigma_trend = pm.HalfNormal('sigma_trend', sigma=1, dims=ss_mod.param_dims['sigma_trend'])
|
|
122
|
+
|
|
123
|
+
seasonal_coefs = pm.Normal('params_seasonal', sigma=1, dims=ss_mod.param_dims['params_seasonal'])
|
|
124
|
+
sigma_seasonal = pm.HalfNormal('sigma_seasonal', sigma=1)
|
|
125
|
+
|
|
126
|
+
sigma_obs = pm.Exponential('sigma_obs', 1, dims=ss_mod.param_dims['sigma_obs'])
|
|
127
|
+
|
|
128
|
+
ss_mod.build_statespace_graph(data)
|
|
129
|
+
idata = pm.sample()
|
|
130
|
+
|
|
131
|
+
References
|
|
132
|
+
----------
|
|
133
|
+
.. [1] Harvey, A. C. (1989). Forecasting, structural time series models and the
|
|
134
|
+
Kalman filter. Cambridge University Press.
|
|
135
|
+
.. [2] Durbin, J., & Koopman, S. J. (2012). Time series analysis by state space
|
|
136
|
+
methods (2nd ed.). Oxford University Press.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
ssm: PytensorRepresentation,
|
|
142
|
+
name: str,
|
|
143
|
+
state_names: list[str],
|
|
144
|
+
observed_state_names: list[str],
|
|
145
|
+
data_names: list[str],
|
|
146
|
+
shock_names: list[str],
|
|
147
|
+
param_names: list[str],
|
|
148
|
+
exog_names: list[str],
|
|
149
|
+
param_dims: dict[str, tuple[int]],
|
|
150
|
+
coords: dict[str, Sequence],
|
|
151
|
+
param_info: dict[str, dict[str, Any]],
|
|
152
|
+
data_info: dict[str, dict[str, Any]],
|
|
153
|
+
component_info: dict[str, dict[str, Any]],
|
|
154
|
+
measurement_error: bool,
|
|
155
|
+
name_to_variable: dict[str, Variable],
|
|
156
|
+
name_to_data: dict[str, Variable] | None = None,
|
|
157
|
+
verbose: bool = True,
|
|
158
|
+
filter_type: str = "standard",
|
|
159
|
+
mode: str | Mode | None = None,
|
|
160
|
+
):
|
|
161
|
+
name = "StructuralTimeSeries" if name is None else name
|
|
162
|
+
|
|
163
|
+
self._name = name
|
|
164
|
+
self._observed_state_names = observed_state_names
|
|
165
|
+
|
|
166
|
+
k_states, k_posdef, k_endog = ssm.k_states, ssm.k_posdef, ssm.k_endog
|
|
167
|
+
param_names, param_dims, param_info = self._add_inital_state_cov_to_properties(
|
|
168
|
+
param_names, param_dims, param_info, k_states
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self._state_names = self._strip_data_names_if_unambiguous(state_names, k_endog)
|
|
172
|
+
self._data_names = self._strip_data_names_if_unambiguous(data_names, k_endog)
|
|
173
|
+
self._shock_names = self._strip_data_names_if_unambiguous(shock_names, k_endog)
|
|
174
|
+
self._param_names = self._strip_data_names_if_unambiguous(param_names, k_endog)
|
|
175
|
+
self._param_dims = param_dims
|
|
176
|
+
|
|
177
|
+
default_coords = make_default_coords(self)
|
|
178
|
+
coords.update(default_coords)
|
|
179
|
+
|
|
180
|
+
self._coords = {
|
|
181
|
+
k: self._strip_data_names_if_unambiguous(v, k_endog) for k, v in coords.items()
|
|
182
|
+
}
|
|
183
|
+
self._param_info = param_info.copy()
|
|
184
|
+
self._data_info = data_info.copy()
|
|
185
|
+
self.measurement_error = measurement_error
|
|
186
|
+
|
|
187
|
+
super().__init__(
|
|
188
|
+
k_endog,
|
|
189
|
+
k_states,
|
|
190
|
+
max(1, k_posdef),
|
|
191
|
+
filter_type=filter_type,
|
|
192
|
+
verbose=verbose,
|
|
193
|
+
measurement_error=measurement_error,
|
|
194
|
+
mode=mode,
|
|
195
|
+
)
|
|
196
|
+
self.ssm = ssm.copy()
|
|
197
|
+
|
|
198
|
+
if k_posdef == 0:
|
|
199
|
+
# If there is no randomness in the model, add dummy matrices to the representation to avoid errors
|
|
200
|
+
# when we go to construct random variables from the matrices
|
|
201
|
+
self.ssm.k_posdef = self.k_posdef
|
|
202
|
+
self.ssm.shapes["state_cov"] = (1, 1, 1)
|
|
203
|
+
self.ssm["state_cov"] = pt.zeros((1, 1, 1))
|
|
204
|
+
|
|
205
|
+
self.ssm.shapes["selection"] = (1, self.k_states, 1)
|
|
206
|
+
self.ssm["selection"] = pt.zeros((1, self.k_states, 1))
|
|
207
|
+
|
|
208
|
+
self._component_info = component_info.copy()
|
|
209
|
+
|
|
210
|
+
self._name_to_variable = name_to_variable.copy()
|
|
211
|
+
self._name_to_data = name_to_data.copy()
|
|
212
|
+
|
|
213
|
+
self._exog_names = exog_names.copy()
|
|
214
|
+
self._needs_exog_data = len(exog_names) > 0
|
|
215
|
+
|
|
216
|
+
P0 = self.make_and_register_variable("P0", shape=(self.k_states, self.k_states))
|
|
217
|
+
self.ssm["initial_state_cov"] = P0
|
|
218
|
+
|
|
219
|
+
def _strip_data_names_if_unambiguous(self, names: list[str], k_endog: int):
|
|
220
|
+
"""
|
|
221
|
+
State names from components should always be of the form name[data_name], in the case that the component is
|
|
222
|
+
associated with multiple observed states. Not doing so leads to ambiguity -- we might have two level states,
|
|
223
|
+
but which goes to which observed component? So we set `level[data_1]` and `level[data_2]`.
|
|
224
|
+
|
|
225
|
+
In cases where there is only one observed state (when k_endog == 1), we can strip the data part and just use
|
|
226
|
+
the state name. This is a bit cleaner.
|
|
227
|
+
"""
|
|
228
|
+
if k_endog == 1:
|
|
229
|
+
[data_name] = self.observed_states
|
|
230
|
+
return [
|
|
231
|
+
name.replace(f"[{data_name}]", "") if isinstance(name, str) else name
|
|
232
|
+
for name in names
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
return names
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _add_inital_state_cov_to_properties(param_names, param_dims, param_info, k_states):
|
|
240
|
+
param_names += ["P0"]
|
|
241
|
+
param_dims["P0"] = (ALL_STATE_DIM, ALL_STATE_AUX_DIM)
|
|
242
|
+
param_info["P0"] = {
|
|
243
|
+
"shape": (k_states, k_states),
|
|
244
|
+
"constraints": "Positive semi-definite",
|
|
245
|
+
"dims": param_dims["P0"],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return param_names, param_dims, param_info
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def param_names(self):
|
|
252
|
+
return self._param_names
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def data_names(self) -> list[str]:
|
|
256
|
+
return self._data_names
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def state_names(self):
|
|
260
|
+
return self._state_names
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def observed_states(self):
|
|
264
|
+
return self._observed_state_names
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def shock_names(self):
|
|
268
|
+
return self._shock_names
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def param_dims(self):
|
|
272
|
+
return self._param_dims
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def coords(self) -> dict[str, Sequence]:
|
|
276
|
+
return self._coords
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def param_info(self) -> dict[str, dict[str, Any]]:
|
|
280
|
+
return self._param_info
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def data_info(self) -> dict[str, dict[str, Any]]:
|
|
284
|
+
return self._data_info
|
|
285
|
+
|
|
286
|
+
def make_symbolic_graph(self) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Assign placeholder pytensor variables among statespace matrices in positions where PyMC variables will go.
|
|
289
|
+
|
|
290
|
+
Notes
|
|
291
|
+
-----
|
|
292
|
+
This assignment is handled by the components, so this function is implemented only to avoid the
|
|
293
|
+
NotImplementedError raised by the base class.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
def _state_slices_from_info(self):
|
|
299
|
+
info = self._component_info.copy()
|
|
300
|
+
comp_states = np.cumsum([0] + [info["k_states"] for info in info.values()])
|
|
301
|
+
state_slices = [slice(i, j) for i, j in pairwise(comp_states)]
|
|
302
|
+
|
|
303
|
+
return state_slices
|
|
304
|
+
|
|
305
|
+
def _hidden_states_from_data(self, data):
|
|
306
|
+
state_slices = self._state_slices_from_info()
|
|
307
|
+
info = self._component_info
|
|
308
|
+
names = info.keys()
|
|
309
|
+
result = []
|
|
310
|
+
|
|
311
|
+
for i, (name, s) in enumerate(zip(names, state_slices)):
|
|
312
|
+
obs_idx = info[name]["obs_state_idx"]
|
|
313
|
+
|
|
314
|
+
if obs_idx is None:
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
X = data[..., s]
|
|
318
|
+
|
|
319
|
+
if info[name]["combine_hidden_states"]:
|
|
320
|
+
sum_idx_joined = np.flatnonzero(obs_idx)
|
|
321
|
+
sum_idx_split = np.split(sum_idx_joined, info[name]["k_endog"])
|
|
322
|
+
for sum_idx in sum_idx_split:
|
|
323
|
+
result.append(X[..., sum_idx].sum(axis=-1)[..., None])
|
|
324
|
+
else:
|
|
325
|
+
n_components = len(self.state_names[s])
|
|
326
|
+
for j in range(n_components):
|
|
327
|
+
result.append(X[..., j, None])
|
|
328
|
+
|
|
329
|
+
return np.concatenate(result, axis=-1)
|
|
330
|
+
|
|
331
|
+
def _get_subcomponent_names(self):
|
|
332
|
+
state_slices = self._state_slices_from_info()
|
|
333
|
+
info = self._component_info
|
|
334
|
+
names = info.keys()
|
|
335
|
+
result = []
|
|
336
|
+
|
|
337
|
+
for i, (name, s) in enumerate(zip(names, state_slices)):
|
|
338
|
+
if info[name]["combine_hidden_states"]:
|
|
339
|
+
if self.k_endog == 1:
|
|
340
|
+
result.append(name)
|
|
341
|
+
else:
|
|
342
|
+
# If there are multiple observed states, we will combine per hidden state, preserving the
|
|
343
|
+
# observed state names. Note this happens even if this *component* has only 1 state for consistency,
|
|
344
|
+
# as long as the statespace model has multiple observed states.
|
|
345
|
+
result.extend(
|
|
346
|
+
[f"{name}[{obs_name}]" for obs_name in info[name]["observed_state_names"]]
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
comp_names = self.state_names[s]
|
|
350
|
+
result.extend([f"{name}[{comp_name}]" for comp_name in comp_names])
|
|
351
|
+
return result
|
|
352
|
+
|
|
353
|
+
def extract_components_from_idata(self, idata: xr.Dataset) -> xr.Dataset:
|
|
354
|
+
r"""
|
|
355
|
+
Extract interpretable hidden states from an InferenceData returned by a PyMCStateSpace sampling method
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
idata: Dataset
|
|
360
|
+
A Dataset object, returned by a PyMCStateSpace sampling method
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
idata: Dataset
|
|
365
|
+
A Dataset object with hidden states transformed to represent only the "interpretable" subcomponents
|
|
366
|
+
of the structural model.
|
|
367
|
+
|
|
368
|
+
Notes
|
|
369
|
+
-----
|
|
370
|
+
In general, a structural statespace model can be represented as:
|
|
371
|
+
|
|
372
|
+
.. math::
|
|
373
|
+
y_t = \mu_t + \nu_t + \cdots + \gamma_t + c_t + \xi_t + \epsilon_t \tag{1}
|
|
374
|
+
|
|
375
|
+
Where:
|
|
376
|
+
|
|
377
|
+
- :math:`\mu_t` is the level of the data at time t
|
|
378
|
+
- :math:`\nu_t` is the slope of the data at time t
|
|
379
|
+
- :math:`\cdots` are higher time derivatives of the position (acceleration, jerk, etc) at time t
|
|
380
|
+
- :math:`\gamma_t` is the seasonal component at time t
|
|
381
|
+
- :math:`c_t` is the cycle component at time t
|
|
382
|
+
- :math:`\xi_t` is the autoregressive error at time t
|
|
383
|
+
- :math:`\varepsilon_t` is the measurement error at time t
|
|
384
|
+
|
|
385
|
+
In state space form, some or all of these components are represented as linear combinations of other
|
|
386
|
+
subcomponents, making interpretation of the outputs of the outputs difficult. The purpose of this function is
|
|
387
|
+
to take the expended statespace representation and return a "reduced form" of only the components shown in
|
|
388
|
+
equation (1).
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
def _extract_and_transform_variable(idata, new_state_names):
|
|
392
|
+
*_, time_dim, state_dim = idata.dims
|
|
393
|
+
state_func = ft.partial(self._hidden_states_from_data)
|
|
394
|
+
new_idata = xr.apply_ufunc(
|
|
395
|
+
state_func,
|
|
396
|
+
idata,
|
|
397
|
+
input_core_dims=[[time_dim, state_dim]],
|
|
398
|
+
output_core_dims=[[time_dim, state_dim]],
|
|
399
|
+
exclude_dims={state_dim},
|
|
400
|
+
)
|
|
401
|
+
new_idata.coords.update({state_dim: new_state_names})
|
|
402
|
+
return new_idata
|
|
403
|
+
|
|
404
|
+
var_names = list(idata.data_vars.keys())
|
|
405
|
+
is_latent = [idata[name].shape[-1] == self.k_states for name in var_names]
|
|
406
|
+
new_state_names = self._get_subcomponent_names()
|
|
407
|
+
|
|
408
|
+
latent_names = [name for latent, name in zip(is_latent, var_names) if latent]
|
|
409
|
+
dropped_vars = set(var_names) - set(latent_names)
|
|
410
|
+
if len(dropped_vars) > 0:
|
|
411
|
+
_log.warning(
|
|
412
|
+
f"Variables {', '.join(dropped_vars)} do not contain all hidden states (their last dimension "
|
|
413
|
+
f"is not {self.k_states}). They will not be present in the modified idata."
|
|
414
|
+
)
|
|
415
|
+
if len(dropped_vars) == len(var_names):
|
|
416
|
+
raise ValueError(
|
|
417
|
+
"Provided idata had no variables with all hidden states; cannot extract components."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
idata_new = xr.Dataset(
|
|
421
|
+
{
|
|
422
|
+
name: _extract_and_transform_variable(idata[name], new_state_names)
|
|
423
|
+
for name in latent_names
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
return idata_new
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class Component:
|
|
430
|
+
r"""
|
|
431
|
+
Base class for a component of a structural timeseries model.
|
|
432
|
+
|
|
433
|
+
This base class contains a subset of the class attributes of the PyMCStateSpace class, and none of the class
|
|
434
|
+
methods. The purpose of a component is to allow the partial definition of a structural model. Components are
|
|
435
|
+
assembled into a full model by the StructuralTimeSeries class.
|
|
436
|
+
|
|
437
|
+
Parameters
|
|
438
|
+
----------
|
|
439
|
+
name : str
|
|
440
|
+
The name of the component.
|
|
441
|
+
k_endog : int
|
|
442
|
+
Number of endogenous (observed) variables being modeled.
|
|
443
|
+
k_states : int
|
|
444
|
+
Number of hidden states in the component model.
|
|
445
|
+
k_posdef : int
|
|
446
|
+
Rank of the state covariance matrix, or the number of sources of innovations
|
|
447
|
+
in the component model.
|
|
448
|
+
state_names : list[str] | None, optional
|
|
449
|
+
Names of the hidden states. If None, defaults to empty list.
|
|
450
|
+
observed_state_names : list[str] | None, optional
|
|
451
|
+
Names of the observed states associated with this component. Must have the same
|
|
452
|
+
length as k_endog. If None, defaults to empty list.
|
|
453
|
+
data_names : list[str] | None, optional
|
|
454
|
+
Names of data variables expected by the component. If None, defaults to empty list.
|
|
455
|
+
shock_names : list[str] | None, optional
|
|
456
|
+
Names of innovation/shock processes. If None, defaults to empty list.
|
|
457
|
+
param_names : list[str] | None, optional
|
|
458
|
+
Names of component parameters. If None, defaults to empty list.
|
|
459
|
+
exog_names : list[str] | None, optional
|
|
460
|
+
Names of exogenous variables. If None, defaults to empty list.
|
|
461
|
+
representation : PytensorRepresentation | None, optional
|
|
462
|
+
Pre-existing state space representation. If None, creates a new one.
|
|
463
|
+
measurement_error : bool, optional
|
|
464
|
+
Whether the component includes measurement error. Default is False.
|
|
465
|
+
combine_hidden_states : bool, optional
|
|
466
|
+
Whether to combine hidden states when extracting from data. Should be True for
|
|
467
|
+
components where individual states have no interpretation (e.g., seasonal,
|
|
468
|
+
autoregressive). Default is True.
|
|
469
|
+
component_from_sum : bool, optional
|
|
470
|
+
Whether this component is created from combining other components. Default is False.
|
|
471
|
+
obs_state_idxs : np.ndarray | None, optional
|
|
472
|
+
Indices indicating which states contribute to observed variables. If None,
|
|
473
|
+
defaults to None.
|
|
474
|
+
share_states : bool, optional
|
|
475
|
+
Whether states are shared across multiple endogenous variables in multivariate
|
|
476
|
+
models. When True, the same latent states affect all observed variables.
|
|
477
|
+
Default is False.
|
|
478
|
+
|
|
479
|
+
Examples
|
|
480
|
+
--------
|
|
481
|
+
Create a simple trend component:
|
|
482
|
+
|
|
483
|
+
.. code:: python
|
|
484
|
+
|
|
485
|
+
from pymc_extras.statespace import structural as st
|
|
486
|
+
|
|
487
|
+
trend = st.LevelTrendComponent(order=2, innovations_order=1)
|
|
488
|
+
seasonal = st.TimeSeasonality(season_length=12, innovations=True)
|
|
489
|
+
model = (trend + seasonal).build()
|
|
490
|
+
|
|
491
|
+
print(f"Model has {model.k_states} states and {model.k_posdef} innovations")
|
|
492
|
+
|
|
493
|
+
See Also
|
|
494
|
+
--------
|
|
495
|
+
StructuralTimeSeries : The complete model class that combines components.
|
|
496
|
+
LevelTrendComponent : Component for modeling level and trend.
|
|
497
|
+
TimeSeasonality : Component for seasonal effects.
|
|
498
|
+
CycleComponent : Component for cyclical effects.
|
|
499
|
+
RegressionComponent : Component for regression effects.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
def __init__(
|
|
503
|
+
self,
|
|
504
|
+
name,
|
|
505
|
+
k_endog,
|
|
506
|
+
k_states,
|
|
507
|
+
k_posdef,
|
|
508
|
+
state_names=None,
|
|
509
|
+
observed_state_names=None,
|
|
510
|
+
data_names=None,
|
|
511
|
+
shock_names=None,
|
|
512
|
+
param_names=None,
|
|
513
|
+
exog_names=None,
|
|
514
|
+
representation: PytensorRepresentation | None = None,
|
|
515
|
+
measurement_error=False,
|
|
516
|
+
combine_hidden_states=True,
|
|
517
|
+
component_from_sum=False,
|
|
518
|
+
obs_state_idxs=None,
|
|
519
|
+
share_states: bool = False,
|
|
520
|
+
):
|
|
521
|
+
self.name = name
|
|
522
|
+
self.k_endog = k_endog
|
|
523
|
+
self.k_states = k_states
|
|
524
|
+
self.share_states = share_states
|
|
525
|
+
self.k_posdef = k_posdef
|
|
526
|
+
self.measurement_error = measurement_error
|
|
527
|
+
|
|
528
|
+
self.state_names = list(state_names) if state_names is not None else []
|
|
529
|
+
self.observed_state_names = (
|
|
530
|
+
list(observed_state_names) if observed_state_names is not None else []
|
|
531
|
+
)
|
|
532
|
+
self.data_names = list(data_names) if data_names is not None else []
|
|
533
|
+
self.shock_names = list(shock_names) if shock_names is not None else []
|
|
534
|
+
self.param_names = list(param_names) if param_names is not None else []
|
|
535
|
+
self.exog_names = list(exog_names) if exog_names is not None else []
|
|
536
|
+
|
|
537
|
+
self.needs_exog_data = len(self.exog_names) > 0
|
|
538
|
+
self.coords = {}
|
|
539
|
+
self.param_dims = {}
|
|
540
|
+
|
|
541
|
+
self.param_info = {}
|
|
542
|
+
self.data_info = {}
|
|
543
|
+
|
|
544
|
+
self.param_counts = {}
|
|
545
|
+
|
|
546
|
+
if representation is None:
|
|
547
|
+
self.ssm = PytensorRepresentation(k_endog=k_endog, k_states=k_states, k_posdef=k_posdef)
|
|
548
|
+
else:
|
|
549
|
+
self.ssm = representation
|
|
550
|
+
|
|
551
|
+
self._name_to_variable = {}
|
|
552
|
+
self._name_to_data = {}
|
|
553
|
+
|
|
554
|
+
if not component_from_sum:
|
|
555
|
+
self.populate_component_properties()
|
|
556
|
+
self.make_symbolic_graph()
|
|
557
|
+
|
|
558
|
+
self._component_info = {
|
|
559
|
+
self.name: {
|
|
560
|
+
"k_states": self.k_states,
|
|
561
|
+
"k_endog": self.k_endog,
|
|
562
|
+
"k_posdef": self.k_posdef,
|
|
563
|
+
"observed_state_names": self.observed_state_names,
|
|
564
|
+
"combine_hidden_states": combine_hidden_states,
|
|
565
|
+
"obs_state_idx": obs_state_idxs,
|
|
566
|
+
"share_states": self.share_states,
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
def make_and_register_variable(self, name, shape, dtype=floatX) -> Variable:
|
|
571
|
+
r"""
|
|
572
|
+
Helper function to create a pytensor symbolic variable and register it in the _name_to_variable dictionary
|
|
573
|
+
|
|
574
|
+
Parameters
|
|
575
|
+
----------
|
|
576
|
+
name : str
|
|
577
|
+
The name of the placeholder variable. Must be the name of a model parameter.
|
|
578
|
+
shape : int or tuple of int
|
|
579
|
+
Shape of the parameter
|
|
580
|
+
dtype : str, default pytensor.config.floatX
|
|
581
|
+
dtype of the parameter
|
|
582
|
+
|
|
583
|
+
Notes
|
|
584
|
+
-----
|
|
585
|
+
Symbolic pytensor variables are used in the ``make_symbolic_graph`` method as placeholders for PyMC random
|
|
586
|
+
variables. The change is made in the ``_insert_random_variables`` method via ``pytensor.graph_replace``. To
|
|
587
|
+
make the change, a dictionary mapping pytensor variables to PyMC random variables needs to be constructed.
|
|
588
|
+
|
|
589
|
+
The purpose of this method is to:
|
|
590
|
+
1. Create the placeholder symbolic variables
|
|
591
|
+
2. Register the placeholder variable in the ``_name_to_variable`` dictionary
|
|
592
|
+
|
|
593
|
+
The shape provided here will define the shape of the prior that will need to be provided by the user.
|
|
594
|
+
|
|
595
|
+
An error is raised if the provided name has already been registered, or if the name is not present in the
|
|
596
|
+
``param_names`` property.
|
|
597
|
+
"""
|
|
598
|
+
if name not in self.param_names:
|
|
599
|
+
raise ValueError(
|
|
600
|
+
f"{name} is not a model parameter. All placeholder variables should correspond to model "
|
|
601
|
+
f"parameters."
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
if name in self._name_to_variable.keys():
|
|
605
|
+
raise ValueError(
|
|
606
|
+
f"{name} is already a registered placeholder variable with shape "
|
|
607
|
+
f"{self._name_to_variable[name].type.shape}"
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
placeholder = pt.tensor(name, shape=shape, dtype=dtype)
|
|
611
|
+
self._name_to_variable[name] = placeholder
|
|
612
|
+
return placeholder
|
|
613
|
+
|
|
614
|
+
def make_and_register_data(self, name, shape, dtype=floatX) -> Variable:
|
|
615
|
+
r"""
|
|
616
|
+
Helper function to create a pytensor symbolic variable and register it in the _name_to_data dictionary
|
|
617
|
+
|
|
618
|
+
Parameters
|
|
619
|
+
----------
|
|
620
|
+
name : str
|
|
621
|
+
The name of the placeholder data. Must be the name of an expected data variable.
|
|
622
|
+
shape : int or tuple of int
|
|
623
|
+
Shape of the parameter
|
|
624
|
+
dtype : str, default pytensor.config.floatX
|
|
625
|
+
dtype of the parameter
|
|
626
|
+
|
|
627
|
+
Notes
|
|
628
|
+
-----
|
|
629
|
+
See docstring for make_and_register_variable for more details. This function is similar, but handles data
|
|
630
|
+
inputs instead of model parameters.
|
|
631
|
+
|
|
632
|
+
An error is raised if the provided name has already been registered, or if the name is not present in the
|
|
633
|
+
``data_names`` property.
|
|
634
|
+
"""
|
|
635
|
+
if name not in self.data_names:
|
|
636
|
+
raise ValueError(
|
|
637
|
+
f"{name} is not a model parameter. All placeholder variables should correspond to model "
|
|
638
|
+
f"parameters."
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
if name in self._name_to_data.keys():
|
|
642
|
+
raise ValueError(
|
|
643
|
+
f"{name} is already a registered placeholder variable with shape "
|
|
644
|
+
f"{self._name_to_data[name].type.shape}"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
placeholder = pt.tensor(name, shape=shape, dtype=dtype)
|
|
648
|
+
self._name_to_data[name] = placeholder
|
|
649
|
+
return placeholder
|
|
650
|
+
|
|
651
|
+
def make_symbolic_graph(self) -> None:
|
|
652
|
+
raise NotImplementedError
|
|
653
|
+
|
|
654
|
+
def populate_component_properties(self):
|
|
655
|
+
raise NotImplementedError
|
|
656
|
+
|
|
657
|
+
def _get_combined_shapes(self, other):
|
|
658
|
+
k_states = self.k_states + other.k_states
|
|
659
|
+
k_posdef = self.k_posdef + other.k_posdef
|
|
660
|
+
|
|
661
|
+
# To count endog states, we have to count unique names between the two components.
|
|
662
|
+
combined_states = self._combine_property(
|
|
663
|
+
other, "observed_state_names", allow_duplicates=False
|
|
664
|
+
)
|
|
665
|
+
k_endog = len(combined_states)
|
|
666
|
+
|
|
667
|
+
return k_states, k_posdef, k_endog
|
|
668
|
+
|
|
669
|
+
def _combine_statespace_representations(self, other):
|
|
670
|
+
def make_slice(name, x, o_x):
|
|
671
|
+
ndim = max(x.ndim, o_x.ndim)
|
|
672
|
+
return (name,) + (slice(None, None, None),) * ndim
|
|
673
|
+
|
|
674
|
+
k_states, k_posdef, k_endog = self._get_combined_shapes(other)
|
|
675
|
+
|
|
676
|
+
self_matrices = [self.ssm[name] for name in LONG_MATRIX_NAMES]
|
|
677
|
+
other_matrices = [other.ssm[name] for name in LONG_MATRIX_NAMES]
|
|
678
|
+
|
|
679
|
+
self_observed_states = self.observed_state_names
|
|
680
|
+
other_observed_states = other.observed_state_names
|
|
681
|
+
|
|
682
|
+
x0, P0, c, d, T, Z, R, H, Q = (
|
|
683
|
+
self.ssm[make_slice(name, x, o_x)]
|
|
684
|
+
for name, x, o_x in zip(LONG_MATRIX_NAMES, self_matrices, other_matrices)
|
|
685
|
+
)
|
|
686
|
+
o_x0, o_P0, o_c, o_d, o_T, o_Z, o_R, o_H, o_Q = (
|
|
687
|
+
other.ssm[make_slice(name, x, o_x)]
|
|
688
|
+
for name, x, o_x in zip(LONG_MATRIX_NAMES, self_matrices, other_matrices)
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
initial_state = pt.concatenate(conform_time_varying_and_time_invariant_matrices(x0, o_x0))
|
|
692
|
+
initial_state.name = x0.name
|
|
693
|
+
|
|
694
|
+
initial_state_cov = pt.linalg.block_diag(P0, o_P0)
|
|
695
|
+
initial_state_cov.name = P0.name
|
|
696
|
+
|
|
697
|
+
state_intercept = pt.concatenate(conform_time_varying_and_time_invariant_matrices(c, o_c))
|
|
698
|
+
state_intercept.name = c.name
|
|
699
|
+
|
|
700
|
+
obs_intercept = add_tensors_by_dim_labels(
|
|
701
|
+
d, o_d, labels=self_observed_states, other_labels=other_observed_states, labeled_axis=-1
|
|
702
|
+
)
|
|
703
|
+
obs_intercept.name = d.name
|
|
704
|
+
|
|
705
|
+
transition = pt.linalg.block_diag(T, o_T)
|
|
706
|
+
transition = pt.specify_shape(
|
|
707
|
+
transition,
|
|
708
|
+
shape=[
|
|
709
|
+
sum(shapes) if not any([s is None for s in shapes]) else None
|
|
710
|
+
for shapes in zip(*[T.type.shape, o_T.type.shape])
|
|
711
|
+
],
|
|
712
|
+
)
|
|
713
|
+
transition.name = T.name
|
|
714
|
+
|
|
715
|
+
design = join_tensors_by_dim_labels(
|
|
716
|
+
*conform_time_varying_and_time_invariant_matrices(Z, o_Z),
|
|
717
|
+
labels=self_observed_states,
|
|
718
|
+
other_labels=other_observed_states,
|
|
719
|
+
labeled_axis=-2,
|
|
720
|
+
join_axis=-1,
|
|
721
|
+
)
|
|
722
|
+
design.name = Z.name
|
|
723
|
+
|
|
724
|
+
selection = pt.linalg.block_diag(R, o_R)
|
|
725
|
+
selection = pt.specify_shape(
|
|
726
|
+
selection,
|
|
727
|
+
shape=[
|
|
728
|
+
sum(shapes) if not any([s is None for s in shapes]) else None
|
|
729
|
+
for shapes in zip(*[R.type.shape, o_R.type.shape])
|
|
730
|
+
],
|
|
731
|
+
)
|
|
732
|
+
selection.name = R.name
|
|
733
|
+
|
|
734
|
+
obs_cov = add_tensors_by_dim_labels(
|
|
735
|
+
H,
|
|
736
|
+
o_H,
|
|
737
|
+
labels=self_observed_states,
|
|
738
|
+
other_labels=other_observed_states,
|
|
739
|
+
labeled_axis=(-1, -2),
|
|
740
|
+
)
|
|
741
|
+
obs_cov.name = H.name
|
|
742
|
+
|
|
743
|
+
state_cov = pt.linalg.block_diag(Q, o_Q)
|
|
744
|
+
state_cov.name = Q.name
|
|
745
|
+
|
|
746
|
+
new_ssm = PytensorRepresentation(
|
|
747
|
+
k_endog=k_endog,
|
|
748
|
+
k_states=k_states,
|
|
749
|
+
k_posdef=k_posdef,
|
|
750
|
+
initial_state=initial_state,
|
|
751
|
+
initial_state_cov=initial_state_cov,
|
|
752
|
+
state_intercept=state_intercept,
|
|
753
|
+
obs_intercept=obs_intercept,
|
|
754
|
+
transition=transition,
|
|
755
|
+
design=design,
|
|
756
|
+
selection=selection,
|
|
757
|
+
obs_cov=obs_cov,
|
|
758
|
+
state_cov=state_cov,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
return new_ssm
|
|
762
|
+
|
|
763
|
+
def _combine_property(self, other, name, allow_duplicates=True):
|
|
764
|
+
self_prop = getattr(self, name)
|
|
765
|
+
other_prop = getattr(other, name)
|
|
766
|
+
|
|
767
|
+
if not isinstance(self_prop, type(other_prop)):
|
|
768
|
+
raise TypeError(
|
|
769
|
+
f"Property {name} of {self} and {other} are not the same and cannot be combined. Found "
|
|
770
|
+
f"{type(self_prop)} for {self} and {type(other_prop)} for {other}'"
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
if not isinstance(self_prop, list | dict):
|
|
774
|
+
raise TypeError(
|
|
775
|
+
f"All component properties are expected to be lists or dicts, but found {type(self_prop)}"
|
|
776
|
+
f"for property {name} of {self} and {type(other_prop)} for {other}'"
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
if isinstance(self_prop, list) and allow_duplicates:
|
|
780
|
+
return self_prop + other_prop
|
|
781
|
+
elif isinstance(self_prop, list) and not allow_duplicates:
|
|
782
|
+
return self_prop + [x for x in other_prop if x not in self_prop]
|
|
783
|
+
elif isinstance(self_prop, dict):
|
|
784
|
+
new_prop = self_prop.copy()
|
|
785
|
+
new_prop.update(other_prop)
|
|
786
|
+
return new_prop
|
|
787
|
+
|
|
788
|
+
def _combine_component_info(self, other):
|
|
789
|
+
combined_info = {}
|
|
790
|
+
for key, value in self._component_info.items():
|
|
791
|
+
if not key.startswith("StateSpace"):
|
|
792
|
+
if key in combined_info.keys():
|
|
793
|
+
raise ValueError(f"Found duplicate component named {key}")
|
|
794
|
+
combined_info[key] = value
|
|
795
|
+
|
|
796
|
+
for key, value in other._component_info.items():
|
|
797
|
+
if not key.startswith("StateSpace"):
|
|
798
|
+
if key in combined_info.keys():
|
|
799
|
+
raise ValueError(f"Found duplicate component named {key}")
|
|
800
|
+
combined_info[key] = value
|
|
801
|
+
|
|
802
|
+
return combined_info
|
|
803
|
+
|
|
804
|
+
def _make_combined_name(self):
|
|
805
|
+
components = self._component_info.keys()
|
|
806
|
+
name = f"StateSpace[{', '.join(components)}]"
|
|
807
|
+
return name
|
|
808
|
+
|
|
809
|
+
def __add__(self, other):
|
|
810
|
+
state_names = self._combine_property(other, "state_names")
|
|
811
|
+
data_names = self._combine_property(other, "data_names")
|
|
812
|
+
observed_state_names = self._combine_property(
|
|
813
|
+
other, "observed_state_names", allow_duplicates=False
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
param_names = self._combine_property(other, "param_names")
|
|
817
|
+
shock_names = self._combine_property(other, "shock_names")
|
|
818
|
+
param_info = self._combine_property(other, "param_info")
|
|
819
|
+
data_info = self._combine_property(other, "data_info")
|
|
820
|
+
param_dims = self._combine_property(other, "param_dims")
|
|
821
|
+
coords = self._combine_property(other, "coords")
|
|
822
|
+
exog_names = self._combine_property(other, "exog_names")
|
|
823
|
+
|
|
824
|
+
_name_to_variable = self._combine_property(other, "_name_to_variable")
|
|
825
|
+
_name_to_data = self._combine_property(other, "_name_to_data")
|
|
826
|
+
|
|
827
|
+
measurement_error = any([self.measurement_error, other.measurement_error])
|
|
828
|
+
|
|
829
|
+
k_states, k_posdef, k_endog = self._get_combined_shapes(other)
|
|
830
|
+
|
|
831
|
+
ssm = self._combine_statespace_representations(other)
|
|
832
|
+
|
|
833
|
+
new_comp = Component(
|
|
834
|
+
name="",
|
|
835
|
+
k_endog=k_endog,
|
|
836
|
+
k_states=k_states,
|
|
837
|
+
k_posdef=k_posdef,
|
|
838
|
+
observed_state_names=observed_state_names,
|
|
839
|
+
measurement_error=measurement_error,
|
|
840
|
+
representation=ssm,
|
|
841
|
+
component_from_sum=True,
|
|
842
|
+
)
|
|
843
|
+
new_comp._component_info = self._combine_component_info(other)
|
|
844
|
+
new_comp.name = new_comp._make_combined_name()
|
|
845
|
+
|
|
846
|
+
names_and_props = [
|
|
847
|
+
("state_names", state_names),
|
|
848
|
+
("observed_state_names", observed_state_names),
|
|
849
|
+
("data_names", data_names),
|
|
850
|
+
("param_names", param_names),
|
|
851
|
+
("shock_names", shock_names),
|
|
852
|
+
("param_dims", param_dims),
|
|
853
|
+
("coords", coords),
|
|
854
|
+
("param_dims", param_dims),
|
|
855
|
+
("param_info", param_info),
|
|
856
|
+
("data_info", data_info),
|
|
857
|
+
("exog_names", exog_names),
|
|
858
|
+
("_name_to_variable", _name_to_variable),
|
|
859
|
+
("_name_to_data", _name_to_data),
|
|
860
|
+
]
|
|
861
|
+
|
|
862
|
+
for prop, value in names_and_props:
|
|
863
|
+
setattr(new_comp, prop, value)
|
|
864
|
+
|
|
865
|
+
return new_comp
|
|
866
|
+
|
|
867
|
+
def build(
|
|
868
|
+
self, name=None, filter_type="standard", verbose=True, mode: str | Mode | None = None
|
|
869
|
+
):
|
|
870
|
+
"""
|
|
871
|
+
Build a StructuralTimeSeries statespace model from the current component(s)
|
|
872
|
+
|
|
873
|
+
Parameters
|
|
874
|
+
----------
|
|
875
|
+
name: str, optional
|
|
876
|
+
Name of the exogenous data being modeled. Default is "data"
|
|
877
|
+
|
|
878
|
+
filter_type : str, optional
|
|
879
|
+
The type of Kalman filter to use. Valid options are "standard", "univariate", "single", "cholesky", and
|
|
880
|
+
"steady_state". For more information, see the docs for each filter. Default is "standard".
|
|
881
|
+
|
|
882
|
+
verbose : bool, optional
|
|
883
|
+
If True, displays information about the initialized model. Defaults to True.
|
|
884
|
+
|
|
885
|
+
mode: str or Mode, optional
|
|
886
|
+
Pytensor compile mode, used in auxiliary sampling methods such as ``sample_conditional_posterior`` and
|
|
887
|
+
``forecast``. The mode does **not** effect calls to ``pm.sample``.
|
|
888
|
+
|
|
889
|
+
Regardless of whether a mode is specified, it can always be overwritten via the ``compile_kwargs`` argument
|
|
890
|
+
to all sampling methods.
|
|
891
|
+
|
|
892
|
+
Returns
|
|
893
|
+
-------
|
|
894
|
+
PyMCStateSpace
|
|
895
|
+
An initialized instance of a PyMCStateSpace, constructed using the system matrices contained in the
|
|
896
|
+
components.
|
|
897
|
+
"""
|
|
898
|
+
|
|
899
|
+
return StructuralTimeSeries(
|
|
900
|
+
self.ssm,
|
|
901
|
+
name=name,
|
|
902
|
+
state_names=self.state_names,
|
|
903
|
+
observed_state_names=self.observed_state_names,
|
|
904
|
+
data_names=self.data_names,
|
|
905
|
+
shock_names=self.shock_names,
|
|
906
|
+
param_names=self.param_names,
|
|
907
|
+
param_dims=self.param_dims,
|
|
908
|
+
coords=self.coords,
|
|
909
|
+
param_info=self.param_info,
|
|
910
|
+
data_info=self.data_info,
|
|
911
|
+
component_info=self._component_info,
|
|
912
|
+
measurement_error=self.measurement_error,
|
|
913
|
+
exog_names=self.exog_names,
|
|
914
|
+
name_to_variable=self._name_to_variable,
|
|
915
|
+
name_to_data=self._name_to_data,
|
|
916
|
+
filter_type=filter_type,
|
|
917
|
+
verbose=verbose,
|
|
918
|
+
mode=mode,
|
|
919
|
+
)
|