pymc-extras 0.3.1__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/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/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.3.1.dist-info → pymc_extras-0.4.0.dist-info}/METADATA +5 -4
- {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.0.dist-info}/RECORD +21 -12
- pymc_extras/statespace/models/structural.py +0 -1679
- {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.0.dist-info}/WHEEL +0 -0
- {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -326,7 +326,7 @@ def find_MAP(
|
|
|
326
326
|
)
|
|
327
327
|
|
|
328
328
|
raveled_optimized = RaveledVars(optimizer_result.x, initial_params.point_map_info)
|
|
329
|
-
unobserved_vars = get_default_varnames(model.unobserved_value_vars, include_transformed)
|
|
329
|
+
unobserved_vars = get_default_varnames(model.unobserved_value_vars, include_transformed=True)
|
|
330
330
|
unobserved_vars_values = model.compile_fn(unobserved_vars, mode="FAST_COMPILE")(
|
|
331
331
|
DictToArrayBijection.rmap(raveled_optimized)
|
|
332
332
|
)
|
|
@@ -335,13 +335,20 @@ def find_MAP(
|
|
|
335
335
|
var.name: value for var, value in zip(unobserved_vars, unobserved_vars_values)
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
idata = map_results_to_inference_data(
|
|
339
|
-
|
|
338
|
+
idata = map_results_to_inference_data(
|
|
339
|
+
map_point=optimized_point, model=frozen_model, include_transformed=include_transformed
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
idata = add_fit_to_inference_data(
|
|
343
|
+
idata=idata, mu=raveled_optimized, H_inv=H_inv, model=frozen_model
|
|
344
|
+
)
|
|
345
|
+
|
|
340
346
|
idata = add_optimizer_result_to_inference_data(
|
|
341
|
-
idata, optimizer_result, method, raveled_optimized, model
|
|
347
|
+
idata=idata, result=optimizer_result, method=method, mu=raveled_optimized, model=model
|
|
342
348
|
)
|
|
349
|
+
|
|
343
350
|
idata = add_data_to_inference_data(
|
|
344
|
-
idata, progressbar=False, model=model, compile_kwargs=compile_kwargs
|
|
351
|
+
idata=idata, progressbar=False, model=model, compile_kwargs=compile_kwargs
|
|
345
352
|
)
|
|
346
353
|
|
|
347
354
|
return idata
|
|
@@ -59,6 +59,7 @@ def make_unpacked_variable_names(names: list[str], model: pm.Model) -> list[str]
|
|
|
59
59
|
def map_results_to_inference_data(
|
|
60
60
|
map_point: dict[str, float | int | np.ndarray],
|
|
61
61
|
model: pm.Model | None = None,
|
|
62
|
+
include_transformed: bool = True,
|
|
62
63
|
):
|
|
63
64
|
"""
|
|
64
65
|
Add the MAP point to an InferenceData object in the posterior group.
|
|
@@ -68,13 +69,13 @@ def map_results_to_inference_data(
|
|
|
68
69
|
|
|
69
70
|
Parameters
|
|
70
71
|
----------
|
|
71
|
-
idata: az.InferenceData
|
|
72
|
-
An InferenceData object to which the MAP point will be added.
|
|
73
72
|
map_point: dict
|
|
74
73
|
A dictionary containing the MAP point estimates for each variable. The keys should be the variable names, and
|
|
75
74
|
the values should be the corresponding MAP estimates.
|
|
76
75
|
model: Model, optional
|
|
77
76
|
A PyMC model. If None, the model is taken from the current model context.
|
|
77
|
+
include_transformed: bool
|
|
78
|
+
Whether to return transformed (unconstrained) variables in the constrained_posterior group. Default is True.
|
|
78
79
|
|
|
79
80
|
Returns
|
|
80
81
|
-------
|
|
@@ -118,7 +119,7 @@ def map_results_to_inference_data(
|
|
|
118
119
|
dims=dims,
|
|
119
120
|
)
|
|
120
121
|
|
|
121
|
-
if unconstrained_names:
|
|
122
|
+
if unconstrained_names and include_transformed:
|
|
122
123
|
unconstrained_posterior = az.from_dict(
|
|
123
124
|
posterior={
|
|
124
125
|
k: np.expand_dims(v, (0, 1))
|
|
@@ -302,7 +302,7 @@ def fit_laplace(
|
|
|
302
302
|
----------
|
|
303
303
|
model : pm.Model
|
|
304
304
|
The PyMC model to be fit. If None, the current model context is used.
|
|
305
|
-
|
|
305
|
+
optimize_method : str
|
|
306
306
|
The optimization method to use. Valid choices are: Nelder-Mead, Powell, CG, BFGS, L-BFGS-B, TNC, SLSQP,
|
|
307
307
|
trust-constr, dogleg, trust-ncg, trust-exact, trust-krylov, and basinhopping.
|
|
308
308
|
|
|
@@ -441,9 +441,11 @@ def fit_laplace(
|
|
|
441
441
|
.rename({"temp_chain": "chain", "temp_draw": "draw"})
|
|
442
442
|
)
|
|
443
443
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
if include_transformed:
|
|
445
|
+
idata.unconstrained_posterior = unstack_laplace_draws(
|
|
446
|
+
new_posterior.laplace_approximation.values, model, chains=chains, draws=draws
|
|
447
|
+
)
|
|
448
|
+
|
|
447
449
|
idata.posterior = new_posterior.drop_vars(
|
|
448
450
|
["laplace_approximation", "unpacked_variable_names"]
|
|
449
451
|
)
|
|
@@ -38,16 +38,15 @@ from pymc.blocking import DictToArrayBijection, RaveledVars
|
|
|
38
38
|
from pymc.initial_point import make_initial_point_fn
|
|
39
39
|
from pymc.model import modelcontext
|
|
40
40
|
from pymc.model.core import Point
|
|
41
|
+
from pymc.progress_bar import CustomProgress, default_progress_theme
|
|
41
42
|
from pymc.pytensorf import (
|
|
42
43
|
compile,
|
|
43
44
|
find_rng_nodes,
|
|
44
45
|
reseed_rngs,
|
|
45
46
|
)
|
|
46
47
|
from pymc.util import (
|
|
47
|
-
CustomProgress,
|
|
48
48
|
RandomSeed,
|
|
49
49
|
_get_seeds_per_chain,
|
|
50
|
-
default_progress_theme,
|
|
51
50
|
get_default_varnames,
|
|
52
51
|
)
|
|
53
52
|
from pytensor.compile.function.types import Function
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pymc_extras.statespace.models.structural.components.autoregressive import (
|
|
2
|
+
AutoregressiveComponent,
|
|
3
|
+
)
|
|
4
|
+
from pymc_extras.statespace.models.structural.components.cycle import CycleComponent
|
|
5
|
+
from pymc_extras.statespace.models.structural.components.level_trend import LevelTrendComponent
|
|
6
|
+
from pymc_extras.statespace.models.structural.components.measurement_error import MeasurementError
|
|
7
|
+
from pymc_extras.statespace.models.structural.components.regression import RegressionComponent
|
|
8
|
+
from pymc_extras.statespace.models.structural.components.seasonality import (
|
|
9
|
+
FrequencySeasonality,
|
|
10
|
+
TimeSeasonality,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"LevelTrendComponent",
|
|
15
|
+
"MeasurementError",
|
|
16
|
+
"AutoregressiveComponent",
|
|
17
|
+
"TimeSeasonality",
|
|
18
|
+
"FrequencySeasonality",
|
|
19
|
+
"RegressionComponent",
|
|
20
|
+
"CycleComponent",
|
|
21
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
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 AR_PARAM_DIM
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AutoregressiveComponent(Component):
|
|
10
|
+
r"""
|
|
11
|
+
Autoregressive timeseries component
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
order: int or sequence of int
|
|
16
|
+
|
|
17
|
+
If int, the number of lags to include in the model.
|
|
18
|
+
If a sequence, an array-like of zeros and ones indicating which lags to include in the model.
|
|
19
|
+
|
|
20
|
+
name: str, default "auto_regressive"
|
|
21
|
+
A name for this autoregressive component. Used to label dimensions and coordinates.
|
|
22
|
+
|
|
23
|
+
observed_state_names: list[str] | None, default None
|
|
24
|
+
List of strings for observed state labels. If None, defaults to ["data"].
|
|
25
|
+
|
|
26
|
+
Notes
|
|
27
|
+
-----
|
|
28
|
+
An autoregressive component can be thought of as a way o introducing serially correlated errors into the model.
|
|
29
|
+
The process is modeled:
|
|
30
|
+
|
|
31
|
+
.. math::
|
|
32
|
+
x_t = \sum_{i=1}^p \rho_i x_{t-i}
|
|
33
|
+
|
|
34
|
+
Where ``p``, the number of autoregressive terms to model, is the order of the process. By default, all lags up to
|
|
35
|
+
``p`` are included in the model. To disable lags, pass a list of zeros and ones to the ``order`` argumnet. For
|
|
36
|
+
example, ``order=[1, 1, 0, 1]`` would become:
|
|
37
|
+
|
|
38
|
+
.. math::
|
|
39
|
+
x_t = \rho_1 x_{t-1} + \rho_2 x_{t-1} + \rho_4 x_{t-1}
|
|
40
|
+
|
|
41
|
+
The coefficient :math:`\rho_3` has been constrained to zero.
|
|
42
|
+
|
|
43
|
+
.. warning:: This class is meant to be used as a component in a structural time series model. For modeling of
|
|
44
|
+
stationary processes with ARIMA, use ``statespace.BayesianSARIMA``.
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
Model a timeseries as an AR(2) process with non-zero mean:
|
|
49
|
+
|
|
50
|
+
.. code:: python
|
|
51
|
+
|
|
52
|
+
from pymc_extras.statespace import structural as st
|
|
53
|
+
import pymc as pm
|
|
54
|
+
import pytensor.tensor as pt
|
|
55
|
+
|
|
56
|
+
trend = st.LevelTrendComponent(order=1, innovations_order=0)
|
|
57
|
+
ar = st.AutoregressiveComponent(2)
|
|
58
|
+
ss_mod = (trend + ar).build()
|
|
59
|
+
|
|
60
|
+
with pm.Model(coords=ss_mod.coords) as model:
|
|
61
|
+
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
|
|
62
|
+
intitial_trend = pm.Normal('initial_trend', sigma=10, dims=ss_mod.param_dims['initial_trend'])
|
|
63
|
+
ar_params = pm.Normal('ar_params', dims=ss_mod.param_dims['ar_params'])
|
|
64
|
+
sigma_ar = pm.Exponential('sigma_ar', 1, dims=ss_mod.param_dims['sigma_ar'])
|
|
65
|
+
|
|
66
|
+
ss_mod.build_statespace_graph(data)
|
|
67
|
+
idata = pm.sample(nuts_sampler='numpyro')
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
order: int = 1,
|
|
74
|
+
name: str = "auto_regressive",
|
|
75
|
+
observed_state_names: list[str] | None = None,
|
|
76
|
+
):
|
|
77
|
+
if observed_state_names is None:
|
|
78
|
+
observed_state_names = ["data"]
|
|
79
|
+
|
|
80
|
+
k_posdef = k_endog = len(observed_state_names)
|
|
81
|
+
|
|
82
|
+
order = order_to_mask(order)
|
|
83
|
+
ar_lags = np.flatnonzero(order).ravel().astype(int) + 1
|
|
84
|
+
k_states = len(order)
|
|
85
|
+
|
|
86
|
+
self.order = order
|
|
87
|
+
self.ar_lags = ar_lags
|
|
88
|
+
|
|
89
|
+
super().__init__(
|
|
90
|
+
name=name,
|
|
91
|
+
k_endog=k_endog,
|
|
92
|
+
k_states=k_states * k_endog,
|
|
93
|
+
k_posdef=k_posdef,
|
|
94
|
+
measurement_error=True,
|
|
95
|
+
combine_hidden_states=True,
|
|
96
|
+
observed_state_names=observed_state_names,
|
|
97
|
+
obs_state_idxs=np.tile(np.r_[[1.0], np.zeros(k_states - 1)], k_endog),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def populate_component_properties(self):
|
|
101
|
+
k_states = self.k_states // self.k_endog # this is also the number of AR lags
|
|
102
|
+
|
|
103
|
+
self.state_names = [
|
|
104
|
+
f"L{i + 1}[{state_name}]"
|
|
105
|
+
for state_name in self.observed_state_names
|
|
106
|
+
for i in range(k_states)
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
self.shock_names = [f"{self.name}[{obs_name}]" for obs_name in self.observed_state_names]
|
|
110
|
+
self.param_names = [f"params_{self.name}", f"sigma_{self.name}"]
|
|
111
|
+
self.param_dims = {f"params_{self.name}": (f"lag_{self.name}",)}
|
|
112
|
+
self.coords = {f"lag_{self.name}": self.ar_lags.tolist()}
|
|
113
|
+
|
|
114
|
+
if self.k_endog > 1:
|
|
115
|
+
self.param_dims[f"params_{self.name}"] = (
|
|
116
|
+
f"endog_{self.name}",
|
|
117
|
+
AR_PARAM_DIM,
|
|
118
|
+
)
|
|
119
|
+
self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
|
|
120
|
+
|
|
121
|
+
self.coords[f"endog_{self.name}"] = self.observed_state_names
|
|
122
|
+
|
|
123
|
+
self.param_info = {
|
|
124
|
+
f"params_{self.name}": {
|
|
125
|
+
"shape": (k_states,) if self.k_endog == 1 else (self.k_endog, k_states),
|
|
126
|
+
"constraints": None,
|
|
127
|
+
"dims": (AR_PARAM_DIM,)
|
|
128
|
+
if self.k_endog == 1
|
|
129
|
+
else (
|
|
130
|
+
f"endog_{self.name}",
|
|
131
|
+
f"lag_{self.name}",
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
f"sigma_{self.name}": {
|
|
135
|
+
"shape": () if self.k_endog == 1 else (self.k_endog,),
|
|
136
|
+
"constraints": "Positive",
|
|
137
|
+
"dims": None if self.k_endog == 1 else (f"endog_{self.name}",),
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def make_symbolic_graph(self) -> None:
|
|
142
|
+
k_endog = self.k_endog
|
|
143
|
+
k_states = self.k_states // k_endog
|
|
144
|
+
k_posdef = self.k_posdef
|
|
145
|
+
|
|
146
|
+
k_nonzero = int(sum(self.order))
|
|
147
|
+
ar_params = self.make_and_register_variable(
|
|
148
|
+
f"params_{self.name}", shape=(k_nonzero,) if k_endog == 1 else (k_endog, k_nonzero)
|
|
149
|
+
)
|
|
150
|
+
sigma_ar = self.make_and_register_variable(
|
|
151
|
+
f"sigma_{self.name}", shape=() if k_endog == 1 else (k_endog,)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if k_endog == 1:
|
|
155
|
+
T = pt.eye(k_states, k=-1)
|
|
156
|
+
ar_idx = (np.zeros(k_nonzero, dtype="int"), np.nonzero(self.order)[0])
|
|
157
|
+
T = T[ar_idx].set(ar_params)
|
|
158
|
+
|
|
159
|
+
else:
|
|
160
|
+
transition_matrices = []
|
|
161
|
+
|
|
162
|
+
for i in range(k_endog):
|
|
163
|
+
T = pt.eye(k_states, k=-1)
|
|
164
|
+
ar_idx = (np.zeros(k_nonzero, dtype="int"), np.nonzero(self.order)[0])
|
|
165
|
+
T = T[ar_idx].set(ar_params[i])
|
|
166
|
+
transition_matrices.append(T)
|
|
167
|
+
T = pt.specify_shape(
|
|
168
|
+
pt.linalg.block_diag(*transition_matrices), (self.k_states, self.k_states)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self.ssm["transition", :, :] = T
|
|
172
|
+
|
|
173
|
+
R = np.eye(k_states)
|
|
174
|
+
R_mask = np.full((k_states), False)
|
|
175
|
+
R_mask[0] = True
|
|
176
|
+
R = R[:, R_mask]
|
|
177
|
+
|
|
178
|
+
self.ssm["selection", :, :] = pt.specify_shape(
|
|
179
|
+
pt.linalg.block_diag(*[R for _ in range(k_endog)]), (self.k_states, self.k_posdef)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
Z = pt.zeros((1, k_states))[0, 0].set(1.0)
|
|
183
|
+
self.ssm["design", :, :] = pt.specify_shape(
|
|
184
|
+
pt.linalg.block_diag(*[Z for _ in range(k_endog)]), (self.k_endog, self.k_states)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
cov_idx = ("state_cov", *np.diag_indices(k_posdef))
|
|
188
|
+
self.ssm[cov_idx] = sigma_ar**2
|
|
@@ -0,0 +1,305 @@
|
|
|
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
|
+
Notes
|
|
47
|
+
-----
|
|
48
|
+
The cycle component is very similar in implementation to the frequency domain seasonal component, expect that it
|
|
49
|
+
is restricted to n=1. The cycle component can be expressed:
|
|
50
|
+
|
|
51
|
+
.. math::
|
|
52
|
+
\begin{align}
|
|
53
|
+
\gamma_t &= \rho \gamma_{t-1} \cos \lambda + \rho \gamma_{t-1}^\star \sin \lambda + \omega_{t} \\
|
|
54
|
+
\gamma_{t}^\star &= -\rho \gamma_{t-1} \sin \lambda + \rho \gamma_{t-1}^\star \cos \lambda + \omega_{t}^\star \\
|
|
55
|
+
\lambda &= \frac{2\pi}{s}
|
|
56
|
+
\end{align}
|
|
57
|
+
|
|
58
|
+
Where :math:`s` is the ``cycle_length``. [1] recommend that this component be used for longer term cyclical
|
|
59
|
+
effects, such as business cycles, and that the seasonal component be used for shorter term effects, such as
|
|
60
|
+
weekly or monthly seasonality.
|
|
61
|
+
|
|
62
|
+
Unlike a FrequencySeasonality component, the length of a CycleComponent can be estimated.
|
|
63
|
+
|
|
64
|
+
**Multivariate Support:**
|
|
65
|
+
For multivariate time series with k endogenous variables, the component creates:
|
|
66
|
+
- 2k states (cosine and sine components for each variable)
|
|
67
|
+
- Block diagonal transition and selection matrices
|
|
68
|
+
- Variable-specific innovation variances (optional)
|
|
69
|
+
- Proper parameter shapes: (k, 2) for initial states, (k,) for innovation variances
|
|
70
|
+
|
|
71
|
+
Examples
|
|
72
|
+
--------
|
|
73
|
+
**Univariate Example:**
|
|
74
|
+
Estimate a business cycle with length between 6 and 12 years:
|
|
75
|
+
|
|
76
|
+
.. code:: python
|
|
77
|
+
|
|
78
|
+
from pymc_extras.statespace import structural as st
|
|
79
|
+
import pymc as pm
|
|
80
|
+
import pytensor.tensor as pt
|
|
81
|
+
import pandas as pd
|
|
82
|
+
import numpy as np
|
|
83
|
+
|
|
84
|
+
data = np.random.normal(size=(100, 1))
|
|
85
|
+
|
|
86
|
+
# Build the structural model
|
|
87
|
+
grw = st.LevelTrendComponent(order=1, innovations_order=1)
|
|
88
|
+
cycle = st.CycleComponent(
|
|
89
|
+
"business_cycle", cycle_length=12, estimate_cycle_length=False, innovations=True, dampen=True
|
|
90
|
+
)
|
|
91
|
+
ss_mod = (grw + cycle).build()
|
|
92
|
+
|
|
93
|
+
# Estimate with PyMC
|
|
94
|
+
with pm.Model(coords=ss_mod.coords) as model:
|
|
95
|
+
P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states), dims=ss_mod.param_dims['P0'])
|
|
96
|
+
|
|
97
|
+
initial_level_trend = pm.Normal('initial_level_trend', dims=ss_mod.param_dims['initial_level_trend'])
|
|
98
|
+
sigma_level_trend = pm.HalfNormal('sigma_level_trend', dims=ss_mod.param_dims['sigma_level_trend'])
|
|
99
|
+
|
|
100
|
+
business_cycle = pm.Normal("business_cycle", dims=ss_mod.param_dims["business_cycle"])
|
|
101
|
+
dampening = pm.Beta("dampening_factor_business_cycle", 2, 2)
|
|
102
|
+
sigma_cycle = pm.HalfNormal("sigma_business_cycle", sigma=1)
|
|
103
|
+
|
|
104
|
+
ss_mod.build_statespace_graph(data)
|
|
105
|
+
idata = pm.sample(
|
|
106
|
+
nuts_sampler="nutpie", nuts_sampler_kwargs={"backend": "JAX", "gradient_backend": "JAX"}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
**Multivariate Example:**
|
|
110
|
+
Model cycles for multiple economic indicators with variable-specific innovation variances:
|
|
111
|
+
|
|
112
|
+
.. code:: python
|
|
113
|
+
|
|
114
|
+
# Multivariate cycle component
|
|
115
|
+
cycle = st.CycleComponent(
|
|
116
|
+
name='business_cycle',
|
|
117
|
+
cycle_length=12,
|
|
118
|
+
estimate_cycle_length=False,
|
|
119
|
+
innovations=True,
|
|
120
|
+
dampen=True,
|
|
121
|
+
observed_state_names=['gdp', 'unemployment', 'inflation']
|
|
122
|
+
)
|
|
123
|
+
ss_mod = cycle.build()
|
|
124
|
+
|
|
125
|
+
with pm.Model(coords=ss_mod.coords) as model:
|
|
126
|
+
P0 = pm.Deterministic("P0", pt.eye(ss_mod.k_states), dims=ss_mod.param_dims["P0"])
|
|
127
|
+
# Initial states: shape (3, 2) for 3 variables, 2 states each
|
|
128
|
+
business_cycle = pm.Normal('business_cycle', dims=ss_mod.param_dims["business_cycle"])
|
|
129
|
+
|
|
130
|
+
# Dampening factor: scalar (shared across variables)
|
|
131
|
+
dampening = pm.Beta("dampening_factor_business_cycle", 2, 2)
|
|
132
|
+
|
|
133
|
+
# Innovation variances: shape (3,) for variable-specific variances
|
|
134
|
+
sigma_cycle = pm.HalfNormal(
|
|
135
|
+
"sigma_business_cycle", dims=ss_mod.param_dims["sigma_business_cycle"]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
ss_mod.build_statespace_graph(data)
|
|
139
|
+
idata = pm.sample(
|
|
140
|
+
nuts_sampler="nutpie", nuts_sampler_kwargs={"backend": "JAX", "gradient_backend": "JAX"}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
References
|
|
144
|
+
----------
|
|
145
|
+
.. [1] Durbin, James, and Siem Jan Koopman. 2012.
|
|
146
|
+
Time Series Analysis by State Space Methods: Second Edition.
|
|
147
|
+
Oxford University Press.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
name: str | None = None,
|
|
153
|
+
cycle_length: int | None = None,
|
|
154
|
+
estimate_cycle_length: bool = False,
|
|
155
|
+
dampen: bool = False,
|
|
156
|
+
innovations: bool = True,
|
|
157
|
+
observed_state_names: list[str] | None = None,
|
|
158
|
+
):
|
|
159
|
+
if observed_state_names is None:
|
|
160
|
+
observed_state_names = ["data"]
|
|
161
|
+
|
|
162
|
+
if cycle_length is None and not estimate_cycle_length:
|
|
163
|
+
raise ValueError("Must specify cycle_length if estimate_cycle_length is False")
|
|
164
|
+
if cycle_length is not None and estimate_cycle_length:
|
|
165
|
+
raise ValueError("Cannot specify cycle_length if estimate_cycle_length is True")
|
|
166
|
+
if name is None:
|
|
167
|
+
cycle = int(cycle_length) if cycle_length is not None else "Estimate"
|
|
168
|
+
name = f"Cycle[s={cycle}, dampen={dampen}, innovations={innovations}]"
|
|
169
|
+
|
|
170
|
+
self.estimate_cycle_length = estimate_cycle_length
|
|
171
|
+
self.cycle_length = cycle_length
|
|
172
|
+
self.innovations = innovations
|
|
173
|
+
self.dampen = dampen
|
|
174
|
+
self.n_coefs = 1
|
|
175
|
+
|
|
176
|
+
k_endog = len(observed_state_names)
|
|
177
|
+
|
|
178
|
+
k_states = 2 * k_endog
|
|
179
|
+
k_posdef = 2 * k_endog
|
|
180
|
+
|
|
181
|
+
obs_state_idx = np.zeros(k_states)
|
|
182
|
+
obs_state_idx[slice(0, k_states, 2)] = 1
|
|
183
|
+
|
|
184
|
+
super().__init__(
|
|
185
|
+
name=name,
|
|
186
|
+
k_endog=k_endog,
|
|
187
|
+
k_states=k_states,
|
|
188
|
+
k_posdef=k_posdef,
|
|
189
|
+
measurement_error=False,
|
|
190
|
+
combine_hidden_states=True,
|
|
191
|
+
obs_state_idxs=obs_state_idx,
|
|
192
|
+
observed_state_names=observed_state_names,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def make_symbolic_graph(self) -> None:
|
|
196
|
+
Z = np.array([1.0, 0.0]).reshape((1, -1))
|
|
197
|
+
design_matrix = block_diag(*[Z for _ in range(self.k_endog)])
|
|
198
|
+
self.ssm["design", :, :] = pt.as_tensor_variable(design_matrix)
|
|
199
|
+
|
|
200
|
+
# selection matrix R defines structure of innovations (always identity for cycle components)
|
|
201
|
+
# when innovations=False, state cov Q=0, hence R @ Q @ R.T = 0
|
|
202
|
+
R = np.eye(2) # 2x2 identity for each cycle component
|
|
203
|
+
selection_matrix = block_diag(*[R for _ in range(self.k_endog)])
|
|
204
|
+
self.ssm["selection", :, :] = pt.as_tensor_variable(selection_matrix)
|
|
205
|
+
|
|
206
|
+
init_state = self.make_and_register_variable(
|
|
207
|
+
f"{self.name}", shape=(self.k_endog, 2) if self.k_endog > 1 else (self.k_states,)
|
|
208
|
+
)
|
|
209
|
+
self.ssm["initial_state", :] = init_state.ravel()
|
|
210
|
+
|
|
211
|
+
if self.estimate_cycle_length:
|
|
212
|
+
lamb = self.make_and_register_variable(f"length_{self.name}", shape=())
|
|
213
|
+
else:
|
|
214
|
+
lamb = self.cycle_length
|
|
215
|
+
|
|
216
|
+
if self.dampen:
|
|
217
|
+
rho = self.make_and_register_variable(f"dampening_factor_{self.name}", shape=())
|
|
218
|
+
else:
|
|
219
|
+
rho = 1
|
|
220
|
+
|
|
221
|
+
T = rho * _frequency_transition_block(lamb, j=1)
|
|
222
|
+
transition = block_diag(*[T for _ in range(self.k_endog)])
|
|
223
|
+
self.ssm["transition"] = pt.specify_shape(transition, (self.k_states, self.k_states))
|
|
224
|
+
|
|
225
|
+
if self.innovations:
|
|
226
|
+
if self.k_endog == 1:
|
|
227
|
+
sigma_cycle = self.make_and_register_variable(f"sigma_{self.name}", shape=())
|
|
228
|
+
self.ssm["state_cov", :, :] = pt.eye(self.k_posdef) * sigma_cycle**2
|
|
229
|
+
else:
|
|
230
|
+
sigma_cycle = self.make_and_register_variable(
|
|
231
|
+
f"sigma_{self.name}", shape=(self.k_endog,)
|
|
232
|
+
)
|
|
233
|
+
state_cov = block_diag(
|
|
234
|
+
*[pt.eye(2) * sigma_cycle[i] ** 2 for i in range(self.k_endog)]
|
|
235
|
+
)
|
|
236
|
+
self.ssm["state_cov"] = pt.specify_shape(state_cov, (self.k_states, self.k_states))
|
|
237
|
+
else:
|
|
238
|
+
# explicitly set state cov to 0 when no innovations
|
|
239
|
+
self.ssm["state_cov", :, :] = pt.zeros((self.k_posdef, self.k_posdef))
|
|
240
|
+
|
|
241
|
+
def populate_component_properties(self):
|
|
242
|
+
self.state_names = [
|
|
243
|
+
f"{f}_{self.name}[{var_name}]" if self.k_endog > 1 else f"{f}_{self.name}"
|
|
244
|
+
for var_name in self.observed_state_names
|
|
245
|
+
for f in ["Cos", "Sin"]
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
self.param_names = [f"{self.name}"]
|
|
249
|
+
|
|
250
|
+
if self.k_endog == 1:
|
|
251
|
+
self.param_dims = {self.name: (f"state_{self.name}",)}
|
|
252
|
+
self.coords = {f"state_{self.name}": self.state_names}
|
|
253
|
+
self.param_info = {
|
|
254
|
+
f"{self.name}": {
|
|
255
|
+
"shape": (2,),
|
|
256
|
+
"constraints": None,
|
|
257
|
+
"dims": (f"state_{self.name}",),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else:
|
|
261
|
+
self.param_dims = {self.name: (f"endog_{self.name}", f"state_{self.name}")}
|
|
262
|
+
self.coords = {
|
|
263
|
+
f"state_{self.name}": [f"Cos_{self.name}", f"Sin_{self.name}"],
|
|
264
|
+
f"endog_{self.name}": self.observed_state_names,
|
|
265
|
+
}
|
|
266
|
+
self.param_info = {
|
|
267
|
+
f"{self.name}": {
|
|
268
|
+
"shape": (self.k_endog, 2),
|
|
269
|
+
"constraints": None,
|
|
270
|
+
"dims": (f"endog_{self.name}", f"state_{self.name}"),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if self.estimate_cycle_length:
|
|
275
|
+
self.param_names += [f"length_{self.name}"]
|
|
276
|
+
self.param_info[f"length_{self.name}"] = {
|
|
277
|
+
"shape": () if self.k_endog == 1 else (self.k_endog,),
|
|
278
|
+
"constraints": "Positive, non-zero",
|
|
279
|
+
"dims": None if self.k_endog == 1 else f"endog_{self.name}",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if self.dampen:
|
|
283
|
+
self.param_names += [f"dampening_factor_{self.name}"]
|
|
284
|
+
self.param_info[f"dampening_factor_{self.name}"] = {
|
|
285
|
+
"shape": () if self.k_endog == 1 else (self.k_endog,),
|
|
286
|
+
"constraints": "0 < x ≤ 1",
|
|
287
|
+
"dims": None if self.k_endog == 1 else (f"endog_{self.name}",),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if self.innovations:
|
|
291
|
+
self.param_names += [f"sigma_{self.name}"]
|
|
292
|
+
if self.k_endog == 1:
|
|
293
|
+
self.param_info[f"sigma_{self.name}"] = {
|
|
294
|
+
"shape": (),
|
|
295
|
+
"constraints": "Positive",
|
|
296
|
+
"dims": None,
|
|
297
|
+
}
|
|
298
|
+
else:
|
|
299
|
+
self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
|
|
300
|
+
self.param_info[f"sigma_{self.name}"] = {
|
|
301
|
+
"shape": (self.k_endog,),
|
|
302
|
+
"constraints": "Positive",
|
|
303
|
+
"dims": (f"endog_{self.name}",),
|
|
304
|
+
}
|
|
305
|
+
self.shock_names = self.state_names.copy()
|