pymc-extras 0.2.7__py3-none-any.whl → 0.4.0__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/inference/__init__.py +2 -2
- pymc_extras/inference/fit.py +1 -1
- pymc_extras/inference/laplace_approx/__init__.py +0 -0
- pymc_extras/inference/laplace_approx/find_map.py +354 -0
- pymc_extras/inference/laplace_approx/idata.py +393 -0
- pymc_extras/inference/laplace_approx/laplace.py +453 -0
- pymc_extras/inference/laplace_approx/scipy_interface.py +242 -0
- pymc_extras/inference/pathfinder/pathfinder.py +3 -4
- pymc_extras/linearmodel.py +3 -1
- pymc_extras/model/marginal/graph_analysis.py +4 -0
- pymc_extras/prior.py +38 -6
- pymc_extras/statespace/core/statespace.py +78 -52
- pymc_extras/statespace/filters/kalman_smoother.py +1 -1
- 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 +188 -0
- pymc_extras/statespace/models/structural/components/cycle.py +305 -0
- pymc_extras/statespace/models/structural/components/level_trend.py +257 -0
- pymc_extras/statespace/models/structural/components/measurement_error.py +137 -0
- pymc_extras/statespace/models/structural/components/regression.py +228 -0
- pymc_extras/statespace/models/structural/components/seasonality.py +445 -0
- pymc_extras/statespace/models/structural/core.py +900 -0
- pymc_extras/statespace/models/structural/utils.py +16 -0
- pymc_extras/statespace/models/utilities.py +285 -0
- pymc_extras/statespace/utils/constants.py +4 -4
- pymc_extras/statespace/utils/data_tools.py +3 -2
- {pymc_extras-0.2.7.dist-info → pymc_extras-0.4.0.dist-info}/METADATA +6 -6
- {pymc_extras-0.2.7.dist-info → pymc_extras-0.4.0.dist-info}/RECORD +30 -18
- pymc_extras/inference/find_map.py +0 -496
- pymc_extras/inference/laplace.py +0 -583
- pymc_extras/statespace/models/structural.py +0 -1679
- {pymc_extras-0.2.7.dist-info → pymc_extras-0.4.0.dist-info}/WHEEL +0 -0
- {pymc_extras-0.2.7.dist-info → pymc_extras-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,1679 +0,0 @@
|
|
|
1
|
-
import functools as ft
|
|
2
|
-
import logging
|
|
3
|
-
|
|
4
|
-
from abc import ABC
|
|
5
|
-
from collections.abc import Sequence
|
|
6
|
-
from itertools import pairwise
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
10
|
-
import pytensor
|
|
11
|
-
import pytensor.tensor as pt
|
|
12
|
-
import xarray as xr
|
|
13
|
-
|
|
14
|
-
from pytensor import Variable
|
|
15
|
-
from pytensor.compile.mode import Mode
|
|
16
|
-
|
|
17
|
-
from pymc_extras.statespace.core import PytensorRepresentation
|
|
18
|
-
from pymc_extras.statespace.core.statespace import PyMCStateSpace
|
|
19
|
-
from pymc_extras.statespace.models.utilities import (
|
|
20
|
-
conform_time_varying_and_time_invariant_matrices,
|
|
21
|
-
make_default_coords,
|
|
22
|
-
)
|
|
23
|
-
from pymc_extras.statespace.utils.constants import (
|
|
24
|
-
ALL_STATE_AUX_DIM,
|
|
25
|
-
ALL_STATE_DIM,
|
|
26
|
-
AR_PARAM_DIM,
|
|
27
|
-
LONG_MATRIX_NAMES,
|
|
28
|
-
POSITION_DERIVATIVE_NAMES,
|
|
29
|
-
TIME_DIM,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
_log = logging.getLogger("pymc.experimental.statespace")
|
|
33
|
-
|
|
34
|
-
floatX = pytensor.config.floatX
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def order_to_mask(order):
|
|
38
|
-
if isinstance(order, int):
|
|
39
|
-
return np.ones(order).astype(bool)
|
|
40
|
-
else:
|
|
41
|
-
return np.array(order).astype(bool)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _frequency_transition_block(s, j):
|
|
45
|
-
lam = 2 * np.pi * j / s
|
|
46
|
-
|
|
47
|
-
return pt.stack([[pt.cos(lam), pt.sin(lam)], [-pt.sin(lam), pt.cos(lam)]])
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class StructuralTimeSeries(PyMCStateSpace):
|
|
51
|
-
r"""
|
|
52
|
-
Structural Time Series Model
|
|
53
|
-
|
|
54
|
-
The structural time series model, named by [1] and presented in statespace form in [2], is a framework for
|
|
55
|
-
decomposing a univariate time series into level, trend, seasonal, and cycle components. It also admits the
|
|
56
|
-
possibility of exogenous regressors. Unlike the SARIMAX framework, the time series is not assumed to be stationary.
|
|
57
|
-
|
|
58
|
-
Notes
|
|
59
|
-
-----
|
|
60
|
-
|
|
61
|
-
.. math::
|
|
62
|
-
y_t = \mu_t + \gamma_t + c_t + \varepsilon_t
|
|
63
|
-
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
def __init__(
|
|
67
|
-
self,
|
|
68
|
-
ssm: PytensorRepresentation,
|
|
69
|
-
state_names: list[str],
|
|
70
|
-
data_names: list[str],
|
|
71
|
-
shock_names: list[str],
|
|
72
|
-
param_names: list[str],
|
|
73
|
-
exog_names: list[str],
|
|
74
|
-
param_dims: dict[str, tuple[int]],
|
|
75
|
-
coords: dict[str, Sequence],
|
|
76
|
-
param_info: dict[str, dict[str, Any]],
|
|
77
|
-
data_info: dict[str, dict[str, Any]],
|
|
78
|
-
component_info: dict[str, dict[str, Any]],
|
|
79
|
-
measurement_error: bool,
|
|
80
|
-
name_to_variable: dict[str, Variable],
|
|
81
|
-
name_to_data: dict[str, Variable] | None = None,
|
|
82
|
-
name: str | None = None,
|
|
83
|
-
verbose: bool = True,
|
|
84
|
-
filter_type: str = "standard",
|
|
85
|
-
mode: str | Mode | None = None,
|
|
86
|
-
):
|
|
87
|
-
# Add the initial state covariance to the parameters
|
|
88
|
-
if name is None:
|
|
89
|
-
name = "data"
|
|
90
|
-
self._name = name
|
|
91
|
-
|
|
92
|
-
k_states, k_posdef, k_endog = ssm.k_states, ssm.k_posdef, ssm.k_endog
|
|
93
|
-
param_names, param_dims, param_info = self._add_inital_state_cov_to_properties(
|
|
94
|
-
param_names, param_dims, param_info, k_states
|
|
95
|
-
)
|
|
96
|
-
self._state_names = state_names.copy()
|
|
97
|
-
self._data_names = data_names.copy()
|
|
98
|
-
self._shock_names = shock_names.copy()
|
|
99
|
-
self._param_names = param_names.copy()
|
|
100
|
-
self._param_dims = param_dims.copy()
|
|
101
|
-
|
|
102
|
-
default_coords = make_default_coords(self)
|
|
103
|
-
coords.update(default_coords)
|
|
104
|
-
|
|
105
|
-
self._coords = coords
|
|
106
|
-
self._param_info = param_info.copy()
|
|
107
|
-
self._data_info = data_info.copy()
|
|
108
|
-
self.measurement_error = measurement_error
|
|
109
|
-
|
|
110
|
-
super().__init__(
|
|
111
|
-
k_endog,
|
|
112
|
-
k_states,
|
|
113
|
-
max(1, k_posdef),
|
|
114
|
-
filter_type=filter_type,
|
|
115
|
-
verbose=verbose,
|
|
116
|
-
measurement_error=measurement_error,
|
|
117
|
-
mode=mode,
|
|
118
|
-
)
|
|
119
|
-
self.ssm = ssm.copy()
|
|
120
|
-
|
|
121
|
-
if k_posdef == 0:
|
|
122
|
-
# If there is no randomness in the model, add dummy matrices to the representation to avoid errors
|
|
123
|
-
# when we go to construct random variables from the matrices
|
|
124
|
-
self.ssm.k_posdef = self.k_posdef
|
|
125
|
-
self.ssm.shapes["state_cov"] = (1, 1, 1)
|
|
126
|
-
self.ssm["state_cov"] = pt.zeros((1, 1, 1))
|
|
127
|
-
|
|
128
|
-
self.ssm.shapes["selection"] = (1, self.k_states, 1)
|
|
129
|
-
self.ssm["selection"] = pt.zeros((1, self.k_states, 1))
|
|
130
|
-
|
|
131
|
-
self._component_info = component_info.copy()
|
|
132
|
-
|
|
133
|
-
self._name_to_variable = name_to_variable.copy()
|
|
134
|
-
self._name_to_data = name_to_data.copy()
|
|
135
|
-
|
|
136
|
-
self._exog_names = exog_names.copy()
|
|
137
|
-
self._needs_exog_data = len(exog_names) > 0
|
|
138
|
-
|
|
139
|
-
P0 = self.make_and_register_variable("P0", shape=(self.k_states, self.k_states))
|
|
140
|
-
self.ssm["initial_state_cov"] = P0
|
|
141
|
-
|
|
142
|
-
@staticmethod
|
|
143
|
-
def _add_inital_state_cov_to_properties(param_names, param_dims, param_info, k_states):
|
|
144
|
-
param_names += ["P0"]
|
|
145
|
-
param_dims["P0"] = (ALL_STATE_DIM, ALL_STATE_AUX_DIM)
|
|
146
|
-
param_info["P0"] = {
|
|
147
|
-
"shape": (k_states, k_states),
|
|
148
|
-
"constraints": "Positive semi-definite",
|
|
149
|
-
"dims": param_dims["P0"],
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return param_names, param_dims, param_info
|
|
153
|
-
|
|
154
|
-
@property
|
|
155
|
-
def param_names(self):
|
|
156
|
-
return self._param_names
|
|
157
|
-
|
|
158
|
-
@property
|
|
159
|
-
def data_names(self) -> list[str]:
|
|
160
|
-
return self._data_names
|
|
161
|
-
|
|
162
|
-
@property
|
|
163
|
-
def state_names(self):
|
|
164
|
-
return self._state_names
|
|
165
|
-
|
|
166
|
-
@property
|
|
167
|
-
def observed_states(self):
|
|
168
|
-
return [self._name]
|
|
169
|
-
|
|
170
|
-
@property
|
|
171
|
-
def shock_names(self):
|
|
172
|
-
return self._shock_names
|
|
173
|
-
|
|
174
|
-
@property
|
|
175
|
-
def param_dims(self):
|
|
176
|
-
return self._param_dims
|
|
177
|
-
|
|
178
|
-
@property
|
|
179
|
-
def coords(self) -> dict[str, Sequence]:
|
|
180
|
-
return self._coords
|
|
181
|
-
|
|
182
|
-
@property
|
|
183
|
-
def param_info(self) -> dict[str, dict[str, Any]]:
|
|
184
|
-
return self._param_info
|
|
185
|
-
|
|
186
|
-
@property
|
|
187
|
-
def data_info(self) -> dict[str, dict[str, Any]]:
|
|
188
|
-
return self._data_info
|
|
189
|
-
|
|
190
|
-
def make_symbolic_graph(self) -> None:
|
|
191
|
-
"""
|
|
192
|
-
Assign placeholder pytensor variables among statespace matrices in positions where PyMC variables will go.
|
|
193
|
-
|
|
194
|
-
Notes
|
|
195
|
-
-----
|
|
196
|
-
This assignment is handled by the components, so this function is implemented only to avoid the
|
|
197
|
-
NotImplementedError raised by the base class.
|
|
198
|
-
"""
|
|
199
|
-
|
|
200
|
-
pass
|
|
201
|
-
|
|
202
|
-
def _state_slices_from_info(self):
|
|
203
|
-
info = self._component_info.copy()
|
|
204
|
-
comp_states = np.cumsum([0] + [info["k_states"] for info in info.values()])
|
|
205
|
-
state_slices = [slice(i, j) for i, j in pairwise(comp_states)]
|
|
206
|
-
|
|
207
|
-
return state_slices
|
|
208
|
-
|
|
209
|
-
def _hidden_states_from_data(self, data):
|
|
210
|
-
state_slices = self._state_slices_from_info()
|
|
211
|
-
info = self._component_info
|
|
212
|
-
names = info.keys()
|
|
213
|
-
result = []
|
|
214
|
-
|
|
215
|
-
for i, (name, s) in enumerate(zip(names, state_slices)):
|
|
216
|
-
obs_idx = info[name]["obs_state_idx"]
|
|
217
|
-
if obs_idx is None:
|
|
218
|
-
continue
|
|
219
|
-
|
|
220
|
-
X = data[..., s]
|
|
221
|
-
if info[name]["combine_hidden_states"]:
|
|
222
|
-
sum_idx = np.flatnonzero(obs_idx)
|
|
223
|
-
result.append(X[..., sum_idx].sum(axis=-1)[..., None])
|
|
224
|
-
else:
|
|
225
|
-
comp_names = self.state_names[s]
|
|
226
|
-
for j, state_name in enumerate(comp_names):
|
|
227
|
-
result.append(X[..., j, None])
|
|
228
|
-
|
|
229
|
-
return np.concatenate(result, axis=-1)
|
|
230
|
-
|
|
231
|
-
def _get_subcomponent_names(self):
|
|
232
|
-
state_slices = self._state_slices_from_info()
|
|
233
|
-
info = self._component_info
|
|
234
|
-
names = info.keys()
|
|
235
|
-
result = []
|
|
236
|
-
|
|
237
|
-
for i, (name, s) in enumerate(zip(names, state_slices)):
|
|
238
|
-
if info[name]["combine_hidden_states"]:
|
|
239
|
-
result.append(name)
|
|
240
|
-
else:
|
|
241
|
-
comp_names = self.state_names[s]
|
|
242
|
-
result.extend([f"{name}[{comp_name}]" for comp_name in comp_names])
|
|
243
|
-
return result
|
|
244
|
-
|
|
245
|
-
def extract_components_from_idata(self, idata: xr.Dataset) -> xr.Dataset:
|
|
246
|
-
r"""
|
|
247
|
-
Extract interpretable hidden states from an InferenceData returned by a PyMCStateSpace sampling method
|
|
248
|
-
|
|
249
|
-
Parameters
|
|
250
|
-
----------
|
|
251
|
-
idata: Dataset
|
|
252
|
-
A Dataset object, returned by a PyMCStateSpace sampling method
|
|
253
|
-
|
|
254
|
-
Returns
|
|
255
|
-
-------
|
|
256
|
-
idata: Dataset
|
|
257
|
-
An Dataset object with hidden states transformed to represent only the "interpretable" subcomponents
|
|
258
|
-
of the structural model.
|
|
259
|
-
|
|
260
|
-
Notes
|
|
261
|
-
-----
|
|
262
|
-
In general, a structural statespace model can be represented as:
|
|
263
|
-
|
|
264
|
-
.. math::
|
|
265
|
-
y_t = \mu_t + \nu_t + \cdots + \gamma_t + c_t + \xi_t + \epsilon_t \tag{1}
|
|
266
|
-
|
|
267
|
-
Where:
|
|
268
|
-
|
|
269
|
-
- :math:`\mu_t` is the level of the data at time t
|
|
270
|
-
- :math:`\nu_t` is the slope of the data at time t
|
|
271
|
-
- :math:`\cdots` are higher time derivatives of the position (acceleration, jerk, etc) at time t
|
|
272
|
-
- :math:`\gamma_t` is the seasonal component at time t
|
|
273
|
-
- :math:`c_t` is the cycle component at time t
|
|
274
|
-
- :math:`\xi_t` is the autoregressive error at time t
|
|
275
|
-
- :math:`\varepsilon_t` is the measurement error at time t
|
|
276
|
-
|
|
277
|
-
In state space form, some or all of these components are represented as linear combinations of other
|
|
278
|
-
subcomponents, making interpretation of the outputs of the outputs difficult. The purpose of this function is
|
|
279
|
-
to take the expended statespace representation and return a "reduced form" of only the components shown in
|
|
280
|
-
equation (1).
|
|
281
|
-
"""
|
|
282
|
-
|
|
283
|
-
def _extract_and_transform_variable(idata, new_state_names):
|
|
284
|
-
*_, time_dim, state_dim = idata.dims
|
|
285
|
-
state_func = ft.partial(self._hidden_states_from_data)
|
|
286
|
-
new_idata = xr.apply_ufunc(
|
|
287
|
-
state_func,
|
|
288
|
-
idata,
|
|
289
|
-
input_core_dims=[[time_dim, state_dim]],
|
|
290
|
-
output_core_dims=[[time_dim, state_dim]],
|
|
291
|
-
exclude_dims={state_dim},
|
|
292
|
-
)
|
|
293
|
-
new_idata.coords.update({state_dim: new_state_names})
|
|
294
|
-
return new_idata
|
|
295
|
-
|
|
296
|
-
var_names = list(idata.data_vars.keys())
|
|
297
|
-
is_latent = [idata[name].shape[-1] == self.k_states for name in var_names]
|
|
298
|
-
new_state_names = self._get_subcomponent_names()
|
|
299
|
-
|
|
300
|
-
latent_names = [name for latent, name in zip(is_latent, var_names) if latent]
|
|
301
|
-
dropped_vars = set(var_names) - set(latent_names)
|
|
302
|
-
if len(dropped_vars) > 0:
|
|
303
|
-
_log.warning(
|
|
304
|
-
f'Variables {", ".join(dropped_vars)} do not contain all hidden states (their last dimension '
|
|
305
|
-
f"is not {self.k_states}). They will not be present in the modified idata."
|
|
306
|
-
)
|
|
307
|
-
if len(dropped_vars) == len(var_names):
|
|
308
|
-
raise ValueError(
|
|
309
|
-
"Provided idata had no variables with all hidden states; cannot extract components."
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
idata_new = xr.Dataset(
|
|
313
|
-
{
|
|
314
|
-
name: _extract_and_transform_variable(idata[name], new_state_names)
|
|
315
|
-
for name in latent_names
|
|
316
|
-
}
|
|
317
|
-
)
|
|
318
|
-
return idata_new
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
class Component(ABC):
|
|
322
|
-
r"""
|
|
323
|
-
Base class for a component of a structural timeseries model.
|
|
324
|
-
|
|
325
|
-
This base class contains a subset of the class attributes of the PyMCStateSpace class, and none of the class
|
|
326
|
-
methods. The purpose of a component is to allow the partial definition of a structural model. Components are
|
|
327
|
-
assembled into a full model by the StructuralTimeSeries class.
|
|
328
|
-
|
|
329
|
-
Parameters
|
|
330
|
-
----------
|
|
331
|
-
name: str
|
|
332
|
-
The name of the component
|
|
333
|
-
k_endog: int
|
|
334
|
-
Number of endogenous variables being modeled. Currently, must be one because structural models only support
|
|
335
|
-
univariate data.
|
|
336
|
-
k_states: int
|
|
337
|
-
Number of hidden states in the component model
|
|
338
|
-
k_posdef: int
|
|
339
|
-
Rank of the state covariance matrix, or the number of sources of innovations in the component model
|
|
340
|
-
measurement_error: bool
|
|
341
|
-
Whether the observation associated with the component has measurement error. Default is False.
|
|
342
|
-
combine_hidden_states: bool
|
|
343
|
-
Flag for the ``extract_hidden_states_from_data`` method. When ``True``, hidden states from the component model
|
|
344
|
-
are extracted as ``hidden_states[:, np.flatnonzero(Z)]``. Should be True in models where hidden states
|
|
345
|
-
individually have no interpretation, such as seasonal or autoregressive components.
|
|
346
|
-
"""
|
|
347
|
-
|
|
348
|
-
def __init__(
|
|
349
|
-
self,
|
|
350
|
-
name,
|
|
351
|
-
k_endog,
|
|
352
|
-
k_states,
|
|
353
|
-
k_posdef,
|
|
354
|
-
state_names=None,
|
|
355
|
-
data_names=None,
|
|
356
|
-
shock_names=None,
|
|
357
|
-
param_names=None,
|
|
358
|
-
exog_names=None,
|
|
359
|
-
representation: PytensorRepresentation | None = None,
|
|
360
|
-
measurement_error=False,
|
|
361
|
-
combine_hidden_states=True,
|
|
362
|
-
component_from_sum=False,
|
|
363
|
-
obs_state_idxs=None,
|
|
364
|
-
):
|
|
365
|
-
self.name = name
|
|
366
|
-
self.k_endog = k_endog
|
|
367
|
-
self.k_states = k_states
|
|
368
|
-
self.k_posdef = k_posdef
|
|
369
|
-
self.measurement_error = measurement_error
|
|
370
|
-
|
|
371
|
-
self.state_names = state_names if state_names is not None else []
|
|
372
|
-
self.data_names = data_names if data_names is not None else []
|
|
373
|
-
self.shock_names = shock_names if shock_names is not None else []
|
|
374
|
-
self.param_names = param_names if param_names is not None else []
|
|
375
|
-
self.exog_names = exog_names if exog_names is not None else []
|
|
376
|
-
|
|
377
|
-
self.needs_exog_data = len(self.exog_names) > 0
|
|
378
|
-
self.coords = {}
|
|
379
|
-
self.param_dims = {}
|
|
380
|
-
|
|
381
|
-
self.param_info = {}
|
|
382
|
-
self.data_info = {}
|
|
383
|
-
|
|
384
|
-
self.param_counts = {}
|
|
385
|
-
|
|
386
|
-
if representation is None:
|
|
387
|
-
self.ssm = PytensorRepresentation(k_endog=k_endog, k_states=k_states, k_posdef=k_posdef)
|
|
388
|
-
else:
|
|
389
|
-
self.ssm = representation
|
|
390
|
-
|
|
391
|
-
self._name_to_variable = {}
|
|
392
|
-
self._name_to_data = {}
|
|
393
|
-
|
|
394
|
-
if not component_from_sum:
|
|
395
|
-
self.populate_component_properties()
|
|
396
|
-
self.make_symbolic_graph()
|
|
397
|
-
|
|
398
|
-
self._component_info = {
|
|
399
|
-
self.name: {
|
|
400
|
-
"k_states": self.k_states,
|
|
401
|
-
"k_enodg": self.k_endog,
|
|
402
|
-
"k_posdef": self.k_posdef,
|
|
403
|
-
"combine_hidden_states": combine_hidden_states,
|
|
404
|
-
"obs_state_idx": obs_state_idxs,
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
def make_and_register_variable(self, name, shape, dtype=floatX) -> Variable:
|
|
409
|
-
r"""
|
|
410
|
-
Helper function to create a pytensor symbolic variable and register it in the _name_to_variable dictionary
|
|
411
|
-
|
|
412
|
-
Parameters
|
|
413
|
-
----------
|
|
414
|
-
name : str
|
|
415
|
-
The name of the placeholder variable. Must be the name of a model parameter.
|
|
416
|
-
shape : int or tuple of int
|
|
417
|
-
Shape of the parameter
|
|
418
|
-
dtype : str, default pytensor.config.floatX
|
|
419
|
-
dtype of the parameter
|
|
420
|
-
|
|
421
|
-
Notes
|
|
422
|
-
-----
|
|
423
|
-
Symbolic pytensor variables are used in the ``make_symbolic_graph`` method as placeholders for PyMC random
|
|
424
|
-
variables. The change is made in the ``_insert_random_variables`` method via ``pytensor.graph_replace``. To
|
|
425
|
-
make the change, a dictionary mapping pytensor variables to PyMC random variables needs to be constructed.
|
|
426
|
-
|
|
427
|
-
The purpose of this method is to:
|
|
428
|
-
1. Create the placeholder symbolic variables
|
|
429
|
-
2. Register the placeholder variable in the ``_name_to_variable`` dictionary
|
|
430
|
-
|
|
431
|
-
The shape provided here will define the shape of the prior that will need to be provided by the user.
|
|
432
|
-
|
|
433
|
-
An error is raised if the provided name has already been registered, or if the name is not present in the
|
|
434
|
-
``param_names`` property.
|
|
435
|
-
"""
|
|
436
|
-
if name not in self.param_names:
|
|
437
|
-
raise ValueError(
|
|
438
|
-
f"{name} is not a model parameter. All placeholder variables should correspond to model "
|
|
439
|
-
f"parameters."
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
if name in self._name_to_variable.keys():
|
|
443
|
-
raise ValueError(
|
|
444
|
-
f"{name} is already a registered placeholder variable with shape "
|
|
445
|
-
f"{self._name_to_variable[name].type.shape}"
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
placeholder = pt.tensor(name, shape=shape, dtype=dtype)
|
|
449
|
-
self._name_to_variable[name] = placeholder
|
|
450
|
-
return placeholder
|
|
451
|
-
|
|
452
|
-
def make_and_register_data(self, name, shape, dtype=floatX) -> Variable:
|
|
453
|
-
r"""
|
|
454
|
-
Helper function to create a pytensor symbolic variable and register it in the _name_to_data dictionary
|
|
455
|
-
|
|
456
|
-
Parameters
|
|
457
|
-
----------
|
|
458
|
-
name : str
|
|
459
|
-
The name of the placeholder data. Must be the name of an expected data variable.
|
|
460
|
-
shape : int or tuple of int
|
|
461
|
-
Shape of the parameter
|
|
462
|
-
dtype : str, default pytensor.config.floatX
|
|
463
|
-
dtype of the parameter
|
|
464
|
-
|
|
465
|
-
Notes
|
|
466
|
-
-----
|
|
467
|
-
See docstring for make_and_register_variable for more details. This function is similar, but handles data
|
|
468
|
-
inputs instead of model parameters.
|
|
469
|
-
|
|
470
|
-
An error is raised if the provided name has already been registered, or if the name is not present in the
|
|
471
|
-
``data_names`` property.
|
|
472
|
-
"""
|
|
473
|
-
if name not in self.data_names:
|
|
474
|
-
raise ValueError(
|
|
475
|
-
f"{name} is not a model parameter. All placeholder variables should correspond to model "
|
|
476
|
-
f"parameters."
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
if name in self._name_to_data.keys():
|
|
480
|
-
raise ValueError(
|
|
481
|
-
f"{name} is already a registered placeholder variable with shape "
|
|
482
|
-
f"{self._name_to_data[name].type.shape}"
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
placeholder = pt.tensor(name, shape=shape, dtype=dtype)
|
|
486
|
-
self._name_to_data[name] = placeholder
|
|
487
|
-
return placeholder
|
|
488
|
-
|
|
489
|
-
def make_symbolic_graph(self) -> None:
|
|
490
|
-
raise NotImplementedError
|
|
491
|
-
|
|
492
|
-
def populate_component_properties(self):
|
|
493
|
-
raise NotImplementedError
|
|
494
|
-
|
|
495
|
-
def _get_combined_shapes(self, other):
|
|
496
|
-
k_states = self.k_states + other.k_states
|
|
497
|
-
k_posdef = self.k_posdef + other.k_posdef
|
|
498
|
-
if self.k_endog != other.k_endog:
|
|
499
|
-
raise NotImplementedError(
|
|
500
|
-
"Merging elements with different numbers of observed states is not supported.>"
|
|
501
|
-
)
|
|
502
|
-
k_endog = self.k_endog
|
|
503
|
-
|
|
504
|
-
return k_states, k_posdef, k_endog
|
|
505
|
-
|
|
506
|
-
def _combine_statespace_representations(self, other):
|
|
507
|
-
def make_slice(name, x, o_x):
|
|
508
|
-
ndim = max(x.ndim, o_x.ndim)
|
|
509
|
-
return (name,) + (slice(None, None, None),) * ndim
|
|
510
|
-
|
|
511
|
-
k_states, k_posdef, k_endog = self._get_combined_shapes(other)
|
|
512
|
-
|
|
513
|
-
self_matrices = [self.ssm[name] for name in LONG_MATRIX_NAMES]
|
|
514
|
-
other_matrices = [other.ssm[name] for name in LONG_MATRIX_NAMES]
|
|
515
|
-
|
|
516
|
-
x0, P0, c, d, T, Z, R, H, Q = (
|
|
517
|
-
self.ssm[make_slice(name, x, o_x)]
|
|
518
|
-
for name, x, o_x in zip(LONG_MATRIX_NAMES, self_matrices, other_matrices)
|
|
519
|
-
)
|
|
520
|
-
o_x0, o_P0, o_c, o_d, o_T, o_Z, o_R, o_H, o_Q = (
|
|
521
|
-
other.ssm[make_slice(name, x, o_x)]
|
|
522
|
-
for name, x, o_x in zip(LONG_MATRIX_NAMES, self_matrices, other_matrices)
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
initial_state = pt.concatenate(conform_time_varying_and_time_invariant_matrices(x0, o_x0))
|
|
526
|
-
initial_state.name = x0.name
|
|
527
|
-
|
|
528
|
-
initial_state_cov = pt.linalg.block_diag(P0, o_P0)
|
|
529
|
-
initial_state_cov.name = P0.name
|
|
530
|
-
|
|
531
|
-
state_intercept = pt.concatenate(conform_time_varying_and_time_invariant_matrices(c, o_c))
|
|
532
|
-
state_intercept.name = c.name
|
|
533
|
-
|
|
534
|
-
obs_intercept = d + o_d
|
|
535
|
-
obs_intercept.name = d.name
|
|
536
|
-
|
|
537
|
-
transition = pt.linalg.block_diag(T, o_T)
|
|
538
|
-
transition.name = T.name
|
|
539
|
-
|
|
540
|
-
design = pt.concatenate(conform_time_varying_and_time_invariant_matrices(Z, o_Z), axis=-1)
|
|
541
|
-
design.name = Z.name
|
|
542
|
-
|
|
543
|
-
selection = pt.linalg.block_diag(R, o_R)
|
|
544
|
-
selection.name = R.name
|
|
545
|
-
|
|
546
|
-
obs_cov = H + o_H
|
|
547
|
-
obs_cov.name = H.name
|
|
548
|
-
|
|
549
|
-
state_cov = pt.linalg.block_diag(Q, o_Q)
|
|
550
|
-
state_cov.name = Q.name
|
|
551
|
-
|
|
552
|
-
new_ssm = PytensorRepresentation(
|
|
553
|
-
k_endog=k_endog,
|
|
554
|
-
k_states=k_states,
|
|
555
|
-
k_posdef=k_posdef,
|
|
556
|
-
initial_state=initial_state,
|
|
557
|
-
initial_state_cov=initial_state_cov,
|
|
558
|
-
state_intercept=state_intercept,
|
|
559
|
-
obs_intercept=obs_intercept,
|
|
560
|
-
transition=transition,
|
|
561
|
-
design=design,
|
|
562
|
-
selection=selection,
|
|
563
|
-
obs_cov=obs_cov,
|
|
564
|
-
state_cov=state_cov,
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
return new_ssm
|
|
568
|
-
|
|
569
|
-
def _combine_property(self, other, name):
|
|
570
|
-
self_prop = getattr(self, name)
|
|
571
|
-
if isinstance(self_prop, list):
|
|
572
|
-
return self_prop + getattr(other, name)
|
|
573
|
-
elif isinstance(self_prop, dict):
|
|
574
|
-
new_prop = self_prop.copy()
|
|
575
|
-
new_prop.update(getattr(other, name))
|
|
576
|
-
return new_prop
|
|
577
|
-
|
|
578
|
-
def _combine_component_info(self, other):
|
|
579
|
-
combined_info = {}
|
|
580
|
-
for key, value in self._component_info.items():
|
|
581
|
-
if not key.startswith("StateSpace"):
|
|
582
|
-
if key in combined_info.keys():
|
|
583
|
-
raise ValueError(f"Found duplicate component named {key}")
|
|
584
|
-
combined_info[key] = value
|
|
585
|
-
|
|
586
|
-
for key, value in other._component_info.items():
|
|
587
|
-
if not key.startswith("StateSpace"):
|
|
588
|
-
if key in combined_info.keys():
|
|
589
|
-
raise ValueError(f"Found duplicate component named {key}")
|
|
590
|
-
combined_info[key] = value
|
|
591
|
-
|
|
592
|
-
return combined_info
|
|
593
|
-
|
|
594
|
-
def _make_combined_name(self):
|
|
595
|
-
components = self._component_info.keys()
|
|
596
|
-
name = f'StateSpace[{", ".join(components)}]'
|
|
597
|
-
return name
|
|
598
|
-
|
|
599
|
-
def __add__(self, other):
|
|
600
|
-
state_names = self._combine_property(other, "state_names")
|
|
601
|
-
data_names = self._combine_property(other, "data_names")
|
|
602
|
-
param_names = self._combine_property(other, "param_names")
|
|
603
|
-
shock_names = self._combine_property(other, "shock_names")
|
|
604
|
-
param_info = self._combine_property(other, "param_info")
|
|
605
|
-
data_info = self._combine_property(other, "data_info")
|
|
606
|
-
param_dims = self._combine_property(other, "param_dims")
|
|
607
|
-
coords = self._combine_property(other, "coords")
|
|
608
|
-
exog_names = self._combine_property(other, "exog_names")
|
|
609
|
-
|
|
610
|
-
_name_to_variable = self._combine_property(other, "_name_to_variable")
|
|
611
|
-
_name_to_data = self._combine_property(other, "_name_to_data")
|
|
612
|
-
|
|
613
|
-
measurement_error = any([self.measurement_error, other.measurement_error])
|
|
614
|
-
|
|
615
|
-
k_states, k_posdef, k_endog = self._get_combined_shapes(other)
|
|
616
|
-
ssm = self._combine_statespace_representations(other)
|
|
617
|
-
|
|
618
|
-
new_comp = Component(
|
|
619
|
-
name="",
|
|
620
|
-
k_endog=1,
|
|
621
|
-
k_states=k_states,
|
|
622
|
-
k_posdef=k_posdef,
|
|
623
|
-
measurement_error=measurement_error,
|
|
624
|
-
representation=ssm,
|
|
625
|
-
component_from_sum=True,
|
|
626
|
-
)
|
|
627
|
-
new_comp._component_info = self._combine_component_info(other)
|
|
628
|
-
new_comp.name = new_comp._make_combined_name()
|
|
629
|
-
|
|
630
|
-
names_and_props = [
|
|
631
|
-
("state_names", state_names),
|
|
632
|
-
("data_names", data_names),
|
|
633
|
-
("param_names", param_names),
|
|
634
|
-
("shock_names", shock_names),
|
|
635
|
-
("param_dims", param_dims),
|
|
636
|
-
("coords", coords),
|
|
637
|
-
("param_dims", param_dims),
|
|
638
|
-
("param_info", param_info),
|
|
639
|
-
("data_info", data_info),
|
|
640
|
-
("exog_names", exog_names),
|
|
641
|
-
("_name_to_variable", _name_to_variable),
|
|
642
|
-
("_name_to_data", _name_to_data),
|
|
643
|
-
]
|
|
644
|
-
|
|
645
|
-
for prop, value in names_and_props:
|
|
646
|
-
setattr(new_comp, prop, value)
|
|
647
|
-
|
|
648
|
-
return new_comp
|
|
649
|
-
|
|
650
|
-
def build(
|
|
651
|
-
self, name=None, filter_type="standard", verbose=True, mode: str | Mode | None = None
|
|
652
|
-
):
|
|
653
|
-
"""
|
|
654
|
-
Build a StructuralTimeSeries statespace model from the current component(s)
|
|
655
|
-
|
|
656
|
-
Parameters
|
|
657
|
-
----------
|
|
658
|
-
name: str, optional
|
|
659
|
-
Name of the exogenous data being modeled. Default is "data"
|
|
660
|
-
|
|
661
|
-
filter_type : str, optional
|
|
662
|
-
The type of Kalman filter to use. Valid options are "standard", "univariate", "single", "cholesky", and
|
|
663
|
-
"steady_state". For more information, see the docs for each filter. Default is "standard".
|
|
664
|
-
|
|
665
|
-
verbose : bool, optional
|
|
666
|
-
If True, displays information about the initialized model. Defaults to True.
|
|
667
|
-
|
|
668
|
-
mode: str or Mode, optional
|
|
669
|
-
Pytensor compile mode, used in auxiliary sampling methods such as ``sample_conditional_posterior`` and
|
|
670
|
-
``forecast``. The mode does **not** effect calls to ``pm.sample``.
|
|
671
|
-
|
|
672
|
-
Regardless of whether a mode is specified, it can always be overwritten via the ``compile_kwargs`` argument
|
|
673
|
-
to all sampling methods.
|
|
674
|
-
|
|
675
|
-
Returns
|
|
676
|
-
-------
|
|
677
|
-
PyMCStateSpace
|
|
678
|
-
An initialized instance of a PyMCStateSpace, constructed using the system matrices contained in the
|
|
679
|
-
components.
|
|
680
|
-
"""
|
|
681
|
-
|
|
682
|
-
return StructuralTimeSeries(
|
|
683
|
-
self.ssm,
|
|
684
|
-
name=name,
|
|
685
|
-
state_names=self.state_names,
|
|
686
|
-
data_names=self.data_names,
|
|
687
|
-
shock_names=self.shock_names,
|
|
688
|
-
param_names=self.param_names,
|
|
689
|
-
param_dims=self.param_dims,
|
|
690
|
-
coords=self.coords,
|
|
691
|
-
param_info=self.param_info,
|
|
692
|
-
data_info=self.data_info,
|
|
693
|
-
component_info=self._component_info,
|
|
694
|
-
measurement_error=self.measurement_error,
|
|
695
|
-
exog_names=self.exog_names,
|
|
696
|
-
name_to_variable=self._name_to_variable,
|
|
697
|
-
name_to_data=self._name_to_data,
|
|
698
|
-
filter_type=filter_type,
|
|
699
|
-
verbose=verbose,
|
|
700
|
-
mode=mode,
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
class LevelTrendComponent(Component):
|
|
705
|
-
r"""
|
|
706
|
-
Level and trend component of a structural time series model
|
|
707
|
-
|
|
708
|
-
Parameters
|
|
709
|
-
----------
|
|
710
|
-
__________
|
|
711
|
-
order : int
|
|
712
|
-
|
|
713
|
-
Number of time derivatives of the trend to include in the model. For example, when order=3, the trend will
|
|
714
|
-
be of the form ``y = a + b * t + c * t ** 2``, where the coefficients ``a, b, c`` come from the initial
|
|
715
|
-
state values.
|
|
716
|
-
|
|
717
|
-
innovations_order : int or sequence of int, optional
|
|
718
|
-
|
|
719
|
-
The number of stochastic innovations to include in the model. By default, ``innovations_order = order``
|
|
720
|
-
|
|
721
|
-
Notes
|
|
722
|
-
-----
|
|
723
|
-
This class implements the level and trend components of the general structural time series model. In the most
|
|
724
|
-
general form, the level and trend is described by a system of two time-varying equations.
|
|
725
|
-
|
|
726
|
-
.. math::
|
|
727
|
-
\begin{align}
|
|
728
|
-
\mu_{t+1} &= \mu_t + \nu_t + \zeta_t \\
|
|
729
|
-
\nu_{t+1} &= \nu_t + \xi_t
|
|
730
|
-
\zeta_t &\sim N(0, \sigma_\zeta) \\
|
|
731
|
-
\xi_t &\sim N(0, \sigma_\xi)
|
|
732
|
-
\end{align}
|
|
733
|
-
|
|
734
|
-
Where :math:`\mu_{t+1}` is the mean of the timeseries at time t, and :math:`\nu_t` is the drift or the slope of
|
|
735
|
-
the process. When both innovations :math:`\zeta_t` and :math:`\xi_t` are included in the model, it is known as a
|
|
736
|
-
*local linear trend* model. This system of two equations, corresponding to ``order=2``, can be expanded or
|
|
737
|
-
contracted by adding or removing equations. ``order=3`` would add an acceleration term to the sytsem:
|
|
738
|
-
|
|
739
|
-
.. math::
|
|
740
|
-
\begin{align}
|
|
741
|
-
\mu_{t+1} &= \mu_t + \nu_t + \zeta_t \\
|
|
742
|
-
\nu_{t+1} &= \nu_t + \eta_t + \xi_t \\
|
|
743
|
-
\eta_{t+1} &= \eta_{t-1} + \omega_t \\
|
|
744
|
-
\zeta_t &\sim N(0, \sigma_\zeta) \\
|
|
745
|
-
\xi_t &\sim N(0, \sigma_\xi) \\
|
|
746
|
-
\omega_t &\sim N(0, \sigma_\omega)
|
|
747
|
-
\end{align}
|
|
748
|
-
|
|
749
|
-
After setting all innovation terms to zero and defining initial states :math:`\mu_0, \nu_0, \eta_0`, these equations
|
|
750
|
-
can be collapsed to:
|
|
751
|
-
|
|
752
|
-
.. math::
|
|
753
|
-
\mu_t = \mu_0 + \nu_0 \cdot t + \eta_0 \cdot t^2
|
|
754
|
-
|
|
755
|
-
Which clarifies how the order and initial states influence the model. In particular, the initial states are the
|
|
756
|
-
coefficients on the intercept, slope, acceleration, and so on.
|
|
757
|
-
|
|
758
|
-
In this light, allowing for innovations can be understood as allowing these coefficients to vary over time. Each
|
|
759
|
-
component can be individually selected for time variation by passing a list to the ``innovations_order`` argument.
|
|
760
|
-
For example, a constant intercept with time varying trend and acceleration is specified as ``order=3,
|
|
761
|
-
innovations_order=[0, 1, 1]``.
|
|
762
|
-
|
|
763
|
-
By choosing the ``order`` and ``innovations_order``, a large variety of models can be obtained. Notable
|
|
764
|
-
models include:
|
|
765
|
-
|
|
766
|
-
* Constant intercept, ``order=1, innovations_order=0``
|
|
767
|
-
|
|
768
|
-
.. math::
|
|
769
|
-
\mu_t = \mu
|
|
770
|
-
|
|
771
|
-
* Constant linear slope, ``order=2, innovations_order=0``
|
|
772
|
-
|
|
773
|
-
.. math::
|
|
774
|
-
\mu_t = \mu_{t-1} + \nu
|
|
775
|
-
|
|
776
|
-
* Gaussian Random Walk, ``order=1, innovations_order=1``
|
|
777
|
-
|
|
778
|
-
.. math::
|
|
779
|
-
\mu_t = \mu_{t-1} + \zeta_t
|
|
780
|
-
|
|
781
|
-
* Gaussian Random Walk with Drift, ``order=2, innovations_order=1``
|
|
782
|
-
|
|
783
|
-
.. math::
|
|
784
|
-
\mu_t = \mu_{t-1} + \nu + \zeta_t
|
|
785
|
-
|
|
786
|
-
* Smooth Trend, ``order=2, innovations_order=[0, 1]``
|
|
787
|
-
|
|
788
|
-
.. math::
|
|
789
|
-
\begin{align}
|
|
790
|
-
\mu_t &= \mu_{t-1} + \nu_{t-1} \\
|
|
791
|
-
\nu_t &= \nu_{t-1} + \xi_t
|
|
792
|
-
\end{align}
|
|
793
|
-
|
|
794
|
-
* Local Level, ``order=2, innovations_order=2``
|
|
795
|
-
|
|
796
|
-
[1] notes that the smooth trend model produces more gradually changing slopes than the full local linear trend
|
|
797
|
-
model, and is equivalent to an "integrated trend model".
|
|
798
|
-
|
|
799
|
-
References
|
|
800
|
-
----------
|
|
801
|
-
.. [1] Durbin, James, and Siem Jan Koopman. 2012.
|
|
802
|
-
Time Series Analysis by State Space Methods: Second Edition.
|
|
803
|
-
Oxford University Press.
|
|
804
|
-
|
|
805
|
-
"""
|
|
806
|
-
|
|
807
|
-
def __init__(
|
|
808
|
-
self,
|
|
809
|
-
order: int | list[int] = 2,
|
|
810
|
-
innovations_order: int | list[int] | None = None,
|
|
811
|
-
name: str = "LevelTrend",
|
|
812
|
-
):
|
|
813
|
-
if innovations_order is None:
|
|
814
|
-
innovations_order = order
|
|
815
|
-
|
|
816
|
-
self._order_mask = order_to_mask(order)
|
|
817
|
-
max_state = np.flatnonzero(self._order_mask)[-1].item() + 1
|
|
818
|
-
|
|
819
|
-
# If the user passes excess zeros, raise an error. The alternative is to prune them, but this would cause
|
|
820
|
-
# the shape of the state to be different to what the user expects.
|
|
821
|
-
if len(self._order_mask) > max_state:
|
|
822
|
-
raise ValueError(
|
|
823
|
-
f"order={order} is invalid. The highest derivative should not be set to zero. If you want a "
|
|
824
|
-
f"lower order model, explicitly omit the zeros."
|
|
825
|
-
)
|
|
826
|
-
k_states = max_state
|
|
827
|
-
|
|
828
|
-
if isinstance(innovations_order, int):
|
|
829
|
-
n = innovations_order
|
|
830
|
-
innovations_order = order_to_mask(k_states)
|
|
831
|
-
if n > 0:
|
|
832
|
-
innovations_order[n:] = False
|
|
833
|
-
else:
|
|
834
|
-
innovations_order[:] = False
|
|
835
|
-
else:
|
|
836
|
-
innovations_order = order_to_mask(innovations_order)
|
|
837
|
-
|
|
838
|
-
self.innovations_order = innovations_order[:max_state]
|
|
839
|
-
k_posdef = int(sum(innovations_order))
|
|
840
|
-
|
|
841
|
-
super().__init__(
|
|
842
|
-
name,
|
|
843
|
-
k_endog=1,
|
|
844
|
-
k_states=k_states,
|
|
845
|
-
k_posdef=k_posdef,
|
|
846
|
-
measurement_error=False,
|
|
847
|
-
combine_hidden_states=False,
|
|
848
|
-
obs_state_idxs=np.array([1.0] + [0.0] * (k_states - 1)),
|
|
849
|
-
)
|
|
850
|
-
|
|
851
|
-
def populate_component_properties(self):
|
|
852
|
-
name_slice = POSITION_DERIVATIVE_NAMES[: self.k_states]
|
|
853
|
-
self.param_names = ["initial_trend"]
|
|
854
|
-
self.state_names = [name for name, mask in zip(name_slice, self._order_mask) if mask]
|
|
855
|
-
self.param_dims = {"initial_trend": ("trend_state",)}
|
|
856
|
-
self.coords = {"trend_state": self.state_names}
|
|
857
|
-
self.param_info = {"initial_trend": {"shape": (self.k_states,), "constraints": None}}
|
|
858
|
-
|
|
859
|
-
if self.k_posdef > 0:
|
|
860
|
-
self.param_names += ["sigma_trend"]
|
|
861
|
-
self.shock_names = [
|
|
862
|
-
name for name, mask in zip(name_slice, self.innovations_order) if mask
|
|
863
|
-
]
|
|
864
|
-
self.param_dims["sigma_trend"] = ("trend_shock",)
|
|
865
|
-
self.coords["trend_shock"] = self.shock_names
|
|
866
|
-
self.param_info["sigma_trend"] = {"shape": (self.k_posdef,), "constraints": "Positive"}
|
|
867
|
-
|
|
868
|
-
for name in self.param_names:
|
|
869
|
-
self.param_info[name]["dims"] = self.param_dims[name]
|
|
870
|
-
|
|
871
|
-
def make_symbolic_graph(self) -> None:
|
|
872
|
-
initial_trend = self.make_and_register_variable("initial_trend", shape=(self.k_states,))
|
|
873
|
-
self.ssm["initial_state", :] = initial_trend
|
|
874
|
-
triu_idx = np.triu_indices(self.k_states)
|
|
875
|
-
self.ssm[np.s_["transition", triu_idx[0], triu_idx[1]]] = 1
|
|
876
|
-
|
|
877
|
-
R = np.eye(self.k_states)
|
|
878
|
-
R = R[:, self.innovations_order]
|
|
879
|
-
self.ssm["selection", :, :] = R
|
|
880
|
-
|
|
881
|
-
self.ssm["design", 0, :] = np.array([1.0] + [0.0] * (self.k_states - 1))
|
|
882
|
-
|
|
883
|
-
if self.k_posdef > 0:
|
|
884
|
-
sigma_trend = self.make_and_register_variable("sigma_trend", shape=(self.k_posdef,))
|
|
885
|
-
diag_idx = np.diag_indices(self.k_posdef)
|
|
886
|
-
idx = np.s_["state_cov", diag_idx[0], diag_idx[1]]
|
|
887
|
-
self.ssm[idx] = sigma_trend**2
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
class MeasurementError(Component):
|
|
891
|
-
r"""
|
|
892
|
-
Measurement error term for a structural timeseries model
|
|
893
|
-
|
|
894
|
-
Parameters
|
|
895
|
-
----------
|
|
896
|
-
name: str, optional
|
|
897
|
-
|
|
898
|
-
Name of the observed data. Default is "obs".
|
|
899
|
-
|
|
900
|
-
Notes
|
|
901
|
-
-----
|
|
902
|
-
This component should only be used in combination with other components, because it has no states. It's only use
|
|
903
|
-
is to add a variance parameter to the model, associated with the observation noise matrix H.
|
|
904
|
-
|
|
905
|
-
Examples
|
|
906
|
-
--------
|
|
907
|
-
Create and estimate a deterministic linear trend with measurement error
|
|
908
|
-
|
|
909
|
-
.. code:: python
|
|
910
|
-
|
|
911
|
-
from pymc_extras.statespace import structural as st
|
|
912
|
-
import pymc as pm
|
|
913
|
-
import pytensor.tensor as pt
|
|
914
|
-
|
|
915
|
-
trend = st.LevelTrendComponent(order=2, innovations_order=0)
|
|
916
|
-
error = st.MeasurementError()
|
|
917
|
-
ss_mod = (trend + error).build()
|
|
918
|
-
|
|
919
|
-
with pm.Model(coords=ss_mod.coords) as model:
|
|
920
|
-
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
|
|
921
|
-
intitial_trend = pm.Normal('initial_trend', sigma=10, dims=ss_mod.param_dims['initial_trend'])
|
|
922
|
-
sigma_obs = pm.Exponential('sigma_obs', 1, dims=ss_mod.param_dims['sigma_obs'])
|
|
923
|
-
|
|
924
|
-
ss_mod.build_statespace_graph(data)
|
|
925
|
-
idata = pm.sample(nuts_sampler='numpyro')
|
|
926
|
-
"""
|
|
927
|
-
|
|
928
|
-
def __init__(self, name: str = "MeasurementError"):
|
|
929
|
-
k_endog = 1
|
|
930
|
-
k_states = 0
|
|
931
|
-
k_posdef = 0
|
|
932
|
-
|
|
933
|
-
super().__init__(
|
|
934
|
-
name, k_endog, k_states, k_posdef, measurement_error=True, combine_hidden_states=False
|
|
935
|
-
)
|
|
936
|
-
|
|
937
|
-
def populate_component_properties(self):
|
|
938
|
-
self.param_names = [f"sigma_{self.name}"]
|
|
939
|
-
self.param_dims = {}
|
|
940
|
-
self.param_info = {
|
|
941
|
-
f"sigma_{self.name}": {
|
|
942
|
-
"shape": (),
|
|
943
|
-
"constraints": "Positive",
|
|
944
|
-
"dims": None,
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
def make_symbolic_graph(self) -> None:
|
|
949
|
-
sigma_shape = ()
|
|
950
|
-
error_sigma = self.make_and_register_variable(f"sigma_{self.name}", shape=sigma_shape)
|
|
951
|
-
diag_idx = np.diag_indices(self.k_endog)
|
|
952
|
-
idx = np.s_["obs_cov", diag_idx[0], diag_idx[1]]
|
|
953
|
-
self.ssm[idx] = error_sigma**2
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
class AutoregressiveComponent(Component):
|
|
957
|
-
r"""
|
|
958
|
-
Autoregressive timeseries component
|
|
959
|
-
|
|
960
|
-
Parameters
|
|
961
|
-
----------
|
|
962
|
-
order: int or sequence of int
|
|
963
|
-
|
|
964
|
-
If int, the number of lags to include in the model.
|
|
965
|
-
If a sequence, an array-like of zeros and ones indicating which lags to include in the model.
|
|
966
|
-
|
|
967
|
-
Notes
|
|
968
|
-
-----
|
|
969
|
-
An autoregressive component can be thought of as a way o introducing serially correlated errors into the model.
|
|
970
|
-
The process is modeled:
|
|
971
|
-
|
|
972
|
-
.. math::
|
|
973
|
-
x_t = \sum_{i=1}^p \rho_i x_{t-i}
|
|
974
|
-
|
|
975
|
-
Where ``p``, the number of autoregressive terms to model, is the order of the process. By default, all lags up to
|
|
976
|
-
``p`` are included in the model. To disable lags, pass a list of zeros and ones to the ``order`` argumnet. For
|
|
977
|
-
example, ``order=[1, 1, 0, 1]`` would become:
|
|
978
|
-
|
|
979
|
-
.. math::
|
|
980
|
-
x_t = \rho_1 x_{t-1} + \rho_2 x_{t-1} + \rho_4 x_{t-1}
|
|
981
|
-
|
|
982
|
-
The coefficient :math:`\rho_3` has been constrained to zero.
|
|
983
|
-
|
|
984
|
-
.. warning:: This class is meant to be used as a component in a structural time series model. For modeling of
|
|
985
|
-
stationary processes with ARIMA, use ``statespace.BayesianSARIMA``.
|
|
986
|
-
|
|
987
|
-
Examples
|
|
988
|
-
--------
|
|
989
|
-
Model a timeseries as an AR(2) process with non-zero mean:
|
|
990
|
-
|
|
991
|
-
.. code:: python
|
|
992
|
-
|
|
993
|
-
from pymc_extras.statespace import structural as st
|
|
994
|
-
import pymc as pm
|
|
995
|
-
import pytensor.tensor as pt
|
|
996
|
-
|
|
997
|
-
trend = st.LevelTrendComponent(order=1, innovations_order=0)
|
|
998
|
-
ar = st.AutoregressiveComponent(2)
|
|
999
|
-
ss_mod = (trend + ar).build()
|
|
1000
|
-
|
|
1001
|
-
with pm.Model(coords=ss_mod.coords) as model:
|
|
1002
|
-
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
|
|
1003
|
-
intitial_trend = pm.Normal('initial_trend', sigma=10, dims=ss_mod.param_dims['initial_trend'])
|
|
1004
|
-
ar_params = pm.Normal('ar_params', dims=ss_mod.param_dims['ar_params'])
|
|
1005
|
-
sigma_ar = pm.Exponential('sigma_ar', 1, dims=ss_mod.param_dims['sigma_ar'])
|
|
1006
|
-
|
|
1007
|
-
ss_mod.build_statespace_graph(data)
|
|
1008
|
-
idata = pm.sample(nuts_sampler='numpyro')
|
|
1009
|
-
|
|
1010
|
-
"""
|
|
1011
|
-
|
|
1012
|
-
def __init__(self, order: int = 1, name: str = "AutoRegressive"):
|
|
1013
|
-
order = order_to_mask(order)
|
|
1014
|
-
ar_lags = np.flatnonzero(order).ravel().astype(int) + 1
|
|
1015
|
-
k_states = len(order)
|
|
1016
|
-
|
|
1017
|
-
self.order = order
|
|
1018
|
-
self.ar_lags = ar_lags
|
|
1019
|
-
|
|
1020
|
-
super().__init__(
|
|
1021
|
-
name=name,
|
|
1022
|
-
k_endog=1,
|
|
1023
|
-
k_states=k_states,
|
|
1024
|
-
k_posdef=1,
|
|
1025
|
-
measurement_error=True,
|
|
1026
|
-
combine_hidden_states=True,
|
|
1027
|
-
obs_state_idxs=np.r_[[1.0], np.zeros(k_states - 1)],
|
|
1028
|
-
)
|
|
1029
|
-
|
|
1030
|
-
def populate_component_properties(self):
|
|
1031
|
-
self.state_names = [f"L{i + 1}.data" for i in range(self.k_states)]
|
|
1032
|
-
self.shock_names = [f"{self.name}_innovation"]
|
|
1033
|
-
self.param_names = ["ar_params", "sigma_ar"]
|
|
1034
|
-
self.param_dims = {"ar_params": (AR_PARAM_DIM,)}
|
|
1035
|
-
self.coords = {AR_PARAM_DIM: self.ar_lags.tolist()}
|
|
1036
|
-
|
|
1037
|
-
self.param_info = {
|
|
1038
|
-
"ar_params": {
|
|
1039
|
-
"shape": (self.k_states,),
|
|
1040
|
-
"constraints": None,
|
|
1041
|
-
"dims": (AR_PARAM_DIM,),
|
|
1042
|
-
},
|
|
1043
|
-
"sigma_ar": {"shape": (), "constraints": "Positive", "dims": None},
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
def make_symbolic_graph(self) -> None:
|
|
1047
|
-
k_nonzero = int(sum(self.order))
|
|
1048
|
-
ar_params = self.make_and_register_variable("ar_params", shape=(k_nonzero,))
|
|
1049
|
-
sigma_ar = self.make_and_register_variable("sigma_ar", shape=())
|
|
1050
|
-
|
|
1051
|
-
T = np.eye(self.k_states, k=-1)
|
|
1052
|
-
self.ssm["transition", :, :] = T
|
|
1053
|
-
self.ssm["selection", 0, 0] = 1
|
|
1054
|
-
self.ssm["design", 0, 0] = 1
|
|
1055
|
-
|
|
1056
|
-
ar_idx = ("transition", np.zeros(k_nonzero, dtype="int"), np.nonzero(self.order)[0])
|
|
1057
|
-
self.ssm[ar_idx] = ar_params
|
|
1058
|
-
|
|
1059
|
-
cov_idx = ("state_cov", *np.diag_indices(1))
|
|
1060
|
-
self.ssm[cov_idx] = sigma_ar**2
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
class TimeSeasonality(Component):
|
|
1064
|
-
r"""
|
|
1065
|
-
Seasonal component, modeled in the time domain
|
|
1066
|
-
|
|
1067
|
-
Parameters
|
|
1068
|
-
----------
|
|
1069
|
-
season_length: int
|
|
1070
|
-
The number of periods in a single seasonal cycle, e.g. 12 for monthly data with annual seasonal pattern, 7 for
|
|
1071
|
-
daily data with weekly seasonal pattern, etc.
|
|
1072
|
-
|
|
1073
|
-
innovations: bool, default True
|
|
1074
|
-
Whether to include stochastic innovations in the strength of the seasonal effect
|
|
1075
|
-
|
|
1076
|
-
name: str, default None
|
|
1077
|
-
A name for this seasonal component. Used to label dimensions and coordinates. Useful when multiple seasonal
|
|
1078
|
-
components are included in the same model. Default is ``f"Seasonal[s={season_length}]"``
|
|
1079
|
-
|
|
1080
|
-
state_names: list of str, default None
|
|
1081
|
-
List of strings for seasonal effect labels. If provided, it must be of length ``season_length``. An example
|
|
1082
|
-
would be ``state_names = ['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun']`` when data is daily with a weekly
|
|
1083
|
-
seasonal pattern (``season_length = 7``).
|
|
1084
|
-
|
|
1085
|
-
If None, states will be numbered ``[State_0, ..., State_s]``
|
|
1086
|
-
|
|
1087
|
-
remove_first_state: bool, default True
|
|
1088
|
-
If True, the first state will be removed from the model. This is done because there are only n-1 degrees of
|
|
1089
|
-
freedom in the seasonal component, and one state is not identified. If False, the first state will be
|
|
1090
|
-
included in the model, but it will not be identified -- you will need to handle this in the priors (e.g. with
|
|
1091
|
-
ZeroSumNormal).
|
|
1092
|
-
|
|
1093
|
-
Notes
|
|
1094
|
-
-----
|
|
1095
|
-
A seasonal effect is any pattern that repeats every fixed interval. Although there are many possible ways to
|
|
1096
|
-
model seasonal effects, the implementation used here is the one described by [1] as the "canonical" time domain
|
|
1097
|
-
representation. The seasonal component can be expressed:
|
|
1098
|
-
|
|
1099
|
-
.. math::
|
|
1100
|
-
\gamma_t = -\sum_{i=1}^{s-1} \gamma_{t-i} + \omega_t, \quad \omega_t \sim N(0, \sigma_\gamma)
|
|
1101
|
-
|
|
1102
|
-
Where :math:`s` is the ``seasonal_length`` parameter and :math:`\omega_t` is the (optional) stochastic innovation.
|
|
1103
|
-
To give interpretation to the :math:`\gamma` terms, it is helpful to work through the algebra for a simple
|
|
1104
|
-
example. Let :math:`s=4`, and omit the shock term. Define initial conditions :math:`\gamma_0, \gamma_{-1},
|
|
1105
|
-
\gamma_{-2}`. The value of the seasonal component for the first 5 timesteps will be:
|
|
1106
|
-
|
|
1107
|
-
.. math::
|
|
1108
|
-
\begin{align}
|
|
1109
|
-
\gamma_1 &= -\gamma_0 - \gamma_{-1} - \gamma_{-2} \\
|
|
1110
|
-
\gamma_2 &= -\gamma_1 - \gamma_0 - \gamma_{-1} \\
|
|
1111
|
-
&= -(-\gamma_0 - \gamma_{-1} - \gamma_{-2}) - \gamma_0 - \gamma_{-1} \\
|
|
1112
|
-
&= (\gamma_0 - \gamma_0 )+ (\gamma_{-1} - \gamma_{-1}) + \gamma_{-2} \\
|
|
1113
|
-
&= \gamma_{-2} \\
|
|
1114
|
-
\gamma_3 &= -\gamma_2 - \gamma_1 - \gamma_0 \\
|
|
1115
|
-
&= -\gamma_{-2} - (-\gamma_0 - \gamma_{-1} - \gamma_{-2}) - \gamma_0 \\
|
|
1116
|
-
&= (\gamma_{-2} - \gamma_{-2}) + \gamma_{-1} + (\gamma_0 - \gamma_0) \\
|
|
1117
|
-
&= \gamma_{-1} \\
|
|
1118
|
-
\gamma_4 &= -\gamma_3 - \gamma_2 - \gamma_1 \\
|
|
1119
|
-
&= -\gamma_{-1} - \gamma_{-2} -(-\gamma_0 - \gamma_{-1} - \gamma_{-2}) \\
|
|
1120
|
-
&= (\gamma_{-2} - \gamma_{-2}) + (\gamma_{-1} - \gamma_{-1}) + \gamma_0 \\
|
|
1121
|
-
&= \gamma_0 \\
|
|
1122
|
-
\gamma_5 &= -\gamma_4 - \gamma_3 - \gamma_2 \\
|
|
1123
|
-
&= -\gamma_0 - \gamma_{-1} - \gamma_{-2} \\
|
|
1124
|
-
&= \gamma_1
|
|
1125
|
-
\end{align}
|
|
1126
|
-
|
|
1127
|
-
This exercise shows that, given a list ``initial_conditions`` of length ``s-1``, the effects of this model will be:
|
|
1128
|
-
|
|
1129
|
-
- Period 1: ``-sum(initial_conditions)``
|
|
1130
|
-
- Period 2: ``initial_conditions[-1]``
|
|
1131
|
-
- Period 3: ``initial_conditions[-2]``
|
|
1132
|
-
- ...
|
|
1133
|
-
- Period s: ``initial_conditions[0]``
|
|
1134
|
-
- Period s+1: ``-sum(initial_condition)``
|
|
1135
|
-
|
|
1136
|
-
And so on. So for interpretation, the ``season_length - 1`` initial states are, when reversed, the coefficients
|
|
1137
|
-
associated with ``state_names[1:]``.
|
|
1138
|
-
|
|
1139
|
-
.. warning::
|
|
1140
|
-
Although the ``state_names`` argument expects a list of length ``season_length``, only ``state_names[1:]``
|
|
1141
|
-
will be saved as model dimensions, since the 1st coefficient is not identified (it is defined as
|
|
1142
|
-
:math:`-\sum_{i=1}^{s} \gamma_{t-i}`).
|
|
1143
|
-
|
|
1144
|
-
Examples
|
|
1145
|
-
--------
|
|
1146
|
-
Estimate monthly with a model with a gaussian random walk trend and monthly seasonality:
|
|
1147
|
-
|
|
1148
|
-
.. code:: python
|
|
1149
|
-
|
|
1150
|
-
from pymc_extras.statespace import structural as st
|
|
1151
|
-
import pymc as pm
|
|
1152
|
-
import pytensor.tensor as pt
|
|
1153
|
-
import pandas as pd
|
|
1154
|
-
|
|
1155
|
-
# Get month names
|
|
1156
|
-
state_names = pd.date_range('1900-01-01', '1900-12-31', freq='MS').month_name().tolist()
|
|
1157
|
-
|
|
1158
|
-
# Build the structural model
|
|
1159
|
-
grw = st.LevelTrendComponent(order=1, innovations_order=1)
|
|
1160
|
-
annual_season = st.TimeSeasonality(season_length=12, name='annual', state_names=state_names, innovations=False)
|
|
1161
|
-
ss_mod = (grw + annual_season).build()
|
|
1162
|
-
|
|
1163
|
-
# Estimate with PyMC
|
|
1164
|
-
with pm.Model(coords=ss_mod.coords) as model:
|
|
1165
|
-
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
|
|
1166
|
-
intitial_trend = pm.Deterministic('initial_trend', pt.zeros(1), dims=ss_mod.param_dims['initial_trend'])
|
|
1167
|
-
annual_coefs = pm.Normal('annual_coefs', sigma=1e-2, dims=ss_mod.param_dims['annual_coefs'])
|
|
1168
|
-
trend_sigmas = pm.HalfNormal('trend_sigmas', sigma=1e-6, dims=ss_mod.param_dims['trend_sigmas'])
|
|
1169
|
-
ss_mod.build_statespace_graph(data)
|
|
1170
|
-
idata = pm.sample(nuts_sampler='numpyro')
|
|
1171
|
-
|
|
1172
|
-
References
|
|
1173
|
-
----------
|
|
1174
|
-
.. [1] Durbin, James, and Siem Jan Koopman. 2012.
|
|
1175
|
-
Time Series Analysis by State Space Methods: Second Edition.
|
|
1176
|
-
Oxford University Press.
|
|
1177
|
-
"""
|
|
1178
|
-
|
|
1179
|
-
def __init__(
|
|
1180
|
-
self,
|
|
1181
|
-
season_length: int,
|
|
1182
|
-
innovations: bool = True,
|
|
1183
|
-
name: str | None = None,
|
|
1184
|
-
state_names: list | None = None,
|
|
1185
|
-
remove_first_state: bool = True,
|
|
1186
|
-
):
|
|
1187
|
-
if name is None:
|
|
1188
|
-
name = f"Seasonal[s={season_length}]"
|
|
1189
|
-
if state_names is None:
|
|
1190
|
-
state_names = [f"{name}_{i}" for i in range(season_length)]
|
|
1191
|
-
else:
|
|
1192
|
-
if len(state_names) != season_length:
|
|
1193
|
-
raise ValueError(
|
|
1194
|
-
f"state_names must be a list of length season_length, got {len(state_names)}"
|
|
1195
|
-
)
|
|
1196
|
-
state_names = state_names.copy()
|
|
1197
|
-
self.innovations = innovations
|
|
1198
|
-
self.remove_first_state = remove_first_state
|
|
1199
|
-
|
|
1200
|
-
if self.remove_first_state:
|
|
1201
|
-
# In traditional models, the first state isn't identified, so we can help out the user by automatically
|
|
1202
|
-
# discarding it.
|
|
1203
|
-
# TODO: Can this be stashed and reconstructed automatically somehow?
|
|
1204
|
-
state_names.pop(0)
|
|
1205
|
-
|
|
1206
|
-
k_states = season_length - int(self.remove_first_state)
|
|
1207
|
-
|
|
1208
|
-
super().__init__(
|
|
1209
|
-
name=name,
|
|
1210
|
-
k_endog=1,
|
|
1211
|
-
k_states=k_states,
|
|
1212
|
-
k_posdef=int(innovations),
|
|
1213
|
-
state_names=state_names,
|
|
1214
|
-
measurement_error=False,
|
|
1215
|
-
combine_hidden_states=True,
|
|
1216
|
-
obs_state_idxs=np.r_[[1.0], np.zeros(k_states - 1)],
|
|
1217
|
-
)
|
|
1218
|
-
|
|
1219
|
-
def populate_component_properties(self):
|
|
1220
|
-
self.param_names = [f"{self.name}_coefs"]
|
|
1221
|
-
self.param_info = {
|
|
1222
|
-
f"{self.name}_coefs": {
|
|
1223
|
-
"shape": (self.k_states,),
|
|
1224
|
-
"constraints": None,
|
|
1225
|
-
"dims": (f"{self.name}_state",),
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
self.param_dims = {f"{self.name}_coefs": (f"{self.name}_state",)}
|
|
1229
|
-
self.coords = {f"{self.name}_state": self.state_names}
|
|
1230
|
-
|
|
1231
|
-
if self.innovations:
|
|
1232
|
-
self.param_names += [f"sigma_{self.name}"]
|
|
1233
|
-
self.param_info[f"sigma_{self.name}"] = {
|
|
1234
|
-
"shape": (),
|
|
1235
|
-
"constraints": "Positive",
|
|
1236
|
-
"dims": None,
|
|
1237
|
-
}
|
|
1238
|
-
self.shock_names = [f"{self.name}"]
|
|
1239
|
-
|
|
1240
|
-
def make_symbolic_graph(self) -> None:
|
|
1241
|
-
if self.remove_first_state:
|
|
1242
|
-
# In this case, parameters are normalized to sum to zero, so the current state is the negative sum of
|
|
1243
|
-
# all previous states.
|
|
1244
|
-
T = np.eye(self.k_states, k=-1)
|
|
1245
|
-
T[0, :] = -1
|
|
1246
|
-
else:
|
|
1247
|
-
# In this case we assume the user to be responsible for ensuring the states sum to zero, so T is just a
|
|
1248
|
-
# circulant matrix that cycles between the states.
|
|
1249
|
-
T = np.eye(self.k_states, k=1)
|
|
1250
|
-
T[-1, 0] = 1
|
|
1251
|
-
|
|
1252
|
-
self.ssm["transition", :, :] = T
|
|
1253
|
-
self.ssm["design", 0, 0] = 1
|
|
1254
|
-
|
|
1255
|
-
initial_states = self.make_and_register_variable(
|
|
1256
|
-
f"{self.name}_coefs", shape=(self.k_states,)
|
|
1257
|
-
)
|
|
1258
|
-
self.ssm["initial_state", np.arange(self.k_states, dtype=int)] = initial_states
|
|
1259
|
-
|
|
1260
|
-
if self.innovations:
|
|
1261
|
-
self.ssm["selection", 0, 0] = 1
|
|
1262
|
-
season_sigma = self.make_and_register_variable(f"sigma_{self.name}", shape=())
|
|
1263
|
-
cov_idx = ("state_cov", *np.diag_indices(1))
|
|
1264
|
-
self.ssm[cov_idx] = season_sigma**2
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
class FrequencySeasonality(Component):
|
|
1268
|
-
r"""
|
|
1269
|
-
Seasonal component, modeled in the frequency domain
|
|
1270
|
-
|
|
1271
|
-
Parameters
|
|
1272
|
-
----------
|
|
1273
|
-
season_length: float
|
|
1274
|
-
The number of periods in a single seasonal cycle, e.g. 12 for monthly data with annual seasonal pattern, 7 for
|
|
1275
|
-
daily data with weekly seasonal pattern, etc. Non-integer seasonal_length is also permitted, for example
|
|
1276
|
-
365.2422 days in a (solar) year.
|
|
1277
|
-
|
|
1278
|
-
n: int
|
|
1279
|
-
Number of fourier features to include in the seasonal component. Default is ``season_length // 2``, which
|
|
1280
|
-
is the maximum possible. A smaller number can be used for a more wave-like seasonal pattern.
|
|
1281
|
-
|
|
1282
|
-
name: str, default None
|
|
1283
|
-
A name for this seasonal component. Used to label dimensions and coordinates. Useful when multiple seasonal
|
|
1284
|
-
components are included in the same model. Default is ``f"Seasonal[s={season_length}, n={n}]"``
|
|
1285
|
-
|
|
1286
|
-
innovations: bool, default True
|
|
1287
|
-
Whether to include stochastic innovations in the strength of the seasonal effect
|
|
1288
|
-
|
|
1289
|
-
Notes
|
|
1290
|
-
-----
|
|
1291
|
-
A seasonal effect is any pattern that repeats every fixed interval. Although there are many possible ways to
|
|
1292
|
-
model seasonal effects, the implementation used here is the one described by [1] as the "canonical" frequency domain
|
|
1293
|
-
representation. The seasonal component can be expressed:
|
|
1294
|
-
|
|
1295
|
-
.. math::
|
|
1296
|
-
\begin{align}
|
|
1297
|
-
\gamma_t &= \sum_{j=1}^{2n} \gamma_{j,t} \\
|
|
1298
|
-
\gamma_{j, t+1} &= \gamma_{j,t} \cos \lambda_j + \gamma_{j,t}^\star \sin \lambda_j + \omega_{j, t} \\
|
|
1299
|
-
\gamma_{j, t}^\star &= -\gamma_{j,t} \sin \lambda_j + \gamma_{j,t}^\star \cos \lambda_j + \omega_{j,t}^\star
|
|
1300
|
-
\lambda_j &= \frac{2\pi j}{s}
|
|
1301
|
-
\end{align}
|
|
1302
|
-
|
|
1303
|
-
Where :math:`s` is the ``seasonal_length``.
|
|
1304
|
-
|
|
1305
|
-
Unlike a ``TimeSeasonality`` component, a ``FrequencySeasonality`` component does not require integer season
|
|
1306
|
-
length. In addition, for long seasonal periods, it is possible to obtain a more compact state space representation
|
|
1307
|
-
by choosing ``n << s // 2``. Using ``TimeSeasonality``, an annual seasonal pattern in daily data requires 364
|
|
1308
|
-
states, whereas ``FrequencySeasonality`` always requires ``2 * n`` states, regardless of the ``seasonal_length``.
|
|
1309
|
-
The price of this compactness is less representational power. At ``n = 1``, the seasonal pattern will be a pure
|
|
1310
|
-
sine wave. At ``n = s // 2``, any arbitrary pattern can be represented.
|
|
1311
|
-
|
|
1312
|
-
One cost of the added flexibility of ``FrequencySeasonality`` is reduced interpretability. States of this model are
|
|
1313
|
-
coefficients :math:`\gamma_1, \gamma^\star_1, \gamma_2, \gamma_2^\star ..., \gamma_n, \gamma^\star_n` associated
|
|
1314
|
-
with different frequencies in the fourier representation of the seasonal pattern. As a result, it is not possible
|
|
1315
|
-
to isolate and identify a "Monday" effect, for instance.
|
|
1316
|
-
"""
|
|
1317
|
-
|
|
1318
|
-
def __init__(self, season_length, n=None, name=None, innovations=True):
|
|
1319
|
-
if n is None:
|
|
1320
|
-
n = int(season_length // 2)
|
|
1321
|
-
if name is None:
|
|
1322
|
-
name = f"Frequency[s={season_length}, n={n}]"
|
|
1323
|
-
|
|
1324
|
-
k_states = n * 2
|
|
1325
|
-
self.n = n
|
|
1326
|
-
self.season_length = season_length
|
|
1327
|
-
self.innovations = innovations
|
|
1328
|
-
|
|
1329
|
-
# If the model is completely saturated (n = s // 2), the last state will not be identified, so it shouldn't
|
|
1330
|
-
# get a parameter assigned to it and should just be fixed to zero.
|
|
1331
|
-
# Test this way (rather than n == s // 2) to catch cases when n is non-integer.
|
|
1332
|
-
self.last_state_not_identified = self.season_length / self.n == 2.0
|
|
1333
|
-
self.n_coefs = k_states - int(self.last_state_not_identified)
|
|
1334
|
-
|
|
1335
|
-
obs_state_idx = np.zeros(k_states)
|
|
1336
|
-
obs_state_idx[slice(0, k_states, 2)] = 1
|
|
1337
|
-
|
|
1338
|
-
super().__init__(
|
|
1339
|
-
name=name,
|
|
1340
|
-
k_endog=1,
|
|
1341
|
-
k_states=k_states,
|
|
1342
|
-
k_posdef=k_states * int(self.innovations),
|
|
1343
|
-
measurement_error=False,
|
|
1344
|
-
combine_hidden_states=True,
|
|
1345
|
-
obs_state_idxs=obs_state_idx,
|
|
1346
|
-
)
|
|
1347
|
-
|
|
1348
|
-
def make_symbolic_graph(self) -> None:
|
|
1349
|
-
self.ssm["design", 0, slice(0, self.k_states, 2)] = 1
|
|
1350
|
-
|
|
1351
|
-
init_state = self.make_and_register_variable(f"{self.name}", shape=(self.n_coefs,))
|
|
1352
|
-
|
|
1353
|
-
init_state_idx = np.arange(self.n_coefs, dtype=int)
|
|
1354
|
-
self.ssm["initial_state", init_state_idx] = init_state
|
|
1355
|
-
|
|
1356
|
-
T_mats = [_frequency_transition_block(self.season_length, j + 1) for j in range(self.n)]
|
|
1357
|
-
T = pt.linalg.block_diag(*T_mats)
|
|
1358
|
-
self.ssm["transition", :, :] = T
|
|
1359
|
-
|
|
1360
|
-
if self.innovations:
|
|
1361
|
-
sigma_season = self.make_and_register_variable(f"sigma_{self.name}", shape=())
|
|
1362
|
-
self.ssm["state_cov", :, :] = pt.eye(self.k_posdef) * sigma_season**2
|
|
1363
|
-
self.ssm["selection", :, :] = np.eye(self.k_states)
|
|
1364
|
-
|
|
1365
|
-
def populate_component_properties(self):
|
|
1366
|
-
self.state_names = [f"{self.name}_{f}_{i}" for i in range(self.n) for f in ["Cos", "Sin"]]
|
|
1367
|
-
self.param_names = [f"{self.name}"]
|
|
1368
|
-
|
|
1369
|
-
self.param_dims = {self.name: (f"{self.name}_state",)}
|
|
1370
|
-
self.param_info = {
|
|
1371
|
-
f"{self.name}": {
|
|
1372
|
-
"shape": (self.k_states - int(self.last_state_not_identified),),
|
|
1373
|
-
"constraints": None,
|
|
1374
|
-
"dims": (f"{self.name}_state",),
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
init_state_idx = np.arange(self.k_states, dtype=int)
|
|
1379
|
-
if self.last_state_not_identified:
|
|
1380
|
-
init_state_idx = init_state_idx[:-1]
|
|
1381
|
-
self.coords = {f"{self.name}_state": [self.state_names[i] for i in init_state_idx]}
|
|
1382
|
-
|
|
1383
|
-
if self.innovations:
|
|
1384
|
-
self.shock_names = self.state_names.copy()
|
|
1385
|
-
self.param_names += [f"sigma_{self.name}"]
|
|
1386
|
-
self.param_info[f"sigma_{self.name}"] = {
|
|
1387
|
-
"shape": (),
|
|
1388
|
-
"constraints": "Positive",
|
|
1389
|
-
"dims": None,
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
class CycleComponent(Component):
|
|
1394
|
-
r"""
|
|
1395
|
-
A component for modeling longer-term cyclical effects
|
|
1396
|
-
|
|
1397
|
-
Parameters
|
|
1398
|
-
----------
|
|
1399
|
-
name: str
|
|
1400
|
-
Name of the component. Used in generated coordinates and state names. If None, a descriptive name will be
|
|
1401
|
-
used.
|
|
1402
|
-
|
|
1403
|
-
cycle_length: int, optional
|
|
1404
|
-
The length of the cycle, in the calendar units of your data. For example, if your data is monthly, and you
|
|
1405
|
-
want to model a 12-month cycle, use ``cycle_length=12``. You cannot specify both ``cycle_length`` and
|
|
1406
|
-
``estimate_cycle_length``.
|
|
1407
|
-
|
|
1408
|
-
estimate_cycle_length: bool, default False
|
|
1409
|
-
Whether to estimate the cycle length. If True, an additional parameter, ``cycle_length`` will be added to the
|
|
1410
|
-
model. You cannot specify both ``cycle_length`` and ``estimate_cycle_length``.
|
|
1411
|
-
|
|
1412
|
-
dampen: bool, default False
|
|
1413
|
-
Whether to dampen the cycle by multiplying by a dampening factor :math:`\rho` at every timestep. If true,
|
|
1414
|
-
an additional parameter, ``dampening_factor`` will be added to the model.
|
|
1415
|
-
|
|
1416
|
-
innovations: bool, default True
|
|
1417
|
-
Whether to include stochastic innovations in the strength of the seasonal effect. If True, an additional
|
|
1418
|
-
parameter, ``sigma_{name}`` will be added to the model.
|
|
1419
|
-
|
|
1420
|
-
Notes
|
|
1421
|
-
-----
|
|
1422
|
-
The cycle component is very similar in implementation to the frequency domain seasonal component, expect that it
|
|
1423
|
-
is restricted to n=1. The cycle component can be expressed:
|
|
1424
|
-
|
|
1425
|
-
.. math::
|
|
1426
|
-
\begin{align}
|
|
1427
|
-
\gamma_t &= \rho \gamma_{t-1} \cos \lambda + \rho \gamma_{t-1}^\star \sin \lambda + \omega_{t} \\
|
|
1428
|
-
\gamma_{t}^\star &= -\rho \gamma_{t-1} \sin \lambda + \rho \gamma_{t-1}^\star \cos \lambda + \omega_{t}^\star \\
|
|
1429
|
-
\lambda &= \frac{2\pi}{s}
|
|
1430
|
-
\end{align}
|
|
1431
|
-
|
|
1432
|
-
Where :math:`s` is the ``cycle_length``. [1] recommend that this component be used for longer term cyclical
|
|
1433
|
-
effects, such as business cycles, and that the seasonal component be used for shorter term effects, such as
|
|
1434
|
-
weekly or monthly seasonality.
|
|
1435
|
-
|
|
1436
|
-
Unlike a FrequencySeasonality component, the length of a CycleComponent can be estimated.
|
|
1437
|
-
|
|
1438
|
-
Examples
|
|
1439
|
-
--------
|
|
1440
|
-
Estimate a business cycle with length between 6 and 12 years:
|
|
1441
|
-
|
|
1442
|
-
.. code:: python
|
|
1443
|
-
|
|
1444
|
-
from pymc_extras.statespace import structural as st
|
|
1445
|
-
import pymc as pm
|
|
1446
|
-
import pytensor.tensor as pt
|
|
1447
|
-
import pandas as pd
|
|
1448
|
-
import numpy as np
|
|
1449
|
-
|
|
1450
|
-
data = np.random.normal(size=(100, 1))
|
|
1451
|
-
|
|
1452
|
-
# Build the structural model
|
|
1453
|
-
grw = st.LevelTrendComponent(order=1, innovations_order=1)
|
|
1454
|
-
cycle = st.CycleComponent('business_cycle', estimate_cycle_length=True, dampen=False)
|
|
1455
|
-
ss_mod = (grw + cycle).build()
|
|
1456
|
-
|
|
1457
|
-
# Estimate with PyMC
|
|
1458
|
-
with pm.Model(coords=ss_mod.coords) as model:
|
|
1459
|
-
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states), dims=ss_mod.param_dims['P0'])
|
|
1460
|
-
intitial_trend = pm.Normal('initial_trend', dims=ss_mod.param_dims['initial_trend'])
|
|
1461
|
-
sigma_trend = pm.HalfNormal('sigma_trend', dims=ss_mod.param_dims['sigma_trend'])
|
|
1462
|
-
|
|
1463
|
-
cycle_strength = pm.Normal('business_cycle')
|
|
1464
|
-
cycle_length = pm.Uniform('business_cycle_length', lower=6, upper=12)
|
|
1465
|
-
|
|
1466
|
-
sigma_cycle = pm.HalfNormal('sigma_business_cycle', sigma=1)
|
|
1467
|
-
ss_mod.build_statespace_graph(data)
|
|
1468
|
-
|
|
1469
|
-
idata = pm.sample(nuts_sampler='numpyro')
|
|
1470
|
-
|
|
1471
|
-
References
|
|
1472
|
-
----------
|
|
1473
|
-
.. [1] Durbin, James, and Siem Jan Koopman. 2012.
|
|
1474
|
-
Time Series Analysis by State Space Methods: Second Edition.
|
|
1475
|
-
Oxford University Press.
|
|
1476
|
-
"""
|
|
1477
|
-
|
|
1478
|
-
def __init__(
|
|
1479
|
-
self,
|
|
1480
|
-
name: str | None = None,
|
|
1481
|
-
cycle_length: int | None = None,
|
|
1482
|
-
estimate_cycle_length: bool = False,
|
|
1483
|
-
dampen: bool = False,
|
|
1484
|
-
innovations: bool = True,
|
|
1485
|
-
):
|
|
1486
|
-
if cycle_length is None and not estimate_cycle_length:
|
|
1487
|
-
raise ValueError("Must specify cycle_length if estimate_cycle_length is False")
|
|
1488
|
-
if cycle_length is not None and estimate_cycle_length:
|
|
1489
|
-
raise ValueError("Cannot specify cycle_length if estimate_cycle_length is True")
|
|
1490
|
-
if name is None:
|
|
1491
|
-
cycle = int(cycle_length) if cycle_length is not None else "Estimate"
|
|
1492
|
-
name = f"Cycle[s={cycle}, dampen={dampen}, innovations={innovations}]"
|
|
1493
|
-
|
|
1494
|
-
self.estimate_cycle_length = estimate_cycle_length
|
|
1495
|
-
self.cycle_length = cycle_length
|
|
1496
|
-
self.innovations = innovations
|
|
1497
|
-
self.dampen = dampen
|
|
1498
|
-
self.n_coefs = 1
|
|
1499
|
-
|
|
1500
|
-
k_states = 2
|
|
1501
|
-
k_endog = 1
|
|
1502
|
-
k_posdef = 2
|
|
1503
|
-
|
|
1504
|
-
obs_state_idx = np.zeros(k_states)
|
|
1505
|
-
obs_state_idx[slice(0, k_states, 2)] = 1
|
|
1506
|
-
|
|
1507
|
-
super().__init__(
|
|
1508
|
-
name=name,
|
|
1509
|
-
k_endog=k_endog,
|
|
1510
|
-
k_states=k_states,
|
|
1511
|
-
k_posdef=k_posdef,
|
|
1512
|
-
measurement_error=False,
|
|
1513
|
-
combine_hidden_states=True,
|
|
1514
|
-
obs_state_idxs=obs_state_idx,
|
|
1515
|
-
)
|
|
1516
|
-
|
|
1517
|
-
def make_symbolic_graph(self) -> None:
|
|
1518
|
-
self.ssm["design", 0, slice(0, self.k_states, 2)] = 1
|
|
1519
|
-
self.ssm["selection", :, :] = np.eye(self.k_states)
|
|
1520
|
-
self.param_dims = {self.name: (f"{self.name}_state",)}
|
|
1521
|
-
self.coords = {f"{self.name}_state": self.state_names}
|
|
1522
|
-
|
|
1523
|
-
init_state = self.make_and_register_variable(f"{self.name}", shape=(self.k_states,))
|
|
1524
|
-
|
|
1525
|
-
self.ssm["initial_state", :] = init_state
|
|
1526
|
-
|
|
1527
|
-
if self.estimate_cycle_length:
|
|
1528
|
-
lamb = self.make_and_register_variable(f"{self.name}_length", shape=())
|
|
1529
|
-
else:
|
|
1530
|
-
lamb = self.cycle_length
|
|
1531
|
-
|
|
1532
|
-
if self.dampen:
|
|
1533
|
-
rho = self.make_and_register_variable(f"{self.name}_dampening_factor", shape=())
|
|
1534
|
-
else:
|
|
1535
|
-
rho = 1
|
|
1536
|
-
|
|
1537
|
-
T = rho * _frequency_transition_block(lamb, j=1)
|
|
1538
|
-
self.ssm["transition", :, :] = T
|
|
1539
|
-
|
|
1540
|
-
if self.innovations:
|
|
1541
|
-
sigma_cycle = self.make_and_register_variable(f"sigma_{self.name}", shape=())
|
|
1542
|
-
self.ssm["state_cov", :, :] = pt.eye(self.k_posdef) * sigma_cycle**2
|
|
1543
|
-
|
|
1544
|
-
def populate_component_properties(self):
|
|
1545
|
-
self.state_names = [f"{self.name}_{f}" for f in ["Cos", "Sin"]]
|
|
1546
|
-
self.param_names = [f"{self.name}"]
|
|
1547
|
-
|
|
1548
|
-
self.param_info = {
|
|
1549
|
-
f"{self.name}": {
|
|
1550
|
-
"shape": (2,),
|
|
1551
|
-
"constraints": None,
|
|
1552
|
-
"dims": (f"{self.name}_state",),
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
if self.estimate_cycle_length:
|
|
1557
|
-
self.param_names += [f"{self.name}_length"]
|
|
1558
|
-
self.param_info[f"{self.name}_length"] = {
|
|
1559
|
-
"shape": (),
|
|
1560
|
-
"constraints": "Positive, non-zero",
|
|
1561
|
-
"dims": None,
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
if self.dampen:
|
|
1565
|
-
self.param_names += [f"{self.name}_dampening_factor"]
|
|
1566
|
-
self.param_info[f"{self.name}_dampening_factor"] = {
|
|
1567
|
-
"shape": (),
|
|
1568
|
-
"constraints": "0 < x ≤ 1",
|
|
1569
|
-
"dims": None,
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
if self.innovations:
|
|
1573
|
-
self.param_names += [f"sigma_{self.name}"]
|
|
1574
|
-
self.param_info[f"sigma_{self.name}"] = {
|
|
1575
|
-
"shape": (),
|
|
1576
|
-
"constraints": "Positive",
|
|
1577
|
-
"dims": None,
|
|
1578
|
-
}
|
|
1579
|
-
self.shock_names = self.state_names.copy()
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
class RegressionComponent(Component):
|
|
1583
|
-
def __init__(
|
|
1584
|
-
self,
|
|
1585
|
-
k_exog: int | None = None,
|
|
1586
|
-
name: str | None = "Exogenous",
|
|
1587
|
-
state_names: list[str] | None = None,
|
|
1588
|
-
innovations=False,
|
|
1589
|
-
):
|
|
1590
|
-
self.innovations = innovations
|
|
1591
|
-
k_exog = self._handle_input_data(k_exog, state_names, name)
|
|
1592
|
-
|
|
1593
|
-
k_states = k_exog
|
|
1594
|
-
k_endog = 1
|
|
1595
|
-
k_posdef = k_exog
|
|
1596
|
-
|
|
1597
|
-
super().__init__(
|
|
1598
|
-
name=name,
|
|
1599
|
-
k_endog=k_endog,
|
|
1600
|
-
k_states=k_states,
|
|
1601
|
-
k_posdef=k_posdef,
|
|
1602
|
-
state_names=self.state_names,
|
|
1603
|
-
measurement_error=False,
|
|
1604
|
-
combine_hidden_states=False,
|
|
1605
|
-
exog_names=[f"data_{name}"],
|
|
1606
|
-
obs_state_idxs=np.ones(k_states),
|
|
1607
|
-
)
|
|
1608
|
-
|
|
1609
|
-
@staticmethod
|
|
1610
|
-
def _get_state_names(k_exog: int | None, state_names: list[str] | None, name: str):
|
|
1611
|
-
if k_exog is None and state_names is None:
|
|
1612
|
-
raise ValueError("Must specify at least one of k_exog or state_names")
|
|
1613
|
-
if state_names is not None and k_exog is not None:
|
|
1614
|
-
if len(state_names) != k_exog:
|
|
1615
|
-
raise ValueError(f"Expected {k_exog} state names, found {len(state_names)}")
|
|
1616
|
-
elif k_exog is None:
|
|
1617
|
-
k_exog = len(state_names)
|
|
1618
|
-
else:
|
|
1619
|
-
state_names = [f"{name}_{i + 1}" for i in range(k_exog)]
|
|
1620
|
-
|
|
1621
|
-
return k_exog, state_names
|
|
1622
|
-
|
|
1623
|
-
def _handle_input_data(self, k_exog: int, state_names: list[str] | None, name) -> int:
|
|
1624
|
-
k_exog, state_names = self._get_state_names(k_exog, state_names, name)
|
|
1625
|
-
self.state_names = state_names
|
|
1626
|
-
|
|
1627
|
-
return k_exog
|
|
1628
|
-
|
|
1629
|
-
def make_symbolic_graph(self) -> None:
|
|
1630
|
-
betas = self.make_and_register_variable(f"beta_{self.name}", shape=(self.k_states,))
|
|
1631
|
-
regression_data = self.make_and_register_data(
|
|
1632
|
-
f"data_{self.name}", shape=(None, self.k_states)
|
|
1633
|
-
)
|
|
1634
|
-
|
|
1635
|
-
self.ssm["initial_state", :] = betas
|
|
1636
|
-
self.ssm["transition", :, :] = np.eye(self.k_states)
|
|
1637
|
-
self.ssm["selection", :, :] = np.eye(self.k_states)
|
|
1638
|
-
self.ssm["design"] = pt.expand_dims(regression_data, 1)
|
|
1639
|
-
|
|
1640
|
-
if self.innovations:
|
|
1641
|
-
sigma_beta = self.make_and_register_variable(
|
|
1642
|
-
f"sigma_beta_{self.name}", (self.k_states,)
|
|
1643
|
-
)
|
|
1644
|
-
row_idx, col_idx = np.diag_indices(self.k_states)
|
|
1645
|
-
self.ssm["state_cov", row_idx, col_idx] = sigma_beta**2
|
|
1646
|
-
|
|
1647
|
-
def populate_component_properties(self) -> None:
|
|
1648
|
-
self.shock_names = self.state_names
|
|
1649
|
-
|
|
1650
|
-
self.param_names = [f"beta_{self.name}"]
|
|
1651
|
-
self.data_names = [f"data_{self.name}"]
|
|
1652
|
-
self.param_dims = {
|
|
1653
|
-
f"beta_{self.name}": ("exog_state",),
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
self.param_info = {
|
|
1657
|
-
f"beta_{self.name}": {
|
|
1658
|
-
"shape": (self.k_states,),
|
|
1659
|
-
"constraints": None,
|
|
1660
|
-
"dims": ("exog_state",),
|
|
1661
|
-
},
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
self.data_info = {
|
|
1665
|
-
f"data_{self.name}": {
|
|
1666
|
-
"shape": (None, self.k_states),
|
|
1667
|
-
"dims": (TIME_DIM, "exog_state"),
|
|
1668
|
-
},
|
|
1669
|
-
}
|
|
1670
|
-
self.coords = {"exog_state": self.state_names}
|
|
1671
|
-
|
|
1672
|
-
if self.innovations:
|
|
1673
|
-
self.param_names += [f"sigma_beta_{self.name}"]
|
|
1674
|
-
self.param_dims[f"sigma_beta_{self.name}"] = "exog_state"
|
|
1675
|
-
self.param_info[f"sigma_beta_{self.name}"] = {
|
|
1676
|
-
"shape": (),
|
|
1677
|
-
"constraints": "Positive",
|
|
1678
|
-
"dims": ("exog_state",),
|
|
1679
|
-
}
|