pymc-extras 0.6.0__py3-none-any.whl → 0.8.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.
Files changed (31) hide show
  1. pymc_extras/distributions/timeseries.py +10 -10
  2. pymc_extras/inference/dadvi/dadvi.py +14 -83
  3. pymc_extras/inference/laplace_approx/laplace.py +187 -159
  4. pymc_extras/inference/pathfinder/pathfinder.py +12 -7
  5. pymc_extras/inference/smc/sampling.py +2 -2
  6. pymc_extras/model/marginal/distributions.py +4 -2
  7. pymc_extras/model/marginal/marginal_model.py +12 -2
  8. pymc_extras/prior.py +3 -3
  9. pymc_extras/statespace/core/properties.py +276 -0
  10. pymc_extras/statespace/core/statespace.py +182 -45
  11. pymc_extras/statespace/filters/distributions.py +19 -34
  12. pymc_extras/statespace/filters/kalman_filter.py +13 -12
  13. pymc_extras/statespace/filters/kalman_smoother.py +2 -2
  14. pymc_extras/statespace/models/DFM.py +179 -168
  15. pymc_extras/statespace/models/ETS.py +177 -151
  16. pymc_extras/statespace/models/SARIMAX.py +149 -152
  17. pymc_extras/statespace/models/VARMAX.py +134 -145
  18. pymc_extras/statespace/models/__init__.py +8 -1
  19. pymc_extras/statespace/models/structural/__init__.py +30 -8
  20. pymc_extras/statespace/models/structural/components/autoregressive.py +87 -45
  21. pymc_extras/statespace/models/structural/components/cycle.py +119 -80
  22. pymc_extras/statespace/models/structural/components/level_trend.py +95 -42
  23. pymc_extras/statespace/models/structural/components/measurement_error.py +27 -17
  24. pymc_extras/statespace/models/structural/components/regression.py +105 -68
  25. pymc_extras/statespace/models/structural/components/seasonality.py +138 -100
  26. pymc_extras/statespace/models/structural/core.py +397 -286
  27. pymc_extras/statespace/models/utilities.py +5 -20
  28. {pymc_extras-0.6.0.dist-info → pymc_extras-0.8.0.dist-info}/METADATA +4 -4
  29. {pymc_extras-0.6.0.dist-info → pymc_extras-0.8.0.dist-info}/RECORD +31 -30
  30. {pymc_extras-0.6.0.dist-info → pymc_extras-0.8.0.dist-info}/WHEEL +0 -0
  31. {pymc_extras-0.6.0.dist-info → pymc_extras-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,13 +1,22 @@
1
+ import warnings
2
+
1
3
  import numpy as np
2
4
 
3
5
  from pytensor import tensor as pt
4
6
 
7
+ from pymc_extras.statespace.core.properties import (
8
+ Coord,
9
+ Data,
10
+ Parameter,
11
+ Shock,
12
+ State,
13
+ )
5
14
  from pymc_extras.statespace.models.structural.core import Component
6
15
  from pymc_extras.statespace.models.utilities import validate_names
7
16
  from pymc_extras.statespace.utils.constants import TIME_DIM
8
17
 
9
18
 
10
- class RegressionComponent(Component):
19
+ class Regression(Component):
11
20
  r"""
12
21
  Regression component for exogenous variables in a structural time series model
13
22
 
@@ -64,8 +73,8 @@ class RegressionComponent(Component):
64
73
  import pymc as pm
65
74
  import pytensor.tensor as pt
66
75
 
67
- trend = st.LevelTrendComponent(order=1, innovations_order=1)
68
- regression = st.RegressionComponent(k_exog=2, state_names=['intercept', 'slope'])
76
+ trend = st.LevelTrend(order=1, innovations_order=1)
77
+ regression = st.Regression(k_exog=2, state_names=['intercept', 'slope'])
69
78
  ss_mod = (trend + regression).build()
70
79
 
71
80
  with pm.Model(coords=ss_mod.coords) as model:
@@ -85,7 +94,7 @@ class RegressionComponent(Component):
85
94
 
86
95
  .. code:: python
87
96
 
88
- regression = st.RegressionComponent(
97
+ regression = st.Regression(
89
98
  k_exog=2,
90
99
  state_names=['price_effect', 'income_effect'],
91
100
  observed_state_names=['sales', 'revenue'],
@@ -118,7 +127,6 @@ class RegressionComponent(Component):
118
127
  self.innovations = innovations
119
128
  validate_names(state_names, var_name="state_names", optional=False)
120
129
  k_exog = len(state_names)
121
- self.state_names = state_names
122
130
 
123
131
  k_states = k_exog
124
132
  k_endog = len(observed_state_names)
@@ -129,15 +137,94 @@ class RegressionComponent(Component):
129
137
  k_endog=k_endog,
130
138
  k_states=k_states * k_endog if not share_states else k_states,
131
139
  k_posdef=k_posdef * k_endog if not share_states else k_posdef,
132
- state_names=self.state_names,
140
+ base_state_names=state_names,
133
141
  share_states=share_states,
134
- observed_state_names=observed_state_names,
142
+ base_observed_state_names=observed_state_names,
135
143
  measurement_error=False,
136
144
  combine_hidden_states=False,
137
- exog_names=[f"data_{name}"],
138
145
  obs_state_idxs=np.ones(k_states),
139
146
  )
140
147
 
148
+ def set_states(self) -> State | tuple[State, ...] | None:
149
+ base_names = self.base_state_names
150
+ observed_state_names = self.base_observed_state_names
151
+
152
+ if self.share_states:
153
+ state_names = [f"{name}[{self.name}_shared]" for name in base_names]
154
+ else:
155
+ state_names = [
156
+ f"{name}[{obs_name}]" for obs_name in observed_state_names for name in base_names
157
+ ]
158
+
159
+ hidden_states = [State(name=name, observed=False, shared=True) for name in state_names]
160
+ observed_states = [
161
+ State(name=name, observed=True, shared=False) for name in observed_state_names
162
+ ]
163
+ return *hidden_states, *observed_states
164
+
165
+ def set_parameters(self) -> Parameter | tuple[Parameter, ...] | None:
166
+ k_endog = self.k_endog
167
+ k_endog_effective = 1 if self.share_states else k_endog
168
+ k_states = self.k_states // k_endog_effective
169
+
170
+ beta_parameter = Parameter(
171
+ name=f"beta_{self.name}",
172
+ shape=(k_endog_effective, k_states) if k_endog_effective > 1 else (k_states,),
173
+ dims=(
174
+ (f"endog_{self.name}", f"state_{self.name}")
175
+ if k_endog_effective > 1
176
+ else (f"state_{self.name}",)
177
+ ),
178
+ constraints=None,
179
+ )
180
+
181
+ params_container = [beta_parameter]
182
+
183
+ if self.innovations:
184
+ sigma_parameter = Parameter(
185
+ name=f"sigma_beta_{self.name}",
186
+ shape=(k_states,),
187
+ dims=(f"state_{self.name}",),
188
+ constraints="Positive",
189
+ )
190
+
191
+ params_container.append(sigma_parameter)
192
+
193
+ return tuple(params_container)
194
+
195
+ def set_data_info(self) -> Data | tuple[Data, ...] | None:
196
+ k_endog = self.k_endog
197
+ k_endog_effective = 1 if self.share_states else k_endog
198
+ k_states = self.k_states // k_endog_effective
199
+
200
+ data_prop = Data(
201
+ name=f"data_{self.name}",
202
+ shape=(None, k_states),
203
+ dims=(TIME_DIM, f"state_{self.name}"),
204
+ is_exogenous=True,
205
+ )
206
+ return (data_prop,)
207
+
208
+ def set_shocks(self) -> Shock | tuple[Shock, ...] | None:
209
+ base_names = self.base_state_names
210
+
211
+ if self.share_states:
212
+ shock_names = [f"{state_name}_shared" for state_name in base_names]
213
+ else:
214
+ shock_names = base_names
215
+
216
+ return tuple(Shock(name=name) for name in shock_names)
217
+
218
+ def set_coords(self) -> tuple[Coord, ...] | None:
219
+ regression_state_coord = Coord(
220
+ dimension=f"state_{self.name}", labels=tuple(self.base_state_names)
221
+ )
222
+ endogenous_state_coord = Coord(
223
+ dimension=f"endog_{self.name}", labels=self.observed_state_names
224
+ )
225
+
226
+ return regression_state_coord, endogenous_state_coord
227
+
141
228
  def make_symbolic_graph(self) -> None:
142
229
  k_endog = self.k_endog
143
230
  k_endog_effective = 1 if self.share_states else k_endog
@@ -172,64 +259,14 @@ class RegressionComponent(Component):
172
259
  row_idx, col_idx = np.diag_indices(self.k_states)
173
260
  self.ssm["state_cov", row_idx, col_idx] = sigma_beta.ravel() ** 2
174
261
 
175
- def populate_component_properties(self) -> None:
176
- k_endog = self.k_endog
177
- k_endog_effective = 1 if self.share_states else k_endog
178
-
179
- k_states = self.k_states // k_endog_effective
180
-
181
- if self.share_states:
182
- self.shock_names = [f"{state_name}_shared" for state_name in self.state_names]
183
- else:
184
- self.shock_names = self.state_names
185
-
186
- self.param_names = [f"beta_{self.name}"]
187
- self.data_names = [f"data_{self.name}"]
188
- self.param_dims = {
189
- f"beta_{self.name}": (f"endog_{self.name}", f"state_{self.name}")
190
- if k_endog_effective > 1
191
- else (f"state_{self.name}",)
192
- }
193
262
 
194
- base_names = self.state_names
195
-
196
- if self.share_states:
197
- self.state_names = [f"{name}[{self.name}_shared]" for name in base_names]
198
- else:
199
- self.state_names = [
200
- f"{name}[{obs_name}]"
201
- for obs_name in self.observed_state_names
202
- for name in base_names
203
- ]
204
-
205
- self.param_info = {
206
- f"beta_{self.name}": {
207
- "shape": (k_endog_effective, k_states) if k_endog_effective > 1 else (k_states,),
208
- "constraints": None,
209
- "dims": (f"endog_{self.name}", f"state_{self.name}")
210
- if k_endog_effective > 1
211
- else (f"state_{self.name}",),
212
- },
213
- }
214
-
215
- self.data_info = {
216
- f"data_{self.name}": {
217
- "shape": (None, k_states),
218
- "dims": (TIME_DIM, f"state_{self.name}"),
219
- },
220
- }
221
- self.coords = {
222
- f"state_{self.name}": base_names,
223
- f"endog_{self.name}": self.observed_state_names,
224
- }
225
-
226
- if self.innovations:
227
- self.param_names += [f"sigma_beta_{self.name}"]
228
- self.param_dims[f"sigma_beta_{self.name}"] = (f"state_{self.name}",)
229
- self.param_info[f"sigma_beta_{self.name}"] = {
230
- "shape": (k_states,),
231
- "constraints": "Positive",
232
- "dims": (f"state_{self.name}",)
233
- if k_endog_effective == 1
234
- else (f"endog_{self.name}", f"state_{self.name}"),
235
- }
263
+ def __getattr__(name: str):
264
+ if name == "RegressionComponent":
265
+ warnings.warn(
266
+ "RegressionComponent is deprecated and will be removed in a future release. "
267
+ "Use Regression instead.",
268
+ FutureWarning,
269
+ stacklevel=2,
270
+ )
271
+ return Regression
272
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -1,7 +1,15 @@
1
+ from collections.abc import Sequence
2
+
1
3
  import numpy as np
2
4
 
3
5
  from pytensor import tensor as pt
4
6
 
7
+ from pymc_extras.statespace.core.properties import (
8
+ Coord,
9
+ Parameter,
10
+ Shock,
11
+ State,
12
+ )
5
13
  from pymc_extras.statespace.models.structural.core import Component
6
14
  from pymc_extras.statespace.models.structural.utils import _frequency_transition_block
7
15
 
@@ -202,7 +210,7 @@ class TimeSeasonality(Component):
202
210
  state_names = pd.date_range('1900-01-01', '1900-12-31', freq='MS').month_name().tolist()
203
211
 
204
212
  # Build the structural model
205
- grw = st.LevelTrendComponent(order=1, innovations_order=1)
213
+ grw = st.LevelTrend(order=1, innovations_order=1)
206
214
  annual_season = st.TimeSeasonality(
207
215
  season_length=12, name="annual", state_names=state_names, innovations=False
208
216
  )
@@ -265,7 +273,7 @@ class TimeSeasonality(Component):
265
273
  raise ValueError(
266
274
  f"state_names must be a list of length season_length*duration, got {len(state_names)}"
267
275
  )
268
- state_names = state_names.copy()
276
+ state_names = list(state_names)
269
277
 
270
278
  self.share_states = share_states
271
279
  self.innovations = innovations
@@ -290,7 +298,7 @@ class TimeSeasonality(Component):
290
298
  k_endog=k_endog,
291
299
  k_states=k_states if share_states else k_states * k_endog,
292
300
  k_posdef=k_posdef if share_states else k_posdef * k_endog,
293
- observed_state_names=observed_state_names,
301
+ base_observed_state_names=observed_state_names,
294
302
  measurement_error=False,
295
303
  combine_hidden_states=True,
296
304
  obs_state_idxs=np.tile(
@@ -299,64 +307,77 @@ class TimeSeasonality(Component):
299
307
  share_states=share_states,
300
308
  )
301
309
 
302
- def populate_component_properties(self):
303
- k_endog = self.k_endog
304
- k_endog_effective = 1 if self.share_states else k_endog
305
-
306
- k_states = self.k_states // k_endog_effective
310
+ def set_states(self) -> State | tuple[State, ...] | None:
311
+ observed_state_names = self.base_observed_state_names
307
312
 
308
313
  if self.share_states:
309
- self.state_names = [
314
+ state_names = [
310
315
  f"{state_name}[{self.name}_shared]" for state_name in self.provided_state_names
311
316
  ]
312
317
  else:
313
- self.state_names = [
318
+ state_names = [
314
319
  f"{state_name}[{endog_name}]"
315
- for endog_name in self.observed_state_names
320
+ for endog_name in observed_state_names
316
321
  for state_name in self.provided_state_names
317
322
  ]
318
323
 
319
- self.param_names = [f"params_{self.name}"]
324
+ hidden_states = [State(name=name, observed=False, shared=True) for name in state_names]
325
+ observed_states = [
326
+ State(name=name, observed=True, shared=False) for name in observed_state_names
327
+ ]
328
+ return *hidden_states, *observed_states
320
329
 
321
- self.param_info = {
322
- f"params_{self.name}": {
323
- "shape": (k_states,) if k_endog == 1 else (k_endog, k_states),
324
- "constraints": None,
325
- "dims": (f"state_{self.name}",)
326
- if k_endog_effective == 1
327
- else (f"endog_{self.name}", f"state_{self.name}"),
328
- }
329
- }
330
-
331
- self.param_dims = {
332
- f"params_{self.name}": (f"state_{self.name}",)
333
- if k_endog_effective == 1
334
- else (f"endog_{self.name}", f"state_{self.name}")
335
- }
330
+ def set_parameters(self) -> Parameter | tuple[Parameter, ...] | None:
331
+ k_endog = self.k_endog
332
+ k_endog_effective = 1 if self.share_states else k_endog
333
+ k_states = self.k_states // k_endog_effective
336
334
 
337
- self.coords = (
338
- {f"state_{self.name}": self.provided_state_names}
335
+ seasonal_param = Parameter(
336
+ name=f"params_{self.name}",
337
+ shape=(k_states,) if k_endog == 1 else (k_endog, k_states),
338
+ dims=(f"state_{self.name}",)
339
339
  if k_endog_effective == 1
340
- else {
341
- f"endog_{self.name}": self.observed_state_names,
342
- f"state_{self.name}": self.provided_state_names,
343
- }
340
+ else (f"endog_{self.name}", f"state_{self.name}"),
341
+ constraints=None,
344
342
  )
345
343
 
344
+ params_container = [seasonal_param]
345
+
346
+ if self.innovations:
347
+ sigma_param = Parameter(
348
+ name=f"sigma_{self.name}",
349
+ shape=() if k_endog_effective == 1 else (k_endog,),
350
+ dims=None if k_endog_effective == 1 else (f"endog_{self.name}",),
351
+ constraints="Positive",
352
+ )
353
+ params_container.append(sigma_param)
354
+
355
+ return tuple(params_container)
356
+
357
+ def set_shocks(self) -> Shock | tuple[Shock, ...] | None:
358
+ observed_state_names = self.observed_state_names
346
359
  if self.innovations:
347
- self.param_names += [f"sigma_{self.name}"]
348
- self.param_info[f"sigma_{self.name}"] = {
349
- "shape": () if k_endog_effective == 1 else (k_endog,),
350
- "constraints": "Positive",
351
- "dims": None if k_endog_effective == 1 else (f"endog_{self.name}",),
352
- }
353
360
  if self.share_states:
354
- self.shock_names = [f"{self.name}[shared]"]
361
+ shock_names = [f"{self.name}[shared]"]
355
362
  else:
356
- self.shock_names = [f"{self.name}[{name}]" for name in self.observed_state_names]
363
+ shock_names = [f"{self.name}[{name}]" for name in observed_state_names]
364
+
365
+ return tuple(Shock(name=name) for name in shock_names)
366
+ return None
367
+
368
+ def set_coords(self) -> Coord | tuple[Coord, ...] | None:
369
+ k_endog = self.k_endog
370
+ k_endog_effective = 1 if self.share_states else k_endog
371
+ observed_state_names = self.observed_state_names
372
+
373
+ state_coord = Coord(dimension=f"state_{self.name}", labels=tuple(self.provided_state_names))
374
+ coords_container = [state_coord]
357
375
 
358
- if k_endog > 1:
359
- self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
376
+ if k_endog_effective > 1:
377
+ endog_coord = Coord(dimension=f"endog_{self.name}", labels=observed_state_names)
378
+ coords_container.append(endog_coord)
379
+
380
+ return tuple(coords_container)
360
381
 
361
382
  def make_symbolic_graph(self) -> None:
362
383
  k_endog = self.k_endog
@@ -490,7 +511,7 @@ class FrequencySeasonality(Component):
490
511
  n: int | None = None,
491
512
  name: str | None = None,
492
513
  innovations: bool = True,
493
- observed_state_names: list[str] | None = None,
514
+ observed_state_names: Sequence[str] | None = None,
494
515
  share_states: bool = False,
495
516
  ):
496
517
  if observed_state_names is None:
@@ -527,12 +548,84 @@ class FrequencySeasonality(Component):
527
548
  if share_states
528
549
  else k_states * int(self.innovations) * k_endog,
529
550
  share_states=share_states,
530
- observed_state_names=observed_state_names,
551
+ base_observed_state_names=observed_state_names,
531
552
  measurement_error=False,
532
553
  combine_hidden_states=True,
533
554
  obs_state_idxs=obs_state_idx,
534
555
  )
535
556
 
557
+ def set_states(self) -> State | tuple[State, ...] | None:
558
+ observed_state_names = self.base_observed_state_names
559
+ base_names = [f"{f}_{i}_{self.name}" for i in range(self.n) for f in ["Cos", "Sin"]]
560
+
561
+ if self.share_states:
562
+ state_names = [f"{name}[shared]" for name in base_names]
563
+ else:
564
+ state_names = [
565
+ f"{name}[{obs_state_name}]"
566
+ for obs_state_name in self.base_observed_state_names
567
+ for name in base_names
568
+ ]
569
+
570
+ hidden_states = [State(name=name, observed=False, shared=True) for name in state_names]
571
+ observed_states = [
572
+ State(name=name, observed=True, shared=False) for name in observed_state_names
573
+ ]
574
+ return *hidden_states, *observed_states
575
+
576
+ def set_parameters(self) -> Parameter | tuple[Parameter, ...] | None:
577
+ k_endog = self.k_endog
578
+ k_endog_effective = 1 if self.share_states else k_endog
579
+ n_coefs = self.n_coefs
580
+
581
+ freq_param = Parameter(
582
+ name=f"params_{self.name}",
583
+ shape=(n_coefs,) if k_endog_effective == 1 else (k_endog_effective, n_coefs),
584
+ dims=(f"state_{self.name}",)
585
+ if k_endog_effective == 1
586
+ else (f"endog_{self.name}", f"state_{self.name}"),
587
+ constraints=None,
588
+ )
589
+
590
+ params_container = [freq_param]
591
+
592
+ if self.innovations:
593
+ sigma_param = Parameter(
594
+ name=f"sigma_{self.name}",
595
+ shape=() if k_endog_effective == 1 else (k_endog_effective, n_coefs),
596
+ dims=None if k_endog_effective == 1 else (f"endog_{self.name}",),
597
+ constraints="Positive",
598
+ )
599
+
600
+ params_container.append(sigma_param)
601
+
602
+ return tuple(params_container)
603
+
604
+ def set_shocks(self) -> Shock | tuple[Shock, ...] | None:
605
+ if self.innovations:
606
+ return tuple(Shock(name=name) for name in self.state_names)
607
+ return None
608
+
609
+ def set_coords(self) -> Coord | tuple[Coord, ...] | None:
610
+ k_endog = self.k_endog
611
+ n_coefs = self.n_coefs
612
+ observed_state_names = self.observed_state_names
613
+
614
+ base_names = [f"{f}_{i}_{self.name}" for i in range(self.n) for f in ["Cos", "Sin"]]
615
+
616
+ # Trim state names if the model is saturated
617
+ param_state_names = base_names[:n_coefs]
618
+
619
+ state_coords = Coord(dimension=f"state_{self.name}", labels=tuple(param_state_names))
620
+
621
+ coord_container = [state_coords]
622
+
623
+ if k_endog > 1:
624
+ endog_coords = Coord(dimension=f"endog_{self.name}", labels=observed_state_names)
625
+ coord_container.append(endog_coords)
626
+
627
+ return tuple(coord_container)
628
+
536
629
  def make_symbolic_graph(self) -> None:
537
630
  k_endog = self.k_endog
538
631
  k_endog_effective = 1 if self.share_states else k_endog
@@ -571,58 +664,3 @@ class FrequencySeasonality(Component):
571
664
  self.ssm["state_cov", :, :] = pt.eye(self.k_posdef) * pt.repeat(
572
665
  sigma_season**2, k_posdef
573
666
  )
574
-
575
- def populate_component_properties(self):
576
- k_endog = self.k_endog
577
- k_endog_effective = 1 if self.share_states else k_endog
578
- n_coefs = self.n_coefs
579
-
580
- base_names = [f"{f}_{i}_{self.name}" for i in range(self.n) for f in ["Cos", "Sin"]]
581
-
582
- if self.share_states:
583
- self.state_names = [f"{name}[shared]" for name in base_names]
584
- else:
585
- self.state_names = [
586
- f"{name}[{obs_state_name}]"
587
- for obs_state_name in self.observed_state_names
588
- for name in base_names
589
- ]
590
-
591
- # Trim state names if the model is saturated
592
- param_state_names = base_names[:n_coefs]
593
-
594
- self.param_names = [f"params_{self.name}"]
595
- self.param_dims = {
596
- f"params_{self.name}": (f"state_{self.name}",)
597
- if k_endog_effective == 1
598
- else (f"endog_{self.name}", f"state_{self.name}")
599
- }
600
- self.param_info = {
601
- f"params_{self.name}": {
602
- "shape": (n_coefs,) if k_endog_effective == 1 else (k_endog_effective, n_coefs),
603
- "constraints": None,
604
- "dims": (f"state_{self.name}",)
605
- if k_endog_effective == 1
606
- else (f"endog_{self.name}", f"state_{self.name}"),
607
- }
608
- }
609
-
610
- self.coords = (
611
- {f"state_{self.name}": param_state_names}
612
- if k_endog == 1
613
- else {
614
- f"endog_{self.name}": self.observed_state_names,
615
- f"state_{self.name}": param_state_names,
616
- }
617
- )
618
-
619
- if self.innovations:
620
- self.param_names += [f"sigma_{self.name}"]
621
- self.shock_names = self.state_names.copy()
622
- self.param_info[f"sigma_{self.name}"] = {
623
- "shape": () if k_endog_effective == 1 else (k_endog_effective, n_coefs),
624
- "constraints": "Positive",
625
- "dims": None if k_endog_effective == 1 else (f"endog_{self.name}",),
626
- }
627
- if k_endog_effective > 1:
628
- self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)