dfm-python 0.2.5__py3-none-any.whl → 0.2.6__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
@@ -50,7 +50,7 @@ Example (Low-level API - For advanced usage):
50
50
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
51
51
  """
52
52
 
53
- __version__ = "0.2.5"
53
+ __version__ = "0.2.6"
54
54
 
55
55
  from .config import DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME
56
56
  from .config import (
dfm_python/core/em.py CHANGED
@@ -368,7 +368,20 @@ def init_conditions(x, r, p, blocks, opt_nan, Rcon, q, nQ, i_idio, clock='m', te
368
368
  # Store loadings (only up to available columns in C_block)
369
369
  n_loadings = min(pC_freq * r_i, C_block.shape[1])
370
370
  if n_loadings > 0:
371
- C_block[j, :n_loadings] = loadings[:n_loadings]
371
+ # Clip initial loadings to prevent extreme values
372
+ # This helps prevent Q explosion during C normalization
373
+ max_loading_norm = safe_get_attr(config, "max_loading_norm", 10.0) if config is not None else 10.0
374
+ # Clip loadings: if norm is too large, scale down
375
+ loadings_to_store = loadings[:n_loadings]
376
+ if loadings_to_store.ndim == 1:
377
+ # Vector case: clip by norm
378
+ loading_norm = np.linalg.norm(loadings_to_store)
379
+ if loading_norm > max_loading_norm:
380
+ loadings_to_store = loadings_to_store * (max_loading_norm / loading_norm)
381
+ else:
382
+ # Multi-dimensional: clip each element
383
+ loadings_to_store = np.clip(loadings_to_store, -max_loading_norm, max_loading_norm)
384
+ C_block[j, :n_loadings] = loadings_to_store
372
385
  except Exception:
373
386
  # If constrained LS fails, use unconstrained (fallback)
374
387
  try:
@@ -377,7 +390,19 @@ def init_conditions(x, r, p, blocks, opt_nan, Rcon, q, nQ, i_idio, clock='m', te
377
390
  loadings = gram_inv @ factor_projection_clean.T @ series_data_clean
378
391
  n_loadings = min(pC_freq * r_i, C_block.shape[1])
379
392
  if n_loadings > 0:
380
- C_block[j, :n_loadings] = loadings[:n_loadings]
393
+ # Clip initial loadings to prevent extreme values
394
+ max_loading_norm = safe_get_attr(config, "max_loading_norm", 10.0) if config is not None else 10.0
395
+ # Clip loadings: if norm is too large, scale down
396
+ loadings_to_store = loadings[:n_loadings]
397
+ if loadings_to_store.ndim == 1:
398
+ # Vector case: clip by norm
399
+ loading_norm = np.linalg.norm(loadings_to_store)
400
+ if loading_norm > max_loading_norm:
401
+ loadings_to_store = loadings_to_store * (max_loading_norm / loading_norm)
402
+ else:
403
+ # Multi-dimensional: clip each element
404
+ loadings_to_store = np.clip(loadings_to_store, -max_loading_norm, max_loading_norm)
405
+ C_block[j, :n_loadings] = loadings_to_store
381
406
  except Exception:
382
407
  pass # Leave as zeros
383
408
  except Exception:
@@ -1180,6 +1205,12 @@ def em_step(params: EMStepParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
1180
1205
  # Hard floor: enforce min diagonal on R (≥ 1e-8)
1181
1206
  idio_min_var = getattr(config, 'idio_min_var', 1e-8) if config is not None else 1e-8
1182
1207
  R_diag = np.maximum(R_diag, idio_min_var)
1208
+
1209
+ # R cap to prevent explosion: maximum allowed observation error variance
1210
+ # For standardized data, R should typically be < 10
1211
+ R_cap = safe_get_attr(config, "max_observation_variance", 10.0) if config is not None else 10.0
1212
+ R_diag = np.minimum(R_diag, R_cap)
1213
+
1183
1214
  R_new = np.diag(R_diag)
1184
1215
 
1185
1216
  # Final stability operations
@@ -1212,22 +1243,55 @@ def em_step(params: EMStepParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
1212
1243
  has_slower_freq = (frequencies is not None and
1213
1244
  any(freq != clock for freq in frequencies if freq is not None))
1214
1245
  num_clock_factors = int(np.sum(r)) # Only clock-frequency factors (ppC=1 part)
1215
- if num_clock_factors > 0 and not has_slower_freq:
1246
+
1247
+ # Q cap to prevent explosion: maximum allowed innovation variance for factors
1248
+ Q_cap_factor = safe_get_attr(config, "max_innovation_variance", 1.0) if config is not None else 1.0
1249
+
1250
+ # Always normalize C for clock-frequency factors to prevent scale issues
1251
+ # Even if there are slower-frequency series, we can still normalize clock-frequency factors
1252
+ # (tent weight constraints only apply to slower-frequency parts, not clock-frequency parts)
1253
+ if num_clock_factors > 0:
1216
1254
  # Only normalize if there are no slower-frequency series
1217
1255
  # (to avoid violating tent weight constraints)
1218
1256
  for j in range(num_clock_factors):
1219
1257
  norm = np.linalg.norm(C_new[:, j])
1220
1258
  if norm > 1e-8:
1221
- # Normalize C column
1259
+ # Improved normalization: clip C norm before normalization to prevent Q explosion
1260
+ # If C norm is too large, we cap it to prevent Q from becoming extreme
1261
+ C_norm_cap = safe_get_attr(config, "max_loading_norm", 10.0) if config is not None else 10.0
1262
+ norm_clipped = min(norm, C_norm_cap)
1263
+
1264
+ # Normalize C column (using clipped norm to prevent extreme Q)
1222
1265
  C_new[:, j] = C_new[:, j] / norm
1223
- # Adjust Q to preserve variance
1224
- # Diagonal: Q[j,j] *= norm^2
1225
- Q_new[j, j] = Q_new[j, j] * (norm ** 2)
1226
- # Off-diagonal: Q[j,k] and Q[k,j] *= norm (for k != j)
1266
+
1267
+ # Adjust Q to preserve variance, but use clipped norm to prevent explosion
1268
+ # This is a compromise: we normalize C fully but limit Q adjustment
1269
+ Q_adjustment = norm_clipped ** 2
1270
+ Q_new[j, j] = Q_new[j, j] * Q_adjustment
1271
+
1272
+ # Off-diagonal: Q[j,k] and Q[k,j] *= norm_clipped (for k != j)
1227
1273
  for k in range(Q_new.shape[0]):
1228
1274
  if k != j:
1229
- Q_new[j, k] = Q_new[j, k] * norm
1230
- Q_new[k, j] = Q_new[k, j] * norm
1275
+ Q_new[j, k] = Q_new[j, k] * norm_clipped
1276
+ Q_new[k, j] = Q_new[k, j] * norm_clipped
1277
+
1278
+ # Re-apply Q floor and cap after C normalization to prevent extreme values
1279
+ # C normalization can make Q very large (if C norm was large) or very small (if C norm was small)
1280
+ Q_diag_after_norm = np.diag(Q_new).copy()
1281
+ if num_factors > 0:
1282
+ # Apply both floor and cap to factors after normalization
1283
+ Q_diag_after_norm[:num_factors] = np.clip(
1284
+ Q_diag_after_norm[:num_factors],
1285
+ Q_min_factor, # Floor: 0.01
1286
+ Q_cap_factor # Cap: 1.0 (configurable)
1287
+ )
1288
+ # Preserve idio components (only floor, no cap needed)
1289
+ if num_factors < len(Q_diag_after_norm):
1290
+ Q_diag_after_norm[num_factors:] = np.maximum(Q_diag_after_norm[num_factors:], idio_min_var)
1291
+ else:
1292
+ Q_diag_after_norm = np.maximum(Q_diag_after_norm, idio_min_var)
1293
+ # Update Q with re-applied floor and cap
1294
+ Q_new = np.diag(Q_diag_after_norm) + (Q_new - np.diag(np.diag(Q_new))) # Preserve off-diagonal
1231
1295
 
1232
1296
  # Spectral radius cap on A (< 0.99) with minimal shrinkage
1233
1297
  eigenvals_A = np.linalg.eigvals(A_new)
@@ -598,20 +598,23 @@ def _safe_determinant(M: np.ndarray, use_logdet: bool = True) -> float:
598
598
  if np.isfinite(det) and det > 0:
599
599
  return float(det)
600
600
  except np.linalg.LinAlgError:
601
- # Not PSD, use LU decomposition
602
- P, L, U = np.linalg.lu(M)
603
- log_det = np.sum(np.log(np.abs(np.diag(U))))
604
- # Check if log_det is too large to avoid overflow in exp
605
- if log_det > 700:
606
- _logger.debug("_safe_determinant: log_det too large, returning 0.0")
607
- return 0.0
608
- # Account for permutation sign
609
- sign = np.linalg.det(P) if P.shape[0] <= 10 else 1.0
610
- with warnings.catch_warnings():
611
- warnings.filterwarnings('ignore', category=RuntimeWarning)
612
- det = sign * np.exp(log_det)
613
- if np.isfinite(det):
614
- return float(det)
601
+ # Not PSD: fall back to slogdet for general matrices
602
+ try:
603
+ sign, log_det = np.linalg.slogdet(M)
604
+ # If determinant is non-positive or invalid, return 0.0
605
+ if not np.isfinite(log_det) or sign <= 0:
606
+ return 0.0
607
+ # Avoid overflow in exp
608
+ if log_det > 700:
609
+ _logger.debug("_safe_determinant: log_det too large, returning 0.0")
610
+ return 0.0
611
+ with warnings.catch_warnings():
612
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
613
+ det = np.exp(log_det)
614
+ if np.isfinite(det):
615
+ return float(det)
616
+ except Exception:
617
+ pass
615
618
  except (np.linalg.LinAlgError, ValueError, OverflowError, RuntimeWarning):
616
619
  pass
617
620
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.2.5
3
+ Version: 0.2.6
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
@@ -709,12 +709,21 @@ Demonstrates:
709
709
 
710
710
  ## Project Status
711
711
 
712
- **Version**: 0.2.5
712
+ **Version**: 0.2.6
713
713
  **Status**: Stable and production-ready
714
714
  **Python**: 3.10+
715
715
  **PyPI**: https://pypi.org/project/dfm-python/
716
716
 
717
- ### Recent Improvements (v0.2.5)
717
+ ### Recent Improvements (v0.2.6)
718
+
719
+ - ✅ **Q matrix cap**: Factor innovation variances bounded above by 1.0 to prevent explosion
720
+ - ✅ **R matrix cap**: Observation error variances bounded above by 10.0 for standardized data
721
+ - ✅ **Improved C normalization**: C norm clipping prevents Q explosion during normalization
722
+ - ✅ **Enhanced initial values**: PCA loadings clipped to prevent extreme initial values
723
+ - ✅ **Bug fix**: Fixed `np.linalg.lu` error by using `np.linalg.slogdet` for determinant computation
724
+ - ✅ **Better stability**: Q and R caps ensure model convergence even with challenging data
725
+
726
+ ### Previous Improvements (v0.2.5)
718
727
 
719
728
  - ✅ **Q matrix floor**: Factor innovation variances bounded below by 0.01 to prevent scale issues
720
729
  - ✅ **C matrix normalization**: Clock-frequency factor loadings normalized (||C[:,j]|| = 1) for scale stability
@@ -749,7 +758,7 @@ If you use `dfm-python` in your research, please cite:
749
758
  author = {DFM Python Contributors},
750
759
  year = {2025},
751
760
  url = {https://pypi.org/project/dfm-python/},
752
- version = {0.2.5}
761
+ version = {0.2.6}
753
762
  }
754
763
  ```
755
764
 
@@ -1,4 +1,4 @@
1
- dfm_python/__init__.py,sha256=8_xxzeEMbtdKxcjH48IytqGh3uSaGJnl8Q8JnCFFLhs,5589
1
+ dfm_python/__init__.py,sha256=arYgbDHYx9jgEjCZ1JMKZO4cZewBc1j-iXi_g6SDu4o,5589
2
2
  dfm_python/api.py,sha256=-mmV7aaXOAqVW9HPhTEut7T6wN1EyW0rS3J0y-z-MRs,16221
3
3
  dfm_python/config.py,sha256=bBiKfoETohNVJ3brRbY1330C1nQROybCR3co9iagIKs,46978
4
4
  dfm_python/config_sources.py,sha256=6LTK7k5WJpnayMmtxtYwZ9SQi_hGXoz-Vg4sRGTabI0,21798
@@ -9,10 +9,10 @@ dfm_python/news.py,sha256=iXbJnsvH5FgKToPzKnqIpTosrZu-QHXBNHjsqwMYrHo,32583
9
9
  dfm_python/utils.py,sha256=qOAJm2KG71yP2a29iJVDGOC-ZUS_HPL5oAo16EgNXA4,15709
10
10
  dfm_python/core/__init__.py,sha256=oDrhAoc1W_ODohgufQ-gN0uri8-vZI0EXsF0dysth_A,331
11
11
  dfm_python/core/diagnostics.py,sha256=2Cm8XIN5jE9jL3FssEUmZNQy12FPuKvYs1k3bYKAySw,20224
12
- dfm_python/core/em.py,sha256=Y1kICJfdNyAWsWYxxJANp4TAiQ__iVR5wwhNw6h0qC0,67571
12
+ dfm_python/core/em.py,sha256=nNLL179l_SYSowxtnZBHFpEDR_54FOU_tNyn8XaVwnU,72357
13
13
  dfm_python/core/helpers.py,sha256=XJvWWEZlUqozBfFNcENKgbOOFkwlm-tGwO-u0LnAf-E,31541
14
- dfm_python/core/numeric.py,sha256=CsufWu5feWdRjjrhGL7pKxwq5CFkaBn-1l3t1v35YQI,26323
15
- dfm_python-0.2.5.dist-info/METADATA,sha256=rmEAZQ0C4RPO7h4G3JJhu6Kq1itjD4dif_aH_2O121w,27552
16
- dfm_python-0.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- dfm_python-0.2.5.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
18
- dfm_python-0.2.5.dist-info/RECORD,,
14
+ dfm_python/core/numeric.py,sha256=IcIwcKtMhIoJ5-7Mz6UNYwTUtEa5W_ZMnQtF87v0SSc,26431
15
+ dfm_python-0.2.6.dist-info/METADATA,sha256=DnwocbXu0WC8ruBF1GSU2PQINCNSYjxMlGrQlRXVWs0,28159
16
+ dfm_python-0.2.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ dfm_python-0.2.6.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
18
+ dfm_python-0.2.6.dist-info/RECORD,,