dfm-python 0.4.3__py3-none-any.whl → 0.4.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dfm_python/__init__.py CHANGED
@@ -67,7 +67,7 @@ Note: DFMConfig and SeriesConfig are internal implementation details.
67
67
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
68
68
  """
69
69
 
70
- __version__ = "0.4.3"
70
+ __version__ = "0.4.5"
71
71
 
72
72
  # ============================================================================
73
73
  # PUBLIC API DEFINITION
@@ -31,13 +31,6 @@ from .scaling import (
31
31
  # Note: dfm_module.py has been removed - DFM class implements EM directly
32
32
  from ..models.dfm import DFMTrainingState
33
33
 
34
- # DDFMLightningModule is deprecated and unused - DDFM class uses DDFMModel directly.
35
- # Keeping ddfm_module.py file for reference only, but not exporting it.
36
- # from .ddfm_module import (
37
- # DDFMLightningModule, # Deprecated: not used (DDFM uses DDFMModel)
38
- # DDFMTrainingState, # Duplicate: also defined in models.ddfm
39
- # )
40
-
41
34
  __all__ = [
42
35
  # Kalman filter
43
36
  'KalmanFilter', # New module class
dfm_python/models/base.py CHANGED
@@ -499,43 +499,6 @@ class BaseFactorModel(pl.LightningModule):
499
499
  self.training_state = None
500
500
  return self
501
501
 
502
- def plot(self, **kwargs) -> 'BaseFactorModel':
503
- """Plot common visualizations (not yet implemented).
504
-
505
- .. note::
506
- This method is a placeholder for future visualization features.
507
- For now, use external plotting libraries (matplotlib, plotly) with
508
- model results: ``result = model.get_result()`` then plot ``result.Z``
509
- (factors) or ``result.X_sm`` (smoothed data).
510
-
511
- Parameters
512
- ----------
513
- **kwargs
514
- Additional keyword arguments (reserved for future use)
515
-
516
- Returns
517
- -------
518
- BaseFactorModel
519
- Self for method chaining
520
-
521
- Raises
522
- ------
523
- ValueError
524
- If model has not been trained yet
525
- """
526
- if self.training_state is None:
527
- error_msg = self._format_error_message(
528
- operation="plotting",
529
- reason="model has not been trained yet",
530
- guidance="Please call trainer.fit(model, data_module) first"
531
- )
532
- raise ValueError(error_msg)
533
- _logger.warning(
534
- "Plot functionality not yet implemented. Use external plotting libraries "
535
- "with model results: result = model.get_result(); plot(result.Z)"
536
- )
537
- return self
538
-
539
502
  def load_pickle(self, path: Union[str, Path], **kwargs) -> 'BaseFactorModel':
540
503
  """Load a saved model from pickle file.
541
504
 
@@ -236,8 +236,6 @@ from .utils import (
236
236
  extract_news,
237
237
  )
238
238
 
239
- # Backward compatibility aliases removed - use direct imports from nowcast.utils
240
-
241
239
  # ============================================================================
242
240
  # Backtest result classes (merged from backtest.py)
243
241
  # ============================================================================
@@ -4,19 +4,19 @@ This module provides custom splitter implementations for nowcasting evaluation
4
4
  that respect publication lags and backward-looking nature of nowcasting.
5
5
  """
6
6
 
7
- from typing import Iterator, Tuple, List, Optional, Union
7
+ from typing import Iterator, Tuple, List, Optional, Union, Any
8
8
  from datetime import datetime, timedelta
9
9
  import numpy as np
10
10
  import pandas as pd
11
11
 
12
- from sktime.split import BaseSplitter
12
+ from sktime.split.base import BaseSplitter
13
13
  from sktime.forecasting.base import BaseForecaster
14
14
 
15
15
  from ..config import DFMConfig
16
16
  from ..utils.time import TimeIndex, parse_timestamp, to_python_datetime
17
17
  from ..utils.data import create_data_view, calculate_release_date
18
- from ..utils.helpers import get_series_ids
19
- from .utils import calc_backward_date, get_higher_frequency, get_clock_frequency
18
+ from ..utils.helpers import get_series_ids, get_clock_frequency
19
+ from .utils import calc_backward_date, get_higher_frequency
20
20
 
21
21
 
22
22
  class NowcastingSplitter(BaseSplitter):
@@ -213,10 +213,6 @@ def check_config(saved_config: Any, current_config: DFMConfig) -> None:
213
213
  # If comparison fails, continue anyway
214
214
 
215
215
 
216
- # transform_data and _transform_series removed - use DataModule with custom transformers instead
217
- # transform_series alias removed
218
-
219
-
220
216
  def extract_news(
221
217
  singlenews: np.ndarray,
222
218
  weight: np.ndarray,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.4.3
3
+ Version: 0.4.5
4
4
  Summary: Dynamic Factor Model (DFM) estimation and nowcasting in Python
5
5
  Project-URL: Homepage, https://github.com/yourusername/dfm-python
6
6
  Project-URL: Documentation, https://github.com/yourusername/dfm-python#readme
@@ -1,4 +1,4 @@
1
- dfm_python/__init__.py,sha256=aQ4O9IEetFvKSTmgyTOccHPiSKWcvTD5b-g5kGaXhZM,6159
1
+ dfm_python/__init__.py,sha256=wSJzWhUQkHZKVcDGx1W36QzTj6zZ-OgZ82qiTMWf0qg,6159
2
2
  dfm_python/config/__init__.py,sha256=NrTwjNzQRcYlk5Ht0hTF8o-6CCl5e4S_JxQAp1mNyCs,1507
3
3
  dfm_python/config/adapter.py,sha256=KS9C35A1FMRjpU-yJ19sYciurIy0KWtrcJxosoIzOCE,27575
4
4
  dfm_python/config/results.py,sha256=k_WY8v_rJyA8e5zr6Pq6KwNLcq1CbCpavndgBYyaqLs,13778
@@ -12,27 +12,24 @@ dfm_python/encoder/__init__.py,sha256=ljPTXq04cpOsGXNnma2EJ447kPFlytT6_mToLyKok7
12
12
  dfm_python/encoder/base.py,sha256=MOSn_bsQP-GVonGprJymQq-_Q6tmCQex8VOWUJjyxNA,3029
13
13
  dfm_python/encoder/pca.py,sha256=FsvCqXD0AuASxymV2-5oAM2QVv_syhSyL3CmxVOXxgM,10584
14
14
  dfm_python/encoder/vae.py,sha256=a5LYl9bb2j2kcZL-irPMTlwHTotCAMOCNA6BmG0xqw4,20431
15
- dfm_python/lightning/__init__.py,sha256=czIV9GYj2z2BxK30Wpbtp7grc6gmRycwzu6NbYgG1uc,1853
16
- dfm_python/lightning/callbacks.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ dfm_python/lightning/__init__.py,sha256=S73pzk72n7Mk8to6urty_PULKyo83Y4FiXNWSScI2-4,1522
17
16
  dfm_python/lightning/data_module.py,sha256=GKXNUgTb4Hivs1NFrj05YVH7UEwIt8o4sa_DEOuoYSI,30478
18
17
  dfm_python/lightning/scaling.py,sha256=o6DlimsZjf0Z3zosezURin6NBTOE9kAfdcdXKHbZdz4,16359
19
- dfm_python/lightning/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
18
  dfm_python/logger/__init__.py,sha256=2GwtfOG2ttbtJ5CrO6338R6GIao3YuY0HLiBeCoC0Ns,1066
21
19
  dfm_python/logger/inference_logger.py,sha256=EeFfux4UxXhIosg8dEjbkkdm38_4LKZdGS_6p_xpzWI,10118
22
20
  dfm_python/logger/logger.py,sha256=cledaY-VvZx_HzsV4Ukuw5tP5sEvkxiAqOKJlwbDWXA,3768
23
21
  dfm_python/logger/train_logger.py,sha256=7ufZqDxexRmTI_kokoOyAGYQBmipRb0dALLXawJqq4E,17759
24
22
  dfm_python/models/__init__.py,sha256=mrs2N5OoFeNWCjS6luPjmwzFosNDkpdMiBoA24NMrE4,640
25
- dfm_python/models/base.py,sha256=0bIMEWbPJGgp5vC_uU_qt3ZtT1XkDQ2rsBiTUiNjwKM,21052
23
+ dfm_python/models/base.py,sha256=QG3Mc2c2ccZx_Zmr1CnNO8B3Ek6yCm86EoKQccycsBA,19718
26
24
  dfm_python/models/ddfm.py,sha256=SbVva4gwYM8dN3ibf725Svqv4jyeTZEFOja87ZHeJqk,107381
27
25
  dfm_python/models/dfm.py,sha256=pfBMI5-vxLPzYkG7xa6yWY1TkDqpBdtGAdv0rRazKpA,25382
28
- dfm_python/models/estimation.py,sha256=hc10Dw4E7reBF2dXDjqyY2Z9_RDLNM4QBZIYSFJMdds,14005
29
26
  dfm_python/nowcast/__init__.py,sha256=AI51eoxdpKh-7EksDHdGcQple6bN_Gferp8VRCNmpYU,954
30
27
  dfm_python/nowcast/dataview.py,sha256=KpLZHVu9QPFDmTWWxZdlTTQ8Z1Q-676NJ47TpWKr2-I,4439
31
- dfm_python/nowcast/helpers.py,sha256=9YgTQbReDFnw0otmsAvO0xA0IfwR8nhb9K0Ru82uVjU,13849
28
+ dfm_python/nowcast/helpers.py,sha256=pUWVrMMrc2N8MwD6qQw_a-kXUAw_t8CKe4Mjnf-z96U,13767
32
29
  dfm_python/nowcast/nowcast.py,sha256=bXlYUVZ-Lx05u2eqD42OtFhGpYAecx2GFQ2c9xpuueg,50746
33
- dfm_python/nowcast/splitters.py,sha256=xaqvcQlWwGl7itlVi7KgWfLGDnNYtBQcxG4lHDWWR5A,15768
30
+ dfm_python/nowcast/splitters.py,sha256=3RWhRxInoLVW6Oh34Shu88IT3WBM9TK3Wpig2cpQ_oI,15778
34
31
  dfm_python/nowcast/transformers.py,sha256=Kf_Wd3_xeCKqpddsNnoPOhip1sWF7AhuzcsKSzaHMQo,10645
35
- dfm_python/nowcast/utils.py,sha256=_bl9_GsEZZP9I8z2qqFnzpjzzYjMeJP_W7U0QiosUJw,9362
32
+ dfm_python/nowcast/utils.py,sha256=aFbXTWbBAYebhznfa15X8kL8drAe7i-yECMtnjMDRvc,9230
36
33
  dfm_python/ssm/__init__.py,sha256=dFpt8_3jb_foISDNhma_JUAzch0VrKZcaeWP9zwpwoM,1161
37
34
  dfm_python/ssm/em.py,sha256=N1cGTpsE1A6phLWDHYB6mYm8RYd09FZSwOweGjoSa8Y,34760
38
35
  dfm_python/ssm/kalman.py,sha256=USWXDJ5nj1sk6FXgG2zXe-VYGdKDYj-kGruIvy8cfHU,24501
@@ -46,7 +43,7 @@ dfm_python/utils/diagnostics.py,sha256=UwrJfnn8VACATfQEkTcWw9eEJlTik-VCgTZ1dJABZ
46
43
  dfm_python/utils/helpers.py,sha256=t-hJNgmPbk5KV5nGP7z_MF0sVh-VJMdQyoDvRBzwTV8,13020
47
44
  dfm_python/utils/statespace.py,sha256=pyKy1D8bLMmd7sDvAVSb2n74U0IfH2YuYreIfyg3MAA,43823
48
45
  dfm_python/utils/time.py,sha256=l9xu1nwLquWpRgWayRELPhTymV9P47s3s4Q8b-gl0IM,35745
49
- dfm_python-0.4.3.dist-info/METADATA,sha256=OLTnar9fKHbI8jSefyJz-jShnp1bSUqZqXxRR8B1PMw,48890
50
- dfm_python-0.4.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
51
- dfm_python-0.4.3.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
52
- dfm_python-0.4.3.dist-info/RECORD,,
46
+ dfm_python-0.4.5.dist-info/METADATA,sha256=0Av0uXKYn-qwlaLTxBPo-K5_Evv56v2j1nkzyXxk2pY,48890
47
+ dfm_python-0.4.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ dfm_python-0.4.5.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
49
+ dfm_python-0.4.5.dist-info/RECORD,,
File without changes
File without changes
@@ -1,371 +0,0 @@
1
- """Dynamic Factor Model (DFM) estimation using PyTorch Lightning.
2
-
3
- This module provides the core DFM estimation function (_dfm_core) which
4
- uses PyTorch Lightning modules for training.
5
-
6
- The implementation uses a clock-based approach, where all latent factors
7
- evolve at a common clock frequency, with lower-frequency observations
8
- mapped to higher-frequency latent states via deterministic tent kernels.
9
-
10
- Note: This module now acts as a wrapper around Lightning modules. The actual
11
- estimation logic is in dfm_python.lightning modules.
12
- """
13
-
14
- import numpy as np
15
- from typing import Tuple, Optional, Any, Dict, Union
16
- import warnings
17
- import logging
18
- import pandas as pd
19
- from ..logger import get_logger
20
-
21
- from ..config import DFMConfig
22
- from ..utils.time import calculate_rmse
23
- from ..utils.diagnostics import (
24
- _display_dfm_tables,
25
- )
26
- from ..utils.helpers import (
27
- safe_get_method, safe_get_attr, resolve_param,
28
- get_series_names, get_frequencies, ParameterResolver
29
- )
30
-
31
- from ..config.utils import (
32
- get_agg_structure,
33
- FREQUENCY_HIERARCHY,
34
- )
35
-
36
- from ..config.results import DFMResult, FitParams
37
-
38
- _logger = get_logger(__name__)
39
-
40
-
41
- def _prepare_data(
42
- X: np.ndarray,
43
- config: DFMConfig,
44
- params: Optional[FitParams] = None,
45
- ) -> Tuple[np.ndarray, np.ndarray, Dict[str, Any]]:
46
- """Prepare data and resolve all parameters from config and overrides.
47
-
48
- Parameters
49
- ----------
50
- X : np.ndarray
51
- Input data matrix (T x N)
52
- config : DFMConfig
53
- Configuration object
54
- params : FitParams, optional
55
- Parameter overrides. If None, all values from config are used.
56
-
57
- Returns
58
- -------
59
- X_clean : np.ndarray
60
- Cleaned input data (Inf replaced with NaN)
61
- blocks : np.ndarray
62
- Block structure array (N x n_blocks)
63
- params_dict : dict
64
- Dictionary of resolved parameters
65
- """
66
- # Clean input data
67
- inf_mask = np.isinf(X)
68
- if np.any(inf_mask):
69
- X = np.where(inf_mask, np.nan, X)
70
- warnings.warn("Data contains Inf values, replaced with NaN", UserWarning)
71
-
72
- blocks = config.get_blocks_array()
73
- T, N = X.shape
74
-
75
- # Initialize params if not provided
76
- if params is None:
77
- params = FitParams()
78
-
79
- # Use ParameterResolver for consistent parameter resolution
80
- resolver = ParameterResolver(config, params)
81
- params_dict = resolver.resolve_estimation_params()
82
-
83
- # Add model structure parameters
84
- params_dict['r'] = (np.array(config.factors_per_block)
85
- if config.factors_per_block is not None
86
- else np.ones(blocks.shape[1]))
87
- params_dict['T'] = T
88
- params_dict['N'] = N
89
-
90
- # Display blocks structure if debug logging enabled
91
- if _logger.isEnabledFor(logging.DEBUG):
92
- try:
93
- series_names = get_series_names(config)
94
- block_names = (config.block_names if len(config.block_names) == blocks.shape[1]
95
- else [f'Block_{i+1}' for i in range(blocks.shape[1])])
96
- # Create pandas DataFrame
97
- df_dict = {block_names[i]: blocks[:, i].tolist() for i in range(blocks.shape[1])}
98
- df_dict['series'] = [name.replace(' ', '_') for name in series_names]
99
- df = pd.DataFrame(df_dict)
100
- _logger.debug('Block Loading Structure')
101
- _logger.debug(f'\n{df}')
102
- _logger.debug(f'Blocks shape: {blocks.shape}')
103
- except Exception as e:
104
- _logger.debug(f'Error displaying block structure: {e}')
105
-
106
- return X, blocks, params_dict
107
-
108
-
109
- def _prepare_agg_structure(
110
- config: DFMConfig,
111
- clock: str
112
- ) -> Tuple[Dict[str, np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], np.ndarray, int, np.ndarray]:
113
- """Prepare aggregation structure for mixed-frequency handling.
114
-
115
- Returns
116
- -------
117
- tent_weights_dict : dict
118
- Dictionary mapping frequency pairs to tent weights
119
- R_mat : np.ndarray or None
120
- Constraint matrix for tent kernel aggregation
121
- q : np.ndarray or None
122
- Constraint vector for tent kernel aggregation
123
- frequencies : np.ndarray or None
124
- Array of frequencies for each series
125
- i_idio : np.ndarray
126
- Indicator array (1 for clock frequency, 0 for slower frequencies)
127
- nQ : int
128
- Number of slower-frequency series
129
- idio_chain_lengths : np.ndarray
130
- Array of idiosyncratic chain lengths per series (0, 1, or tent length)
131
- """
132
- from ..config.utils import compute_idio_lengths
133
-
134
- agg_info = get_agg_structure(config, clock=clock)
135
- tent_weights_dict = agg_info.get('tent_weights', {})
136
- frequencies = np.array(get_frequencies(config)) if config.series else None
137
-
138
- # Find R_mat and q for tent kernel constraints
139
- R_mat = None
140
- q = None
141
- if agg_info['structures']:
142
- max_periods = 0
143
- for (slower_freq, clock_freq), (R, q_vec) in agg_info['structures'].items():
144
- if R is not None:
145
- n_periods = R.shape[1]
146
- if n_periods > max_periods:
147
- max_periods = n_periods
148
- R_mat = R
149
- q = q_vec
150
-
151
- # Compute i_idio and nQ
152
- if frequencies is not None:
153
- clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, 3)
154
- N = len(frequencies)
155
- i_idio = np.array([
156
- 1 if j >= len(frequencies) or FREQUENCY_HIERARCHY.get(frequencies[j], 3) <= clock_hierarchy
157
- else 0
158
- for j in range(N)
159
- ])
160
- nQ = N - np.sum(i_idio)
161
- else:
162
- i_idio = np.ones(config.get_blocks_array().shape[0])
163
- nQ = 0
164
-
165
- # Compute idio chain lengths
166
- idio_chain_lengths = compute_idio_lengths(config, clock, tent_weights_dict)
167
-
168
- return tent_weights_dict, R_mat, q, frequencies, i_idio, nQ, idio_chain_lengths
169
-
170
-
171
-
172
-
173
- def _dfm_core(
174
- X: np.ndarray,
175
- config: DFMConfig,
176
- params: Optional[FitParams] = None,
177
- Mx: Optional[np.ndarray] = None,
178
- Wx: Optional[np.ndarray] = None,
179
- **kwargs
180
- ) -> DFMResult:
181
- """Estimate dynamic factor model using EM algorithm.
182
-
183
- This is the main function for estimating a Dynamic Factor Model (DFM). It implements
184
- the complete EM algorithm workflow:
185
-
186
- 1. **Initialization**: Compute initial parameter estimates via PCA and OLS
187
- 2. **EM Iterations**: Iteratively update parameters until convergence
188
- 3. **Final Smoothing**: Run Kalman smoother with final parameters to obtain
189
- smoothed factors and data
190
-
191
- The DFM models observed time series as:
192
-
193
- .. math::
194
- y_t = C Z_t + e_t, e_t \\sim N(0, R)
195
- Z_t = A Z_{t-1} + v_t, v_t \\sim N(0, Q)
196
-
197
- where:
198
- - y_t is the n x 1 vector of observed series at time t
199
- - Z_t is the m x 1 vector of unobserved factors
200
- - C is the n x m loading matrix
201
- - A is the m x m transition matrix
202
- - R and Q are covariance matrices
203
-
204
- Parameters
205
- ----------
206
- X : np.ndarray
207
- Data matrix (T x N), where T is time periods and N is number of series.
208
- Data should already be transformed and standardized (use DFMDataModule with custom transformer).
209
- Data can contain missing values (NaN), which are handled via spline interpolation.
210
- Missing values are allowed but excessive missing data (>50%) will trigger warnings.
211
- config : DFMConfig
212
- Unified DFM configuration object containing:
213
- - Model structure: Blocks (N x n_blocks), Frequency (per series),
214
- Transformation (per series), factors_per_block
215
- - Estimation parameters: ar_lag, threshold, max_iter, nan_method, nan_k
216
- Typically obtained from `load_config()`.
217
- params : FitParams, optional
218
- Parameter overrides. If None, all values from config are used.
219
- All parameters in FitParams are optional and override corresponding config values.
220
- Mx : np.ndarray, optional
221
- Mean values used for standardization (N,). Required if data is already standardized.
222
- Wx : np.ndarray, optional
223
- Standard deviation values used for standardization (N,). Required if data is already standardized.
224
- **kwargs
225
- Additional parameter overrides (merged into params if provided).
226
- Valid parameter names: threshold, max_iter, ar_lag, nan_method, nan_k,
227
- clock, clip_ar_coefficients, ar_clip_min, ar_clip_max, clip_data_values,
228
- data_clip_threshold, use_regularization, regularization_scale,
229
- min_eigenvalue, max_eigenvalue, use_damped_updates, damping_factor.
230
-
231
- Returns
232
- -------
233
- DFMResult
234
- Dataclass containing all estimation results:
235
- - x_sm, X_sm: Smoothed data (standardized and unstandardized)
236
- - Z: Smoothed factor estimates
237
- - C, A, Q, R: Estimated parameters
238
- - Mx, Wx: Standardization parameters
239
- - Z_0, V_0: Initial state and covariance
240
- - r, p: Model structure parameters
241
-
242
- Raises
243
- ------
244
- ValueError
245
- If inputs are invalid (dimensions, data quality, parameters).
246
- Also raised during EM iterations if numerical issues occur (e.g., NaN/Inf).
247
- TypeError
248
- If input types are incorrect (e.g., X is not numpy array).
249
-
250
- Notes
251
- -----
252
- - The function expects data to already be transformed and standardized.
253
- Use DFMDataModule with custom sktime transformer to apply transformations and standardization.
254
- - Initial conditions are computed via `init_conditions()`
255
- - EM iterations continue until convergence or max_iter=5000
256
- - Missing data is handled by the Kalman filter during estimation
257
- - Convergence messages and progress are printed during execution
258
-
259
- Examples
260
- --------
261
- >>> from dfm_python import DFM
262
- >>> from dfm_python.config.adapter import YamlSource
263
- >>> from dfm_python import DFMDataModule
264
- >>> from dfm_python.data.utils import load_data
265
- >>> from datetime import datetime
266
- >>> # Load configuration from YAML or create DFMConfig directly
267
- >>> config = YamlSource('config.yaml').load()
268
- >>> # Load data from file
269
- >>> X, Time, Z = load_data('data.csv', config, sample_start=datetime(2000, 1, 1))
270
- >>> # Estimate DFM
271
- >>> model = DFM()
272
- >>> Res = model.fit(X, config, threshold=1e-4)
273
- >>> # Access results
274
- >>> factors = Res.Z # (T x m) factor estimates
275
- >>> loadings = Res.C # (N x m) factor loadings
276
- >>> smoothed_data = Res.X_sm # (T x N) smoothed data
277
- >>> # Compute common factor (first factor)
278
- >>> common_factor = Res.Z[:, 0]
279
- >>> # Project factor onto a series
280
- >>> series_idx = 0
281
- >>> series_factor = Res.Z @ Res.C[series_idx, :].T
282
- """
283
- _logger.info('Estimating the dynamic factor model (DFM)')
284
-
285
- # Merge kwargs into params if provided
286
- if kwargs:
287
- if params is None:
288
- params = FitParams.from_kwargs(**kwargs)
289
- else:
290
- # Update params with kwargs (only valid parameter names)
291
- valid_params = {
292
- 'threshold', 'max_iter', 'ar_lag', 'nan_method', 'nan_k',
293
- 'clock', 'clip_ar_coefficients', 'ar_clip_min', 'ar_clip_max',
294
- 'clip_data_values', 'data_clip_threshold', 'use_regularization',
295
- 'regularization_scale', 'min_eigenvalue', 'max_eigenvalue',
296
- 'use_damped_updates', 'damping_factor'
297
- }
298
- for k, v in kwargs.items():
299
- if k in valid_params and hasattr(params, k):
300
- setattr(params, k, v)
301
-
302
- # Step 1: Prepare data and resolve parameters
303
- X, blocks, params_dict = _prepare_data(X, config, params)
304
-
305
- # Extract key parameters
306
- r = params_dict['r']
307
- nan_method = params_dict['nan_method']
308
- nan_k = params_dict['nan_k']
309
- threshold = params_dict['threshold']
310
- max_iter = params_dict['max_iter']
311
- clock = params_dict['clock']
312
-
313
- # Step 2: Prepare aggregation structure (for diagnostics only)
314
- _, _, _, _, _, nQ, _ = _prepare_agg_structure(config, clock)
315
-
316
- # Step 3: Validate preprocessing
317
- if Mx is None or Wx is None:
318
- raise ValueError(
319
- "Mx and Wx must be provided. Data should be preprocessed using DFMDataModule with custom transformer. "
320
- "before calling _dfm_core()."
321
- )
322
-
323
- # Step 3.5: Handle missing data implicitly
324
- # Missing data (NaN) is handled automatically by the Kalman filter during estimation.
325
- # The Kalman filter's handle_missing_data() method removes NaN observations at each time step,
326
- # allowing the EM algorithm to work with incomplete data. This is the standard approach
327
- # in state-space models (see Harvey 1989, Mariano & Murasawa 2003).
328
- # Preprocessing (rem_nans_spline) handles leading/trailing NaNs, but remaining NaNs
329
- # are handled implicitly during Kalman filtering.
330
- if np.any(np.isnan(X)):
331
- nan_ratio = np.sum(np.isnan(X)) / X.size
332
- if nan_ratio > 0.5:
333
- _logger.warning(
334
- f"High proportion of missing data ({nan_ratio:.1%}). "
335
- "Results may be unstable. Consider preprocessing data more thoroughly."
336
- )
337
- else:
338
- _logger.debug(
339
- f"Data contains {nan_ratio:.1%} missing values. "
340
- "These will be handled implicitly by the Kalman filter during estimation."
341
- )
342
-
343
- # Step 4-8: Use Lightning module for estimation
344
- import torch
345
-
346
- # Convert to torch tensor
347
- X_torch = torch.tensor(X, dtype=torch.float32)
348
-
349
- # Create DFM model (Lightning module)
350
- from ..models.dfm import DFM
351
- model = DFM(
352
- config=config,
353
- num_factors=int(np.sum(r)),
354
- threshold=threshold,
355
- max_iter=max_iter,
356
- nan_method=nan_method,
357
- nan_k=nan_k
358
- )
359
-
360
- # Run EM algorithm
361
- model.fit_em(X_torch, Mx=Mx, Wx=Wx)
362
-
363
- # Extract results
364
- Res = model.get_result()
365
-
366
- # Display diagnostic tables if debug logging is enabled
367
- if _logger.isEnabledFor(logging.DEBUG):
368
- _display_dfm_tables(Res, config, nQ)
369
-
370
- return Res
371
-