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,11 +1,17 @@
1
1
  from collections.abc import Sequence
2
- from typing import Any
3
2
 
4
3
  import pytensor
5
4
  import pytensor.tensor as pt
6
5
 
6
+ from pymc_extras.statespace.core.properties import (
7
+ Coord,
8
+ Data,
9
+ Parameter,
10
+ Shock,
11
+ State,
12
+ )
7
13
  from pymc_extras.statespace.core.statespace import PyMCStateSpace
8
- from pymc_extras.statespace.models.utilities import make_default_coords, validate_names
14
+ from pymc_extras.statespace.models.utilities import validate_names
9
15
  from pymc_extras.statespace.utils.constants import (
10
16
  ALL_STATE_AUX_DIM,
11
17
  ALL_STATE_DIM,
@@ -247,10 +253,10 @@ class BayesianDynamicFactor(PyMCStateSpace):
247
253
  and are modeled as a white noise process, i.e., :math:`f_t = \varepsilon_{f,t}`.
248
254
  Therefore, the state vector will include one state per factor and "factor_ar" will not exist.
249
255
 
250
- endog_names : list of str, optional
256
+ endog_names : Sequence of str, optional
251
257
  Names of the observed time series.
252
258
 
253
- exog_names : Sequence[str], optional
259
+ exog_names : Sequence of str, optional
254
260
  Names of the exogenous variables.
255
261
 
256
262
  shared_exog_states: bool, optional
@@ -281,7 +287,7 @@ class BayesianDynamicFactor(PyMCStateSpace):
281
287
 
282
288
  validate_names(endog_names, var_name="endog_names", optional=False)
283
289
  k_endog = len(endog_names)
284
- self.endog_names = endog_names
290
+ self.endog_names = tuple(endog_names)
285
291
  self.k_endog = k_endog
286
292
  self.k_factors = k_factors
287
293
  self.factor_order = factor_order
@@ -322,9 +328,7 @@ class BayesianDynamicFactor(PyMCStateSpace):
322
328
  # Typically, the latent factors introduce k_factors shocks.
323
329
  # If error_order > 0 and errors are modeled jointly or separately, add appropriate count.
324
330
  k_posdef = k_factors + (k_endog if error_order > 0 else 0) + self.k_exog_states
325
- # k_posdef = (k_factors + (k_endog if error_order > 0 else 0) + self.k_exog_states if self.exog_innovations else 0)
326
331
 
327
- # Initialize the PyMCStateSpace base class.
328
332
  super().__init__(
329
333
  k_endog=k_endog,
330
334
  k_states=k_states,
@@ -333,99 +337,134 @@ class BayesianDynamicFactor(PyMCStateSpace):
333
337
  measurement_error=measurement_error,
334
338
  )
335
339
 
336
- @property
337
- def param_names(self):
338
- names = [
339
- "x0",
340
- "P0",
341
- "factor_loadings",
342
- "factor_ar",
343
- "error_ar",
344
- "error_sigma",
345
- "error_cov",
346
- "sigma_obs",
347
- "beta",
348
- "beta_sigma",
349
- ]
340
+ def set_parameters(self) -> Parameter | tuple[Parameter, ...] | None:
341
+ parameters = []
350
342
 
351
- # Handle cases where parameters should be excluded based on model settings
352
- if self.factor_order == 0:
353
- names.remove("factor_ar")
354
- if self.error_order == 0:
355
- names.remove("error_ar")
356
- if self.error_cov_type in ["scalar", "diagonal"]:
357
- names.remove("error_cov")
358
- if self.error_cov_type == "unstructured":
359
- names.remove("error_sigma")
360
- if not self.measurement_error:
361
- names.remove("sigma_obs")
362
- if not self.exog_flag:
363
- names.remove("beta")
364
- names.remove("beta_sigma")
365
- if self.exog_flag and not self.exog_innovations:
366
- names.remove("beta_sigma")
367
-
368
- return names
369
-
370
- @property
371
- def param_info(self) -> dict[str, dict[str, Any]]:
372
- info = {
373
- "x0": {
374
- "shape": (self.k_states,),
375
- "constraints": None,
376
- },
377
- "P0": {
378
- "shape": (self.k_states, self.k_states),
379
- "constraints": "Positive Semi-definite",
380
- },
381
- "factor_loadings": {
382
- "shape": (self.k_endog, self.k_factors),
383
- "constraints": None,
384
- },
385
- "factor_ar": {
386
- "shape": (self.k_factors, self.factor_order * self.k_factors),
387
- "constraints": None,
388
- },
389
- "error_ar": {
390
- "shape": (
391
- self.k_endog,
392
- self.error_order * self.k_endog if self.error_var else self.error_order,
393
- ),
394
- "constraints": None,
395
- },
396
- "error_sigma": {
397
- "shape": (self.k_endog,) if self.error_cov_type == "diagonal" else (),
398
- "constraints": "Positive",
399
- },
400
- "error_cov": {
401
- "shape": (self.k_endog, self.k_endog),
402
- "constraints": "Positive Semi-definite",
403
- },
404
- "sigma_obs": {
405
- "shape": (self.k_endog,),
406
- "constraints": "Positive",
407
- },
408
- "beta": {
409
- "shape": (self.k_exog_states,),
410
- "constraints": None,
411
- },
412
- "beta_sigma": {
413
- "shape": (self.k_exog_states,),
414
- "constraints": "Positive",
415
- },
416
- }
417
-
418
- for name in self.param_names:
419
- info[name]["dims"] = self.param_dims[name]
420
-
421
- return {name: info[name] for name in self.param_names}
422
-
423
- @property
424
- def state_names(self) -> list[str]:
425
- """
426
- Returns the names of the hidden states: first factor states (with lags),
427
- idiosyncratic error states (with lags), then exogenous states.
428
- """
343
+ k_endog = self.k_endog
344
+ k_states = self.k_states
345
+
346
+ # x0 - initial state
347
+ parameters.append(
348
+ Parameter(
349
+ name="x0",
350
+ shape=(k_states,),
351
+ dims=(ALL_STATE_DIM,),
352
+ constraints=None,
353
+ )
354
+ )
355
+
356
+ # P0 - initial covariance
357
+ parameters.append(
358
+ Parameter(
359
+ name="P0",
360
+ shape=(k_states, k_states),
361
+ dims=(ALL_STATE_DIM, ALL_STATE_AUX_DIM),
362
+ constraints="Positive Semi-definite",
363
+ )
364
+ )
365
+
366
+ # factor_loadings
367
+ parameters.append(
368
+ Parameter(
369
+ name="factor_loadings",
370
+ shape=(k_endog, self.k_factors),
371
+ dims=(OBS_STATE_DIM, FACTOR_DIM),
372
+ constraints=None,
373
+ )
374
+ )
375
+
376
+ # factor_ar - only if factor_order > 0
377
+ if self.factor_order > 0:
378
+ parameters.append(
379
+ Parameter(
380
+ name="factor_ar",
381
+ shape=(self.k_factors, self.factor_order * self.k_factors),
382
+ dims=(FACTOR_DIM, AR_PARAM_DIM),
383
+ constraints=None,
384
+ )
385
+ )
386
+
387
+ # error_ar - only if error_order > 0
388
+ if self.error_order > 0:
389
+ error_ar_shape = (
390
+ (k_endog, self.error_order * k_endog)
391
+ if self.error_var
392
+ else (k_endog, self.error_order)
393
+ )
394
+ parameters.append(
395
+ Parameter(
396
+ name="error_ar",
397
+ shape=error_ar_shape,
398
+ dims=(OBS_STATE_DIM, ERROR_AR_PARAM_DIM),
399
+ constraints=None,
400
+ )
401
+ )
402
+
403
+ # error_sigma or error_cov depending on error_cov_type
404
+ if self.error_cov_type == "scalar":
405
+ parameters.append(
406
+ Parameter(
407
+ name="error_sigma",
408
+ shape=(),
409
+ dims=(),
410
+ constraints="Positive",
411
+ )
412
+ )
413
+ elif self.error_cov_type == "diagonal":
414
+ parameters.append(
415
+ Parameter(
416
+ name="error_sigma",
417
+ shape=(k_endog,),
418
+ dims=(OBS_STATE_DIM,),
419
+ constraints="Positive",
420
+ )
421
+ )
422
+ elif self.error_cov_type == "unstructured":
423
+ parameters.append(
424
+ Parameter(
425
+ name="error_cov",
426
+ shape=(k_endog, k_endog),
427
+ dims=(OBS_STATE_DIM, OBS_STATE_AUX_DIM),
428
+ constraints="Positive Semi-definite",
429
+ )
430
+ )
431
+
432
+ # sigma_obs - only if measurement_error
433
+ if self.measurement_error:
434
+ parameters.append(
435
+ Parameter(
436
+ name="sigma_obs",
437
+ shape=(k_endog,),
438
+ dims=(OBS_STATE_DIM,),
439
+ constraints="Positive",
440
+ )
441
+ )
442
+
443
+ # beta - only if exog_flag
444
+ if self.exog_flag:
445
+ parameters.append(
446
+ Parameter(
447
+ name="beta",
448
+ shape=(self.k_exog_states,),
449
+ dims=(EXOG_STATE_DIM,),
450
+ constraints=None,
451
+ )
452
+ )
453
+
454
+ # beta_sigma - only if exog_innovations
455
+ if self.exog_innovations:
456
+ parameters.append(
457
+ Parameter(
458
+ name="beta_sigma",
459
+ shape=(self.k_exog_states,),
460
+ dims=(EXOG_STATE_DIM,),
461
+ constraints="Positive",
462
+ )
463
+ )
464
+
465
+ return tuple(parameters)
466
+
467
+ def set_states(self) -> State | tuple[State, ...] | None:
429
468
  names = [
430
469
  f"L{lag}.factor_{i}"
431
470
  for i in range(self.k_factors)
@@ -446,37 +485,15 @@ class BayesianDynamicFactor(PyMCStateSpace):
446
485
  for exog_name in self.exog_names
447
486
  for endog_name in self.endog_names
448
487
  )
449
- return names
450
488
 
451
- @property
452
- def observed_states(self) -> list[str]:
453
- """
454
- Returns the names of the observed states (i.e., the endogenous variables).
455
- """
456
- return self.endog_names
457
-
458
- @property
459
- def coords(self) -> dict[str, Sequence]:
460
- coords = make_default_coords(self)
461
-
462
- coords[FACTOR_DIM] = [f"factor_{i+1}" for i in range(self.k_factors)]
463
-
464
- if self.factor_order > 0:
465
- coords[AR_PARAM_DIM] = list(range(1, (self.factor_order * self.k_factors) + 1))
466
-
467
- if self.error_order > 0:
468
- if self.error_var:
469
- coords[ERROR_AR_PARAM_DIM] = list(range(1, (self.error_order * self.k_endog) + 1))
470
- else:
471
- coords[ERROR_AR_PARAM_DIM] = list(range(1, self.error_order + 1))
472
-
473
- if self.exog_flag:
474
- coords[EXOG_STATE_DIM] = list(range(1, self.k_exog_states + 1))
489
+ hidden_states = [State(name=name, observed=False, shared=False) for name in names]
490
+ observed_states = [
491
+ State(name=name, observed=True, shared=False) for name in self.endog_names
492
+ ]
475
493
 
476
- return coords
494
+ return *hidden_states, *observed_states
477
495
 
478
- @property
479
- def shock_names(self) -> list[str]:
496
+ def set_shocks(self) -> Shock | tuple[Shock, ...] | None:
480
497
  shock_names = [f"factor_shock_{i}" for i in range(self.k_factors)]
481
498
 
482
499
  if self.error_order > 0:
@@ -492,56 +509,50 @@ class BayesianDynamicFactor(PyMCStateSpace):
492
509
  for j in range(self.k_endog)
493
510
  )
494
511
 
495
- return shock_names
512
+ return tuple(Shock(name=name) for name in shock_names)
496
513
 
497
- @property
498
- def param_dims(self):
499
- coord_map = {
500
- "x0": (ALL_STATE_DIM,),
501
- "P0": (ALL_STATE_DIM, ALL_STATE_AUX_DIM),
502
- "factor_loadings": (OBS_STATE_DIM, FACTOR_DIM),
503
- }
504
- if self.factor_order > 0:
505
- coord_map["factor_ar"] = (FACTOR_DIM, AR_PARAM_DIM)
514
+ def set_data_info(self) -> Data | tuple[Data, ...] | None:
515
+ data = []
506
516
 
507
- if self.error_order > 0:
508
- coord_map["error_ar"] = (OBS_STATE_DIM, ERROR_AR_PARAM_DIM)
509
-
510
- if self.error_cov_type in ["scalar"]:
511
- coord_map["error_sigma"] = ()
517
+ if self.exog_flag:
518
+ data.append(
519
+ Data(
520
+ name="exog_data",
521
+ shape=(None, self.k_exog),
522
+ dims=(TIME_DIM, EXOG_STATE_DIM),
523
+ is_exogenous=True,
524
+ )
525
+ )
512
526
 
513
- elif self.error_cov_type in ["diagonal"]:
514
- coord_map["error_sigma"] = (OBS_STATE_DIM,)
527
+ return tuple(data)
515
528
 
516
- if self.error_cov_type == "unstructured":
517
- coord_map["error_cov"] = (OBS_STATE_DIM, OBS_STATE_AUX_DIM)
529
+ def set_coords(self) -> Coord | tuple[Coord, ...] | None:
530
+ k_endog = self.k_endog
531
+ coords = list(self.default_coords())
518
532
 
519
- if self.measurement_error:
520
- coord_map["sigma_obs"] = (OBS_STATE_DIM,)
533
+ # Factor coords
534
+ factor_labels = tuple(f"factor_{i+1}" for i in range(self.k_factors))
535
+ coords.append(Coord(dimension=FACTOR_DIM, labels=factor_labels))
521
536
 
522
- if self.exog_flag:
523
- coord_map["beta"] = (EXOG_STATE_DIM,)
524
- if self.exog_innovations:
525
- coord_map["beta_sigma"] = (EXOG_STATE_DIM,)
537
+ # AR param coords for factors
538
+ if self.factor_order > 0:
539
+ ar_labels = tuple(range(1, (self.factor_order * self.k_factors) + 1))
540
+ coords.append(Coord(dimension=AR_PARAM_DIM, labels=ar_labels))
526
541
 
527
- return coord_map
542
+ # AR param coords for errors
543
+ if self.error_order > 0:
544
+ if self.error_var:
545
+ error_ar_labels = tuple(range(1, (self.error_order * k_endog) + 1))
546
+ else:
547
+ error_ar_labels = tuple(range(1, self.error_order + 1))
548
+ coords.append(Coord(dimension=ERROR_AR_PARAM_DIM, labels=error_ar_labels))
528
549
 
529
- @property
530
- def data_info(self):
550
+ # Exogenous coords
531
551
  if self.exog_flag:
532
- return {
533
- "exog_data": {
534
- "shape": (None, self.k_exog),
535
- "dims": (TIME_DIM, EXOG_STATE_DIM),
536
- },
537
- }
538
- return {}
539
-
540
- @property
541
- def data_names(self):
542
- if self.exog_flag:
543
- return ["exog_data"]
544
- return []
552
+ exog_labels = tuple(range(1, self.k_exog_states + 1))
553
+ coords.append(Coord(dimension=EXOG_STATE_DIM, labels=exog_labels))
554
+
555
+ return tuple(coords)
545
556
 
546
557
  def make_symbolic_graph(self):
547
558
  if not self.exog_flag: