dfm-python 0.2.6__py3-none-any.whl → 0.2.7__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 +83 -77
- dfm_python/core/numeric.py +7 -7
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.7.dist-info}/METADATA +12 -4
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.7.dist-info}/RECORD +7 -7
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.7.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.6.dist-info → dfm_python-0.2.7.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.7"
|
|
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
|
@@ -809,8 +809,8 @@ def em_step(params: EMStepParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
|
|
|
809
809
|
lambda_ridge = (trace_val / size_val) * 1e-5 # Scale factor
|
|
810
810
|
EZZ_lag_sub = EZZ_lag_sub + lambda_ridge * np.eye(size_val)
|
|
811
811
|
# Recompute condition number after regularization
|
|
812
|
-
|
|
813
|
-
|
|
812
|
+
eigenvals = np.linalg.eigvals(EZZ_lag_sub)
|
|
813
|
+
cond_num = (np.max(eigenvals) / max(np.min(eigenvals), 1e-12)) if np.max(eigenvals) > 0 else 1.0
|
|
814
814
|
EZZ_lag_inv = pinv(EZZ_lag_sub, rcond=1e-8) if cond_num > 1e12 else inv(EZZ_lag_sub)
|
|
815
815
|
except _NUMERICAL_EXCEPTIONS:
|
|
816
816
|
EZZ_lag_inv = pinv(EZZ_lag_sub)
|
|
@@ -949,55 +949,55 @@ def em_step(params: EMStepParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
|
|
|
949
949
|
# Keep C fixed for idio components (not updated in M-step)
|
|
950
950
|
else:
|
|
951
951
|
# Fallback to old approach for backward compatibility
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
952
|
+
i_subset = slice(idio_start_idx, idio_start_idx + n_idio)
|
|
953
|
+
i_subset_slice = slice(i_subset.start, i_subset.stop)
|
|
954
|
+
Z_idio = Zsmooth[i_subset_slice, 1:] if idio_start_idx < Zsmooth.shape[1] else np.zeros((n_idio, T))
|
|
955
|
+
n_idio_actual = Z_idio.shape[0] if Z_idio.shape[0] > 0 else n_idio
|
|
956
|
+
expected_idio_current_sq = np.sum(Z_idio**2, axis=1)
|
|
957
|
+
vsmooth_idio_block = vsmooth[i_subset_slice, :, :][:, i_subset_slice, 1:]
|
|
958
|
+
vsmooth_idio_sum = np.sum(vsmooth_idio_block, axis=2)
|
|
959
|
+
if vsmooth_idio_sum.ndim == 2 and vsmooth_idio_sum.shape[0] >= n_idio_actual and vsmooth_idio_sum.shape[1] >= n_idio_actual:
|
|
960
|
+
vsmooth_idio_diag = np.diag(vsmooth_idio_sum[:n_idio_actual, :n_idio_actual])
|
|
961
|
+
else:
|
|
962
|
+
vsmooth_idio_diag = np.zeros(n_idio_actual)
|
|
963
|
+
expected_idio_current_sq = expected_idio_current_sq + vsmooth_idio_diag
|
|
964
|
+
Z_idio_lag = Zsmooth[i_subset_slice, :-1]
|
|
965
|
+
expected_idio_lag_sq = np.sum(Z_idio_lag**2, axis=1)
|
|
966
|
+
vsmooth_lag_block = vsmooth[i_subset_slice, :, :][:, i_subset_slice, :-1]
|
|
967
|
+
vsmooth_lag_sum = np.sum(vsmooth_lag_block, axis=2)
|
|
968
|
+
if vsmooth_lag_sum.ndim == 2 and vsmooth_lag_sum.shape[0] >= n_idio_actual and vsmooth_lag_sum.shape[1] >= n_idio_actual:
|
|
969
|
+
vsmooth_lag_diag = np.diag(vsmooth_lag_sum[:n_idio_actual, :n_idio_actual])
|
|
970
|
+
else:
|
|
971
|
+
vsmooth_lag_diag = np.zeros(n_idio_actual)
|
|
972
|
+
expected_idio_lag_sq = expected_idio_lag_sq + vsmooth_lag_diag
|
|
973
|
+
min_cols = min(Z_idio.shape[1], Z_idio_lag.shape[1])
|
|
974
|
+
expected_idio_cross = np.sum(Z_idio[:, :min_cols] * Z_idio_lag[:, :min_cols], axis=1)
|
|
975
|
+
vvsmooth_block = vvsmooth[i_subset_slice, :, :][:, i_subset_slice, :]
|
|
976
|
+
vvsmooth_sum = np.sum(vvsmooth_block, axis=2)
|
|
977
|
+
if vvsmooth_sum.ndim == 2 and vvsmooth_sum.shape[0] >= n_idio_actual and vvsmooth_sum.shape[1] >= n_idio_actual:
|
|
978
|
+
vvsmooth_diag = np.diag(vvsmooth_sum[:n_idio_actual, :n_idio_actual])
|
|
979
|
+
else:
|
|
980
|
+
vvsmooth_diag = np.zeros(n_idio_actual)
|
|
981
|
+
expected_idio_cross = expected_idio_cross + vvsmooth_diag
|
|
982
|
+
ar_coeffs_diag, _ = _estimate_ar_coefficient(expected_idio_cross, expected_idio_lag_sq, vsmooth_sum=vsmooth_lag_diag)
|
|
983
|
+
A_block_idio = np.diag(ar_coeffs_diag)
|
|
984
|
+
innovation_cov_diag = (np.maximum(expected_idio_current_sq, 0.0) - ar_coeffs_diag * expected_idio_cross) / T
|
|
985
|
+
innovation_cov_diag = np.maximum(innovation_cov_diag, MIN_INNOVATION_VARIANCE)
|
|
986
|
+
Q_block_idio = np.diag(innovation_cov_diag)
|
|
987
|
+
i_subset_size = i_subset.stop - i_subset.start
|
|
988
|
+
if n_idio_actual == i_subset_size:
|
|
989
|
+
A_new[i_subset, i_subset] = A_block_idio
|
|
990
|
+
Q_new[i_subset, i_subset] = Q_block_idio
|
|
991
|
+
elif n_idio_actual < i_subset_size:
|
|
992
|
+
A_new[i_subset.start:i_subset.start + n_idio_actual, i_subset.start:i_subset.start + n_idio_actual] = A_block_idio
|
|
993
|
+
Q_new[i_subset.start:i_subset.start + n_idio_actual, i_subset.start:i_subset.start + n_idio_actual] = Q_block_idio
|
|
994
|
+
else:
|
|
995
|
+
A_new[i_subset, i_subset] = A_block_idio[:i_subset_size, :i_subset_size]
|
|
996
|
+
Q_new[i_subset, i_subset] = Q_block_idio[:i_subset_size, :i_subset_size]
|
|
997
|
+
vsmooth_sub = vsmooth[i_subset_slice, :, :][:, i_subset_slice, 0]
|
|
998
|
+
vsmooth_diag = np.diag(vsmooth_sub[:n_idio_actual, :n_idio_actual]) if vsmooth_sub.ndim == 2 else np.zeros(n_idio_actual)
|
|
999
|
+
for idx in range(min(n_idio_actual, i_subset_size)):
|
|
1000
|
+
V_0_new[i_subset.start + idx, i_subset.start + idx] = vsmooth_diag[idx] if idx < len(vsmooth_diag) else 0.0
|
|
1001
1001
|
|
|
1002
1002
|
Z_0 = Zsmooth[0, :].copy()
|
|
1003
1003
|
nanY = np.isnan(y)
|
|
@@ -1247,33 +1247,39 @@ def em_step(params: EMStepParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
|
|
|
1247
1247
|
# Q cap to prevent explosion: maximum allowed innovation variance for factors
|
|
1248
1248
|
Q_cap_factor = safe_get_attr(config, "max_innovation_variance", 1.0) if config is not None else 1.0
|
|
1249
1249
|
|
|
1250
|
-
#
|
|
1251
|
-
#
|
|
1252
|
-
# (
|
|
1250
|
+
# Normalize C for clock-frequency factors to prevent scale issues
|
|
1251
|
+
# Only normalize if C norm is in reasonable range (not too small, not too large)
|
|
1252
|
+
# This prevents Q from becoming too small (when C norm is small) or too large (when C norm is large)
|
|
1253
1253
|
if num_clock_factors > 0:
|
|
1254
|
-
#
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
Q_new[j,
|
|
1276
|
-
|
|
1254
|
+
# Check if there are slower-frequency series (tent weight constraints)
|
|
1255
|
+
has_slower_freq = (frequencies is not None and
|
|
1256
|
+
any(freq != clock for freq in frequencies if freq is not None))
|
|
1257
|
+
|
|
1258
|
+
# Only normalize if no slower-frequency series (to avoid violating tent weight constraints)
|
|
1259
|
+
if not has_slower_freq:
|
|
1260
|
+
for j in range(num_clock_factors):
|
|
1261
|
+
norm = np.linalg.norm(C_new[:, j])
|
|
1262
|
+
if norm > 1e-8:
|
|
1263
|
+
# Only normalize if norm is in reasonable range
|
|
1264
|
+
# Too small norm (< 0.1) would make Q too small
|
|
1265
|
+
# Too large norm (> 10) would make Q too large
|
|
1266
|
+
C_norm_min = 0.1 # Minimum norm to normalize
|
|
1267
|
+
C_norm_cap = safe_get_attr(config, "max_loading_norm", 10.0) if config is not None else 10.0
|
|
1268
|
+
|
|
1269
|
+
if C_norm_min <= norm <= C_norm_cap:
|
|
1270
|
+
# Normalize C column
|
|
1271
|
+
C_new[:, j] = C_new[:, j] / norm
|
|
1272
|
+
|
|
1273
|
+
# Adjust Q to preserve variance: Q[j,j] *= norm^2
|
|
1274
|
+
Q_adjustment = norm ** 2
|
|
1275
|
+
Q_new[j, j] = Q_new[j, j] * Q_adjustment
|
|
1276
|
+
|
|
1277
|
+
# Off-diagonal: Q[j,k] and Q[k,j] *= norm (for k != j)
|
|
1278
|
+
for k in range(Q_new.shape[0]):
|
|
1279
|
+
if k != j:
|
|
1280
|
+
Q_new[j, k] = Q_new[j, k] * norm
|
|
1281
|
+
Q_new[k, j] = Q_new[k, j] * norm
|
|
1282
|
+
# If norm is too small or too large, skip normalization to preserve stability
|
|
1277
1283
|
|
|
1278
1284
|
# Re-apply Q floor and cap after C normalization to prevent extreme values
|
|
1279
1285
|
# C normalization can make Q very large (if C norm was large) or very small (if C norm was small)
|
dfm_python/core/numeric.py
CHANGED
|
@@ -605,14 +605,14 @@ def _safe_determinant(M: np.ndarray, use_logdet: bool = True) -> float:
|
|
|
605
605
|
if not np.isfinite(log_det) or sign <= 0:
|
|
606
606
|
return 0.0
|
|
607
607
|
# Avoid overflow in exp
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
613
|
det = np.exp(log_det)
|
|
614
|
-
|
|
615
|
-
|
|
614
|
+
if np.isfinite(det):
|
|
615
|
+
return float(det)
|
|
616
616
|
except Exception:
|
|
617
617
|
pass
|
|
618
618
|
except (np.linalg.LinAlgError, ValueError, OverflowError, RuntimeWarning):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
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,20 @@ Demonstrates:
|
|
|
709
709
|
|
|
710
710
|
## Project Status
|
|
711
711
|
|
|
712
|
-
**Version**: 0.2.
|
|
712
|
+
**Version**: 0.2.7
|
|
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.7)
|
|
718
|
+
|
|
719
|
+
- ✅ **Fixed C normalization issue**: C normalization now only applies when C norm is in reasonable range (0.1 ~ 10)
|
|
720
|
+
- Prevents Q from becoming too small when C norm is very small
|
|
721
|
+
- Prevents factor values from becoming extreme
|
|
722
|
+
- ✅ **Improved stability**: Conditional C normalization prevents pathological factor paths
|
|
723
|
+
- ✅ **Better diagnostics**: Added variance guard binding checks in tutorial
|
|
724
|
+
|
|
725
|
+
### Previous Improvements (v0.2.6)
|
|
718
726
|
|
|
719
727
|
- ✅ **Q matrix cap**: Factor innovation variances bounded above by 1.0 to prevent explosion
|
|
720
728
|
- ✅ **R matrix cap**: Observation error variances bounded above by 10.0 for standardized data
|
|
@@ -758,7 +766,7 @@ If you use `dfm-python` in your research, please cite:
|
|
|
758
766
|
author = {DFM Python Contributors},
|
|
759
767
|
year = {2025},
|
|
760
768
|
url = {https://pypi.org/project/dfm-python/},
|
|
761
|
-
version = {0.2.
|
|
769
|
+
version = {0.2.7}
|
|
762
770
|
}
|
|
763
771
|
```
|
|
764
772
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
dfm_python/__init__.py,sha256=
|
|
1
|
+
dfm_python/__init__.py,sha256=YYhRgV773DAFbaTrfF6LVF45rmbR8SpsH6LOYtDAHnU,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=ltMVW1izAVmMwwYKf3GIFLAP-nLAgGy9zdOenuK-SiU,72509
|
|
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=__uwDgRj3TV4S0vXhNc_93kk8U6ewtW8smk0OBZLbdg,26403
|
|
15
|
+
dfm_python-0.2.7.dist-info/METADATA,sha256=ZeeKWsk4BkRbihDNjkLkOq-DofODB9RhwG7dxMRIbQI,28599
|
|
16
|
+
dfm_python-0.2.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
dfm_python-0.2.7.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
|
|
18
|
+
dfm_python-0.2.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|