SuperModelingFactory 0.6.6__tar.gz → 0.6.8__tar.gz

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.
Files changed (116) hide show
  1. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/Feature_Screen.py +199 -5
  2. supermodelingfactory-0.6.8/Modeling_Tool/Feature/Screen_Gates.py +491 -0
  3. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/Weighted_Screen.py +234 -13
  4. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/LRM_Tool.py +31 -1
  5. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/LRM_Tool.pyi +1 -0
  6. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/_common.py +15 -0
  7. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/credit_model.py +80 -0
  8. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/feature_validation.py +297 -6
  9. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/field_meta.py +41 -0
  10. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/sample_analysis.py +188 -19
  11. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/screening_artifact.py +46 -0
  12. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +489 -55
  13. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +9 -4
  14. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/__init__.py +1 -1
  15. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/PKG-INFO +2 -2
  16. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/README.md +1 -1
  17. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  18. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/SuperModelingFactory.egg-info/SOURCES.txt +1 -0
  19. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/pyproject.toml +1 -1
  20. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/setup.py +1 -1
  21. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/ExcelMaster/ExcelFormatTool.py +0 -0
  22. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/ExcelMaster/ExcelMaster.py +0 -0
  23. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/ExcelMaster/Template.py +0 -0
  24. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/ExcelMaster/Utility.py +0 -0
  25. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/ExcelMaster/__init__.py +0 -0
  26. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/LICENSE +0 -0
  27. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/MANIFEST.in +0 -0
  28. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  29. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  30. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  31. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  32. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  33. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  34. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  35. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  36. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  37. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  38. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  39. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  40. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  41. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/__init__.py +0 -0
  42. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/kDataFrame.py +0 -0
  43. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  44. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  45. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Core/utils.py +0 -0
  46. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  47. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  48. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  49. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  50. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/__init__.py +0 -0
  51. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  52. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  53. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  54. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  55. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  56. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Explainability/__init__.py +0 -0
  57. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  58. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  59. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  60. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  61. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/ODPS_Distribution_Tool.py +0 -0
  62. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/ODPS_Distribution_Tool.pyi +0 -0
  63. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  64. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  65. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  66. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Feature/__init__.py +0 -0
  67. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  68. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  69. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  70. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  71. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  72. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Model/__init__.py +0 -0
  73. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/__init__.py +0 -0
  74. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  75. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  76. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  77. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  78. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  79. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  80. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  81. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  82. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  83. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  84. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  85. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/Sample/__init__.py +0 -0
  86. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  87. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/UAT/__init__.py +0 -0
  88. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  89. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  90. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  91. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  92. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  93. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  94. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  95. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  96. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  97. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  98. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/__init__.py +0 -0
  99. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  100. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  101. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/_utils/__init__.py +0 -0
  102. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/_utils/nan_guard.py +0 -0
  103. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/_utils/robust.py +0 -0
  104. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/_utils/sentinels.py +0 -0
  105. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  106. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  107. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/ref_font/__init__.py +0 -0
  108. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  109. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Report/Report_Tool.py +0 -0
  110. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/Report/__init__.py +0 -0
  111. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  112. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  113. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/SuperModelingFactory.egg-info/requires.txt +0 -0
  114. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  115. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/requirements.txt +0 -0
  116. {supermodelingfactory-0.6.6 → supermodelingfactory-0.6.8}/setup.cfg +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import warnings
5
6
  from dataclasses import dataclass, field
6
7
  from pathlib import Path
7
8
  from typing import Any, Literal, Mapping
@@ -14,16 +15,20 @@ from Modeling_Tool._utils.sentinels import SMF_MISSING_BIN
14
15
 
15
16
  from .Weighted_Screen import (
16
17
  WeightedScreenResult,
18
+ _DROPPED_DETAIL_COLS,
17
19
  _apply_missing_rate_stage,
18
20
  _apply_stage_keep,
19
21
  _corr_dedup_weighted,
22
+ _gate_ranking_iv_map,
23
+ _iv_band_keep,
20
24
  _legacy_unweighted_screen,
21
25
  _psi_from_distributions,
22
26
  _resolve_splits,
23
27
  _summary_row,
28
+ _unit_weight_floored_iv,
24
29
  _weighted_bin_distribution,
25
30
  _weighted_corr_for_screen,
26
- _weighted_iv_from_assigned_bins,
31
+ _weighted_iv_detail,
27
32
  _weighted_screen_impl,
28
33
  )
29
34
 
@@ -37,6 +42,14 @@ _MONOTONE_INIT_KEYS = frozenset({
37
42
  "missing_woe",
38
43
  "special_values",
39
44
  "bin_label_decimals",
45
+ "min_bad_count",
46
+ "min_good_count",
47
+ "small_bin_policy",
48
+ "monotone_direction",
49
+ "reference_target",
50
+ "direction_conflict_policy",
51
+ "missing_bin_strategy",
52
+ "refine_min_n_bins_policy",
40
53
  })
41
54
  _MONOTONE_FIT_KEYS = frozenset({"chi2_binning", "chi2_p", "chi2_init_size", "n_jobs"})
42
55
 
@@ -50,6 +63,7 @@ class FeatureScreenConfig:
50
63
  psi_use_woe_bins: bool = False
51
64
  iv_enabled: bool = True
52
65
  iv_threshold: float = 0.02
66
+ iv_upper_threshold: float | None = None
53
67
  iv_bins: int = 10
54
68
  iv_min_bin_prop: float = 0.05
55
69
  iv_equal_freq: bool = True
@@ -77,6 +91,28 @@ class FeatureScreenConfig:
77
91
  content: float = 1e-6
78
92
  precision: int = 5
79
93
  min_bin_prop: float = 0.05
94
+ # --- Post-corr selection gates (G03/G04/G05/G06); all legacy-off ------
95
+ # G03 group stability (requires SelectionEvidence — FVP only)
96
+ monthly_iv_min: float | None = None
97
+ monthly_iv_cv_max: float | None = None
98
+ direction_consistency_min: float | None = None
99
+ min_group_n: int | None = None
100
+ insufficient_group_policy: Literal["keep_warn", "drop", "raise"] = "keep_warn"
101
+ # G04 multi-target joint gate (requires SelectionEvidence — FVP only)
102
+ target_rules: Literal["all", "any", "min_pass_count"] | None = None
103
+ min_pass_count: int | None = None
104
+ per_target_iv_range: Any = None # (low, high) or {target: (low, high)}
105
+ direction_reference_target: str | None = None
106
+ # G05 hard top-N truncation
107
+ max_selected_features: int | None = None
108
+ min_selected_features: int | None = None
109
+ ranking_metric: str = "iv"
110
+ tie_breaker: str = "name"
111
+ # G06 VIF gate (needs the optional statsmodels extra)
112
+ vif_enabled: bool = False
113
+ vif_threshold: float = 10.0
114
+ vif_min_features: int = 2
115
+ vif_tie_break_metric: str = "iv"
80
116
 
81
117
 
82
118
  def screen_config_from_mapping(
@@ -100,6 +136,9 @@ def screen_config_from_mapping(
100
136
  psi_use_woe_bins=bool(cfg.get("psi_use_woe_bins", False)),
101
137
  iv_enabled=bool(cfg.get("iv_enabled", True)),
102
138
  iv_threshold=float(cfg.get("iv_threshold", 0.02)),
139
+ iv_upper_threshold=(
140
+ float(cfg["iv_upper_threshold"]) if cfg.get("iv_upper_threshold") is not None else None
141
+ ),
103
142
  iv_bins=iv_nbins,
104
143
  iv_min_bin_prop=float(cfg.get("iv_min_bin_prop", 0.05)),
105
144
  iv_equal_freq=bool(cfg.get("iv_equal_freq", True)),
@@ -125,6 +164,23 @@ def screen_config_from_mapping(
125
164
  content=float(cfg.get("content", 1e-6)),
126
165
  precision=int(cfg.get("precision", 5)),
127
166
  min_bin_prop=float(cfg.get("min_bin_prop", cfg.get("iv_min_bin_prop", 0.05))),
167
+ monthly_iv_min=cfg.get("monthly_iv_min"),
168
+ monthly_iv_cv_max=cfg.get("monthly_iv_cv_max"),
169
+ direction_consistency_min=cfg.get("direction_consistency_min"),
170
+ min_group_n=cfg.get("min_group_n"),
171
+ insufficient_group_policy=str(cfg.get("insufficient_group_policy", "keep_warn")), # type: ignore[arg-type]
172
+ target_rules=cfg.get("target_rules"),
173
+ min_pass_count=cfg.get("min_pass_count"),
174
+ per_target_iv_range=cfg.get("per_target_iv_range"),
175
+ direction_reference_target=cfg.get("direction_reference_target"),
176
+ max_selected_features=cfg.get("max_selected_features"),
177
+ min_selected_features=cfg.get("min_selected_features"),
178
+ ranking_metric=str(cfg.get("ranking_metric", "iv")),
179
+ tie_breaker=str(cfg.get("tie_breaker", "name")),
180
+ vif_enabled=bool(cfg.get("vif_enabled", False)),
181
+ vif_threshold=float(cfg.get("vif_threshold", 10.0)),
182
+ vif_min_features=int(cfg.get("vif_min_features", 2)),
183
+ vif_tie_break_metric=str(cfg.get("vif_tie_break_metric", "iv")),
128
184
  )
129
185
 
130
186
 
@@ -213,12 +269,15 @@ def _woe_bins_unweighted_screen(
213
269
  config: FeatureScreenConfig,
214
270
  *,
215
271
  prefit_woe_engine: Any | None = None,
272
+ selection_evidence: Any | None = None,
216
273
  ) -> FeatureScreenResult:
217
274
  from Modeling_Tool import CorrelationFilter, PSICalculator, VarExtractionInsights
218
275
 
219
276
  ins, oos, oot = splits["ins"], splits["oos"], splits["oot"]
220
277
  current = list(feature_cols)
221
278
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, None)]
279
+ dropped_rows: list[dict] = []
280
+ stage_tables: dict[str, pd.DataFrame] = {}
222
281
  current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
223
282
  ins,
224
283
  current,
@@ -228,6 +287,7 @@ def _woe_bins_unweighted_screen(
228
287
  on_empty_stage=config.on_empty_stage,
229
288
  )
230
289
  binner = _resolve_screening_binner(splits, current, target_col, config, prefit_woe_engine)
290
+ engine_fit_features = list(current)
231
291
 
232
292
  psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
233
293
  if config.psi_enabled:
@@ -264,6 +324,7 @@ def _woe_bins_unweighted_screen(
264
324
  summary_rows.append(_summary_row("psi", n_before, len(current), config.psi_threshold, None))
265
325
 
266
326
  iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
327
+ iv_floored_map: dict[str, float] = {}
267
328
  if config.iv_enabled:
268
329
  use_binner = config.iv_use_woe_bins and binner is not None
269
330
  vi = VarExtractionInsights(
@@ -290,7 +351,20 @@ def _woe_bins_unweighted_screen(
290
351
  )
291
352
  if iv is not None and not iv.empty:
292
353
  iv_table = iv.rename(columns={"iv": "iv_weighted"})[["var", "iv_weighted", "n_bins", "missing_rate"]]
293
- keep = iv.loc[iv["iv"] >= config.iv_threshold, "var"].tolist()
354
+ if config.iv_upper_threshold is not None:
355
+ gate_frame = iv[["var", "iv"]].copy()
356
+ gate_frame["iv_floored"] = _unit_weight_floored_iv(
357
+ ins, list(gate_frame["var"]), target_col,
358
+ iv_bins=config.iv_bins, min_bin_prop=config.iv_min_bin_prop,
359
+ content=config.content,
360
+ )
361
+ iv_floored_map = dict(zip(gate_frame["var"], gate_frame["iv_floored"]))
362
+ keep = _iv_band_keep(
363
+ gate_frame, "iv", config.iv_threshold, config.iv_upper_threshold,
364
+ dropped_rows, upper_col="iv_floored",
365
+ )
366
+ else:
367
+ keep = _iv_band_keep(iv, "iv", config.iv_threshold, config.iv_upper_threshold, dropped_rows)
294
368
  n_before = len(current)
295
369
  current = _apply_stage_keep(
296
370
  current, keep, "iv", summary_rows,
@@ -312,7 +386,23 @@ def _woe_bins_unweighted_screen(
312
386
  current = cf.remove_highly_correlated(current, max_iterations=config.corr_max_iterations)
313
387
  summary_rows.append(_summary_row("corr", n_before, len(current), config.corr_threshold, None))
314
388
 
389
+ from .Screen_Gates import apply_post_corr_gates
390
+
391
+ gate_iv_map = _gate_ranking_iv_map(iv_table, iv_floored_map, config.iv_upper_threshold)
392
+ current = apply_post_corr_gates(
393
+ ins, current, config, selection_evidence, gate_iv_map,
394
+ summary_rows, dropped_rows, stage_tables,
395
+ weight_col=None, on_empty_stage=config.on_empty_stage,
396
+ )
397
+
315
398
  summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, None))
399
+ engine_obj, engine_meta = _screen_engine_payload(
400
+ binner,
401
+ fit_features=engine_fit_features,
402
+ target_col=target_col,
403
+ weight_col=None,
404
+ summary_rows=summary_rows,
405
+ )
316
406
  return FeatureScreenResult(
317
407
  selected_features=list(current),
318
408
  iv_table=iv_table,
@@ -321,6 +411,13 @@ def _woe_bins_unweighted_screen(
321
411
  summary=pd.DataFrame(summary_rows),
322
412
  missing_rate_table=missing_rate_table,
323
413
  missing_rate_dropped=missing_rate_dropped,
414
+ woe_engine=engine_obj,
415
+ woe_engine_meta=engine_meta,
416
+ dropped_detail=(
417
+ pd.DataFrame(dropped_rows, columns=_DROPPED_DETAIL_COLS)
418
+ if dropped_rows else pd.DataFrame(columns=_DROPPED_DETAIL_COLS)
419
+ ),
420
+ stage_tables=stage_tables,
324
421
  )
325
422
 
326
423
 
@@ -330,6 +427,52 @@ def _bins_to_numpy(bins: Any) -> np.ndarray:
330
427
  return np.asarray(bins, dtype=object)
331
428
 
332
429
 
430
+ def _screen_engine_payload(
431
+ binner: Any | None,
432
+ *,
433
+ fit_features: list[str],
434
+ target_col: str,
435
+ weight_col: str | None,
436
+ summary_rows: list[dict],
437
+ ) -> tuple[Any | None, dict[str, Any]]:
438
+ """Build the (woe_engine, woe_engine_meta) result payload (G00).
439
+
440
+ Monotone engines attach as-is — they hold only per-feature bin tables.
441
+ WOE_Master engines are never attached (they retain the full training
442
+ frame at ``self.train_data``); their woe_table + metadata travel instead.
443
+ """
444
+ if binner is None:
445
+ return None, {}
446
+ engine_name = type(binner).__name__
447
+ meta: dict[str, Any] = {
448
+ "engine_name": engine_name,
449
+ "fit_features": list(fit_features),
450
+ "target_col": target_col,
451
+ "weight_col": weight_col,
452
+ "screen_stages": [row.get("stage") for row in summary_rows],
453
+ }
454
+ if engine_name == "WOE_Master":
455
+ from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
456
+
457
+ try:
458
+ meta["woe_table"] = as_woe_engine(binner).get_woe_table(varlist=list(fit_features))
459
+ except Exception as exc:
460
+ meta["woe_table"] = None
461
+ meta["woe_table_error"] = repr(exc)
462
+ meta["engine_attached"] = False
463
+ warnings.warn(
464
+ "screening WOE engine is a WOE_Master, which retains the full "
465
+ "training frame — attaching its woe_table and metadata only. "
466
+ "Use woe_engine='monotone' to get a reusable, serializable "
467
+ "screening engine on the result.",
468
+ UserWarning,
469
+ stacklevel=3,
470
+ )
471
+ return None, meta
472
+ meta["engine_attached"] = True
473
+ return binner, meta
474
+
475
+
333
476
  def _weighted_woe_bins_screen(
334
477
  splits: dict[str, pd.DataFrame],
335
478
  feature_cols: list[str],
@@ -338,6 +481,7 @@ def _weighted_woe_bins_screen(
338
481
  config: FeatureScreenConfig,
339
482
  *,
340
483
  prefit_woe_engine: Any | None = None,
484
+ selection_evidence: Any | None = None,
341
485
  ) -> FeatureScreenResult:
342
486
  from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
343
487
 
@@ -346,6 +490,8 @@ def _weighted_woe_bins_screen(
346
490
  y_ins = ins[target_col].astype(float).to_numpy()
347
491
  current = list(feature_cols)
348
492
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, weight_col)]
493
+ dropped_rows: list[dict] = []
494
+ stage_tables: dict[str, pd.DataFrame] = {}
349
495
  current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
350
496
  ins,
351
497
  current,
@@ -369,6 +515,7 @@ def _weighted_woe_bins_screen(
369
515
  categorical_features=config.categorical_features,
370
516
  )
371
517
  adapter = as_woe_engine(binner)
518
+ engine_fit_features = list(current)
372
519
 
373
520
  # Assign fitted bins once per split. PSI and IV both consume these labels;
374
521
  # doing it here avoids a full WOE transform for every feature and stage.
@@ -420,7 +567,9 @@ def _weighted_woe_bins_screen(
420
567
  psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
421
568
 
422
569
  iv_records: list[dict] = []
570
+ iv_floored_map: dict[str, float] = {}
423
571
  if config.iv_enabled:
572
+ floor_content = config.content if config.iv_upper_threshold is not None else None
424
573
  for var in current:
425
574
  if var not in ins.columns or ins[var].nunique(dropna=False) <= 1:
426
575
  continue
@@ -431,12 +580,16 @@ def _weighted_woe_bins_screen(
431
580
  else:
432
581
  # Categorical/object feature: IV itself is computed from the
433
582
  # already-assigned bins; ``x`` only feeds np.isfinite() inside
434
- # _weighted_iv_from_assigned_bins to derive the weighted
583
+ # _weighted_iv_detail to derive the weighted
435
584
  # missing rate. A hard float cast crashes on string levels
436
585
  # (e.g. '4.高中'), so encode observed -> 1.0 / missing -> NaN,
437
586
  # matching the notna semantics of _missing_rate_for_series.
438
587
  x_ins = np.where(x_series.notna().to_numpy(), 1.0, np.nan)
439
- iv_val, n_b, miss = _weighted_iv_from_assigned_bins(y_ins, w_ins, bins_ins, x_ins)
588
+ iv_val, n_b, miss, iv_floored = _weighted_iv_detail(
589
+ y_ins, w_ins, bins_ins, x_ins, content=floor_content,
590
+ )
591
+ if floor_content is not None:
592
+ iv_floored_map[var] = iv_floored
440
593
  iv_records.append({
441
594
  "var": var,
442
595
  "iv_weighted": iv_val,
@@ -447,7 +600,15 @@ def _weighted_woe_bins_screen(
447
600
  columns=["var", "iv_weighted", "n_bins", "missing_rate"],
448
601
  )
449
602
  if not iv_table.empty:
450
- keep = iv_table.loc[iv_table["iv_weighted"] >= config.iv_threshold, "var"].tolist()
603
+ if config.iv_upper_threshold is not None:
604
+ gate_frame = iv_table[["var", "iv_weighted"]].copy()
605
+ gate_frame["iv_floored"] = gate_frame["var"].map(iv_floored_map)
606
+ keep = _iv_band_keep(
607
+ gate_frame, "iv_weighted", config.iv_threshold,
608
+ config.iv_upper_threshold, dropped_rows, upper_col="iv_floored",
609
+ )
610
+ else:
611
+ keep = _iv_band_keep(iv_table, "iv_weighted", config.iv_threshold, config.iv_upper_threshold, dropped_rows)
451
612
  n_before = len(current)
452
613
  current = _apply_stage_keep(
453
614
  current, keep, "iv", summary_rows,
@@ -479,7 +640,23 @@ def _weighted_woe_bins_screen(
479
640
  )
480
641
  summary_rows.append(_summary_row("corr", n_before, len(current), config.corr_threshold, weight_col))
481
642
 
643
+ from .Screen_Gates import apply_post_corr_gates
644
+
645
+ gate_iv_map = _gate_ranking_iv_map(iv_table, iv_floored_map, config.iv_upper_threshold)
646
+ current = apply_post_corr_gates(
647
+ ins, current, config, selection_evidence, gate_iv_map,
648
+ summary_rows, dropped_rows, stage_tables,
649
+ weight_col=weight_col, on_empty_stage=config.on_empty_stage,
650
+ )
651
+
482
652
  summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, weight_col))
653
+ engine_obj, engine_meta = _screen_engine_payload(
654
+ binner,
655
+ fit_features=engine_fit_features,
656
+ target_col=target_col,
657
+ weight_col=weight_col,
658
+ summary_rows=summary_rows,
659
+ )
483
660
  return FeatureScreenResult(
484
661
  selected_features=list(current),
485
662
  iv_table=iv_table,
@@ -488,6 +665,13 @@ def _weighted_woe_bins_screen(
488
665
  summary=pd.DataFrame(summary_rows),
489
666
  missing_rate_table=missing_rate_table,
490
667
  missing_rate_dropped=missing_rate_dropped,
668
+ woe_engine=engine_obj,
669
+ woe_engine_meta=engine_meta,
670
+ dropped_detail=(
671
+ pd.DataFrame(dropped_rows, columns=_DROPPED_DETAIL_COLS)
672
+ if dropped_rows else pd.DataFrame(columns=_DROPPED_DETAIL_COLS)
673
+ ),
674
+ stage_tables=stage_tables,
491
675
  )
492
676
 
493
677
 
@@ -499,6 +683,7 @@ def feature_screen(
499
683
  weight_col: str | None = None,
500
684
  config: FeatureScreenConfig | None = None,
501
685
  prefit_woe_engine: Any | None = None,
686
+ selection_evidence: Any | None = None,
502
687
  ) -> FeatureScreenResult:
503
688
  """Run PSI -> IV -> correlation screening on pre-split INS/OOS/OOT frames."""
504
689
  cfg = config or FeatureScreenConfig()
@@ -513,6 +698,7 @@ def feature_screen(
513
698
  weight_col,
514
699
  cfg,
515
700
  prefit_woe_engine=prefit_woe_engine,
701
+ selection_evidence=selection_evidence,
516
702
  )
517
703
  return _weighted_screen_impl(
518
704
  splits,
@@ -524,6 +710,7 @@ def feature_screen(
524
710
  psi_compare_splits=list(cfg.psi_compare_splits),
525
711
  iv_enabled=cfg.iv_enabled,
526
712
  iv_threshold=cfg.iv_threshold,
713
+ iv_upper_threshold=cfg.iv_upper_threshold,
527
714
  iv_bins=cfg.iv_bins,
528
715
  min_bin_prop=cfg.min_bin_prop,
529
716
  corr_enabled=cfg.corr_enabled,
@@ -538,6 +725,8 @@ def feature_screen(
538
725
  prefit_woe_engine=prefit_woe_engine,
539
726
  missing_rate_threshold=cfg.missing_rate_threshold,
540
727
  missing_rate_ref=cfg.missing_rate_ref,
728
+ gates_config=cfg,
729
+ selection_evidence=selection_evidence,
541
730
  )
542
731
 
543
732
  if use_woe_bins or prefit_woe_engine is not None:
@@ -547,6 +736,7 @@ def feature_screen(
547
736
  target_col,
548
737
  cfg,
549
738
  prefit_woe_engine=prefit_woe_engine,
739
+ selection_evidence=selection_evidence,
550
740
  )
551
741
 
552
742
  return _legacy_unweighted_screen(
@@ -558,6 +748,7 @@ def feature_screen(
558
748
  psi_compare_splits=list(cfg.psi_compare_splits),
559
749
  iv_enabled=cfg.iv_enabled,
560
750
  iv_threshold=cfg.iv_threshold,
751
+ iv_upper_threshold=cfg.iv_upper_threshold,
561
752
  iv_bins=cfg.iv_bins,
562
753
  iv_min_bin_prop=cfg.iv_min_bin_prop,
563
754
  corr_enabled=cfg.corr_enabled,
@@ -570,6 +761,9 @@ def feature_screen(
570
761
  on_empty_stage=cfg.on_empty_stage,
571
762
  missing_rate_threshold=cfg.missing_rate_threshold,
572
763
  missing_rate_ref=cfg.missing_rate_ref,
764
+ gates_config=cfg,
765
+ selection_evidence=selection_evidence,
766
+ content=cfg.content,
573
767
  )
574
768
 
575
769