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.
Files changed (35) hide show
  1. pymc_extras/distributions/__init__.py +5 -5
  2. pymc_extras/distributions/histogram_utils.py +1 -1
  3. pymc_extras/inference/__init__.py +1 -1
  4. pymc_extras/inference/laplace_approx/find_map.py +12 -5
  5. pymc_extras/inference/laplace_approx/idata.py +4 -3
  6. pymc_extras/inference/laplace_approx/laplace.py +6 -4
  7. pymc_extras/inference/pathfinder/pathfinder.py +1 -2
  8. pymc_extras/printing.py +1 -1
  9. pymc_extras/statespace/__init__.py +4 -4
  10. pymc_extras/statespace/core/__init__.py +1 -1
  11. pymc_extras/statespace/core/representation.py +8 -8
  12. pymc_extras/statespace/core/statespace.py +94 -23
  13. pymc_extras/statespace/filters/__init__.py +3 -3
  14. pymc_extras/statespace/filters/kalman_filter.py +16 -11
  15. pymc_extras/statespace/models/SARIMAX.py +138 -74
  16. pymc_extras/statespace/models/VARMAX.py +248 -57
  17. pymc_extras/statespace/models/__init__.py +2 -2
  18. pymc_extras/statespace/models/structural/__init__.py +21 -0
  19. pymc_extras/statespace/models/structural/components/__init__.py +0 -0
  20. pymc_extras/statespace/models/structural/components/autoregressive.py +213 -0
  21. pymc_extras/statespace/models/structural/components/cycle.py +325 -0
  22. pymc_extras/statespace/models/structural/components/level_trend.py +289 -0
  23. pymc_extras/statespace/models/structural/components/measurement_error.py +154 -0
  24. pymc_extras/statespace/models/structural/components/regression.py +257 -0
  25. pymc_extras/statespace/models/structural/components/seasonality.py +628 -0
  26. pymc_extras/statespace/models/structural/core.py +919 -0
  27. pymc_extras/statespace/models/structural/utils.py +16 -0
  28. pymc_extras/statespace/models/utilities.py +285 -0
  29. pymc_extras/statespace/utils/constants.py +21 -18
  30. pymc_extras/statespace/utils/data_tools.py +4 -3
  31. {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/METADATA +5 -4
  32. {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/RECORD +34 -25
  33. pymc_extras/statespace/models/structural.py +0 -1679
  34. {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/WHEEL +0 -0
  35. {pymc_extras-0.3.1.dist-info → pymc_extras-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,154 @@
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
+ share_states: bool, default False
21
+ Whether latent states are shared across the observed states. If True, there will be only one set of latent
22
+ states, which are observed by all observed states. If False, each observed state has its own set of
23
+ latent states. This argument has no effect if `k_endog` is 1.
24
+
25
+ Notes
26
+ -----
27
+ The measurement error component models observation noise as:
28
+
29
+ .. math::
30
+
31
+ y_t = \text{signal}_t + \varepsilon_t, \quad \varepsilon_t \sim N(0, \sigma^2)
32
+
33
+ Where :math:`\text{signal}_t` is the true signal from other components and
34
+ :math:`\sigma^2` is the measurement error variance.
35
+
36
+ This component:
37
+ - Has no hidden states (k_states = 0)
38
+ - Has no innovations (k_posdef = 0)
39
+ - Adds a single parameter: sigma_{name}
40
+ - Modifies the observation covariance matrix H
41
+
42
+ Examples
43
+ --------
44
+ **Basic usage with trend component:**
45
+
46
+ .. code:: python
47
+
48
+ from pymc_extras.statespace import structural as st
49
+ import pymc as pm
50
+ import pytensor.tensor as pt
51
+
52
+ trend = st.LevelTrendComponent(order=2, innovations_order=1)
53
+ error = st.MeasurementError()
54
+
55
+ ss_mod = (trend + error).build()
56
+
57
+ # Use with PyMC
58
+ with pm.Model(coords=ss_mod.coords) as model:
59
+ P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
60
+ initial_trend = pm.Normal('initial_trend', sigma=10, dims=ss_mod.param_dims['initial_trend'])
61
+ sigma_obs = pm.Exponential('sigma_obs', 1, dims=ss_mod.param_dims['sigma_obs'])
62
+
63
+ ss_mod.build_statespace_graph(data)
64
+ idata = pm.sample()
65
+
66
+ **Multivariate measurement error:**
67
+
68
+ .. code:: python
69
+
70
+ # For multiple observed variables
71
+ # This creates separate measurement error variances for each variable
72
+ # sigma_obs_error will have shape (3,) for the three variables
73
+ error = st.MeasurementError(
74
+ name="obs_error",
75
+ observed_state_names=["gdp", "unemployment", "inflation"]
76
+ )
77
+
78
+ **Complete model example:**
79
+
80
+ .. code:: python
81
+
82
+ trend = st.LevelTrendComponent(order=2, innovations_order=1)
83
+ seasonal = st.TimeSeasonality(season_length=12, innovations=True)
84
+ error = st.MeasurementError()
85
+
86
+ model = (trend + seasonal + error).build()
87
+
88
+ # The model now includes:
89
+ # - Trend parameters: level_trend, sigma_trend
90
+ # - Seasonal parameters: seasonal_coefs, sigma_seasonal
91
+ # - Measurement error parameter: sigma_obs
92
+
93
+ See Also
94
+ --------
95
+ Component : Base class for all structural components.
96
+ StructuralTimeSeries : Complete model class.
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ name: str = "MeasurementError",
102
+ observed_state_names: list[str] | None = None,
103
+ share_states: bool = False,
104
+ ):
105
+ if observed_state_names is None:
106
+ observed_state_names = ["data"]
107
+
108
+ self.share_states = share_states
109
+
110
+ k_endog = len(observed_state_names)
111
+ k_states = 0
112
+ k_posdef = 0
113
+
114
+ super().__init__(
115
+ name,
116
+ k_endog,
117
+ k_states,
118
+ k_posdef,
119
+ measurement_error=True,
120
+ combine_hidden_states=False,
121
+ observed_state_names=observed_state_names,
122
+ share_states=share_states,
123
+ )
124
+
125
+ def populate_component_properties(self):
126
+ k_endog = self.k_endog
127
+ k_endog_effective = 1 if self.share_states else k_endog
128
+
129
+ self.param_names = [f"sigma_{self.name}"]
130
+ self.param_dims = {}
131
+ self.coords = {}
132
+
133
+ if k_endog_effective > 1:
134
+ self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
135
+ self.coords[f"endog_{self.name}"] = self.observed_state_names
136
+
137
+ self.param_info = {
138
+ f"sigma_{self.name}": {
139
+ "shape": (k_endog_effective,) if k_endog_effective > 1 else (),
140
+ "constraints": "Positive",
141
+ "dims": (f"endog_{self.name}",) if k_endog_effective > 1 else None,
142
+ }
143
+ }
144
+
145
+ def make_symbolic_graph(self) -> None:
146
+ k_endog = self.k_endog
147
+ k_endog_effective = 1 if self.share_states else k_endog
148
+
149
+ sigma_shape = () if k_endog_effective == 1 else (k_endog_effective,)
150
+ error_sigma = self.make_and_register_variable(f"sigma_{self.name}", shape=sigma_shape)
151
+
152
+ diag_idx = np.diag_indices(self.k_endog)
153
+ idx = np.s_["obs_cov", diag_idx[0], diag_idx[1]]
154
+ self.ssm[idx] = error_sigma**2
@@ -0,0 +1,257 @@
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
+ share_states: bool, default False
35
+ Whether latent states are shared across the observed states. If True, there will be only one set of latent
36
+ states, which are observed by all observed states. If False, each observed state has its own set of
37
+ latent states.
38
+
39
+ Notes
40
+ -----
41
+ This component implements regression with exogenous variables in a structural time series
42
+ model. The regression component can be expressed as:
43
+
44
+ .. math::
45
+ y_t = \beta_t^T x_t + \epsilon_t
46
+
47
+ Where :math:`y_t` is the dependent variable, :math:`x_t` is the vector of exogenous
48
+ variables, :math:`\beta_t` is the vector of regression coefficients, and :math:`\epsilon_t`
49
+ is the error term.
50
+
51
+ When ``innovations=False`` (default), the coefficients are constant over time:
52
+ :math:`\beta_t = \beta_0` for all t.
53
+
54
+ When ``innovations=True``, the coefficients follow a random walk:
55
+ :math:`\beta_{t+1} = \beta_t + \eta_t`, where :math:`\eta_t \sim N(0, \Sigma_\beta)`.
56
+
57
+ The component supports both univariate and multivariate regression. In the multivariate
58
+ case, separate coefficients are estimated for each endogenous variable (i.e time series).
59
+
60
+ Examples
61
+ --------
62
+ Simple regression with constant coefficients:
63
+
64
+ .. code:: python
65
+
66
+ from pymc_extras.statespace import structural as st
67
+ import pymc as pm
68
+ import pytensor.tensor as pt
69
+
70
+ trend = st.LevelTrendComponent(order=1, innovations_order=1)
71
+ regression = st.RegressionComponent(k_exog=2, state_names=['intercept', 'slope'])
72
+ ss_mod = (trend + regression).build()
73
+
74
+ with pm.Model(coords=ss_mod.coords) as model:
75
+ # Prior for regression coefficients
76
+ betas = pm.Normal('betas', dims=ss_mod.param_dims['beta_regression'])
77
+
78
+ # Prior for trend innovations
79
+ sigma_trend = pm.Exponential('sigma_trend', 1)
80
+
81
+ ss_mod.build_statespace_graph(data)
82
+ idata = pm.sample()
83
+
84
+ Multivariate regression with time-varying coefficients:
85
+ - There are 2 exogenous variables (price and income effects)
86
+ - There are 2 endogenous variables (sales and revenue)
87
+ - The regression coefficients are allowed to vary over time (`innovations=True`)
88
+
89
+ .. code:: python
90
+
91
+ regression = st.RegressionComponent(
92
+ k_exog=2,
93
+ state_names=['price_effect', 'income_effect'],
94
+ observed_state_names=['sales', 'revenue'],
95
+ innovations=True
96
+ )
97
+
98
+ with pm.Model(coords=ss_mod.coords) as model:
99
+ betas = pm.Normal('betas', dims=ss_mod.param_dims['beta_regression'])
100
+
101
+ # Innovation variance for time-varying coefficients
102
+ sigma_beta = pm.Exponential('sigma_beta', 1, dims=ss_mod.param_dims['sigma_beta_regression'])
103
+
104
+ ss_mod.build_statespace_graph(data)
105
+ idata = pm.sample()
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ k_exog: int | None = None,
111
+ name: str | None = "regression",
112
+ state_names: list[str] | None = None,
113
+ observed_state_names: list[str] | None = None,
114
+ innovations=False,
115
+ share_states: bool = False,
116
+ ):
117
+ self.share_states = share_states
118
+
119
+ if observed_state_names is None:
120
+ observed_state_names = ["data"]
121
+
122
+ self.innovations = innovations
123
+ k_exog = self._handle_input_data(k_exog, state_names, name)
124
+
125
+ k_states = k_exog
126
+ k_endog = len(observed_state_names)
127
+ k_posdef = k_exog
128
+
129
+ super().__init__(
130
+ name=name,
131
+ k_endog=k_endog,
132
+ k_states=k_states * k_endog if not share_states else k_states,
133
+ k_posdef=k_posdef * k_endog if not share_states else k_posdef,
134
+ state_names=self.state_names,
135
+ share_states=share_states,
136
+ observed_state_names=observed_state_names,
137
+ measurement_error=False,
138
+ combine_hidden_states=False,
139
+ exog_names=[f"data_{name}"],
140
+ obs_state_idxs=np.ones(k_states),
141
+ )
142
+
143
+ @staticmethod
144
+ def _get_state_names(k_exog: int | None, state_names: list[str] | None, name: str):
145
+ if k_exog is None and state_names is None:
146
+ raise ValueError("Must specify at least one of k_exog or state_names")
147
+ if state_names is not None and k_exog is not None:
148
+ if len(state_names) != k_exog:
149
+ raise ValueError(f"Expected {k_exog} state names, found {len(state_names)}")
150
+ elif k_exog is None:
151
+ k_exog = len(state_names)
152
+ else:
153
+ state_names = [f"{name}_{i + 1}" for i in range(k_exog)]
154
+
155
+ return k_exog, state_names
156
+
157
+ def _handle_input_data(self, k_exog: int, state_names: list[str] | None, name) -> int:
158
+ k_exog, state_names = self._get_state_names(k_exog, state_names, name)
159
+ self.state_names = state_names
160
+
161
+ return k_exog
162
+
163
+ def make_symbolic_graph(self) -> None:
164
+ k_endog = self.k_endog
165
+ k_endog_effective = 1 if self.share_states else k_endog
166
+
167
+ k_states = self.k_states // k_endog_effective
168
+
169
+ betas = self.make_and_register_variable(
170
+ f"beta_{self.name}", shape=(k_endog, k_states) if k_endog_effective > 1 else (k_states,)
171
+ )
172
+ regression_data = self.make_and_register_data(f"data_{self.name}", shape=(None, k_states))
173
+
174
+ self.ssm["initial_state", :] = betas.ravel()
175
+ self.ssm["transition", :, :] = pt.eye(self.k_states)
176
+ self.ssm["selection", :, :] = pt.eye(self.k_states)
177
+
178
+ if self.share_states:
179
+ self.ssm["design"] = pt.specify_shape(
180
+ pt.join(1, *[pt.expand_dims(regression_data, 1) for _ in range(k_endog)]),
181
+ (None, k_endog, self.k_states),
182
+ )
183
+ else:
184
+ Z = pt.linalg.block_diag(*[pt.expand_dims(regression_data, 1) for _ in range(k_endog)])
185
+ self.ssm["design"] = pt.specify_shape(
186
+ Z, (None, k_endog, regression_data.type.shape[1] * k_endog)
187
+ )
188
+
189
+ if self.innovations:
190
+ sigma_beta = self.make_and_register_variable(
191
+ f"sigma_beta_{self.name}",
192
+ (k_states,) if k_endog_effective == 1 else (k_endog, k_states),
193
+ )
194
+ row_idx, col_idx = np.diag_indices(self.k_states)
195
+ self.ssm["state_cov", row_idx, col_idx] = sigma_beta.ravel() ** 2
196
+
197
+ def populate_component_properties(self) -> None:
198
+ k_endog = self.k_endog
199
+ k_endog_effective = 1 if self.share_states else k_endog
200
+
201
+ k_states = self.k_states // k_endog_effective
202
+
203
+ if self.share_states:
204
+ self.shock_names = [f"{state_name}_shared" for state_name in self.state_names]
205
+ else:
206
+ self.shock_names = self.state_names
207
+
208
+ self.param_names = [f"beta_{self.name}"]
209
+ self.data_names = [f"data_{self.name}"]
210
+ self.param_dims = {
211
+ f"beta_{self.name}": (f"endog_{self.name}", f"state_{self.name}")
212
+ if k_endog_effective > 1
213
+ else (f"state_{self.name}",)
214
+ }
215
+
216
+ base_names = self.state_names
217
+
218
+ if self.share_states:
219
+ self.state_names = [f"{name}[{self.name}_shared]" for name in base_names]
220
+ else:
221
+ self.state_names = [
222
+ f"{name}[{obs_name}]"
223
+ for obs_name in self.observed_state_names
224
+ for name in base_names
225
+ ]
226
+
227
+ self.param_info = {
228
+ f"beta_{self.name}": {
229
+ "shape": (k_endog_effective, k_states) if k_endog_effective > 1 else (k_states,),
230
+ "constraints": None,
231
+ "dims": (f"endog_{self.name}", f"state_{self.name}")
232
+ if k_endog_effective > 1
233
+ else (f"state_{self.name}",),
234
+ },
235
+ }
236
+
237
+ self.data_info = {
238
+ f"data_{self.name}": {
239
+ "shape": (None, k_states),
240
+ "dims": (TIME_DIM, f"state_{self.name}"),
241
+ },
242
+ }
243
+ self.coords = {
244
+ f"state_{self.name}": base_names,
245
+ f"endog_{self.name}": self.observed_state_names,
246
+ }
247
+
248
+ if self.innovations:
249
+ self.param_names += [f"sigma_beta_{self.name}"]
250
+ self.param_dims[f"sigma_beta_{self.name}"] = (f"state_{self.name}",)
251
+ self.param_info[f"sigma_beta_{self.name}"] = {
252
+ "shape": (k_states,),
253
+ "constraints": "Positive",
254
+ "dims": (f"state_{self.name}",)
255
+ if k_endog_effective == 1
256
+ else (f"endog_{self.name}", f"state_{self.name}"),
257
+ }