Skip to content

Commit 5de65eb

Browse files
Add ETS model (#369)
* Add ETS model * Add innovation hidden state, data is observed state * Adjust statespace to match statsmodels * Rebase from main and run new pre-commit * Add helper function to sample from statespace models * Add `BayesianETS` and `compile_statespace` to `statespace.__all__` * Match statsmodels implementation Add direct and transformed parameterizations * Draft example notebook * draft notebook * work on example notebook * Allow mutlivariate ETS models * Add approximate stationary initialization * Example notebook for ETS * Test for stationary initialization * Rename first seasonal state to "seasonality" * Add example of decomposition to notebook * Use absolute imports in `test_ETS` * Apply requested changes * Re-run example notebook * Remove special logic for `solve_discrete_lyapunov` * Simplify docstring
1 parent c0dac7f commit 5de65eb

File tree

11 files changed

+5070
-9
lines changed

11 files changed

+5070
-9
lines changed

notebooks/Exponential Trend Smoothing.ipynb

+3,913
Large diffs are not rendered by default.
+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from pymc_experimental.statespace.core.compile import compile_statespace
12
from pymc_experimental.statespace.models import structural
3+
from pymc_experimental.statespace.models.ETS import BayesianETS
24
from pymc_experimental.statespace.models.SARIMAX import BayesianSARIMA
35
from pymc_experimental.statespace.models.VARMAX import BayesianVARMAX
46

5-
__all__ = ["structural", "BayesianSARIMA", "BayesianVARMAX"]
7+
__all__ = ["structural", "BayesianSARIMA", "BayesianVARMAX", "BayesianETS", "compile_statespace"]
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
# ruff: noqa: I001
2+
13
from pymc_experimental.statespace.core.representation import PytensorRepresentation
24
from pymc_experimental.statespace.core.statespace import PyMCStateSpace
5+
from pymc_experimental.statespace.core.compile import compile_statespace
36

4-
__all__ = ["PytensorRepresentation", "PyMCStateSpace"]
7+
__all__ = ["PytensorRepresentation", "PyMCStateSpace", "compile_statespace"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import numpy as np
2+
import pymc as pm
3+
import pytensor
4+
import pytensor.tensor as pt
5+
6+
from pymc_experimental.statespace.core import PyMCStateSpace
7+
from pymc_experimental.statespace.filters.distributions import LinearGaussianStateSpace
8+
from pymc_experimental.statespace.utils.constants import SHORT_NAME_TO_LONG
9+
10+
11+
def compile_statespace(
12+
statespace_model: PyMCStateSpace, steps: int | None = None, **compile_kwargs
13+
):
14+
if steps is None:
15+
steps = pt.iscalar("steps")
16+
17+
x0, _, c, d, T, Z, R, H, Q = statespace_model._unpack_statespace_with_placeholders()
18+
19+
sequence_names = [x.name for x in [c, d] if x.ndim == 2]
20+
sequence_names += [x.name for x in [T, Z, R, H, Q] if x.ndim == 3]
21+
22+
rename_dict = {v: k for k, v in SHORT_NAME_TO_LONG.items()}
23+
sequence_names = list(map(rename_dict.get, sequence_names))
24+
25+
P0 = pt.zeros((x0.shape[0], x0.shape[0]))
26+
27+
outputs = LinearGaussianStateSpace.dist(
28+
x0, P0, c, d, T, Z, R, H, Q, steps=steps, sequence_names=sequence_names
29+
)
30+
31+
inputs = list(pytensor.graph.basic.explicit_graph_inputs(outputs))
32+
33+
_f = pm.compile_pymc(inputs, outputs, on_unused_input="ignore", **compile_kwargs)
34+
35+
def f(*, draws=1, **params):
36+
if isinstance(steps, pt.Variable):
37+
inner_steps = params.get("steps", 100)
38+
else:
39+
inner_steps = steps
40+
41+
output = [np.empty((draws, inner_steps + 1, x.type.shape[-1])) for x in outputs]
42+
for i in range(draws):
43+
draw = _f(**params)
44+
for j, x in enumerate(draw):
45+
output[j][i] = x
46+
return [x.squeeze() for x in output]
47+
48+
return f

pymc_experimental/statespace/core/statespace.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,19 @@ def _print_data_requirements(self) -> None:
304304
f"{out}"
305305
)
306306

307-
def _unpack_statespace_with_placeholders(self) -> tuple[pt.TensorVariable]:
307+
def _unpack_statespace_with_placeholders(
308+
self,
309+
) -> tuple[
310+
pt.TensorVariable,
311+
pt.TensorVariable,
312+
pt.TensorVariable,
313+
pt.TensorVariable,
314+
pt.TensorVariable,
315+
pt.TensorVariable,
316+
pt.TensorVariable,
317+
pt.TensorVariable,
318+
pt.TensorVariable,
319+
]:
308320
"""
309321
Helper function to quickly obtain all statespace matrices in the standard order. Matrices returned by this
310322
method will include pytensor placeholders.
@@ -448,8 +460,8 @@ def add_default_priors(self) -> None:
448460
raise NotImplementedError("The add_default_priors property has not been implemented!")
449461

450462
def make_and_register_variable(
451-
self, name, shape: int | tuple[int] | None = None, dtype=floatX
452-
) -> Variable:
463+
self, name, shape: int | tuple[int, ...] | None = None, dtype=floatX
464+
) -> pt.TensorVariable:
453465
"""
454466
Helper function to create a pytensor symbolic variable and register it in the _name_to_variable dictionary
455467

0 commit comments

Comments
 (0)