SuperModelingFactory 0.5.8__tar.gz → 0.5.10__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 (113) hide show
  1. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Binning_Tool.py +57 -29
  2. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Binning_Tool.pyi +1 -1
  3. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/utils.py +22 -21
  4. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/_common.py +6 -0
  5. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/credit_model.py +1 -0
  6. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/feature_validation.py +72 -23
  7. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/score_comparison.py +13 -5
  8. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/screening_artifact.py +29 -6
  9. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Master.py +27 -14
  10. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Tool.py +189 -103
  11. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Tool.pyi +1 -1
  12. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/__init__.py +1 -1
  13. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/PKG-INFO +2 -2
  14. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/README.md +1 -1
  15. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  16. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/pyproject.toml +1 -1
  17. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/setup.py +1 -1
  18. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/ExcelMaster/ExcelFormatTool.py +0 -0
  19. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/ExcelMaster/ExcelMaster.py +0 -0
  20. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/ExcelMaster/Template.py +0 -0
  21. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/ExcelMaster/Utility.py +0 -0
  22. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/ExcelMaster/__init__.py +0 -0
  23. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/LICENSE +0 -0
  24. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/MANIFEST.in +0 -0
  25. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  26. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  27. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  28. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  29. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  30. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  31. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  32. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  33. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  34. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  35. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  36. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/__init__.py +0 -0
  37. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/kDataFrame.py +0 -0
  38. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  39. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  40. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  41. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  42. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  43. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  44. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/__init__.py +0 -0
  45. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  46. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  47. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  48. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  49. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  50. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Explainability/__init__.py +0 -0
  51. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  52. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  53. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  54. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  55. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
  56. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  57. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  58. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  59. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Weighted_Screen.py +0 -0
  60. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/__init__.py +0 -0
  61. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  62. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  63. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  64. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  65. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  66. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  67. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  68. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/__init__.py +0 -0
  69. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/__init__.py +0 -0
  70. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/field_meta.py +0 -0
  71. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  72. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  73. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  74. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  75. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  76. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  77. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  78. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  79. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  80. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  81. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  82. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/__init__.py +0 -0
  83. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  84. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/UAT/__init__.py +0 -0
  85. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  86. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  87. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  88. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  89. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  90. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  91. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  92. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  93. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  94. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/__init__.py +0 -0
  95. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  96. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  97. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/__init__.py +0 -0
  98. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/nan_guard.py +0 -0
  99. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  108. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  109. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  110. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/requires.txt +0 -0
  111. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  112. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/requirements.txt +0 -0
  113. {supermodelingfactory-0.5.8 → supermodelingfactory-0.5.10}/setup.cfg +0 -0
@@ -706,6 +706,46 @@ def get_bin_range(edges, precision = 5, ascending = False, left_sign = '(', righ
706
706
  return res
707
707
 
708
708
 
709
+ def _materialize_bin_columns(data, binned, bin_range_list, bin_num_col, bin_range_col):
710
+ """Attach categorical bin numbers and labels without Python row callbacks."""
711
+ codes = binned.cat.codes.to_numpy(dtype=np.intp, copy=False)
712
+ range_values = np.empty(len(codes), dtype=object)
713
+ range_values[:] = np.nan
714
+ valid = codes >= 0
715
+ if valid.any():
716
+ labels = np.asarray(bin_range_list, dtype=object)
717
+ if codes[valid].max() >= len(labels):
718
+ raise ValueError("Binning category codes do not match bin range labels")
719
+ range_values[valid] = np.take(labels, codes[valid])
720
+
721
+ data[bin_num_col] = binned.astype(object)
722
+ data[bin_range_col] = range_values
723
+ return data
724
+
725
+
726
+ def _parse_bin_range_bounds(data, col="_bin_range"):
727
+ """Parse interval labels into numeric left and right bounds vectorially."""
728
+ if col not in data.columns:
729
+ raise KeyError(f"Column {col!r} is not present in the bin mapping table")
730
+
731
+ ranges = data[col].astype("string").str.strip()
732
+ bounds = ranges.str.extract(
733
+ r"^[\[\(]\s*([^,]+?)\s*,\s*([^\]\)]+?)\s*[\]\)]$",
734
+ expand=True,
735
+ )
736
+ invalid = bounds.isna().any(axis=1)
737
+ if invalid.any():
738
+ examples = ranges.loc[invalid].dropna().head(3).tolist()
739
+ raise ValueError(f"Unable to parse bin range labels in {col!r}: {examples}")
740
+
741
+ try:
742
+ left = bounds.iloc[:, 0].str.strip().astype(float).to_numpy()
743
+ right = bounds.iloc[:, 1].str.strip().astype(float).to_numpy()
744
+ except (TypeError, ValueError) as exc:
745
+ raise ValueError(f"Bin range column {col!r} contains non-numeric bounds") from exc
746
+ return left, right
747
+
748
+
709
749
  def get_bin_range_list(data, col = "_bin_range"):
710
750
  """
711
751
  将分箱区间字符串列转换为边界值列表。
@@ -730,21 +770,8 @@ def get_bin_range_list(data, col = "_bin_range"):
730
770
  >>> unique_range = get_bin_range_list(data, col="bin_range")
731
771
  """
732
772
 
733
- data["bin_value_list"] = data[col].apply(lambda x: x.replace("[", "")\
734
- .replace("]", "")\
735
- .replace("(", "")\
736
- .replace(")", "")\
737
- .split(","))
738
- bin_range = [np.inf if v.strip() == 'inf' else -np.inf if v.strip() == '-inf' else float(v.strip()) for x in data["bin_value_list"].tolist() for v in x]
739
-
740
- unique_range = []
741
- for x in bin_range:
742
- if x not in unique_range:
743
- unique_range.append(x)
744
-
745
- unique_range.sort()
746
-
747
- return unique_range
773
+ left, right = _parse_bin_range_bounds(data, col=col)
774
+ return np.unique(np.concatenate([left, right])).tolist()
748
775
 
749
776
 
750
777
  def chi2_auto_binning(df_pvt, max_bins, min_cnt_in_bin, p=0.95):
@@ -1165,9 +1192,8 @@ class Binning:
1165
1192
  bin_num_col = self.bin_colnames[0]
1166
1193
  bin_range_col = self.bin_colnames[1]
1167
1194
 
1168
- self.data[bin_num_col] = binned.astype(object)
1169
- self.data[bin_range_col] = self.data[bin_num_col].apply(
1170
- lambda x: bin_range_list[int(x)] if self.include_missing else bin_range_list[int(x - 1)]
1195
+ self.data = _materialize_bin_columns(
1196
+ self.data, binned, bin_range_list, bin_num_col, bin_range_col
1171
1197
  )
1172
1198
 
1173
1199
  self.result = self.data
@@ -1237,9 +1263,8 @@ class Binning:
1237
1263
  bin_num_col = self.bin_colnames[0]
1238
1264
  bin_range_col = self.bin_colnames[1]
1239
1265
 
1240
- self.data[bin_num_col] = binned.astype(object)
1241
- self.data[bin_range_col] = self.data[bin_num_col].apply(
1242
- lambda x: bin_range_list[int(x)] if self.include_missing else bin_range_list[int(x - 1)]
1266
+ self.data = _materialize_bin_columns(
1267
+ self.data, binned, bin_range_list, bin_num_col, bin_range_col
1243
1268
  )
1244
1269
 
1245
1270
  self.bin_edges = sorted([np.inf if str(x).lower() == 'inf' else -np.inf if str(x).lower() == '-inf' else x for x in bin_edges])
@@ -1385,10 +1410,9 @@ def chi2_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, t
1385
1410
  binned = binning_series
1386
1411
  rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not include_missing else [i for i in range(0, len(bin_range_list))]
1387
1412
  binned = binned.cat.rename_categories(rename_catlist)
1388
- data[bin_num_col] = binned.astype(object)
1389
- # print(bin_range_list)
1390
- # print(data[bin_num_col])
1391
- data[bin_range_col] = data[bin_num_col].apply(lambda x: bin_range_list[int(x)] if include_missing else bin_range_list[int(x - 1)])
1413
+ data = _materialize_bin_columns(
1414
+ data, binned, bin_range_list, bin_num_col, bin_range_col
1415
+ )
1392
1416
 
1393
1417
  fnl_res = data
1394
1418
 
@@ -1399,7 +1423,7 @@ def chi2_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, t
1399
1423
 
1400
1424
  def run_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, equal_freq = True,
1401
1425
  bin_colnames = ("bin_num", "bin_range"), ascending = False, right = True, include_lowest = False,
1402
- tree_binning = False, target = None, random_state=42, spec_values = []):
1426
+ tree_binning = False, target = None, random_state=42, spec_values = [], fillna = -999999):
1403
1427
  """
1404
1428
  通用分箱函数,支持等频或等距分箱。
1405
1429
 
@@ -1437,6 +1461,8 @@ def run_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, in
1437
1461
  随机种子
1438
1462
  spec_values : list, default []
1439
1463
  特殊值列表
1464
+ fillna : scalar, default -999999
1465
+ `include_missing=True` 时用于承载缺失值的分箱哨兵。
1440
1466
 
1441
1467
  Returns
1442
1468
  -------
@@ -1481,7 +1507,8 @@ def run_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, in
1481
1507
  ascending = ascending,
1482
1508
  include_missing = include_missing,
1483
1509
  random_state = random_state,
1484
- spec_values = spec_values)
1510
+ spec_values = spec_values,
1511
+ fillna = fillna)
1485
1512
 
1486
1513
  left_sign='[' if include_lowest else '('
1487
1514
  right_sign=']' if right else ')'
@@ -1489,8 +1516,9 @@ def run_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, in
1489
1516
 
1490
1517
  rename_catlist = [i for i in range(1, len(bin_range_list) + 1)] if not include_missing else [i for i in range(0, len(bin_range_list))]
1491
1518
  binned = binned.cat.rename_categories(rename_catlist)
1492
- data[bin_num_col] = binned.astype(object)
1493
- data[bin_range_col] = data[bin_num_col].apply(lambda x: bin_range_list[int(x)] if include_missing else bin_range_list[int(x - 1)])
1519
+ data = _materialize_bin_columns(
1520
+ data, binned, bin_range_list, bin_num_col, bin_range_col
1521
+ )
1494
1522
 
1495
1523
  return data, bin_edges
1496
1524
 
@@ -44,5 +44,5 @@ class Binning:
44
44
  def run(self): ...
45
45
  def get_result(self, return_edges = True): ...
46
46
  def chi2_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, tgt_name = None, include_missing = True, equal_freq = True, bin_colnames = ('_bin_num', '_bin_range'), ascending = True, chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], init_points = None): ...
47
- def run_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, equal_freq = True, bin_colnames = ('bin_num', 'bin_range'), ascending = False, right = True, include_lowest = False, tree_binning = False, target = None, random_state = 42, spec_values = []): ...
47
+ def run_binning(data, column, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, equal_freq = True, bin_colnames = ('bin_num', 'bin_range'), ascending = False, right = True, include_lowest = False, tree_binning = False, target = None, random_state = 42, spec_values = [], fillna = -999999): ...
48
48
  def super_binning(data, score, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, equal_freq = True, chi2_method = False, chi2_p = 0.95, init_equi_bins = 2000, fillna = -999999, spec_values = [], tree_binning = False, random_state = 42, return_edges = False, ascending = True, bin_colnames = ('_bin_num', '_bin_range')): ...
@@ -1688,6 +1688,19 @@ def parse_sql_file(sql_path:str=None,
1688
1688
 
1689
1689
 
1690
1690
 
1691
+ def _calc_woe_iv_values(data, bad_pct, good_pct, fillwoe=True, filliv=True):
1692
+ """Calculate WOE and IV together with one vectorized logarithm."""
1693
+ if len(data[bad_pct]) > 0 and len(data[good_pct]) > 0:
1694
+ bad_values = data[bad_pct]
1695
+ good_values = data[good_pct]
1696
+ woe = np.log(bad_values / good_values)
1697
+ iv = (bad_values - good_values) * woe
1698
+ else:
1699
+ woe = 0 if fillwoe else np.nan
1700
+ iv = 0 if filliv else np.nan
1701
+ return woe, iv
1702
+
1703
+
1691
1704
  def calc_woe(data, bad_pct, good_pct, fillwoe=True):
1692
1705
  """
1693
1706
  计算WOE(Weight of Evidence)值。
@@ -1716,15 +1729,7 @@ def calc_woe(data, bad_pct, good_pct, fillwoe=True):
1716
1729
  >>> calc_woe(df, 'bad_pct', 'good_pct')
1717
1730
  """
1718
1731
 
1719
- if len(data[bad_pct]) > 0 and len(data[good_pct]) > 0:
1720
- woe = np.log(data[bad_pct] / data[good_pct])
1721
- else:
1722
- if fillwoe:
1723
- woe = 0
1724
- else:
1725
- woe = np.nan
1726
-
1727
- return woe
1732
+ return _calc_woe_iv_values(data, bad_pct, good_pct, fillwoe=fillwoe)[0]
1728
1733
 
1729
1734
 
1730
1735
  def calc_iv(data, bad_pct, good_pct, filliv=True):
@@ -1755,15 +1760,7 @@ def calc_iv(data, bad_pct, good_pct, filliv=True):
1755
1760
  >>> calc_iv(df, 'bad_pct', 'good_pct')
1756
1761
  """
1757
1762
 
1758
- if len(data[bad_pct]) > 0 and len(data[good_pct]) > 0:
1759
- iv = (data[bad_pct] - data[good_pct]) * np.log(data[bad_pct] / data[good_pct])
1760
- else:
1761
- if filliv:
1762
- iv = 0
1763
- else:
1764
- iv = np.nan
1765
-
1766
- return iv
1763
+ return _calc_woe_iv_values(data, bad_pct, good_pct, filliv=filliv)[1]
1767
1764
 
1768
1765
 
1769
1766
  def save_model(model, filename):
@@ -2413,9 +2410,13 @@ class WOEIVCalculator:
2413
2410
  -------
2414
2411
  tuple
2415
2412
  """
2416
- woe = self.calc_woe(fillna)
2417
- iv = self.calc_iv(fillna)
2418
- return woe, iv
2413
+ return _calc_woe_iv_values(
2414
+ self.data,
2415
+ self.bad_pct_col,
2416
+ self.good_pct_col,
2417
+ fillwoe=fillna,
2418
+ filliv=fillna,
2419
+ )
2419
2420
 
2420
2421
 
2421
2422
  def get_feature_names(model, model_type=None):
@@ -55,6 +55,12 @@ def as_list(value: Any | Iterable[Any] | None, default: list[Any] | None = None)
55
55
  return list(value)
56
56
 
57
57
 
58
+ def normalize_split_values(values: pd.Series) -> pd.Series:
59
+ """Normalize declared split labels for case-insensitive pipeline routing."""
60
+ normalized = values.astype("string").str.strip().str.lower()
61
+ return normalized.mask(normalized.eq(""), pd.NA)
62
+
63
+
58
64
  def normalize_group_specs(
59
65
  value: Any,
60
66
  *,
@@ -446,6 +446,7 @@ class CreditModelPipeline:
446
446
  return FeatureScreeningArtifact.from_fvp_result(
447
447
  cfg.feature_validation_result,
448
448
  target_col=cfg.target_col,
449
+ weight_col=cfg.weight_col,
449
450
  )
450
451
  return None
451
452
 
@@ -17,6 +17,7 @@ from ._common import (
17
17
  as_list,
18
18
  make_dirs,
19
19
  normalize_group_specs,
20
+ normalize_split_values,
20
21
  safe_to_csv,
21
22
  split_oot_by_flag,
22
23
  validate_woe_fit_query_columns,
@@ -132,6 +133,7 @@ class FeatureValidationPipelineResult:
132
133
  selected_features: list[str] = field(default_factory=list)
133
134
  selection_summary: dict[str, Any] = field(default_factory=dict)
134
135
  screening_artifact: Any | None = None
136
+ config_snapshot: dict[str, Any] = field(default_factory=dict)
135
137
 
136
138
 
137
139
  class FeatureValidationPipeline:
@@ -180,6 +182,12 @@ class FeatureValidationPipeline:
180
182
  incumbent_features = self._resolve_incumbent_features(work, new_features)
181
183
  target_cols = [col for col in as_list(cfg.target_cols) if col in work.columns]
182
184
  self._validate_input(work, new_features, incumbent_features, target_cols)
185
+ config_snapshot = self._build_config_snapshot(
186
+ new_features,
187
+ incumbent_features,
188
+ target_cols,
189
+ batch_mode=False,
190
+ )
183
191
 
184
192
  output_dir = Path(cfg.output_dir)
185
193
  if cfg.write_outputs or cfg.write_excel:
@@ -224,6 +232,7 @@ class FeatureValidationPipeline:
224
232
  incumbent_features=incumbent_features,
225
233
  target_cols=target_cols,
226
234
  woe_artifacts=woe_artifacts,
235
+ config_snapshot=config_snapshot,
227
236
  )
228
237
  validation_summary = self._build_validation_summary(
229
238
  data=combined,
@@ -270,6 +279,7 @@ class FeatureValidationPipeline:
270
279
  selected_features=selected_features,
271
280
  selection_summary=selection_summary,
272
281
  screening_artifact=screening_artifact,
282
+ config_snapshot=config_snapshot,
273
283
  )
274
284
 
275
285
  def _resolve_input_type(self, data: pd.DataFrame | str | Path) -> str:
@@ -407,6 +417,7 @@ class FeatureValidationPipeline:
407
417
  selected_features=list(result.selected_features),
408
418
  selection_summary=dict(result.selection_summary or {}),
409
419
  screening_artifact=result.screening_artifact,
420
+ config_snapshot=dict(result.config_snapshot or {}),
410
421
  )
411
422
 
412
423
  def _validate_batch_config(self, new_features: list[str]) -> None:
@@ -496,6 +507,10 @@ class FeatureValidationPipeline:
496
507
  return self._dedupe([col for col in candidates if col and col in header_set])
497
508
 
498
509
  def _stable_split_labels(self, base_df: pd.DataFrame, splits: dict[str, pd.DataFrame]) -> pd.Series:
510
+ cfg = self.config
511
+ if cfg.split_col and cfg.split_col in base_df.columns:
512
+ return normalize_split_values(base_df[cfg.split_col])
513
+
499
514
  labels = pd.Series("ins", index=base_df.index, dtype=object)
500
515
  oos_ids = set(splits.get("oos", pd.DataFrame()).get("_smf_batch_row_id", pd.Series(dtype=int)).tolist())
501
516
  oot_ids = set(splits.get("oot", pd.DataFrame()).get("_smf_batch_row_id", pd.Series(dtype=int)).tolist())
@@ -503,6 +518,11 @@ class FeatureValidationPipeline:
503
518
  row_ids = base_df["_smf_batch_row_id"]
504
519
  labels.loc[row_ids.isin(oos_ids)] = "oos"
505
520
  labels.loc[row_ids.isin(unique_oot_ids)] = "oot"
521
+ for name, frame in splits.items():
522
+ if name in {"ins", "oos", "oot"}:
523
+ continue
524
+ ids = set(frame.get("_smf_batch_row_id", pd.Series(dtype=int)).tolist())
525
+ labels.loc[row_ids.isin(ids)] = name
506
526
  return labels
507
527
 
508
528
  def _merge_batch_results(
@@ -554,6 +574,12 @@ class FeatureValidationPipeline:
554
574
  selected_features = self._dedupe(
555
575
  [feat for res in batch_results for feat in as_list(res.selected_features)]
556
576
  )
577
+ config_snapshot = self._build_config_snapshot(
578
+ new_features,
579
+ incumbent_features,
580
+ target_cols,
581
+ batch_mode=True,
582
+ )
557
583
  selection_summary: dict[str, Any] = {}
558
584
  screening_artifact = None
559
585
  if self.config.selection_enabled and target_cols:
@@ -561,12 +587,7 @@ class FeatureValidationPipeline:
561
587
  "initial_features": list(new_features),
562
588
  "final_features": selected_features,
563
589
  "target_col": target_cols[0],
564
- "config_snapshot": {
565
- "selection_enabled": True,
566
- "selection_params": dict(self.config.selection_params or {}),
567
- "weight_col": self.config.weight_col,
568
- "batch_mode": True,
569
- },
590
+ "config_snapshot": config_snapshot,
570
591
  }
571
592
  if selected_features:
572
593
  from .screening_artifact import FeatureScreeningArtifact
@@ -619,6 +640,7 @@ class FeatureValidationPipeline:
619
640
  selected_features=selected_features,
620
641
  selection_summary=selection_summary,
621
642
  screening_artifact=screening_artifact,
643
+ config_snapshot=config_snapshot,
622
644
  )
623
645
 
624
646
  @staticmethod
@@ -854,6 +876,30 @@ class FeatureValidationPipeline:
854
876
  ]
855
877
  return pd.DataFrame(rows)
856
878
 
879
+ def _build_config_snapshot(
880
+ self,
881
+ new_features: list[str],
882
+ incumbent_features: list[str],
883
+ target_cols: list[str],
884
+ *,
885
+ batch_mode: bool,
886
+ ) -> dict[str, Any]:
887
+ cfg = self.config
888
+ return {
889
+ "selection_enabled": bool(cfg.selection_enabled),
890
+ "selection_params": dict(cfg.selection_params or {}),
891
+ "weight_col": cfg.weight_col,
892
+ "target_col": target_cols[0] if target_cols else None,
893
+ "target_cols": list(target_cols),
894
+ "new_feature_cols": list(new_features),
895
+ "incumbent_feature_cols": list(incumbent_features),
896
+ "woe_engine": cfg.woe_engine,
897
+ "woe_fit_query": cfg.woe_fit_query,
898
+ "woe_params": dict(cfg.woe_params or {}),
899
+ "monotone_woe_params": dict(cfg.monotone_woe_params or {}),
900
+ "batch_mode": bool(batch_mode),
901
+ }
902
+
857
903
  def _validate_input(
858
904
  self,
859
905
  data: pd.DataFrame,
@@ -864,6 +910,8 @@ class FeatureValidationPipeline:
864
910
  cfg = self.config
865
911
  missing = [col for col in [cfg.id_col, cfg.apply_time_col] if col and col not in data.columns]
866
912
  missing += [col for col in new_features + incumbent_features + target_cols if col not in data.columns]
913
+ if cfg.weight_col and cfg.weight_col not in data.columns:
914
+ missing.append(cfg.weight_col)
867
915
  if missing:
868
916
  raise KeyError(f"Missing required columns: {sorted(set(missing))}")
869
917
  if not new_features:
@@ -917,18 +965,20 @@ class FeatureValidationPipeline:
917
965
  raise KeyError(f"Missing split_col {cfg.split_col!r}")
918
966
  if sample_col in work.columns:
919
967
  raw_split = work[sample_col]
920
- lower = raw_split.astype(str).str.strip().str.lower()
921
- if cfg.split_col:
922
- invalid = sorted(set(raw_split.dropna().astype(str).str.strip().str.lower()) - {"ins", "oos", "oot"})
923
- if invalid:
924
- raise ValueError(f"split_col {cfg.split_col!r} only supports ins/oos/oot values, got {invalid}")
925
- ins = work[lower == "ins"].copy()
926
- oos = work[lower == "oos"].copy()
927
- oot = work[lower == "oot"].copy()
968
+ normalized = normalize_split_values(raw_split)
969
+ ins = work[normalized.eq("ins").fillna(False)].copy()
970
+ oos = work[normalized.eq("oos").fillna(False)].copy()
971
+ oot = work[normalized.eq("oot").fillna(False)].copy()
928
972
  if len(ins) and len(oos):
929
973
  if not len(oot):
930
974
  oot = oos.copy()
931
- return {"ins": ins, "oos": oos, "oot": oot}
975
+ splits = {"ins": ins, "oos": oos, "oot": oot}
976
+ if cfg.split_col:
977
+ reserved = set(splits)
978
+ for label in normalized.dropna().drop_duplicates().tolist():
979
+ if label not in reserved:
980
+ splits[str(label)] = work[normalized.eq(label).fillna(False)].copy()
981
+ return splits
932
982
  if cfg.split_col:
933
983
  raise ValueError(f"split_col {cfg.split_col!r} must contain non-empty ins and oos samples")
934
984
 
@@ -1251,6 +1301,7 @@ class FeatureValidationPipeline:
1251
1301
  incumbent_features: list[str],
1252
1302
  target_cols: list[str],
1253
1303
  woe_artifacts: dict[str, Any],
1304
+ config_snapshot: dict[str, Any],
1254
1305
  ) -> tuple[list[str], dict[str, Any], Any | None]:
1255
1306
  from Modeling_Tool.Feature.Feature_Screen import feature_screen
1256
1307
  from .screening_artifact import FeatureScreeningArtifact, screen_result_to_summary
@@ -1274,24 +1325,22 @@ class FeatureValidationPipeline:
1274
1325
  config=screen_cfg,
1275
1326
  prefit_woe_engine=engine,
1276
1327
  )
1277
- config_snapshot = {
1278
- "selection_enabled": True,
1279
- "selection_params": dict(cfg.selection_params or {}),
1328
+ selection_config_snapshot = dict(config_snapshot)
1329
+ selection_config_snapshot.update({
1280
1330
  "missing_rate_threshold": screen_cfg.missing_rate_threshold,
1281
- "weight_col": cfg.weight_col,
1282
1331
  "psi_use_woe_bins": screen_cfg.psi_use_woe_bins,
1283
1332
  "iv_use_woe_bins": screen_cfg.iv_use_woe_bins,
1284
1333
  "corr_use_woe_bins": screen_cfg.corr_use_woe_bins,
1285
1334
  "n_incumbent_features": len(incumbent_features),
1286
- }
1335
+ })
1287
1336
  selection_summary = {
1288
1337
  "initial_features": list(new_features),
1289
1338
  "final_features": list(result.selected_features),
1290
1339
  "target_col": target,
1291
- "config_snapshot": config_snapshot,
1340
+ "config_snapshot": selection_config_snapshot,
1292
1341
  }
1293
1342
  selection_summary.update(screen_result_to_summary(result, new_features))
1294
- selection_summary["config_snapshot"] = config_snapshot
1343
+ selection_summary["config_snapshot"] = selection_config_snapshot
1295
1344
  artifact = FeatureScreeningArtifact.from_screen_result(
1296
1345
  result,
1297
1346
  initial_features=list(new_features),
@@ -1299,7 +1348,7 @@ class FeatureValidationPipeline:
1299
1348
  weight_col=cfg.weight_col,
1300
1349
  woe_artifacts=woe_artifacts,
1301
1350
  source="fvp",
1302
- config_snapshot=config_snapshot,
1351
+ config_snapshot=selection_config_snapshot,
1303
1352
  )
1304
1353
  return list(result.selected_features), selection_summary, artifact
1305
1354
 
@@ -7,7 +7,14 @@ from typing import Any, Callable
7
7
 
8
8
  import pandas as pd
9
9
 
10
- from ._common import as_list, make_dirs, normalize_group_specs, safe_to_csv, write_basic_excel
10
+ from ._common import (
11
+ as_list,
12
+ make_dirs,
13
+ normalize_group_specs,
14
+ normalize_split_values,
15
+ safe_to_csv,
16
+ write_basic_excel,
17
+ )
11
18
 
12
19
  _logger = logging.getLogger(__name__)
13
20
 
@@ -287,6 +294,8 @@ class ScoreComparisonPipeline:
287
294
  data.loc[missing_mask, col] = pd.NA
288
295
  elif cfg.include_missing:
289
296
  data.loc[missing_mask, col] = "[Missing]"
297
+ if cfg.split_col and cfg.split_col in data.columns:
298
+ data[cfg.split_col] = normalize_split_values(data[cfg.split_col])
290
299
 
291
300
  def _resolve_scores(self, data: pd.DataFrame) -> list[str]:
292
301
  cfg = self.config
@@ -314,10 +323,9 @@ class ScoreComparisonPipeline:
314
323
  if missing:
315
324
  raise KeyError(f"Missing required columns: {missing}")
316
325
  if cfg.split_col:
317
- values = data[cfg.split_col].dropna().astype(str).str.strip().str.lower()
318
- invalid = sorted(set(values) - {"ins", "oos", "oot"})
319
- if invalid:
320
- raise ValueError(f"split_col {cfg.split_col!r} only supports ins/oos/oot values, got {invalid}")
326
+ values = normalize_split_values(data[cfg.split_col]).dropna()
327
+ if values.empty:
328
+ raise ValueError(f"split_col {cfg.split_col!r} must contain at least one non-empty value")
321
329
 
322
330
  def _custom_metrics_func(self, sub_df: pd.DataFrame) -> pd.Series:
323
331
  cfg = self.config
@@ -81,24 +81,47 @@ class FeatureScreeningArtifact:
81
81
  result: Any,
82
82
  *,
83
83
  target_col: str | None = None,
84
+ weight_col: str | None = None,
84
85
  ) -> FeatureScreeningArtifact:
85
86
  if getattr(result, "screening_artifact", None) is not None:
86
87
  return result.screening_artifact
88
+ summary = dict(getattr(result, "selection_summary", {}) or {})
89
+ summary_snapshot = dict(summary.get("config_snapshot", {}) or {})
90
+ result_snapshot = dict(getattr(result, "config_snapshot", {}) or {})
91
+ config_snapshot = {**summary_snapshot, **result_snapshot}
87
92
  resolved_target = target_col
88
93
  if not resolved_target:
89
- summary = getattr(result, "selection_summary", {}) or {}
90
94
  resolved_target = summary.get("target_col")
95
+ if not resolved_target:
96
+ resolved_target = config_snapshot.get("target_col")
97
+ if not resolved_target:
98
+ target_cols = list(config_snapshot.get("target_cols") or [])
99
+ resolved_target = target_cols[0] if target_cols else None
91
100
  if not resolved_target:
92
101
  raise ValueError("target_col is required when building artifact from FVP result.")
93
- summary = dict(getattr(result, "selection_summary", {}) or {})
94
- config_snapshot = dict(summary.get("config_snapshot", {}) or {})
102
+ selected_features = list(getattr(result, "selected_features", []) or [])
103
+ if not selected_features and config_snapshot.get("selection_enabled") is False:
104
+ selected_features = list(config_snapshot.get("new_feature_cols") or [])
105
+ if not summary and config_snapshot.get("selection_enabled") is False:
106
+ summary = {
107
+ "initial_features": list(selected_features),
108
+ "final_features": list(selected_features),
109
+ "target_col": str(resolved_target),
110
+ "selection_skipped": True,
111
+ "config_snapshot": config_snapshot,
112
+ }
113
+ resolved_weight_col = (
114
+ config_snapshot["weight_col"]
115
+ if "weight_col" in config_snapshot
116
+ else weight_col
117
+ )
95
118
  return cls(
96
- selected_features=list(getattr(result, "selected_features", []) or []),
119
+ selected_features=selected_features,
97
120
  selection_summary=summary,
98
121
  woe_artifacts=getattr(result, "woe_artifacts", None),
99
122
  source="fvp",
100
123
  target_col=str(resolved_target),
101
- weight_col=config_snapshot.get("weight_col"),
124
+ weight_col=resolved_weight_col,
102
125
  config_snapshot=config_snapshot,
103
126
  created_at=datetime.now(timezone.utc).isoformat(),
104
127
  )
@@ -124,4 +147,4 @@ class FeatureScreeningArtifact:
124
147
  __all__ = [
125
148
  "FeatureScreeningArtifact",
126
149
  "screen_result_to_summary",
127
- ]
150
+ ]