SuperModelingFactory 0.5.9__tar.gz → 0.6.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 (113) hide show
  1. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Binning_Tool.py +57 -29
  2. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Binning_Tool.pyi +1 -1
  3. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Proc_Compare.py +189 -36
  4. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/kDataFrame.py +8 -16
  5. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/utils.py +32 -28
  6. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Model_Eval_Tool.py +26 -46
  7. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/evaluate_model.py +48 -19
  8. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/weighted_eval_utils.py +13 -2
  9. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Explainability/Coalition_Structure.py +5 -5
  10. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Explainability/Model_Explainer.py +106 -26
  11. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Distribution_Tool.py +105 -29
  12. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Distribution_Tool.pyi +3 -3
  13. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Feature_Insights.py +82 -36
  14. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Feature_Screen.py +18 -4
  15. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/PSI_Tool.py +244 -0
  16. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/PSI_Tool.pyi +1 -1
  17. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +181 -61
  18. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Weighted_Screen.py +94 -20
  19. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/feature_validation.py +322 -96
  20. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/field_meta.py +9 -0
  21. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/sample_analysis.py +124 -101
  22. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/score_consistency_uat.py +2 -0
  23. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Sample_Split.py +32 -3
  24. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Sample_Split.pyi +1 -0
  25. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +73 -50
  26. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Adapter.py +45 -6
  27. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Adapter.pyi +1 -0
  28. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Master.py +27 -14
  29. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +30 -9
  30. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +1 -1
  31. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Tool.py +271 -138
  32. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Tool.pyi +1 -1
  33. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/__init__.py +1 -1
  34. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/PKG-INFO +2 -2
  35. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/README.md +1 -1
  36. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  37. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/pyproject.toml +1 -1
  38. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/setup.py +1 -1
  39. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/ExcelMaster/ExcelFormatTool.py +0 -0
  40. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/ExcelMaster/ExcelMaster.py +0 -0
  41. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/ExcelMaster/Template.py +0 -0
  42. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/ExcelMaster/Utility.py +0 -0
  43. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/ExcelMaster/__init__.py +0 -0
  44. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/LICENSE +0 -0
  45. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/MANIFEST.in +0 -0
  46. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  47. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  48. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  49. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  50. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  51. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  52. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  53. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  54. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  55. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  56. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/__init__.py +0 -0
  57. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  58. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  59. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  60. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  61. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  62. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/__init__.py +0 -0
  63. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  64. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Explainability/__init__.py +0 -0
  65. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  66. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/__init__.py +0 -0
  67. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  68. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  69. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  70. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  71. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  72. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  73. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  74. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/__init__.py +0 -0
  75. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/__init__.py +0 -0
  76. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/_common.py +0 -0
  77. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/credit_model.py +0 -0
  78. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  79. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  80. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  81. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  82. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  83. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  84. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  85. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  86. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  87. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/__init__.py +0 -0
  88. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/UAT/__init__.py +0 -0
  89. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  90. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  91. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  92. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  93. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  94. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/__init__.py +0 -0
  95. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  96. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  97. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/__init__.py +0 -0
  98. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/nan_guard.py +0 -0
  99. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  108. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  109. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  110. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/requires.txt +0 -0
  111. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  112. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/requirements.txt +0 -0
  113. {supermodelingfactory-0.5.9 → supermodelingfactory-0.6.0}/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')): ...
@@ -34,6 +34,7 @@ class ProcCompareConfig:
34
34
  chunk_size: int = 200_000
35
35
  n_partitions: int = 16
36
36
  backend: CompareBackend = "sequential"
37
+ compare_block_size: int = 64
37
38
 
38
39
  numeric_tol: float = 1e-8
39
40
  numeric_rtol: float = 0.0
@@ -107,6 +108,8 @@ class ProcCompareEngine:
107
108
  raise ValueError("chunk_size must be a positive integer.")
108
109
  if cfg.n_partitions <= 0:
109
110
  raise ValueError("n_partitions must be a positive integer.")
111
+ if cfg.compare_block_size <= 0:
112
+ raise ValueError("compare_block_size must be a positive integer.")
110
113
  if cfg.numeric_tol < 0 or cfg.numeric_rtol < 0 or cfg.datetime_tol_seconds < 0:
111
114
  raise ValueError("tolerance values must be non-negative.")
112
115
  if cfg.top_n <= 0:
@@ -294,44 +297,54 @@ class ProcCompareEngine:
294
297
  mismatch_frames = []
295
298
  mismatch_cols_by_row: dict[int, list[str]] = {}
296
299
 
297
- for col in compare_cols:
298
- left_col = f"{col}_left"
299
- right_col = f"{col}_right"
300
- comp = self._compare_series(col, merged[left_col], merged[right_col], common_mask)
301
- mismatch_mask = comp["mismatch"]
302
- n_mismatch = int(mismatch_mask.sum())
303
- if n_mismatch:
304
- mismatch_indexes = merged.index[mismatch_mask]
305
- for idx in mismatch_indexes:
300
+ common_values = common_mask.to_numpy(dtype=bool)
301
+ n_compared = int(common_values.sum())
302
+ block_size = int(self.config.compare_block_size)
303
+ for start in range(0, len(compare_cols), block_size):
304
+ block_cols = compare_cols[start : start + block_size]
305
+ comp = self._compare_column_block(merged, block_cols, common_values)
306
+ mismatch_matrix = comp["mismatch"]
307
+ one_null_matrix = comp["one_null"]
308
+ both_null_matrix = comp["both_null"]
309
+ diff_matrix = comp["diff"]
310
+ abs_diff_matrix = comp["abs_diff"]
311
+ left_values = comp["left_values"]
312
+ right_values = comp["right_values"]
313
+
314
+ for col_idx, col in enumerate(block_cols):
315
+ mismatch_positions = np.flatnonzero(mismatch_matrix[:, col_idx])
316
+ n_mismatch = int(len(mismatch_positions))
317
+ for idx in mismatch_positions:
306
318
  mismatch_cols_by_row.setdefault(int(idx), []).append(col)
307
- if self.config.detail_mode != "none":
308
- detail = merged.loc[mismatch_mask, key_cols].copy()
319
+ if n_mismatch and self.config.detail_mode != "none":
320
+ detail = merged.iloc[mismatch_positions][key_cols].copy()
309
321
  detail["column"] = col
310
- detail["left_value"] = merged.loc[mismatch_mask, left_col].to_numpy()
311
- detail["right_value"] = merged.loc[mismatch_mask, right_col].to_numpy()
312
- detail["diff"] = comp["diff"].loc[mismatch_mask].to_numpy()
313
- detail["abs_diff"] = comp["abs_diff"].loc[mismatch_mask].to_numpy()
322
+ detail["left_value"] = left_values[mismatch_positions, col_idx]
323
+ detail["right_value"] = right_values[mismatch_positions, col_idx]
324
+ detail["diff"] = diff_matrix[mismatch_positions, col_idx]
325
+ detail["abs_diff"] = abs_diff_matrix[mismatch_positions, col_idx]
314
326
  mismatch_frames.append(detail)
315
327
 
316
- n_compared = int(common_mask.sum())
317
- n_one_null = int(comp["one_null"].sum())
318
- n_both_null = int(comp["both_null"].sum())
319
- n_equal = n_compared - n_mismatch
320
- column_rows.append(
321
- {
322
- "column": col,
323
- "n_compared": n_compared,
324
- "n_equal": n_equal,
325
- "n_mismatch": n_mismatch,
326
- "pct_mismatch": round(n_mismatch / n_compared * 100, 6) if n_compared else 0.0,
327
- "n_one_side_null": n_one_null,
328
- "n_both_null": n_both_null,
329
- "mean_diff": comp["diff"].mean(skipna=True),
330
- "max_abs_diff": comp["abs_diff"].max(skipna=True),
331
- "_diff_sum": comp["diff"].sum(skipna=True),
332
- "_diff_count": int(comp["diff"].notna().sum()),
333
- }
334
- )
328
+ diff_values = diff_matrix[:, col_idx]
329
+ abs_diff_values = abs_diff_matrix[:, col_idx]
330
+ diff_valid = np.isfinite(diff_values)
331
+ diff_sum = float(np.nansum(diff_values)) if diff_valid.any() else 0.0
332
+ diff_count = int(diff_valid.sum())
333
+ column_rows.append(
334
+ {
335
+ "column": col,
336
+ "n_compared": n_compared,
337
+ "n_equal": n_compared - n_mismatch,
338
+ "n_mismatch": n_mismatch,
339
+ "pct_mismatch": round(n_mismatch / n_compared * 100, 6) if n_compared else 0.0,
340
+ "n_one_side_null": int(one_null_matrix[:, col_idx].sum()),
341
+ "n_both_null": int(both_null_matrix[:, col_idx].sum()),
342
+ "mean_diff": diff_sum / diff_count if diff_count else np.nan,
343
+ "max_abs_diff": float(np.nanmax(abs_diff_values)) if np.isfinite(abs_diff_values).any() else np.nan,
344
+ "_diff_sum": diff_sum,
345
+ "_diff_count": diff_count,
346
+ }
347
+ )
335
348
 
336
349
  for idx, cols in mismatch_cols_by_row.items():
337
350
  row_summary.loc[idx, "n_cell_mismatch"] = len(cols)
@@ -375,6 +388,146 @@ class ProcCompareEngine:
375
388
  duplicate_key_summary=duplicate_key_summary,
376
389
  )
377
390
 
391
+ def _compare_column_block(
392
+ self,
393
+ merged: pd.DataFrame,
394
+ columns: list[str],
395
+ common_mask: np.ndarray,
396
+ ) -> dict[str, np.ndarray]:
397
+ """Compare a bounded field block with vectorized type-specific kernels."""
398
+ n_rows = len(merged)
399
+ n_cols = len(columns)
400
+ left_series = [self._normalize_series_missing(merged[f"{col}_left"]) for col in columns]
401
+ right_series = [self._normalize_series_missing(merged[f"{col}_right"]) for col in columns]
402
+ left_values = np.column_stack([series.to_numpy(dtype=object) for series in left_series])
403
+ right_values = np.column_stack([series.to_numpy(dtype=object) for series in right_series])
404
+ mismatch = np.zeros((n_rows, n_cols), dtype=bool)
405
+ one_null = np.zeros((n_rows, n_cols), dtype=bool)
406
+ both_null = np.zeros((n_rows, n_cols), dtype=bool)
407
+ diff = np.full((n_rows, n_cols), np.nan, dtype=float)
408
+ abs_diff = np.full((n_rows, n_cols), np.nan, dtype=float)
409
+
410
+ datetime_idx = [
411
+ idx
412
+ for idx, col in enumerate(columns)
413
+ if self._should_compare_datetime(col, left_series[idx], right_series[idx])
414
+ ]
415
+ datetime_set = set(datetime_idx)
416
+ numeric_idx = [
417
+ idx
418
+ for idx in range(n_cols)
419
+ if idx not in datetime_set
420
+ and self._should_compare_numeric(left_series[idx], right_series[idx])
421
+ ]
422
+ numeric_set = set(numeric_idx)
423
+ string_idx = [
424
+ idx
425
+ for idx in range(n_cols)
426
+ if idx not in datetime_set and idx not in numeric_set
427
+ ]
428
+ common = common_mask[:, None]
429
+
430
+ if datetime_idx:
431
+ left_dt = np.column_stack(
432
+ [self._datetime_seconds(left_series[idx]) for idx in datetime_idx]
433
+ )
434
+ right_dt = np.column_stack(
435
+ [self._datetime_seconds(right_series[idx]) for idx in datetime_idx]
436
+ )
437
+ block_diff = left_dt - right_dt
438
+ block_abs = np.abs(block_diff)
439
+ left_null = ~np.isfinite(left_dt)
440
+ right_null = ~np.isfinite(right_dt)
441
+ tolerances = np.asarray(
442
+ [self._column_datetime_tol(columns[idx]) for idx in datetime_idx],
443
+ dtype=float,
444
+ )
445
+ block_one = (left_null ^ right_null) & common
446
+ block_both = left_null & right_null & common
447
+ block_mismatch = (block_abs > tolerances[None, :]) | block_one
448
+ if not self.config.both_null_equal:
449
+ block_mismatch |= block_both
450
+ block_mismatch &= common
451
+ diff[:, datetime_idx] = block_diff
452
+ abs_diff[:, datetime_idx] = block_abs
453
+ one_null[:, datetime_idx] = block_one
454
+ both_null[:, datetime_idx] = block_both
455
+ mismatch[:, datetime_idx] = block_mismatch
456
+
457
+ if numeric_idx:
458
+ left_num = np.column_stack(
459
+ [pd.to_numeric(left_series[idx], errors="coerce").to_numpy(dtype=float, na_value=np.nan) for idx in numeric_idx]
460
+ )
461
+ right_num = np.column_stack(
462
+ [pd.to_numeric(right_series[idx], errors="coerce").to_numpy(dtype=float, na_value=np.nan) for idx in numeric_idx]
463
+ )
464
+ block_diff = left_num - right_num
465
+ block_abs = np.abs(block_diff)
466
+ left_null = ~np.isfinite(left_num)
467
+ right_null = ~np.isfinite(right_num)
468
+ tolerances = np.asarray(
469
+ [self._column_numeric_tol(columns[idx]) for idx in numeric_idx],
470
+ dtype=float,
471
+ )
472
+ limit = tolerances[:, 0][None, :] + tolerances[:, 1][None, :] * np.abs(right_num)
473
+ block_one = (left_null ^ right_null) & common
474
+ block_both = left_null & right_null & common
475
+ block_mismatch = (block_abs > limit) | block_one
476
+ if not self.config.both_null_equal:
477
+ block_mismatch |= block_both
478
+ block_mismatch &= common
479
+ diff[:, numeric_idx] = block_diff
480
+ abs_diff[:, numeric_idx] = block_abs
481
+ one_null[:, numeric_idx] = block_one
482
+ both_null[:, numeric_idx] = block_both
483
+ mismatch[:, numeric_idx] = block_mismatch
484
+
485
+ if string_idx:
486
+ left_null = np.column_stack([left_series[idx].isna().to_numpy() for idx in string_idx])
487
+ right_null = np.column_stack([right_series[idx].isna().to_numpy() for idx in string_idx])
488
+ left_text = np.column_stack(
489
+ [left_series[idx].astype("string").fillna("").astype(str).to_numpy() for idx in string_idx]
490
+ )
491
+ right_text = np.column_stack(
492
+ [right_series[idx].astype("string").fillna("").astype(str).to_numpy() for idx in string_idx]
493
+ )
494
+ block_one = (left_null ^ right_null) & common
495
+ block_both = left_null & right_null & common
496
+ block_mismatch = (left_text != right_text) | block_one
497
+ if self.config.both_null_equal:
498
+ block_mismatch &= ~block_both
499
+ else:
500
+ block_mismatch |= block_both
501
+ block_mismatch &= common
502
+ one_null[:, string_idx] = block_one
503
+ both_null[:, string_idx] = block_both
504
+ mismatch[:, string_idx] = block_mismatch
505
+
506
+ return {
507
+ "mismatch": mismatch,
508
+ "one_null": one_null,
509
+ "both_null": both_null,
510
+ "diff": diff,
511
+ "abs_diff": abs_diff,
512
+ "left_values": left_values,
513
+ "right_values": right_values,
514
+ }
515
+
516
+ @staticmethod
517
+ def _datetime_seconds(series: pd.Series) -> np.ndarray:
518
+ values = ProcCompareEngine._to_datetime_series(series)
519
+ missing = values.isna().to_numpy()
520
+ result = values.astype("int64").to_numpy(dtype=float) / 1_000_000_000.0
521
+ result[missing] = np.nan
522
+ return result
523
+
524
+ @staticmethod
525
+ def _to_datetime_series(series: pd.Series) -> pd.Series:
526
+ try:
527
+ return pd.to_datetime(series, errors="coerce", format="mixed")
528
+ except (TypeError, ValueError):
529
+ return pd.to_datetime(series, errors="coerce")
530
+
378
531
  def _compare_series(
379
532
  self,
380
533
  col: str,
@@ -391,8 +544,8 @@ class ProcCompareEngine:
391
544
  abs_diff = pd.Series(np.nan, index=left.index, dtype="float64")
392
545
 
393
546
  if self._should_compare_datetime(col, left, right):
394
- left_dt = pd.to_datetime(left, errors="coerce")
395
- right_dt = pd.to_datetime(right, errors="coerce")
547
+ left_dt = self._to_datetime_series(left)
548
+ right_dt = self._to_datetime_series(right)
396
549
  diff = (left_dt - right_dt).dt.total_seconds()
397
550
  abs_diff = diff.abs()
398
551
  tol = self._column_datetime_tol(col)
@@ -57,13 +57,7 @@ class kSeries(Series):
57
57
  """
58
58
  Scale the model scores (for internt segment of MCI model)
59
59
  """
60
- data = self.copy()
61
- def app_func(x):
62
- scores = x * 1.112
63
- if (scores > 0.9999999):
64
- return 0.9999999
65
- return scores
66
- return data.apply(app_func)
60
+ return (self * 1.112).clip(upper=0.9999999)
67
61
 
68
62
  def proc_freq(self) -> pd.DataFrame:
69
63
  """
@@ -128,8 +122,12 @@ class kDataFrame(DataFrame):
128
122
  data = self
129
123
  else:
130
124
  data = self.copy()
131
- import re
132
- data[vintage_colname] = data[by].apply(lambda x: re.search("\d{4}-\d{2}", x).group().replace('-', ''))
125
+ data[vintage_colname] = (
126
+ data[by]
127
+ .astype("string")
128
+ .str.extract(r"(\d{4}-\d{2})", expand=False)
129
+ .str.replace("-", "", regex=False)
130
+ )
133
131
  if inplace:
134
132
  self = data
135
133
  return data
@@ -165,13 +163,7 @@ class kDataFrame(DataFrame):
165
163
  """
166
164
  Scale the model scores (for internt segment of MCI model)
167
165
  """
168
- data = self.copy()
169
- def app_func(x):
170
- scores = x * 1.112
171
- if (scores > 0.9999999):
172
- return 0.9999999
173
- return scores
174
- return data[pb_score].apply(app_func)
166
+ return (self[pb_score] * 1.112).clip(upper=0.9999999)
175
167
 
176
168
  def proc_freq(self, var: str):
177
169
  """
@@ -342,8 +342,12 @@ def convert_to_vintage(data, vintage_colname = 'VINTAGE', by = 'TRAN_TMS', retur
342
342
  >>> df = pd.DataFrame({'TRAN_TMS': ['2025-03-15 10:00:00', '2025-03-20 11:00:00']})
343
343
  >>> convert_to_vintage(df)
344
344
  """
345
- import re
346
- data[vintage_colname] = data[by].apply(lambda x: re.search("\\d{4}-\\d{2}", x).group().replace('-', ''))
345
+ data[vintage_colname] = (
346
+ data[by]
347
+ .astype("string")
348
+ .str.extract(r"(\d{4}-\d{2})", expand=False)
349
+ .str.replace("-", "", regex=False)
350
+ )
347
351
 
348
352
  if return_kDF:
349
353
  return kDataFrame(data)
@@ -1052,10 +1056,9 @@ def bool_to_str(data):
1052
1056
  >>> bool_to_str(df)
1053
1057
  """
1054
1058
  dfc = data.copy()
1055
- type_dict = data.dtypes.to_dict()
1056
- for k,v in type_dict.items():
1057
- if str(v).lower() == 'bool':
1058
- dfc = dfc.astype({k: str})
1059
+ bool_cols = [col for col in data.columns if pd.api.types.is_bool_dtype(data[col])]
1060
+ if bool_cols:
1061
+ dfc[bool_cols] = dfc[bool_cols].astype(str)
1059
1062
  return dfc
1060
1063
 
1061
1064
 
@@ -1688,6 +1691,19 @@ def parse_sql_file(sql_path:str=None,
1688
1691
 
1689
1692
 
1690
1693
 
1694
+ def _calc_woe_iv_values(data, bad_pct, good_pct, fillwoe=True, filliv=True):
1695
+ """Calculate WOE and IV together with one vectorized logarithm."""
1696
+ if len(data[bad_pct]) > 0 and len(data[good_pct]) > 0:
1697
+ bad_values = data[bad_pct]
1698
+ good_values = data[good_pct]
1699
+ woe = np.log(bad_values / good_values)
1700
+ iv = (bad_values - good_values) * woe
1701
+ else:
1702
+ woe = 0 if fillwoe else np.nan
1703
+ iv = 0 if filliv else np.nan
1704
+ return woe, iv
1705
+
1706
+
1691
1707
  def calc_woe(data, bad_pct, good_pct, fillwoe=True):
1692
1708
  """
1693
1709
  计算WOE(Weight of Evidence)值。
@@ -1716,15 +1732,7 @@ def calc_woe(data, bad_pct, good_pct, fillwoe=True):
1716
1732
  >>> calc_woe(df, 'bad_pct', 'good_pct')
1717
1733
  """
1718
1734
 
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
1735
+ return _calc_woe_iv_values(data, bad_pct, good_pct, fillwoe=fillwoe)[0]
1728
1736
 
1729
1737
 
1730
1738
  def calc_iv(data, bad_pct, good_pct, filliv=True):
@@ -1755,15 +1763,7 @@ def calc_iv(data, bad_pct, good_pct, filliv=True):
1755
1763
  >>> calc_iv(df, 'bad_pct', 'good_pct')
1756
1764
  """
1757
1765
 
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
1766
+ return _calc_woe_iv_values(data, bad_pct, good_pct, filliv=filliv)[1]
1767
1767
 
1768
1768
 
1769
1769
  def save_model(model, filename):
@@ -2413,9 +2413,13 @@ class WOEIVCalculator:
2413
2413
  -------
2414
2414
  tuple
2415
2415
  """
2416
- woe = self.calc_woe(fillna)
2417
- iv = self.calc_iv(fillna)
2418
- return woe, iv
2416
+ return _calc_woe_iv_values(
2417
+ self.data,
2418
+ self.bad_pct_col,
2419
+ self.good_pct_col,
2420
+ fillwoe=fillna,
2421
+ filliv=fillna,
2422
+ )
2419
2423
 
2420
2424
 
2421
2425
  def get_feature_names(model, model_type=None):
@@ -2669,4 +2673,4 @@ def get_feature_names_batch(models, model_type=None):
2669
2673
  elif isinstance(models, list):
2670
2674
  return [get_feature_names(model, model_type=model_type) for model in models]
2671
2675
  else:
2672
- raise TypeError("models参数应为dict或list类型")
2676
+ raise TypeError("models参数应为dict或list类型")
@@ -88,51 +88,31 @@ def _get_gains_table_scr(data, score, dep, nbins = 10, precision = 5,
88
88
  bin_colnames = ("_bin_num", "_bin_range"),
89
89
  ascending = ascending)
90
90
 
91
- def _compute_gains_tmp(group):
92
- # 计算各项指标
93
- min_val = group[score].min()
94
- max_val = group[score].max()
95
- # NOTE: pandas 2.3+ 起 groupby(...).apply(func) 传入 func 的 group
96
- # DataFrame 不再包含分组列,不能再用 group['_bin_num']。
97
- # len(group) 与原 .count() 数值等价(_bin_num 为分组主键,不为 NaN)。
98
- n = len(group)
99
- avg_score = group[score].mean()
100
- unique_score = group[score].nunique()
101
-
102
- # dep相关
103
- dep_vals = group[dep]
104
-
105
- grand_total = res.shape[0]
106
- grand_perf_cnt = res[dep].count()
107
- grand_total_bad = res[dep].sum()
108
- grand_total_good = (grand_perf_cnt - grand_total_bad)
109
-
110
- perf_cnt = dep_vals.count()
111
- n_bad = (dep_vals == 1).sum()
112
- n_good = (dep_vals == 0).sum()
113
- avg_bad = n_bad / dep_vals.count() if dep_vals.count() > 0 else 0 # 避免除以0
114
- avg_good = n_good / dep_vals.count() if dep_vals.count() > 0 else 0
115
-
116
- lift = avg_bad / res[dep].mean()
117
- prop = n / grand_total
118
-
119
- # 返回Series
120
- return pd.Series({
121
- 'MIN': min_val,
122
- 'MAX': max_val,
123
- 'N': n,
124
- 'PROP': prop,
125
- 'PERF_CNT': perf_cnt,
126
- 'AVG_SCORE': avg_score,
127
- 'UNIQUE_SCORE': unique_score,
128
- 'AVG_BAD': avg_bad,
129
- 'AVG_GOOD': avg_good,
130
- 'N_BAD': n_bad,
131
- 'N_GOOD': n_good,
132
- 'LIFT': lift
133
- })
134
-
135
- gains_table = res.groupby(["_bin_num", "_bin_range"], dropna=False).apply(_compute_gains_tmp)
91
+ res = res.copy()
92
+ res["_smf_bad_ind"] = res[dep].eq(1).astype(np.int64)
93
+ res["_smf_good_ind"] = res[dep].eq(0).astype(np.int64)
94
+ grouped = res.groupby(["_bin_num", "_bin_range"], dropna=False)
95
+ gains_table = grouped.agg(
96
+ MIN=(score, "min"),
97
+ MAX=(score, "max"),
98
+ N=(dep, "size"),
99
+ PERF_CNT=(dep, "count"),
100
+ AVG_SCORE=(score, "mean"),
101
+ UNIQUE_SCORE=(score, "nunique"),
102
+ N_BAD=("_smf_bad_ind", "sum"),
103
+ N_GOOD=("_smf_good_ind", "sum"),
104
+ )
105
+ perf_denom = gains_table["PERF_CNT"].replace(0, np.nan)
106
+ gains_table["PROP"] = gains_table["N"] / max(len(res), 1)
107
+ gains_table["AVG_BAD"] = (gains_table["N_BAD"] / perf_denom).fillna(0.0)
108
+ gains_table["AVG_GOOD"] = (gains_table["N_GOOD"] / perf_denom).fillna(0.0)
109
+ gains_table["LIFT"] = gains_table["AVG_BAD"] / res[dep].mean()
110
+ gains_table = gains_table[
111
+ [
112
+ "MIN", "MAX", "N", "PROP", "PERF_CNT", "AVG_SCORE",
113
+ "UNIQUE_SCORE", "AVG_BAD", "AVG_GOOD", "N_BAD", "N_GOOD", "LIFT",
114
+ ]
115
+ ]
136
116
 
137
117
  gains_table["BAD_PCT_IN_EACH_BIN"] = gains_table["N_BAD"] / gains_table["N_BAD"].sum()
138
118
  gains_table["GOOD_PCT_IN_EACH_BIN"] = gains_table["N_GOOD"] / gains_table["N_GOOD"].sum()
@@ -146,7 +126,7 @@ def _get_gains_table_scr(data, score, dep, nbins = 10, precision = 5,
146
126
 
147
127
 
148
128
  gains_table["TRUE_BAD_SHIFT"] = (gains_table['AVG_BAD'].shift(1) / gains_table['AVG_BAD'] - 1) if not ascending else (gains_table['AVG_BAD'] / gains_table['AVG_BAD'].shift(1) - 1)
149
- gains_table["RANK_ORDER_BUMP"] = gains_table["TRUE_BAD_SHIFT"].apply(lambda x: 1 if x < 0 else 0)
129
+ gains_table["RANK_ORDER_BUMP"] = gains_table["TRUE_BAD_SHIFT"].lt(0).astype(int)
150
130
 
151
131
  gains_table["WOE"] = calc_woe(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")
152
132
  gains_table["IV"] = calc_iv(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")