SuperModelingFactory 0.2.0__py3-none-any.whl

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 (79) hide show
  1. ExcelMaster/ExcelFormatTool.py +487 -0
  2. ExcelMaster/ExcelMaster.py +917 -0
  3. ExcelMaster/Template.py +525 -0
  4. ExcelMaster/Utility.py +233 -0
  5. ExcelMaster/__init__.py +3 -0
  6. Modeling_Tool/Core/Binning_Tool.py +1608 -0
  7. Modeling_Tool/Core/Binning_Tool.pyi +48 -0
  8. Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
  9. Modeling_Tool/Core/Json_Data_Converter.py +621 -0
  10. Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
  11. Modeling_Tool/Core/ODPS_Tool.py +284 -0
  12. Modeling_Tool/Core/Slope_Tool.py +356 -0
  13. Modeling_Tool/Core/Slope_Tool.pyi +31 -0
  14. Modeling_Tool/Core/XOR_Encryptor.py +207 -0
  15. Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
  16. Modeling_Tool/Core/__init__.py +99 -0
  17. Modeling_Tool/Core/kDataFrame.py +228 -0
  18. Modeling_Tool/Core/kDataFrame.pyi +43 -0
  19. Modeling_Tool/Core/sample_weight_utils.py +77 -0
  20. Modeling_Tool/Core/utils.py +2672 -0
  21. Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
  22. Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
  23. Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
  24. Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
  25. Modeling_Tool/Eval/__init__.py +54 -0
  26. Modeling_Tool/Eval/evaluate_model.py +2008 -0
  27. Modeling_Tool/Eval/evaluate_model.pyi +50 -0
  28. Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
  29. Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
  30. Modeling_Tool/Explainability/Model_Explainer.py +743 -0
  31. Modeling_Tool/Explainability/__init__.py +24 -0
  32. Modeling_Tool/Feature/Distribution_Tool.py +509 -0
  33. Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
  34. Modeling_Tool/Feature/Feature_Insights.py +762 -0
  35. Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
  36. Modeling_Tool/Feature/PSI_Tool.py +1195 -0
  37. Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
  38. Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
  39. Modeling_Tool/Feature/__init__.py +40 -0
  40. Modeling_Tool/Model/Backward_Tool.py +778 -0
  41. Modeling_Tool/Model/Backward_Tool.pyi +45 -0
  42. Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
  43. Modeling_Tool/Model/GBM_Tool.py +1610 -0
  44. Modeling_Tool/Model/GBM_Tool.pyi +90 -0
  45. Modeling_Tool/Model/LRM_Tool.py +1198 -0
  46. Modeling_Tool/Model/LRM_Tool.pyi +47 -0
  47. Modeling_Tool/Model/__init__.py +61 -0
  48. Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
  49. Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
  50. Modeling_Tool/Sample/Reject_Infer.py +413 -0
  51. Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
  52. Modeling_Tool/Sample/Sample_Split.py +520 -0
  53. Modeling_Tool/Sample/Sample_Split.pyi +43 -0
  54. Modeling_Tool/Sample/__init__.py +31 -0
  55. Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
  56. Modeling_Tool/UAT/__init__.py +19 -0
  57. Modeling_Tool/WOE/WOE_Adapter.py +204 -0
  58. Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
  59. Modeling_Tool/WOE/WOE_Master.py +491 -0
  60. Modeling_Tool/WOE/WOE_Master.pyi +40 -0
  61. Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
  62. Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
  63. Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
  64. Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
  65. Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
  66. Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
  67. Modeling_Tool/WOE/WOE_Tool.py +1094 -0
  68. Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
  69. Modeling_Tool/WOE/__init__.py +86 -0
  70. Modeling_Tool/WOE/plot_woe_tool.py +290 -0
  71. Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
  72. Modeling_Tool/__init__.py +176 -0
  73. Report/Report_Tool.py +380 -0
  74. Report/__init__.py +3 -0
  75. supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
  76. supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
  77. supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
  78. supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
  79. supermodelingfactory-0.2.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,2548 @@
1
+ import logging
2
+ import os
3
+ import numpy as np
4
+ import pandas as pd
5
+ from Modeling_Tool.Core.Binning_Tool import get_bin_range_list, super_binning
6
+ from Modeling_Tool.Core.utils import load_model, calc_iv, calc_woe
7
+ from .evaluate_model import evaluate_performance
8
+ from . import weighted_eval_utils as _weighted_eval
9
+
10
+ ###################################################### Private Functions #############################################################
11
+
12
+ def _get_gains_table_scr(data, score, dep, nbins = 10, precision = 5,
13
+ min_bin_prop = 0.05, include_missing = True, equal_freq = True,
14
+ chi2_method = False, chi2_p = 0.95, init_equi_bins = 2000,
15
+ fillna = -999999, spec_values = [], retSummary = False,
16
+ tree_binning = False, random_state=42, ascending = False,
17
+ withSummary = False, add_func = None):
18
+ """
19
+ 计算指定分数字段的收益表(Gains Table)。
20
+
21
+ 对数据进行分箱处理后,计算每个分箱的统计指标,包括样本数、坏样本率、
22
+ 累计好/坏样本数、WOE、IV等。
23
+
24
+ Parameters
25
+ ----------
26
+ data : pandas.DataFrame
27
+ 输入数据表
28
+ score : str
29
+ 分数字段名
30
+ dep : str
31
+ 目标变量名(二分类标签,0和1)
32
+ nbins : int, default 10
33
+ 分箱数量
34
+ precision : int, default 5
35
+ 边界值精度
36
+ min_bin_prop : float, default 0.05
37
+ 每箱最小样本占比
38
+ include_missing : bool, default True
39
+ 是否包含缺失值
40
+ equal_freq : bool, default True
41
+ True为等频分箱,False为等距分箱
42
+ chi2_method : bool, default False
43
+ 是否使用卡方分箱
44
+ chi2_p : float, default 0.95
45
+ 卡方检验显著性水平
46
+ init_equi_bins : int, default 2000
47
+ 初始等频分箱数量
48
+ fillna : any, default -999999
49
+ 缺失值填充值
50
+ spec_values : list, default []
51
+ 特殊值列表
52
+ retSummary : bool, default False
53
+ 是否只返回汇总指标
54
+ tree_binning : bool, default False
55
+ 是否使用决策树分箱
56
+ random_state : int, default 42
57
+ 随机种子
58
+ ascending : bool, default False
59
+ 分箱顺序是否升序
60
+ withSummary : bool, default False
61
+ 是否包含总体汇总行
62
+ add_func : callable, optional
63
+ 自定义统计函数
64
+
65
+ Returns
66
+ -------
67
+ pandas.DataFrame
68
+ 收益表,包含各分箱统计指标
69
+ """
70
+
71
+ res, edges = super_binning(data = data,
72
+ score = score,
73
+ dep = dep,
74
+ nbins = nbins,
75
+ precision = precision,
76
+ min_bin_prop = min_bin_prop,
77
+ include_missing = include_missing,
78
+ equal_freq = equal_freq,
79
+ chi2_method = chi2_method,
80
+ chi2_p = chi2_p,
81
+ init_equi_bins = init_equi_bins,
82
+ fillna = fillna,
83
+ spec_values = spec_values,
84
+ tree_binning = tree_binning,
85
+ random_state = random_state,
86
+ return_edges = True,
87
+ bin_colnames = ("_bin_num", "_bin_range"),
88
+ ascending = ascending)
89
+
90
+ def _compute_gains_tmp(group):
91
+ # 计算各项指标
92
+ min_val = group[score].min()
93
+ max_val = group[score].max()
94
+ # NOTE: pandas 2.3+ 起 groupby(...).apply(func) 传入 func 的 group
95
+ # DataFrame 不再包含分组列,不能再用 group['_bin_num']。
96
+ # len(group) 与原 .count() 数值等价(_bin_num 为分组主键,不为 NaN)。
97
+ n = len(group)
98
+ avg_score = group[score].mean()
99
+ unique_score = group[score].nunique()
100
+
101
+ # dep相关
102
+ dep_vals = group[dep]
103
+
104
+ grand_total = res.shape[0]
105
+ grand_perf_cnt = res[dep].count()
106
+ grand_total_bad = res[dep].sum()
107
+ grand_total_good = (grand_perf_cnt - grand_total_bad)
108
+
109
+ perf_cnt = dep_vals.count()
110
+ n_bad = (dep_vals == 1).sum()
111
+ n_good = (dep_vals == 0).sum()
112
+ avg_bad = n_bad / dep_vals.count() if dep_vals.count() > 0 else 0 # 避免除以0
113
+ avg_good = n_good / dep_vals.count() if dep_vals.count() > 0 else 0
114
+
115
+ lift = avg_bad / res[dep].mean()
116
+ prop = n / grand_total
117
+
118
+ # 返回Series
119
+ return pd.Series({
120
+ 'MIN': min_val,
121
+ 'MAX': max_val,
122
+ 'N': n,
123
+ 'PROP': prop,
124
+ 'PERF_CNT': perf_cnt,
125
+ 'AVG_SCORE': avg_score,
126
+ 'UNIQUE_SCORE': unique_score,
127
+ 'AVG_BAD': avg_bad,
128
+ 'AVG_GOOD': avg_good,
129
+ 'N_BAD': n_bad,
130
+ 'N_GOOD': n_good,
131
+ 'LIFT': lift
132
+ })
133
+
134
+ gains_table = res.groupby(["_bin_num", "_bin_range"], dropna=False).apply(_compute_gains_tmp)
135
+
136
+ gains_table["BAD_PCT_IN_EACH_BIN"] = gains_table["N_BAD"] / gains_table["N_BAD"].sum()
137
+ gains_table["GOOD_PCT_IN_EACH_BIN"] = gains_table["N_GOOD"] / gains_table["N_GOOD"].sum()
138
+
139
+ gains_table["N_CUM_BAD"] = gains_table["N_BAD"].cumsum()
140
+ gains_table["N_CUM_GOOD"] = gains_table["N_GOOD"].cumsum()
141
+
142
+ gains_table["CUM_BAD_PCT"] = gains_table["N_CUM_BAD"]/gains_table["N_BAD"].sum()
143
+ gains_table["CUM_GOOD_PCT"] = gains_table["N_CUM_GOOD"]/gains_table["N_GOOD"].sum()
144
+ gains_table["KS_PER_BIN"] = np.abs((gains_table["CUM_BAD_PCT"] - gains_table["CUM_GOOD_PCT"]))
145
+
146
+
147
+ 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)
148
+ gains_table["RANK_ORDER_BUMP"] = gains_table["TRUE_BAD_SHIFT"].apply(lambda x: 1 if x < 0 else 0)
149
+
150
+ gains_table["WOE"] = calc_woe(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")
151
+ gains_table["IV"] = calc_iv(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")
152
+
153
+
154
+ if add_func is not None:
155
+ gains_table_add = res.groupby(["_bin_num", "_bin_range"], dropna=False).apply(add_func)
156
+ gains_table = gains_table.merge(gains_table_add, right_index = True, left_index = True, how = 'left')
157
+
158
+ if retSummary:
159
+ summ_metrics = ["N_BUMP", "MIN_RISK_DEP", "MAX_RISK_DEP", "KS_IN_GAINS", "LIFT_IN_GAINS", "IV", "N_BINS"]
160
+ res_summary = {
161
+ "N_BUMP": gains_table["RANK_ORDER_BUMP"].sum(),
162
+ "MIN_RISK_DEP": gains_table["TRUE_BAD_SHIFT"].round(4).min(),
163
+ "MAX_RISK_DEP": gains_table["TRUE_BAD_SHIFT"].round(4).max(),
164
+ "KS_IN_GAINS": gains_table["KS_PER_BIN"].round(4).max(),
165
+ "LIFT_IN_GAINS": gains_table["LIFT"].round(4).max(),
166
+ "IV": gains_table["IV"].replace([np.inf, -np.inf], 0).sum(),
167
+ "N_BINS": gains_table.shape[0]
168
+ }
169
+
170
+ res_summary = pd.DataFrame(res_summary, index = [0])
171
+ res_summary["LABEL_NAME"] = dep
172
+ res_summary["SCR_NAME"] = score
173
+ return res_summary[summ_metrics]
174
+
175
+ if withSummary:
176
+ grand_total = {"MIN": res[score].min(),
177
+ "MAX": res[score].max(),
178
+ "N": res.shape[0],
179
+ "AVG_SCORE": res[score].mean(),
180
+ "UNIQUE_SCORE": res[score].nunique(),
181
+ "AVG_BAD": res[dep].mean(),
182
+ "AVG_GOOD": 1 - res[dep].mean(),
183
+ "N_BAD": res[dep].sum(),
184
+ "N_GOOD": (res[dep] == 0).sum(),
185
+ "BAD_PCT_IN_EACH_BIN": gains_table["BAD_PCT_IN_EACH_BIN"].sum(),
186
+ "GOOD_PCT_IN_EACH_BIN": gains_table["GOOD_PCT_IN_EACH_BIN"].sum(),
187
+ "N_CUM_BAD": res[dep].sum(),
188
+ "N_CUM_GOOD": (res[dep] == 0).sum(),
189
+ "CUM_BAD_PCT": 1,
190
+ "CUM_GOOD_PCT": 1,
191
+ "KS_PER_BIN": gains_table["KS_PER_BIN"].max(),
192
+ "LIFT": 1,
193
+ "TRUE_BAD_SHIFT": 1,
194
+ "RANK_ORDER_BUMP": gains_table["RANK_ORDER_BUMP"].sum(),
195
+ "WOE": gains_table["WOE"].mean(),
196
+ "IV": gains_table["IV"].sum(),
197
+ "PROP": res.shape[0] / res.shape[0],
198
+ "PERF_CNT": gains_table["PERF_CNT"].sum()}
199
+ grand_total = pd.DataFrame(grand_total, index = [("Grand Summary", "")])
200
+ gains_table = pd.concat([gains_table, grand_total])
201
+
202
+ gains_table.index.names = ("_bin_num", "_bin_range")
203
+
204
+ return gains_table
205
+
206
+
207
+ def _get_gains_table_single(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True,
208
+ score = None, model = None, varlist = None, equal_freq = True, chi2_method = False,
209
+ chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], retSummary = False,
210
+ tree_binning = False, random_state=42, ascending = False,
211
+ withSummary = False, add_func = None):
212
+ """
213
+ 计算单个模型的收益表。
214
+
215
+ 根据传入的score字段或模型预测结果,计算收益表。
216
+ 优先使用传入的score字段,若无则使用模型预测概率。
217
+
218
+ Parameters
219
+ ----------
220
+ data : pandas.DataFrame
221
+ 输入数据表
222
+ dep : str
223
+ 目标变量名
224
+ nbins : int, default 10
225
+ 分箱数量
226
+ precision : int, default 5
227
+ 边界值精度
228
+ min_bin_prop : float, default 0.05
229
+ 每箱最小样本占比
230
+ include_missing : bool, default True
231
+ 是否包含缺失值
232
+ score : str, optional
233
+ 分数字段名
234
+ model : sklearn-like model, optional
235
+ 机器学习模型(当score为None时使用)
236
+ varlist : list, optional
237
+ 模型特征列表
238
+ equal_freq : bool, default True
239
+ True为等频分箱
240
+ chi2_method : bool, default False
241
+ 是否使用卡方分箱
242
+ chi2_p : float, default 0.95
243
+ 卡方检验显著性水平
244
+ init_equi_bins : int, default 100
245
+ 初始等频分箱数量
246
+ fillna : any, default -999999
247
+ 缺失值填充值
248
+ spec_values : list, default []
249
+ 特殊值列表
250
+ retSummary : bool, default False
251
+ 是否只返回汇总指标
252
+ tree_binning : bool, default False
253
+ 是否使用决策树分箱
254
+ random_state : int, default 42
255
+ 随机种子
256
+ ascending : bool, default False
257
+ 分箱顺序是否升序
258
+ withSummary : bool, default False
259
+ 是否包含总体汇总行
260
+ add_func : callable, optional
261
+ 自定义统计函数
262
+
263
+ Returns
264
+ -------
265
+ pandas.DataFrame or int
266
+ 收益表;若缺少必要参数返回-1/-2/-3
267
+ """
268
+
269
+ if score is None and model is None and varlist is None:
270
+ return -1
271
+
272
+ if score is None and model is None:
273
+ return -2
274
+
275
+ if score is None and varlist is None:
276
+ return -3
277
+
278
+ if score is None:
279
+ data['_mdl_scr'] = model.predict_proba(data.loc[:, varlist])[:, 1]
280
+ score = '_mdl_scr'
281
+
282
+ res = _get_gains_table_scr(data = data,
283
+ score = score,
284
+ dep = dep,
285
+ nbins = nbins,
286
+ precision = precision,
287
+ min_bin_prop=min_bin_prop,
288
+ include_missing = include_missing,
289
+ equal_freq = equal_freq,
290
+ chi2_method = chi2_method,
291
+ chi2_p = chi2_p,
292
+ init_equi_bins = init_equi_bins,
293
+ fillna = fillna,
294
+ spec_values = spec_values,
295
+ retSummary = retSummary,
296
+ tree_binning = tree_binning,
297
+ random_state = random_state,
298
+ ascending = ascending,
299
+ withSummary = withSummary,
300
+ add_func = add_func)
301
+ return res
302
+
303
+
304
+ def _get_perf_summary_single(train,
305
+ validation,
306
+ oot,
307
+ tgt_name,
308
+ scr_name = None,
309
+ model = None,
310
+ feature_cols = None,
311
+ fig_save_path = None,
312
+ rpt_save_path = None,
313
+ to_show = False,
314
+ display = True,
315
+ dist_bins = 20,
316
+ pct_bins = 10,
317
+ precision = 5,
318
+ min_bin_prop = 0.05,
319
+ include_missing = False,
320
+ equal_freq = True,
321
+ chi2_method = False,
322
+ init_equi_bins = 1000,
323
+ chi2_p = 0.9,
324
+ tree_binning = False,
325
+ random_state = 42,
326
+ gains_table = False):
327
+ """
328
+ 计算单个模型的性能评估汇总。
329
+
330
+ 对训练集、验证集和oot样本进行模型性能评估,包括AUC、KS、
331
+ Lift等指标,并可选择生成收益表。
332
+
333
+ Parameters
334
+ ----------
335
+ train : pandas.DataFrame, optional
336
+ 训练数据集
337
+ validation : pandas.DataFrame, optional
338
+ 验证数据集
339
+ oot : pandas.DataFrame, optional
340
+ oot(Out-of-Time)数据集
341
+ tgt_name : str
342
+ 目标变量名
343
+ scr_name : str, optional
344
+ 分数字段名
345
+ model : sklearn-like model, optional
346
+ 机器学习模型
347
+ feature_cols : list, optional
348
+ 模型特征列表
349
+ fig_save_path : str, optional
350
+ 图片保存路径
351
+ rpt_save_path : str, optional
352
+ 报告保存路径
353
+ to_show : bool, default False
354
+ 是否显示图形
355
+ display : bool, default True
356
+ 是否打印结果
357
+ dist_bins : int, default 20
358
+ 分布分箱数
359
+ pct_bins : int, default 10
360
+ 百分比分箱数
361
+ precision : int, default 5
362
+ 边界值精度
363
+ min_bin_prop : float, default 0.05
364
+ 每箱最小样本占比
365
+ include_missing : bool, default False
366
+ 是否包含缺失值
367
+ equal_freq : bool, default True
368
+ True为等频分箱
369
+ chi2_method : bool, default False
370
+ 是否使用卡方分箱
371
+ init_equi_bins : int, default 1000
372
+ 初始等频分箱数量
373
+ chi2_p : float, default 0.9
374
+ 卡方检验显著性水平
375
+ tree_binning : bool, default False
376
+ 是否使用决策树分箱
377
+ random_state : int, default 42
378
+ 随机种子
379
+ gains_table : bool, default True
380
+ 是否计算收益表
381
+
382
+ Returns
383
+ -------
384
+ pandas.DataFrame or int
385
+ 性能评估汇总表;若缺少必要参数返回-1/-2/-3
386
+ """
387
+
388
+ if scr_name is None and model is None and feature_cols is None:
389
+ return -1
390
+
391
+ if scr_name is None and model is None:
392
+ return -2
393
+
394
+ if scr_name is None and feature_cols is None:
395
+ return -3
396
+
397
+ if model is not None and feature_cols is not None:
398
+ ins_prob = model.predict_proba(train.loc[:, feature_cols])[:, 1] if train is not None else None
399
+ oos_prob = model.predict_proba(validation.loc[:, feature_cols])[:, 1] if validation is not None else None
400
+ oot_prob = model.predict_proba(oot.loc[:, feature_cols])[:, 1] if oot is not None else None
401
+
402
+ if scr_name is not None:
403
+ ins_prob = train[scr_name] if train is not None else None
404
+ oos_prob = validation[scr_name] if validation is not None else None
405
+ oot_prob = oot[scr_name] if oot is not None else None
406
+
407
+ datasets = {}
408
+ if ins_prob is not None:
409
+ datasets['ins'] = {"y_true": train[tgt_name], "y_score": ins_prob}
410
+ if oos_prob is not None:
411
+ datasets['oos'] = {"y_true": validation[tgt_name], "y_score": oos_prob}
412
+ if oot_prob is not None:
413
+ datasets['oot'] = {"y_true": oot[tgt_name], "y_score": oot_prob}
414
+
415
+ model_eval_result_df = evaluate_performance(
416
+ datasets=datasets,
417
+ dist_bins=dist_bins,
418
+ pct_bins=pct_bins,
419
+ square_figsize=5,
420
+ to_show=to_show,
421
+ save_path = fig_save_path,
422
+ gains_table = gains_table,
423
+ equal_freq = equal_freq
424
+ )
425
+
426
+ # print(model_eval_result_df)
427
+ # if model_eval_result_df.shape[0] == 0:
428
+ # return model_eval_result_df
429
+ # print(model_eval_result_df.columns)
430
+
431
+ # print(pct_bins)
432
+ quantile = np.ceil((10 / pct_bins) * 10) if (np.ceil((10 / pct_bins) * 10) - ((10 / pct_bins) * 10)) < 0.5 else np.floor((10 / pct_bins) * 10)
433
+ # print(quantile)
434
+ quantile = int(quantile)
435
+ # display(model_eval_result_df)
436
+
437
+ import re
438
+ # btm_str = [x for x in model_eval_result_df.columns if x.startswith("Btm")][0]
439
+ # top_str = [x for x in model_eval_result_df.columns if x.startswith("Top")][0]
440
+ btm_cols = [x for x in model_eval_result_df.columns if x.startswith("Btm")]
441
+ top_cols = [x for x in model_eval_result_df.columns if x.startswith("Top")]
442
+
443
+ if btm_cols and top_cols:
444
+ btm_str = btm_cols[0]
445
+ top_str = top_cols[0]
446
+ # 提取数字
447
+ numbers = re.findall(r'\d+', btm_str)
448
+ if numbers:
449
+ quantile = int(numbers[0])
450
+ else:
451
+ # 默认值,例如设为 pct_bins 的倒数?
452
+ quantile = pct_bins # 或其他合理默认
453
+ else:
454
+ # 如果没有这些列,说明数据不足或未生成,跳过后续计算或赋予默认值
455
+ # 这里可以选择跳过 Lift 列的计算,直接返回 model_eval_result_df
456
+ return model_eval_result_df
457
+
458
+ # quantile = int(re.findall(r'\d+', btm_str)[0])
459
+
460
+ model_eval_result_df[f"Btm{quantile}%_Lift"] = model_eval_result_df[btm_str]/model_eval_result_df["avgTrue"]
461
+ model_eval_result_df[f"Top{quantile}%_Lift"] = model_eval_result_df[top_str]/model_eval_result_df["avgTrue"]
462
+
463
+ model_eval_result_df["AUC_Shift"] = model_eval_result_df["AUC"].shift(1)/model_eval_result_df["AUC"] - 1
464
+ model_eval_result_df["KS_Shift"] = model_eval_result_df["KS"].shift(1)/model_eval_result_df["KS"] - 1
465
+
466
+ ### Gains Table Summary
467
+ gains_table_cols = ['N_BUMP', 'MIN_RISK_DEP', 'MAX_RISK_DEP', 'KS_IN_GAINS', 'LIFT_IN_GAINS', 'IV', 'N_BINS']
468
+ if train is not None:
469
+ ins_gains = get_gains_table(data = train,
470
+ dep = tgt_name,
471
+ nbins=pct_bins,
472
+ precision=precision,
473
+ min_bin_prop=min_bin_prop,
474
+ include_missing=include_missing,
475
+ score=scr_name,
476
+ equal_freq=equal_freq,
477
+ chi2_method=chi2_method,
478
+ model = model,
479
+ varlist = feature_cols,
480
+ init_equi_bins = init_equi_bins,
481
+ chi2_p = chi2_p,
482
+ retSummary=True,
483
+ tree_binning = tree_binning,
484
+ random_state = random_state)
485
+ else:
486
+ ins_gains = pd.DataFrame([], columns = gains_table_cols)
487
+
488
+ if validation is not None:
489
+ oos_gains = get_gains_table(data = validation,
490
+ dep = tgt_name,
491
+ nbins = pct_bins,
492
+ precision=precision,
493
+ min_bin_prop=min_bin_prop,
494
+ include_missing=include_missing,
495
+ score=scr_name,
496
+ equal_freq=equal_freq,
497
+ chi2_method=chi2_method,
498
+ model = model,
499
+ varlist = feature_cols,
500
+ init_equi_bins=init_equi_bins,
501
+ chi2_p=chi2_p,
502
+ retSummary=True,
503
+ tree_binning = tree_binning,
504
+ random_state = random_state)
505
+ else:
506
+ oos_gains = pd.DataFrame([], columns = gains_table_cols)
507
+
508
+ if oot is not None:
509
+ oot_gains = get_gains_table(data = oot,
510
+ dep = tgt_name,
511
+ nbins=pct_bins,
512
+ precision=precision,
513
+ min_bin_prop=min_bin_prop,
514
+ include_missing=include_missing,
515
+ score=scr_name,
516
+ equal_freq=equal_freq,
517
+ chi2_method=chi2_method,
518
+ model = model,
519
+ varlist = feature_cols,
520
+ init_equi_bins=init_equi_bins,
521
+ chi2_p=chi2_p,
522
+ retSummary=True,
523
+ tree_binning = tree_binning,
524
+ random_state = random_state)
525
+ else:
526
+ oot_gains = pd.DataFrame([], columns = gains_table_cols)
527
+
528
+ ins_gains['index'] = 'ins'
529
+ oos_gains['index'] = 'oos'
530
+ oot_gains['index'] = 'oot'
531
+
532
+ gains_summ = pd.concat([ins_gains, oos_gains, oot_gains])
533
+
534
+ model_eval_result_df = model_eval_result_df.merge(gains_summ, on = ['index'], how = 'left')
535
+
536
+ if display:
537
+ from IPython.display import display
538
+ display(model_eval_result_df)
539
+
540
+ if rpt_save_path:
541
+ model_eval_result_df.to_csv(rpt_save_path, index=False)
542
+
543
+ return model_eval_result_df
544
+
545
+
546
+ def _get_gains_by_custom_metrics_scr(data, score, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True, equal_freq = True,
547
+ chi2_method = False, chi2_p = 0.95, init_equi_bins = 2000, fillna = -999999, spec_values = [],
548
+ tree_binning = False, random_state=42,
549
+ eval_metrics = ["age", "monthly_income", "education"], metric_agg_func = "mean",
550
+ ascending = False, withSummary = False):
551
+ """
552
+ 计算指定分数字段的收益表,并包含自定义指标的聚合统计。
553
+
554
+ 对数据进行分箱处理后,计算每个分箱的基础统计指标以及
555
+ 自定义指标的聚合值(如均值等)。
556
+
557
+ Parameters
558
+ ----------
559
+ data : pandas.DataFrame
560
+ 输入数据表
561
+ score : str
562
+ 分数字段名
563
+ dep : str
564
+ 目标变量名
565
+ nbins : int, default 10
566
+ 分箱数量
567
+ precision : int, default 5
568
+ 边界值精度
569
+ min_bin_prop : float, default 0.05
570
+ 每箱最小样本占比
571
+ include_missing : bool, default True
572
+ 是否包含缺失值
573
+ equal_freq : bool, default True
574
+ True为等频分箱
575
+ chi2_method : bool, default False
576
+ 是否使用卡方分箱
577
+ chi2_p : float, default 0.95
578
+ 卡方检验显著性水平
579
+ init_equi_bins : int, default 2000
580
+ 初始等频分箱数量
581
+ fillna : any, default -999999
582
+ 缺失值填充值
583
+ spec_values : list, default []
584
+ 特殊值列表
585
+ tree_binning : bool, default False
586
+ 是否使用决策树分箱
587
+ random_state : int, default 42
588
+ 随机种子
589
+ eval_metrics : list, default ["age", "monthly_income", "education"]
590
+ 需要统计的自定义指标列表
591
+ metric_agg_func : str or callable, default "mean"
592
+ 自定义指标的聚合函数
593
+ ascending : bool, default False
594
+ 分箱顺序是否升序
595
+ withSummary : bool, default False
596
+ 是否包含总体汇总行
597
+
598
+ Returns
599
+ -------
600
+ pandas.DataFrame
601
+ 收益表,包含基础统计和自定义指标聚合值
602
+ """
603
+
604
+ res, edges = super_binning(data = data,
605
+ score = score,
606
+ dep = dep,
607
+ nbins = nbins,
608
+ precision = precision,
609
+ min_bin_prop = min_bin_prop,
610
+ include_missing = include_missing,
611
+ equal_freq = equal_freq,
612
+ chi2_method = chi2_method,
613
+ chi2_p = chi2_p,
614
+ init_equi_bins = init_equi_bins,
615
+ fillna = fillna,
616
+ spec_values = spec_values,
617
+ tree_binning = tree_binning,
618
+ random_state = random_state,
619
+ return_edges = True,
620
+ bin_colnames = ("_bin_num", "_bin_range"),
621
+ ascending = ascending)
622
+
623
+
624
+ # 计算每个分箱的统计信息
625
+ gains_table_info = res.groupby(["_bin_num", "_bin_range"], dropna = False)\
626
+ .agg(MIN = (score, "min"),
627
+ MAX = (score, "max"),
628
+ N = ("_bin_num", "count"),
629
+ AVG_SCORE = (score, "mean"),
630
+ AVG_BAD = (dep, "mean"),
631
+ N_BAD = (dep, "sum"),
632
+ N_GOOD = (dep, lambda x: (x==0).sum()))
633
+
634
+ gains_table_metric = res.groupby(["_bin_num", "_bin_range"], dropna = False).agg({metric: metric_agg_func for metric in eval_metrics})
635
+
636
+ fnl_res = gains_table_info.merge(gains_table_metric, left_index = True, right_index = True)
637
+
638
+ if withSummary:
639
+ grand_total = {"MIN": res[score].min(),
640
+ "MAX": res[score].max(),
641
+ "N": res.shape[0],
642
+ "AVG_SCORE": res[score].mean(),
643
+ "AVG_BAD": res[dep].mean(),
644
+ "N_BAD": res[dep].sum(),
645
+ "N_GOOD": (res[dep] == 0).sum()}
646
+ grand_total.update({x: res[x].mean() for x in eval_metrics})
647
+ grand_total = pd.DataFrame(grand_total, index = [("Grand Summary", "")])
648
+ fnl_res = pd.concat([fnl_res, grand_total])
649
+
650
+ fnl_res.index.names = ("_bin_num", "_bin_range")
651
+
652
+ return fnl_res
653
+
654
+
655
+ def _get_cust_gains_table_single(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True,
656
+ score = None, model = None, varlist = None, equal_freq = True, chi2_method = False,
657
+ chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [],
658
+ tree_binning = False, random_state=42, ascending = True,
659
+ eval_metrics = ["age", "monthly_income", "education"], metric_agg_func = "mean",
660
+ withSummary = False):
661
+ """
662
+ 计算单个模型的自定义指标收益表。
663
+
664
+ 根据传入的score字段或模型预测结果,计算包含自定义指标聚合统计的收益表。
665
+
666
+ Parameters
667
+ ----------
668
+ data : pandas.DataFrame
669
+ 输入数据表
670
+ dep : str
671
+ 目标变量名
672
+ nbins : int, default 10
673
+ 分箱数量
674
+ precision : int, default 5
675
+ 边界值精度
676
+ min_bin_prop : float, default 0.05
677
+ 每箱最小样本占比
678
+ include_missing : bool, default True
679
+ 是否包含缺失值
680
+ score : str, optional
681
+ 分数字段名
682
+ model : sklearn-like model, optional
683
+ 机器学习模型
684
+ varlist : list, optional
685
+ 模型特征列表
686
+ equal_freq : bool, default True
687
+ True为等频分箱
688
+ chi2_method : bool, default False
689
+ 是否使用卡方分箱
690
+ chi2_p : float, default 0.95
691
+ 卡方检验显著性水平
692
+ init_equi_bins : int, default 100
693
+ 初始等频分箱数量
694
+ fillna : any, default -999999
695
+ 缺失值填充值
696
+ spec_values : list, default []
697
+ 特殊值列表
698
+ tree_binning : bool, default False
699
+ 是否使用决策树分箱
700
+ random_state : int, default 42
701
+ 随机种子
702
+ ascending : bool, default True
703
+ 分箱顺序是否升序
704
+ eval_metrics : list, default ["age", "monthly_income", "education"]
705
+ 需要统计的自定义指标列表
706
+ metric_agg_func : str or callable, default "mean"
707
+ 自定义指标的聚合函数
708
+ withSummary : bool, default False
709
+ 是否包含总体汇总行
710
+
711
+ Returns
712
+ -------
713
+ pandas.DataFrame or int
714
+ 收益表;若缺少必要参数返回-1/-2/-3
715
+ """
716
+
717
+ if score is None and model is None and varlist is None:
718
+ return -1
719
+
720
+ if score is None and model is None:
721
+ return -2
722
+
723
+ if score is None and varlist is None:
724
+ return -3
725
+
726
+ if score is None:
727
+ data['_mdl_scr'] = model.predict_proba(data.loc[:, varlist])[:, 1]
728
+ score = '_mdl_scr'
729
+
730
+ res = _get_gains_by_custom_metrics_scr(data = data,
731
+ score = score,
732
+ dep = dep,
733
+ nbins = nbins,
734
+ precision = precision,
735
+ min_bin_prop=min_bin_prop,
736
+ include_missing = include_missing,
737
+ equal_freq = equal_freq,
738
+ chi2_method = chi2_method,
739
+ chi2_p = chi2_p,
740
+ init_equi_bins = init_equi_bins,
741
+ fillna = fillna,
742
+ spec_values = spec_values,
743
+ eval_metrics = eval_metrics,
744
+ tree_binning = tree_binning,
745
+ random_state = random_state,
746
+ metric_agg_func = metric_agg_func,
747
+ ascending = ascending,
748
+ withSummary = withSummary)
749
+ return res
750
+
751
+ ###################################################### Private Functions (End) #############################################################
752
+
753
+
754
+ def get_gains_table(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True,
755
+ score = None, model = None, varlist = None, equal_freq = True, chi2_method = False,
756
+ grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True,
757
+ chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [], retSummary = False,
758
+ tree_binning = False, random_state=42, ascending = False, withSummary = False, wholeGroup = False,
759
+ add_func = None, weight_col = None, weighted_binning = None):
760
+ """
761
+ 计算分组收益表。
762
+
763
+ 根据分组字段对数据进行分组,分别计算每个分组的收益表。
764
+ 若未指定分组字段,则计算整体收益表。
765
+
766
+ Parameters
767
+ ----------
768
+ data : pandas.DataFrame
769
+ 输入数据表
770
+ dep : str
771
+ 目标变量名
772
+ nbins : int, default 10
773
+ 分箱数量
774
+ precision : int, default 5
775
+ 边界值精度
776
+ min_bin_prop : float, default 0.05
777
+ 每箱最小样本占比
778
+ include_missing : bool, default True
779
+ 是否包含缺失值
780
+ score : str, optional
781
+ 分数字段名
782
+ model : sklearn-like model, optional
783
+ 机器学习模型
784
+ varlist : list, optional
785
+ 模型特征列表
786
+ equal_freq : bool, default True
787
+ True为等频分箱
788
+ chi2_method : bool, default False
789
+ 是否使用卡方分箱
790
+ grp_name : str, optional
791
+ 分组字段名
792
+ min_data_size : int, default 100
793
+ 每组最小样本数
794
+ grp_colname : str, optional
795
+ 分组结果列名
796
+ sync_range : bool, default True
797
+ 是否同步分箱边界
798
+ chi2_p : float, default 0.95
799
+ 卡方检验显著性水平
800
+ init_equi_bins : int, default 100
801
+ 初始等频分箱数量
802
+ fillna : any, default -999999
803
+ 缺失值填充值
804
+ spec_values : list, default []
805
+ 特殊值列表
806
+ retSummary : bool, default False
807
+ 是否只返回汇总指标
808
+ tree_binning : bool, default False
809
+ 是否使用决策树分箱
810
+ random_state : int, default 42
811
+ 随机种子
812
+ ascending : bool, default False
813
+ 分箱顺序是否升序
814
+ withSummary : bool, default False
815
+ 是否包含总体汇总行
816
+ wholeGroup : bool, default False
817
+ 是否使用全部数据分组
818
+ add_func : callable, optional
819
+ 自定义统计函数
820
+ weight_col : str, optional
821
+ 样本权重列名;提供且无 ``grp_name`` 时按权重聚合 Gains(输出 ``N`` 为权重和、``N_RAW`` 为行数)
822
+ weighted_binning : bool, optional
823
+ True 时按累计权重等频分箱;False 时按行数(默认)
824
+
825
+ Returns
826
+ -------
827
+ pandas.DataFrame
828
+ 分组收益表
829
+ """
830
+
831
+ if weight_col is not None and grp_name is None:
832
+ work_data = data.copy()
833
+ work_score = score
834
+ if work_score is None:
835
+ if model is None or varlist is None:
836
+ return _get_gains_table_single(
837
+ data=data,
838
+ dep=dep,
839
+ nbins=nbins,
840
+ precision=precision,
841
+ min_bin_prop=min_bin_prop,
842
+ include_missing=include_missing,
843
+ score=score,
844
+ model=model,
845
+ varlist=varlist,
846
+ equal_freq=equal_freq,
847
+ chi2_method=chi2_method,
848
+ chi2_p=chi2_p,
849
+ init_equi_bins=init_equi_bins,
850
+ fillna=fillna,
851
+ spec_values=spec_values,
852
+ retSummary=retSummary,
853
+ tree_binning=tree_binning,
854
+ random_state=random_state,
855
+ ascending=ascending,
856
+ withSummary=withSummary,
857
+ add_func=add_func,
858
+ )
859
+ work_score = "_mdl_scr"
860
+ work_data[work_score] = model.predict_proba(work_data.loc[:, varlist])[:, 1]
861
+ gains = _weighted_eval.get_gains_table(
862
+ work_data,
863
+ dep,
864
+ work_score,
865
+ nbins=nbins,
866
+ weight_col=weight_col,
867
+ weighted_binning=weighted_binning,
868
+ )
869
+ if retSummary:
870
+ return pd.DataFrame({
871
+ "N_BUMP": [gains["RANK_ORDER_BUMP"].sum()],
872
+ "MIN_RISK_DEP": [gains["TRUE_BAD_SHIFT"].round(4).min()],
873
+ "MAX_RISK_DEP": [gains["TRUE_BAD_SHIFT"].round(4).max()],
874
+ "KS_IN_GAINS": [gains["KS_PER_BIN"].round(4).max()],
875
+ "LIFT_IN_GAINS": [gains["LIFT"].round(4).max()],
876
+ "IV": [gains["IV"].replace([np.inf, -np.inf], 0).sum()],
877
+ "N_BINS": [gains.shape[0]],
878
+ })
879
+ return gains
880
+
881
+ if grp_colname is None:
882
+ grp_colname = grp_name
883
+
884
+ if grp_name is None:
885
+ fnl_df = _get_gains_table_single(data = data,
886
+ dep = dep,
887
+ nbins = nbins,
888
+ precision = precision,
889
+ min_bin_prop = min_bin_prop,
890
+ include_missing = include_missing,
891
+ score = score,
892
+ model = model,
893
+ varlist = varlist,
894
+ equal_freq = equal_freq,
895
+ chi2_method = chi2_method,
896
+ chi2_p = chi2_p,
897
+ init_equi_bins = init_equi_bins,
898
+ fillna = fillna,
899
+ spec_values = spec_values,
900
+ retSummary = retSummary,
901
+ tree_binning = tree_binning,
902
+ random_state = random_state,
903
+ ascending = ascending,
904
+ withSummary = withSummary,
905
+ add_func = add_func)
906
+ return fnl_df
907
+
908
+ withSummary = False
909
+ grp_list = data[grp_name].sort_values(ascending = True).unique().tolist()
910
+ valid_grp_list = [x for x in grp_list if data[data[grp_name].isin([x])].shape[0] >= min_data_size]
911
+
912
+ if len(valid_grp_list) == 0:
913
+ fnl_df = _get_gains_table_single(data = data,
914
+ dep = dep,
915
+ nbins = nbins,
916
+ precision = precision,
917
+ min_bin_prop = min_bin_prop,
918
+ include_missing = include_missing,
919
+ score = score,
920
+ model = model,
921
+ varlist = varlist,
922
+ equal_freq = equal_freq,
923
+ chi2_method = chi2_method,
924
+ chi2_p = chi2_p,
925
+ init_equi_bins = init_equi_bins,
926
+ fillna = fillna,
927
+ spec_values = spec_values,
928
+ retSummary = retSummary,
929
+ tree_binning = tree_binning,
930
+ random_state = random_state,
931
+ ascending = ascending,
932
+ withSummary = withSummary,
933
+ add_func = add_func)
934
+ fnl_df[grp_colname] = np.nan
935
+ col_index = pd.MultiIndex.from_tuples([], names=['_bin_num', '_bin_range'])
936
+ fnl_df = pd.DataFrame([], columns = fnl_df.columns, index = col_index)
937
+ return fnl_df
938
+
939
+ first_grp = valid_grp_list[0]
940
+ data_grp = data[data[grp_name].isin([first_grp])]
941
+
942
+ grp_for_binning = data_grp if not wholeGroup else data
943
+
944
+ if sync_range and retSummary:
945
+
946
+ full_gains = _get_gains_table_single(data = grp_for_binning,
947
+ dep = dep,
948
+ nbins = nbins,
949
+ precision = precision,
950
+ min_bin_prop = min_bin_prop,
951
+ include_missing = include_missing,
952
+ score = score,
953
+ model = model,
954
+ varlist = varlist,
955
+ equal_freq = equal_freq,
956
+ chi2_method = chi2_method,
957
+ chi2_p = chi2_p,
958
+ init_equi_bins = init_equi_bins,
959
+ fillna = fillna,
960
+ spec_values = spec_values,
961
+ retSummary = False,
962
+ tree_binning = tree_binning,
963
+ random_state = random_state,
964
+ ascending = ascending,
965
+ withSummary = withSummary,
966
+ add_func = add_func)
967
+
968
+ bin_range = get_bin_range_list(full_gains.reset_index())
969
+ bin_range = bin_range + [-np.inf, np.inf]
970
+ bin_range = sorted(list(set(bin_range)))
971
+
972
+ fnl_df = _get_gains_table_single(data = data_grp,
973
+ dep = dep,
974
+ nbins = nbins,
975
+ precision = precision,
976
+ min_bin_prop = min_bin_prop,
977
+ include_missing = include_missing,
978
+ score = score,
979
+ model = model,
980
+ varlist = varlist,
981
+ equal_freq = equal_freq,
982
+ chi2_method = chi2_method,
983
+ chi2_p = chi2_p,
984
+ init_equi_bins = init_equi_bins,
985
+ fillna = fillna,
986
+ spec_values = spec_values,
987
+ retSummary = retSummary,
988
+ tree_binning = tree_binning,
989
+ random_state = random_state,
990
+ ascending = ascending,
991
+ withSummary = withSummary,
992
+ add_func = add_func)
993
+
994
+ fnl_df[grp_colname] = first_grp
995
+
996
+ nbins = bin_range
997
+
998
+ equal_freq = False
999
+ chi2_method = False
1000
+
1001
+ if sync_range and not retSummary:
1002
+
1003
+ fnl_df = _get_gains_table_single(data = data_grp,
1004
+ dep = dep,
1005
+ nbins = nbins,
1006
+ precision = precision,
1007
+ min_bin_prop = min_bin_prop,
1008
+ include_missing = include_missing,
1009
+ score = score,
1010
+ model = model,
1011
+ varlist = varlist,
1012
+ equal_freq = equal_freq,
1013
+ chi2_method = chi2_method,
1014
+ chi2_p = chi2_p,
1015
+ init_equi_bins = init_equi_bins,
1016
+ fillna = fillna,
1017
+ spec_values = spec_values,
1018
+ retSummary = retSummary,
1019
+ tree_binning = tree_binning,
1020
+ random_state = random_state,
1021
+ ascending = ascending,
1022
+ withSummary = withSummary,
1023
+ add_func = add_func)
1024
+
1025
+ fnl_df[grp_colname] = first_grp
1026
+
1027
+ nbins = get_bin_range_list(fnl_df.reset_index())
1028
+ nbins = nbins + [-np.inf, np.inf]
1029
+ nbins = sorted(list(set(nbins)))
1030
+
1031
+ equal_freq = False
1032
+ chi2_method = False
1033
+
1034
+ if (not sync_range and not retSummary) or (not sync_range and retSummary):
1035
+
1036
+ fnl_df = _get_gains_table_single(data = data_grp,
1037
+ dep = dep,
1038
+ nbins = nbins,
1039
+ precision = precision,
1040
+ min_bin_prop = min_bin_prop,
1041
+ include_missing = include_missing,
1042
+ score = score,
1043
+ model = model,
1044
+ varlist = varlist,
1045
+ equal_freq = equal_freq,
1046
+ chi2_method = chi2_method,
1047
+ chi2_p = chi2_p,
1048
+ init_equi_bins = init_equi_bins,
1049
+ fillna = fillna,
1050
+ spec_values = spec_values,
1051
+ retSummary = retSummary,
1052
+ tree_binning = tree_binning,
1053
+ random_state = random_state,
1054
+ ascending = ascending,
1055
+ withSummary = withSummary,
1056
+ add_func = add_func)
1057
+
1058
+ fnl_df[grp_colname] = first_grp
1059
+
1060
+ i = 1
1061
+ while i < len(valid_grp_list):
1062
+
1063
+ grp = grp_list[i]
1064
+ data_grp = data[data[grp_name].isin([grp])]
1065
+
1066
+ perf_res = _get_gains_table_single(data = data_grp,
1067
+ dep = dep,
1068
+ nbins = nbins,
1069
+ precision = precision,
1070
+ min_bin_prop = min_bin_prop,
1071
+ include_missing = include_missing,
1072
+ score = score,
1073
+ model = model,
1074
+ varlist = varlist,
1075
+ equal_freq = equal_freq,
1076
+ chi2_method = chi2_method,
1077
+ chi2_p = chi2_p,
1078
+ init_equi_bins = init_equi_bins,
1079
+ fillna = fillna,
1080
+ spec_values = spec_values,
1081
+ retSummary = retSummary,
1082
+ tree_binning = tree_binning,
1083
+ random_state = random_state,
1084
+ ascending = ascending,
1085
+ withSummary = withSummary,
1086
+ add_func = add_func)
1087
+
1088
+ perf_res[grp_colname] = grp
1089
+
1090
+ fnl_df = pd.concat([fnl_df, perf_res])
1091
+ i += 1
1092
+
1093
+ return fnl_df
1094
+
1095
+
1096
+ def get_perf_summary(train, validation, oot, tgt_name,
1097
+ scr_name = None,
1098
+ model = None,
1099
+ feature_cols = None,
1100
+ fig_save_path = None,
1101
+ rpt_save_path = None,
1102
+ to_show = False,
1103
+ display = True,
1104
+ dist_bins = 20,
1105
+ pct_bins = 10,
1106
+ precision = 5,
1107
+ min_bin_prop = 0.05,
1108
+ include_missing = False,
1109
+ equal_freq = True,
1110
+ chi2_method = False,
1111
+ init_equi_bins = 1000,
1112
+ chi2_p = 0.9,
1113
+ oot_grp_name = None,
1114
+ min_data_size = 100,
1115
+ grp_colname = None,
1116
+ tree_binning = False,
1117
+ random_state = 42,
1118
+ gains_table = False,
1119
+ weight_col = None):
1120
+ """
1121
+ 计算分组性能评估汇总。
1122
+
1123
+ 对训练集、验证集和oot样本进行分组性能评估,根据oot_grp_name
1124
+ 字段分组后分别计算各组的性能指标。
1125
+
1126
+ Parameters
1127
+ ----------
1128
+ train : pandas.DataFrame, optional
1129
+ 训练数据集
1130
+ validation : pandas.DataFrame, optional
1131
+ 验证数据集
1132
+ oot : pandas.DataFrame, optional
1133
+ oot数据集
1134
+ tgt_name : str
1135
+ 目标变量名
1136
+ scr_name : str, optional
1137
+ 分数字段名
1138
+ model : sklearn-like model, optional
1139
+ 机器学习模型
1140
+ feature_cols : list, optional
1141
+ 模型特征列表
1142
+ fig_save_path : str, optional
1143
+ 图片保存路径
1144
+ rpt_save_path : str, optional
1145
+ 报告保存路径
1146
+ to_show : bool, default False
1147
+ 是否显示图形
1148
+ display : bool, default True
1149
+ 是否打印结果
1150
+ dist_bins : int, default 20
1151
+ 分布分箱数
1152
+ pct_bins : int, default 10
1153
+ 百分比分箱数
1154
+ precision : int, default 5
1155
+ 边界值精度
1156
+ min_bin_prop : float, default 0.05
1157
+ 每箱最小样本占比
1158
+ include_missing : bool, default False
1159
+ 是否包含缺失值
1160
+ equal_freq : bool, default True
1161
+ True为等频分箱
1162
+ chi2_method : bool, default False
1163
+ 是否使用卡方分箱
1164
+ init_equi_bins : int, default 1000
1165
+ 初始等频分箱数量
1166
+ chi2_p : float, default 0.9
1167
+ 卡方检验显著性水平
1168
+ oot_grp_name : str, optional
1169
+ oot分组字段名
1170
+ min_data_size : int, default 100
1171
+ 每组最小样本数
1172
+ grp_colname : str, optional
1173
+ 分组结果列名
1174
+ tree_binning : bool, default False
1175
+ 是否使用决策树分箱
1176
+ random_state : int, default 42
1177
+ 随机种子
1178
+ gains_table : bool, default True
1179
+ 是否计算收益表
1180
+ weight_col : str, optional
1181
+ 各数据集 DataFrame 中的样本权重列;无分组时用于加权 AUC/KS 等指标
1182
+
1183
+ Returns
1184
+ -------
1185
+ pandas.DataFrame
1186
+ 分组性能评估汇总表
1187
+ """
1188
+
1189
+ if weight_col is not None and oot_grp_name is None:
1190
+ return _weighted_eval.get_perf_summary(
1191
+ train=train,
1192
+ validation=validation,
1193
+ oot=oot,
1194
+ tgt_name=tgt_name,
1195
+ scr_name=scr_name,
1196
+ weight_col=weight_col,
1197
+ nbins=pct_bins,
1198
+ )
1199
+
1200
+ if grp_colname is None:
1201
+ grp_colname = oot_grp_name
1202
+
1203
+ if oot_grp_name is None:
1204
+ fnl_df = _get_perf_summary_single(train = train,
1205
+ validation = validation,
1206
+ oot = oot,
1207
+ scr_name = scr_name,
1208
+ model = model,
1209
+ tgt_name = tgt_name,
1210
+ display=display,
1211
+ feature_cols=feature_cols,
1212
+ to_show=to_show,
1213
+ fig_save_path=fig_save_path,
1214
+ rpt_save_path=rpt_save_path,
1215
+ dist_bins=dist_bins,
1216
+ pct_bins=pct_bins,
1217
+ precision = precision,
1218
+ min_bin_prop = min_bin_prop,
1219
+ include_missing = include_missing,
1220
+ equal_freq = equal_freq,
1221
+ chi2_method = chi2_method,
1222
+ init_equi_bins = init_equi_bins,
1223
+ chi2_p = chi2_p,
1224
+ tree_binning = tree_binning,
1225
+ random_state = random_state,
1226
+ gains_table = gains_table)
1227
+ return fnl_df
1228
+
1229
+ grp_list = oot[oot_grp_name].sort_values(ascending = True).unique().tolist()
1230
+ valid_grp_list = [x for x in grp_list if oot[oot[oot_grp_name].isin([x])].shape[0] >= min_data_size]
1231
+
1232
+ if len(valid_grp_list) == 0:
1233
+ return pd.DataFrame([])
1234
+
1235
+ first_grp = valid_grp_list[0]
1236
+ oot_grp = oot[oot[oot_grp_name].isin([first_grp])]
1237
+
1238
+ fnl_df = _get_perf_summary_single(train = train,
1239
+ validation = validation,
1240
+ oot = oot_grp,
1241
+ scr_name = scr_name,
1242
+ model = model,
1243
+ tgt_name = tgt_name,
1244
+ display=display,
1245
+ feature_cols=feature_cols,
1246
+ to_show=to_show,
1247
+ fig_save_path=fig_save_path,
1248
+ rpt_save_path=rpt_save_path,
1249
+ dist_bins=dist_bins,
1250
+ pct_bins=pct_bins,
1251
+ precision = precision,
1252
+ min_bin_prop = min_bin_prop,
1253
+ include_missing = include_missing,
1254
+ equal_freq = equal_freq,
1255
+ chi2_method = chi2_method,
1256
+ init_equi_bins = init_equi_bins,
1257
+ chi2_p = chi2_p,
1258
+ tree_binning = tree_binning,
1259
+ random_state = random_state,
1260
+ gains_table = gains_table)
1261
+ fnl_df[grp_colname] = first_grp
1262
+
1263
+ i = 1
1264
+ while i < len(valid_grp_list):
1265
+
1266
+ grp = grp_list[i]
1267
+ oot_grp = oot[oot[oot_grp_name].isin([grp])]
1268
+
1269
+ # print(grp)
1270
+
1271
+ perf_res = _get_perf_summary_single(train = train,
1272
+ validation = validation,
1273
+ oot = oot_grp,
1274
+ scr_name = scr_name,
1275
+ model = model,
1276
+ tgt_name = tgt_name,
1277
+ display=display,
1278
+ feature_cols=feature_cols,
1279
+ to_show=to_show,
1280
+ fig_save_path=fig_save_path,
1281
+ rpt_save_path=rpt_save_path,
1282
+ dist_bins=dist_bins,
1283
+ pct_bins=pct_bins,
1284
+ precision = precision,
1285
+ min_bin_prop = min_bin_prop,
1286
+ include_missing = include_missing,
1287
+ equal_freq = equal_freq,
1288
+ chi2_method = chi2_method,
1289
+ init_equi_bins = init_equi_bins,
1290
+ chi2_p = chi2_p,
1291
+ tree_binning = tree_binning,
1292
+ random_state = random_state,
1293
+ gains_table = gains_table)
1294
+ perf_res[grp_colname] = grp
1295
+
1296
+ fnl_df = pd.concat([fnl_df, perf_res])
1297
+ i += 1
1298
+
1299
+ return fnl_df
1300
+
1301
+
1302
+ def cross_risk(data, score_list, dep, nbins, agg_col = None, precision = 5, min_bin_prop = 0.05, include_missing = False,
1303
+ equal_freq = True, binning_numeric = [True, True], agg_func = 'mean', chi2_method = False,
1304
+ chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [],
1305
+ tree_binning = False, random_state = 42, weight_col = None, sample_weight = None, wgt_col = None):
1306
+ """
1307
+ 创建交叉风险表。
1308
+
1309
+ 对两个分数字段进行分箱后,计算交叉分组的风险聚合值。
1310
+ 支持对数值型变量进行自动分箱。
1311
+
1312
+ Parameters
1313
+ ----------
1314
+ data : pandas.DataFrame
1315
+ 输入数据表
1316
+ score_list : list
1317
+ 分数字段列表,长度为2
1318
+ dep : str
1319
+ 目标变量名
1320
+ nbins : int or list
1321
+ 分箱数量(整数或长度为2的列表)
1322
+ agg_col : str, optional
1323
+ 聚合列名,默认为dep
1324
+ precision : int or list, default 5
1325
+ 边界值精度
1326
+ min_bin_prop : float or list, default 0.05
1327
+ 每箱最小样本占比
1328
+ include_missing : bool, default False
1329
+ 是否包含缺失值
1330
+ equal_freq : bool, default True
1331
+ True为等频分箱
1332
+ binning_numeric : list, default [True, True]
1333
+ 是否对数值型字段分箱
1334
+ agg_func : str, callable, tuple or dict, default 'mean'
1335
+ 聚合函数。
1336
+
1337
+ 常规用法与 ``pandas.crosstab`` 的 ``aggfunc`` 一致,例如 ``'mean'``、
1338
+ ``'sum'``、``'count'`` 或自定义函数。
1339
+
1340
+ 也支持直接计算两个字段聚合后的比值:
1341
+
1342
+ 1. 简写形式:
1343
+ ``agg_col=(numerator_col, denominator_col), agg_func='ratio'``
1344
+ 2. tuple形式:
1345
+ ``agg_func=('ratio', numerator_col, denominator_col)``
1346
+ 3. dict形式:
1347
+ ``agg_func={
1348
+ 'func': 'ratio',
1349
+ 'numerator': numerator_col,
1350
+ 'denominator': denominator_col,
1351
+ 'numerator_agg': 'sum',
1352
+ 'denominator_agg': 'sum',
1353
+ 'return_count': True,
1354
+ 'return_count_pct': True,
1355
+ 'count_name': 'N',
1356
+ 'count_pct_name': 'N_Pct',
1357
+ 'ratio_name': 'ratio',
1358
+ 'valid_only': True
1359
+ }``
1360
+
1361
+ 当 ``return_count=True`` 时,返回结果的 columns 会增加一层指标名,
1362
+ 包含 ratio 矩阵和 N 矩阵;当 ``return_count_pct=True`` 时,
1363
+ 额外返回每个格子的 N 占比矩阵,计算方式为:
1364
+ 当前格子 count / 右下角 Total count。
1365
+ chi2_method : bool, default False
1366
+ 是否使用卡方分箱
1367
+ chi2_p : float, default 0.95
1368
+ 卡方检验显著性水平
1369
+ init_equi_bins : int, default 100
1370
+ 初始等频分箱数量
1371
+ fillna : any, default -999999
1372
+ 缺失值填充值
1373
+ spec_values : list, default []
1374
+ 特殊值列表
1375
+ tree_binning : bool, default False
1376
+ 是否使用决策树分箱
1377
+ random_state : int, default 42
1378
+ 随机种子
1379
+ weight_col : str, optional
1380
+ Column in ``data`` with per-row sample weights (alias: ``wgt_col``).
1381
+ sample_weight : array-like, optional
1382
+ Explicit per-row weights; must align with ``len(data)``.
1383
+ wgt_col : str, optional
1384
+ Alias for ``weight_col``.
1385
+
1386
+ Returns
1387
+ -------
1388
+ pandas.DataFrame
1389
+ 交叉风险表
1390
+
1391
+ Examples
1392
+ --------
1393
+ >>> cross_risk(data, score_list=['score1', 'score2'], dep='target', nbins=10)
1394
+ """
1395
+
1396
+ from pandas.api.types import is_numeric_dtype
1397
+
1398
+ if agg_col is None:
1399
+ agg_col = dep
1400
+
1401
+ resolved_weight = None
1402
+ if weight_col is not None or sample_weight is not None or wgt_col is not None:
1403
+ resolved_weight = _weighted_eval.resolve_weights(
1404
+ data=data,
1405
+ weight_col=weight_col or wgt_col,
1406
+ sample_weight=sample_weight,
1407
+ expected_len=len(data),
1408
+ )
1409
+
1410
+ def _parse_ratio_agg(agg_col, agg_func):
1411
+ """
1412
+ Parse ratio aggregation syntax while keeping backward compatibility
1413
+ with the original cross_risk agg_func behavior.
1414
+ """
1415
+ ratio_cfg = None
1416
+
1417
+ if isinstance(agg_func, str) and agg_func.lower() in ["ratio", "rate", "divide"]:
1418
+ if not isinstance(agg_col, (list, tuple)) or len(agg_col) != 2:
1419
+ raise ValueError(
1420
+ "When agg_func='ratio', `agg_col` must be a two-element "
1421
+ "list/tuple: (numerator_col, denominator_col)."
1422
+ )
1423
+ ratio_cfg = {
1424
+ "numerator": agg_col[0],
1425
+ "denominator": agg_col[1],
1426
+ }
1427
+
1428
+ elif isinstance(agg_func, (list, tuple)) and len(agg_func) >= 3:
1429
+ if str(agg_func[0]).lower() in ["ratio", "rate", "divide"]:
1430
+ ratio_cfg = {
1431
+ "numerator": agg_func[1],
1432
+ "denominator": agg_func[2],
1433
+ }
1434
+ if len(agg_func) >= 4:
1435
+ ratio_cfg["return_count"] = bool(agg_func[3])
1436
+
1437
+ elif isinstance(agg_func, dict):
1438
+ func_name = str(
1439
+ agg_func.get("func", agg_func.get("type", agg_func.get("agg_func", "")))
1440
+ ).lower()
1441
+ if func_name in ["ratio", "rate", "divide"]:
1442
+ ratio_cfg = dict(agg_func)
1443
+ ratio_cfg["numerator"] = ratio_cfg.get(
1444
+ "numerator", ratio_cfg.get("num", ratio_cfg.get("numerator_col"))
1445
+ )
1446
+ ratio_cfg["denominator"] = ratio_cfg.get(
1447
+ "denominator", ratio_cfg.get("denom", ratio_cfg.get("denominator_col"))
1448
+ )
1449
+
1450
+ if ratio_cfg is None:
1451
+ return None
1452
+
1453
+ if ratio_cfg.get("numerator") is None or ratio_cfg.get("denominator") is None:
1454
+ raise ValueError(
1455
+ "Ratio aggregation requires both numerator and denominator columns. "
1456
+ "Use agg_func={'func': 'ratio', 'numerator': ..., 'denominator': ...}."
1457
+ )
1458
+
1459
+ ratio_cfg.setdefault("numerator_agg", "sum")
1460
+ ratio_cfg.setdefault("denominator_agg", "sum")
1461
+ ratio_cfg.setdefault("return_count", False)
1462
+ ratio_cfg.setdefault("return_count_pct", False)
1463
+ ratio_cfg.setdefault("count_name", "N")
1464
+ ratio_cfg.setdefault("count_pct_name", "N_Pct")
1465
+ ratio_cfg.setdefault("ratio_name", f"{ratio_cfg['numerator']}/{ratio_cfg['denominator']}")
1466
+ ratio_cfg.setdefault("valid_only", True)
1467
+ ratio_cfg.setdefault("zero_division", np.nan)
1468
+ return ratio_cfg
1469
+
1470
+ def _parse_regular_agg(agg_func):
1471
+ """
1472
+ Parse the extended regular aggregation syntax:
1473
+ agg_func={
1474
+ 'func': ['mean', 'count'],
1475
+ 'return_count_pct': True,
1476
+ 'count_func_name': 'count',
1477
+ 'count_pct_name': 'N_Pct'
1478
+ }
1479
+ """
1480
+ if not isinstance(agg_func, dict):
1481
+ return None
1482
+
1483
+ func_name = str(
1484
+ agg_func.get("func", agg_func.get("type", agg_func.get("agg_func", "")))
1485
+ ).lower()
1486
+ if func_name in ["ratio", "rate", "divide"]:
1487
+ return None
1488
+
1489
+ if "func" not in agg_func and "agg_func" not in agg_func:
1490
+ return None
1491
+
1492
+ regular_cfg = dict(agg_func)
1493
+ regular_cfg["func"] = regular_cfg.get("func", regular_cfg.get("agg_func"))
1494
+ regular_cfg.setdefault("return_count_pct", False)
1495
+ regular_cfg.setdefault("count_func_name", "count")
1496
+ regular_cfg.setdefault("count_pct_name", "N_Pct")
1497
+ regular_cfg.setdefault("margins_name", "Total_Avg_Risk")
1498
+ return regular_cfg
1499
+
1500
+ ratio_cfg = _parse_ratio_agg(agg_col, agg_func)
1501
+ regular_cfg = _parse_regular_agg(agg_func) if ratio_cfg is None else None
1502
+
1503
+ if is_numeric_dtype(data[score_list[0]]) and binning_numeric[0]:
1504
+
1505
+ data, edges1 = super_binning(data = data,
1506
+ score = score_list[0],
1507
+ dep = dep,
1508
+ nbins = nbins[0] if isinstance(nbins, list) else nbins,
1509
+ precision = precision[0] if isinstance(precision, list) else precision,
1510
+ min_bin_prop = min_bin_prop[0] if isinstance(min_bin_prop, list) else min_bin_prop,
1511
+ include_missing = include_missing,
1512
+ equal_freq = equal_freq,
1513
+ chi2_method = chi2_method,
1514
+ chi2_p = chi2_p,
1515
+ init_equi_bins = init_equi_bins,
1516
+ fillna = fillna,
1517
+ spec_values = spec_values,
1518
+ tree_binning = tree_binning,
1519
+ random_state = random_state,
1520
+ return_edges = True,
1521
+ bin_colnames = ("_bin_num1", "_bin_range1"),
1522
+ ascending = True)
1523
+
1524
+ else:
1525
+ data["_bin_num1"] = data[score_list[0]]
1526
+ data["_bin_range1"] = data[score_list[0]]
1527
+
1528
+ if is_numeric_dtype(data[score_list[1]]) and binning_numeric[1]:
1529
+
1530
+ data, edges2 = super_binning(data = data,
1531
+ score = score_list[1],
1532
+ dep = dep,
1533
+ nbins = nbins[1] if isinstance(nbins, list) else nbins,
1534
+ precision = precision[1] if isinstance(precision, list) else precision,
1535
+ min_bin_prop = min_bin_prop[1] if isinstance(min_bin_prop, list) else min_bin_prop,
1536
+ include_missing = include_missing,
1537
+ equal_freq = equal_freq,
1538
+ chi2_method = chi2_method,
1539
+ chi2_p = chi2_p,
1540
+ init_equi_bins = init_equi_bins,
1541
+ fillna = fillna,
1542
+ spec_values = spec_values,
1543
+ tree_binning = tree_binning,
1544
+ random_state = random_state,
1545
+ return_edges = True,
1546
+ bin_colnames = ("_bin_num2", "_bin_range2"),
1547
+ ascending = True)
1548
+
1549
+ else:
1550
+
1551
+ data["_bin_num2"] = data[score_list[1]]
1552
+ data["_bin_range2"] = data[score_list[1]]
1553
+
1554
+
1555
+ if ratio_cfg is not None:
1556
+ numerator = ratio_cfg["numerator"]
1557
+ denominator = ratio_cfg["denominator"]
1558
+ numerator_agg = ratio_cfg["numerator_agg"]
1559
+ denominator_agg = ratio_cfg["denominator_agg"]
1560
+ return_count = ratio_cfg["return_count"]
1561
+ return_count_pct = ratio_cfg["return_count_pct"]
1562
+ count_name = ratio_cfg["count_name"]
1563
+ count_pct_name = ratio_cfg["count_pct_name"]
1564
+ ratio_name = ratio_cfg["ratio_name"]
1565
+ valid_only = ratio_cfg["valid_only"]
1566
+ zero_division = ratio_cfg["zero_division"]
1567
+ margin_name = ratio_cfg.get("margins_name", "Total_Avg_Risk")
1568
+
1569
+ missing_cols = [c for c in [numerator, denominator] if c not in data.columns]
1570
+ if len(missing_cols) > 0:
1571
+ raise ValueError(f"Columns not found in data for ratio aggregation: {missing_cols}")
1572
+
1573
+ agg_data = data.copy()
1574
+ if valid_only:
1575
+ agg_data = agg_data.loc[
1576
+ agg_data[numerator].notna()
1577
+ & agg_data[denominator].notna()
1578
+ & (agg_data[denominator] != 0)
1579
+ ].copy()
1580
+
1581
+ numerator_res = pd.crosstab([agg_data['_bin_num1'], agg_data['_bin_range1']],
1582
+ [agg_data['_bin_num2'], agg_data['_bin_range2']],
1583
+ rownames=[score_list[0], score_list[0]],
1584
+ colnames=[score_list[1], score_list[1]],
1585
+ values=agg_data[numerator],
1586
+ aggfunc=numerator_agg,
1587
+ margins=True,
1588
+ margins_name=margin_name)
1589
+
1590
+ denominator_res = pd.crosstab([agg_data['_bin_num1'], agg_data['_bin_range1']],
1591
+ [agg_data['_bin_num2'], agg_data['_bin_range2']],
1592
+ rownames=[score_list[0], score_list[0]],
1593
+ colnames=[score_list[1], score_list[1]],
1594
+ values=agg_data[denominator],
1595
+ aggfunc=denominator_agg,
1596
+ margins=True,
1597
+ margins_name=margin_name)
1598
+
1599
+ res = numerator_res / denominator_res.replace(0, np.nan)
1600
+ if not pd.isna(zero_division):
1601
+ res = res.fillna(zero_division)
1602
+
1603
+ if return_count or return_count_pct:
1604
+ count_col = ratio_cfg.get("count_col", numerator)
1605
+ count_res = pd.crosstab([agg_data['_bin_num1'], agg_data['_bin_range1']],
1606
+ [agg_data['_bin_num2'], agg_data['_bin_range2']],
1607
+ rownames=[score_list[0], score_list[0]],
1608
+ colnames=[score_list[1], score_list[1]],
1609
+ values=agg_data[count_col],
1610
+ aggfunc="count",
1611
+ margins=True,
1612
+ margins_name=margin_name)
1613
+ output_dict = {ratio_name: res}
1614
+
1615
+ if return_count:
1616
+ output_dict[count_name] = count_res
1617
+
1618
+ if return_count_pct:
1619
+ total_count = count_res.iloc[-1, -1] if count_res.shape[0] > 0 and count_res.shape[1] > 0 else 0
1620
+ if total_count == 0 or pd.isna(total_count):
1621
+ count_pct_res = count_res * np.nan
1622
+ else:
1623
+ count_pct_res = count_res / total_count
1624
+ output_dict[count_pct_name] = count_pct_res
1625
+
1626
+ res = pd.concat(output_dict, axis=1)
1627
+
1628
+ return res
1629
+
1630
+ actual_agg_func = regular_cfg["func"] if regular_cfg is not None else agg_func
1631
+ margin_name = regular_cfg.get("margins_name", "Total_Avg_Risk") if regular_cfg is not None else "Total_Avg_Risk"
1632
+
1633
+ if resolved_weight is not None:
1634
+ if ratio_cfg is not None or regular_cfg is not None:
1635
+ raise ValueError(
1636
+ "weighted cross_risk currently supports only simple mean aggregation "
1637
+ "(agg_func='mean' without extended ratio/regular dict syntax)."
1638
+ )
1639
+ agg_name = getattr(actual_agg_func, "__name__", str(actual_agg_func)).lower()
1640
+ if agg_name != "mean":
1641
+ raise ValueError(
1642
+ "weighted cross_risk currently supports only agg_func='mean'; got {0!r}".format(actual_agg_func)
1643
+ )
1644
+ return _weighted_eval.cross_risk_weighted_mean(
1645
+ data=data,
1646
+ agg_col=agg_col,
1647
+ sample_weight=resolved_weight,
1648
+ score_list=score_list,
1649
+ margin_name=margin_name,
1650
+ )
1651
+
1652
+ res = pd.crosstab([data['_bin_num1'], data['_bin_range1']],
1653
+ [data['_bin_num2'], data['_bin_range2']],
1654
+ rownames=[score_list[0], score_list[0]],
1655
+ colnames=[score_list[1], score_list[1]],
1656
+ values = data[agg_col], aggfunc = actual_agg_func,
1657
+ margins = True, margins_name=margin_name)
1658
+
1659
+ if regular_cfg is not None and regular_cfg.get("return_count_pct", False):
1660
+ count_func_name = regular_cfg.get("count_func_name", "count")
1661
+ count_pct_name = regular_cfg.get("count_pct_name", "N_Pct")
1662
+
1663
+ count_res = None
1664
+ if isinstance(res.columns, pd.MultiIndex) and count_func_name in res.columns.get_level_values(0):
1665
+ count_res = res[count_func_name]
1666
+
1667
+ if count_res is None:
1668
+ count_res = pd.crosstab([data['_bin_num1'], data['_bin_range1']],
1669
+ [data['_bin_num2'], data['_bin_range2']],
1670
+ rownames=[score_list[0], score_list[0]],
1671
+ colnames=[score_list[1], score_list[1]],
1672
+ values=data[agg_col],
1673
+ aggfunc="count",
1674
+ margins=True,
1675
+ margins_name=margin_name)
1676
+
1677
+ total_count = count_res.iloc[-1, -1] if count_res.shape[0] > 0 and count_res.shape[1] > 0 else 0
1678
+ if total_count == 0 or pd.isna(total_count):
1679
+ count_pct_res = count_res * np.nan
1680
+ else:
1681
+ count_pct_res = count_res / total_count
1682
+
1683
+ if isinstance(res.columns, pd.MultiIndex):
1684
+ res = pd.concat([res, pd.concat({count_pct_name: count_pct_res}, axis=1)], axis=1)
1685
+ else:
1686
+ res = pd.concat({regular_cfg.get("func_name", str(actual_agg_func)): res,
1687
+ count_pct_name: count_pct_res}, axis=1)
1688
+
1689
+ return res
1690
+
1691
+
1692
+ def get_gains_table_by_cust_metrics(data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05, include_missing = True,
1693
+ score = None, model = None, varlist = None, equal_freq = True, chi2_method = False,
1694
+ grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True,
1695
+ chi2_p = 0.95, init_equi_bins = 100, fillna = -999999, spec_values = [],
1696
+ tree_binning = False, random_state=42, ascending = True,
1697
+ eval_metrics = ["age", "monthly_income", "education"], metric_agg_func = "mean", withSummary = False):
1698
+ """
1699
+ 计算分组自定义指标收益表。
1700
+
1701
+ 根据分组字段对数据进行分组,分别计算每个分组的自定义指标收益表。
1702
+ 收益表包含基础统计指标以及自定义指标的聚合值。
1703
+
1704
+ Parameters
1705
+ ----------
1706
+ data : pandas.DataFrame
1707
+ 输入数据表
1708
+ dep : str
1709
+ 目标变量名
1710
+ nbins : int, default 10
1711
+ 分箱数量
1712
+ precision : int, default 5
1713
+ 边界值精度
1714
+ min_bin_prop : float, default 0.05
1715
+ 每箱最小样本占比
1716
+ include_missing : bool, default True
1717
+ 是否包含缺失值
1718
+ score : str, optional
1719
+ 分数字段名
1720
+ model : sklearn-like model, optional
1721
+ 机器学习模型
1722
+ varlist : list, optional
1723
+ 模型特征列表
1724
+ equal_freq : bool, default True
1725
+ True为等频分箱
1726
+ chi2_method : bool, default False
1727
+ 是否使用卡方分箱
1728
+ grp_name : str, optional
1729
+ 分组字段名
1730
+ min_data_size : int, default 100
1731
+ 每组最小样本数
1732
+ grp_colname : str, optional
1733
+ 分组结果列名
1734
+ sync_range : bool, default True
1735
+ 是否同步分箱边界
1736
+ chi2_p : float, default 0.95
1737
+ 卡方检验显著性水平
1738
+ init_equi_bins : int, default 100
1739
+ 初始等频分箱数量
1740
+ fillna : any, default -999999
1741
+ 缺失值填充值
1742
+ spec_values : list, default []
1743
+ 特殊值列表
1744
+ tree_binning : bool, default False
1745
+ 是否使用决策树分箱
1746
+ random_state : int, default 42
1747
+ 随机种子
1748
+ ascending : bool, default True
1749
+ 分箱顺序是否升序
1750
+ eval_metrics : list, default ["age", "monthly_income", "education"]
1751
+ 需要统计的自定义指标列表
1752
+ metric_agg_func : str or callable, default "mean"
1753
+ 自定义指标的聚合函数
1754
+ withSummary : bool, default False
1755
+ 是否包含总体汇总行
1756
+
1757
+ Returns
1758
+ -------
1759
+ pandas.DataFrame
1760
+ 分组自定义指标收益表
1761
+ """
1762
+
1763
+ if grp_colname is None:
1764
+ grp_colname = grp_name
1765
+
1766
+ if grp_name is None:
1767
+ fnl_df = _get_cust_gains_table_single(data = data,
1768
+ dep = dep,
1769
+ nbins = nbins,
1770
+ precision = precision,
1771
+ min_bin_prop = min_bin_prop,
1772
+ include_missing = include_missing,
1773
+ score = score,
1774
+ model = model,
1775
+ varlist = varlist,
1776
+ equal_freq = equal_freq,
1777
+ chi2_method = chi2_method,
1778
+ chi2_p = chi2_p,
1779
+ init_equi_bins = init_equi_bins,
1780
+ fillna = fillna,
1781
+ spec_values = spec_values,
1782
+ eval_metrics = eval_metrics,
1783
+ tree_binning = tree_binning,
1784
+ random_state = random_state,
1785
+ metric_agg_func = metric_agg_func,
1786
+ ascending = ascending,
1787
+ withSummary = withSummary)
1788
+ return fnl_df
1789
+
1790
+ grp_list = data[grp_name].sort_values(ascending = True).unique().tolist()
1791
+ valid_grp_list = [x for x in grp_list if data[data[grp_name].isin([x])].shape[0] >= min_data_size]
1792
+
1793
+ first_grp = valid_grp_list[0]
1794
+ data_grp = data[data[grp_name].isin([first_grp])]
1795
+
1796
+ if sync_range:
1797
+
1798
+ fnl_df = _get_cust_gains_table_single(data = data_grp,
1799
+ dep = dep,
1800
+ nbins = nbins,
1801
+ precision = precision,
1802
+ min_bin_prop = min_bin_prop,
1803
+ include_missing = include_missing,
1804
+ score = score,
1805
+ model = model,
1806
+ varlist = varlist,
1807
+ equal_freq = equal_freq,
1808
+ chi2_method = chi2_method,
1809
+ chi2_p = chi2_p,
1810
+ init_equi_bins = init_equi_bins,
1811
+ fillna = fillna,
1812
+ spec_values = spec_values,
1813
+ eval_metrics = eval_metrics,
1814
+ tree_binning = tree_binning,
1815
+ random_state = random_state,
1816
+ metric_agg_func = metric_agg_func,
1817
+ ascending = ascending,
1818
+ withSummary = withSummary)
1819
+
1820
+ fnl_df[grp_colname] = first_grp
1821
+
1822
+ nbins = get_bin_range_list(fnl_df.reset_index())
1823
+ equal_freq = False
1824
+ chi2_method = False
1825
+
1826
+ else:
1827
+
1828
+ fnl_df = _get_cust_gains_table_single(data = data_grp,
1829
+ dep = dep,
1830
+ nbins = nbins,
1831
+ precision = precision,
1832
+ min_bin_prop = min_bin_prop,
1833
+ include_missing = include_missing,
1834
+ score = score,
1835
+ model = model,
1836
+ varlist = varlist,
1837
+ equal_freq = equal_freq,
1838
+ chi2_method = chi2_method,
1839
+ chi2_p = chi2_p,
1840
+ init_equi_bins = init_equi_bins,
1841
+ fillna = fillna,
1842
+ spec_values = spec_values,
1843
+ eval_metrics = eval_metrics,
1844
+ tree_binning = tree_binning,
1845
+ random_state = random_state,
1846
+ metric_agg_func = metric_agg_func,
1847
+ ascending = ascending,
1848
+ withSummary = withSummary)
1849
+
1850
+ fnl_df[grp_colname] = first_grp
1851
+
1852
+ i = 1
1853
+ while i < len(valid_grp_list):
1854
+
1855
+ grp = grp_list[i]
1856
+ data_grp = data[data[grp_name].isin([grp])]
1857
+
1858
+ perf_res = _get_cust_gains_table_single(data = data_grp,
1859
+ dep = dep,
1860
+ nbins = nbins,
1861
+ precision = precision,
1862
+ min_bin_prop = min_bin_prop,
1863
+ include_missing = include_missing,
1864
+ score = score,
1865
+ model = model,
1866
+ varlist = varlist,
1867
+ equal_freq = equal_freq,
1868
+ chi2_method = chi2_method,
1869
+ chi2_p = chi2_p,
1870
+ init_equi_bins = init_equi_bins,
1871
+ fillna = fillna,
1872
+ spec_values = spec_values,
1873
+ eval_metrics = eval_metrics,
1874
+ tree_binning = tree_binning,
1875
+ random_state = random_state,
1876
+ metric_agg_func = metric_agg_func,
1877
+ ascending = ascending,
1878
+ withSummary = withSummary)
1879
+ perf_res[grp_colname] = grp
1880
+
1881
+ fnl_df = pd.concat([fnl_df, perf_res])
1882
+ i += 1
1883
+
1884
+ return fnl_df
1885
+
1886
+
1887
+ def tie_score_rate(data, score):
1888
+ """
1889
+ 计算分数重复率。
1890
+
1891
+ 计算分数中非唯一值的比例,即存在重复的样本占比。
1892
+
1893
+ Parameters
1894
+ ----------
1895
+ data : pandas.DataFrame
1896
+ 输入数据表
1897
+ score : str
1898
+ 分数字段名
1899
+
1900
+ Returns
1901
+ -------
1902
+ float
1903
+ 分数重复率(0到1之间)
1904
+
1905
+ Examples
1906
+ --------
1907
+ >>> tie_score_rate(data, 'score')
1908
+ 0.15 # 表示15%的样本存在分数重复
1909
+ """
1910
+
1911
+ n_unique_scr = len(data[score].unique())
1912
+ unique_scr_prop = n_unique_scr / data.shape[0]
1913
+ return (1 - unique_scr_prop)
1914
+
1915
+
1916
+ def score_unique_rate(data, score):
1917
+ """
1918
+ 计算分数唯一率。
1919
+
1920
+ 计算分数中唯一值占总样本数的比例。
1921
+
1922
+ Parameters
1923
+ ----------
1924
+ data : pandas.DataFrame
1925
+ 输入数据表(此参数保留但未使用)
1926
+ score : array-like
1927
+ 分数字段或数组
1928
+
1929
+ Returns
1930
+ -------
1931
+ float
1932
+ 分数唯一率(0到1之间)
1933
+
1934
+ Examples
1935
+ --------
1936
+ >>> score_unique_rate(data, data['score'])
1937
+ 0.85 # 表示85%的样本分数是唯一的
1938
+ """
1939
+
1940
+ return len(np.unique(score)) / len(score)
1941
+
1942
+
1943
+ class GainsTableCalculator:
1944
+ """
1945
+ 收益表计算器。
1946
+
1947
+ 整合了收益表计算的多种功能,支持基础收益表和自定义指标收益表。
1948
+ 提供面向对象的接口进行分组收益表计算。
1949
+
1950
+ Parameters
1951
+ ----------
1952
+ data : pandas.DataFrame
1953
+ 输入数据表
1954
+ dep : str
1955
+ 目标变量名
1956
+ nbins : int, default 10
1957
+ 分箱数量
1958
+ precision : int, default 5
1959
+ 边界值精度
1960
+ min_bin_prop : float, default 0.05
1961
+ 每箱最小样本占比
1962
+ include_missing : bool, default True
1963
+ 是否包含缺失值
1964
+ score : str, optional
1965
+ 分数字段名
1966
+ model : sklearn-like model, optional
1967
+ 机器学习模型
1968
+ varlist : list, optional
1969
+ 模型特征列表
1970
+ equal_freq : bool, default True
1971
+ True为等频分箱
1972
+ chi2_method : bool, default False
1973
+ 是否使用卡方分箱
1974
+ chi2_p : float, default 0.95
1975
+ 卡方检验显著性水平
1976
+ init_equi_bins : int, default 100
1977
+ 初始等频分箱数量
1978
+ fillna : any, default -999999
1979
+ 缺失值填充值
1980
+ spec_values : list, default []
1981
+ 特殊值列表
1982
+ tree_binning : bool, default False
1983
+ 是否使用决策树分箱
1984
+ random_state : int, default 42
1985
+ 随机种子
1986
+ ascending : bool, default False
1987
+ 分箱顺序是否升序
1988
+
1989
+ Examples
1990
+ --------
1991
+ >>> calc = GainsTableCalculator(data, dep='target', score='score', nbins=10)
1992
+ >>> result = calc.calculate(grp_name='region')
1993
+ """
1994
+
1995
+ def __init__(self, data, dep, nbins = 10, precision = 5, min_bin_prop = 0.05,
1996
+ include_missing = True, score = None, model = None, varlist = None,
1997
+ equal_freq = True, chi2_method = False, chi2_p = 0.95,
1998
+ init_equi_bins = 100, fillna = -999999, spec_values = [],
1999
+ tree_binning = False, random_state = 42, ascending = False,
2000
+ weight_col = None, weighted_binning = None):
2001
+ """
2002
+ 初始化收益表计算器。
2003
+
2004
+ Parameters
2005
+ ----------
2006
+ data : pandas.DataFrame
2007
+ 输入数据表
2008
+ dep : str
2009
+ 目标变量名
2010
+ nbins : int, default 10
2011
+ 分箱数量
2012
+ precision : int, default 5
2013
+ 边界值精度
2014
+ min_bin_prop : float, default 0.05
2015
+ 每箱最小样本占比
2016
+ include_missing : bool, default True
2017
+ 是否包含缺失值
2018
+ score : str, optional
2019
+ 分数字段名
2020
+ model : sklearn-like model, optional
2021
+ 机器学习模型
2022
+ varlist : list, optional
2023
+ 模型特征列表
2024
+ equal_freq : bool, default True
2025
+ True为等频分箱
2026
+ chi2_method : bool, default False
2027
+ 是否使用卡方分箱
2028
+ chi2_p : float, default 0.95
2029
+ 卡方检验显著性水平
2030
+ init_equi_bins : int, default 100
2031
+ 初始等频分箱数量
2032
+ fillna : any, default -999999
2033
+ 缺失值填充值
2034
+ spec_values : list, default []
2035
+ 特殊值列表
2036
+ tree_binning : bool, default False
2037
+ 是否使用决策树分箱
2038
+ random_state : int, default 42
2039
+ 随机种子
2040
+ ascending : bool, default False
2041
+ 分箱顺序是否升序
2042
+ weight_col : str, optional
2043
+ 样本权重列名(须在 ``data`` 中)
2044
+ weighted_binning : bool, optional
2045
+ True 时按累计权重等频分箱
2046
+ """
2047
+ self.data = data
2048
+ self.dep = dep
2049
+ self.nbins = nbins
2050
+ self.precision = precision
2051
+ self.min_bin_prop = min_bin_prop
2052
+ self.include_missing = include_missing
2053
+ self.score = score
2054
+ self.model = model
2055
+ self.varlist = varlist
2056
+ self.equal_freq = equal_freq
2057
+ self.chi2_method = chi2_method
2058
+ self.chi2_p = chi2_p
2059
+ self.init_equi_bins = init_equi_bins
2060
+ self.fillna = fillna
2061
+ self.spec_values = spec_values
2062
+ self.tree_binning = tree_binning
2063
+ self.random_state = random_state
2064
+ self.ascending = ascending
2065
+ self.weight_col = weight_col
2066
+ self.weighted_binning = weighted_binning
2067
+
2068
+ def calculate(self, grp_name = None, min_data_size = 100, grp_colname = None,
2069
+ sync_range = True, retSummary = False, withSummary = False,
2070
+ wholeGroup = False, add_func = None, weight_col = None):
2071
+ """
2072
+ 计算收益表。
2073
+
2074
+ Parameters
2075
+ ----------
2076
+ grp_name : str, optional
2077
+ 分组字段名
2078
+ min_data_size : int, default 100
2079
+ 每组最小样本数
2080
+ grp_colname : str, optional
2081
+ 分组结果列名
2082
+ sync_range : bool, default True
2083
+ 是否同步分箱边界
2084
+ retSummary : bool, default False
2085
+ 是否只返回汇总指标
2086
+ withSummary : bool, default False
2087
+ 是否包含总体汇总行
2088
+ wholeGroup : bool, default False
2089
+ 是否使用全部数据分组
2090
+ add_func : callable, optional
2091
+ 自定义统计函数
2092
+
2093
+ Returns
2094
+ -------
2095
+ pandas.DataFrame
2096
+ 收益表
2097
+ """
2098
+ return get_gains_table(
2099
+ data = self.data,
2100
+ dep = self.dep,
2101
+ nbins = self.nbins,
2102
+ precision = self.precision,
2103
+ min_bin_prop = self.min_bin_prop,
2104
+ include_missing = self.include_missing,
2105
+ score = self.score,
2106
+ model = self.model,
2107
+ varlist = self.varlist,
2108
+ equal_freq = self.equal_freq,
2109
+ chi2_method = self.chi2_method,
2110
+ grp_name = grp_name,
2111
+ min_data_size = min_data_size,
2112
+ grp_colname = grp_colname,
2113
+ sync_range = sync_range,
2114
+ chi2_p = self.chi2_p,
2115
+ init_equi_bins = self.init_equi_bins,
2116
+ fillna = self.fillna,
2117
+ spec_values = self.spec_values,
2118
+ retSummary = retSummary,
2119
+ tree_binning = self.tree_binning,
2120
+ random_state = self.random_state,
2121
+ ascending = self.ascending,
2122
+ withSummary = withSummary,
2123
+ wholeGroup = wholeGroup,
2124
+ add_func = add_func,
2125
+ weight_col = self.weight_col if weight_col is None else weight_col,
2126
+ weighted_binning = self.weighted_binning,
2127
+ )
2128
+
2129
+
2130
+ class PerformanceEvaluator:
2131
+ """
2132
+ 性能评估器。
2133
+
2134
+ 整合了模型性能评估的多种功能,支持多数据集、多分组的性能评估。
2135
+ 提供面向对象的接口进行性能指标计算和汇总。
2136
+
2137
+ Parameters
2138
+ ----------
2139
+ tgt_name : str or list of str
2140
+ 目标变量名。可传入多个 y 标签的 list/tuple → 逐标签评估后纵向拼接 (输出新增 tgt_name 列),
2141
+ 并为每个标签各自出图。
2142
+ scr_name : str, optional
2143
+ 分数字段名
2144
+ model : sklearn-like model, optional
2145
+ 机器学习模型
2146
+ feature_cols : list, optional
2147
+ 模型特征列表
2148
+ dist_bins : int, default 20
2149
+ 分布分箱数
2150
+ pct_bins : int, default 10
2151
+ 百分比分箱数
2152
+ precision : int, default 5
2153
+ 边界值精度
2154
+ min_bin_prop : float, default 0.05
2155
+ 每箱最小样本占比
2156
+ include_missing : bool, default False
2157
+ 是否包含缺失值
2158
+ equal_freq : bool, default True
2159
+ True为等频分箱
2160
+ chi2_method : bool, default False
2161
+ 是否使用卡方分箱
2162
+ init_equi_bins : int, default 1000
2163
+ 初始等频分箱数量
2164
+ chi2_p : float, default 0.9
2165
+ 卡方检验显著性水平
2166
+ tree_binning : bool, default False
2167
+ 是否使用决策树分箱
2168
+ random_state : int, default 42
2169
+ 随机种子
2170
+ weight_col : str, optional
2171
+ 默认权重列;各 ``add_dataset`` 也可单独指定
2172
+
2173
+ Examples
2174
+ --------
2175
+ >>> evaluator = PerformanceEvaluator(tgt_name='target', model=model, feature_cols=features)
2176
+ >>> evaluator.add_dataset('train', train_df)
2177
+ >>> evaluator.add_dataset('validation', val_df)
2178
+ >>> evaluator.add_dataset('oot', oot_df)
2179
+ >>> result = evaluator.evaluate()
2180
+ >>>
2181
+ >>> # 多 y 标签: tgt_name 传 list → 输出含 tgt_name 列的纵向拼接表, 每个标签各自出图
2182
+ >>> evaluator = PerformanceEvaluator(tgt_name=['bad_dpd7', 'bad_dpd30'], model=model, feature_cols=features)
2183
+ >>> evaluator.add_dataset('train', train_df).add_dataset('oot', oot_df)
2184
+ >>> result = evaluator.evaluate(to_show=True)
2185
+ """
2186
+
2187
+ def __init__(self, tgt_name, scr_name = None, model = None, feature_cols = None,
2188
+ dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05,
2189
+ include_missing = False, equal_freq = True, chi2_method = False,
2190
+ init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42,
2191
+ weight_col = None):
2192
+ """
2193
+ 初始化性能评估器。
2194
+
2195
+ Parameters
2196
+ ----------
2197
+ tgt_name : str or list of str
2198
+ 目标变量名。可传入多个 y 标签的 list/tuple → 逐标签评估后纵向拼接 (输出新增 tgt_name 列),
2199
+ 并为每个标签各自出图。
2200
+ scr_name : str, optional
2201
+ 分数字段名
2202
+ model : sklearn-like model, optional
2203
+ 机器学习模型
2204
+ feature_cols : list, optional
2205
+ 模型特征列表
2206
+ dist_bins : int, default 20
2207
+ 分布分箱数
2208
+ pct_bins : int, default 10
2209
+ 百分比分箱数
2210
+ precision : int, default 5
2211
+ 边界值精度
2212
+ min_bin_prop : float, default 0.05
2213
+ 每箱最小样本占比
2214
+ include_missing : bool, default False
2215
+ 是否包含缺失值
2216
+ equal_freq : bool, default True
2217
+ True为等频分箱
2218
+ chi2_method : bool, default False
2219
+ 是否使用卡方分箱
2220
+ init_equi_bins : int, default 1000
2221
+ 初始等频分箱数量
2222
+ chi2_p : float, default 0.9
2223
+ 卡方检验显著性水平
2224
+ tree_binning : bool, default False
2225
+ 是否使用决策树分箱
2226
+ random_state : int, default 42
2227
+ 随机种子
2228
+ weight_col : str, optional
2229
+ 默认权重列;各 ``add_dataset`` 也可单独指定
2230
+ """
2231
+ self.tgt_name = tgt_name
2232
+ self.scr_name = scr_name
2233
+ self.model = model
2234
+ self.feature_cols = feature_cols
2235
+ self.dist_bins = dist_bins
2236
+ self.pct_bins = pct_bins
2237
+ self.precision = precision
2238
+ self.min_bin_prop = min_bin_prop
2239
+ self.include_missing = include_missing
2240
+ self.equal_freq = equal_freq
2241
+ self.chi2_method = chi2_method
2242
+ self.init_equi_bins = init_equi_bins
2243
+ self.chi2_p = chi2_p
2244
+ self.tree_binning = tree_binning
2245
+ self.random_state = random_state
2246
+ self.weight_col = weight_col
2247
+ self.datasets = {}
2248
+ self.dataset_weight_cols = {}
2249
+
2250
+ def add_dataset(self, name, data, weight_col = None):
2251
+ """
2252
+ 添加数据集。
2253
+
2254
+ Parameters
2255
+ ----------
2256
+ name : str
2257
+ 数据集名称(如'train'、'validation'、'oot')
2258
+ data : pandas.DataFrame
2259
+ 数据集
2260
+
2261
+ Returns
2262
+ -------
2263
+ self
2264
+ 返回自身以便链式调用
2265
+ """
2266
+ self.datasets[name] = data
2267
+ self.dataset_weight_cols[name] = weight_col
2268
+ return self
2269
+
2270
+ def evaluate(self, oot_grp_name = None, min_data_size = 100, grp_colname = None,
2271
+ fig_save_path = None, rpt_save_path = None, to_show = False,
2272
+ display = True, gains_table = False, benchmark_dataset = None,
2273
+ weight_col = None):
2274
+ """
2275
+ 执行性能评估。
2276
+
2277
+ Parameters
2278
+ ----------
2279
+ oot_grp_name : str, optional
2280
+ oot分组字段名
2281
+ min_data_size : int, default 100
2282
+ 每组最小样本数
2283
+ grp_colname : str, optional
2284
+ 分组结果列名
2285
+ fig_save_path : str, optional
2286
+ 图片保存路径
2287
+ rpt_save_path : str, optional
2288
+ 报告保存路径
2289
+ to_show : bool, default False
2290
+ 是否显示图形
2291
+ display : bool, default True
2292
+ 是否打印结果
2293
+ gains_table : bool, default True
2294
+ 是否计算收益表
2295
+ benchmark_dataset : str or pandas.DataFrame, optional
2296
+ 固定分箱边界的基准数据集。若传入str, 则从add_dataset添加的数据集中按名称获取;
2297
+ 若传入DataFrame, 则直接使用该DataFrame。默认None表示各数据集独立分箱。
2298
+
2299
+ Returns
2300
+ -------
2301
+ pandas.DataFrame
2302
+ 性能评估汇总表。若实例的 ``tgt_name`` 为多标签 list/tuple, 则对每个标签分别评估,
2303
+ 结果新增 ``tgt_name`` 列后纵向拼接; ``to_show=True`` 时每个标签各出一张图,
2304
+ ``fig_save_path`` 自动按标签加后缀 (如 ``perf.png`` → ``perf_<label>.png``)。
2305
+ """
2306
+ if len(self.datasets) == 0:
2307
+ return pd.DataFrame([])
2308
+
2309
+ if self.scr_name is None and self.model is None and self.feature_cols is None:
2310
+ return -1
2311
+
2312
+ if self.scr_name is None and self.model is None:
2313
+ return -2
2314
+
2315
+ if self.scr_name is None and self.feature_cols is None:
2316
+ return -3
2317
+
2318
+ active_weight_col = weight_col or self.weight_col
2319
+ has_dataset_weight = any(v is not None for v in self.dataset_weight_cols.values())
2320
+ if (active_weight_col is not None or has_dataset_weight) and oot_grp_name is None and benchmark_dataset is None and not isinstance(self.tgt_name, (list, tuple)):
2321
+ rows = []
2322
+ for name, data in self.datasets.items():
2323
+ if data is None:
2324
+ continue
2325
+ wc = self.dataset_weight_cols.get(name) or active_weight_col
2326
+ work_data = data.copy()
2327
+ scr_name = self.scr_name
2328
+ if scr_name is None:
2329
+ scr_name = "_mdl_scr"
2330
+ work_data[scr_name] = self.model.predict_proba(work_data.loc[:, self.feature_cols])[:, 1]
2331
+ rows.append(
2332
+ _weighted_eval.dataset_summary(
2333
+ name,
2334
+ work_data,
2335
+ self.tgt_name,
2336
+ scr_name,
2337
+ weight_col=wc,
2338
+ nbins=self.pct_bins,
2339
+ )
2340
+ )
2341
+ fnl_df = pd.DataFrame(rows)
2342
+ if display:
2343
+ from IPython.display import display as _ipy_display
2344
+ _ipy_display(fnl_df)
2345
+ if rpt_save_path:
2346
+ fnl_df.to_csv(rpt_save_path, index=False)
2347
+ return fnl_df
2348
+
2349
+ # ── 多 y 标签支持: tgt_name 为 list/tuple 时, 逐标签评估后纵向拼接 ──
2350
+ # 表格: 每个标签结果新增 tgt_name 列后纵向拼接;
2351
+ # 图片: 每个标签各自出图 (to_show=True 时循环显示, fig_save_path 自动按标签加后缀)。
2352
+ if isinstance(self.tgt_name, (list, tuple)):
2353
+ import os as _os
2354
+
2355
+ def _suffix_path(p, lab):
2356
+ if not p:
2357
+ return None
2358
+ root, ext = _os.path.splitext(str(p))
2359
+ return "{0}_{1}{2}".format(root, lab, ext)
2360
+
2361
+ _orig_tgt = self.tgt_name
2362
+ multi_results = []
2363
+ try:
2364
+ for _t in list(self.tgt_name):
2365
+ self.tgt_name = _t
2366
+ sub_df = self.evaluate(
2367
+ oot_grp_name = oot_grp_name,
2368
+ min_data_size = min_data_size,
2369
+ grp_colname = grp_colname,
2370
+ fig_save_path = _suffix_path(fig_save_path, _t),
2371
+ rpt_save_path = None, # 拼接后统一保存 (见下)
2372
+ to_show = to_show, # 每个标签各自出图
2373
+ display = False, # 拼接后统一展示
2374
+ gains_table = gains_table,
2375
+ benchmark_dataset = benchmark_dataset,
2376
+ )
2377
+ if isinstance(sub_df, pd.DataFrame) and sub_df.shape[0] > 0:
2378
+ sub_df.insert(0, "tgt_name", _t)
2379
+ multi_results.append(sub_df)
2380
+ finally:
2381
+ self.tgt_name = _orig_tgt
2382
+
2383
+ fnl_df = pd.concat(multi_results, ignore_index = True) if multi_results else pd.DataFrame([])
2384
+
2385
+ if display:
2386
+ from IPython.display import display as _ipy_display
2387
+ _ipy_display(fnl_df)
2388
+ if rpt_save_path:
2389
+ fnl_df.to_csv(rpt_save_path, index = False)
2390
+ return fnl_df
2391
+
2392
+ def _get_score(data):
2393
+ if self.scr_name is not None:
2394
+ return data[self.scr_name]
2395
+ return self.model.predict_proba(data.loc[:, self.feature_cols])[:, 1]
2396
+
2397
+ def _get_benchmark_bin_edges():
2398
+ if benchmark_dataset is None:
2399
+ return None
2400
+
2401
+ if isinstance(benchmark_dataset, str):
2402
+ if benchmark_dataset not in self.datasets:
2403
+ raise ValueError("benchmark_dataset must be one of added datasets: {0}".format(list(self.datasets.keys())))
2404
+ benchmark_data = self.datasets[benchmark_dataset]
2405
+ else:
2406
+ benchmark_data = benchmark_dataset
2407
+
2408
+ benchmark_score = "_benchmark_score"
2409
+ benchmark_df = pd.DataFrame({
2410
+ self.tgt_name: benchmark_data[self.tgt_name],
2411
+ benchmark_score: _get_score(benchmark_data)
2412
+ })
2413
+
2414
+ _, benchmark_bin_edges = super_binning(
2415
+ data = benchmark_df,
2416
+ score = benchmark_score,
2417
+ dep = self.tgt_name,
2418
+ nbins = self.pct_bins,
2419
+ precision = self.precision,
2420
+ min_bin_prop = self.min_bin_prop,
2421
+ include_missing = self.include_missing,
2422
+ equal_freq = self.equal_freq,
2423
+ chi2_method = self.chi2_method,
2424
+ chi2_p = self.chi2_p,
2425
+ init_equi_bins = self.init_equi_bins,
2426
+ tree_binning = self.tree_binning,
2427
+ random_state = self.random_state,
2428
+ return_edges = True,
2429
+ ascending = True
2430
+ )
2431
+
2432
+ if benchmark_bin_edges is None or len(benchmark_bin_edges) < 2:
2433
+ raise ValueError("Cannot generate benchmark bin edges from benchmark_dataset.")
2434
+
2435
+ return list(benchmark_bin_edges)
2436
+
2437
+ benchmark_bin_edges = _get_benchmark_bin_edges()
2438
+
2439
+ def _evaluate_dataset_dict(dataset_dict, save_path = None):
2440
+ eval_datasets = {}
2441
+ for name, data in dataset_dict.items():
2442
+ if data is None:
2443
+ continue
2444
+ eval_datasets[name] = {
2445
+ "y_true": data[self.tgt_name],
2446
+ "y_score": _get_score(data)
2447
+ }
2448
+
2449
+ if len(eval_datasets) == 0:
2450
+ return pd.DataFrame([])
2451
+
2452
+ model_eval_result_df = evaluate_performance(
2453
+ datasets = eval_datasets,
2454
+ dist_bins = self.dist_bins,
2455
+ pct_bins = self.pct_bins,
2456
+ square_figsize = 5,
2457
+ to_show = to_show,
2458
+ save_path = save_path,
2459
+ gains_table = gains_table,
2460
+ equal_freq = self.equal_freq,
2461
+ pct_bin_edges = benchmark_bin_edges
2462
+ )
2463
+
2464
+ import re
2465
+ btm_cols = [x for x in model_eval_result_df.columns if x.startswith("Btm")]
2466
+ top_cols = [x for x in model_eval_result_df.columns if x.startswith("Top")]
2467
+
2468
+ if btm_cols and top_cols:
2469
+ btm_str = btm_cols[0]
2470
+ top_str = top_cols[0]
2471
+ numbers = re.findall(r'\d+', btm_str)
2472
+ if numbers:
2473
+ quantile = int(numbers[0])
2474
+ else:
2475
+ quantile = self.pct_bins
2476
+
2477
+ model_eval_result_df[f"Btm{quantile}%_Lift"] = model_eval_result_df[btm_str] / model_eval_result_df["avgTrue"]
2478
+ model_eval_result_df[f"Top{quantile}%_Lift"] = model_eval_result_df[top_str] / model_eval_result_df["avgTrue"]
2479
+ model_eval_result_df["AUC_Shift"] = model_eval_result_df["AUC"].shift(1) / model_eval_result_df["AUC"] - 1
2480
+ model_eval_result_df["KS_Shift"] = model_eval_result_df["KS"].shift(1) / model_eval_result_df["KS"] - 1
2481
+
2482
+ gains_table_cols = ['N_BUMP', 'MIN_RISK_DEP', 'MAX_RISK_DEP', 'KS_IN_GAINS', 'LIFT_IN_GAINS', 'IV', 'N_BINS']
2483
+ gains_summ_list = []
2484
+ for name, data in dataset_dict.items():
2485
+ if data is None:
2486
+ gains_res = pd.DataFrame([], columns = gains_table_cols)
2487
+ else:
2488
+ gains_res = get_gains_table(
2489
+ data = data,
2490
+ dep = self.tgt_name,
2491
+ nbins = benchmark_bin_edges if benchmark_bin_edges is not None else self.pct_bins,
2492
+ precision = self.precision,
2493
+ min_bin_prop = self.min_bin_prop,
2494
+ include_missing = self.include_missing,
2495
+ score = self.scr_name,
2496
+ equal_freq = self.equal_freq,
2497
+ chi2_method = False if benchmark_bin_edges is not None else self.chi2_method,
2498
+ model = self.model,
2499
+ varlist = self.feature_cols,
2500
+ init_equi_bins = self.init_equi_bins,
2501
+ chi2_p = self.chi2_p,
2502
+ retSummary = True,
2503
+ tree_binning = False if benchmark_bin_edges is not None else self.tree_binning,
2504
+ random_state = self.random_state
2505
+ )
2506
+ gains_res['index'] = name
2507
+ gains_summ_list.append(gains_res)
2508
+
2509
+ if gains_summ_list:
2510
+ gains_summ = pd.concat(gains_summ_list)
2511
+ model_eval_result_df = model_eval_result_df.merge(gains_summ, on = ['index'], how = 'left')
2512
+
2513
+ return model_eval_result_df
2514
+
2515
+ if oot_grp_name is None:
2516
+ fnl_df = _evaluate_dataset_dict(self.datasets, save_path = fig_save_path)
2517
+ else:
2518
+ if grp_colname is None:
2519
+ grp_colname = oot_grp_name
2520
+
2521
+ grouped_results = []
2522
+ for dataset_name, data in self.datasets.items():
2523
+ if data is None or oot_grp_name not in data.columns:
2524
+ continue
2525
+
2526
+ grp_list = data[oot_grp_name].sort_values(ascending = True).unique().tolist()
2527
+ valid_grp_list = [x for x in grp_list if data[data[oot_grp_name].isin([x])].shape[0] >= min_data_size]
2528
+
2529
+ for grp in valid_grp_list:
2530
+ data_grp = data[data[oot_grp_name].isin([grp])]
2531
+ perf_res = _evaluate_dataset_dict({dataset_name: data_grp}, save_path = None)
2532
+ if perf_res.shape[0] > 0:
2533
+ perf_res[grp_colname] = grp
2534
+ grouped_results.append(perf_res)
2535
+
2536
+ if len(grouped_results) == 0:
2537
+ fnl_df = pd.DataFrame([])
2538
+ else:
2539
+ fnl_df = pd.concat(grouped_results, ignore_index = True)
2540
+
2541
+ if display:
2542
+ from IPython.display import display
2543
+ display(fnl_df)
2544
+
2545
+ if rpt_save_path:
2546
+ fnl_df.to_csv(rpt_save_path, index = False)
2547
+
2548
+ return fnl_df