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,325 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from pytensor import tensor as pt
|
|
4
|
+
from pytensor.tensor.slinalg import block_diag
|
|
5
|
+
|
|
6
|
+
from pymc_extras.statespace.models.structural.core import Component
|
|
7
|
+
from pymc_extras.statespace.models.structural.utils import _frequency_transition_block
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CycleComponent(Component):
|
|
11
|
+
r"""
|
|
12
|
+
A component for modeling longer-term cyclical effects
|
|
13
|
+
|
|
14
|
+
Supports both univariate and multivariate time series. For multivariate time series,
|
|
15
|
+
each endogenous variable gets its own independent cycle component with separate
|
|
16
|
+
cosine/sine states and optional variable-specific innovation variances.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
name: str
|
|
21
|
+
Name of the component. Used in generated coordinates and state names. If None, a descriptive name will be
|
|
22
|
+
used.
|
|
23
|
+
|
|
24
|
+
cycle_length: int, optional
|
|
25
|
+
The length of the cycle, in the calendar units of your data. For example, if your data is monthly, and you
|
|
26
|
+
want to model a 12-month cycle, use ``cycle_length=12``. You cannot specify both ``cycle_length`` and
|
|
27
|
+
``estimate_cycle_length``.
|
|
28
|
+
|
|
29
|
+
estimate_cycle_length: bool, default False
|
|
30
|
+
Whether to estimate the cycle length. If True, an additional parameter, ``cycle_length`` will be added to the
|
|
31
|
+
model. You cannot specify both ``cycle_length`` and ``estimate_cycle_length``.
|
|
32
|
+
|
|
33
|
+
dampen: bool, default False
|
|
34
|
+
Whether to dampen the cycle by multiplying by a dampening factor :math:`\rho` at every timestep. If true,
|
|
35
|
+
an additional parameter, ``dampening_factor`` will be added to the model.
|
|
36
|
+
|
|
37
|
+
innovations: bool, default True
|
|
38
|
+
Whether to include stochastic innovations in the strength of the seasonal effect. If True, an additional
|
|
39
|
+
parameter, ``sigma_{name}`` will be added to the model.
|
|
40
|
+
For multivariate time series, this is a vector (variable-specific innovation variances).
|
|
41
|
+
|
|
42
|
+
observed_state_names: list[str], optional
|
|
43
|
+
Names of the observed state variables. For univariate time series, defaults to ``["data"]``.
|
|
44
|
+
For multivariate time series, specify a list of names for each endogenous variable.
|
|
45
|
+
|
|
46
|
+
share_states: bool, default False
|
|
47
|
+
Whether latent states are shared across the observed states. If True, there will be only one set of latent
|
|
48
|
+
states, which are observed by all observed states. If False, each observed state has its own set of
|
|
49
|
+
latent states. This argument has no effect if `k_endog` is 1.
|
|
50
|
+
|
|
51
|
+
Notes
|
|
52
|
+
-----
|
|
53
|
+
The cycle component is very similar in implementation to the frequency domain seasonal component, expect that it
|
|
54
|
+
is restricted to n=1. The cycle component can be expressed:
|
|
55
|
+
|
|
56
|
+
.. math::
|
|
57
|
+
\begin{align}
|
|
58
|
+
\gamma_t &= \rho \gamma_{t-1} \cos \lambda + \rho \gamma_{t-1}^\star \sin \lambda + \omega_{t} \\
|
|
59
|
+
\gamma_{t}^\star &= -\rho \gamma_{t-1} \sin \lambda + \rho \gamma_{t-1}^\star \cos \lambda + \omega_{t}^\star \\
|
|
60
|
+
\lambda &= \frac{2\pi}{s}
|
|
61
|
+
\end{align}
|
|
62
|
+
|
|
63
|
+
Where :math:`s` is the ``cycle_length``. [1] recommend that this component be used for longer term cyclical
|
|
64
|
+
effects, such as business cycles, and that the seasonal component be used for shorter term effects, such as
|
|
65
|
+
weekly or monthly seasonality.
|
|
66
|
+
|
|
67
|
+
Unlike a FrequencySeasonality component, the length of a CycleComponent can be estimated.
|
|
68
|
+
|
|
69
|
+
**Multivariate Support:**
|
|
70
|
+
For multivariate time series with k endogenous variables, the component creates:
|
|
71
|
+
- 2k states (cosine and sine components for each variable)
|
|
72
|
+
- Block diagonal transition and selection matrices
|
|
73
|
+
- Variable-specific innovation variances (optional)
|
|
74
|
+
- Proper parameter shapes: (k, 2) for initial states, (k,) for innovation variances
|
|
75
|
+
|
|
76
|
+
Examples
|
|
77
|
+
--------
|
|
78
|
+
**Univariate Example:**
|
|
79
|
+
Estimate a business cycle with length between 6 and 12 years:
|
|
80
|
+
|
|
81
|
+
.. code:: python
|
|
82
|
+
|
|
83
|
+
from pymc_extras.statespace import structural as st
|
|
84
|
+
import pymc as pm
|
|
85
|
+
import pytensor.tensor as pt
|
|
86
|
+
import pandas as pd
|
|
87
|
+
import numpy as np
|
|
88
|
+
|
|
89
|
+
data = np.random.normal(size=(100, 1))
|
|
90
|
+
|
|
91
|
+
# Build the structural model
|
|
92
|
+
grw = st.LevelTrendComponent(order=1, innovations_order=1)
|
|
93
|
+
cycle = st.CycleComponent(
|
|
94
|
+
"business_cycle", cycle_length=12, estimate_cycle_length=False, innovations=True, dampen=True
|
|
95
|
+
)
|
|
96
|
+
ss_mod = (grw + cycle).build()
|
|
97
|
+
|
|
98
|
+
# Estimate with PyMC
|
|
99
|
+
with pm.Model(coords=ss_mod.coords) as model:
|
|
100
|
+
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states), dims=ss_mod.param_dims['P0'])
|
|
101
|
+
|
|
102
|
+
initial_level_trend = pm.Normal('initial_level_trend', dims=ss_mod.param_dims['initial_level_trend'])
|
|
103
|
+
sigma_level_trend = pm.HalfNormal('sigma_level_trend', dims=ss_mod.param_dims['sigma_level_trend'])
|
|
104
|
+
|
|
105
|
+
business_cycle = pm.Normal("business_cycle", dims=ss_mod.param_dims["business_cycle"])
|
|
106
|
+
dampening = pm.Beta("dampening_factor_business_cycle", 2, 2)
|
|
107
|
+
sigma_cycle = pm.HalfNormal("sigma_business_cycle", sigma=1)
|
|
108
|
+
|
|
109
|
+
ss_mod.build_statespace_graph(data)
|
|
110
|
+
idata = pm.sample(
|
|
111
|
+
nuts_sampler="nutpie", nuts_sampler_kwargs={"backend": "JAX", "gradient_backend": "JAX"}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
**Multivariate Example:**
|
|
115
|
+
Model cycles for multiple economic indicators with variable-specific innovation variances:
|
|
116
|
+
|
|
117
|
+
.. code:: python
|
|
118
|
+
|
|
119
|
+
# Multivariate cycle component
|
|
120
|
+
cycle = st.CycleComponent(
|
|
121
|
+
name='business_cycle',
|
|
122
|
+
cycle_length=12,
|
|
123
|
+
estimate_cycle_length=False,
|
|
124
|
+
innovations=True,
|
|
125
|
+
dampen=True,
|
|
126
|
+
observed_state_names=['gdp', 'unemployment', 'inflation']
|
|
127
|
+
)
|
|
128
|
+
ss_mod = cycle.build()
|
|
129
|
+
|
|
130
|
+
with pm.Model(coords=ss_mod.coords) as model:
|
|
131
|
+
P0 = pm.Deterministic("P0", pt.eye(ss_mod.k_states), dims=ss_mod.param_dims["P0"])
|
|
132
|
+
# Initial states: shape (3, 2) for 3 variables, 2 states each
|
|
133
|
+
business_cycle = pm.Normal('business_cycle', dims=ss_mod.param_dims["business_cycle"])
|
|
134
|
+
|
|
135
|
+
# Dampening factor: scalar (shared across variables)
|
|
136
|
+
dampening = pm.Beta("dampening_factor_business_cycle", 2, 2)
|
|
137
|
+
|
|
138
|
+
# Innovation variances: shape (3,) for variable-specific variances
|
|
139
|
+
sigma_cycle = pm.HalfNormal(
|
|
140
|
+
"sigma_business_cycle", dims=ss_mod.param_dims["sigma_business_cycle"]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
ss_mod.build_statespace_graph(data)
|
|
144
|
+
idata = pm.sample(
|
|
145
|
+
nuts_sampler="nutpie", nuts_sampler_kwargs={"backend": "JAX", "gradient_backend": "JAX"}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
References
|
|
149
|
+
----------
|
|
150
|
+
.. [1] Durbin, James, and Siem Jan Koopman. 2012.
|
|
151
|
+
Time Series Analysis by State Space Methods: Second Edition.
|
|
152
|
+
Oxford University Press.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
name: str | None = None,
|
|
158
|
+
cycle_length: int | None = None,
|
|
159
|
+
estimate_cycle_length: bool = False,
|
|
160
|
+
dampen: bool = False,
|
|
161
|
+
innovations: bool = True,
|
|
162
|
+
observed_state_names: list[str] | None = None,
|
|
163
|
+
share_states: bool = False,
|
|
164
|
+
):
|
|
165
|
+
if observed_state_names is None:
|
|
166
|
+
observed_state_names = ["data"]
|
|
167
|
+
|
|
168
|
+
if cycle_length is None and not estimate_cycle_length:
|
|
169
|
+
raise ValueError("Must specify cycle_length if estimate_cycle_length is False")
|
|
170
|
+
if cycle_length is not None and estimate_cycle_length:
|
|
171
|
+
raise ValueError("Cannot specify cycle_length if estimate_cycle_length is True")
|
|
172
|
+
if name is None:
|
|
173
|
+
cycle = int(cycle_length) if cycle_length is not None else "Estimate"
|
|
174
|
+
name = f"Cycle[s={cycle}, dampen={dampen}, innovations={innovations}]"
|
|
175
|
+
|
|
176
|
+
self.share_states = share_states
|
|
177
|
+
self.estimate_cycle_length = estimate_cycle_length
|
|
178
|
+
self.cycle_length = cycle_length
|
|
179
|
+
self.innovations = innovations
|
|
180
|
+
self.dampen = dampen
|
|
181
|
+
self.n_coefs = 1
|
|
182
|
+
|
|
183
|
+
k_endog = len(observed_state_names)
|
|
184
|
+
|
|
185
|
+
k_states = 2 if share_states else 2 * k_endog
|
|
186
|
+
k_posdef = 2 if share_states else 2 * k_endog
|
|
187
|
+
|
|
188
|
+
obs_state_idx = np.zeros(k_states)
|
|
189
|
+
obs_state_idx[slice(0, k_states, 2)] = 1
|
|
190
|
+
|
|
191
|
+
super().__init__(
|
|
192
|
+
name=name,
|
|
193
|
+
k_endog=k_endog,
|
|
194
|
+
k_states=k_states,
|
|
195
|
+
k_posdef=k_posdef,
|
|
196
|
+
measurement_error=False,
|
|
197
|
+
combine_hidden_states=True,
|
|
198
|
+
obs_state_idxs=obs_state_idx,
|
|
199
|
+
observed_state_names=observed_state_names,
|
|
200
|
+
share_states=share_states,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def make_symbolic_graph(self) -> None:
|
|
204
|
+
k_endog = self.k_endog
|
|
205
|
+
k_endog_effective = 1 if self.share_states else k_endog
|
|
206
|
+
|
|
207
|
+
Z = np.array([1.0, 0.0]).reshape((1, -1))
|
|
208
|
+
design_matrix = block_diag(*[Z for _ in range(k_endog_effective)])
|
|
209
|
+
self.ssm["design", :, :] = pt.as_tensor_variable(design_matrix)
|
|
210
|
+
|
|
211
|
+
# selection matrix R defines structure of innovations (always identity for cycle components)
|
|
212
|
+
# when innovations=False, state cov Q=0, hence R @ Q @ R.T = 0
|
|
213
|
+
R = np.eye(2) # 2x2 identity for each cycle component
|
|
214
|
+
selection_matrix = block_diag(*[R for _ in range(k_endog_effective)])
|
|
215
|
+
self.ssm["selection", :, :] = pt.as_tensor_variable(selection_matrix)
|
|
216
|
+
|
|
217
|
+
init_state = self.make_and_register_variable(
|
|
218
|
+
f"params_{self.name}",
|
|
219
|
+
shape=(k_endog_effective, 2) if k_endog_effective > 1 else (self.k_states,),
|
|
220
|
+
)
|
|
221
|
+
self.ssm["initial_state", :] = init_state.ravel()
|
|
222
|
+
|
|
223
|
+
if self.estimate_cycle_length:
|
|
224
|
+
lamb = self.make_and_register_variable(f"length_{self.name}", shape=())
|
|
225
|
+
else:
|
|
226
|
+
lamb = self.cycle_length
|
|
227
|
+
|
|
228
|
+
if self.dampen:
|
|
229
|
+
rho = self.make_and_register_variable(f"dampening_factor_{self.name}", shape=())
|
|
230
|
+
else:
|
|
231
|
+
rho = 1
|
|
232
|
+
|
|
233
|
+
T = rho * _frequency_transition_block(lamb, j=1)
|
|
234
|
+
transition = block_diag(*[T for _ in range(k_endog_effective)])
|
|
235
|
+
self.ssm["transition"] = pt.specify_shape(transition, (self.k_states, self.k_states))
|
|
236
|
+
|
|
237
|
+
if self.innovations:
|
|
238
|
+
if k_endog_effective == 1:
|
|
239
|
+
sigma_cycle = self.make_and_register_variable(f"sigma_{self.name}", shape=())
|
|
240
|
+
self.ssm["state_cov", :, :] = pt.eye(self.k_posdef) * sigma_cycle**2
|
|
241
|
+
else:
|
|
242
|
+
sigma_cycle = self.make_and_register_variable(
|
|
243
|
+
f"sigma_{self.name}", shape=(k_endog_effective,)
|
|
244
|
+
)
|
|
245
|
+
state_cov = block_diag(
|
|
246
|
+
*[pt.eye(2) * sigma_cycle[i] ** 2 for i in range(k_endog_effective)]
|
|
247
|
+
)
|
|
248
|
+
self.ssm["state_cov"] = pt.specify_shape(state_cov, (self.k_states, self.k_states))
|
|
249
|
+
else:
|
|
250
|
+
# explicitly set state cov to 0 when no innovations
|
|
251
|
+
self.ssm["state_cov", :, :] = pt.zeros((self.k_posdef, self.k_posdef))
|
|
252
|
+
|
|
253
|
+
def populate_component_properties(self):
|
|
254
|
+
k_endog = self.k_endog
|
|
255
|
+
k_endog_effective = 1 if self.share_states else k_endog
|
|
256
|
+
|
|
257
|
+
base_names = [f"{f}_{self.name}" for f in ["Cos", "Sin"]]
|
|
258
|
+
|
|
259
|
+
if self.share_states:
|
|
260
|
+
self.state_names = [f"{name}[shared]" for name in base_names]
|
|
261
|
+
else:
|
|
262
|
+
self.state_names = [
|
|
263
|
+
f"{name}[{var_name}]" if k_endog_effective > 1 else name
|
|
264
|
+
for var_name in self.observed_state_names
|
|
265
|
+
for name in base_names
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
self.param_names = [f"params_{self.name}"]
|
|
269
|
+
|
|
270
|
+
if k_endog_effective == 1:
|
|
271
|
+
self.param_dims = {f"params_{self.name}": (f"state_{self.name}",)}
|
|
272
|
+
self.coords = {f"state_{self.name}": base_names}
|
|
273
|
+
self.param_info = {
|
|
274
|
+
f"params_{self.name}": {
|
|
275
|
+
"shape": (2,),
|
|
276
|
+
"constraints": None,
|
|
277
|
+
"dims": (f"state_{self.name}",),
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else:
|
|
281
|
+
self.param_dims = {f"params_{self.name}": (f"endog_{self.name}", f"state_{self.name}")}
|
|
282
|
+
self.coords = {
|
|
283
|
+
f"state_{self.name}": [f"Cos_{self.name}", f"Sin_{self.name}"],
|
|
284
|
+
f"endog_{self.name}": self.observed_state_names,
|
|
285
|
+
}
|
|
286
|
+
self.param_info = {
|
|
287
|
+
f"params_{self.name}": {
|
|
288
|
+
"shape": (k_endog_effective, 2),
|
|
289
|
+
"constraints": None,
|
|
290
|
+
"dims": (f"endog_{self.name}", f"state_{self.name}"),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if self.estimate_cycle_length:
|
|
295
|
+
self.param_names += [f"length_{self.name}"]
|
|
296
|
+
self.param_info[f"length_{self.name}"] = {
|
|
297
|
+
"shape": () if k_endog_effective == 1 else (k_endog_effective,),
|
|
298
|
+
"constraints": "Positive, non-zero",
|
|
299
|
+
"dims": None if k_endog_effective == 1 else (f"endog_{self.name}",),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if self.dampen:
|
|
303
|
+
self.param_names += [f"dampening_factor_{self.name}"]
|
|
304
|
+
self.param_info[f"dampening_factor_{self.name}"] = {
|
|
305
|
+
"shape": () if k_endog_effective == 1 else (k_endog_effective,),
|
|
306
|
+
"constraints": "0 < x ≤ 1",
|
|
307
|
+
"dims": None if k_endog_effective == 1 else (f"endog_{self.name}",),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if self.innovations:
|
|
311
|
+
self.param_names += [f"sigma_{self.name}"]
|
|
312
|
+
if k_endog_effective == 1:
|
|
313
|
+
self.param_info[f"sigma_{self.name}"] = {
|
|
314
|
+
"shape": (),
|
|
315
|
+
"constraints": "Positive",
|
|
316
|
+
"dims": None,
|
|
317
|
+
}
|
|
318
|
+
else:
|
|
319
|
+
self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
|
|
320
|
+
self.param_info[f"sigma_{self.name}"] = {
|
|
321
|
+
"shape": (k_endog_effective,),
|
|
322
|
+
"constraints": "Positive",
|
|
323
|
+
"dims": (f"endog_{self.name}",),
|
|
324
|
+
}
|
|
325
|
+
self.shock_names = self.state_names.copy()
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytensor.tensor as pt
|
|
3
|
+
|
|
4
|
+
from pymc_extras.statespace.models.structural.core import Component
|
|
5
|
+
from pymc_extras.statespace.models.structural.utils import order_to_mask
|
|
6
|
+
from pymc_extras.statespace.utils.constants import POSITION_DERIVATIVE_NAMES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LevelTrendComponent(Component):
|
|
10
|
+
r"""
|
|
11
|
+
Level and trend component of a structural time series model
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
order : int
|
|
16
|
+
Number of time derivatives of the trend to include in the model. For example, when order=3, the trend will
|
|
17
|
+
be of the form ``y = a + b * t + c * t ** 2``, where the coefficients ``a, b, c`` come from the initial
|
|
18
|
+
state values.
|
|
19
|
+
|
|
20
|
+
innovations_order : int or sequence of int, optional
|
|
21
|
+
The number of stochastic innovations to include in the model. By default, ``innovations_order = order``
|
|
22
|
+
|
|
23
|
+
name : str, default "level_trend"
|
|
24
|
+
A name for this level-trend component. Used to label dimensions and coordinates.
|
|
25
|
+
|
|
26
|
+
observed_state_names : list[str] | None, default None
|
|
27
|
+
List of strings for observed state labels. If None, defaults to ["data"].
|
|
28
|
+
|
|
29
|
+
share_states: bool, default False
|
|
30
|
+
Whether latent states are shared across the observed states. If True, there will be only one set of latent
|
|
31
|
+
states, which are observed by all observed states. If False, each observed state has its own set of
|
|
32
|
+
latent states. This argument has no effect if `k_endog` is 1.
|
|
33
|
+
|
|
34
|
+
Notes
|
|
35
|
+
-----
|
|
36
|
+
This class implements the level and trend components of the general structural time series model. In the most
|
|
37
|
+
general form, the level and trend is described by a system of two time-varying equations.
|
|
38
|
+
|
|
39
|
+
.. math::
|
|
40
|
+
\begin{align}
|
|
41
|
+
\mu_{t+1} &= \mu_t + \nu_t + \zeta_t \\
|
|
42
|
+
\nu_{t+1} &= \nu_t + \xi_t
|
|
43
|
+
\zeta_t &\sim N(0, \sigma_\zeta) \\
|
|
44
|
+
\xi_t &\sim N(0, \sigma_\xi)
|
|
45
|
+
\end{align}
|
|
46
|
+
|
|
47
|
+
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
|
|
48
|
+
the process. When both innovations :math:`\zeta_t` and :math:`\xi_t` are included in the model, it is known as a
|
|
49
|
+
*local linear trend* model. This system of two equations, corresponding to ``order=2``, can be expanded or
|
|
50
|
+
contracted by adding or removing equations. ``order=3`` would add an acceleration term to the sytsem:
|
|
51
|
+
|
|
52
|
+
.. math::
|
|
53
|
+
\begin{align}
|
|
54
|
+
\mu_{t+1} &= \mu_t + \nu_t + \zeta_t \\
|
|
55
|
+
\nu_{t+1} &= \nu_t + \eta_t + \xi_t \\
|
|
56
|
+
\eta_{t+1} &= \eta_{t-1} + \omega_t \\
|
|
57
|
+
\zeta_t &\sim N(0, \sigma_\zeta) \\
|
|
58
|
+
\xi_t &\sim N(0, \sigma_\xi) \\
|
|
59
|
+
\omega_t &\sim N(0, \sigma_\omega)
|
|
60
|
+
\end{align}
|
|
61
|
+
|
|
62
|
+
After setting all innovation terms to zero and defining initial states :math:`\mu_0, \nu_0, \eta_0`, these equations
|
|
63
|
+
can be collapsed to:
|
|
64
|
+
|
|
65
|
+
.. math::
|
|
66
|
+
\mu_t = \mu_0 + \nu_0 \cdot t + \eta_0 \cdot t^2
|
|
67
|
+
|
|
68
|
+
Which clarifies how the order and initial states influence the model. In particular, the initial states are the
|
|
69
|
+
coefficients on the intercept, slope, acceleration, and so on.
|
|
70
|
+
|
|
71
|
+
In this light, allowing for innovations can be understood as allowing these coefficients to vary over time. Each
|
|
72
|
+
component can be individually selected for time variation by passing a list to the ``innovations_order`` argument.
|
|
73
|
+
For example, a constant intercept with time varying trend and acceleration is specified as ``order=3,
|
|
74
|
+
innovations_order=[0, 1, 1]``.
|
|
75
|
+
|
|
76
|
+
By choosing the ``order`` and ``innovations_order``, a large variety of models can be obtained. Notable
|
|
77
|
+
models include:
|
|
78
|
+
|
|
79
|
+
* Constant intercept, ``order=1, innovations_order=0``
|
|
80
|
+
|
|
81
|
+
.. math::
|
|
82
|
+
\mu_t = \mu
|
|
83
|
+
|
|
84
|
+
* Constant linear slope, ``order=2, innovations_order=0``
|
|
85
|
+
|
|
86
|
+
.. math::
|
|
87
|
+
\mu_t = \mu_{t-1} + \nu
|
|
88
|
+
|
|
89
|
+
* Gaussian Random Walk, ``order=1, innovations_order=1``
|
|
90
|
+
|
|
91
|
+
.. math::
|
|
92
|
+
\mu_t = \mu_{t-1} + \zeta_t
|
|
93
|
+
|
|
94
|
+
* Gaussian Random Walk with Drift, ``order=2, innovations_order=1``
|
|
95
|
+
|
|
96
|
+
.. math::
|
|
97
|
+
\mu_t = \mu_{t-1} + \nu + \zeta_t
|
|
98
|
+
|
|
99
|
+
* Smooth Trend, ``order=2, innovations_order=[0, 1]``
|
|
100
|
+
|
|
101
|
+
.. math::
|
|
102
|
+
\begin{align}
|
|
103
|
+
\mu_t &= \mu_{t-1} + \nu_{t-1} \\
|
|
104
|
+
\nu_t &= \nu_{t-1} + \xi_t
|
|
105
|
+
\end{align}
|
|
106
|
+
|
|
107
|
+
* Local Level, ``order=2, innovations_order=2``
|
|
108
|
+
|
|
109
|
+
[1] notes that the smooth trend model produces more gradually changing slopes than the full local linear trend
|
|
110
|
+
model, and is equivalent to an "integrated trend model".
|
|
111
|
+
|
|
112
|
+
References
|
|
113
|
+
----------
|
|
114
|
+
.. [1] Durbin, James, and Siem Jan Koopman. 2012.
|
|
115
|
+
Time Series Analysis by State Space Methods: Second Edition.
|
|
116
|
+
Oxford University Press.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
order: int | list[int] = 2,
|
|
123
|
+
innovations_order: int | list[int] | None = None,
|
|
124
|
+
name: str = "level_trend",
|
|
125
|
+
observed_state_names: list[str] | None = None,
|
|
126
|
+
share_states: bool = False,
|
|
127
|
+
):
|
|
128
|
+
self.share_states = share_states
|
|
129
|
+
|
|
130
|
+
if innovations_order is None:
|
|
131
|
+
innovations_order = order
|
|
132
|
+
|
|
133
|
+
if observed_state_names is None:
|
|
134
|
+
observed_state_names = ["data"]
|
|
135
|
+
k_endog = len(observed_state_names)
|
|
136
|
+
|
|
137
|
+
self._order_mask = order_to_mask(order)
|
|
138
|
+
max_state = np.flatnonzero(self._order_mask)[-1].item() + 1
|
|
139
|
+
|
|
140
|
+
# If the user passes excess zeros, raise an error. The alternative is to prune them, but this would cause
|
|
141
|
+
# the shape of the state to be different to what the user expects.
|
|
142
|
+
if len(self._order_mask) > max_state:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"order={order} is invalid. The highest derivative should not be set to zero. If you want a "
|
|
145
|
+
f"lower order model, explicitly omit the zeros."
|
|
146
|
+
)
|
|
147
|
+
k_states = max_state
|
|
148
|
+
|
|
149
|
+
if isinstance(innovations_order, int):
|
|
150
|
+
n = innovations_order
|
|
151
|
+
innovations_order = order_to_mask(k_states)
|
|
152
|
+
if n > 0:
|
|
153
|
+
innovations_order[n:] = False
|
|
154
|
+
else:
|
|
155
|
+
innovations_order[:] = False
|
|
156
|
+
else:
|
|
157
|
+
innovations_order = order_to_mask(innovations_order)
|
|
158
|
+
|
|
159
|
+
self.innovations_order = innovations_order[:max_state]
|
|
160
|
+
k_posdef = int(sum(innovations_order))
|
|
161
|
+
|
|
162
|
+
super().__init__(
|
|
163
|
+
name,
|
|
164
|
+
k_endog=k_endog,
|
|
165
|
+
k_states=k_states * k_endog if not share_states else k_states,
|
|
166
|
+
k_posdef=k_posdef * k_endog if not share_states else k_posdef,
|
|
167
|
+
observed_state_names=observed_state_names,
|
|
168
|
+
measurement_error=False,
|
|
169
|
+
combine_hidden_states=False,
|
|
170
|
+
obs_state_idxs=np.tile(
|
|
171
|
+
np.array([1.0] + [0.0] * (k_states - 1)), k_endog if not share_states else 1
|
|
172
|
+
),
|
|
173
|
+
share_states=share_states,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def populate_component_properties(self):
|
|
177
|
+
k_endog = self.k_endog
|
|
178
|
+
k_endog_effective = 1 if self.share_states else k_endog
|
|
179
|
+
|
|
180
|
+
k_states = self.k_states // k_endog_effective
|
|
181
|
+
k_posdef = self.k_posdef // k_endog_effective
|
|
182
|
+
|
|
183
|
+
name_slice = POSITION_DERIVATIVE_NAMES[:k_states]
|
|
184
|
+
self.param_names = [f"initial_{self.name}"]
|
|
185
|
+
base_names = [name for name, mask in zip(name_slice, self._order_mask) if mask]
|
|
186
|
+
|
|
187
|
+
if self.share_states:
|
|
188
|
+
self.state_names = [f"{name}[{self.name}_shared]" for name in base_names]
|
|
189
|
+
else:
|
|
190
|
+
self.state_names = [
|
|
191
|
+
f"{name}[{obs_name}]"
|
|
192
|
+
for obs_name in self.observed_state_names
|
|
193
|
+
for name in base_names
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
self.param_dims = {f"initial_{self.name}": (f"state_{self.name}",)}
|
|
197
|
+
self.coords = {f"state_{self.name}": base_names}
|
|
198
|
+
|
|
199
|
+
if k_endog > 1:
|
|
200
|
+
self.coords[f"endog_{self.name}"] = self.observed_state_names
|
|
201
|
+
|
|
202
|
+
if k_endog_effective > 1:
|
|
203
|
+
self.param_dims[f"state_{self.name}"] = (
|
|
204
|
+
f"endog_{self.name}",
|
|
205
|
+
f"state_{self.name}",
|
|
206
|
+
)
|
|
207
|
+
self.param_dims = {f"initial_{self.name}": (f"endog_{self.name}", f"state_{self.name}")}
|
|
208
|
+
|
|
209
|
+
shape = (k_endog_effective, k_states) if k_endog_effective > 1 else (k_states,)
|
|
210
|
+
self.param_info = {f"initial_{self.name}": {"shape": shape, "constraints": None}}
|
|
211
|
+
|
|
212
|
+
if self.k_posdef > 0:
|
|
213
|
+
self.param_names += [f"sigma_{self.name}"]
|
|
214
|
+
|
|
215
|
+
base_shock_names = [
|
|
216
|
+
name for name, mask in zip(name_slice, self.innovations_order) if mask
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
if self.share_states:
|
|
220
|
+
self.shock_names = [f"{name}[{self.name}_shared]" for name in base_shock_names]
|
|
221
|
+
else:
|
|
222
|
+
self.shock_names = [
|
|
223
|
+
f"{name}[{obs_name}]"
|
|
224
|
+
for obs_name in self.observed_state_names
|
|
225
|
+
for name in base_shock_names
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
self.param_dims[f"sigma_{self.name}"] = (
|
|
229
|
+
(f"shock_{self.name}",)
|
|
230
|
+
if k_endog_effective == 1
|
|
231
|
+
else (f"endog_{self.name}", f"shock_{self.name}")
|
|
232
|
+
)
|
|
233
|
+
self.coords[f"shock_{self.name}"] = base_shock_names
|
|
234
|
+
self.param_info[f"sigma_{self.name}"] = {
|
|
235
|
+
"shape": (k_posdef,) if k_endog_effective == 1 else (k_endog_effective, k_posdef),
|
|
236
|
+
"constraints": "Positive",
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for name in self.param_names:
|
|
240
|
+
self.param_info[name]["dims"] = self.param_dims[name]
|
|
241
|
+
|
|
242
|
+
def make_symbolic_graph(self) -> None:
|
|
243
|
+
k_endog = self.k_endog
|
|
244
|
+
k_endog_effective = 1 if self.share_states else k_endog
|
|
245
|
+
|
|
246
|
+
k_states = self.k_states // k_endog_effective
|
|
247
|
+
k_posdef = self.k_posdef // k_endog_effective
|
|
248
|
+
|
|
249
|
+
initial_trend = self.make_and_register_variable(
|
|
250
|
+
f"initial_{self.name}",
|
|
251
|
+
shape=(k_states,) if k_endog_effective == 1 else (k_endog, k_states),
|
|
252
|
+
)
|
|
253
|
+
self.ssm["initial_state", :] = initial_trend.ravel()
|
|
254
|
+
|
|
255
|
+
triu_idx = pt.triu_indices(k_states)
|
|
256
|
+
T = pt.zeros((k_states, k_states))[triu_idx[0], triu_idx[1]].set(1)
|
|
257
|
+
|
|
258
|
+
self.ssm["transition", :, :] = pt.specify_shape(
|
|
259
|
+
pt.linalg.block_diag(*[T for _ in range(k_endog_effective)]),
|
|
260
|
+
(self.k_states, self.k_states),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
R = np.eye(k_states)
|
|
264
|
+
R = R[:, self.innovations_order]
|
|
265
|
+
|
|
266
|
+
self.ssm["selection", :, :] = pt.specify_shape(
|
|
267
|
+
pt.linalg.block_diag(*[R for _ in range(k_endog_effective)]),
|
|
268
|
+
(self.k_states, self.k_posdef),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
Z = np.array([1.0] + [0.0] * (k_states - 1)).reshape((1, -1))
|
|
272
|
+
|
|
273
|
+
if self.share_states:
|
|
274
|
+
self.ssm["design", :, :] = pt.specify_shape(
|
|
275
|
+
pt.join(0, *[Z for _ in range(k_endog)]), (self.k_endog, self.k_states)
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
self.ssm["design", :, :] = pt.specify_shape(
|
|
279
|
+
pt.linalg.block_diag(*[Z for _ in range(k_endog)]), (self.k_endog, self.k_states)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if k_posdef > 0:
|
|
283
|
+
sigma_trend = self.make_and_register_variable(
|
|
284
|
+
f"sigma_{self.name}",
|
|
285
|
+
shape=(k_posdef,) if k_endog_effective == 1 else (k_endog, k_posdef),
|
|
286
|
+
)
|
|
287
|
+
diag_idx = np.diag_indices(k_posdef * k_endog_effective)
|
|
288
|
+
idx = np.s_["state_cov", diag_idx[0], diag_idx[1]]
|
|
289
|
+
self.ssm[idx] = (sigma_trend**2).ravel()
|