Skip to content

Commit a6327b7

Browse files
Update autoregressive component and tests
1 parent 85b78fe commit a6327b7

File tree

4 files changed

+123
-28
lines changed

4 files changed

+123
-28
lines changed

pymc_extras/statespace/models/structural/components/autoregressive.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class AutoregressiveComponent(Component):
6565
def __init__(
6666
self,
6767
order: int = 1,
68-
name: str = "AutoRegressive",
68+
name: str = "auto_regressive",
6969
observed_state_names: list[str] | None = None,
7070
):
7171
if observed_state_names is None:
@@ -92,27 +92,30 @@ def __init__(
9292
)
9393

9494
def populate_component_properties(self):
95+
k_states = self.k_states // self.k_endog
96+
9597
self.state_names = [
96-
f"L{i + 1}.{state_name}"
97-
for i in range(self.k_states)
98+
f"L{i + 1}[{state_name}]"
9899
for state_name in self.observed_state_names
100+
for i in range(k_states)
99101
]
100-
self.shock_names = [f"{name}_{self.name}_innovation" for name in self.observed_state_names]
101-
self.param_names = ["ar_params", "sigma_ar"]
102-
self.param_dims = {"ar_params": (AR_PARAM_DIM,)}
103-
self.coords = {AR_PARAM_DIM: self.ar_lags.tolist()}
102+
103+
self.shock_names = self.observed_state_names.copy()
104+
self.param_names = [f"{self.name}_params", f"{self.name}_sigma"]
105+
self.param_dims = {f"{self.name}_params": (f"{self.name}_lag",)}
106+
self.coords = {f"{self.name}_lag": self.ar_lags.tolist()}
104107

105108
if self.k_endog > 1:
106-
self.param_dims["ar_params"] = (
109+
self.param_dims[f"{self.name}_params"] = (
107110
f"{self.name}_endog",
108111
AR_PARAM_DIM,
109112
)
110-
self.param_dims["sigma_ar"] = (f"{self.name}_endog",)
113+
self.param_dims[f"{self.name}_sigma"] = (f"{self.name}_endog",)
111114

112115
self.coords[f"{self.name}_endog"] = self.observed_state_names
113116

114117
self.param_info = {
115-
"ar_params": {
118+
f"{self.name}_params": {
116119
"shape": (self.k_states,) if self.k_endog == 1 else (self.k_endog, self.k_states),
117120
"constraints": None,
118121
"dims": (AR_PARAM_DIM,)
@@ -122,7 +125,7 @@ def populate_component_properties(self):
122125
AR_PARAM_DIM,
123126
),
124127
},
125-
"sigma_ar": {
128+
f"{self.name}_sigma": {
126129
"shape": () if self.k_endog == 1 else (self.k_endog,),
127130
"constraints": "Positive",
128131
"dims": None if self.k_endog == 1 else (f"{self.name}_endog",),
@@ -136,10 +139,10 @@ def make_symbolic_graph(self) -> None:
136139

137140
k_nonzero = int(sum(self.order))
138141
ar_params = self.make_and_register_variable(
139-
"ar_params", shape=(k_nonzero,) if k_endog == 1 else (k_endog, k_nonzero)
142+
f"{self.name}_params", shape=(k_nonzero,) if k_endog == 1 else (k_endog, k_nonzero)
140143
)
141144
sigma_ar = self.make_and_register_variable(
142-
"sigma_ar", shape=() if k_endog == 1 else (k_endog,)
145+
f"{self.name}_sigma", shape=() if k_endog == 1 else (k_endog,)
143146
)
144147

145148
if k_endog == 1:
Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import numpy as np
2+
import pytensor
23
import pytest
34

45
from numpy.testing import assert_allclose
56
from pytensor import config
7+
from pytensor.graph.basic import explicit_graph_inputs
68

79
from pymc_extras.statespace.models import structural as st
810
from tests.statespace.models.structural.conftest import _assert_basic_coords_correct
@@ -11,37 +13,126 @@
1113

1214
@pytest.mark.parametrize("order", [1, 2, [1, 0, 1]], ids=["AR1", "AR2", "AR(1,0,1)"])
1315
def test_autoregressive_model(order, rng):
14-
ar = st.AutoregressiveComponent(order=order)
16+
k = sum(order) if isinstance(order, list) else order
17+
ar = st.AutoregressiveComponent(order=order).build(verbose=False)
1518
params = {
16-
"ar_params": np.full((sum(ar.order),), 0.5, dtype=config.floatX),
17-
"sigma_ar": 0.0,
19+
"auto_regressive_params": np.full((k,), 0.5, dtype=config.floatX),
20+
"auto_regressive_sigma": 0.1,
21+
"initial_state_cov": np.eye(k),
1822
}
1923

20-
x, y = simulate_from_numpy_model(ar, rng, params, steps=100)
21-
2224
# Check coords
23-
ar.build(verbose=False)
2425
_assert_basic_coords_correct(ar)
26+
2527
lags = np.arange(len(order) if isinstance(order, list) else order, dtype="int") + 1
2628
if isinstance(order, list):
2729
lags = lags[np.flatnonzero(order)]
28-
assert_allclose(ar.coords["ar_lag"], lags)
30+
assert_allclose(ar.coords["auto_regressive_lag"], lags)
2931

3032

31-
def test_autoregressive_multiple_observed(rng):
33+
def test_autoregressive_multiple_observed_build(rng):
3234
ar = st.AutoregressiveComponent(order=3, observed_state_names=["data_1", "data_2"])
3335
mod = ar.build(verbose=False)
3436

37+
assert mod.k_endog == 2
38+
assert mod.k_states == 6
39+
assert mod.k_posdef == 2
40+
41+
assert mod.state_names == [
42+
"L1[data_1]",
43+
"L2[data_1]",
44+
"L3[data_1]",
45+
"L1[data_2]",
46+
"L2[data_2]",
47+
"L3[data_2]",
48+
]
49+
50+
assert mod.shock_names == ["data_1", "data_2"]
51+
3552
params = {
36-
"ar_params": np.full(
53+
"auto_regressive_params": np.full(
3754
(
3855
2,
3956
sum(ar.order),
4057
),
4158
0.5,
4259
dtype=config.floatX,
4360
),
44-
"sigma_ar": np.ones((2,)) * 1e-3,
61+
"auto_regressive_sigma": np.array([0.05, 0.12]),
4562
}
63+
_, _, _, _, T, Z, R, _, Q = mod._unpack_statespace_with_placeholders()
64+
input_vars = explicit_graph_inputs([T, Z, R, Q])
65+
fn = pytensor.function(
66+
inputs=list(input_vars),
67+
outputs=[T, Z, R, Q],
68+
mode="FAST_COMPILE",
69+
)
70+
71+
T, Z, R, Q = fn(**params)
72+
73+
np.testing.assert_allclose(
74+
T,
75+
np.array(
76+
[
77+
[0.5, 0.5, 0.5, 0.0, 0.0, 0.0],
78+
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
79+
[0.0, 1.0, 0.0, 0.0, 0.0, 0.0],
80+
[0.0, 0.0, 0.0, 0.5, 0.5, 0.5],
81+
[0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
82+
[0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
83+
]
84+
),
85+
)
86+
87+
np.testing.assert_allclose(
88+
Z, np.array([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0]])
89+
)
90+
91+
np.testing.assert_allclose(
92+
R, np.array([[1.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 1.0], [0.0, 0.0], [0.0, 0.0]])
93+
)
94+
95+
np.testing.assert_allclose(Q, np.diag([0.05**2, 0.12**2]))
96+
97+
98+
def test_autoregressive_multiple_observed_data(rng):
99+
ar = st.AutoregressiveComponent(order=1, observed_state_names=["data_1", "data_2", "data_3"])
100+
mod = ar.build(verbose=False)
101+
102+
params = {
103+
"auto_regressive_params": np.array([0.9, 0.8, 0.5]).reshape((3, 1)),
104+
"auto_regressive_sigma": np.array([0.05, 0.12, 0.22]),
105+
"initial_state_cov": np.eye(3),
106+
}
107+
108+
# Recover the AR(1) coefficients from the simulated data via OLS
109+
x, y = simulate_from_numpy_model(mod, rng, params, steps=2000)
110+
for i in range(3):
111+
ols_coefs = np.polyfit(y[:-1, i], y[1:, i], 1)
112+
np.testing.assert_allclose(ols_coefs[0], params["auto_regressive_params"][i, 0], atol=1e-1)
113+
114+
115+
def test_add_autoregressive_different_observed():
116+
mod_1 = st.AutoregressiveComponent(order=1, name="ar1", observed_state_names=["data_1"])
117+
mod_2 = st.AutoregressiveComponent(name="ar6", order=6, observed_state_names=["data_2"])
118+
119+
mod = (mod_1 + mod_2).build(verbose=False)
120+
121+
print(mod.coords)
122+
123+
assert mod.k_endog == 2
124+
assert mod.k_states == 7
125+
assert mod.k_posdef == 2
126+
assert mod.state_names == [
127+
"L1[data_1]",
128+
"L1[data_2]",
129+
"L2[data_2]",
130+
"L3[data_2]",
131+
"L4[data_2]",
132+
"L5[data_2]",
133+
"L6[data_2]",
134+
]
46135

47-
x, y = simulate_from_numpy_model(ar, rng, params, steps=100)
136+
assert mod.shock_names == ["data_1", "data_2"]
137+
assert mod.coords["ar1_lag"] == [1]
138+
assert mod.coords["ar6_lag"] == [1, 2, 3, 4, 5, 6]

tests/statespace/models/structural/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ def _assert_basic_coords_correct(mod):
2424
assert mod.coords[SHOCK_DIM] == mod.shock_names
2525
assert mod.coords[SHOCK_AUX_DIM] == mod.shock_names
2626
expected_obs = mod.observed_state_names if hasattr(mod, "observed_state_names") else ["data"]
27+
2728
assert mod.coords[OBS_STATE_DIM] == expected_obs
2829
assert mod.coords[OBS_STATE_AUX_DIM] == expected_obs

tests/statespace/models/structural/test_against_statsmodels.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,20 +404,20 @@ def create_structural_model_and_equivalent_statsmodel(
404404
components.append(comp)
405405

406406
if autoregressive is not None:
407-
ar_names = [f"L{i+1}.data" for i in range(autoregressive)]
407+
ar_names = [f"L{i+1}" for i in range(autoregressive)]
408408
ar_params = rng.normal(size=(autoregressive,)).astype(floatX)
409409
if autoregressive == 1:
410410
ar_params = ar_params.item()
411411
sigma2 = np.abs(rng.normal()).astype(floatX)
412412

413413
params["ar_params"] = ar_params
414-
params["sigma_ar"] = np.sqrt(sigma2)
414+
params["ar_sigma"] = np.sqrt(sigma2)
415415
expected_param_dims["ar_params"] += (AR_PARAM_DIM,)
416416
expected_coords[AR_PARAM_DIM] += tuple(list(range(1, autoregressive + 1)))
417417
expected_coords[ALL_STATE_DIM] += ar_names
418418
expected_coords[ALL_STATE_AUX_DIM] += ar_names
419-
expected_coords[SHOCK_DIM] += ["data_ar_innovation"]
420-
expected_coords[SHOCK_AUX_DIM] += ["data_ar_innovation"]
419+
expected_coords[SHOCK_DIM] += ["data"]
420+
expected_coords[SHOCK_AUX_DIM] += ["data"]
421421

422422
sm_params["sigma2.ar"] = sigma2
423423
for i, rho in enumerate(ar_params):

0 commit comments

Comments
 (0)