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.
- pymc_extras/inference/laplace_approx/laplace.py +2 -2
- pymc_extras/inference/pathfinder/pathfinder.py +1 -1
- pymc_extras/prior.py +3 -3
- pymc_extras/statespace/core/properties.py +276 -0
- pymc_extras/statespace/core/statespace.py +180 -44
- pymc_extras/statespace/filters/distributions.py +12 -29
- pymc_extras/statespace/filters/kalman_filter.py +1 -1
- pymc_extras/statespace/models/DFM.py +179 -168
- pymc_extras/statespace/models/ETS.py +177 -151
- pymc_extras/statespace/models/SARIMAX.py +149 -152
- pymc_extras/statespace/models/VARMAX.py +134 -145
- pymc_extras/statespace/models/__init__.py +8 -1
- pymc_extras/statespace/models/structural/__init__.py +30 -8
- pymc_extras/statespace/models/structural/components/autoregressive.py +87 -45
- pymc_extras/statespace/models/structural/components/cycle.py +119 -80
- pymc_extras/statespace/models/structural/components/level_trend.py +95 -42
- pymc_extras/statespace/models/structural/components/measurement_error.py +27 -17
- pymc_extras/statespace/models/structural/components/regression.py +105 -68
- pymc_extras/statespace/models/structural/components/seasonality.py +138 -100
- pymc_extras/statespace/models/structural/core.py +397 -286
- pymc_extras/statespace/models/utilities.py +5 -20
- {pymc_extras-0.7.0.dist-info → pymc_extras-0.8.0.dist-info}/METADATA +3 -3
- {pymc_extras-0.7.0.dist-info → pymc_extras-0.8.0.dist-info}/RECORD +25 -24
- {pymc_extras-0.7.0.dist-info → pymc_extras-0.8.0.dist-info}/WHEEL +0 -0
- {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.
|
|
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
|
|
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
|
|
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 |
|
|
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 =
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
318
|
-
names.remove("sigma_state")
|
|
319
|
-
else:
|
|
320
|
-
names.remove("state_cov")
|
|
303
|
+
parameters = []
|
|
321
304
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
def state_names(self):
|
|
387
|
-
states = ["innovation", "level"]
|
|
360
|
+
# Beta - only if trend is enabled
|
|
388
361
|
if self.trend:
|
|
389
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
|
395
|
-
|
|
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
|
|
432
|
+
return tuple(parameters)
|
|
398
433
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return self.endog_names
|
|
434
|
+
def set_states(self) -> State | tuple[State, ...] | None:
|
|
435
|
+
k_endog = self.k_endog
|
|
402
436
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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.
|
|
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
|