SuperModelingFactory 0.6.9__tar.gz → 0.7.0__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.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/Model_Eval_Tool.py +7 -4
  2. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +1 -1
  3. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/evaluate_model.py +91 -59
  4. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/evaluate_model.pyi +2 -2
  5. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/weighted_eval_utils.py +1 -0
  6. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/Feature_Screen.py +87 -2
  7. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/Screen_Gates.py +102 -9
  8. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/credit_model.py +32 -30
  9. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/feature_validation.py +16 -11
  10. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/field_meta.py +1 -1
  11. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +1 -1
  12. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +1 -1
  13. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/__init__.py +1 -1
  14. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/PKG-INFO +2 -2
  15. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/README.md +1 -1
  16. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  17. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/pyproject.toml +1 -1
  18. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/setup.py +1 -1
  19. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/ExcelMaster/ExcelFormatTool.py +0 -0
  20. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/ExcelMaster/ExcelMaster.py +0 -0
  21. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/ExcelMaster/Template.py +0 -0
  22. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/ExcelMaster/Utility.py +0 -0
  23. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/ExcelMaster/__init__.py +0 -0
  24. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/LICENSE +0 -0
  25. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/MANIFEST.in +0 -0
  26. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  27. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  28. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  29. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  30. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  31. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  32. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  33. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  34. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  35. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  36. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  37. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  38. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  39. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/__init__.py +0 -0
  40. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/kDataFrame.py +0 -0
  41. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  42. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  43. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Core/utils.py +0 -0
  44. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  45. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  46. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Eval/__init__.py +0 -0
  47. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  48. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  49. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Explainability/__init__.py +0 -0
  50. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  51. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  52. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  53. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  54. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/ODPS_Distribution_Tool.py +0 -0
  55. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/ODPS_Distribution_Tool.pyi +0 -0
  56. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  57. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  58. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  59. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/Weighted_Screen.py +0 -0
  60. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Feature/__init__.py +0 -0
  61. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  62. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  63. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  64. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  65. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  66. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  67. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  68. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Model/__init__.py +0 -0
  69. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/__init__.py +0 -0
  70. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/_common.py +0 -0
  71. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  72. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  73. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  74. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  75. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  76. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  77. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  78. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  79. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  80. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  81. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  82. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  83. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  84. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/Sample/__init__.py +0 -0
  85. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  86. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/UAT/__init__.py +0 -0
  87. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  88. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  89. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  90. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  91. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  92. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  93. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  94. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  95. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  96. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  97. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/__init__.py +0 -0
  98. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  99. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  100. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/_utils/__init__.py +0 -0
  101. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/_utils/nan_guard.py +0 -0
  102. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/_utils/robust.py +0 -0
  103. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/_utils/sentinels.py +0 -0
  104. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  105. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  106. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/ref_font/__init__.py +0 -0
  107. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  108. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Report/Report_Tool.py +0 -0
  109. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/Report/__init__.py +0 -0
  110. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  111. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  112. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  113. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/SuperModelingFactory.egg-info/requires.txt +0 -0
  114. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  115. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/requirements.txt +0 -0
  116. {supermodelingfactory-0.6.9 → supermodelingfactory-0.7.0}/setup.cfg +0 -0
@@ -846,6 +846,7 @@ def get_gains_table(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, i
846
846
  nbins=nbins,
847
847
  weight_col=weight_col,
848
848
  weighted_binning=weighted_binning,
849
+ ascending=ascending,
849
850
  )
850
851
  if retSummary:
851
852
  return pd.DataFrame({
@@ -2228,8 +2229,8 @@ class PerformanceEvaluator:
2228
2229
  # spec_values: sentinel scores (e.g. -1 for all-missing overrides)
2229
2230
  # that get their own evaluation bin and are excluded from quantile
2230
2231
  # 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.
2232
+ # function's legacy direction; an explicit bool is threaded through
2233
+ # summary, gains, and figures (including weighted paths) uniformly.
2233
2234
  self.spec_values = list(spec_values) if spec_values else []
2234
2235
  self.ascending = ascending
2235
2236
  self.datasets = {}
@@ -2420,6 +2421,7 @@ class PerformanceEvaluator:
2420
2421
  save_path=fig_save_path,
2421
2422
  gains_table=gains_table,
2422
2423
  equal_freq=self.equal_freq,
2424
+ ascending=self.ascending,
2423
2425
  )
2424
2426
  if rpt_save_path:
2425
2427
  fnl_df.to_csv(rpt_save_path, index=False)
@@ -2522,7 +2524,7 @@ class PerformanceEvaluator:
2522
2524
  tree_binning = self.tree_binning,
2523
2525
  random_state = self.random_state,
2524
2526
  return_edges = True,
2525
- ascending = True
2527
+ ascending = True if self.ascending is None else bool(self.ascending)
2526
2528
  )
2527
2529
 
2528
2530
  if benchmark_bin_edges is None or len(benchmark_bin_edges) < 2:
@@ -2556,7 +2558,8 @@ class PerformanceEvaluator:
2556
2558
  save_path = save_path,
2557
2559
  gains_table = gains_table,
2558
2560
  equal_freq = self.equal_freq,
2559
- pct_bin_edges = benchmark_bin_edges
2561
+ pct_bin_edges = benchmark_bin_edges,
2562
+ ascending = self.ascending,
2560
2563
  )
2561
2564
 
2562
2565
  import re
@@ -20,7 +20,7 @@ import pandas as pd
20
20
  from Modeling_Tool.Core.Binning_Tool import get_bin_range_list, super_binning
21
21
  from Modeling_Tool.Core.utils import load_model, calc_iv, calc_woe
22
22
  from .evaluate_model import evaluate_performance
23
- def get_gains_table(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, score = None, model = None, varlist = None, equal_freq = True, chi2_method = False, grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True, chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], retSummary = False, tree_binning = False, random_state = 42, ascending = False, withSummary = False, wholeGroup = False, add_func = None): ...
23
+ def get_gains_table(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, score = None, model = None, varlist = None, equal_freq = True, chi2_method = False, grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True, chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], retSummary = False, tree_binning = False, random_state = 42, ascending = False, withSummary = False, wholeGroup = False, add_func = None, weight_col = None, weighted_binning = None): ...
24
24
  def get_perf_summary(train, validation, oot, tgt_name, scr_name = None, model = None, feature_cols = None, fig_save_path = None, rpt_save_path = None, to_show = False, display = True, 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, oot_grp_name = None, min_data_size = 100, grp_colname = None, tree_binning = False, random_state = 42, gains_table = False): ...
25
25
  def cross_risk(data, score_list, dep, nbins, agg_col = None, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, binning_numeric = [True, True], agg_func = 'mean', chi2_method = False, chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], tree_binning = False, random_state = 42): ...
26
26
  def get_gains_table_by_cust_metrics(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, score = None, model = None, varlist = None, equal_freq = True, chi2_method = False, grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True, chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], tree_binning = False, random_state = 42, ascending = True, eval_metrics = ['age', 'monthly_income', 'education'], metric_agg_func = 'mean', withSummary = False): ...
@@ -889,7 +889,13 @@ def calc_equid_pct(y_true, y_score, y_group=None, bins=10, ascending=True, sampl
889
889
  等分分组后各组统计量数据集
890
890
  """
891
891
  if sample_weight is not None:
892
- return _weighted_eval.calc_equid_pct(y_true, y_score, bins=bins, sample_weight=sample_weight)
892
+ return _weighted_eval.calc_equid_pct(
893
+ y_true,
894
+ y_score,
895
+ bins=bins,
896
+ ascending=ascending,
897
+ sample_weight=sample_weight,
898
+ )
893
899
 
894
900
  y_true = np.array(y_true)
895
901
  y_score = np.array(y_score)
@@ -947,7 +953,12 @@ def calc_fixed_pct(y_true, y_score, y_group=None, bin_edges=None, ascending=True
947
953
  固定分箱后各组统计量数据集
948
954
  """
949
955
  if sample_weight is not None:
950
- return _weighted_eval.calc_fixed_pct(y_true, y_score, sample_weight=sample_weight)
956
+ return _weighted_eval.calc_fixed_pct(
957
+ y_true,
958
+ y_score,
959
+ ascending=ascending,
960
+ sample_weight=sample_weight,
961
+ )
951
962
 
952
963
  if bin_edges is None:
953
964
  raise ValueError("bin_edges cannot be None when using fixed pct bins.")
@@ -1007,20 +1018,27 @@ def summarize_pct(pct_df, ascending=True):
1007
1018
  bins = pct_df.shape[0]
1008
1019
  interval = 100 / pct_df.shape[0]
1009
1020
 
1010
- if ascending:
1011
- pct_info = {
1012
- 'pct_bins': bins,
1013
- 'pct_interval': interval,
1014
- 'pct_top_avgTrue'.format(interval): pct_df['avg_true'].iloc[bins-1],
1015
- 'pct_btm_avgTrue'.format(interval): pct_df['avg_true'].iloc[0],
1016
- }
1017
- else:
1018
- pct_info = {
1019
- 'pct_bins': bins,
1020
- 'pct_interval': interval,
1021
- 'pct_top_captureRate'.format(interval): pct_df['capture_rate'].iloc[bins-1],
1022
- 'pct_btm_captureRate'.format(interval): pct_df['capture_rate'].iloc[0],
1023
- }
1021
+ pct_info = {
1022
+ 'pct_bins': bins,
1023
+ 'pct_interval': interval,
1024
+ }
1025
+
1026
+ # Precomputed gain frames historically only needed thresholds, gain, and
1027
+ # capture_rate. Keep summarize_pct usable for those frames while exposing
1028
+ # target-rate summary keys whenever the richer percentile schema is used.
1029
+ if 'avg_true' in pct_df.columns:
1030
+ top_idx, btm_idx = (bins - 1, 0) if ascending else (0, bins - 1)
1031
+ pct_info.update({
1032
+ 'pct_top_avgTrue'.format(interval): pct_df['avg_true'].iloc[top_idx],
1033
+ 'pct_btm_avgTrue'.format(interval): pct_df['avg_true'].iloc[btm_idx],
1034
+ })
1035
+
1036
+ if not ascending and 'capture_rate' in pct_df.columns:
1037
+ # Preserve the historical descending-summary keys for direct callers.
1038
+ pct_info.update({
1039
+ 'pct_top_captureRate'.format(interval): pct_df['capture_rate'].iloc[bins-1],
1040
+ 'pct_btm_captureRate'.format(interval): pct_df['capture_rate'].iloc[0],
1041
+ })
1024
1042
 
1025
1043
  return pct_info
1026
1044
 
@@ -1374,7 +1392,7 @@ def __plot_pct_axes_base(ax, fontdicts):
1374
1392
  ax.set_ylabel('Target rate', fontdict=fontdicts['axislabel'])
1375
1393
 
1376
1394
 
1377
- def __plot_single_pct_axes(pct_df, ax, fontdicts):
1395
+ def __plot_single_pct_axes(pct_df, ax, fontdicts, ascending=True):
1378
1396
  """在axes上绘制单个Score分布图.
1379
1397
 
1380
1398
  Parameters
@@ -1397,7 +1415,7 @@ def __plot_single_pct_axes(pct_df, ax, fontdicts):
1397
1415
  ax.plot(X, pct_df['avg_true'], color=palette['ClassicBlueRedGrey'][1], linewidth=2, marker='.', markersize=5, label='True')
1398
1416
  ax.axhline(y=pct_df['cumavg_true'][pct_df.shape[0]-1], linestyle='--', color=palette['ClassicBlueRedGrey'][2], linewidth=2, label='Random')
1399
1417
 
1400
- pct_info = summarize_pct(pct_df)
1418
+ pct_info = summarize_pct(pct_df, ascending=ascending)
1401
1419
  _b, _i, _top, _btm = pct_info['pct_bins'], pct_info['pct_interval'], pct_info['pct_top_avgTrue'], pct_info['pct_btm_avgTrue']
1402
1420
  ax.set_title("Bins={0} Top{1:.0f}%={2:.2%} BTM{1:.0f}%={3:.2%}".format(_b, _i, _top, _btm), fontdict=fontdicts['subtitle'])
1403
1421
  ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
@@ -1524,7 +1542,7 @@ def __plot_multi_cumpct_axes(pct_dfs, ax, fontdicts):
1524
1542
 
1525
1543
  # Gain Curve
1526
1544
  @timeit_decorator
1527
- def plot_gain_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
1545
+ def plot_gain_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None, ascending=False):
1528
1546
  """绘制Score分布曲线图.
1529
1547
 
1530
1548
  Parameters
@@ -1537,6 +1555,8 @@ def plot_gain_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_s
1537
1555
  是否展示图片. 默认为True
1538
1556
  save_path: str
1539
1557
  结果图片存放文件地址. 默认值为None, 即不保存
1558
+ ascending: bool, default False
1559
+ Gain 图按分数升序(True)或降序(False)累计。
1540
1560
  """
1541
1561
  plt.figure(figsize=(square_figsize, square_figsize))
1542
1562
  plt.suptitle('Gain Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
@@ -1545,9 +1565,9 @@ def plot_gain_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_s
1545
1565
  models = list(pct_dfs.keys())
1546
1566
  if len(models) == 1:
1547
1567
  pct_df = pct_dfs[models[0]]
1548
- __plot_single_gain_axes(pct_df, ax, fontdicts)
1568
+ __plot_single_gain_axes(pct_df, ax, fontdicts, ascending=ascending)
1549
1569
  else:
1550
- __plot_multi_gain_axes(pct_dfs, ax, fontdicts)
1570
+ __plot_multi_gain_axes(pct_dfs, ax, fontdicts, ascending=ascending)
1551
1571
  ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1552
1572
 
1553
1573
  if to_show:
@@ -1557,7 +1577,7 @@ def plot_gain_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_s
1557
1577
  plt.close()
1558
1578
 
1559
1579
 
1560
- def __plot_gain_axes_base(ax, fontdicts):
1580
+ def __plot_gain_axes_base(ax, fontdicts, ascending=False):
1561
1581
  """在axes上绘制Score分布图基础.
1562
1582
 
1563
1583
  Parameters
@@ -1570,11 +1590,12 @@ def __plot_gain_axes_base(ax, fontdicts):
1570
1590
  ax.plot([0,100], [0,1], color='k', linestyle='--', linewidth=1)
1571
1591
  ax.set_xlim([0,100])
1572
1592
  ax.set_ylim([0,1])
1573
- ax.set_xlabel('Percentile % (Score Descending)', fontdict=fontdicts['axislabel'])
1593
+ direction = 'Ascending' if ascending else 'Descending'
1594
+ ax.set_xlabel(f'Percentile % (Score {direction})', fontdict=fontdicts['axislabel'])
1574
1595
  ax.set_ylabel('gain', fontdict=fontdicts['axislabel'])
1575
1596
 
1576
1597
 
1577
- def __plot_single_gain_axes(pct_df, ax, fontdicts):
1598
+ def __plot_single_gain_axes(pct_df, ax, fontdicts, ascending=False):
1578
1599
  """在axes上绘制单个Score分布图.
1579
1600
 
1580
1601
  Parameters
@@ -1586,11 +1607,11 @@ def __plot_single_gain_axes(pct_df, ax, fontdicts):
1586
1607
  fontdicts: dict
1587
1608
  绘图相关字体字典
1588
1609
  """
1589
- __plot_gain_axes_base(ax, fontdicts)
1610
+ __plot_gain_axes_base(ax, fontdicts, ascending=ascending)
1590
1611
 
1591
1612
  plot_df = pct_df.copy()
1592
1613
  if {'avg_score', 'proportion', 'capture_rate'}.issubset(plot_df.columns):
1593
- plot_df = plot_df.sort_values('avg_score', ascending=False).reset_index(drop=True)
1614
+ plot_df = plot_df.sort_values('avg_score', ascending=ascending).reset_index(drop=True)
1594
1615
  plot_df['thresholds'] = plot_df['proportion'].cumsum() * 100
1595
1616
  plot_df['gain'] = plot_df['capture_rate'].cumsum()
1596
1617
 
@@ -1600,14 +1621,15 @@ def __plot_single_gain_axes(pct_df, ax, fontdicts):
1600
1621
  Y = np.insert(Y, 0, 0)
1601
1622
  ax.plot(X, Y, color=palette['ClassicBlueRedGrey'][0], linewidth=2, marker='.', markersize=5, label='avgScore')
1602
1623
 
1603
- pct_info = summarize_pct(plot_df, ascending=False)
1624
+ pct_info = summarize_pct(plot_df, ascending=ascending)
1604
1625
  _b, _i = pct_info['pct_bins'], pct_info['pct_interval']
1605
- _top = plot_df['capture_rate'].iloc[0] if plot_df.shape[0] > 0 else np.nan
1606
- _btm = plot_df['capture_rate'].iloc[-1] if plot_df.shape[0] > 0 else np.nan
1626
+ _top_idx, _btm_idx = (-1, 0) if ascending else (0, -1)
1627
+ _top = plot_df['capture_rate'].iloc[_top_idx] if plot_df.shape[0] > 0 else np.nan
1628
+ _btm = plot_df['capture_rate'].iloc[_btm_idx] if plot_df.shape[0] > 0 else np.nan
1607
1629
  ax.set_title("Bins={0} Top{1:.0f}%={2:.2%} BTM{1:.0f}%={3:.2%}".format(_b, _i, _top, _btm), fontdict=fontdicts['subtitle'])
1608
1630
 
1609
1631
 
1610
- def __plot_multi_gain_axes(pct_dfs, ax, fontdicts):
1632
+ def __plot_multi_gain_axes(pct_dfs, ax, fontdicts, ascending=False):
1611
1633
  """在axes上绘制多个Score分布图.
1612
1634
 
1613
1635
  Parameters
@@ -1619,14 +1641,14 @@ def __plot_multi_gain_axes(pct_dfs, ax, fontdicts):
1619
1641
  fontdicts: dict
1620
1642
  绘图相关字体字典
1621
1643
  """
1622
- __plot_gain_axes_base(ax, fontdicts)
1644
+ __plot_gain_axes_base(ax, fontdicts, ascending=ascending)
1623
1645
 
1624
1646
  models = list(pct_dfs.keys())
1625
1647
  for i in range(len(models)):
1626
1648
  md = models[i]
1627
1649
  pct_df = pct_dfs[md].copy()
1628
1650
  if {'avg_score', 'proportion', 'capture_rate'}.issubset(pct_df.columns):
1629
- pct_df = pct_df.sort_values('avg_score', ascending=False).reset_index(drop=True)
1651
+ pct_df = pct_df.sort_values('avg_score', ascending=ascending).reset_index(drop=True)
1630
1652
  pct_df['thresholds'] = pct_df['proportion'].cumsum() * 100
1631
1653
  pct_df['gain'] = pct_df['capture_rate'].cumsum()
1632
1654
  X = np.array(pct_df['thresholds'])
@@ -1636,7 +1658,7 @@ def __plot_multi_gain_axes(pct_dfs, ax, fontdicts):
1636
1658
  ax.plot(X, Y, color=palette['MorandiDark'][i], linewidth=2, marker='.', markersize=5, label='{0} avgTrue'.format(md))
1637
1659
 
1638
1660
 
1639
- def _weighted_gains_to_plot_frames(weighted_gains):
1661
+ def _weighted_gains_to_plot_frames(weighted_gains, pct_ascending=True, gain_ascending=False):
1640
1662
  """Convert weighted gains output into the plotting helpers' long schema."""
1641
1663
  if weighted_gains is None or weighted_gains.empty:
1642
1664
  return pd.DataFrame(), pd.DataFrame()
@@ -1646,8 +1668,8 @@ def _weighted_gains_to_plot_frames(weighted_gains):
1646
1668
  if missing:
1647
1669
  raise KeyError(f"Weighted gains table missing plotting columns: {missing}")
1648
1670
 
1649
- descending = weighted_gains.reset_index(drop=True).copy()
1650
- total_bad = float(descending["N_BAD"].sum())
1671
+ gains = weighted_gains.reset_index(drop=True).copy()
1672
+ total_bad = float(gains["N_BAD"].sum())
1651
1673
 
1652
1674
  def _convert(source):
1653
1675
  source = source.reset_index(drop=True)
@@ -1680,10 +1702,10 @@ def _weighted_gains_to_plot_frames(weighted_gains):
1680
1702
  }
1681
1703
  )
1682
1704
 
1683
- # Percentile charts historically run from low score to high score, while
1684
- # gain charts run from the highest score downward.
1685
- percentile_frame = _convert(descending.iloc[::-1])
1686
- gain_frame = _convert(descending)
1705
+ # An explicit direction governs both plots. The optional defaults retain
1706
+ # the historical mixed behavior for direct callers that leave it unset.
1707
+ percentile_frame = _convert(gains.sort_values("AVG_SCORE", ascending=pct_ascending))
1708
+ gain_frame = _convert(gains.sort_values("AVG_SCORE", ascending=gain_ascending))
1687
1709
  return percentile_frame, gain_frame
1688
1710
 
1689
1711
 
@@ -1696,7 +1718,7 @@ def _set_weighted_axis_title(ax, fontdicts):
1696
1718
 
1697
1719
 
1698
1720
  @timeit_decorator
1699
- def evaluate_performance(datasets, dist_bins=20, pct_bins=10, square_figsize=5, fontdicts=fontdicts['sub'], to_show=True, save_path=None, gains_table = True, equal_freq = True, pct_bin_edges = None, sample_weight=None):
1721
+ def evaluate_performance(datasets, dist_bins=20, pct_bins=10, square_figsize=5, fontdicts=fontdicts['sub'], to_show=True, save_path=None, gains_table = True, equal_freq = True, pct_bin_edges = None, sample_weight=None, ascending=None):
1700
1722
  """绘制单模型预测效果评价图.
1701
1723
 
1702
1724
  Parameters
@@ -1715,6 +1737,9 @@ def evaluate_performance(datasets, dist_bins=20, pct_bins=10, square_figsize=5,
1715
1737
  是否展示图片. 默认为True
1716
1738
  save_path: str
1717
1739
  结果图片存放文件地址. 默认值为None, 即不保存
1740
+ ascending: bool, optional
1741
+ 显式值统一控制收益表、百分位图和 Gain 图的分数方向;None 保留
1742
+ 历史百分位升序、Gain 降序的行为。
1718
1743
 
1719
1744
  Returns
1720
1745
  -------
@@ -1746,7 +1771,7 @@ def evaluate_performance(datasets, dist_bins=20, pct_bins=10, square_figsize=5,
1746
1771
  y_true = datasets[d]['y_true']
1747
1772
  y_score = datasets[d]['y_score']
1748
1773
  dataset_weight = datasets[d].get('sample_weight', sample_weight)
1749
- result.update({d: __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins, fontdicts, gains_table, equal_freq, pct_bin_edges, sample_weight=dataset_weight)})
1774
+ result.update({d: __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins, fontdicts, gains_table, equal_freq, pct_bin_edges, sample_weight=dataset_weight, ascending=ascending)})
1750
1775
 
1751
1776
  result_df = pd.DataFrame.from_dict(result, orient='index').reset_index()
1752
1777
 
@@ -1779,7 +1804,7 @@ def resturct_gains(gains_table):
1779
1804
 
1780
1805
  return gains_table
1781
1806
 
1782
- def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins, fontdicts, gains_table = True, equal_freq = True, pct_bin_edges = None, sample_weight=None):
1807
+ def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins, fontdicts, gains_table = True, equal_freq = True, pct_bin_edges = None, sample_weight=None, ascending=None):
1783
1808
  """绘制单模型在单样本集上预测效果评价图.
1784
1809
  包括: ROC、KDE、PCT、Gain四图.
1785
1810
 
@@ -1806,6 +1831,13 @@ def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins,
1806
1831
  # from Model_Eval_Tool import get_gains_table
1807
1832
  from .Model_Eval_Tool import get_gains_table
1808
1833
 
1834
+ # ``None`` preserves the historical PCT-ascending / gain-descending
1835
+ # split. An explicit bool applies one direction consistently.
1836
+ pct_ascending = True if ascending is None else bool(ascending)
1837
+ gain_ascending = False if ascending is None else bool(ascending)
1838
+ unweighted_gains_ascending = True if ascending is None else bool(ascending)
1839
+ weighted_gains_ascending = False if ascending is None else bool(ascending)
1840
+
1809
1841
  # 清理数据
1810
1842
  mask = np.isfinite(y_score) & np.isfinite(y_true)
1811
1843
  if sample_weight is not None:
@@ -1832,7 +1864,7 @@ def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins,
1832
1864
  include_missing = False,
1833
1865
  score = 'y_score',
1834
1866
  equal_freq = equal_freq,
1835
- ascending = True ,
1867
+ ascending = unweighted_gains_ascending,
1836
1868
  withSummary = False,
1837
1869
  weight_col = '_sample_weight' if sample_weight is not None else None,
1838
1870
  )
@@ -1847,28 +1879,28 @@ def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins,
1847
1879
  "y_score",
1848
1880
  nbins=bin_count,
1849
1881
  weight_col="w",
1882
+ ascending=weighted_gains_ascending,
1883
+ )
1884
+ pct_df, pct_desc_df = _weighted_gains_to_plot_frames(
1885
+ weighted_gains,
1886
+ pct_ascending=pct_ascending,
1887
+ gain_ascending=gain_ascending,
1850
1888
  )
1851
- pct_df, pct_desc_df = _weighted_gains_to_plot_frames(weighted_gains)
1852
1889
  if gains_table:
1853
1890
  y_gains = weighted_gains
1854
- pct_info = {
1855
- "pct_bins": weighted_gains.shape[0],
1856
- "pct_interval": 100.0 / weighted_gains.shape[0] if weighted_gains.shape[0] else np.nan,
1857
- "pct_top_avgTrue": float(weighted_gains["AVG_BAD"].iloc[0]) if len(weighted_gains) else np.nan,
1858
- "pct_btm_avgTrue": float(weighted_gains["AVG_BAD"].iloc[-1]) if len(weighted_gains) else np.nan,
1859
- }
1891
+ pct_info = summarize_pct(pct_df, ascending=pct_ascending)
1860
1892
  else:
1861
1893
  if pct_bin_edges is not None:
1862
- pct_df = calc_fixed_pct(y_true, y_score, None, bin_edges=pct_bin_edges, ascending=True, sample_weight=sample_weight)
1894
+ pct_df = calc_fixed_pct(y_true, y_score, None, bin_edges=pct_bin_edges, ascending=pct_ascending, sample_weight=sample_weight)
1863
1895
  else:
1864
- pct_df = calc_equid_pct(y_true, y_score, None, bins=pct_bins, sample_weight=sample_weight)
1896
+ pct_df = calc_equid_pct(y_true, y_score, None, bins=pct_bins, ascending=pct_ascending, sample_weight=sample_weight)
1865
1897
 
1866
1898
  if gains_table:
1867
1899
  pct_index = pct_df["thresholds"]
1868
1900
  pct_df = resturct_gains(y_gains)
1869
1901
  pct_df["thresholds"] = pct_index
1870
1902
 
1871
- pct_info = summarize_pct(pct_df, ascending=True)
1903
+ pct_info = summarize_pct(pct_df, ascending=pct_ascending)
1872
1904
 
1873
1905
  if len(y_true) < 2 or len(np.unique(y_true)) < 2:
1874
1906
  # 返回默认性能指标(全部为NaN)
@@ -1884,9 +1916,9 @@ def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins,
1884
1916
 
1885
1917
  if sample_weight is None:
1886
1918
  if pct_bin_edges is not None:
1887
- pct_desc_df = calc_fixed_pct(y_true, y_score, None, bin_edges=pct_bin_edges, ascending=False, sample_weight=sample_weight)
1919
+ pct_desc_df = calc_fixed_pct(y_true, y_score, None, bin_edges=pct_bin_edges, ascending=gain_ascending, sample_weight=sample_weight)
1888
1920
  else:
1889
- pct_desc_df = calc_equid_pct(y_true, y_score, None, bins=pct_bins, ascending=False, sample_weight=sample_weight)
1921
+ pct_desc_df = calc_equid_pct(y_true, y_score, None, bins=pct_bins, ascending=gain_ascending, sample_weight=sample_weight)
1890
1922
 
1891
1923
  ax_roc = plt.subplot(nrow, ncol, i*ncol+1)
1892
1924
  __plot_single_roc_axes(roc_df, ax_roc, fontdicts)
@@ -1900,16 +1932,16 @@ def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins,
1900
1932
  sample_weight=sample_weight,
1901
1933
  )
1902
1934
  if sample_weight is None:
1903
- __plot_single_pct_axes(pct_df, plt.subplot(nrow, ncol, i*ncol+3), fontdicts)
1904
- __plot_single_gain_axes(pct_desc_df, plt.subplot(nrow, ncol, i*ncol+4), fontdicts)
1935
+ __plot_single_pct_axes(pct_df, plt.subplot(nrow, ncol, i*ncol+3), fontdicts, ascending=pct_ascending)
1936
+ __plot_single_gain_axes(pct_desc_df, plt.subplot(nrow, ncol, i*ncol+4), fontdicts, ascending=gain_ascending)
1905
1937
  else:
1906
1938
  _set_weighted_axis_title(ax_roc, fontdicts)
1907
1939
  _set_weighted_axis_title(ax_kde, fontdicts)
1908
1940
  ax_pct = plt.subplot(nrow, ncol, i * ncol + 3)
1909
- __plot_single_pct_axes(pct_df, ax_pct, fontdicts)
1941
+ __plot_single_pct_axes(pct_df, ax_pct, fontdicts, ascending=pct_ascending)
1910
1942
  _set_weighted_axis_title(ax_pct, fontdicts)
1911
1943
  ax_gain = plt.subplot(nrow, ncol, i * ncol + 4)
1912
- __plot_single_gain_axes(pct_desc_df, ax_gain, fontdicts)
1944
+ __plot_single_gain_axes(pct_desc_df, ax_gain, fontdicts, ascending=gain_ascending)
1913
1945
  _set_weighted_axis_title(ax_gain, fontdicts)
1914
1946
 
1915
1947
  info = {
@@ -42,8 +42,8 @@ def plot_dist_curve(dist_dfs, square_figsize = 8, fontdicts = fontdicts['main'],
42
42
  def plot_cumdist_curve(dist_dfs, square_figsize = 8, fontdicts = fontdicts['main'], to_show = True, save_path = None): ...
43
43
  def plot_pct_curve(pct_dfs, square_figsize = 8, fontdicts = fontdicts['main'], to_show = True, save_path = None): ...
44
44
  def plot_cumpct_curve(pct_dfs, square_figsize = 8, fontdicts = fontdicts['main'], to_show = True, save_path = None): ...
45
- def plot_gain_curve(pct_dfs, square_figsize = 8, fontdicts = fontdicts['main'], to_show = True, save_path = None): ...
46
- def evaluate_performance(datasets, dist_bins = 20, pct_bins = 10, square_figsize = 5, fontdicts = fontdicts['sub'], to_show = True, save_path = None, gains_table = True, equal_freq = True, pct_bin_edges = None): ...
45
+ def plot_gain_curve(pct_dfs, square_figsize = 8, fontdicts = fontdicts['main'], to_show = True, save_path = None, ascending = False): ...
46
+ def evaluate_performance(datasets, dist_bins = 20, pct_bins = 10, square_figsize = 5, fontdicts = fontdicts['sub'], to_show = True, save_path = None, gains_table = True, equal_freq = True, pct_bin_edges = None, sample_weight = None, ascending = None): ...
47
47
  def resturct_gains(gains_table): ...
48
48
  def evaluate_distribution(datasets, dist_bins = 10, square_figsize = 5, fontdicts = fontdicts['sub'], toplot = True, save_path = None): ...
49
49
  def comparison_performance(datasets, pct_bins = 10, square_figsize = 5, fontdicts = fontdicts['sub'], to_show = True, save_path = None): ...
@@ -269,6 +269,7 @@ def calc_equid_dist(y_true, y_score, bins=10, sample_weight=None, **kwargs):
269
269
  "s",
270
270
  nbins=bins,
271
271
  weight_col="w",
272
+ **kwargs,
272
273
  )
273
274
 
274
275
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import warnings
6
6
  from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
- from typing import Any, Literal, Mapping
8
+ from typing import Any, Callable, Literal, Mapping
9
9
 
10
10
  import numpy as np
11
11
  import pandas as pd
@@ -113,6 +113,11 @@ class FeatureScreenConfig:
113
113
  vif_threshold: float = 10.0
114
114
  vif_min_features: int = 2
115
115
  vif_tie_break_metric: str = "iv"
116
+ # G06-2: False (legacy) computes VIF on raw values with non-numeric
117
+ # survivors excluded+warned (raw string columns crash statsmodels);
118
+ # True computes VIF on the WOE-encoded INS view (all-numeric, matches
119
+ # the LR design-matrix collinearity semantics).
120
+ vif_use_woe_bins: bool = False
116
121
 
117
122
 
118
123
  def screen_config_from_mapping(
@@ -181,6 +186,7 @@ def screen_config_from_mapping(
181
186
  vif_threshold=float(cfg.get("vif_threshold", 10.0)),
182
187
  vif_min_features=int(cfg.get("vif_min_features", 2)),
183
188
  vif_tie_break_metric=str(cfg.get("vif_tie_break_metric", "iv")),
189
+ vif_use_woe_bins=bool(cfg.get("vif_use_woe_bins", False)),
184
190
  )
185
191
 
186
192
 
@@ -199,6 +205,10 @@ def fit_screening_woe_engine(
199
205
  from Modeling_Tool.Pipeline._common import apply_woe_fit_query
200
206
 
201
207
  fit_ins, _ = apply_woe_fit_query(train, woe_fit_query, target=target_col)
208
+ # A screening WOE fit is an internal diagnostic artifact. In particular,
209
+ # WOE_Master can append transformed columns while fitting/transforming, so
210
+ # never let an opt-in screen stage mutate the caller's INS frame.
211
+ fit_ins = fit_ins.copy()
202
212
  if woe_engine.lower() == "monotone":
203
213
  from Modeling_Tool import MonotoneWOEBinner
204
214
 
@@ -236,7 +246,12 @@ def fit_screening_woe_engine(
236
246
 
237
247
 
238
248
  def _needs_woe_bins(config: FeatureScreenConfig) -> bool:
239
- return config.psi_use_woe_bins or config.iv_use_woe_bins or config.corr_use_woe_bins
249
+ return (
250
+ config.psi_use_woe_bins
251
+ or config.iv_use_woe_bins
252
+ or config.corr_use_woe_bins
253
+ or (config.vif_enabled and config.vif_use_woe_bins)
254
+ )
240
255
 
241
256
 
242
257
  def _resolve_screening_binner(
@@ -250,6 +265,22 @@ def _resolve_screening_binner(
250
265
  return prefit_woe_engine
251
266
  if not _needs_woe_bins(config):
252
267
  return None
268
+ categorical = [
269
+ col for col in (config.categorical_features or []) if col in feature_cols
270
+ ]
271
+ if (
272
+ config.vif_enabled
273
+ and config.vif_use_woe_bins
274
+ and categorical
275
+ and str(config.woe_engine).lower() != "monotone"
276
+ ):
277
+ raise ValueError(
278
+ "vif_use_woe_bins=True with categorical_features requires "
279
+ "woe_engine='monotone'. The equal_freq WOE_Master screening "
280
+ "engine does not support categorical fitting on this path; use "
281
+ "the monotone engine or leave vif_use_woe_bins=False to retain "
282
+ "the raw-basis exclude-and-warn behavior."
283
+ )
253
284
  return fit_screening_woe_engine(
254
285
  splits["ins"],
255
286
  feature_cols,
@@ -393,6 +424,11 @@ def _woe_bins_unweighted_screen(
393
424
  ins, current, config, selection_evidence, gate_iv_map,
394
425
  summary_rows, dropped_rows, stage_tables,
395
426
  weight_col=None, on_empty_stage=config.on_empty_stage,
427
+ woe_frame_fn=(
428
+ _vif_woe_frame_fn(ins, binner)
429
+ if config.vif_enabled and config.vif_use_woe_bins
430
+ else None
431
+ ),
396
432
  )
397
433
 
398
434
  summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, None))
@@ -427,6 +463,50 @@ def _bins_to_numpy(bins: Any) -> np.ndarray:
427
463
  return np.asarray(bins, dtype=object)
428
464
 
429
465
 
466
+ def _vif_woe_frame_fn(
467
+ ins: pd.DataFrame, binner: Any | None
468
+ ) -> Callable[[list[str]], pd.DataFrame] | None:
469
+ """Lazy WOE-encoded INS view for the VIF gate (G06-2,
470
+ ``vif_use_woe_bins=True``). Returns None when no screening engine was
471
+ fitted — the gate raises an actionable error in that case. Columns are
472
+ renamed back to the raw feature names so drop bookkeeping stays on the
473
+ selection's own vocabulary."""
474
+ if binner is None:
475
+ return None
476
+ def _frame(feats: list[str]) -> pd.DataFrame:
477
+ names = list(feats)
478
+ # Construct the adapter only when this opt-in gate actually reaches
479
+ # VIF. This preserves the historical opaque-prefit-engine path when
480
+ # VIF/WOE is disabled and avoids doing work at a min-feature floor.
481
+ from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
482
+
483
+ engine_suffix = str(getattr(binner, "woe_suffix", "_woe"))
484
+ try:
485
+ adapter = as_woe_engine(binner, woe_suffix=engine_suffix)
486
+ except TypeError as exc:
487
+ raise ValueError(
488
+ "vif_use_woe_bins=True requires a supported fitted WOE "
489
+ "engine (WOE_Master, MonotoneWOEBinner, or WOEEngineAdapter)."
490
+ ) from exc
491
+ suffix = adapter.woe_suffix
492
+ # Transform a copy as well: some legacy engines retain or append
493
+ # intermediate WOE columns on the passed frame.
494
+ tx = adapter.transform(ins.copy(), varlist=names, suffix=suffix)
495
+ expected = [f"{name}{suffix}" for name in names]
496
+ missing = [name for name, column in zip(names, expected) if column not in tx.columns]
497
+ if missing:
498
+ raise ValueError(
499
+ "vif_use_woe_bins=True could not produce WOE columns for "
500
+ f"{missing!r}. Refit the screening WOE engine or supply a "
501
+ "prefit engine that covers every surviving feature."
502
+ )
503
+ frame = tx[expected].copy()
504
+ frame.columns = names
505
+ return frame
506
+
507
+ return _frame
508
+
509
+
430
510
  def _screen_engine_payload(
431
511
  binner: Any | None,
432
512
  *,
@@ -647,6 +727,11 @@ def _weighted_woe_bins_screen(
647
727
  ins, current, config, selection_evidence, gate_iv_map,
648
728
  summary_rows, dropped_rows, stage_tables,
649
729
  weight_col=weight_col, on_empty_stage=config.on_empty_stage,
730
+ woe_frame_fn=(
731
+ _vif_woe_frame_fn(ins, binner)
732
+ if config.vif_enabled and config.vif_use_woe_bins
733
+ else None
734
+ ),
650
735
  )
651
736
 
652
737
  summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, weight_col))