@@ -13,13 +13,11 @@ class LevelTrendComponent(Component):
13
13
Parameters
14
14
----------
15
15
order : int
16
-
17
16
Number of time derivatives of the trend to include in the model. For example, when order=3, the trend will
18
17
be of the form ``y = a + b * t + c * t ** 2``, where the coefficients ``a, b, c`` come from the initial
19
18
state values.
20
19
21
20
innovations_order : int or sequence of int, optional
22
-
23
21
The number of stochastic innovations to include in the model. By default, ``innovations_order = order``
24
22
25
23
name : str, default "level_trend"
@@ -28,6 +26,11 @@ class LevelTrendComponent(Component):
28
26
observed_state_names : list[str] | None, default None
29
27
List of strings for observed state labels. If None, defaults to ["data"].
30
28
29
+ share_states: bool, default False
30
+ Whether latent states are shared across the observed states. If True, there will be only one set of latent
31
+ states, which are observed by all observed states. If False, each observed state has its own set of
32
+ latent states. This argument has no effect if `k_endog` is 1.
33
+
31
34
Notes
32
35
-----
33
36
This class implements the level and trend components of the general structural time series model. In the most
@@ -120,7 +123,10 @@ def __init__(
120
123
innovations_order : int | list [int ] | None = None ,
121
124
name : str = "level_trend" ,
122
125
observed_state_names : list [str ] | None = None ,
126
+ share_states : bool = False ,
123
127
):
128
+ self .share_states = share_states
129
+
124
130
if innovations_order is None :
125
131
innovations_order = order
126
132
@@ -156,37 +162,50 @@ def __init__(
156
162
super ().__init__ (
157
163
name ,
158
164
k_endog = k_endog ,
159
- k_states = k_states * k_endog ,
160
- k_posdef = k_posdef * k_endog ,
165
+ k_states = k_states * k_endog if not share_states else k_states ,
166
+ k_posdef = k_posdef * k_endog if not share_states else k_posdef ,
161
167
observed_state_names = observed_state_names ,
162
168
measurement_error = False ,
163
169
combine_hidden_states = False ,
164
- obs_state_idxs = np .tile (np .array ([1.0 ] + [0.0 ] * (k_states - 1 )), k_endog ),
170
+ obs_state_idxs = np .tile (
171
+ np .array ([1.0 ] + [0.0 ] * (k_states - 1 )), k_endog if not share_states else 1
172
+ ),
165
173
)
166
174
167
175
def populate_component_properties (self ):
168
176
k_endog = self .k_endog
169
- k_states = self .k_states // k_endog
170
- k_posdef = self .k_posdef // k_endog
177
+ k_endog_effective = 1 if self .share_states else k_endog
178
+
179
+ k_states = self .k_states // k_endog_effective
180
+ k_posdef = self .k_posdef // k_endog_effective
171
181
172
182
name_slice = POSITION_DERIVATIVE_NAMES [:k_states ]
173
183
self .param_names = [f"initial_{ self .name } " ]
174
184
base_names = [name for name , mask in zip (name_slice , self ._order_mask ) if mask ]
175
- self .state_names = [
176
- f"{ name } [{ obs_name } ]" for obs_name in self .observed_state_names for name in base_names
177
- ]
185
+
186
+ if self .share_states :
187
+ self .state_names = [f"{ name } [{ self .name } _shared]" for name in base_names ]
188
+ else :
189
+ self .state_names = [
190
+ f"{ name } [{ obs_name } ]"
191
+ for obs_name in self .observed_state_names
192
+ for name in base_names
193
+ ]
194
+
178
195
self .param_dims = {f"initial_{ self .name } " : (f"state_{ self .name } " ,)}
179
196
self .coords = {f"state_{ self .name } " : base_names }
180
197
181
198
if k_endog > 1 :
199
+ self .coords [f"endog_{ self .name } " ] = self .observed_state_names
200
+
201
+ if k_endog_effective > 1 :
182
202
self .param_dims [f"state_{ self .name } " ] = (
183
203
f"endog_{ self .name } " ,
184
204
f"state_{ self .name } " ,
185
205
)
186
206
self .param_dims = {f"initial_{ self .name } " : (f"endog_{ self .name } " , f"state_{ self .name } " )}
187
- self .coords [f"endog_{ self .name } " ] = self .observed_state_names
188
207
189
- shape = (k_endog , k_states ) if k_endog > 1 else (k_states ,)
208
+ shape = (k_endog_effective , k_states ) if k_endog_effective > 1 else (k_states ,)
190
209
self .param_info = {f"initial_{ self .name } " : {"shape" : shape , "constraints" : None }}
191
210
192
211
if self .k_posdef > 0 :
@@ -196,20 +215,23 @@ def populate_component_properties(self):
196
215
name for name , mask in zip (name_slice , self .innovations_order ) if mask
197
216
]
198
217
199
- self .shock_names = [
200
- f"{ name } [{ obs_name } ]"
201
- for obs_name in self .observed_state_names
202
- for name in base_shock_names
203
- ]
218
+ if self .share_states :
219
+ self .shock_names = [f"{ name } [{ self .name } _shared]" for name in base_shock_names ]
220
+ else :
221
+ self .shock_names = [
222
+ f"{ name } [{ obs_name } ]"
223
+ for obs_name in self .observed_state_names
224
+ for name in base_shock_names
225
+ ]
204
226
205
227
self .param_dims [f"sigma_{ self .name } " ] = (
206
228
(f"shock_{ self .name } " ,)
207
- if k_endog == 1
229
+ if k_endog_effective == 1
208
230
else (f"endog_{ self .name } " , f"shock_{ self .name } " )
209
231
)
210
232
self .coords [f"shock_{ self .name } " ] = base_shock_names
211
233
self .param_info [f"sigma_{ self .name } " ] = {
212
- "shape" : (k_posdef ,) if k_endog == 1 else (k_endog , k_posdef ),
234
+ "shape" : (k_posdef ,) if k_endog_effective == 1 else (k_endog_effective , k_posdef ),
213
235
"constraints" : "Positive" ,
214
236
}
215
237
@@ -218,40 +240,49 @@ def populate_component_properties(self):
218
240
219
241
def make_symbolic_graph (self ) -> None :
220
242
k_endog = self .k_endog
221
- k_states = self .k_states // k_endog
222
- k_posdef = self .k_posdef // k_endog
243
+ k_endog_effective = 1 if self .share_states else k_endog
244
+
245
+ k_states = self .k_states // k_endog_effective
246
+ k_posdef = self .k_posdef // k_endog_effective
223
247
224
248
initial_trend = self .make_and_register_variable (
225
249
f"initial_{ self .name } " ,
226
- shape = (k_states ,) if k_endog == 1 else (k_endog , k_states ),
250
+ shape = (k_states ,) if k_endog_effective == 1 else (k_endog , k_states ),
227
251
)
228
252
self .ssm ["initial_state" , :] = initial_trend .ravel ()
229
253
230
254
triu_idx = pt .triu_indices (k_states )
231
255
T = pt .zeros ((k_states , k_states ))[triu_idx [0 ], triu_idx [1 ]].set (1 )
232
256
233
257
self .ssm ["transition" , :, :] = pt .specify_shape (
234
- pt .linalg .block_diag (* [T for _ in range (k_endog )]), (self .k_states , self .k_states )
258
+ pt .linalg .block_diag (* [T for _ in range (k_endog_effective )]),
259
+ (self .k_states , self .k_states ),
235
260
)
236
261
237
262
R = np .eye (k_states )
238
263
R = R [:, self .innovations_order ]
239
264
240
265
self .ssm ["selection" , :, :] = pt .specify_shape (
241
- pt .linalg .block_diag (* [R for _ in range (k_endog )]), (self .k_states , self .k_posdef )
266
+ pt .linalg .block_diag (* [R for _ in range (k_endog_effective )]),
267
+ (self .k_states , self .k_posdef ),
242
268
)
243
269
244
270
Z = np .array ([1.0 ] + [0.0 ] * (k_states - 1 )).reshape ((1 , - 1 ))
245
271
246
- self .ssm ["design" , :, :] = pt .specify_shape (
247
- pt .linalg .block_diag (* [Z for _ in range (k_endog )]), (self .k_endog , self .k_states )
248
- )
272
+ if self .share_states :
273
+ self .ssm ["design" , :, :] = pt .specify_shape (
274
+ pt .join (0 , * [Z for _ in range (k_endog )]), (self .k_endog , self .k_states )
275
+ )
276
+ else :
277
+ self .ssm ["design" , :, :] = pt .specify_shape (
278
+ pt .linalg .block_diag (* [Z for _ in range (k_endog )]), (self .k_endog , self .k_states )
279
+ )
249
280
250
281
if k_posdef > 0 :
251
282
sigma_trend = self .make_and_register_variable (
252
283
f"sigma_{ self .name } " ,
253
- shape = (k_posdef ,) if k_endog == 1 else (k_endog , k_posdef ),
284
+ shape = (k_posdef ,) if k_endog_effective == 1 else (k_endog , k_posdef ),
254
285
)
255
- diag_idx = np .diag_indices (k_posdef * k_endog )
286
+ diag_idx = np .diag_indices (k_posdef * k_endog_effective )
256
287
idx = np .s_ ["state_cov" , diag_idx [0 ], diag_idx [1 ]]
257
288
self .ssm [idx ] = (sigma_trend ** 2 ).ravel ()
0 commit comments