SuperModelingFactory 0.6.5__tar.gz → 0.6.7__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.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/Model_Eval_Tool.py +55 -8
  2. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +1 -1
  3. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/weighted_eval_utils.py +102 -13
  4. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/Feature_Screen.py +169 -2
  5. supermodelingfactory-0.6.7/Modeling_Tool/Feature/Screen_Gates.py +470 -0
  6. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/Weighted_Screen.py +94 -4
  7. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/LRM_Tool.py +31 -1
  8. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/LRM_Tool.pyi +1 -0
  9. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/_common.py +35 -0
  10. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/credit_model.py +191 -8
  11. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/feature_validation.py +256 -6
  12. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/field_meta.py +47 -0
  13. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/sample_analysis.py +188 -19
  14. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/screening_artifact.py +46 -0
  15. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +489 -55
  16. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +9 -4
  17. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/__init__.py +1 -1
  18. {supermodelingfactory-0.6.5/SuperModelingFactory.egg-info → supermodelingfactory-0.6.7}/PKG-INFO +2 -2
  19. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/README.md +1 -1
  20. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7/SuperModelingFactory.egg-info}/PKG-INFO +2 -2
  21. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/SuperModelingFactory.egg-info/SOURCES.txt +1 -0
  22. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/pyproject.toml +1 -1
  23. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/setup.py +1 -1
  24. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/ExcelMaster/ExcelFormatTool.py +0 -0
  25. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/ExcelMaster/ExcelMaster.py +0 -0
  26. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/ExcelMaster/Template.py +0 -0
  27. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/ExcelMaster/Utility.py +0 -0
  28. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/ExcelMaster/__init__.py +0 -0
  29. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/LICENSE +0 -0
  30. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/MANIFEST.in +0 -0
  31. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  32. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  33. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  34. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  35. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  36. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  37. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  38. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  39. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  40. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  41. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  42. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  43. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  44. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/__init__.py +0 -0
  45. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/kDataFrame.py +0 -0
  46. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  47. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  48. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Core/utils.py +0 -0
  49. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  50. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  51. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/__init__.py +0 -0
  52. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  53. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  54. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  55. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  56. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Explainability/__init__.py +0 -0
  57. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  58. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  59. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  60. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  61. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/ODPS_Distribution_Tool.py +0 -0
  62. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/ODPS_Distribution_Tool.pyi +0 -0
  63. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  64. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  65. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  66. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Feature/__init__.py +0 -0
  67. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  68. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  69. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  70. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  71. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  72. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Model/__init__.py +0 -0
  73. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/__init__.py +0 -0
  74. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  75. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  76. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  77. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  78. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  79. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  80. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  81. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  82. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  83. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  84. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  85. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/Sample/__init__.py +0 -0
  86. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  87. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/UAT/__init__.py +0 -0
  88. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  89. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  90. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  91. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  92. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  93. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  94. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  95. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  96. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  97. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  98. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/__init__.py +0 -0
  99. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  100. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  101. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/_utils/__init__.py +0 -0
  102. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/_utils/nan_guard.py +0 -0
  103. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/_utils/robust.py +0 -0
  104. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/_utils/sentinels.py +0 -0
  105. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  106. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  107. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/ref_font/__init__.py +0 -0
  108. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  109. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Report/Report_Tool.py +0 -0
  110. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/Report/__init__.py +0 -0
  111. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  112. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  113. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/SuperModelingFactory.egg-info/requires.txt +0 -0
  114. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  115. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/requirements.txt +0 -0
  116. {supermodelingfactory-0.6.5 → supermodelingfactory-0.6.7}/setup.cfg +0 -0
@@ -2169,7 +2169,7 @@ class PerformanceEvaluator:
2169
2169
  dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05,
2170
2170
  include_missing = False, equal_freq = True, chi2_method = False,
2171
2171
  init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42,
2172
- weight_col = None):
2172
+ weight_col = None, spec_values = None, ascending = None):
2173
2173
  """
2174
2174
  初始化性能评估器。
2175
2175
 
@@ -2225,9 +2225,29 @@ class PerformanceEvaluator:
2225
2225
  self.tree_binning = tree_binning
2226
2226
  self.random_state = random_state
2227
2227
  self.weight_col = weight_col
2228
+ # spec_values: sentinel scores (e.g. -1 for all-missing overrides)
2229
+ # that get their own evaluation bin and are excluded from quantile
2230
+ # edges and ranking metrics. ascending: None keeps every underlying
2231
+ # function's own legacy default; an explicit bool is threaded through
2232
+ # summary/gains/plot and the weighted path uniformly.
2233
+ self.spec_values = list(spec_values) if spec_values else []
2234
+ self.ascending = ascending
2228
2235
  self.datasets = {}
2229
2236
  self.dataset_weight_cols = {}
2230
2237
  self.evaluate_status = None
2238
+
2239
+ def _governed_eval_kwargs(self):
2240
+ """Kwargs to thread spec_values/ascending into underlying eval calls.
2241
+
2242
+ Nothing is passed when unset, so every legacy default stays intact
2243
+ (unweighted summary False, comparison paths True, weighted False).
2244
+ """
2245
+ kwargs = {}
2246
+ if self.spec_values:
2247
+ kwargs["spec_values"] = list(self.spec_values)
2248
+ if self.ascending is not None:
2249
+ kwargs["ascending"] = bool(self.ascending)
2250
+ return kwargs
2231
2251
 
2232
2252
  def add_dataset(self, name, data, weight_col = None, overwrite = False):
2233
2253
  """
@@ -2351,6 +2371,8 @@ class PerformanceEvaluator:
2351
2371
  scr_name,
2352
2372
  weight_col=wc,
2353
2373
  nbins=self.pct_bins,
2374
+ ascending=bool(self.ascending) if self.ascending is not None else False,
2375
+ spec_values=self.spec_values or None,
2354
2376
  )
2355
2377
  )
2356
2378
  fnl_df = pd.DataFrame(rows)
@@ -2368,15 +2390,23 @@ class PerformanceEvaluator:
2368
2390
  if scr_name is None:
2369
2391
  scr_name = "_mdl_scr"
2370
2392
  work_data[scr_name] = self.model.predict_proba(work_data.loc[:, self.feature_cols])[:, 1]
2371
- payload = {
2372
- "y_true": work_data[self.tgt_name],
2373
- "y_score": work_data[scr_name],
2374
- }
2375
2393
  resolved_weight = _weighted_eval.resolve_weights(
2376
2394
  work_data,
2377
2395
  weight_col=wc,
2378
2396
  expected_len=len(work_data),
2379
2397
  )
2398
+ y_true = work_data[self.tgt_name].to_numpy()
2399
+ y_score = work_data[scr_name].to_numpy()
2400
+ if self.spec_values:
2401
+ keep = ~pd.Series(y_score).isin(self.spec_values).to_numpy()
2402
+ y_true = y_true[keep]
2403
+ y_score = y_score[keep]
2404
+ if resolved_weight is not None:
2405
+ resolved_weight = np.asarray(resolved_weight)[keep]
2406
+ payload = {
2407
+ "y_true": y_true,
2408
+ "y_score": y_score,
2409
+ }
2380
2410
  if resolved_weight is not None:
2381
2411
  payload["sample_weight"] = resolved_weight
2382
2412
  eval_datasets[name] = payload
@@ -2445,6 +2475,21 @@ class PerformanceEvaluator:
2445
2475
  return data[self.scr_name]
2446
2476
  return self.model.predict_proba(data.loc[:, self.feature_cols])[:, 1]
2447
2477
 
2478
+ def _rank_payload(data):
2479
+ """(y_true, y_score) with special sentinel scores removed.
2480
+
2481
+ Sentinels like the all-missing override -1 carry no ordering
2482
+ information: AUC/KS/distribution figures are computed on the
2483
+ non-special rows; their bin-level accounting lives in the gains
2484
+ table via spec_values.
2485
+ """
2486
+ y_true = data[self.tgt_name]
2487
+ y_score = _get_score(data)
2488
+ if not self.spec_values:
2489
+ return y_true, y_score
2490
+ keep = ~pd.Series(np.asarray(y_score)).isin(self.spec_values).to_numpy()
2491
+ return np.asarray(y_true)[keep], np.asarray(y_score)[keep]
2492
+
2448
2493
  def _get_benchmark_bin_edges():
2449
2494
  if benchmark_dataset is None:
2450
2495
  return None
@@ -2492,9 +2537,10 @@ class PerformanceEvaluator:
2492
2537
  for name, data in dataset_dict.items():
2493
2538
  if data is None:
2494
2539
  continue
2540
+ y_true, y_score = _rank_payload(data)
2495
2541
  eval_datasets[name] = {
2496
- "y_true": data[self.tgt_name],
2497
- "y_score": _get_score(data)
2542
+ "y_true": y_true,
2543
+ "y_score": y_score
2498
2544
  }
2499
2545
 
2500
2546
  if len(eval_datasets) == 0:
@@ -2553,7 +2599,8 @@ class PerformanceEvaluator:
2553
2599
  chi2_p = self.chi2_p,
2554
2600
  retSummary = True,
2555
2601
  tree_binning = False if benchmark_bin_edges is not None else self.tree_binning,
2556
- random_state = self.random_state
2602
+ random_state = self.random_state,
2603
+ **self._governed_eval_kwargs(),
2557
2604
  )
2558
2605
  gains_res['index'] = name
2559
2606
  gains_summ_list.append(gains_res)
@@ -32,6 +32,6 @@ class GainsTableCalculator:
32
32
  def calculate(self, grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True, retSummary = False, withSummary = False, wholeGroup = False, add_func = None): ...
33
33
 
34
34
  class PerformanceEvaluator:
35
- def __init__(self, tgt_name, scr_name = None, model = None, feature_cols = None, dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, chi2_method = False, init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42, weight_col = None): ...
35
+ def __init__(self, tgt_name, scr_name = None, model = None, feature_cols = None, dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, chi2_method = False, init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42, weight_col = None, spec_values = None, ascending = None): ...
36
36
  def add_dataset(self, name, data, weight_col = None, overwrite = False): ...
37
37
  def evaluate(self, oot_grp_name = None, min_data_size = 100, grp_colname = None, fig_save_path = None, rpt_save_path = None, to_show = False, display = True, gains_table = False, benchmark_dataset = None, weight_col = None): ...
@@ -95,10 +95,18 @@ def calc_pr(y_true, y_score, sample_weight=None):
95
95
  )
96
96
 
97
97
 
98
- def rank_bins(score, weight, nbins):
98
+ def rank_bins(score, weight, nbins, ascending=False):
99
+ """Weighted equal-frequency rank bins.
100
+
101
+ ``ascending=False`` (default, legacy) sorts scores descending — bin 1 is
102
+ the highest-score bucket. ``ascending=True`` sorts scores ascending so
103
+ bin 1 is the lowest-score bucket, matching the unweighted
104
+ ``gains_ascending=True`` convention.
105
+ """
99
106
  score = np.asarray(score, dtype=float)
100
107
  weight = np.ones(len(score), dtype=float) if weight is None else np.asarray(weight, dtype=float)
101
- order = np.lexsort((np.arange(len(score)), -score))
108
+ sort_key = score if ascending else -score
109
+ order = np.lexsort((np.arange(len(score)), sort_key))
102
110
  bins = np.empty(len(score), dtype=int)
103
111
  total = float(weight.sum()) or float(len(score)) or 1.0
104
112
  cum_weight = np.cumsum(weight[order])
@@ -108,18 +116,35 @@ def rank_bins(score, weight, nbins):
108
116
  return bins
109
117
 
110
118
 
111
- def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binning=None, **kwargs):
119
+ def _split_special_scores(df, score, spec_values):
120
+ """Split a frame into (normal, special) rows by exact score membership.
121
+
122
+ Special score values (e.g. the all-missing override ``-1``) are business
123
+ sentinels, not model outputs: they must not participate in quantile
124
+ binning or ranking metrics.
125
+ """
126
+ if not spec_values:
127
+ return df, None
128
+ spec_mask = df[score].isin(list(spec_values))
129
+ if not bool(spec_mask.any()):
130
+ return df, None
131
+ return df[~spec_mask], df[spec_mask]
132
+
133
+
134
+ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binning=None,
135
+ ascending=False, spec_values=None, **kwargs):
112
136
  cols = [dep, score]
113
137
  if weight_col is not None and weight_col in data.columns:
114
138
  cols.append(weight_col)
115
139
  df = data[cols].copy()
140
+ df, special_df = _split_special_scores(df, score, spec_values)
116
141
  weight = resolve_weights(df, weight_col=weight_col, expected_len=len(df))
117
142
  if weight is None:
118
143
  weight = np.ones(len(df), dtype=float)
119
144
 
120
145
  y = df[dep].astype(float).to_numpy()
121
146
  s = df[score].astype(float).to_numpy()
122
- df["_bin_num"] = rank_bins(s, weight, nbins)
147
+ df["_bin_num"] = rank_bins(s, weight, nbins, ascending=ascending)
123
148
  df["_bin_range"] = df["_bin_num"]
124
149
  df["_w"] = weight
125
150
  df["_bad_w"] = weight * y
@@ -165,13 +190,56 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
165
190
  out["KS_PER_BIN"] = (out["CUM_BAD_PCT"] - out["CUM_GOOD_PCT"]).abs()
166
191
  out["KS"] = out["KS_PER_BIN"]
167
192
  out["LIFT"] = out["AVG_BAD"] / overall_bad_rate if overall_bad_rate else np.nan
168
- out["TRUE_BAD_SHIFT"] = out["AVG_BAD"].shift(1) / out["AVG_BAD"] - 1
193
+ out["TRUE_BAD_SHIFT"] = (
194
+ (out["AVG_BAD"].shift(1) / out["AVG_BAD"] - 1)
195
+ if not ascending
196
+ else (out["AVG_BAD"] / out["AVG_BAD"].shift(1) - 1)
197
+ )
169
198
  out["RANK_ORDER_BUMP"] = out["TRUE_BAD_SHIFT"].lt(0).astype(int)
170
199
  with np.errstate(divide="ignore", invalid="ignore"):
171
200
  out["WOE"] = np.log(out["BAD_PCT_IN_EACH_BIN"] / out["GOOD_PCT_IN_EACH_BIN"])
172
201
  out["WOE"] = out["WOE"].replace([np.inf, -np.inf], 0).fillna(0)
173
202
  out["IV"] = (out["BAD_PCT_IN_EACH_BIN"] - out["GOOD_PCT_IN_EACH_BIN"]) * out["WOE"]
174
203
  out["AUC"] = safe_auc(y, s, sample_weight=weight)
204
+
205
+ if special_df is not None and len(special_df):
206
+ # Special sentinel scores (e.g. -1 for all-missing rows) get their own
207
+ # descriptive rows: never part of quantile edges, cumulative columns,
208
+ # or ranking metrics (those stay NaN by construction).
209
+ spec_weight = resolve_weights(special_df, weight_col=weight_col, expected_len=len(special_df))
210
+ if spec_weight is None:
211
+ spec_weight = np.ones(len(special_df), dtype=float)
212
+ grand_total = total_weight + float(np.sum(spec_weight))
213
+ spec_rows = []
214
+ for value, part in special_df.groupby(score, sort=True):
215
+ part_w = resolve_weights(part, weight_col=weight_col, expected_len=len(part))
216
+ if part_w is None:
217
+ part_w = np.ones(len(part), dtype=float)
218
+ part_y = part[dep].astype(float).to_numpy()
219
+ n_w = float(np.sum(part_w))
220
+ n_bad = float(np.sum(part_w * part_y))
221
+ label = f"special:{value}"
222
+ spec_rows.append(
223
+ pd.DataFrame(
224
+ {
225
+ "MIN": [value],
226
+ "MAX": [value],
227
+ "N": [n_w],
228
+ "N_RAW": [int(len(part))],
229
+ "PERF_CNT": [n_w],
230
+ "N_BAD": [n_bad],
231
+ "N_GOOD": [n_w - n_bad],
232
+ "AVG_SCORE": [float(value)],
233
+ "UNIQUE_SCORE": [1],
234
+ "PROP": [n_w / grand_total if grand_total else np.nan],
235
+ "AVG_BAD": [n_bad / n_w if n_w else np.nan],
236
+ "AVG_GOOD": [(n_w - n_bad) / n_w if n_w else np.nan],
237
+ },
238
+ index=pd.MultiIndex.from_tuples([(label, label)], names=out.index.names),
239
+ )
240
+ )
241
+ out = pd.concat([out] + spec_rows)
242
+ out["AUC"] = out["AUC"].iloc[0]
175
243
  return out
176
244
 
177
245
 
@@ -212,32 +280,53 @@ def calc_fixed_pct(y_true, y_score, sample_weight=None, **kwargs):
212
280
  return calc_equid_dist(y_true, y_score, sample_weight=sample_weight, **kwargs)
213
281
 
214
282
 
215
- def dataset_summary(name, data, tgt_name, scr_name, weight_col=None, nbins=10):
283
+ def dataset_summary(name, data, tgt_name, scr_name, weight_col=None, nbins=10,
284
+ ascending=False, spec_values=None):
216
285
  weight = resolve_weights(data, weight_col=weight_col, expected_len=len(data))
217
286
  y_true = data[tgt_name].to_numpy()
218
287
  y_score = data[scr_name].to_numpy()
219
- roc_df = calc_roc(y_true, y_score, sample_weight=weight)
220
- gains = get_gains_table(data, tgt_name, scr_name, nbins=nbins, weight_col=weight_col)
221
- return {
288
+ # Ranking metrics (AUC/KS/avgScore) are computed on non-special rows only:
289
+ # sentinel scores like -1 carry no ordering information.
290
+ rank_data, special_part = _split_special_scores(data, scr_name, spec_values)
291
+ rank_weight = resolve_weights(rank_data, weight_col=weight_col, expected_len=len(rank_data))
292
+ rank_true = rank_data[tgt_name].to_numpy()
293
+ rank_score = rank_data[scr_name].to_numpy()
294
+ roc_df = calc_roc(rank_true, rank_score, sample_weight=rank_weight)
295
+ gains = get_gains_table(
296
+ data, tgt_name, scr_name, nbins=nbins, weight_col=weight_col,
297
+ ascending=ascending, spec_values=spec_values,
298
+ )
299
+ summary = {
222
300
  "index": name,
223
301
  "dataset": name,
224
302
  "DATASET": name,
225
- "AUC": safe_auc(y_true, y_score, sample_weight=weight),
303
+ "AUC": safe_auc(rank_true, rank_score, sample_weight=rank_weight),
226
304
  "KS": float(roc_df["KS"].max()) if "KS" in roc_df else np.nan,
227
305
  "LIFT": float(gains["LIFT"].max()) if "LIFT" in gains else np.nan,
228
306
  "IV": float(gains["IV"].sum()) if "IV" in gains else np.nan,
229
307
  "N": float(np.sum(weight)) if weight is not None else float(len(data)),
230
308
  "N_RAW": int(len(data)),
231
309
  "avgTrue": safe_weighted_average(y_true, weight),
232
- "avgScore": safe_weighted_average(y_score, weight),
310
+ "avgScore": safe_weighted_average(rank_score, rank_weight),
233
311
  }
312
+ if special_part is not None:
313
+ spec_weight = resolve_weights(special_part, weight_col=weight_col, expected_len=len(special_part))
314
+ summary["N_SPECIAL"] = (
315
+ float(np.sum(spec_weight)) if spec_weight is not None else float(len(special_part))
316
+ )
317
+ summary["N_SPECIAL_RAW"] = int(len(special_part))
318
+ return summary
234
319
 
235
320
 
236
- def get_perf_summary(train=None, validation=None, oot=None, tgt_name=None, scr_name=None, weight_col=None, nbins=10, **kwargs):
321
+ def get_perf_summary(train=None, validation=None, oot=None, tgt_name=None, scr_name=None, weight_col=None, nbins=10,
322
+ ascending=False, spec_values=None, **kwargs):
237
323
  rows = []
238
324
  for name, data in (("ins", train), ("oos", validation), ("oot", oot)):
239
325
  if data is not None:
240
- rows.append(dataset_summary(name, data, tgt_name, scr_name, weight_col=weight_col, nbins=nbins))
326
+ rows.append(dataset_summary(
327
+ name, data, tgt_name, scr_name, weight_col=weight_col, nbins=nbins,
328
+ ascending=ascending, spec_values=spec_values,
329
+ ))
241
330
  return pd.DataFrame(rows)
242
331
 
243
332
 
@@ -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,9 +15,11 @@ 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
+ _iv_band_keep,
20
23
  _legacy_unweighted_screen,
21
24
  _psi_from_distributions,
22
25
  _resolve_splits,
@@ -37,6 +40,14 @@ _MONOTONE_INIT_KEYS = frozenset({
37
40
  "missing_woe",
38
41
  "special_values",
39
42
  "bin_label_decimals",
43
+ "min_bad_count",
44
+ "min_good_count",
45
+ "small_bin_policy",
46
+ "monotone_direction",
47
+ "reference_target",
48
+ "direction_conflict_policy",
49
+ "missing_bin_strategy",
50
+ "refine_min_n_bins_policy",
40
51
  })
41
52
  _MONOTONE_FIT_KEYS = frozenset({"chi2_binning", "chi2_p", "chi2_init_size", "n_jobs"})
42
53
 
@@ -50,6 +61,7 @@ class FeatureScreenConfig:
50
61
  psi_use_woe_bins: bool = False
51
62
  iv_enabled: bool = True
52
63
  iv_threshold: float = 0.02
64
+ iv_upper_threshold: float | None = None
53
65
  iv_bins: int = 10
54
66
  iv_min_bin_prop: float = 0.05
55
67
  iv_equal_freq: bool = True
@@ -77,6 +89,28 @@ class FeatureScreenConfig:
77
89
  content: float = 1e-6
78
90
  precision: int = 5
79
91
  min_bin_prop: float = 0.05
92
+ # --- Post-corr selection gates (G03/G04/G05/G06); all legacy-off ------
93
+ # G03 group stability (requires SelectionEvidence — FVP only)
94
+ monthly_iv_min: float | None = None
95
+ monthly_iv_cv_max: float | None = None
96
+ direction_consistency_min: float | None = None
97
+ min_group_n: int | None = None
98
+ insufficient_group_policy: Literal["keep_warn", "drop", "raise"] = "keep_warn"
99
+ # G04 multi-target joint gate (requires SelectionEvidence — FVP only)
100
+ target_rules: Literal["all", "any", "min_pass_count"] | None = None
101
+ min_pass_count: int | None = None
102
+ per_target_iv_range: Any = None # (low, high) or {target: (low, high)}
103
+ direction_reference_target: str | None = None
104
+ # G05 hard top-N truncation
105
+ max_selected_features: int | None = None
106
+ min_selected_features: int | None = None
107
+ ranking_metric: str = "iv"
108
+ tie_breaker: str = "name"
109
+ # G06 VIF gate (needs the optional statsmodels extra)
110
+ vif_enabled: bool = False
111
+ vif_threshold: float = 10.0
112
+ vif_min_features: int = 2
113
+ vif_tie_break_metric: str = "iv"
80
114
 
81
115
 
82
116
  def screen_config_from_mapping(
@@ -100,6 +134,9 @@ def screen_config_from_mapping(
100
134
  psi_use_woe_bins=bool(cfg.get("psi_use_woe_bins", False)),
101
135
  iv_enabled=bool(cfg.get("iv_enabled", True)),
102
136
  iv_threshold=float(cfg.get("iv_threshold", 0.02)),
137
+ iv_upper_threshold=(
138
+ float(cfg["iv_upper_threshold"]) if cfg.get("iv_upper_threshold") is not None else None
139
+ ),
103
140
  iv_bins=iv_nbins,
104
141
  iv_min_bin_prop=float(cfg.get("iv_min_bin_prop", 0.05)),
105
142
  iv_equal_freq=bool(cfg.get("iv_equal_freq", True)),
@@ -125,6 +162,23 @@ def screen_config_from_mapping(
125
162
  content=float(cfg.get("content", 1e-6)),
126
163
  precision=int(cfg.get("precision", 5)),
127
164
  min_bin_prop=float(cfg.get("min_bin_prop", cfg.get("iv_min_bin_prop", 0.05))),
165
+ monthly_iv_min=cfg.get("monthly_iv_min"),
166
+ monthly_iv_cv_max=cfg.get("monthly_iv_cv_max"),
167
+ direction_consistency_min=cfg.get("direction_consistency_min"),
168
+ min_group_n=cfg.get("min_group_n"),
169
+ insufficient_group_policy=str(cfg.get("insufficient_group_policy", "keep_warn")), # type: ignore[arg-type]
170
+ target_rules=cfg.get("target_rules"),
171
+ min_pass_count=cfg.get("min_pass_count"),
172
+ per_target_iv_range=cfg.get("per_target_iv_range"),
173
+ direction_reference_target=cfg.get("direction_reference_target"),
174
+ max_selected_features=cfg.get("max_selected_features"),
175
+ min_selected_features=cfg.get("min_selected_features"),
176
+ ranking_metric=str(cfg.get("ranking_metric", "iv")),
177
+ tie_breaker=str(cfg.get("tie_breaker", "name")),
178
+ vif_enabled=bool(cfg.get("vif_enabled", False)),
179
+ vif_threshold=float(cfg.get("vif_threshold", 10.0)),
180
+ vif_min_features=int(cfg.get("vif_min_features", 2)),
181
+ vif_tie_break_metric=str(cfg.get("vif_tie_break_metric", "iv")),
128
182
  )
129
183
 
130
184
 
@@ -213,12 +267,15 @@ def _woe_bins_unweighted_screen(
213
267
  config: FeatureScreenConfig,
214
268
  *,
215
269
  prefit_woe_engine: Any | None = None,
270
+ selection_evidence: Any | None = None,
216
271
  ) -> FeatureScreenResult:
217
272
  from Modeling_Tool import CorrelationFilter, PSICalculator, VarExtractionInsights
218
273
 
219
274
  ins, oos, oot = splits["ins"], splits["oos"], splits["oot"]
220
275
  current = list(feature_cols)
221
276
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, None)]
277
+ dropped_rows: list[dict] = []
278
+ stage_tables: dict[str, pd.DataFrame] = {}
222
279
  current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
223
280
  ins,
224
281
  current,
@@ -228,6 +285,7 @@ def _woe_bins_unweighted_screen(
228
285
  on_empty_stage=config.on_empty_stage,
229
286
  )
230
287
  binner = _resolve_screening_binner(splits, current, target_col, config, prefit_woe_engine)
288
+ engine_fit_features = list(current)
231
289
 
232
290
  psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
233
291
  if config.psi_enabled:
@@ -290,7 +348,7 @@ def _woe_bins_unweighted_screen(
290
348
  )
291
349
  if iv is not None and not iv.empty:
292
350
  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()
351
+ keep = _iv_band_keep(iv, "iv", config.iv_threshold, config.iv_upper_threshold, dropped_rows)
294
352
  n_before = len(current)
295
353
  current = _apply_stage_keep(
296
354
  current, keep, "iv", summary_rows,
@@ -312,7 +370,25 @@ def _woe_bins_unweighted_screen(
312
370
  current = cf.remove_highly_correlated(current, max_iterations=config.corr_max_iterations)
313
371
  summary_rows.append(_summary_row("corr", n_before, len(current), config.corr_threshold, None))
314
372
 
373
+ from .Screen_Gates import apply_post_corr_gates
374
+
375
+ gate_iv_map = (
376
+ dict(zip(iv_table["var"], iv_table["iv_weighted"])) if not iv_table.empty else {}
377
+ )
378
+ current = apply_post_corr_gates(
379
+ ins, current, config, selection_evidence, gate_iv_map,
380
+ summary_rows, dropped_rows, stage_tables,
381
+ weight_col=None, on_empty_stage=config.on_empty_stage,
382
+ )
383
+
315
384
  summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, None))
385
+ engine_obj, engine_meta = _screen_engine_payload(
386
+ binner,
387
+ fit_features=engine_fit_features,
388
+ target_col=target_col,
389
+ weight_col=None,
390
+ summary_rows=summary_rows,
391
+ )
316
392
  return FeatureScreenResult(
317
393
  selected_features=list(current),
318
394
  iv_table=iv_table,
@@ -321,6 +397,13 @@ def _woe_bins_unweighted_screen(
321
397
  summary=pd.DataFrame(summary_rows),
322
398
  missing_rate_table=missing_rate_table,
323
399
  missing_rate_dropped=missing_rate_dropped,
400
+ woe_engine=engine_obj,
401
+ woe_engine_meta=engine_meta,
402
+ dropped_detail=(
403
+ pd.DataFrame(dropped_rows, columns=_DROPPED_DETAIL_COLS)
404
+ if dropped_rows else pd.DataFrame(columns=_DROPPED_DETAIL_COLS)
405
+ ),
406
+ stage_tables=stage_tables,
324
407
  )
325
408
 
326
409
 
@@ -330,6 +413,52 @@ def _bins_to_numpy(bins: Any) -> np.ndarray:
330
413
  return np.asarray(bins, dtype=object)
331
414
 
332
415
 
416
+ def _screen_engine_payload(
417
+ binner: Any | None,
418
+ *,
419
+ fit_features: list[str],
420
+ target_col: str,
421
+ weight_col: str | None,
422
+ summary_rows: list[dict],
423
+ ) -> tuple[Any | None, dict[str, Any]]:
424
+ """Build the (woe_engine, woe_engine_meta) result payload (G00).
425
+
426
+ Monotone engines attach as-is — they hold only per-feature bin tables.
427
+ WOE_Master engines are never attached (they retain the full training
428
+ frame at ``self.train_data``); their woe_table + metadata travel instead.
429
+ """
430
+ if binner is None:
431
+ return None, {}
432
+ engine_name = type(binner).__name__
433
+ meta: dict[str, Any] = {
434
+ "engine_name": engine_name,
435
+ "fit_features": list(fit_features),
436
+ "target_col": target_col,
437
+ "weight_col": weight_col,
438
+ "screen_stages": [row.get("stage") for row in summary_rows],
439
+ }
440
+ if engine_name == "WOE_Master":
441
+ from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
442
+
443
+ try:
444
+ meta["woe_table"] = as_woe_engine(binner).get_woe_table(varlist=list(fit_features))
445
+ except Exception as exc:
446
+ meta["woe_table"] = None
447
+ meta["woe_table_error"] = repr(exc)
448
+ meta["engine_attached"] = False
449
+ warnings.warn(
450
+ "screening WOE engine is a WOE_Master, which retains the full "
451
+ "training frame — attaching its woe_table and metadata only. "
452
+ "Use woe_engine='monotone' to get a reusable, serializable "
453
+ "screening engine on the result.",
454
+ UserWarning,
455
+ stacklevel=3,
456
+ )
457
+ return None, meta
458
+ meta["engine_attached"] = True
459
+ return binner, meta
460
+
461
+
333
462
  def _weighted_woe_bins_screen(
334
463
  splits: dict[str, pd.DataFrame],
335
464
  feature_cols: list[str],
@@ -338,6 +467,7 @@ def _weighted_woe_bins_screen(
338
467
  config: FeatureScreenConfig,
339
468
  *,
340
469
  prefit_woe_engine: Any | None = None,
470
+ selection_evidence: Any | None = None,
341
471
  ) -> FeatureScreenResult:
342
472
  from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
343
473
 
@@ -346,6 +476,8 @@ def _weighted_woe_bins_screen(
346
476
  y_ins = ins[target_col].astype(float).to_numpy()
347
477
  current = list(feature_cols)
348
478
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, weight_col)]
479
+ dropped_rows: list[dict] = []
480
+ stage_tables: dict[str, pd.DataFrame] = {}
349
481
  current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
350
482
  ins,
351
483
  current,
@@ -369,6 +501,7 @@ def _weighted_woe_bins_screen(
369
501
  categorical_features=config.categorical_features,
370
502
  )
371
503
  adapter = as_woe_engine(binner)
504
+ engine_fit_features = list(current)
372
505
 
373
506
  # Assign fitted bins once per split. PSI and IV both consume these labels;
374
507
  # doing it here avoids a full WOE transform for every feature and stage.
@@ -447,7 +580,7 @@ def _weighted_woe_bins_screen(
447
580
  columns=["var", "iv_weighted", "n_bins", "missing_rate"],
448
581
  )
449
582
  if not iv_table.empty:
450
- keep = iv_table.loc[iv_table["iv_weighted"] >= config.iv_threshold, "var"].tolist()
583
+ keep = _iv_band_keep(iv_table, "iv_weighted", config.iv_threshold, config.iv_upper_threshold, dropped_rows)
451
584
  n_before = len(current)
452
585
  current = _apply_stage_keep(
453
586
  current, keep, "iv", summary_rows,
@@ -479,7 +612,25 @@ def _weighted_woe_bins_screen(
479
612
  )
480
613
  summary_rows.append(_summary_row("corr", n_before, len(current), config.corr_threshold, weight_col))
481
614
 
615
+ from .Screen_Gates import apply_post_corr_gates
616
+
617
+ gate_iv_map = (
618
+ dict(zip(iv_table["var"], iv_table["iv_weighted"])) if not iv_table.empty else {}
619
+ )
620
+ current = apply_post_corr_gates(
621
+ ins, current, config, selection_evidence, gate_iv_map,
622
+ summary_rows, dropped_rows, stage_tables,
623
+ weight_col=weight_col, on_empty_stage=config.on_empty_stage,
624
+ )
625
+
482
626
  summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, weight_col))
627
+ engine_obj, engine_meta = _screen_engine_payload(
628
+ binner,
629
+ fit_features=engine_fit_features,
630
+ target_col=target_col,
631
+ weight_col=weight_col,
632
+ summary_rows=summary_rows,
633
+ )
483
634
  return FeatureScreenResult(
484
635
  selected_features=list(current),
485
636
  iv_table=iv_table,
@@ -488,6 +639,13 @@ def _weighted_woe_bins_screen(
488
639
  summary=pd.DataFrame(summary_rows),
489
640
  missing_rate_table=missing_rate_table,
490
641
  missing_rate_dropped=missing_rate_dropped,
642
+ woe_engine=engine_obj,
643
+ woe_engine_meta=engine_meta,
644
+ dropped_detail=(
645
+ pd.DataFrame(dropped_rows, columns=_DROPPED_DETAIL_COLS)
646
+ if dropped_rows else pd.DataFrame(columns=_DROPPED_DETAIL_COLS)
647
+ ),
648
+ stage_tables=stage_tables,
491
649
  )
492
650
 
493
651
 
@@ -499,6 +657,7 @@ def feature_screen(
499
657
  weight_col: str | None = None,
500
658
  config: FeatureScreenConfig | None = None,
501
659
  prefit_woe_engine: Any | None = None,
660
+ selection_evidence: Any | None = None,
502
661
  ) -> FeatureScreenResult:
503
662
  """Run PSI -> IV -> correlation screening on pre-split INS/OOS/OOT frames."""
504
663
  cfg = config or FeatureScreenConfig()
@@ -513,6 +672,7 @@ def feature_screen(
513
672
  weight_col,
514
673
  cfg,
515
674
  prefit_woe_engine=prefit_woe_engine,
675
+ selection_evidence=selection_evidence,
516
676
  )
517
677
  return _weighted_screen_impl(
518
678
  splits,
@@ -524,6 +684,7 @@ def feature_screen(
524
684
  psi_compare_splits=list(cfg.psi_compare_splits),
525
685
  iv_enabled=cfg.iv_enabled,
526
686
  iv_threshold=cfg.iv_threshold,
687
+ iv_upper_threshold=cfg.iv_upper_threshold,
527
688
  iv_bins=cfg.iv_bins,
528
689
  min_bin_prop=cfg.min_bin_prop,
529
690
  corr_enabled=cfg.corr_enabled,
@@ -538,6 +699,8 @@ def feature_screen(
538
699
  prefit_woe_engine=prefit_woe_engine,
539
700
  missing_rate_threshold=cfg.missing_rate_threshold,
540
701
  missing_rate_ref=cfg.missing_rate_ref,
702
+ gates_config=cfg,
703
+ selection_evidence=selection_evidence,
541
704
  )
542
705
 
543
706
  if use_woe_bins or prefit_woe_engine is not None:
@@ -547,6 +710,7 @@ def feature_screen(
547
710
  target_col,
548
711
  cfg,
549
712
  prefit_woe_engine=prefit_woe_engine,
713
+ selection_evidence=selection_evidence,
550
714
  )
551
715
 
552
716
  return _legacy_unweighted_screen(
@@ -558,6 +722,7 @@ def feature_screen(
558
722
  psi_compare_splits=list(cfg.psi_compare_splits),
559
723
  iv_enabled=cfg.iv_enabled,
560
724
  iv_threshold=cfg.iv_threshold,
725
+ iv_upper_threshold=cfg.iv_upper_threshold,
561
726
  iv_bins=cfg.iv_bins,
562
727
  iv_min_bin_prop=cfg.iv_min_bin_prop,
563
728
  corr_enabled=cfg.corr_enabled,
@@ -570,6 +735,8 @@ def feature_screen(
570
735
  on_empty_stage=cfg.on_empty_stage,
571
736
  missing_rate_threshold=cfg.missing_rate_threshold,
572
737
  missing_rate_ref=cfg.missing_rate_ref,
738
+ gates_config=cfg,
739
+ selection_evidence=selection_evidence,
573
740
  )
574
741
 
575
742