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.
@@ -0,0 +1,257 @@
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
+
17
+ Number of time derivatives of the trend to include in the model. For example, when order=3, the trend will
18
+ be of the form ``y = a + b * t + c * t ** 2``, where the coefficients ``a, b, c`` come from the initial
19
+ state values.
20
+
21
+ innovations_order : int or sequence of int, optional
22
+
23
+ The number of stochastic innovations to include in the model. By default, ``innovations_order = order``
24
+
25
+ name : str, default "level_trend"
26
+ A name for this level-trend component. Used to label dimensions and coordinates.
27
+
28
+ observed_state_names : list[str] | None, default None
29
+ List of strings for observed state labels. If None, defaults to ["data"].
30
+
31
+ Notes
32
+ -----
33
+ This class implements the level and trend components of the general structural time series model. In the most
34
+ general form, the level and trend is described by a system of two time-varying equations.
35
+
36
+ .. math::
37
+ \begin{align}
38
+ \mu_{t+1} &= \mu_t + \nu_t + \zeta_t \\
39
+ \nu_{t+1} &= \nu_t + \xi_t
40
+ \zeta_t &\sim N(0, \sigma_\zeta) \\
41
+ \xi_t &\sim N(0, \sigma_\xi)
42
+ \end{align}
43
+
44
+ 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
45
+ the process. When both innovations :math:`\zeta_t` and :math:`\xi_t` are included in the model, it is known as a
46
+ *local linear trend* model. This system of two equations, corresponding to ``order=2``, can be expanded or
47
+ contracted by adding or removing equations. ``order=3`` would add an acceleration term to the sytsem:
48
+
49
+ .. math::
50
+ \begin{align}
51
+ \mu_{t+1} &= \mu_t + \nu_t + \zeta_t \\
52
+ \nu_{t+1} &= \nu_t + \eta_t + \xi_t \\
53
+ \eta_{t+1} &= \eta_{t-1} + \omega_t \\
54
+ \zeta_t &\sim N(0, \sigma_\zeta) \\
55
+ \xi_t &\sim N(0, \sigma_\xi) \\
56
+ \omega_t &\sim N(0, \sigma_\omega)
57
+ \end{align}
58
+
59
+ After setting all innovation terms to zero and defining initial states :math:`\mu_0, \nu_0, \eta_0`, these equations
60
+ can be collapsed to:
61
+
62
+ .. math::
63
+ \mu_t = \mu_0 + \nu_0 \cdot t + \eta_0 \cdot t^2
64
+
65
+ Which clarifies how the order and initial states influence the model. In particular, the initial states are the
66
+ coefficients on the intercept, slope, acceleration, and so on.
67
+
68
+ In this light, allowing for innovations can be understood as allowing these coefficients to vary over time. Each
69
+ component can be individually selected for time variation by passing a list to the ``innovations_order`` argument.
70
+ For example, a constant intercept with time varying trend and acceleration is specified as ``order=3,
71
+ innovations_order=[0, 1, 1]``.
72
+
73
+ By choosing the ``order`` and ``innovations_order``, a large variety of models can be obtained. Notable
74
+ models include:
75
+
76
+ * Constant intercept, ``order=1, innovations_order=0``
77
+
78
+ .. math::
79
+ \mu_t = \mu
80
+
81
+ * Constant linear slope, ``order=2, innovations_order=0``
82
+
83
+ .. math::
84
+ \mu_t = \mu_{t-1} + \nu
85
+
86
+ * Gaussian Random Walk, ``order=1, innovations_order=1``
87
+
88
+ .. math::
89
+ \mu_t = \mu_{t-1} + \zeta_t
90
+
91
+ * Gaussian Random Walk with Drift, ``order=2, innovations_order=1``
92
+
93
+ .. math::
94
+ \mu_t = \mu_{t-1} + \nu + \zeta_t
95
+
96
+ * Smooth Trend, ``order=2, innovations_order=[0, 1]``
97
+
98
+ .. math::
99
+ \begin{align}
100
+ \mu_t &= \mu_{t-1} + \nu_{t-1} \\
101
+ \nu_t &= \nu_{t-1} + \xi_t
102
+ \end{align}
103
+
104
+ * Local Level, ``order=2, innovations_order=2``
105
+
106
+ [1] notes that the smooth trend model produces more gradually changing slopes than the full local linear trend
107
+ model, and is equivalent to an "integrated trend model".
108
+
109
+ References
110
+ ----------
111
+ .. [1] Durbin, James, and Siem Jan Koopman. 2012.
112
+ Time Series Analysis by State Space Methods: Second Edition.
113
+ Oxford University Press.
114
+
115
+ """
116
+
117
+ def __init__(
118
+ self,
119
+ order: int | list[int] = 2,
120
+ innovations_order: int | list[int] | None = None,
121
+ name: str = "level_trend",
122
+ observed_state_names: list[str] | None = None,
123
+ ):
124
+ if innovations_order is None:
125
+ innovations_order = order
126
+
127
+ if observed_state_names is None:
128
+ observed_state_names = ["data"]
129
+ k_endog = len(observed_state_names)
130
+
131
+ self._order_mask = order_to_mask(order)
132
+ max_state = np.flatnonzero(self._order_mask)[-1].item() + 1
133
+
134
+ # If the user passes excess zeros, raise an error. The alternative is to prune them, but this would cause
135
+ # the shape of the state to be different to what the user expects.
136
+ if len(self._order_mask) > max_state:
137
+ raise ValueError(
138
+ f"order={order} is invalid. The highest derivative should not be set to zero. If you want a "
139
+ f"lower order model, explicitly omit the zeros."
140
+ )
141
+ k_states = max_state
142
+
143
+ if isinstance(innovations_order, int):
144
+ n = innovations_order
145
+ innovations_order = order_to_mask(k_states)
146
+ if n > 0:
147
+ innovations_order[n:] = False
148
+ else:
149
+ innovations_order[:] = False
150
+ else:
151
+ innovations_order = order_to_mask(innovations_order)
152
+
153
+ self.innovations_order = innovations_order[:max_state]
154
+ k_posdef = int(sum(innovations_order))
155
+
156
+ super().__init__(
157
+ name,
158
+ k_endog=k_endog,
159
+ k_states=k_states * k_endog,
160
+ k_posdef=k_posdef * k_endog,
161
+ observed_state_names=observed_state_names,
162
+ measurement_error=False,
163
+ combine_hidden_states=False,
164
+ obs_state_idxs=np.tile(np.array([1.0] + [0.0] * (k_states - 1)), k_endog),
165
+ )
166
+
167
+ def populate_component_properties(self):
168
+ k_endog = self.k_endog
169
+ k_states = self.k_states // k_endog
170
+ k_posdef = self.k_posdef // k_endog
171
+
172
+ name_slice = POSITION_DERIVATIVE_NAMES[:k_states]
173
+ self.param_names = [f"initial_{self.name}"]
174
+ base_names = [name for name, mask in zip(name_slice, self._order_mask) if mask]
175
+ self.state_names = [
176
+ f"{name}[{obs_name}]" for obs_name in self.observed_state_names for name in base_names
177
+ ]
178
+ self.param_dims = {f"initial_{self.name}": (f"state_{self.name}",)}
179
+ self.coords = {f"state_{self.name}": base_names}
180
+
181
+ if k_endog > 1:
182
+ self.param_dims[f"state_{self.name}"] = (
183
+ f"endog_{self.name}",
184
+ f"state_{self.name}",
185
+ )
186
+ self.param_dims = {f"initial_{self.name}": (f"endog_{self.name}", f"state_{self.name}")}
187
+ self.coords[f"endog_{self.name}"] = self.observed_state_names
188
+
189
+ shape = (k_endog, k_states) if k_endog > 1 else (k_states,)
190
+ self.param_info = {f"initial_{self.name}": {"shape": shape, "constraints": None}}
191
+
192
+ if self.k_posdef > 0:
193
+ self.param_names += [f"sigma_{self.name}"]
194
+
195
+ base_shock_names = [
196
+ name for name, mask in zip(name_slice, self.innovations_order) if mask
197
+ ]
198
+
199
+ self.shock_names = [
200
+ f"{name}[{obs_name}]"
201
+ for obs_name in self.observed_state_names
202
+ for name in base_shock_names
203
+ ]
204
+
205
+ self.param_dims[f"sigma_{self.name}"] = (
206
+ (f"shock_{self.name}",)
207
+ if k_endog == 1
208
+ else (f"endog_{self.name}", f"shock_{self.name}")
209
+ )
210
+ self.coords[f"shock_{self.name}"] = base_shock_names
211
+ self.param_info[f"sigma_{self.name}"] = {
212
+ "shape": (k_posdef,) if k_endog == 1 else (k_endog, k_posdef),
213
+ "constraints": "Positive",
214
+ }
215
+
216
+ for name in self.param_names:
217
+ self.param_info[name]["dims"] = self.param_dims[name]
218
+
219
+ def make_symbolic_graph(self) -> None:
220
+ k_endog = self.k_endog
221
+ k_states = self.k_states // k_endog
222
+ k_posdef = self.k_posdef // k_endog
223
+
224
+ initial_trend = self.make_and_register_variable(
225
+ f"initial_{self.name}",
226
+ shape=(k_states,) if k_endog == 1 else (k_endog, k_states),
227
+ )
228
+ self.ssm["initial_state", :] = initial_trend.ravel()
229
+
230
+ triu_idx = pt.triu_indices(k_states)
231
+ T = pt.zeros((k_states, k_states))[triu_idx[0], triu_idx[1]].set(1)
232
+
233
+ self.ssm["transition", :, :] = pt.specify_shape(
234
+ pt.linalg.block_diag(*[T for _ in range(k_endog)]), (self.k_states, self.k_states)
235
+ )
236
+
237
+ R = np.eye(k_states)
238
+ R = R[:, self.innovations_order]
239
+
240
+ self.ssm["selection", :, :] = pt.specify_shape(
241
+ pt.linalg.block_diag(*[R for _ in range(k_endog)]), (self.k_states, self.k_posdef)
242
+ )
243
+
244
+ Z = np.array([1.0] + [0.0] * (k_states - 1)).reshape((1, -1))
245
+
246
+ self.ssm["design", :, :] = pt.specify_shape(
247
+ pt.linalg.block_diag(*[Z for _ in range(k_endog)]), (self.k_endog, self.k_states)
248
+ )
249
+
250
+ if k_posdef > 0:
251
+ sigma_trend = self.make_and_register_variable(
252
+ f"sigma_{self.name}",
253
+ shape=(k_posdef,) if k_endog == 1 else (k_endog, k_posdef),
254
+ )
255
+ diag_idx = np.diag_indices(k_posdef * k_endog)
256
+ idx = np.s_["state_cov", diag_idx[0], diag_idx[1]]
257
+ self.ssm[idx] = (sigma_trend**2).ravel()
@@ -0,0 +1,137 @@
1
+ import numpy as np
2
+
3
+ from pymc_extras.statespace.models.structural.core import Component
4
+
5
+
6
+ class MeasurementError(Component):
7
+ r"""
8
+ Measurement error component for structural time series models.
9
+
10
+ This component adds observation noise to the model by introducing a variance parameter
11
+ that affects the observation covariance matrix H. Unlike other components, it has no
12
+ hidden states and should only be used in combination with other components.
13
+
14
+ Parameters
15
+ ----------
16
+ name : str, optional
17
+ Name of the measurement error component. Default is "MeasurementError".
18
+ observed_state_names : list[str] | None, optional
19
+ Names of the observed variables. If None, defaults to ["data"].
20
+
21
+ Notes
22
+ -----
23
+ The measurement error component models observation noise as:
24
+
25
+ .. math::
26
+
27
+ y_t = \text{signal}_t + \varepsilon_t, \quad \varepsilon_t \sim N(0, \sigma^2)
28
+
29
+ Where :math:`\text{signal}_t` is the true signal from other components and
30
+ :math:`\sigma^2` is the measurement error variance.
31
+
32
+ This component:
33
+ - Has no hidden states (k_states = 0)
34
+ - Has no innovations (k_posdef = 0)
35
+ - Adds a single parameter: sigma_{name}
36
+ - Modifies the observation covariance matrix H
37
+
38
+ Examples
39
+ --------
40
+ **Basic usage with trend component:**
41
+
42
+ .. code:: python
43
+
44
+ from pymc_extras.statespace import structural as st
45
+ import pymc as pm
46
+ import pytensor.tensor as pt
47
+
48
+ trend = st.LevelTrendComponent(order=2, innovations_order=1)
49
+ error = st.MeasurementError()
50
+
51
+ ss_mod = (trend + error).build()
52
+
53
+ # Use with PyMC
54
+ with pm.Model(coords=ss_mod.coords) as model:
55
+ P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
56
+ initial_trend = pm.Normal('initial_trend', sigma=10, dims=ss_mod.param_dims['initial_trend'])
57
+ sigma_obs = pm.Exponential('sigma_obs', 1, dims=ss_mod.param_dims['sigma_obs'])
58
+
59
+ ss_mod.build_statespace_graph(data)
60
+ idata = pm.sample()
61
+
62
+ **Multivariate measurement error:**
63
+
64
+ .. code:: python
65
+
66
+ # For multiple observed variables
67
+ # This creates separate measurement error variances for each variable
68
+ # sigma_obs_error will have shape (3,) for the three variables
69
+ error = st.MeasurementError(
70
+ name="obs_error",
71
+ observed_state_names=["gdp", "unemployment", "inflation"]
72
+ )
73
+
74
+ **Complete model example:**
75
+
76
+ .. code:: python
77
+
78
+ trend = st.LevelTrendComponent(order=2, innovations_order=1)
79
+ seasonal = st.TimeSeasonality(season_length=12, innovations=True)
80
+ error = st.MeasurementError()
81
+
82
+ model = (trend + seasonal + error).build()
83
+
84
+ # The model now includes:
85
+ # - Trend parameters: level_trend, sigma_trend
86
+ # - Seasonal parameters: seasonal_coefs, sigma_seasonal
87
+ # - Measurement error parameter: sigma_obs
88
+
89
+ See Also
90
+ --------
91
+ Component : Base class for all structural components.
92
+ StructuralTimeSeries : Complete model class.
93
+ """
94
+
95
+ def __init__(
96
+ self, name: str = "MeasurementError", observed_state_names: list[str] | None = None
97
+ ):
98
+ if observed_state_names is None:
99
+ observed_state_names = ["data"]
100
+
101
+ k_endog = len(observed_state_names)
102
+ k_states = 0
103
+ k_posdef = 0
104
+
105
+ super().__init__(
106
+ name,
107
+ k_endog,
108
+ k_states,
109
+ k_posdef,
110
+ measurement_error=True,
111
+ combine_hidden_states=False,
112
+ observed_state_names=observed_state_names,
113
+ )
114
+
115
+ def populate_component_properties(self):
116
+ self.param_names = [f"sigma_{self.name}"]
117
+ self.param_dims = {}
118
+ self.coords = {}
119
+
120
+ if self.k_endog > 1:
121
+ self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
122
+ self.coords[f"endog_{self.name}"] = self.observed_state_names
123
+
124
+ self.param_info = {
125
+ f"sigma_{self.name}": {
126
+ "shape": (self.k_endog,) if self.k_endog > 1 else (),
127
+ "constraints": "Positive",
128
+ "dims": (f"endog_{self.name}",) if self.k_endog > 1 else None,
129
+ }
130
+ }
131
+
132
+ def make_symbolic_graph(self) -> None:
133
+ sigma_shape = () if self.k_endog == 1 else (self.k_endog,)
134
+ error_sigma = self.make_and_register_variable(f"sigma_{self.name}", shape=sigma_shape)
135
+ diag_idx = np.diag_indices(self.k_endog)
136
+ idx = np.s_["obs_cov", diag_idx[0], diag_idx[1]]
137
+ self.ssm[idx] = error_sigma**2
@@ -0,0 +1,228 @@
1
+ import numpy as np
2
+
3
+ from pytensor import tensor as pt
4
+
5
+ from pymc_extras.statespace.models.structural.core import Component
6
+ from pymc_extras.statespace.utils.constants import TIME_DIM
7
+
8
+
9
+ class RegressionComponent(Component):
10
+ r"""
11
+ Regression component for exogenous variables in a structural time series model
12
+
13
+ Parameters
14
+ ----------
15
+ k_exog : int | None, default None
16
+ Number of exogenous variables to include in the regression. Must be specified if
17
+ state_names is not provided.
18
+
19
+ name : str | None, default "regression"
20
+ A name for this regression component. Used to label dimensions and coordinates.
21
+
22
+ state_names : list[str] | None, default None
23
+ List of strings for regression coefficient labels. If provided, must be of length
24
+ k_exog. If None and k_exog is provided, coefficients will be named
25
+ "{name}_1, {name}_2, ...".
26
+
27
+ observed_state_names : list[str] | None, default None
28
+ List of strings for observed state labels. If None, defaults to ["data"].
29
+
30
+ innovations : bool, default False
31
+ Whether to include stochastic innovations in the regression coefficients,
32
+ allowing them to vary over time. If True, coefficients follow a random walk.
33
+
34
+ Notes
35
+ -----
36
+ This component implements regression with exogenous variables in a structural time series
37
+ model. The regression component can be expressed as:
38
+
39
+ .. math::
40
+ y_t = \beta_t^T x_t + \epsilon_t
41
+
42
+ Where :math:`y_t` is the dependent variable, :math:`x_t` is the vector of exogenous
43
+ variables, :math:`\beta_t` is the vector of regression coefficients, and :math:`\epsilon_t`
44
+ is the error term.
45
+
46
+ When ``innovations=False`` (default), the coefficients are constant over time:
47
+ :math:`\beta_t = \beta_0` for all t.
48
+
49
+ When ``innovations=True``, the coefficients follow a random walk:
50
+ :math:`\beta_{t+1} = \beta_t + \eta_t`, where :math:`\eta_t \sim N(0, \Sigma_\beta)`.
51
+
52
+ The component supports both univariate and multivariate regression. In the multivariate
53
+ case, separate coefficients are estimated for each endogenous variable (i.e time series).
54
+
55
+ Examples
56
+ --------
57
+ Simple regression with constant coefficients:
58
+
59
+ .. code:: python
60
+
61
+ from pymc_extras.statespace import structural as st
62
+ import pymc as pm
63
+ import pytensor.tensor as pt
64
+
65
+ trend = st.LevelTrendComponent(order=1, innovations_order=1)
66
+ regression = st.RegressionComponent(k_exog=2, state_names=['intercept', 'slope'])
67
+ ss_mod = (trend + regression).build()
68
+
69
+ with pm.Model(coords=ss_mod.coords) as model:
70
+ # Prior for regression coefficients
71
+ betas = pm.Normal('betas', dims=ss_mod.param_dims['beta_regression'])
72
+
73
+ # Prior for trend innovations
74
+ sigma_trend = pm.Exponential('sigma_trend', 1)
75
+
76
+ ss_mod.build_statespace_graph(data)
77
+ idata = pm.sample()
78
+
79
+ Multivariate regression with time-varying coefficients:
80
+ - There are 2 exogenous variables (price and income effects)
81
+ - There are 2 endogenous variables (sales and revenue)
82
+ - The regression coefficients are allowed to vary over time (`innovations=True`)
83
+
84
+ .. code:: python
85
+
86
+ regression = st.RegressionComponent(
87
+ k_exog=2,
88
+ state_names=['price_effect', 'income_effect'],
89
+ observed_state_names=['sales', 'revenue'],
90
+ innovations=True
91
+ )
92
+
93
+ with pm.Model(coords=ss_mod.coords) as model:
94
+ betas = pm.Normal('betas', dims=ss_mod.param_dims['beta_regression'])
95
+
96
+ # Innovation variance for time-varying coefficients
97
+ sigma_beta = pm.Exponential('sigma_beta', 1, dims=ss_mod.param_dims['sigma_beta_regression'])
98
+
99
+ ss_mod.build_statespace_graph(data)
100
+ idata = pm.sample()
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ k_exog: int | None = None,
106
+ name: str | None = "regression",
107
+ state_names: list[str] | None = None,
108
+ observed_state_names: list[str] | None = None,
109
+ innovations=False,
110
+ ):
111
+ if observed_state_names is None:
112
+ observed_state_names = ["data"]
113
+
114
+ self.innovations = innovations
115
+ k_exog = self._handle_input_data(k_exog, state_names, name)
116
+
117
+ k_states = k_exog
118
+ k_endog = len(observed_state_names)
119
+ k_posdef = k_exog
120
+
121
+ super().__init__(
122
+ name=name,
123
+ k_endog=k_endog,
124
+ k_states=k_states * k_endog,
125
+ k_posdef=k_posdef * k_endog,
126
+ state_names=self.state_names,
127
+ observed_state_names=observed_state_names,
128
+ measurement_error=False,
129
+ combine_hidden_states=False,
130
+ exog_names=[f"data_{name}"],
131
+ obs_state_idxs=np.ones(k_states),
132
+ )
133
+
134
+ @staticmethod
135
+ def _get_state_names(k_exog: int | None, state_names: list[str] | None, name: str):
136
+ if k_exog is None and state_names is None:
137
+ raise ValueError("Must specify at least one of k_exog or state_names")
138
+ if state_names is not None and k_exog is not None:
139
+ if len(state_names) != k_exog:
140
+ raise ValueError(f"Expected {k_exog} state names, found {len(state_names)}")
141
+ elif k_exog is None:
142
+ k_exog = len(state_names)
143
+ else:
144
+ state_names = [f"{name}_{i + 1}" for i in range(k_exog)]
145
+
146
+ return k_exog, state_names
147
+
148
+ def _handle_input_data(self, k_exog: int, state_names: list[str] | None, name) -> int:
149
+ k_exog, state_names = self._get_state_names(k_exog, state_names, name)
150
+ self.state_names = state_names
151
+
152
+ return k_exog
153
+
154
+ def make_symbolic_graph(self) -> None:
155
+ k_endog = self.k_endog
156
+ k_states = self.k_states // k_endog
157
+
158
+ betas = self.make_and_register_variable(
159
+ f"beta_{self.name}", shape=(k_endog, k_states) if k_endog > 1 else (k_states,)
160
+ )
161
+ regression_data = self.make_and_register_data(f"data_{self.name}", shape=(None, k_states))
162
+
163
+ self.ssm["initial_state", :] = betas.ravel()
164
+ self.ssm["transition", :, :] = pt.eye(self.k_states)
165
+ self.ssm["selection", :, :] = pt.eye(self.k_states)
166
+
167
+ Z = pt.linalg.block_diag(*[pt.expand_dims(regression_data, 1) for _ in range(k_endog)])
168
+ self.ssm["design"] = pt.specify_shape(
169
+ Z, (None, k_endog, regression_data.type.shape[1] * k_endog)
170
+ )
171
+
172
+ if self.innovations:
173
+ sigma_beta = self.make_and_register_variable(
174
+ f"sigma_beta_{self.name}", (k_states,) if k_endog == 1 else (k_endog, k_states)
175
+ )
176
+ row_idx, col_idx = np.diag_indices(self.k_states)
177
+ self.ssm["state_cov", row_idx, col_idx] = sigma_beta.ravel() ** 2
178
+
179
+ def populate_component_properties(self) -> None:
180
+ k_endog = self.k_endog
181
+ k_states = self.k_states // k_endog
182
+
183
+ self.shock_names = self.state_names
184
+
185
+ self.param_names = [f"beta_{self.name}"]
186
+ self.data_names = [f"data_{self.name}"]
187
+ self.param_dims = {
188
+ f"beta_{self.name}": (f"endog_{self.name}", f"state_{self.name}")
189
+ if k_endog > 1
190
+ else (f"state_{self.name}",)
191
+ }
192
+
193
+ base_names = self.state_names
194
+ self.state_names = [
195
+ f"{name}[{obs_name}]" for obs_name in self.observed_state_names for name in base_names
196
+ ]
197
+
198
+ self.param_info = {
199
+ f"beta_{self.name}": {
200
+ "shape": (k_endog, k_states) if k_endog > 1 else (k_states,),
201
+ "constraints": None,
202
+ "dims": (f"endog_{self.name}", f"state_{self.name}")
203
+ if k_endog > 1
204
+ else (f"state_{self.name}",),
205
+ },
206
+ }
207
+
208
+ self.data_info = {
209
+ f"data_{self.name}": {
210
+ "shape": (None, k_states),
211
+ "dims": (TIME_DIM, f"state_{self.name}"),
212
+ },
213
+ }
214
+ self.coords = {
215
+ f"state_{self.name}": base_names,
216
+ f"endog_{self.name}": self.observed_state_names,
217
+ }
218
+
219
+ if self.innovations:
220
+ self.param_names += [f"sigma_beta_{self.name}"]
221
+ self.param_dims[f"sigma_beta_{self.name}"] = (f"state_{self.name}",)
222
+ self.param_info[f"sigma_beta_{self.name}"] = {
223
+ "shape": (k_states,),
224
+ "constraints": "Positive",
225
+ "dims": (f"state_{self.name}",)
226
+ if k_endog == 1
227
+ else (f"endog_{self.name}", f"state_{self.name}"),
228
+ }