Skip to content

Commit e15cdd3

Browse files
committed
Update of DFM draft implementation
In the notebook a comparison between the custom DFM and the implemented DFM (which has an hardcoded version of make_symbolic_graph, that work just in this case)
1 parent 8e618b9 commit e15cdd3

File tree

2 files changed

+564
-151
lines changed

2 files changed

+564
-151
lines changed

notebooks/Making a Custom DFM.ipynb

Lines changed: 454 additions & 107 deletions
Large diffs are not rendered by default.

pymc_extras/statespace/models/DFM.py

Lines changed: 110 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
from pymc_extras.statespace.core.statespace import PyMCStateSpace
66
from pymc_extras.statespace.utils.constants import (
7+
ALL_STATE_AUX_DIM,
78
ALL_STATE_DIM,
89
AR_PARAM_DIM,
910
MA_PARAM_DIM,
11+
OBS_STATE_DIM,
1012
SHOCK_DIM,
1113
)
1214

@@ -28,6 +30,8 @@ class BayesianDynamicFactor(PyMCStateSpace):
2830
2931
exog : array_like, optional
3032
Array of exogenous regressors for the observation equation (nobs x k_exog).
33+
Default is None, meaning no exogenous regressors.
34+
Not implemented yet.
3135
3236
error_order : int, optional
3337
Order of the AR process for the observation error component.
@@ -58,6 +62,9 @@ class BayesianDynamicFactor(PyMCStateSpace):
5862
the observed time series are driven by a set of latent factors that evolve
5963
according to a VAR process, possibly along with an autoregressive error term.
6064
65+
Up to now just a draft implementation to test the working of the class and comparing
66+
with the Custom model done in the Notebook (notebook/Making a Custom DFM.ipynb).
67+
The model work just with two observations and one factor (k_endog=2, k_factors=1).
6168
6269
6370
"""
@@ -113,9 +120,15 @@ def __init__(
113120

114121
@property
115122
def param_names(self):
116-
names = ["factor_loadings", "factor_ar", "error_ar", "error_sigma"]
117-
118-
# factor_sigma is fixed and equal to the identity matrix
123+
names = [
124+
"x0",
125+
"P0",
126+
"factor_loadings",
127+
"factor_ar",
128+
"factor_sigma",
129+
"error_ar",
130+
"error_sigma",
131+
]
119132

120133
# Handle cases where parameters should be excluded based on model settings
121134
if self.factor_order == 0:
@@ -130,6 +143,14 @@ def param_names(self):
130143
@property
131144
def param_info(self) -> dict[str, dict[str, Any]]:
132145
info = {
146+
"x0": {
147+
"shape": (self.k_factors,),
148+
"constraints": None,
149+
},
150+
"P0": {
151+
"shape": (self.k_factors, self.k_factors),
152+
"constraints": "Positive Semi-definite",
153+
},
133154
"factor_loadings": {
134155
"shape": (self.k_endog, self.k_factors),
135156
"constraints": None,
@@ -138,6 +159,10 @@ def param_info(self) -> dict[str, dict[str, Any]]:
138159
"shape": (self.k_factors, self.factor_order, self.k_factors),
139160
"constraints": None,
140161
},
162+
"factor_sigma": {
163+
"shape": (self.k_factors,),
164+
"constraints": "Positive",
165+
},
141166
"error_ar": {
142167
"shape": (self.k_endog, self.error_order, self.k_endog)
143168
if self.error_var
@@ -167,9 +192,7 @@ def param_info(self) -> dict[str, dict[str, Any]]:
167192

168193
@property
169194
def state_names(self):
170-
# Initialize state names based on the endogenous variables
171-
state_names = self.endog_names.copy()
172-
195+
state_names = []
173196
# Add names for the factor loadings (one per observation and factor)
174197
for i in range(self.k_endog):
175198
for j in range(self.k_factors):
@@ -216,10 +239,6 @@ def state_names(self):
216239

217240
return state_names
218241

219-
@property
220-
def observed_states(self):
221-
return self.endog_names
222-
223242
@property
224243
def shock_names(self):
225244
shock_names = []
@@ -235,39 +254,86 @@ def shock_names(self):
235254

236255
return shock_names
237256

257+
@property
258+
def param_dims(self):
259+
"""
260+
Define parameter dimensions for the Dynamic Factor Model (DFM).
261+
262+
Returns
263+
-------
264+
dict
265+
Dictionary mapping parameter names to their respective dimensions.
266+
"""
267+
coord_map = {
268+
"x0": (ALL_STATE_DIM,), # Initial state dimension
269+
"P0": (ALL_STATE_DIM, ALL_STATE_DIM), # Initial state covariance dimension
270+
"factor_loadings": (OBS_STATE_DIM, ALL_STATE_AUX_DIM), # Factor loadings dimension
271+
"factor_sigma": (ALL_STATE_DIM,), # Factor variances dimension
272+
}
238273

239-
@property
240-
def param_dims(self):
241-
"""
242-
Define parameter dimensions for the Dynamic Factor Model (DFM).
274+
# Factor AR coefficients if applicable
275+
if self.factor_order > 0:
276+
coord_map["factor_ar"] = (AR_PARAM_DIM, SHOCK_DIM, SHOCK_DIM)
243277

244-
Returns
245-
-------
246-
dict
247-
Dictionary mapping parameter names to their respective dimensions.
248-
"""
249-
coord_map = {
250-
"factor_loadings": (ALL_STATE_DIM, SHOCK_DIM), # Factor loadings dimension
251-
"factor_sigma": (SHOCK_DIM,), # Factor shocks (one per factor)
252-
}
253-
254-
# Factor AR coefficients if applicable
255-
if self.factor_order > 0:
256-
coord_map["factor_ar"] = (AR_PARAM_DIM, SHOCK_DIM, SHOCK_DIM)
257-
258-
# Error AR coefficients and variances
259-
if self.error_order > 0:
260-
if self.error_cov_type == "diagonal":
261-
coord_map["error_ar"] = (MA_PARAM_DIM, SHOCK_DIM) # AR for errors
262-
coord_map["error_sigma"] = (SHOCK_DIM,) # One variance for each observed variable
263-
elif self.error_cov_type == "scalar":
264-
coord_map["error_ar"] = (MA_PARAM_DIM, SHOCK_DIM)
265-
coord_map["error_sigma"] = None # Single scalar for error variance
266-
elif self.error_cov_type == "unstructured":
267-
coord_map["error_ar"] = (MA_PARAM_DIM, SHOCK_DIM, SHOCK_DIM) # AR for errors
268-
coord_map["error_cov_L"] = (SHOCK_DIM, SHOCK_DIM) # Lower triangular Cholesky factor
269-
coord_map["error_cov_sd"] = (SHOCK_DIM,) # Standard deviations for diagonal
270-
else:
271-
raise ValueError("Invalid error covariance type.")
272-
273-
return coord_map
278+
# Error AR coefficients and variances
279+
if self.error_order > 0:
280+
if self.error_cov_type == "diagonal":
281+
coord_map["error_ar"] = (MA_PARAM_DIM, SHOCK_DIM) # AR for errors
282+
coord_map["error_sigma"] = (SHOCK_DIM,) # One variance for each observed variable
283+
elif self.error_cov_type == "scalar":
284+
coord_map["error_ar"] = (MA_PARAM_DIM, SHOCK_DIM)
285+
coord_map["error_sigma"] = None # Single scalar for error variance
286+
elif self.error_cov_type == "unstructured":
287+
coord_map["error_ar"] = (MA_PARAM_DIM, SHOCK_DIM, SHOCK_DIM) # AR for errors
288+
coord_map["error_cov_L"] = (
289+
SHOCK_DIM,
290+
SHOCK_DIM,
291+
) # Lower triangular Cholesky factor
292+
coord_map["error_cov_sd"] = (SHOCK_DIM,) # Standard deviations for diagonal
293+
else:
294+
raise ValueError("Invalid error covariance type.")
295+
296+
return coord_map
297+
298+
# def make_symbolic_graph(self):
299+
# We will implement this in a moment. For now, we need to overwrite it with nothing to avoid a NotImplementedError
300+
# when we initialize a class instance.
301+
# pass
302+
303+
def make_symbolic_graph(self):
304+
"""
305+
Create the symbolic graph for the Dynamic Factor Model (DFM).
306+
This method sets up the state space model, including the design, transition,
307+
selection, and initial state matrices, as well as the parameters for the model.
308+
309+
310+
Up to know just a draft implementation to test the working of the class and comparing
311+
with the Custom model done in the Notebook (notebook/Making a Custom DFM.ipynb).
312+
"""
313+
314+
# Create symbolic variables for 1D state
315+
x0 = self.make_and_register_variable("x0", shape=(1,))
316+
P0 = self.make_and_register_variable("P0", shape=(1, 1))
317+
factor_loading = self.make_and_register_variable("factor_loadings", shape=(2, 1))
318+
319+
factor_ar = self.make_and_register_variable("factor_ar", shape=())
320+
sigma_f = self.make_and_register_variable("factor_sigma", shape=())
321+
322+
# Initialize matrices with correct dimensions
323+
self.ssm["design", :, :] = np.array([[0.0], [0.0]]) # 2x1 matrix
324+
self.ssm["transition", :, :] = np.array([[0.0]]) # 1x1 matrix
325+
self.ssm["selection", :, :] = np.array([[1.0]]) # 1x1 matrix
326+
327+
# Set initial state and covariance
328+
self.ssm["initial_state", :] = x0
329+
self.ssm["initial_state_cov", :, :] = P0
330+
331+
# Set design matrix parameters
332+
self.ssm["design", 0, 0] = factor_loading[0, 0] # First observation loading
333+
self.ssm["design", 1, 0] = factor_loading[1, 0] # Second observation loading
334+
335+
# Set transition parameter (AR coefficient)
336+
self.ssm["transition", 0, 0] = factor_ar
337+
338+
# Set state covariance
339+
self.ssm["state_cov", 0, 0] = sigma_f

0 commit comments

Comments
 (0)