@@ -43,14 +43,8 @@ class BayesianDynamicFactor(PyMCStateSpace):
4343 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`.
4444 At least one of `k_endog` or `endog_names` must be provided.
4545
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.
5448
5549 exog_names : Sequence[str], optional
5650 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):
8276
8377 Notes
8478 -----
79+ TODO: adding to notes, how exog variables are handled and add them in the example?
8580 The Dynamic Factor Model (DFM) is a multivariate state-space model used to represent high-dimensional time series
8681 as being driven by a smaller set of unobserved dynamic factors.
8782
@@ -304,7 +299,7 @@ def __init__(
304299 self .error_cov_type = error_cov_type
305300
306301 if k_exog is None and exog_names is None :
307- self ._exog = False
302+ self ._exog = False # flag for presence of exogeneous variables
308303 self .k_exog = 0
309304 else :
310305 self ._exog = True
@@ -325,7 +320,7 @@ def __init__(
325320 # I start implementing a version of exog support based on pymc_extras/statespace/models/structural/components/regression.py
326321 # exog_innovations control if the beta coefficient follows a random walk
327322 # 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
329324
330325 # Determine the dimension for the latent factor states.
331326 # For static factors, one use k_factors.
@@ -345,7 +340,6 @@ def __init__(
345340 # Number of independent shocks.
346341 # Typically, the latent factors introduce k_factors shocks.
347342 # 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
349343 k_posdef = (
350344 k_factors
351345 + (k_endog if error_order > 0 else 0 )
@@ -465,12 +459,16 @@ def state_names(self) -> list[str]:
465459 if self .shared_exog_states :
466460 # Shared exogenous states
467461 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
469465 else :
470466 # Exogenous states
471467 for i in range (self .k_exog ):
472468 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
474472
475473 return names
476474
@@ -517,6 +515,15 @@ def shock_names(self):
517515 for i in range (self .k_endog ):
518516 shock_names .append (f"error_shock_{ i } " )
519517
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+
520527 return shock_names
521528
522529 @property
@@ -580,12 +587,6 @@ def make_symbolic_graph(self):
580587 x0_2 = self .make_and_register_variable (
581588 "beta" , shape = (self .k_exog_states ,), dtype = floatX
582589 )
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)
589590 x0 = pt .concatenate ([x0_1 , x0_2 ], axis = 0 )
590591
591592 self .ssm ["initial_state" , :] = x0
@@ -604,6 +605,7 @@ def make_symbolic_graph(self):
604605 # Start with factor loadings
605606 matrix_parts = [factor_loadings ]
606607
608+ # Leave space for higher-order factors
607609 if self .factor_order > 1 :
608610 matrix_parts .append (
609611 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):
722724 transition_blocks .append (
723725 build_independent_var_block_matrix (error_ar , self .k_endog , self .error_order )
724726 )
727+ # Exogenous variables are either constant or follow a random walk
725728 if self ._exog :
726729 transition_blocks .append (pt .eye (self .k_exog_states , dtype = floatX ))
730+
727731 # Final block diagonal transition matrix
728732 self .ssm ["transition" , :, :] = pt .linalg .block_diag (* transition_blocks )
729733
@@ -738,17 +742,21 @@ def build_independent_var_block_matrix(ar_coeffs, k_series, p):
738742 self .ssm ["selection" , row , col ] = 1.0
739743
740744 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+ )
752760
753761 factor_cov = pt .eye (self .k_factors , dtype = floatX )
754762
@@ -804,9 +812,11 @@ def build_independent_var_block_matrix(ar_coeffs, k_series, p):
804812 "sigma_obs" , shape = (self .k_endog ,), dtype = floatX
805813 )
806814 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)
808816 else :
809817 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
810820 # Idiosyncratic + measurement error
811821 sigma_obs = self .make_and_register_variable (
812822 "sigma_obs" , shape = (self .k_endog ,), dtype = floatX
0 commit comments