@@ -43,14 +43,8 @@ class BayesianDynamicFactor(PyMCStateSpace):
43
43
Names of the observed time series. If not provided, default names will be generated as `endog_1`, `endog_2`, ..., `endog_k` based on `k_endog`.
44
44
At least one of `k_endog` or `endog_names` must be provided.
45
45
46
- k_endog : int
47
- Number of observed time series.
48
-
49
- endog_names : Sequence[str], optional
50
- Names of the observed time series. If not provided, default names will be generated as `endog_1`, `endog_2`, ..., `endog_k`.
51
-
52
- k_exog : int
53
- Number of exogenous variables, optional. If not provided, model will not have exogenous variables.
46
+ k_exog : int, optional
47
+ Number of exogenous variables. If not provided, the model will not have exogenous variables.
54
48
55
49
exog_names : Sequence[str], optional
56
50
Names of the exogenous variables. If not provided, but `k_exog` is specified, default names will be generated as `exog_1`, `exog_2`, ..., `exog_k`.
@@ -82,6 +76,7 @@ class BayesianDynamicFactor(PyMCStateSpace):
82
76
83
77
Notes
84
78
-----
79
+ TODO: adding to notes, how exog variables are handled and add them in the example?
85
80
The Dynamic Factor Model (DFM) is a multivariate state-space model used to represent high-dimensional time series
86
81
as being driven by a smaller set of unobserved dynamic factors.
87
82
@@ -304,7 +299,7 @@ def __init__(
304
299
self .error_cov_type = error_cov_type
305
300
306
301
if k_exog is None and exog_names is None :
307
- self ._exog = False
302
+ self ._exog = False # flag for presence of exogeneous variables
308
303
self .k_exog = 0
309
304
else :
310
305
self ._exog = True
@@ -325,7 +320,7 @@ def __init__(
325
320
# I start implementing a version of exog support based on pymc_extras/statespace/models/structural/components/regression.py
326
321
# exog_innovations control if the beta coefficient follows a random walk
327
322
# shared_exog_states control if the exogenous states are shared across equations
328
- # I tested the case of shared_exog_states=True and exog_innovations=False vs the stats case by looking at trajectory and it works well
323
+ # I tested the case of shared_exog_states=True and exog_innovations=False vs the stats case by looking at trajectory and everything works well
329
324
330
325
# Determine the dimension for the latent factor states.
331
326
# For static factors, one use k_factors.
@@ -345,7 +340,6 @@ def __init__(
345
340
# Number of independent shocks.
346
341
# Typically, the latent factors introduce k_factors shocks.
347
342
# If error_order > 0 and errors are modeled jointly or separately, add appropriate count.
348
- # TODO currently the implementation does not support for innovation on betas coefficient
349
343
k_posdef = (
350
344
k_factors
351
345
+ (k_endog if error_order > 0 else 0 )
@@ -465,12 +459,16 @@ def state_names(self) -> list[str]:
465
459
if self .shared_exog_states :
466
460
# Shared exogenous states
467
461
for i in range (self .k_exog ):
468
- names .append (f"exog_{ i } .shared" )
462
+ names .append (
463
+ f"exog_{ i } .shared"
464
+ ) # better to call beta? But could make confusion between parameter of the model and name of the state
469
465
else :
470
466
# Exogenous states
471
467
for i in range (self .k_exog ):
472
468
for j in range (self .k_endog ):
473
- names .append (f"exog_{ i } .endog_{ j } " )
469
+ names .append (
470
+ f"exog_{ i } .endog_{ j } "
471
+ ) # better to call beta? But could make confusion between parameter of the model and name of the state
474
472
475
473
return names
476
474
@@ -517,6 +515,15 @@ def shock_names(self):
517
515
for i in range (self .k_endog ):
518
516
shock_names .append (f"error_shock_{ i } " )
519
517
518
+ if self ._exog :
519
+ if self .shared_exog_states :
520
+ for i in range (self .k_exog ):
521
+ shock_names .append (f"exog_shock_{ i } .shared" )
522
+ else :
523
+ for i in range (self .k_exog ):
524
+ for j in range (self .k_endog ):
525
+ shock_names .append (f"exog_shock_{ i } .endog_{ j } " )
526
+
520
527
return shock_names
521
528
522
529
@property
@@ -580,12 +587,6 @@ def make_symbolic_graph(self):
580
587
x0_2 = self .make_and_register_variable (
581
588
"beta" , shape = (self .k_exog_states ,), dtype = floatX
582
589
)
583
- # if self.shared_exog_states:
584
- # x0_1 = self.make_and_register_variable("x0", shape=(self.k_states-self.k_exog,), dtype=floatX)
585
- # x0_2 = self.make_and_register_variable("beta", shape=(self.k_exog,), dtype=floatX)
586
- # else:
587
- # x0_1 = self.make_and_register_variable("x0", shape=(self.k_states-self.k_endog*self.k_exog,), dtype=floatX)
588
- # x0_2 = self.make_and_register_variable("beta", shape=(self.k_endog*self.k_exog,), dtype=floatX)
589
590
x0 = pt .concatenate ([x0_1 , x0_2 ], axis = 0 )
590
591
591
592
self .ssm ["initial_state" , :] = x0
@@ -604,6 +605,7 @@ def make_symbolic_graph(self):
604
605
# Start with factor loadings
605
606
matrix_parts = [factor_loadings ]
606
607
608
+ # Leave space for higher-order factors
607
609
if self .factor_order > 1 :
608
610
matrix_parts .append (
609
611
pt .zeros ((self .k_endog , self .k_factors * (self .factor_order - 1 )), dtype = floatX )
@@ -722,8 +724,10 @@ def build_independent_var_block_matrix(ar_coeffs, k_series, p):
722
724
transition_blocks .append (
723
725
build_independent_var_block_matrix (error_ar , self .k_endog , self .error_order )
724
726
)
727
+ # Exogenous variables are either constant or follow a random walk
725
728
if self ._exog :
726
729
transition_blocks .append (pt .eye (self .k_exog_states , dtype = floatX ))
730
+
727
731
# Final block diagonal transition matrix
728
732
self .ssm ["transition" , :, :] = pt .linalg .block_diag (* transition_blocks )
729
733
@@ -738,17 +742,21 @@ def build_independent_var_block_matrix(ar_coeffs, k_series, p):
738
742
self .ssm ["selection" , row , col ] = 1.0
739
743
740
744
if self ._exog and self .exog_innovations :
741
- for i in range (self .k_exog_states ):
742
- if self .error_order > 0 :
743
- self .ssm [
744
- "selection" ,
745
- self .k_states - self .k_exog_states + i ,
746
- self .k_factors + self .k_endog + i ,
747
- ] = 1.0
748
- else :
749
- self .ssm [
750
- "selection" , self .k_states - self .k_exog_states + i , self .k_factors + i
751
- ] = 1.0
745
+ # Calculate row and column indices for the identity block
746
+ row_start = self .k_states - self .k_exog_states
747
+ row_end = self .k_states
748
+
749
+ if self .error_order > 0 :
750
+ col_start = self .k_factors + self .k_endog
751
+ col_end = self .k_factors + self .k_endog + self .k_exog_states
752
+ else :
753
+ col_start = self .k_factors
754
+ col_end = self .k_factors + self .k_exog_states
755
+
756
+ # Set the identity block directly
757
+ self .ssm ["selection" , row_start :row_end , col_start :col_end ] = pt .eye (
758
+ self .k_exog_states , dtype = floatX
759
+ )
752
760
753
761
factor_cov = pt .eye (self .k_factors , dtype = floatX )
754
762
@@ -804,9 +812,11 @@ def build_independent_var_block_matrix(ar_coeffs, k_series, p):
804
812
"sigma_obs" , shape = (self .k_endog ,), dtype = floatX
805
813
)
806
814
self .ssm ["obs_cov" , :, :] = pt .diag (sigma_obs )
807
- # else: obs_cov remains zero (no measurement noise and idiosyncratic noise captured in state)
815
+ # else: obs_cov remains zero (no measurement noise and idiosyncratic noise captured in state)
808
816
else :
809
817
if self .measurement_error :
818
+ # TODO: check this decision
819
+ # in this case error_order = 0, so there is no error term in the state, so the sigma error could not be added there
810
820
# Idiosyncratic + measurement error
811
821
sigma_obs = self .make_and_register_variable (
812
822
"sigma_obs" , shape = (self .k_endog ,), dtype = floatX
0 commit comments