Skip to content

Commit 8950d56

Browse files
Re-run example notebooks
1 parent 9ef88ef commit 8950d56

File tree

6 files changed

+1420
-1436
lines changed

6 files changed

+1420
-1436
lines changed

notebooks/Making a Custom Statespace Model.ipynb

Lines changed: 63 additions & 137 deletions
Large diffs are not rendered by default.

notebooks/SARMA Example.ipynb

Lines changed: 603 additions & 572 deletions
Large diffs are not rendered by default.

notebooks/Structural Timeseries Modeling.ipynb

Lines changed: 344 additions & 405 deletions
Large diffs are not rendered by default.

notebooks/VARMAX Example.ipynb

Lines changed: 345 additions & 288 deletions
Large diffs are not rendered by default.

pymc_experimental/statespace/core/representation.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
from typing import Optional, Type, Union
23

34
import numpy as np
@@ -10,7 +11,7 @@
1011
)
1112

1213
floatX = pytensor.config.floatX
13-
KeyLike = Union[tuple[Union[str, int]], str]
14+
KeyLike = Union[tuple[str | int, ...], str]
1415

1516

1617
class PytensorRepresentation:
@@ -152,6 +153,22 @@ class PytensorRepresentation:
152153
http://www.chadfulton.com/files/fulton_statsmodels_2017_v1.pdf
153154
"""
154155

156+
__slots__ = (
157+
"k_endog",
158+
"k_states",
159+
"k_posdef",
160+
"shapes",
161+
"design",
162+
"obs_intercept",
163+
"obs_cov",
164+
"transition",
165+
"state_intercept",
166+
"selection",
167+
"state_cov",
168+
"initial_state",
169+
"initial_state_cov",
170+
)
171+
155172
def __init__(
156173
self,
157174
k_endog: int,
@@ -206,16 +223,17 @@ def _validate_key(self, key: KeyLike) -> None:
206223
if key not in self.shapes:
207224
raise IndexError(f"{key} is an invalid state space matrix name")
208225

209-
def _update_shape(self, key: KeyLike, value: Union[np.ndarray, pt.TensorType]) -> None:
226+
def _update_shape(self, key: KeyLike, value: Union[np.ndarray, pt.Variable]) -> None:
210227
if isinstance(value, (pt.TensorConstant, pt.TensorVariable)):
211228
shape = value.type.shape
212229
else:
213230
shape = value.shape
214231

215232
old_shape = self.shapes[key]
216-
if not all([a == b for a, b in zip(shape[1:], old_shape[1:])]):
233+
ndim_core = 1 if key in VECTOR_VALUED else 2
234+
if not all([a == b for a, b in zip(shape[-ndim_core:], old_shape[-ndim_core:])]):
217235
raise ValueError(
218-
f"The last two dimensions of {key} must be {old_shape[1:]}, found {shape[1:]}"
236+
f"The last two dimensions of {key} must be {old_shape[-ndim_core:]}, found {shape[-ndim_core:]}"
219237
)
220238

221239
# Add time dimension dummy if none present
@@ -229,7 +247,7 @@ def _update_shape(self, key: KeyLike, value: Union[np.ndarray, pt.TensorType]) -
229247

230248
def _add_time_dim_to_slice(
231249
self, name: str, slice_: Union[list[int], tuple[int]], n_dim: int
232-
) -> tuple[int]:
250+
) -> tuple[int | slice, ...]:
233251
# Case 1: There is never a time dim. No changes needed.
234252
if name in NEVER_TIME_VARYING:
235253
return slice_
@@ -389,7 +407,7 @@ def __getitem__(self, key: KeyLike) -> pt.TensorVariable:
389407
else:
390408
raise IndexError("First index must the name of a valid state space matrix.")
391409

392-
def __setitem__(self, key: KeyLike, value: Union[float, int, np.ndarray]) -> None:
410+
def __setitem__(self, key: KeyLike, value: Union[float, int, np.ndarray, pt.Variable]) -> None:
393411
_type = type(key)
394412

395413
# Case 1: key is a string: we are setting an entire matrix.
@@ -416,3 +434,6 @@ def __setitem__(self, key: KeyLike, value: Union[float, int, np.ndarray]) -> Non
416434
matrix.name = name
417435

418436
setattr(self, name, matrix)
437+
438+
def copy(self):
439+
return copy.copy(self)

pymc_experimental/statespace/models/structural.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,21 @@ class StructuralTimeSeries(PyMCStateSpace):
6161
def __init__(
6262
self,
6363
ssm: PytensorRepresentation,
64-
state_names,
65-
data_names,
66-
shock_names,
67-
param_names,
68-
exog_names,
69-
param_dims,
70-
coords,
71-
param_info,
72-
data_info,
73-
component_info,
74-
measurement_error,
75-
name_to_variable,
76-
name_to_data,
77-
name=None,
78-
verbose=True,
64+
state_names: list[str],
65+
data_names: list[str],
66+
shock_names: list[str],
67+
param_names: list[str],
68+
exog_names: list[str],
69+
param_dims: dict[str, tuple[int]],
70+
coords: dict[str, Sequence],
71+
param_info: dict[str, dict[str, Any]],
72+
data_info: dict[str, dict[str, Any]],
73+
component_info: dict[str, dict[str, Any]],
74+
measurement_error: bool,
75+
name_to_variable: dict[str, Variable],
76+
name_to_data: dict[str, Variable] | None = None,
77+
name: str | None = None,
78+
verbose: bool = True,
7979
filter_type: str = "standard",
8080
):
8181
# Add the initial state covariance to the parameters
@@ -87,36 +87,46 @@ def __init__(
8787
param_names, param_dims, param_info = self._add_inital_state_cov_to_properties(
8888
param_names, param_dims, param_info, k_states
8989
)
90-
self._state_names = state_names
91-
self._data_names = data_names
92-
self._shock_names = shock_names
93-
self._param_names = param_names
94-
self._param_dims = param_dims
90+
self._state_names = state_names.copy()
91+
self._data_names = data_names.copy()
92+
self._shock_names = shock_names.copy()
93+
self._param_names = param_names.copy()
94+
self._param_dims = param_dims.copy()
9595

9696
default_coords = make_default_coords(self)
9797
coords.update(default_coords)
9898

9999
self._coords = coords
100-
self._param_info = param_info
101-
self._data_info = data_info
100+
self._param_info = param_info.copy()
101+
self._data_info = data_info.copy()
102102
self.measurement_error = measurement_error
103103

104104
super().__init__(
105105
k_endog,
106106
k_states,
107-
k_posdef,
107+
max(1, k_posdef),
108108
filter_type=filter_type,
109109
verbose=verbose,
110110
measurement_error=measurement_error,
111111
)
112+
self.ssm = ssm.copy()
112113

113-
self.ssm = ssm
114-
self._component_info = component_info
114+
if k_posdef == 0:
115+
# If there is no randomness in the model, add dummy matrices to the representation to avoid errors
116+
# when we go to construct random variables from the matrices
117+
self.ssm.k_posdef = self.k_posdef
118+
self.ssm.shapes["state_cov"] = (1, 1, 1)
119+
self.ssm["state_cov"] = pt.zeros((1, 1, 1))
115120

116-
self._name_to_variable = name_to_variable
117-
self._name_to_data = name_to_data
121+
self.ssm.shapes["selection"] = (1, self.k_states, 1)
122+
self.ssm["selection"] = pt.zeros((1, self.k_states, 1))
118123

119-
self._exog_names = exog_names
124+
self._component_info = component_info.copy()
125+
126+
self._name_to_variable = name_to_variable.copy()
127+
self._name_to_data = name_to_data.copy()
128+
129+
self._exog_names = exog_names.copy()
120130
self._needs_exog_data = len(exog_names) > 0
121131

122132
P0 = self.make_and_register_variable("P0", shape=(self.k_states, self.k_states))

0 commit comments

Comments
 (0)