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 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.6"
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
- 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
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
- 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
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
- # 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)
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
- # Only normalize if there are no slower-frequency series
1255
- # (to avoid violating tent weight constraints)
1256
- for j in range(num_clock_factors):
1257
- norm = np.linalg.norm(C_new[:, j])
1258
- if norm > 1e-8:
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)
1265
- C_new[:, j] = C_new[:, j] / norm
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)
1273
- for k in range(Q_new.shape[0]):
1274
- if k != j:
1275
- Q_new[j, k] = Q_new[j, k] * norm_clipped
1276
- Q_new[k, j] = Q_new[k, j] * norm_clipped
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)
@@ -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
- 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)
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
- if np.isfinite(det):
615
- return float(det)
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.6
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.6
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.6)
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.6}
769
+ version = {0.2.7}
762
770
  }
763
771
  ```
764
772
 
@@ -1,4 +1,4 @@
1
- dfm_python/__init__.py,sha256=arYgbDHYx9jgEjCZ1JMKZO4cZewBc1j-iXi_g6SDu4o,5589
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=nNLL179l_SYSowxtnZBHFpEDR_54FOU_tNyn8XaVwnU,72357
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=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,,
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,,