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 +1 -1
- dfm_python/core/em.py +74 -10
- dfm_python/core/numeric.py +17 -14
- {dfm_python-0.2.5.dist-info → dfm_python-0.2.6.dist-info}/METADATA +13 -4
- {dfm_python-0.2.5.dist-info → dfm_python-0.2.6.dist-info}/RECORD +7 -7
- {dfm_python-0.2.5.dist-info → dfm_python-0.2.6.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.5.dist-info → dfm_python-0.2.6.dist-info}/licenses/LICENSE +0 -0
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
1224
|
-
#
|
|
1225
|
-
|
|
1226
|
-
|
|
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] *
|
|
1230
|
-
Q_new[k, j] = Q_new[k, j] *
|
|
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)
|
dfm_python/core/numeric.py
CHANGED
|
@@ -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
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
warnings.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
761
|
+
version = {0.2.6}
|
|
753
762
|
}
|
|
754
763
|
```
|
|
755
764
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
dfm_python/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
15
|
-
dfm_python-0.2.
|
|
16
|
-
dfm_python-0.2.
|
|
17
|
-
dfm_python-0.2.
|
|
18
|
-
dfm_python-0.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|