pymc-extras 0.7.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 (25) hide show
  1. pymc_extras/inference/laplace_approx/laplace.py +2 -2
  2. pymc_extras/inference/pathfinder/pathfinder.py +1 -1
  3. pymc_extras/prior.py +3 -3
  4. pymc_extras/statespace/core/properties.py +276 -0
  5. pymc_extras/statespace/core/statespace.py +180 -44
  6. pymc_extras/statespace/filters/distributions.py +12 -29
  7. pymc_extras/statespace/filters/kalman_filter.py +1 -1
  8. pymc_extras/statespace/models/DFM.py +179 -168
  9. pymc_extras/statespace/models/ETS.py +177 -151
  10. pymc_extras/statespace/models/SARIMAX.py +149 -152
  11. pymc_extras/statespace/models/VARMAX.py +134 -145
  12. pymc_extras/statespace/models/__init__.py +8 -1
  13. pymc_extras/statespace/models/structural/__init__.py +30 -8
  14. pymc_extras/statespace/models/structural/components/autoregressive.py +87 -45
  15. pymc_extras/statespace/models/structural/components/cycle.py +119 -80
  16. pymc_extras/statespace/models/structural/components/level_trend.py +95 -42
  17. pymc_extras/statespace/models/structural/components/measurement_error.py +27 -17
  18. pymc_extras/statespace/models/structural/components/regression.py +105 -68
  19. pymc_extras/statespace/models/structural/components/seasonality.py +138 -100
  20. pymc_extras/statespace/models/structural/core.py +397 -286
  21. pymc_extras/statespace/models/utilities.py +5 -20
  22. {pymc_extras-0.7.0.dist-info → pymc_extras-0.8.0.dist-info}/METADATA +3 -3
  23. {pymc_extras-0.7.0.dist-info → pymc_extras-0.8.0.dist-info}/RECORD +25 -24
  24. {pymc_extras-0.7.0.dist-info → pymc_extras-0.8.0.dist-info}/WHEEL +0 -0
  25. {pymc_extras-0.7.0.dist-info → pymc_extras-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,20 @@
1
1
  from collections.abc import Sequence
2
- from typing import Any
3
2
 
4
3
  import numpy as np
5
4
  import pytensor.tensor as pt
6
5
 
7
6
  from pytensor import graph_replace
8
7
  from pytensor.compile.mode import Mode
9
- from pytensor.tensor.slinalg import solve_discrete_lyapunov
8
+ from pytensor.tensor.linalg import solve_discrete_lyapunov
10
9
 
10
+ from pymc_extras.statespace.core.properties import (
11
+ Coord,
12
+ Parameter,
13
+ Shock,
14
+ State,
15
+ )
11
16
  from pymc_extras.statespace.core.statespace import PyMCStateSpace, floatX
12
- from pymc_extras.statespace.models.utilities import make_default_coords, validate_names
17
+ from pymc_extras.statespace.models.utilities import validate_names
13
18
  from pymc_extras.statespace.utils.constants import (
14
19
  ALL_STATE_AUX_DIM,
15
20
  ALL_STATE_DIM,
@@ -138,7 +143,7 @@ class BayesianETS(PyMCStateSpace):
138
143
  or 'N'.
139
144
  If provided, the model will be initialized from the given order, and the `trend`, `damped_trend`, and `seasonal`
140
145
  arguments will be ignored.
141
- endog_names: str or list of str
146
+ endog_names: str or Sequence of str
142
147
  Names associated with observed states. If a list, the length should be equal to the number of time series
143
148
  to be estimated.
144
149
  trend: bool
@@ -209,7 +214,7 @@ class BayesianETS(PyMCStateSpace):
209
214
  def __init__(
210
215
  self,
211
216
  order: tuple[str, str, str] | None = None,
212
- endog_names: str | list[str] | None = None,
217
+ endog_names: str | Sequence[str] | None = None,
213
218
  trend: bool = True,
214
219
  damped_trend: bool = False,
215
220
  seasonal: bool = False,
@@ -263,7 +268,9 @@ class BayesianETS(PyMCStateSpace):
263
268
 
264
269
  validate_names(endog_names, var_name="endog_names", optional=False)
265
270
  k_endog = len(endog_names)
266
- self.endog_names = list(endog_names)
271
+ self.endog_names = (
272
+ tuple(endog_names) if not isinstance(endog_names, str) else (endog_names,)
273
+ )
267
274
 
268
275
  if dense_innovation_covariance and k_endog == 1:
269
276
  dense_innovation_covariance = False
@@ -288,169 +295,188 @@ class BayesianETS(PyMCStateSpace):
288
295
  mode=mode,
289
296
  )
290
297
 
291
- @property
292
- def param_names(self):
293
- names = [
294
- "initial_level",
295
- "initial_trend",
296
- "initial_seasonal",
297
- "P0",
298
- "alpha",
299
- "beta",
300
- "gamma",
301
- "phi",
302
- "sigma_state",
303
- "state_cov",
304
- "sigma_obs",
305
- ]
306
- if not self.trend:
307
- names.remove("initial_trend")
308
- names.remove("beta")
309
- if not self.damped_trend:
310
- names.remove("phi")
311
- if not self.seasonal:
312
- names.remove("initial_seasonal")
313
- names.remove("gamma")
314
- if not self.measurement_error:
315
- names.remove("sigma_obs")
298
+ def set_parameters(self) -> Parameter | tuple[Parameter, ...] | None:
299
+ k_endog = self.k_endog
300
+ k_states = self.k_states
301
+ k_posdef = self.k_posdef
316
302
 
317
- if self.dense_innovation_covariance:
318
- names.remove("sigma_state")
319
- else:
320
- names.remove("state_cov")
303
+ parameters = []
321
304
 
322
- if self.stationary_initialization:
323
- names.remove("P0")
324
-
325
- return names
326
-
327
- @property
328
- def param_info(self) -> dict[str, dict[str, Any]]:
329
- info = {
330
- "P0": {
331
- "shape": (self.k_states, self.k_states),
332
- "constraints": "Positive Semi-definite",
333
- },
334
- "initial_level": {
335
- "shape": None if self.k_endog == 1 else (self.k_endog,),
336
- "constraints": None,
337
- },
338
- "initial_trend": {
339
- "shape": None if self.k_endog == 1 else (self.k_endog,),
340
- "constraints": None,
341
- },
342
- "initial_seasonal": {"shape": (self.seasonal_periods,), "constraints": None},
343
- "sigma_obs": {
344
- "shape": None if self.k_endog == 1 else (self.k_endog,),
345
- "constraints": "Positive",
346
- },
347
- "sigma_state": {
348
- "shape": None if self.k_posdef == 1 else (self.k_posdef,),
349
- "constraints": "Positive",
350
- },
351
- "alpha": {
352
- "shape": None if self.k_endog == 1 else (self.k_endog,),
353
- "constraints": "0 < alpha < 1",
354
- },
355
- "beta": {
356
- "shape": None if self.k_endog == 1 else (self.k_endog,),
357
- "constraints": "0 < beta < 1"
358
- if not self.use_transformed_parameterization
359
- else "0 < beta < alpha",
360
- },
361
- "gamma": {
362
- "shape": None if self.k_endog == 1 else (self.k_endog,),
363
- "constraints": "0 < gamma< 1"
364
- if not self.use_transformed_parameterization
365
- else "0 < gamma < (1 - alpha)",
366
- },
367
- "phi": {
368
- "shape": None if self.k_endog == 1 else (self.k_endog,),
369
- "constraints": "0 < phi < 1",
370
- },
371
- }
305
+ # Initial level - always present
306
+ parameters.append(
307
+ Parameter(
308
+ name="initial_level",
309
+ shape=() if k_endog == 1 else (k_endog,),
310
+ dims=None if k_endog == 1 else (OBS_STATE_DIM,),
311
+ constraints=None,
312
+ )
313
+ )
372
314
 
373
- if self.dense_innovation_covariance:
374
- del info["sigma_state"]
375
- info["state_cov"] = {
376
- "shape": (self.k_posdef, self.k_posdef),
377
- "constraints": "Positive Semi-definite",
378
- }
315
+ # Initial trend - only if trend is enabled
316
+ if self.trend:
317
+ parameters.append(
318
+ Parameter(
319
+ name="initial_trend",
320
+ shape=() if k_endog == 1 else (k_endog,),
321
+ dims=None if k_endog == 1 else (OBS_STATE_DIM,),
322
+ constraints=None,
323
+ )
324
+ )
325
+
326
+ # Initial seasonal - only if seasonal is enabled
327
+ if self.seasonal:
328
+ parameters.append(
329
+ Parameter(
330
+ name="initial_seasonal",
331
+ shape=(self.seasonal_periods,)
332
+ if k_endog == 1
333
+ else (k_endog, self.seasonal_periods),
334
+ dims=(ETS_SEASONAL_DIM,) if k_endog == 1 else (OBS_STATE_DIM, ETS_SEASONAL_DIM),
335
+ constraints=None,
336
+ )
337
+ )
379
338
 
380
- for name in self.param_names:
381
- info[name]["dims"] = self.param_dims.get(name, None)
339
+ # P0 - only if not stationary initialization
340
+ if not self.stationary_initialization:
341
+ parameters.append(
342
+ Parameter(
343
+ name="P0",
344
+ shape=(k_states, k_states),
345
+ dims=(ALL_STATE_DIM, ALL_STATE_AUX_DIM),
346
+ constraints="Positive Semi-definite",
347
+ )
348
+ )
382
349
 
383
- return {name: info[name] for name in self.param_names}
350
+ # Alpha - always present
351
+ parameters.append(
352
+ Parameter(
353
+ name="alpha",
354
+ shape=() if k_endog == 1 else (k_endog,),
355
+ dims=None if k_endog == 1 else (OBS_STATE_DIM,),
356
+ constraints="0 < alpha < 1",
357
+ )
358
+ )
384
359
 
385
- @property
386
- def state_names(self):
387
- states = ["innovation", "level"]
360
+ # Beta - only if trend is enabled
388
361
  if self.trend:
389
- states += ["trend"]
362
+ beta_constraint = (
363
+ "0 < beta < alpha" if self.use_transformed_parameterization else "0 < beta < 1"
364
+ )
365
+ parameters.append(
366
+ Parameter(
367
+ name="beta",
368
+ shape=() if k_endog == 1 else (k_endog,),
369
+ dims=None if k_endog == 1 else (OBS_STATE_DIM,),
370
+ constraints=beta_constraint,
371
+ )
372
+ )
373
+
374
+ # Gamma - only if seasonal is enabled
390
375
  if self.seasonal:
391
- states += ["seasonality"]
392
- states += [f"L{i}.season" for i in range(1, self.seasonal_periods)]
376
+ gamma_constraint = (
377
+ "0 < gamma < (1 - alpha)"
378
+ if self.use_transformed_parameterization
379
+ else "0 < gamma < 1"
380
+ )
381
+ parameters.append(
382
+ Parameter(
383
+ name="gamma",
384
+ shape=() if k_endog == 1 else (k_endog,),
385
+ dims=None if k_endog == 1 else (OBS_STATE_DIM,),
386
+ constraints=gamma_constraint,
387
+ )
388
+ )
389
+
390
+ # Phi - only if damped trend is enabled
391
+ if self.damped_trend:
392
+ parameters.append(
393
+ Parameter(
394
+ name="phi",
395
+ shape=() if k_endog == 1 else (k_endog,),
396
+ dims=None if k_endog == 1 else (OBS_STATE_DIM,),
397
+ constraints="0 < phi < 1",
398
+ )
399
+ )
400
+
401
+ # State covariance
402
+ if self.dense_innovation_covariance:
403
+ parameters.append(
404
+ Parameter(
405
+ name="state_cov",
406
+ shape=(k_posdef, k_posdef),
407
+ dims=(OBS_STATE_DIM, OBS_STATE_AUX_DIM),
408
+ constraints="Positive Semi-definite",
409
+ )
410
+ )
411
+ else:
412
+ parameters.append(
413
+ Parameter(
414
+ name="sigma_state",
415
+ shape=() if k_posdef == 1 else (k_posdef,),
416
+ dims=None if k_posdef == 1 else (OBS_STATE_DIM,),
417
+ constraints="Positive",
418
+ )
419
+ )
393
420
 
394
- if self.k_endog > 1:
395
- states = [f"{name}_{state}" for name in self.endog_names for state in states]
421
+ # Observation covariance - only if measurement error is enabled
422
+ if self.measurement_error:
423
+ parameters.append(
424
+ Parameter(
425
+ name="sigma_obs",
426
+ shape=() if k_endog == 1 else (k_endog,),
427
+ dims=None if k_endog == 1 else (OBS_STATE_DIM,),
428
+ constraints="Positive",
429
+ )
430
+ )
396
431
 
397
- return states
432
+ return tuple(parameters)
398
433
 
399
- @property
400
- def observed_states(self):
401
- return self.endog_names
434
+ def set_states(self) -> State | tuple[State, ...] | None:
435
+ k_endog = self.k_endog
402
436
 
403
- @property
404
- def shock_names(self):
405
- return (
406
- ["innovation"]
407
- if self.k_endog == 1
408
- else [f"{name}_innovation" for name in self.endog_names]
409
- )
437
+ base_states = ["innovation", "level"]
438
+ if self.trend:
439
+ base_states.append("trend")
440
+ if self.seasonal:
441
+ base_states.append("seasonality")
442
+ base_states += [f"L{i}.season" for i in range(1, self.seasonal_periods)]
410
443
 
411
- @property
412
- def param_dims(self):
413
- coord_map = {
414
- "P0": (ALL_STATE_DIM, ALL_STATE_AUX_DIM),
415
- "sigma_obs": (OBS_STATE_DIM,),
416
- "sigma_state": (OBS_STATE_DIM,),
417
- "initial_level": (OBS_STATE_DIM,),
418
- "initial_trend": (OBS_STATE_DIM,),
419
- "initial_seasonal": (ETS_SEASONAL_DIM,),
420
- "seasonal_param": (ETS_SEASONAL_DIM,),
421
- }
444
+ if k_endog > 1:
445
+ state_names = [f"{name}_{state}" for name in self.endog_names for state in base_states]
446
+ else:
447
+ state_names = base_states
422
448
 
423
- if self.dense_innovation_covariance:
424
- del coord_map["sigma_state"]
425
- coord_map["state_cov"] = (OBS_STATE_DIM, OBS_STATE_AUX_DIM)
449
+ hidden_states = [State(name=name, observed=False, shared=False) for name in state_names]
426
450
 
427
- if self.k_endog == 1:
428
- coord_map["sigma_state"] = None
429
- coord_map["sigma_obs"] = None
430
- coord_map["initial_level"] = None
431
- coord_map["initial_trend"] = None
451
+ observed_states = [
452
+ State(name=name, observed=True, shared=False) for name in self.endog_names
453
+ ]
454
+
455
+ return *hidden_states, *observed_states
456
+
457
+ def set_shocks(self) -> Shock | tuple[Shock, ...] | None:
458
+ k_endog = self.k_endog
459
+
460
+ if k_endog == 1:
461
+ shock_names = ["innovation"]
432
462
  else:
433
- coord_map["alpha"] = (OBS_STATE_DIM,)
434
- coord_map["beta"] = (OBS_STATE_DIM,)
435
- coord_map["gamma"] = (OBS_STATE_DIM,)
436
- coord_map["phi"] = (OBS_STATE_DIM,)
437
- coord_map["initial_seasonal"] = (OBS_STATE_DIM, ETS_SEASONAL_DIM)
438
- coord_map["seasonal_param"] = (OBS_STATE_DIM, ETS_SEASONAL_DIM)
439
-
440
- if not self.measurement_error:
441
- del coord_map["sigma_obs"]
442
- if not self.seasonal:
443
- del coord_map["seasonal_param"]
444
-
445
- return coord_map
446
-
447
- @property
448
- def coords(self) -> dict[str, Sequence]:
449
- coords = make_default_coords(self)
463
+ shock_names = [f"{name}_innovation" for name in self.endog_names]
464
+
465
+ return tuple(Shock(name=name) for name in shock_names)
466
+
467
+ def set_coords(self) -> Coord | tuple[Coord, ...] | None:
468
+ coords = list(self.default_coords())
469
+
470
+ # Seasonal coords
450
471
  if self.seasonal:
451
- coords.update({ETS_SEASONAL_DIM: list(range(1, self.seasonal_periods + 1))})
472
+ coords.append(
473
+ Coord(
474
+ dimension=ETS_SEASONAL_DIM,
475
+ labels=tuple(range(1, self.seasonal_periods + 1)),
476
+ )
477
+ )
452
478
 
453
- return coords
479
+ return tuple(coords)
454
480
 
455
481
  def _stationary_initialization(self, T_stationary):
456
482
  # Solve for matrix quadratic for P0