Skip to content

Commit bff3e80

Browse files
committed
Small adjustments and improvements in DFM.py
1 parent dcc1293 commit bff3e80

File tree

1 file changed

+41
-31
lines changed
  • pymc_extras/statespace/models

1 file changed

+41
-31
lines changed

pymc_extras/statespace/models/DFM.py

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)