SuperModelingFactory 0.5.9__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.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Binning_Tool.py +57 -29
  2. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Binning_Tool.pyi +1 -1
  3. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/utils.py +22 -21
  4. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Master.py +27 -14
  5. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Tool.py +189 -103
  6. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Tool.pyi +1 -1
  7. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/__init__.py +1 -1
  8. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/PKG-INFO +2 -2
  9. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/README.md +1 -1
  10. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  11. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/pyproject.toml +1 -1
  12. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/setup.py +1 -1
  13. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/ExcelMaster/ExcelFormatTool.py +0 -0
  14. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/ExcelMaster/ExcelMaster.py +0 -0
  15. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/ExcelMaster/Template.py +0 -0
  16. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/ExcelMaster/Utility.py +0 -0
  17. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/ExcelMaster/__init__.py +0 -0
  18. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/LICENSE +0 -0
  19. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/MANIFEST.in +0 -0
  20. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  21. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  22. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  23. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  24. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  25. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  26. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  27. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  28. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  29. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  30. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  31. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/__init__.py +0 -0
  32. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/kDataFrame.py +0 -0
  33. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  34. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  35. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  36. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  37. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  38. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  39. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/__init__.py +0 -0
  40. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  41. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  42. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  43. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  44. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  45. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Explainability/__init__.py +0 -0
  46. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  47. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  48. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  49. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  50. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
  51. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  52. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  53. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  54. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/Weighted_Screen.py +0 -0
  55. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Feature/__init__.py +0 -0
  56. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  57. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  58. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  59. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  60. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  61. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  62. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  63. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Model/__init__.py +0 -0
  64. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/__init__.py +0 -0
  65. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/_common.py +0 -0
  66. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/credit_model.py +0 -0
  67. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/feature_validation.py +0 -0
  68. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/field_meta.py +0 -0
  69. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  70. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  71. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  72. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  73. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  74. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  75. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  76. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  77. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  78. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  79. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  80. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  81. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  82. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/Sample/__init__.py +0 -0
  83. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  84. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/UAT/__init__.py +0 -0
  85. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  86. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  87. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  88. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  89. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  90. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  91. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  92. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  93. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  94. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/__init__.py +0 -0
  95. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  96. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  97. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/__init__.py +0 -0
  98. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/nan_guard.py +0 -0
  99. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  108. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  109. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  110. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/requires.txt +0 -0
  111. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  112. {supermodelingfactory-0.5.9 → supermodelingfactory-0.5.10}/requirements.txt +0 -0
  113. {supermodelingfactory-0.5.9 → 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):
@@ -4,10 +4,10 @@ import numpy as np
4
4
  import warnings
5
5
 
6
6
  from .WOE_Plot_Tool import get_bivar_graph
7
- from .WOE_Tool import mapping_woe, woe_transform
7
+ from .WOE_Tool import _vectorized_group_slopes, mapping_woe, woe_transform
8
8
 
9
9
  from Modeling_Tool.Core.Binning_Tool import run_binning, get_bin_range_list
10
- from Modeling_Tool.Core.utils import calc_woe, calc_iv
10
+ from Modeling_Tool.Core.utils import _calc_woe_iv_values
11
11
  from Modeling_Tool._utils.sentinels import SMF_MISSING_BIN
12
12
 
13
13
  def get_overall_woe_table(woe_master, data, varlist=None):
@@ -29,8 +29,9 @@ def get_overall_woe_table(woe_master, data, varlist=None):
29
29
  right = first_bin[-1] == ']'
30
30
 
31
31
  fill_val = woe_master.missing_ref_value
32
- data_proc = data.copy()
32
+ data_proc = data[[var, woe_master.dep]].copy()
33
33
  data_proc[var] = data_proc[var].fillna(fill_val)
34
+ data_proc["_smf_good_n"] = data_proc[woe_master.dep].eq(0).astype(np.int64)
34
35
 
35
36
  # 使用训练集边界分箱
36
37
  binned, _ = run_binning(
@@ -54,14 +55,15 @@ def get_overall_woe_table(woe_master, data, varlist=None):
54
55
  N=(var, "count"),
55
56
  AVG_BAD=(dep, "mean"),
56
57
  N_BAD=(dep, "sum"),
57
- N_GOOD=(dep, lambda x: (x == 0).sum())
58
+ N_GOOD=("_smf_good_n", "sum")
58
59
  ).reset_index()
59
60
 
60
61
  # 计算 WOE / IV / LIFT
61
62
  stats["BAD_PCT_PER_BIN"] = stats["N_BAD"] / stats["N_BAD"].sum()
62
63
  stats["GOOD_PCT_PER_BIN"] = stats["N_GOOD"] / stats["N_GOOD"].sum()
63
- stats["WOE"] = calc_woe(stats, bad_pct="BAD_PCT_PER_BIN", good_pct="GOOD_PCT_PER_BIN")
64
- stats["IV"] = calc_iv(stats, bad_pct="BAD_PCT_PER_BIN", good_pct="GOOD_PCT_PER_BIN")
64
+ stats["WOE"], stats["IV"] = _calc_woe_iv_values(
65
+ stats, "BAD_PCT_PER_BIN", "GOOD_PCT_PER_BIN"
66
+ )
65
67
  stats["LIFT"] = stats["AVG_BAD"] / stats["AVG_BAD"].mean()
66
68
  stats["VAR"] = var
67
69
 
@@ -95,8 +97,9 @@ def get_group_woe_table(woe_master, data, group, varlist=None):
95
97
  right = first_bin[-1] == ']'
96
98
  fill_val = woe_master.missing_ref_value
97
99
 
98
- data_proc = data.copy()
100
+ data_proc = data[[group, var, woe_master.dep]].copy()
99
101
  data_proc[var] = data_proc[var].fillna(fill_val)
102
+ data_proc["_smf_good_n"] = data_proc[woe_master.dep].eq(0).astype(np.int64)
100
103
 
101
104
  binned, _ = run_binning(
102
105
  data=data_proc,
@@ -120,7 +123,7 @@ def get_group_woe_table(woe_master, data, group, varlist=None):
120
123
  N=(var, "count"),
121
124
  AVG_BAD=(dep, "mean"),
122
125
  N_BAD=(dep, "sum"),
123
- N_GOOD=(dep, lambda x: (x == 0).sum())
126
+ N_GOOD=("_smf_good_n", "sum")
124
127
  ).reset_index()
125
128
 
126
129
  # 计算各分箱内 WOE(全局分母)
@@ -128,8 +131,9 @@ def get_group_woe_table(woe_master, data, group, varlist=None):
128
131
  total_good = stats["N_GOOD"].sum()
129
132
  stats["BAD_PCT_PER_BIN"] = stats["N_BAD"] / total_bad
130
133
  stats["GOOD_PCT_PER_BIN"] = stats["N_GOOD"] / total_good
131
- stats["WOE"] = calc_woe(stats, bad_pct="BAD_PCT_PER_BIN", good_pct="GOOD_PCT_PER_BIN")
132
- stats["IV"] = calc_iv(stats, bad_pct="BAD_PCT_PER_BIN", good_pct="GOOD_PCT_PER_BIN")
134
+ stats["WOE"], stats["IV"] = _calc_woe_iv_values(
135
+ stats, "BAD_PCT_PER_BIN", "GOOD_PCT_PER_BIN"
136
+ )
133
137
  stats["LIFT"] = stats["AVG_BAD"] / stats["AVG_BAD"].mean()
134
138
  stats["VAR"] = var
135
139
 
@@ -147,10 +151,12 @@ def get_group_woe_table(woe_master, data, group, varlist=None):
147
151
  ).reset_index()
148
152
 
149
153
  # 计算单调性斜率(可选)
150
- from Modeling_Tool.Core.Slope_Tool import calculate_slope_manual
151
- slopes = stats.groupby(group).apply(lambda x: calculate_slope_manual(x, "AVG_BAD"))
154
+ slopes = _vectorized_group_slopes(stats, group, "AVG_BAD")
152
155
  sum_grp["SLOPE"] = sum_grp[group].map(slopes)
153
- sum_grp["direction"] = sum_grp["SLOPE"].apply(lambda x: 1 if x > 0 else -1 if x < 0 else 0)
156
+ slope_values = sum_grp["SLOPE"].to_numpy()
157
+ sum_grp["direction"] = np.select(
158
+ [slope_values > 0, slope_values < 0], [1, -1], default=0
159
+ )
154
160
  sum_grp["VAR"] = var
155
161
 
156
162
  summaries.append(sum_grp)
@@ -364,7 +370,14 @@ class WOE_Master(object):
364
370
  varlist = self.varlist
365
371
 
366
372
  woe_mapping_table = self.get_mapping_table()
367
- data_woe = mapping_woe(data, varlist, woe_mapping_table, suffix=self.woe_suffix, drop_bin_info=True)
373
+ data_woe = mapping_woe(
374
+ data,
375
+ varlist,
376
+ woe_mapping_table,
377
+ suffix=self.woe_suffix,
378
+ drop_bin_info=True,
379
+ missing_ref_value=self.missing_ref_value,
380
+ )
368
381
  return data_woe
369
382
 
370
383
  def update_woe(self, varlist, nbins=10, equal_freq=True, tree_binning_seed=None, chi2_config=None,
@@ -7,9 +7,53 @@ import numpy as np
7
7
  import pandas as pd
8
8
  import logging
9
9
 
10
- from Modeling_Tool.Core.Binning_Tool import run_binning, super_binning, get_bin_range_list
10
+ from Modeling_Tool.Core.Binning_Tool import (
11
+ _parse_bin_range_bounds,
12
+ get_bin_range_list,
13
+ run_binning,
14
+ super_binning,
15
+ )
11
16
  from Modeling_Tool.Core.Slope_Tool import calculate_slope_manual
12
- from Modeling_Tool.Core.utils import calc_iv, calc_woe
17
+ from Modeling_Tool.Core.utils import _calc_woe_iv_values, calc_iv, calc_woe
18
+
19
+
20
+ def _vectorized_lookup(values, mapping_keys, mapping_values):
21
+ """Map a whole Series through a unique lookup table with integer codes."""
22
+ key_index = pd.Index(np.asarray(list(mapping_keys), dtype=object))
23
+ if not key_index.is_unique:
24
+ raise ValueError("WOE mapping keys must be unique")
25
+
26
+ positions = key_index.get_indexer(values.astype(object).to_numpy())
27
+ output = np.full(len(values), np.nan, dtype=float)
28
+ valid = positions >= 0
29
+ if valid.any():
30
+ mapped = np.asarray(list(mapping_values), dtype=float)
31
+ output[valid] = np.take(mapped, positions[valid])
32
+ return pd.Series(output, index=values.index)
33
+
34
+
35
+ def _vectorized_group_slopes(data, group_col, value_col):
36
+ """Calculate per-group OLS slopes without ``groupby.apply`` callbacks."""
37
+ work = data[[group_col, value_col]].copy()
38
+ work["_smf_x"] = work.groupby(group_col, sort=False).cumcount().astype(float)
39
+ work["_smf_y"] = pd.to_numeric(work[value_col], errors="coerce")
40
+ work["_smf_xy"] = work["_smf_x"] * work["_smf_y"]
41
+ work["_smf_x2"] = work["_smf_x"] ** 2
42
+
43
+ grouped = work.groupby(group_col, sort=False).agg(
44
+ n=("_smf_y", "size"),
45
+ n_valid=("_smf_y", "count"),
46
+ sum_x=("_smf_x", "sum"),
47
+ sum_y=("_smf_y", "sum"),
48
+ sum_xy=("_smf_xy", "sum"),
49
+ sum_x2=("_smf_x2", "sum"),
50
+ )
51
+ numerator = grouped["n"] * grouped["sum_xy"] - grouped["sum_x"] * grouped["sum_y"]
52
+ denominator = grouped["n"] * grouped["sum_x2"] - grouped["sum_x"] ** 2
53
+ slopes = numerator / denominator
54
+ slopes[(grouped["n"] != grouped["n_valid"]) | denominator.eq(0)] = np.nan
55
+ slopes.name = "SLOPE"
56
+ return slopes
13
57
 
14
58
  def is_monotonic(data, column, direction='auto', strict=False, handle_nan='drop'):
15
59
  """检查Pandas Series或DataFrame列是否单调(递增或递减)。
@@ -216,24 +260,33 @@ class WOETransformer:
216
260
  tuple
217
261
  (woe_table, woe_mapping_dict) 元组
218
262
  """
219
- woe_table = binning_res.groupby([f"_bin_num_{var}", f"_bin_range_{var}"], dropna=False)\
220
- .agg(
263
+ working = binning_res.copy()
264
+ target = working[dep]
265
+ working["_smf_target_n"] = target.notna().astype(np.int64)
266
+ working["_smf_bad_n"] = target.eq(1).astype(np.int64)
267
+ working["_smf_good_n"] = target.eq(0).astype(np.int64)
268
+ woe_table = working.groupby(
269
+ [f"_bin_num_{var}", f"_bin_range_{var}"], dropna=False
270
+ ).agg(
221
271
  MIN=(var, "min"),
222
272
  MAX=(var, "max"),
223
273
  N=(f"_bin_num_{var}", "count"),
224
274
  AVG_SCORE=(var, "mean"),
225
- AVG_BAD=(dep, lambda x: ((x == 1).sum() / x.count())),
226
- AVG_GOOD=(dep, lambda x: ((x == 0).sum() / x.count())),
227
- N_BAD=(dep, "sum"),
228
- N_GOOD=(dep, lambda x: (x == 0).sum())
275
+ TARGET_N=("_smf_target_n", "sum"),
276
+ N_BAD=("_smf_bad_n", "sum"),
277
+ N_GOOD=("_smf_good_n", "sum"),
229
278
  )
279
+ woe_table["AVG_BAD"] = woe_table["N_BAD"] / woe_table["TARGET_N"]
280
+ woe_table["AVG_GOOD"] = woe_table["N_GOOD"] / woe_table["TARGET_N"]
281
+ woe_table = woe_table.drop(columns="TARGET_N")
230
282
 
231
283
  # IV/WOE Calculation
232
284
  woe_table["BAD_PCT_PER_BIN"] = woe_table["N_BAD"] / woe_table["N_BAD"].sum()
233
285
  woe_table["GOOD_PCT_PER_BIN"] = woe_table["N_GOOD"] / woe_table["N_GOOD"].sum()
234
286
  woe_table["LIFT"] = woe_table['AVG_BAD'] / woe_table['AVG_BAD'].mean()
235
- woe_table["WOE"] = calc_woe(data=woe_table, bad_pct="BAD_PCT_PER_BIN", good_pct="GOOD_PCT_PER_BIN")
236
- woe_table["IV"] = calc_iv(data=woe_table, bad_pct="BAD_PCT_PER_BIN", good_pct="GOOD_PCT_PER_BIN")
287
+ woe_table["WOE"], woe_table["IV"] = _calc_woe_iv_values(
288
+ woe_table, "BAD_PCT_PER_BIN", "GOOD_PCT_PER_BIN"
289
+ )
237
290
 
238
291
  # WOE Mapping Dictionary
239
292
  woe_table = woe_table.reset_index(drop=False)
@@ -300,7 +353,11 @@ class WOETransformer:
300
353
  logging.warning(f"WARNING: {var} WOE values are NOT monotonic in Train Dataset!")
301
354
 
302
355
  # WOE Mapping to DataFrame
303
- train_res[f"{var}_woe"] = train_res[f"_bin_range_{var}"].map(train_woe_mapping_dict)
356
+ train_res[f"{var}_woe"] = _vectorized_lookup(
357
+ train_res[f"_bin_range_{var}"],
358
+ train_woe_mapping_dict.keys(),
359
+ train_woe_mapping_dict.values(),
360
+ )
304
361
 
305
362
  # Drop Bin Info
306
363
  if self.drop_bin_info:
@@ -312,7 +369,10 @@ class WOETransformer:
312
369
  woe_mapping_table["BIN_NUM"] = woe_mapping_table[f"_bin_num_{var}"]
313
370
  woe_mapping_table["VAR"] = var
314
371
  oot_res = _mapping_woe_single_var(
315
- data=oot_df, var=var, woe_mapping_table=woe_mapping_table
372
+ data=oot_df,
373
+ var=var,
374
+ woe_mapping_table=woe_mapping_table,
375
+ missing_ref_value=self.fillna,
316
376
  )
317
377
 
318
378
  if self.ret_woe_table and oot_df is not None:
@@ -426,24 +486,12 @@ def convert_single_var_woe(data, var, woe_mapping_table, missing_ref=None, ret_b
426
486
  var_woe_mapping = woe_mapping_table.query(f"var_name == '{var}_woe'")
427
487
  var_woe_mapping = var_woe_mapping[["bin_no", "bin_value", "woe", "n"]]
428
488
 
429
- var_woe_mapping["bin_value_list"] = var_woe_mapping["bin_value"].apply(
430
- lambda x: x.replace("[", "").replace(")", "").split(",")
431
- )
432
-
433
- woe_mapping_dict = dict(zip(var_woe_mapping['bin_no'], var_woe_mapping['woe']))
434
- bin_range = [
435
- float(v.strip()) if v.strip() != 'inf' else np.inf
436
- for x in var_woe_mapping["bin_value_list"].tolist()
437
- for v in x
438
- ]
439
-
440
- unique_range = []
441
- for x in bin_range:
442
- if x not in unique_range:
443
- unique_range.append(x)
489
+ left, right = _parse_bin_range_bounds(var_woe_mapping, col="bin_value")
490
+ unique_range = np.unique(np.concatenate([left, right])).tolist()
444
491
 
445
- if missing_ref:
446
- var_serires = data[var].fillna(missing_ref)
492
+ var_serires = data[var]
493
+ if missing_ref is not None:
494
+ var_serires = var_serires.fillna(missing_ref)
447
495
 
448
496
  bin_no_transform = pd.cut(
449
497
  var_serires,
@@ -455,9 +503,11 @@ def convert_single_var_woe(data, var, woe_mapping_table, missing_ref=None, ret_b
455
503
  if ret_bin_no:
456
504
  return bin_no_transform
457
505
 
458
- woe_res = bin_no_transform.map(woe_mapping_dict)
459
-
460
- return woe_res
506
+ return _vectorized_lookup(
507
+ pd.Series(bin_no_transform.astype(object), index=data.index),
508
+ var_woe_mapping["bin_no"],
509
+ var_woe_mapping["woe"],
510
+ )
461
511
 
462
512
 
463
513
  class WOEMappingTransformer:
@@ -727,16 +777,14 @@ def get_woe_table(data, var, dep, grp_name=None, nbins=10, precision=5,
727
777
  direction = 1 if dep_slope > 0 else -1 if dep_slope < 0 else 0
728
778
 
729
779
  if grp_name:
730
- slope_grp = pd.DataFrame(
731
- gains_table.groupby([grp_name]).apply(calculate_slope_manual, "AVG_BAD"),
732
- columns=["SLOPE"]
733
- )
780
+ slope_grp = _vectorized_group_slopes(gains_table, grp_name, "AVG_BAD").to_frame()
734
781
  grp_summary = gains_table.groupby([grp_name]).agg(
735
782
  N=("N", sum), IV=("IV", sum), KS=("KS_PER_BIN", max),
736
783
  BTM_LIFT=("LIFT", min), TOP_LIFT=("LIFT", max)
737
784
  ).merge(slope_grp, left_index=True, right_index=True)
738
- grp_summary['direction'] = grp_summary['SLOPE'].apply(
739
- lambda x: 1 if x > 0 else -1 if x < 0 else 0
785
+ slope_values = grp_summary["SLOPE"].to_numpy()
786
+ grp_summary['direction'] = np.select(
787
+ [slope_values > 0, slope_values < 0], [1, -1], default=0
740
788
  )
741
789
  grp_woe_pvt = gains_table.pivot_table(
742
790
  index=["BIN_NUM", "BIN_RANGE"],
@@ -969,7 +1017,58 @@ def woe_transformation(train_df, varlist, dep, oot_df=None, nbins=10, chi2_confi
969
1017
  return transformer.transform(train_df, varlist, dep, oot_df)
970
1018
 
971
1019
 
972
- def _mapping_woe_single_var(data, var, woe_mapping_table, suffix="_woe", drop_bin_info=True):
1020
+ def _map_woe_arrays(data, var, woe_mapping_table, suffix="_woe", missing_ref_value=-999999):
1021
+ """Return vectorized WOE and bin columns for one feature."""
1022
+ var_woe_mapping = woe_mapping_table.loc[
1023
+ woe_mapping_table["VAR"].eq(var), ["BIN_NUM", "BIN_RANGE", "WOE", "N"]
1024
+ ].copy()
1025
+ if var_woe_mapping.empty:
1026
+ raise KeyError(f"No WOE mapping rows found for variable {var!r}")
1027
+
1028
+ first_range = var_woe_mapping["BIN_RANGE"].astype("string").dropna().iloc[0]
1029
+ include_lowest = first_range.startswith("[")
1030
+ right = first_range.endswith("]")
1031
+ bin_range = get_bin_range_list(var_woe_mapping, col="BIN_RANGE")
1032
+
1033
+ binned, _ = run_binning(
1034
+ data=data[[var]].copy(),
1035
+ column=var,
1036
+ nbins=sorted(set([*bin_range, -np.inf, np.inf])),
1037
+ include_missing=True,
1038
+ equal_freq=True,
1039
+ bin_colnames=("_BIN_NUM_", "_BIN_RANGE_"),
1040
+ ascending=True,
1041
+ include_lowest=include_lowest,
1042
+ right=right,
1043
+ fillna=missing_ref_value,
1044
+ )
1045
+ woe_values = _vectorized_lookup(
1046
+ binned["_BIN_RANGE_"],
1047
+ var_woe_mapping["BIN_RANGE"],
1048
+ var_woe_mapping["WOE"],
1049
+ )
1050
+ missing_count = int(woe_values.isna().sum())
1051
+ if missing_count:
1052
+ logging.warning(f"WARNING: Failed to Map WOE values for {missing_count} Records!")
1053
+
1054
+ return pd.DataFrame(
1055
+ {
1056
+ "_BIN_NUM_": binned["_BIN_NUM_"].to_numpy(copy=False),
1057
+ "_BIN_RANGE_": binned["_BIN_RANGE_"].to_numpy(copy=False),
1058
+ f"{var}{suffix}": woe_values.to_numpy(copy=False),
1059
+ },
1060
+ index=data.index,
1061
+ )
1062
+
1063
+
1064
+ def _mapping_woe_single_var(
1065
+ data,
1066
+ var,
1067
+ woe_mapping_table,
1068
+ suffix="_woe",
1069
+ drop_bin_info=True,
1070
+ missing_ref_value=-999999,
1071
+ ):
973
1072
  """基于WOE映射表为单个变量映射WOE值。
974
1073
 
975
1074
  使用预计算的WOE映射表对新数据进行转换,
@@ -987,6 +1086,9 @@ def _mapping_woe_single_var(data, var, woe_mapping_table, suffix="_woe", drop_bi
987
1086
  变量名后缀,默认为'_woe'
988
1087
  drop_bin_info : bool, optional
989
1088
  是否删除中间分箱信息列,默认为True
1089
+ missing_ref_value : scalar, optional
1090
+ 转换缺失值时使用的训练期分箱哨兵,默认 -999999。若训练使用了
1091
+ 自定义哨兵,转换时必须传入同一个值。
990
1092
 
991
1093
  返回:
992
1094
  --------
@@ -1000,62 +1102,29 @@ def _mapping_woe_single_var(data, var, woe_mapping_table, suffix="_woe", drop_bi
1000
1102
  # from Modeling_Tool.Modeling_Tool.Model_Eval_Tool import run_binning
1001
1103
  # from 代码优化.Model_Eval_Tool import get_bin_range_list
1002
1104
 
1003
- left = woe_mapping_table.loc[woe_mapping_table["VAR"] == var, "BIN_RANGE"].str[0].unique()[0]
1004
- right = woe_mapping_table.loc[woe_mapping_table["VAR"] == var, "BIN_RANGE"].str[-1].unique()[0]
1005
-
1006
- include_lowest = False if left == '(' else True
1007
- right = False if right == ')' else True
1008
-
1009
- var_woe_mapping = woe_mapping_table.query(f"VAR == '{var}'")
1010
- var_woe_mapping = var_woe_mapping[["BIN_NUM", "BIN_RANGE", "WOE", "N"]]
1011
- bin_range = get_bin_range_list(var_woe_mapping, col="BIN_RANGE")
1012
- woe_mapping_dict = dict(zip(var_woe_mapping['BIN_RANGE'], var_woe_mapping['WOE']))
1013
-
1014
- res, edges = run_binning(
1015
- data=data,
1016
- column=var,
1017
- nbins=sorted(set(list(bin_range) + [-np.inf, np.inf])),
1018
- include_missing=True,
1019
- equal_freq=True,
1020
- bin_colnames=(f"_BIN_NUM_", f"_BIN_RANGE_"),
1021
- ascending=True,
1022
- include_lowest=include_lowest,
1023
- right=right
1024
- )
1025
-
1026
- res[f"{var}{suffix}"] = res["_BIN_RANGE_"].map(woe_mapping_dict)
1027
-
1028
- res_woe_table = res.groupby(["_BIN_RANGE_"]).agg(
1029
- {f"_BIN_RANGE_": "count", var: [min, max], f"{var}{suffix}": [min, max, "mean"]}
1030
- )
1031
-
1032
- check_res = res_woe_table.dropna()
1033
- assert (check_res[(f"{var}{suffix}", "min")] == check_res[(f"{var}{suffix}", "max")]).all(), \
1034
- f"Error occurred when mapping var: {var}{suffix}"
1035
- assert data.shape[0] == res.shape[0]
1036
-
1037
- missing_woe = pd.isnull(res[var + suffix]).sum() > 0
1038
- if missing_woe:
1039
- logging.warning(f"WARNING: Failed to Map WOE values for {pd.isnull(res[var + suffix]).sum()} Records!")
1040
-
1041
- woe = pd.DataFrame(
1042
- res_woe_table.loc[
1043
- res_woe_table[(var, "min")] != res_woe_table[(var, "max")],
1044
- (f"{var}_woe", "mean")
1045
- ]
1105
+ mapped = _map_woe_arrays(
1106
+ data,
1107
+ var,
1108
+ woe_mapping_table,
1109
+ suffix=suffix,
1110
+ missing_ref_value=missing_ref_value,
1046
1111
  )
1047
- monotonicity = is_monotonic(woe, (f"{var}_woe", "mean"))
1048
-
1049
- if monotonicity[0]:
1050
- logging.warning(f"WARNING: {var} WOE values are NOT monotonic in the given Dataset!")
1051
-
1052
- if drop_bin_info:
1053
- res = res.drop(columns=["_BIN_NUM_", "_BIN_RANGE_"])
1054
-
1055
- return res
1056
-
1057
-
1058
- def mapping_woe(data, varlist, woe_mapping_table, suffix="_woe", drop_bin_info = True):
1112
+ mapped_cols = [f"{var}{suffix}"]
1113
+ if not drop_bin_info:
1114
+ mapped_cols = ["_BIN_NUM_", "_BIN_RANGE_", *mapped_cols]
1115
+
1116
+ base = data.drop(columns=mapped_cols, errors="ignore")
1117
+ return pd.concat([base, mapped[mapped_cols]], axis=1)
1118
+
1119
+
1120
+ def mapping_woe(
1121
+ data,
1122
+ varlist,
1123
+ woe_mapping_table,
1124
+ suffix="_woe",
1125
+ drop_bin_info=True,
1126
+ missing_ref_value=-999999,
1127
+ ):
1059
1128
  """基于WOE映射表批量映射WOE值。
1060
1129
 
1061
1130
  使用预计算的WOE映射表对多个变量进行WOE转换。
@@ -1072,6 +1141,9 @@ def mapping_woe(data, varlist, woe_mapping_table, suffix="_woe", drop_bin_info =
1072
1141
  变量名后缀,默认为'_woe'
1073
1142
  drop_bin_info : bool, optional
1074
1143
  是否删除中间分箱信息列,默认为True
1144
+ missing_ref_value : scalar, optional
1145
+ 转换缺失值时使用的训练期分箱哨兵,默认 -999999。若训练使用了
1146
+ 自定义哨兵,转换时必须传入同一个值。
1075
1147
 
1076
1148
  返回:
1077
1149
  --------
@@ -1082,13 +1154,27 @@ def mapping_woe(data, varlist, woe_mapping_table, suffix="_woe", drop_bin_info =
1082
1154
  --------
1083
1155
  >>> result = mapping_woe(df, ['var1', 'var2'], woe_table)
1084
1156
  """
1085
- transformer = WOEMappingTransformer(
1086
- woe_mapping_table=woe_mapping_table,
1087
- suffix=suffix
1088
- # drop_bin_info=drop_bin_info
1089
- )
1090
- res = data.copy()
1157
+ woe_columns = {}
1158
+ last_bin_columns = None
1091
1159
  for var in varlist:
1092
- res = _mapping_woe_single_var(data=res, var=var, woe_mapping_table=woe_mapping_table,
1093
- suffix=suffix, drop_bin_info=drop_bin_info)
1094
- return res
1160
+ mapped = _map_woe_arrays(
1161
+ data,
1162
+ var,
1163
+ woe_mapping_table,
1164
+ suffix=suffix,
1165
+ missing_ref_value=missing_ref_value,
1166
+ )
1167
+ woe_columns[f"{var}{suffix}"] = mapped[f"{var}{suffix}"].to_numpy(copy=False)
1168
+ last_bin_columns = mapped[["_BIN_NUM_", "_BIN_RANGE_"]]
1169
+
1170
+ output_cols = list(woe_columns)
1171
+ if not drop_bin_info:
1172
+ output_cols.extend(["_BIN_NUM_", "_BIN_RANGE_"])
1173
+ base = data.drop(columns=output_cols, errors="ignore").copy()
1174
+
1175
+ frames = [base]
1176
+ if not drop_bin_info and last_bin_columns is not None:
1177
+ frames.append(last_bin_columns)
1178
+ if woe_columns:
1179
+ frames.append(pd.DataFrame(woe_columns, index=data.index))
1180
+ return pd.concat(frames, axis=1)
@@ -38,4 +38,4 @@ def get_woe_table(data, var, dep, grp_name = None, nbins = 10, precision = 5, mi
38
38
  def plot_monotonicity_check(data, column, title = None, include_missing = True): ...
39
39
  def woe_transform(train_df, var, dep, nbins, oot_df = None, chi2_config = None, tree_binning_seed = None, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, ascending = True, fillna = -999999, spec_values = None, drop_bin_info = True, ret_woe_table = True, check_monotonicity = False): ...
40
40
  def woe_transformation(train_df, varlist, dep, oot_df = None, nbins = 10, chi2_config = None, tree_binning_seed = None, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, fillna = -999999, spec_values = None, drop_bin_info = True, ret_woe_table = True): ...
41
- def mapping_woe(data, varlist, woe_mapping_table, suffix = '_woe', drop_bin_info = True): ...
41
+ def mapping_woe(data, varlist, woe_mapping_table, suffix = '_woe', drop_bin_info = True, missing_ref_value = -999999): ...
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  __author__ = "Jingkai Sun"
4
- __version__ = "0.5.9"
4
+ __version__ = "0.5.10"
5
5
 
6
6
  from ._utils import SMF_MISSING_BIN
7
7
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.5.9
3
+ Version: 0.5.10
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.5.9
268
+ - **Version**: 0.5.10
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -208,7 +208,7 @@ em.close_workbook()
208
208
 
209
209
  ## 版本
210
210
 
211
- - **Version**: 0.5.9
211
+ - **Version**: 0.5.10
212
212
  - **Author**: Jingkai Sun
213
213
 
214
214
  ## 许可证
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.5.9
3
+ Version: 0.5.10
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.5.9
268
+ - **Version**: 0.5.10
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "SuperModelingFactory"
15
- version = "0.5.9"
15
+ version = "0.5.10"
16
16
  description = "Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting."
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.10"
@@ -15,7 +15,7 @@ def _read(path: str) -> str:
15
15
 
16
16
  setup(
17
17
  name="SuperModelingFactory",
18
- version=os.environ.get("SMF_VERSION", "0.5.9"),
18
+ version=os.environ.get("SMF_VERSION", "0.5.10"),
19
19
  description="Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.",
20
20
  long_description=_read("README.md"),
21
21
  long_description_content_type="text/markdown",