@@ -31,6 +31,11 @@ class RegressionComponent(Component):
31
31
Whether to include stochastic innovations in the regression coefficients,
32
32
allowing them to vary over time. If True, coefficients follow a random walk.
33
33
34
+ share_states: bool, default False
35
+ Whether latent states are shared across the observed states. If True, there will be only one set of latent
36
+ states, which are observed by all observed states. If False, each observed state has its own set of
37
+ latent states.
38
+
34
39
Notes
35
40
-----
36
41
This component implements regression with exogenous variables in a structural time series
@@ -107,7 +112,10 @@ def __init__(
107
112
state_names : list [str ] | None = None ,
108
113
observed_state_names : list [str ] | None = None ,
109
114
innovations = False ,
115
+ share_states : bool = False ,
110
116
):
117
+ self .share_states = share_states
118
+
111
119
if observed_state_names is None :
112
120
observed_state_names = ["data" ]
113
121
@@ -121,8 +129,8 @@ def __init__(
121
129
super ().__init__ (
122
130
name = name ,
123
131
k_endog = k_endog ,
124
- k_states = k_states * k_endog ,
125
- k_posdef = k_posdef * k_endog ,
132
+ k_states = k_states * k_endog if not share_states else k_states ,
133
+ k_posdef = k_posdef * k_endog if not share_states else k_posdef ,
126
134
state_names = self .state_names ,
127
135
observed_state_names = observed_state_names ,
128
136
measurement_error = False ,
@@ -153,54 +161,74 @@ def _handle_input_data(self, k_exog: int, state_names: list[str] | None, name) -
153
161
154
162
def make_symbolic_graph (self ) -> None :
155
163
k_endog = self .k_endog
156
- k_states = self .k_states // k_endog
164
+ k_endog_effective = 1 if self .share_states else k_endog
165
+
166
+ k_states = self .k_states // k_endog_effective
157
167
158
168
betas = self .make_and_register_variable (
159
- f"beta_{ self .name } " , shape = (k_endog , k_states ) if k_endog > 1 else (k_states ,)
169
+ f"beta_{ self .name } " , shape = (k_endog , k_states ) if k_endog_effective > 1 else (k_states ,)
160
170
)
161
171
regression_data = self .make_and_register_data (f"data_{ self .name } " , shape = (None , k_states ))
162
172
163
173
self .ssm ["initial_state" , :] = betas .ravel ()
164
174
self .ssm ["transition" , :, :] = pt .eye (self .k_states )
165
175
self .ssm ["selection" , :, :] = pt .eye (self .k_states )
166
176
167
- Z = pt .linalg .block_diag (* [pt .expand_dims (regression_data , 1 ) for _ in range (k_endog )])
168
- self .ssm ["design" ] = pt .specify_shape (
169
- Z , (None , k_endog , regression_data .type .shape [1 ] * k_endog )
170
- )
177
+ if self .share_states :
178
+ self .ssm ["design" ] = pt .specify_shape (
179
+ pt .join (1 , * [pt .expand_dims (regression_data , 1 ) for _ in range (k_endog )]),
180
+ (None , k_endog , self .k_states ),
181
+ )
182
+ else :
183
+ Z = pt .linalg .block_diag (* [pt .expand_dims (regression_data , 1 ) for _ in range (k_endog )])
184
+ self .ssm ["design" ] = pt .specify_shape (
185
+ Z , (None , k_endog , regression_data .type .shape [1 ] * k_endog )
186
+ )
171
187
172
188
if self .innovations :
173
189
sigma_beta = self .make_and_register_variable (
174
- f"sigma_beta_{ self .name } " , (k_states ,) if k_endog == 1 else (k_endog , k_states )
190
+ f"sigma_beta_{ self .name } " ,
191
+ (k_states ,) if k_endog_effective == 1 else (k_endog , k_states ),
175
192
)
176
193
row_idx , col_idx = np .diag_indices (self .k_states )
177
194
self .ssm ["state_cov" , row_idx , col_idx ] = sigma_beta .ravel () ** 2
178
195
179
196
def populate_component_properties (self ) -> None :
180
197
k_endog = self .k_endog
181
- k_states = self .k_states // k_endog
198
+ k_endog_effective = 1 if self .share_states else k_endog
199
+
200
+ k_states = self .k_states // k_endog_effective
182
201
183
- self .shock_names = self .state_names
202
+ if self .share_states :
203
+ self .shock_names = [f"{ state_name } _shared" for state_name in self .state_names ]
204
+ else :
205
+ self .shock_names = self .state_names
184
206
185
207
self .param_names = [f"beta_{ self .name } " ]
186
208
self .data_names = [f"data_{ self .name } " ]
187
209
self .param_dims = {
188
210
f"beta_{ self .name } " : (f"endog_{ self .name } " , f"state_{ self .name } " )
189
- if k_endog > 1
211
+ if k_endog_effective > 1
190
212
else (f"state_{ self .name } " ,)
191
213
}
192
214
193
215
base_names = self .state_names
194
- self .state_names = [
195
- f"{ name } [{ obs_name } ]" for obs_name in self .observed_state_names for name in base_names
196
- ]
216
+
217
+ if self .share_states :
218
+ self .state_names = [f"{ name } [{ self .name } _shared]" for name in base_names ]
219
+ else :
220
+ self .state_names = [
221
+ f"{ name } [{ obs_name } ]"
222
+ for obs_name in self .observed_state_names
223
+ for name in base_names
224
+ ]
197
225
198
226
self .param_info = {
199
227
f"beta_{ self .name } " : {
200
- "shape" : (k_endog , k_states ) if k_endog > 1 else (k_states ,),
228
+ "shape" : (k_endog_effective , k_states ) if k_endog_effective > 1 else (k_states ,),
201
229
"constraints" : None ,
202
230
"dims" : (f"endog_{ self .name } " , f"state_{ self .name } " )
203
- if k_endog > 1
231
+ if k_endog_effective > 1
204
232
else (f"state_{ self .name } " ,),
205
233
},
206
234
}
@@ -223,6 +251,6 @@ def populate_component_properties(self) -> None:
223
251
"shape" : (k_states ,),
224
252
"constraints" : "Positive" ,
225
253
"dims" : (f"state_{ self .name } " ,)
226
- if k_endog == 1
254
+ if k_endog_effective == 1
227
255
else (f"endog_{ self .name } " , f"state_{ self .name } " ),
228
256
}
0 commit comments