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,2008 @@
1
+ # coding: utf-8
2
+ import os
3
+ from tracemalloc import start
4
+ import numpy as np
5
+ import pandas as pd
6
+ from pandas.core.groupby.generic import NamedAgg
7
+ from sklearn.metrics import roc_curve, precision_recall_curve, auc
8
+ from matplotlib.font_manager import FontProperties, findfont
9
+ import seaborn as sns
10
+ import matplotlib.pyplot as plt
11
+ from sklearn.utils.extmath import density
12
+ import time
13
+ from functools import wraps
14
+ from . import weighted_eval_utils as _weighted_eval
15
+
16
+ # zhfont = FontProperties(fname=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ref_font/KaiTi.ttf'))
17
+
18
+ __all__=[
19
+ 'calc_pr', # PR: Precision-Recall
20
+ 'summarize_pr', # PR: P-R BEP
21
+ 'plot_pr_curve', # PR
22
+ 'calc_roc', # ROC: TPR-FPR
23
+ # 'summary_roc', # ROC: AUC、KS
24
+ 'plot_ks_curve', # ROC: KS-Curve
25
+ 'plot_roc_curve', # ROC: ROC-Curve
26
+ 'calc_equid_dist', # Dist: Stats
27
+ 'plot_kde_curve', # Dist: KDE
28
+ 'plot_dist_curve', # Dist: Count-avgTrue twin
29
+ 'calc_equid_pct', # PCT: Stats
30
+ 'summarize_pct', # PCT: Top、BTM
31
+ 'plot_pct_curve', # PCT: avgTrue
32
+
33
+ 'evaluate_performance', # Single(ROC、KDE、PCT、Gain)
34
+ 'evaluate_distribution', # Single+Group(DIST、CumDist)
35
+ 'comparison_performance', # Multiple(ROC、PCT、CumPCT、Gain)
36
+ 'calc_lift_apt',
37
+ ]
38
+
39
+ palette = {
40
+ 'ClassicBlueRedGrey': [
41
+ '#0099CC', # 蓝
42
+ '#FF6666', # 红
43
+ '#CCCCCC', # 灰
44
+ ],
45
+ 'ClassicGreyRed': [
46
+ '#333333', # 深灰
47
+ '#CC0033', # 深红
48
+ ],
49
+ 'Colors': [
50
+ '#0099CC', # 蓝
51
+ '#FF6666', # 红
52
+ '#99CC99', # 绿
53
+ '#FF9966', # 橙 FF9933
54
+ ],
55
+ 'MorandiDark': [
56
+ '#965454', # 红棕
57
+ '#656565', # 墨绿
58
+ '#6b5152', # 深棕
59
+ ],
60
+ }
61
+
62
+ fontdicts = {
63
+ 'sub': {
64
+ 'suptitle': {
65
+ 'size': 16,
66
+ 'weight': 'bold',
67
+ },
68
+ 'subtitle': {
69
+ 'size': 14,
70
+ },
71
+ 'axislabel': {
72
+ 'size': 12,
73
+ },
74
+ 'legend': {
75
+ 'size': 10,
76
+ },
77
+ },
78
+ 'main': {
79
+ 'suptitle': {
80
+ 'size': 20,
81
+ 'weight': 'bold',
82
+ },
83
+ 'subtitle': {
84
+ 'size': 17,
85
+ },
86
+ 'axislabel': {
87
+ 'size': 14,
88
+ },
89
+ 'legend': {
90
+ 'size': 14,
91
+ },
92
+ },
93
+ }
94
+
95
+ def timeit_decorator(func):
96
+ @wraps(func)
97
+ def wrapper(*args, **kwargs):
98
+ start_time = time.time()
99
+ result = func(*args, **kwargs)
100
+ end_time = time.time()
101
+ elapsed_time = end_time - start_time
102
+ # print(f"Function '{func.__name__}' took {elapsed_time:.6f} seconds") #需要每个函数耗时把这个打开
103
+ return result
104
+ return wrapper
105
+
106
+ # P-R Curve
107
+ @timeit_decorator
108
+ def calc_pr(y_true, y_score, sample_weight=None):
109
+ """计算P-R曲线相关统计量.
110
+ 基于sklearn.metrics.precision_recall_curve
111
+
112
+ Parameters
113
+ ----------
114
+ y_true: array like
115
+ 实际样本标签序列, 只接受0-1
116
+ y_score: array like
117
+ 预测概率值序列
118
+
119
+ Returns
120
+ -------
121
+ pr_df: pandas.DataFrame
122
+ PR相关的Precision、Recall、Thresholds等统计量数据集
123
+ """
124
+ if sample_weight is not None:
125
+ return _weighted_eval.calc_pr(y_true, y_score, sample_weight=sample_weight)
126
+
127
+ pr_df = pd.DataFrame(precision_recall_curve(y_true, y_score)).T
128
+ pr_df.columns = ['precision', 'recall', 'thresholds']
129
+ pr_df['thresholds_percentile'] = [100 * np.mean(y_score <= x) for x in pr_df['thresholds']]
130
+
131
+ return pr_df
132
+
133
+ @timeit_decorator
134
+ def summarize_pr(pr_df):
135
+ """统计P-R曲线信息.
136
+ 统计量如下:
137
+ 1.平衡点(Break-Even Point, 简称BEP)阈值及对应Precision、Recall等统计量
138
+
139
+ Parameters
140
+ ----------
141
+ pr_df: pandas.DataFrame
142
+ PR相关的Precision、Recall、Thresholds等统计量数据集
143
+
144
+ Returns
145
+ -------
146
+ pr_info: dict
147
+ P-R曲线统计信息字典
148
+ """
149
+ equalind = np.argmin(abs(pr_df['precision'] - pr_df['recall']))
150
+ pr_info = {
151
+ 'bep_index': equalind,
152
+ 'bep_threshold': pr_df['thresholds'][equalind],
153
+ 'bep_precision': pr_df['precision'][equalind],
154
+ 'bep_recall': pr_df['recall'][equalind],
155
+ }
156
+
157
+ return pr_info
158
+
159
+ @timeit_decorator
160
+ def plot_pr_curve(pr_dfs, square_figsize=8, to_show=True, save_path=None):
161
+ """绘制P-R曲线图.
162
+
163
+ Parameters
164
+ ----------
165
+ pr_dfs: Dict
166
+ 单个或多个Score名下的PR数据集. 键值对格式为: {name: pr_df}
167
+ square_figsize: float
168
+ 正方形图边英寸. 默认值为8
169
+ to_show: bool
170
+ 是否展示图片. 默认为True
171
+ save_path: str
172
+ 结果图片存放文件地址. 默认值为None, 即不保存
173
+ """
174
+ plt.figure(figsize=(square_figsize, square_figsize))
175
+ plt.suptitle('P-R Curve', fontsize=20, fontweight='bold') #, findfont=zhfont)
176
+ ax = plt.subplot(1,1,1)
177
+
178
+ models = list(pr_dfs.keys())
179
+ if len(models) == 1:
180
+ pr_df = pr_dfs[models[0]]
181
+ __plot_single_pr_axes(pr_df, ax)
182
+ else:
183
+ __plot_multi_pr_axes(pr_dfs, ax)
184
+ if to_show:
185
+ plt.show()
186
+ if bool(save_path):
187
+ plt.savefig(save_path, bbox_inches='tight')
188
+ plt.close()
189
+
190
+
191
+ def __plot_single_pr_axes(pr_df, ax):
192
+ """在axes上绘制单个P-R曲线图.
193
+
194
+ Parameters
195
+ ----------
196
+ pr_df: pandas.DataFrame
197
+ PR相关的Precision、Recall、Thresholds等统计量数据集
198
+ ax: matplotlib.pyplot.plt.axes
199
+ 绘图axes
200
+ """
201
+ ax.set_xlim([0,1])
202
+ ax.set_ylim([0,1])
203
+ ax.set_xlabel('Recall', fontsize=14)
204
+ ax.set_ylabel('Precision', fontsize=14)
205
+ ax.plot([0,1], [0,1], color=palette['ClassicBlueRedGrey'][0], linestyle='--', linewidth=1)
206
+
207
+ ax.plot(pr_df['recall'], pr_df['precision'], color=palette['ClassicBlueRedGrey'][0], label='P-R', linewidth=2)
208
+ pr_info = summarize_pr(pr_df)
209
+
210
+ bep_x, bep_y = pr_info['bep_recall'], pr_info['bep_precision']
211
+ ax.plot([[bep_x]], [[bep_y]], marker='.', markersize=20, color=palette['ClassicBlueRedGrey'][1], label='BEP')
212
+ ax.plot([bep_y, bep_x], [0, bep_y], linestyle='--', color=palette['ClassicBlueRedGrey'][1])
213
+ ax.plot([0, bep_y], [bep_y, bep_y], linestyle='--', color=palette['ClassicBlueRedGrey'][1])
214
+
215
+ ax.set_title('BEP: Threshold={0:.3f} Precison={1:.2%}'.format(pr_info['bep_threshold'], bep_y), fontsize=15)
216
+ ax.legend(loc=1, fontsize=12)
217
+
218
+
219
+ def __plot_multi_pr_axes(pr_dfs, ax):
220
+ """在axes上绘制多个P-R曲线图.
221
+
222
+ Parameters
223
+ ----------
224
+ pr_dfs: Dict
225
+ 单个或多个Score名下的PR数据集. 键值对格式为: {name: pr_df}
226
+ ax: matplotlib.pyplot.plt.axes
227
+ 绘图axes
228
+ """
229
+ ax.set_xlim([0,1])
230
+ ax.set_ylim([0,1])
231
+ ax.set_xlabel('Recall', fontsize=14)
232
+ ax.set_ylabel('Precision', fontsize=14)
233
+ ax.plot([0,1], [0,1], color=palette['ClassicBlueRedGrey'][0], linestyle='--', linewidth=1)
234
+
235
+ models = list(pr_dfs.keys())
236
+ for i in range(len(models)):
237
+ md = models[i]
238
+ pr_df = pr_dfs[md]
239
+ pr_info = summarize_pr(pr_df)
240
+ bep_x, bep_y = pr_info['bep_recall'], pr_info['bep_precision']
241
+ color = palette['MorandiDark'][i]
242
+ label = '{0} (BEP P={1:.2%})'.format(md, bep_y)
243
+ ax.plot(pr_df['recall'], pr_df['precision'], color=color, linewidth=2)
244
+ ax.plot([[bep_x]], [[bep_y]], marker='.', markersize=15, color=color, label=label)
245
+ ax.plot([bep_y, bep_x], [0, bep_y], linestyle='--', color=color)
246
+ ax.plot([0, bep_y], [bep_y, bep_y], linestyle='--', color=color)
247
+ # ax.set_title('BEP: Threshold={0:.3f} Precison={1:.2%}'.format(pr_info['bep_threshold'], bep_y), fontsize=15)
248
+ ax.legend(loc=1, fontsize=12)
249
+
250
+
251
+ # ROC Curve
252
+ @timeit_decorator
253
+ def calc_roc(y_true, y_score, sample_weight=None):
254
+ """计算ROC曲线相关统计量.
255
+ 基于sklearn.metrics.roc_curve
256
+
257
+ Parameters
258
+ ----------
259
+ y_true: array like
260
+ 实际样本标签序列, 只接受0-1
261
+ y_score: array like
262
+ 预测概率值序列
263
+
264
+ Returns
265
+ -------
266
+ roc_df: pandas.DataFrame
267
+ ROC相关的TPR、FPR、Thresholds等统计量数据集
268
+ """
269
+
270
+ if sample_weight is not None:
271
+ return _weighted_eval.calc_roc(y_true, y_score, sample_weight=sample_weight)
272
+
273
+ # 移除无效值
274
+ mask = np.isfinite(y_score) & np.isfinite(y_true)
275
+ y_true_clean = np.array(y_true)[mask]
276
+ y_score_clean = np.array(y_score)[mask]
277
+
278
+ if len(y_true_clean) == 0:
279
+ # 返回一个空的 ROC DataFrame
280
+ return pd.DataFrame(columns=['fpr', 'tpr', 'thresholds', 'thresholds_percentile'])
281
+
282
+ roc_df = pd.DataFrame(roc_curve(y_true_clean, y_score_clean)).T
283
+ roc_df.columns = ['fpr', 'tpr', 'thresholds']
284
+ roc_df['thresholds_percentile'] = [100 * np.mean(y_score_clean <= x) for x in roc_df['thresholds']]
285
+
286
+ return roc_df
287
+
288
+ # def calc_roc(y_true, y_score):
289
+ # """计算ROC曲线相关统计量.
290
+ # 基于sklearn.metrics.roc_curve
291
+
292
+ # Parameters
293
+ # ----------
294
+ # y_true: array like
295
+ # 实际样本标签序列, 只接受0-1
296
+ # y_score: array like
297
+ # 预测概率值序列
298
+
299
+ # Returns
300
+ # -------
301
+ # roc_df: pandas.DataFrame
302
+ # ROC相关的TPR、FPR、Thresholds等统计量数据集
303
+ # """
304
+ # y_true = np.array(y_true)
305
+ # y_score = np.array(y_score)
306
+ # roc_df = pd.DataFrame(roc_curve(y_true, y_score)).T
307
+ # roc_df.columns = ['fpr', 'tpr', 'thresholds']
308
+ # roc_df['thresholds_percentile'] = [100 * np.mean(y_score <= x) for x in roc_df['thresholds']]
309
+
310
+ # return roc_df
311
+
312
+ @timeit_decorator
313
+ def summarize_roc(roc_df):
314
+ """统计ROC曲线信息.
315
+ 统计量如下:
316
+ 1. AUC
317
+ 2. KS及其对应阈值
318
+
319
+ Parameters
320
+ ----------
321
+ roc_df: pandas.DataFrame
322
+ ROC相关的TPR、FPR、Thresholds等统计量数据集
323
+
324
+ Returns
325
+ -------
326
+ roc_info: dict
327
+ ROC曲线统计信息字典
328
+ """
329
+
330
+ if roc_df.empty:
331
+ return {'auc': np.nan, 'ks_index': np.nan, 'ks_threshold': np.nan, 'ks': np.nan}
332
+
333
+ f = roc_df['tpr'] - roc_df['fpr']
334
+ roc_info = {
335
+ 'auc': auc(roc_df['fpr'], roc_df['tpr']),
336
+ 'ks_index': np.argmax(f),
337
+ 'ks_threshold': roc_df['thresholds'][np.argmax(f)],
338
+ 'ks': max(abs(f)),
339
+ }
340
+
341
+ return roc_info
342
+
343
+ @timeit_decorator
344
+ def plot_ks_curve(roc_df, square_figsize=8, to_show=True, save_path=None):
345
+ """绘制KS曲线图.
346
+ 只能绘制单个Score的KS曲线.
347
+
348
+ Parameters
349
+ ----------
350
+ roc_df: pandas.DataFrame
351
+ ROC相关的TPR、FPR、Thresholds等统计量数据集
352
+ square_figsize: float
353
+ 正方形图边英寸. 默认值为8
354
+ to_show: bool
355
+ 是否展示图片. 默认为True
356
+ save_path: str
357
+ 结果图片存放文件地址. 默认值为None, 即不保存
358
+ """
359
+ plt.figure(figsize=(square_figsize, square_figsize))
360
+ plt.suptitle('KS Curve', fontsize=20, fontweight='bold') #, findfont=zhfont)
361
+ ax = plt.subplot(1,1,1)
362
+ __plot_ks_axes(roc_df, ax)
363
+ if to_show:
364
+ plt.show()
365
+ if bool(save_path):
366
+ plt.savefig(save_path, bbox_inches='tight')
367
+ plt.close()
368
+
369
+
370
+ def __plot_ks_axes(roc_df, ax):
371
+ """在axes上绘制单个KS曲线图.
372
+
373
+ Parameters
374
+ ----------
375
+ roc_df: pandas.DataFrame
376
+ ROC相关的TPR、FPR、Thresholds等统计量数据集
377
+ roc_summary: dict
378
+ ROC相关的AUC、KS、TargetRate等统计量字典
379
+ ax: matplotlib.pyplot.plt.axes
380
+ 绘图axes
381
+ """
382
+ ax.set_xlim([0,100])
383
+ ax.set_ylim([0,1])
384
+ ax.set_xlabel('Percentile %', fontsize=14)
385
+ ax.set_ylabel('Rate', fontsize=14)
386
+ X = roc_df['thresholds_percentile']
387
+ ax.plot(X, roc_df['fpr'], color=palette['ClassicBlueRedGrey'][0], label='False Positive Rate', linewidth=2)
388
+ ax.plot(X, roc_df['tpr'], color=palette['ClassicBlueRedGrey'][1], label='True Positive Rate', linewidth=2)
389
+
390
+ roc_info = summarize_roc(roc_df)
391
+ ks_vector = [
392
+ [X[roc_info['ks_index']], X[roc_info['ks_index']]],
393
+ [roc_df['fpr'][roc_info['ks_index']], roc_df['tpr'][roc_info['ks_index']]],
394
+ ]
395
+ ax.plot(ks_vector[0], ks_vector[1], linewidth=4, color=palette['ClassicBlueRedGrey'][2], label='KS')
396
+ ax.set_title('Threshold={0:.3f} KS={1:.3f}'.format(roc_info['ks_threshold'], roc_info['ks']), fontsize=15)
397
+ ax.legend(loc=1, fontsize=12)
398
+
399
+ @timeit_decorator
400
+ def plot_roc_curve(roc_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
401
+ """绘制ROC曲线图.
402
+ 可绘制单个或多个Score的曲线.
403
+
404
+ Parameters
405
+ ----------
406
+ roc_dfs: dict
407
+ 单个或多个Score名下的ROC相关统计量字典
408
+ 键值对格式为: {name: roc_df}
409
+ square_figsize: float
410
+ 正方形图边英寸. 默认值为8
411
+ to_show: bool
412
+ 是否展示图片. 默认为True
413
+ save_path: str
414
+ 结果图片存放文件地址. 默认值为None, 即不保存
415
+ """
416
+ plt.figure(figsize=(square_figsize, square_figsize))
417
+ plt.suptitle('ROC Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight'])
418
+ ax = plt.subplot(1,1,1)
419
+ models = list(roc_dfs.keys())
420
+ if len(models) == 1:
421
+ roc_df = roc_dfs[models[0]]
422
+ __plot_single_roc_axes(roc_df, ax, fontdicts)
423
+ else:
424
+ __plot_multi_roc_axes(roc_dfs, ax, fontdicts)
425
+ if to_show:
426
+ plt.show()
427
+ if bool(save_path):
428
+ plt.savefig(save_path, bbox_inches='tight')
429
+ plt.close()
430
+
431
+
432
+ def __plot_roc_axes_base(ax, fontdicts):
433
+ """在axes上绘制roc图基础元素.
434
+
435
+ Parameters
436
+ ----------
437
+ ax: matplotlib.pyplot.plt.axes
438
+ 绘图axes
439
+ fontdicts: dict
440
+ 绘图相关字体字典
441
+ """
442
+ ax.plot([0,1], [0,1], color='k', linestyle='--', linewidth=1)
443
+ ax.set_xlim([0,1])
444
+ ax.set_ylim([0,1])
445
+ ax.set_xlabel('False Positive Rate', fontdict=fontdicts['axislabel'])
446
+ ax.set_ylabel('True Positive Rate', fontdict=fontdicts['axislabel'])
447
+
448
+
449
+ def __plot_single_roc_axes(roc_df, ax, fontdicts):
450
+ """在axes上绘制单个ROC曲线图.
451
+
452
+ Parameters
453
+ ----------
454
+ roc_df: pandas.DataFrame
455
+ ROC相关的TPR、FPR、Thresholds等统计量数据集
456
+ ax: matplotlib.pyplot.plt.axes
457
+ 绘图axes
458
+ fontdicts: dict
459
+ 绘图相关字体字典
460
+ """
461
+ __plot_roc_axes_base(ax, fontdicts)
462
+
463
+ if roc_df.empty:
464
+ ax.set_title('KS=NaN AUC=NaN', fontdict=fontdicts['subtitle'])
465
+ return
466
+
467
+ ax.plot(roc_df['fpr'], roc_df['tpr'], color=palette['ClassicBlueRedGrey'][0], linewidth=2, label='ROC')
468
+
469
+ roc_info = summarize_roc(roc_df)
470
+ if roc_info['ks_index'] >= 0: # 有效索引
471
+ ks_vector = [
472
+ [roc_df['fpr'][roc_info['ks_index']], roc_df['fpr'][roc_info['ks_index']]],
473
+ [roc_df['fpr'][roc_info['ks_index']], roc_df['tpr'][roc_info['ks_index']]]
474
+ ]
475
+ ax.plot(ks_vector[0], ks_vector[1], linewidth=4, color='r', label='KS')
476
+ ax.set_title('KS={0:.3f} AUC={1:.3f}'.format(roc_info['ks'], roc_info['auc']), fontdict=fontdicts['subtitle'])
477
+
478
+
479
+ def __plot_multi_roc_axes(roc_dfs, ax, fontdicts):
480
+ """在axes上绘制多个ROC曲线图.
481
+
482
+ Parameters
483
+ ----------
484
+ roc_dfs: dict
485
+ 单个或多个Score名下的ROC相关统计量字典. 键值对格式为: {name: roc_df}
486
+ ax: matplotlib.pyplot.plt.axes
487
+ 绘图axes
488
+ """
489
+ __plot_roc_axes_base(ax, fontdicts)
490
+
491
+ models = list(roc_dfs.keys())
492
+ for i in range(len(models)):
493
+ md = models[i]
494
+ roc_df = roc_dfs[md]
495
+ roc_info = summarize_roc(roc_df)
496
+ label='{0} (KS={1:.3f} AUC={2:.3f})'.format(md, roc_info['ks'], roc_info['auc'])
497
+ color=palette['MorandiDark'][i]
498
+ ax.plot(roc_df['fpr'], roc_df['tpr'], linewidth=2, color=color, label=label)
499
+ ax.legend(loc=4, fontsize=fontdicts['legend']['size'])
500
+
501
+
502
+ # Kde Curve
503
+ @timeit_decorator
504
+ def plot_kde_curve(y_true, y_score_dict, bins=20, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
505
+ """绘制Score核密度估计(kernel density estimate, 简称KDE)曲线.
506
+
507
+ Parameters
508
+ ----------
509
+ y_true: array like
510
+ 实际样本标签序列, 只接受0-1
511
+ y_score_dict: dict
512
+ 单个或多个Score序列字典. 键值对格式为: {name: Score}
513
+ bins: int
514
+ 分组数
515
+ square_figsize: float
516
+ 正方形图边英寸. 默认值为8
517
+ fontdicts: dict
518
+ 绘图相关字体字典
519
+ to_show: bool
520
+ 是否展示图片. 默认为True
521
+ save_path: str
522
+ 结果图片存放文件地址. 默认值为None, 即不保存
523
+ """
524
+ y_true = np.array(y_true)
525
+ plt.figure(figsize=(square_figsize, square_figsize))
526
+ plt.suptitle('Score KDE Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
527
+ ax = plt.subplot(1,1,1)
528
+ models = list(y_score_dict.keys())
529
+ if len(models) == 1:
530
+ y_score = y_score_dict[models[0]]
531
+ y_score = np.array(y_score)
532
+ __plot_single_kde_axes(y_true, y_score, bins, ax, fontdicts)
533
+ else:
534
+ __plot_multi_kde_axes(y_true, y_score_dict, bins, ax, fontdicts)
535
+ if to_show:
536
+ plt.show()
537
+ if bool(save_path):
538
+ plt.savefig(save_path, bbox_inches='tight')
539
+ plt.close()
540
+
541
+
542
+ def __plot_kde_axes_base(ax, fontdicts):
543
+ """在axes上绘制kde图基础元素.
544
+
545
+ Parameters
546
+ ----------
547
+ ax: matplotlib.pyplot.plt.axes
548
+ 绘图axes
549
+ fontdicts: dict
550
+ 绘图相关字体字典
551
+ """
552
+ ax.set_xlim([0,1])
553
+ ax.set_xlabel('Score', fontdict=fontdicts['axislabel'])
554
+ ax.set_ylabel('Density', fontdict=fontdicts['axislabel'])
555
+
556
+
557
+ def __plot_single_kde_axes(y_true, y_score, bins, ax, fontdicts):
558
+ """在axes上绘制单个kde图.
559
+
560
+ Parameters
561
+ ----------
562
+ y_true: numpy.array
563
+ 实际样本标签序列, 只接受0-1
564
+ y_score: numpy.array
565
+ 预测概率值序列
566
+ bins: int
567
+ 分组数
568
+ ax: matplotlib.pyplot.plt.axes
569
+ 绘图axes
570
+ fontdicts: dict
571
+ 绘图相关字体字典
572
+ """
573
+ __plot_kde_axes_base(ax, fontdicts)
574
+
575
+ sns.distplot(y_score, bins=bins, hist=True, kde=False, ax=ax,
576
+ hist_kws={'density': True, 'rwidth': 0.95, 'color': palette['ClassicBlueRedGrey'][2], 'alpha': 1, 'label': 'Total'}, )
577
+ sns.distplot(y_score[np.where(y_true==0)], bins=bins, hist=False, kde=True,
578
+ kde_kws={'bw': 1/bins/2, 'color': palette['ClassicBlueRedGrey'][0], 'label': 'Neg KDE'}, )
579
+ sns.distplot(y_score[np.where(y_true==1)], hist=False, kde=True,
580
+ kde_kws={'bw': 1/bins/2, 'color': palette['ClassicBlueRedGrey'][1], 'label': 'Pos KDE'}, )
581
+ ax.axvline(x=np.mean(y_true), linestyle='-', linewidth=1, color=palette['ClassicGreyRed'][0],label='True')
582
+ ax.axvline(x=np.mean(y_score), linestyle='--', linewidth=1, color=palette['ClassicGreyRed'][1], label='Score')
583
+
584
+ ax.set_title('N={0:,} True={1:.2%} Score={2:.2%}'.format(len(y_true), np.mean(y_true), np.mean(y_score)), fontdict=fontdicts['subtitle'])
585
+ ax.legend(loc=1, fontsize=fontdicts['legend']['size'])
586
+
587
+
588
+ def __plot_multi_kde_axes(y_true, y_score_dict, bins, ax, fontdicts):
589
+ """在axes上绘制多个kde图.
590
+
591
+ Parameters
592
+ ----------
593
+ y_true: array like
594
+ 实际样本标签序列, 只接受0-1
595
+ y_score_dict: dict
596
+ 单个或多个Score序列字典. 键值对格式为: {name: Score}
597
+ ax: matplotlib.pyplot.plt.axes
598
+ 绘图axes
599
+ fontdicts: dict
600
+ 绘图相关字体字典
601
+ """
602
+ __plot_kde_axes_base(ax, fontdicts)
603
+
604
+ models = list(y_score_dict.keys())
605
+ for i in range(len(models)):
606
+ md = models[i]
607
+ y_score = np.array(y_score_dict[md])
608
+ sns.distplot(y_score, bins=bins, hist=False, kde=True, ax=ax,
609
+ kde_kws={'bw': 1/bins/2, 'color': palette['MorandiDark'][i], 'label': '{0} (Score={1:.2%})'.format(md, np.mean(y_score))}, )
610
+ ax.axvline(x=np.mean(y_score), linestyle='--', linewidth=1, color=palette['MorandiDark'][i])
611
+ ax.axvline(x=np.mean(y_true), linestyle='-', linewidth=1, color=palette['ClassicGreyRed'][0], label='True')
612
+
613
+ ax.set_title(
614
+ '{0} (N={1:,} True={2:.2%})'.format(' vs. '.join(models), len(y_true), np.mean(y_true)),
615
+ fontdict=fontdicts['subtitle'],
616
+ )
617
+ ax.legend(loc=1, fontsize=fontdicts['legend']['size'])
618
+
619
+
620
+ # Agg
621
+ def __agg(df):
622
+ """计算各组统计量.
623
+
624
+ Parameters
625
+ ----------
626
+ df: pandas.DataFrame
627
+ 包含y_true, y_score, thresholds数据集
628
+
629
+ Returns
630
+ -------
631
+ df_agg: pandas.DataFrame
632
+ 等距分组后各组统计量数据集
633
+ """
634
+
635
+ N = len(df['y_true'])
636
+ N1 = sum(df['y_true'])
637
+ if 'y_group' in df.columns:
638
+ group_cols = ['y_group', 'thresholds']
639
+ else:
640
+ group_cols = ['thresholds']
641
+ df_agg = df.groupby(group_cols).agg(
642
+ min_score=pd.NamedAgg(column='y_score', aggfunc='min'),
643
+ max_score=pd.NamedAgg(column='y_score', aggfunc='max'),
644
+ n=pd.NamedAgg(column='y_true', aggfunc='count'),
645
+ sum_true=pd.NamedAgg(column='y_true', aggfunc='sum'),
646
+ avg_true=pd.NamedAgg(column='y_true', aggfunc='mean'),
647
+ avg_score=pd.NamedAgg(column='y_score', aggfunc='mean'),
648
+ sum_score=pd.NamedAgg(column='y_score', aggfunc='sum'),
649
+ ).reset_index()
650
+ df_agg[['n', 'sum_true', 'sum_score']] = df_agg[['n', 'sum_true', 'sum_score']].fillna(0)
651
+ df_agg.loc[:, 'proportion'] = [x / N for x in df_agg['n']]
652
+ df_agg.loc[:, 'capture_rate'] = [x / N1 for x in df_agg['sum_true']]
653
+
654
+ if 'y_group' in df_agg.columns:
655
+ gs = list(set(df_agg['y_group']))
656
+ for g in gs:
657
+ g_idx = (df_agg['y_group'] == g)
658
+ df_agg.loc[g_idx, 'cumsum_n'] = np.cumsum(df_agg.loc[g_idx, 'n'])
659
+ df_agg.loc[g_idx, 'cumsum_proportion'] = np.cumsum(df_agg.loc[g_idx, 'proportion'])
660
+ df_agg.loc[g_idx, 'cumsum_true'] = np.cumsum(df_agg.loc[g_idx, 'sum_true'])
661
+ df_agg.loc[g_idx, 'cumsum_score'] = np.cumsum(df_agg.loc[g_idx, 'sum_score'])
662
+ else:
663
+ df_agg.loc[:, 'cumsum_n'] = np.cumsum(df_agg['n'])
664
+ df_agg.loc[:, 'cumsum_proportion'] = np.cumsum(df_agg['proportion'])
665
+ df_agg.loc[:, 'cumsum_true'] = np.cumsum(df_agg['sum_true'])
666
+ df_agg.loc[:, 'cumsum_score'] = np.cumsum(df_agg['sum_score'])
667
+
668
+ df_agg.loc[:, 'cumavg_true'] = [x / y if y>0 else np.nan for x,y in zip(df_agg['cumsum_true'], df_agg['cumsum_n'])]
669
+ df_agg.loc[:, 'cumavg_score'] = [x / y if y>0 else np.nan for x,y in zip(df_agg['cumsum_score'], df_agg['cumsum_n'])]
670
+
671
+ columns = group_cols + ['min_score', 'max_score', 'n', 'proportion', 'sum_true', 'sum_score', 'avg_true', 'avg_score', 'capture_rate',
672
+ 'cumsum_n', 'cumsum_proportion', 'cumsum_true', 'cumsum_score', 'cumavg_true', 'cumavg_score', ]
673
+
674
+ return df_agg[columns]
675
+
676
+
677
+ def __calc_digit_max(value):
678
+ """计算数值所在位数的上界限.
679
+ 1.若为正数, 且恰好为10的倍数则为本身, 否则进1位的最小值
680
+ 2.若非正数, 则为0
681
+
682
+ Parameters
683
+ ----------
684
+ value: numerical
685
+ 数值
686
+
687
+ Returns
688
+ -------
689
+ rst: int
690
+ 上界值
691
+ """
692
+ if value > 0:
693
+ rst = 10 ** np.ceil(np.log10(value))
694
+ else:
695
+ rst = 0
696
+
697
+ return rst
698
+
699
+
700
+ def __calc_digit_min(value):
701
+ """计算数值所在位数的下界限.
702
+ 1.若为负数, 且恰好为10的倍数则为本身, 否则为当前位数的最小值
703
+ 2.若非负数, 则为0
704
+
705
+ Parameters
706
+ ----------
707
+ value: numerical
708
+ 数值
709
+
710
+ Returns
711
+ -------
712
+ rst: int
713
+ 下界值
714
+ """
715
+ if value >= 0:
716
+ rst = 0
717
+ else:
718
+ rst = -10 ** np.ceil(np.log10(abs(value)))
719
+
720
+ return rst
721
+
722
+ @timeit_decorator
723
+ def calc_equid_dist(y_true, y_score, y_group=None, bins=10, sample_weight=None):
724
+ """将Score等距分组, 计算各组统计量.
725
+
726
+ Parameters
727
+ ----------
728
+ y_true: array like
729
+ 实际样本标签序列, 只接受0-1
730
+ y_score: array like
731
+ 预测概率值序列
732
+ y_group: array like
733
+ 数据组别序列. 默认为None, 即无组别
734
+ bins: int
735
+ 分组数
736
+
737
+ Returns
738
+ -------
739
+ dist_df: pandas.DataFrame
740
+ 等距分组后各组统计量数据集
741
+ """
742
+ if sample_weight is not None:
743
+ return _weighted_eval.calc_equid_dist(y_true, y_score, bins=bins, sample_weight=sample_weight)
744
+
745
+ y_true = np.array(y_true)
746
+ y_score = np.array(y_score)
747
+
748
+ min_score = __calc_digit_min(np.min(y_score))
749
+ max_score = __calc_digit_max(np.max(y_score))
750
+ step = (max_score - min_score) / bins
751
+
752
+ binvalues = list(np.arange(min_score, max_score, step))
753
+ labels = binvalues[1:]
754
+ labels.append(max_score)
755
+ binvalues.append(np.inf)
756
+ thresholds = pd.cut(x=y_score, bins=binvalues, right=False, labels=labels)
757
+
758
+ if y_group is not None:
759
+ df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'thresholds': thresholds, 'y_group': y_group})
760
+ else:
761
+ df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'thresholds': thresholds})
762
+ dist_df = __agg(df)
763
+
764
+ return dist_df
765
+
766
+ @timeit_decorator
767
+ def calc_equid_pct(y_true, y_score, y_group=None, bins=10, ascending=True, sample_weight=None):
768
+ """将Score严格的等分分组, 计算各组统计量.
769
+
770
+ Parameters
771
+ ----------
772
+ y_true: array like
773
+ 实际样本标签序列, 只接受0-1
774
+ y_score: array like
775
+ 预测概率值序列
776
+ y_group: array like
777
+ 数据组别序列. 默认为None, 即无组别
778
+ bins: int
779
+ 分组数
780
+ ascending: bool
781
+ y_score是否按升序排序, 默认为True
782
+
783
+ Returns
784
+ -------
785
+ pct_df: pandas.DataFrame
786
+ 等分分组后各组统计量数据集
787
+ """
788
+ if sample_weight is not None:
789
+ return _weighted_eval.calc_equid_pct(y_true, y_score, bins=bins, sample_weight=sample_weight)
790
+
791
+ y_true = np.array(y_true)
792
+ y_score = np.array(y_score)
793
+ size = len(y_true)
794
+ binsize = int(size / bins) # 向下取整
795
+ indices = np.argsort(y_score) if ascending else np.argsort(y_score)[::-1]
796
+
797
+ thresholds = np.array([0]*size)
798
+ thresholds_percentile = [100 * (i+1) / bins for i in range(bins)]
799
+ for i in range(bins):
800
+ s = i*binsize
801
+ e = (i+1)*binsize if i < bins-1 else np.max([size, (i+1)*binsize]) # 严格排序
802
+ thresholds[indices[s:e]] = thresholds_percentile[i]
803
+ if bool(y_group):
804
+ df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'y_group': y_group, 'thresholds': thresholds})
805
+ else:
806
+ df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'thresholds': thresholds})
807
+ pct_df = __agg(df)
808
+
809
+ avg_true = np.mean(y_true)
810
+ pct_df['lift'] = [x / avg_true for x in pct_df['cumavg_true']]
811
+ pct_df['gain'] = np.cumsum(pct_df['capture_rate'])
812
+
813
+ return pct_df
814
+
815
+
816
+ @timeit_decorator
817
+ def calc_fixed_pct(y_true, y_score, y_group=None, bin_edges=None, ascending=True, sample_weight=None):
818
+ """使用固定Score边界分组, 计算各组统计量.
819
+
820
+ Parameters
821
+ ----------
822
+ y_true: array like
823
+ 实际样本标签序列, 只接受0-1
824
+ y_score: array like
825
+ 预测概率值序列
826
+ y_group: array like
827
+ 数据组别序列. 默认为None, 即无组别
828
+ bin_edges: array like
829
+ 固定分箱边界, 通常来自benchmark数据集
830
+ ascending: bool
831
+ y_score是否按升序分箱. 默认为True
832
+
833
+ Returns
834
+ -------
835
+ pct_df: pandas.DataFrame
836
+ 固定分箱后各组统计量数据集
837
+ """
838
+ if sample_weight is not None:
839
+ return _weighted_eval.calc_fixed_pct(y_true, y_score, sample_weight=sample_weight)
840
+
841
+ if bin_edges is None:
842
+ raise ValueError("bin_edges cannot be None when using fixed pct bins.")
843
+
844
+ y_true = np.array(y_true)
845
+ y_score = np.array(y_score)
846
+ 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])
847
+ n_bins = len(bin_edges) - 1
848
+ labels = [100 * (i + 1) / n_bins for i in range(n_bins)]
849
+ if not ascending:
850
+ labels = labels[::-1]
851
+
852
+ thresholds = pd.cut(
853
+ x=y_score,
854
+ bins=bin_edges,
855
+ right=True,
856
+ include_lowest=False,
857
+ labels=labels
858
+ )
859
+
860
+ if y_group is not None:
861
+ df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'y_group': y_group, 'thresholds': thresholds})
862
+ else:
863
+ df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'thresholds': thresholds})
864
+
865
+ pct_df = __agg(df)
866
+
867
+ avg_true = np.mean(y_true)
868
+ pct_df['lift'] = [x / avg_true for x in pct_df['cumavg_true']]
869
+ pct_df['gain'] = np.cumsum(pct_df['capture_rate'])
870
+
871
+ return pct_df
872
+
873
+ @timeit_decorator
874
+ def summarize_pct(pct_df, ascending=True):
875
+ """统计等分分组信息.
876
+
877
+ Parameters
878
+ ----------
879
+ pct_df: pandas.DataFrame
880
+ 等分分组后各组统计量数据集
881
+
882
+ Returns
883
+ -------
884
+ pct_info: dict
885
+ 等分分组统计信息字典
886
+ """
887
+
888
+ if pct_df.empty:
889
+ return {
890
+ 'pct_bins': np.nan,
891
+ 'pct_interval': np.nan,
892
+ 'pct_top_avgTrue': np.nan,
893
+ 'pct_btm_avgTrue': np.nan,
894
+ }
895
+
896
+ bins = pct_df.shape[0]
897
+ interval = 100 / pct_df.shape[0]
898
+
899
+ if ascending:
900
+ pct_info = {
901
+ 'pct_bins': bins,
902
+ 'pct_interval': interval,
903
+ 'pct_top_avgTrue'.format(interval): pct_df['avg_true'].iloc[bins-1],
904
+ 'pct_btm_avgTrue'.format(interval): pct_df['avg_true'].iloc[0],
905
+ }
906
+ else:
907
+ pct_info = {
908
+ 'pct_bins': bins,
909
+ 'pct_interval': interval,
910
+ 'pct_top_captureRate'.format(interval): pct_df['capture_rate'].iloc[bins-1],
911
+ 'pct_btm_captureRate'.format(interval): pct_df['capture_rate'].iloc[0],
912
+ }
913
+
914
+ return pct_info
915
+
916
+
917
+ # Dist Curve
918
+ @timeit_decorator
919
+ def plot_dist_curve(dist_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
920
+ """绘制Score分布曲线图.
921
+
922
+ Parameters
923
+ ----------
924
+ dist_dfs: dict
925
+ 单个或多个Score名下的相同组数, 等距分组后各组统计量数据集. 键值对格式为: {name: dist_df}
926
+ square_figsize: float
927
+ 正方形图边英寸. 默认值为8
928
+ fontdicts: dict
929
+ 绘图相关字体字典
930
+ to_show: bool
931
+ 是否展示图片. 默认为True
932
+ save_path: str
933
+ 结果图片存放文件地址. 默认值为None, 即不保存
934
+ """
935
+ plt.figure(figsize=(square_figsize, square_figsize))
936
+ plt.suptitle('Score Distribution Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
937
+ ax = plt.subplot(1,1,1)
938
+ models = list(dist_dfs.keys())
939
+ if len(models) == 1:
940
+ dist_df = dist_dfs[models[0]]
941
+ if 'y_group' in dist_df.columns:
942
+ __plot_single_stack_dist_axes(dist_df, ax, fontdicts)
943
+ else:
944
+ __plot_single_dist_axes(dist_df, ax, fontdicts)
945
+ else:
946
+ __plot_multi_dist_axes(dist_dfs, ax, fontdicts)
947
+ if bool(save_path):
948
+ plt.savefig(save_path, bbox_inches='tight')
949
+ if to_show:
950
+ plt.show()
951
+ plt.close()
952
+
953
+
954
+ def __plot_dist_axes_base(ax, fontdicts):
955
+ """在axes上绘制dist图基础元素.
956
+
957
+ Parameters
958
+ ----------
959
+ ax: matplotlib.pyplot.plt.axes
960
+ 绘图axes
961
+ fontdicts: dict
962
+ 绘图相关字体字典
963
+ """
964
+ ax.set_xlabel('Score', fontdict=fontdicts['axislabel'])
965
+ ax.set_ylabel('Proportion', fontdict=fontdicts['axislabel'])
966
+
967
+
968
+ def __plot_single_dist_axes(dist_df, ax, fontdicts):
969
+ """在axes上绘制单个Score分布图.
970
+
971
+ Parameters
972
+ ----------
973
+ dist_df: pandas.DataFrame
974
+ 等距分组后各组统计量数据集
975
+ ax: matplotlib.pyplot.plt.axes
976
+ 绘图axes
977
+ fontdicts: dict
978
+ 绘图相关字体字典
979
+ """
980
+ __plot_dist_axes_base(ax, fontdicts)
981
+
982
+ X = np.arange(dist_df.shape[0])
983
+ tick_label = dist_df['thresholds']
984
+ ax.bar(X, dist_df['proportion'], align='edge', tick_label=tick_label, color=palette['ClassicBlueRedGrey'][0], width=-0.9, alpha=0.8)
985
+
986
+ ax_2 = ax.twinx()
987
+ ax_2.set_ylim(bottom=0)
988
+ ax_2.set_ylabel('Target Rate', fontdict=fontdicts['axislabel'])
989
+
990
+ ax_2.plot(X-0.5, dist_df['avg_true'], color=palette['ClassicBlueRedGrey'][1], linewidth=2, marker='.', markersize=5, label='True')
991
+ ax_2.axhline(y=dist_df['cumavg_true'][dist_df.shape[0]-1], linestyle='--', color=palette['ClassicBlueRedGrey'][2], linewidth=2, label='Random')
992
+
993
+ _n, _t, _s = dist_df['cumsum_n'].iloc[dist_df.shape[0]-1], dist_df['cumavg_true'].iloc[dist_df.shape[0]-1], dist_df['cumavg_score'].iloc[dist_df.shape[0]-1]
994
+ ax.set_title('N={0:,} True={1:.2%} Score={2:.2%}'.format(_n, _t, _s), fontsize=fontdicts['subtitle']['size'])
995
+ ax_2.legend(loc=2, fontsize=fontdicts['legend']['size'])
996
+
997
+
998
+ def __plot_single_stack_dist_axes(dist_df, ax, fontdicts):
999
+ """在axes上绘制单个Score分布图.
1000
+
1001
+ Parameters
1002
+ ----------
1003
+ dist_df: pandas.DataFrame
1004
+ 等距分组后各组统计量数据集
1005
+ ax: matplotlib.pyplot.plt.axes
1006
+ 绘图axes
1007
+ fontdicts: dict
1008
+ 绘图相关字体字典
1009
+ """
1010
+ __plot_dist_axes_base(ax, fontdicts)
1011
+
1012
+ gs = list(set(dist_df['y_group']))
1013
+ m = len(gs)
1014
+ tick_label = dist_df['thresholds'].drop_duplicates()
1015
+ n = len(tick_label)
1016
+
1017
+ X = np.arange(n)
1018
+ alpha = 0.8 / m
1019
+ y_offset = np.zeros(n)
1020
+ for i in range(m):
1021
+ g = gs[i]
1022
+ Y = dist_df.loc[dist_df['y_group']==g, 'proportion']
1023
+ ax.bar(X, Y, align='edge', bottom=y_offset, tick_label=tick_label, label=g, color=palette['ClassicBlueRedGrey'][0], width=-0.9, alpha=1-alpha*i)
1024
+ y_offset+=Y
1025
+ ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1026
+
1027
+ ax_2 = ax.twinx()
1028
+ ax_2.set_ylabel('Target Rate', fontdict=fontdicts['axislabel'])
1029
+ ax_2.set_ylim([0, np.max(dist_df['avg_true'])*1.1])
1030
+ for i in range(m):
1031
+ g = gs[i]
1032
+ Y = dist_df.loc[dist_df['y_group']==g, 'avg_true']
1033
+ ax_2.plot(X-0.5, Y, color=palette['ClassicGreyRed'][i], linewidth=2, marker='.', markersize=5, label='{0} True'.format(g))
1034
+
1035
+ ax_2.legend(loc=1, fontsize=fontdicts['legend']['size'])
1036
+
1037
+
1038
+ def __plot_multi_dist_axes(dist_dfs, ax, fontdicts):
1039
+ """在axes上绘制多个Score分布图.
1040
+
1041
+ Parameters
1042
+ ----------
1043
+ dist_df: pandas.DataFrame
1044
+ 单个或多个Score名下的相同组数, 等距分组后各组统计量数据集. 键值对格式为: {name: dist_df}
1045
+ ax: matplotlib.pyplot.plt.axes
1046
+ 绘图axes
1047
+ fontdicts: dict
1048
+ 绘图相关字体字典
1049
+ """
1050
+ __plot_dist_axes_base(ax, fontdicts)
1051
+
1052
+ models = list(dist_dfs.keys())
1053
+ for i in range(len(models)):
1054
+ md = models[i]
1055
+ dist_df = dist_dfs[md]
1056
+ X = np.arange(dist_df.shape[0])
1057
+ ax.bar(X, dist_df['proportion'], align='edge', tick_label=dist_df['thresholds'], color=palette['MorandiDark'][i], width=0.7, alpha=0.5)
1058
+
1059
+ ax_2 = ax.twinx()
1060
+ ax_2.set_ylabel('Target Rate', fontdict=fontdicts['axislabel'])
1061
+ for i in range(len(models)):
1062
+ md = models[i]
1063
+ dist_df = dist_dfs[md]
1064
+ ax_2.plot(X+0.5, dist_df['avg_true'], color=palette['MorandiDark'][i], linewidth=2, label='{0}'.format(md))
1065
+ ax_2.axhline(y=dist_df['cumavg_true'][dist_df.shape[0]-1], linestyle='--', color=palette['ClassicBlueRedGrey'][2], linewidth=2, label='Random')
1066
+
1067
+ ax_2.legend(loc=2, fontsize=fontdicts['legend']['size'])
1068
+
1069
+ @timeit_decorator
1070
+ def plot_cumdist_curve(dist_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
1071
+ """绘制Score分布曲线图.
1072
+
1073
+ Parameters
1074
+ ----------
1075
+ dist_dfs: dict
1076
+ 单个或多个Score名下的相同组数, 等距分组后各组统计量数据集. 键值对格式为: {name: dist_df}
1077
+ square_figsize: float
1078
+ 正方形图边英寸. 默认值为8
1079
+ fontdicts: dict
1080
+ 绘图相关字体字典
1081
+ to_show: bool
1082
+ 是否展示图片. 默认为True
1083
+ save_path: str
1084
+ 结果图片存放文件地址. 默认值为None, 即不保存
1085
+ """
1086
+ plt.figure(figsize=(square_figsize, square_figsize))
1087
+ plt.suptitle('Score Cumulative Distribution Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
1088
+ ax = plt.subplot(1,1,1)
1089
+ models = list(dist_dfs.keys())
1090
+ if len(models) == 1:
1091
+ dist_df = dist_dfs[models[0]]
1092
+ if 'y_group' in dist_df.columns:
1093
+ __plot_single_stack_cumdist_axes(dist_df, ax, fontdicts)
1094
+ else:
1095
+ __plot_single_cumdist_axes(dist_df, ax, fontdicts)
1096
+ else:
1097
+ __plot_multi_cumdist_axes(dist_dfs, ax, fontdicts)
1098
+ if bool(save_path):
1099
+ plt.savefig(save_path, bbox_inches='tight')
1100
+ if to_show:
1101
+ plt.show()
1102
+ plt.close()
1103
+
1104
+
1105
+ def __plot_cumdist_axes_base(ax, fontdicts):
1106
+ """在axes上绘制dist图基础元素.
1107
+
1108
+ Parameters
1109
+ ----------
1110
+ ax: matplotlib.pyplot.plt.axes
1111
+ 绘图axes
1112
+ fontdicts: dict
1113
+ 绘图相关字体字典
1114
+ """
1115
+ ax.set_xlim([0,1])
1116
+ ax.set_xlabel('Score', fontdict=fontdicts['axislabel'])
1117
+ ax.set_ylabel('Cumulative Percentile', fontdict=fontdicts['axislabel'])
1118
+
1119
+
1120
+ def __plot_single_cumdist_axes(dist_df, ax, fontdicts):
1121
+ """在axes上绘制单个Score分布图.
1122
+
1123
+ Parameters
1124
+ ----------
1125
+ dist_df: pandas.DataFrame
1126
+ 等距分组后各组统计量数据集
1127
+ ax: matplotlib.pyplot.plt.axes
1128
+ 绘图axes
1129
+ """
1130
+ __plot_cumdist_axes_base(ax, fontdicts)
1131
+
1132
+ X = np.arange(dist_df.shape[0])
1133
+ tick_label = dist_df['thresholds']
1134
+ ax.bar(X, dist_df['cumsum_proportion'], align='edge', tick_label=tick_label, color=palette['ClassicBlueRedGrey'][2], width=0.9, alpha=0.8)
1135
+
1136
+ ax_2 = ax.twinx()
1137
+ ax_2.set_ylim([0, np.max(dist_df['cumavg_true']) * 1.1])
1138
+ ax_2.set_ylabel('Target Rate', fontdict=fontdicts['axislabel'])
1139
+
1140
+ ax_2.plot(X+0.5, dist_df['cumavg_score'], color=palette['ClassicBlueRedGrey'][0], linewidth=2, marker='.', label='Score')
1141
+ ax_2.plot(X+0.5, dist_df['cumavg_true'], color=palette['ClassicBlueRedGrey'][1], linewidth=2, marker='.', label='True')
1142
+
1143
+ _n, _t, _s = dist_df['cumsum_n'].iloc[dist_df.shape[0]-1], dist_df['cumavg_true'].iloc[dist_df.shape[0]-1], dist_df['cumavg_score'].iloc[dist_df.shape[0]-1]
1144
+ ax.set_title('N={0:,} True={1:.2%} Score={2:.2%}'.format(_n, _t, _s), fontsize=fontdicts['subtitle']['size'])
1145
+ ax_2.legend(loc=2, fontsize=fontdicts['legend']['size'])
1146
+
1147
+
1148
+ def __plot_single_stack_cumdist_axes(dist_df, ax, fontdicts):
1149
+ """在axes上绘制单个Score分布图.
1150
+
1151
+ Parameters
1152
+ ----------
1153
+ dist_df: pandas.DataFrame
1154
+ 等距分组后各组统计量数据集
1155
+ ax: matplotlib.pyplot.plt.axes
1156
+ 绘图axes
1157
+ fontdicts: dict
1158
+ 绘图相关字体字典
1159
+ """
1160
+ __plot_cumdist_axes_base(ax, fontdicts)
1161
+
1162
+ gs = list(set(dist_df['y_group']))
1163
+ m = len(gs)
1164
+ tick_label = dist_df['thresholds'].drop_duplicates()
1165
+ n = len(tick_label)
1166
+
1167
+ X = np.arange(n)
1168
+ alpha = 0.8 / m
1169
+ y_offset = np.zeros(n)
1170
+ for i in range(m):
1171
+ g = gs[i]
1172
+ Y = dist_df.loc[dist_df['y_group']==g, 'cumsum_proportion']
1173
+ ax.bar(X, Y, align='edge', bottom=y_offset, tick_label=tick_label, label=g, color=palette['ClassicBlueRedGrey'][0], width=0.9, alpha=1-alpha*i)
1174
+ y_offset+=Y
1175
+ ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1176
+
1177
+ ax_2 = ax.twinx()
1178
+ ax_2.set_ylabel('Target Rate', fontdict=fontdicts['axislabel'])
1179
+ ax_2.set_ylim([0, np.max(dist_df['cumavg_true'])*1.1])
1180
+ for i in range(m):
1181
+ g = gs[i]
1182
+ Y = dist_df.loc[dist_df['y_group']==g, 'cumavg_true']
1183
+ ax_2.plot(X+0.5, Y, color=palette['ClassicGreyRed'][i], linewidth=2, marker='.', markersize=5, label='{0} True'.format(g), alpha=1-alpha*i)
1184
+
1185
+ ax_2.legend(loc=1, fontsize=fontdicts['legend']['size'])
1186
+
1187
+
1188
+ def __plot_multi_cumdist_axes(dist_dfs, ax, fontdicts):
1189
+ """在axes上绘制多个Score分布图.
1190
+
1191
+ Parameters
1192
+ ----------
1193
+ dist_df: pandas.DataFrame
1194
+ 单个或多个Score名下的相同组数, 等距分组后各组统计量数据集. 键值对格式为: {name: dist_df}
1195
+ ax: matplotlib.pyplot.plt.axes
1196
+ 绘图axes
1197
+ """
1198
+ __plot_cumdist_axes_base(ax, fontdicts)
1199
+
1200
+ models = list(dist_dfs.keys())
1201
+ for i in range(len(models)):
1202
+ md = models[i]
1203
+ dist_df = dist_dfs[md]
1204
+ X = np.arange(dist_df.shape[0])
1205
+ ax.bar(X, dist_df['cumsum_proportion'], align='edge', tick_label=dist_df['thresholds'], color=palette['MorandiDark'][i], width=0.7, alpha=0.5)
1206
+
1207
+ ax_2 = ax.twinx()
1208
+ ax_2.set_ylabel('Target Rate', fontdict=fontdicts['axislabel'])
1209
+ for i in range(len(models)):
1210
+ md = models[i]
1211
+ dist_df = dist_dfs[md]
1212
+ ax_2.plot(X+0.5, dist_df['cumavg_true'], color=palette['MorandiDark'][i], linewidth=2, marker='.', label='{0} True'.format(md))
1213
+
1214
+ ax_2.legend(loc=2, fontsize=fontdicts['legend']['size'])
1215
+
1216
+
1217
+ # PCT Curve
1218
+ @timeit_decorator
1219
+ def plot_pct_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
1220
+ """绘制Score分布曲线图.
1221
+
1222
+ Parameters
1223
+ ----------
1224
+ pct_dfs: dict
1225
+ 单个或多个Score名下的等分组后各组统计量数据集. 键值对格式为: {name: pct_df}
1226
+ square_figsize: float
1227
+ 正方形图边英寸. 默认值为8
1228
+ to_show: bool
1229
+ 是否展示图片. 默认为True
1230
+ save_path: str
1231
+ 结果图片存放文件地址. 默认值为None, 即不保存
1232
+ """
1233
+ plt.figure(figsize=(square_figsize, square_figsize))
1234
+ plt.suptitle('Score Percentile Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
1235
+ ax = plt.subplot(1,1,1)
1236
+
1237
+ models = list(pct_dfs.keys())
1238
+ if len(models) == 1:
1239
+ pct_df = pct_dfs[models[0]]
1240
+ __plot_single_pct_axes(pct_df, ax, fontdicts)
1241
+ else:
1242
+ __plot_multi_pct_axes(pct_dfs, ax, fontdicts)
1243
+
1244
+ if to_show:
1245
+ plt.show()
1246
+ if bool(save_path):
1247
+ plt.savefig(save_path, bbox_inches='tight')
1248
+ plt.close()
1249
+
1250
+
1251
+ def __plot_pct_axes_base(ax, fontdicts):
1252
+ """在axes上绘制dist图基础元素.
1253
+
1254
+ Parameters
1255
+ ----------
1256
+ ax: matplotlib.pyplot.plt.axes
1257
+ 绘图axes
1258
+ fontdicts: dict
1259
+ 绘图相关字体字典
1260
+ """
1261
+ ax.set_xlim([0,100])
1262
+ ax.set_xlabel('Percentile %', fontdict=fontdicts['axislabel'])
1263
+ ax.set_ylabel('Target rate', fontdict=fontdicts['axislabel'])
1264
+
1265
+
1266
+ def __plot_single_pct_axes(pct_df, ax, fontdicts):
1267
+ """在axes上绘制单个Score分布图.
1268
+
1269
+ Parameters
1270
+ ----------
1271
+ pct_df: pandas.DataFrame
1272
+ 等分分组后各组统计量数据集
1273
+ ax: matplotlib.pyplot.plt.axes
1274
+ 绘图axes
1275
+ """
1276
+
1277
+ if pct_df.empty:
1278
+ ax.set_title('Percentile Chart (No Data)', fontdict=fontdicts['subtitle'])
1279
+ return
1280
+
1281
+ __plot_pct_axes_base(ax, fontdicts)
1282
+
1283
+ X = pct_df['thresholds'].rolling(2).mean()
1284
+ X[0] = np.mean([pct_df['thresholds'][0], 0])
1285
+ ax.plot(X, pct_df['avg_score'], color=palette['ClassicBlueRedGrey'][0], linewidth=2, marker='.', markersize=5, label='Score')
1286
+ ax.plot(X, pct_df['avg_true'], color=palette['ClassicBlueRedGrey'][1], linewidth=2, marker='.', markersize=5, label='True')
1287
+ ax.axhline(y=pct_df['cumavg_true'][pct_df.shape[0]-1], linestyle='--', color=palette['ClassicBlueRedGrey'][2], linewidth=2, label='Random')
1288
+
1289
+ pct_info = summarize_pct(pct_df)
1290
+ _b, _i, _top, _btm = pct_info['pct_bins'], pct_info['pct_interval'], pct_info['pct_top_avgTrue'], pct_info['pct_btm_avgTrue']
1291
+ ax.set_title("Bins={0} Top{1:.0f}%={2:.2%} BTM{1:.0f}%={3:.2%}".format(_b, _i, _top, _btm), fontdict=fontdicts['subtitle'])
1292
+ ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1293
+
1294
+
1295
+ def __plot_multi_pct_axes(pct_dfs, ax, fontdicts):
1296
+ """在axes上绘制多个Score分布图.
1297
+
1298
+ Parameters
1299
+ ----------
1300
+ pct_dfs: dict
1301
+ 单个或多个Score名下的等分组后各组统计量数据集. 键值对格式为: {name: pct_df}
1302
+ ax: matplotlib.pyplot.plt.axes
1303
+ 绘图axes
1304
+ """
1305
+ __plot_pct_axes_base(ax, fontdicts)
1306
+
1307
+ models = list(pct_dfs.keys())
1308
+ for i in range(len(models)):
1309
+ md = models[i]
1310
+ pct_df = pct_dfs[md]
1311
+ X = pct_df['thresholds'].rolling(2).mean()
1312
+ X[0] = np.mean([pct_df['thresholds'][0], 0])
1313
+ ax.plot(X, pct_df['avg_true'], color=palette['MorandiDark'][i], linewidth=2, marker='.', markersize=5, label='{0} True'.format(md))
1314
+ ax.axhline(y=pct_df['cumavg_true'][pct_df.shape[0]-1], linestyle='--', color=palette['ClassicBlueRedGrey'][2], linewidth=2, label='Random')
1315
+ _n, _t = pct_df['cumsum_n'].iloc[pct_df.shape[0]-1], pct_df['cumavg_true'].iloc[pct_df.shape[0]-1]
1316
+ ax.set_title('N={0:,} True={1:.2%}'.format(_n, _t), fontdict=fontdicts['subtitle'])
1317
+ ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1318
+
1319
+ @timeit_decorator
1320
+ def plot_cumpct_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
1321
+ """绘制Score分布曲线图.
1322
+
1323
+ Parameters
1324
+ ----------
1325
+ pct_dfs: dict
1326
+ 单个或多个Score名下的等分组后各组统计量数据集. 键值对格式为: {name: pct_df}
1327
+ square_figsize: float
1328
+ 正方形图边英寸. 默认值为8
1329
+ to_show: bool
1330
+ 是否展示图片. 默认为True
1331
+ save_path: str
1332
+ 结果图片存放文件地址. 默认值为None, 即不保存
1333
+ """
1334
+ plt.figure(figsize=(square_figsize, square_figsize))
1335
+ plt.suptitle('Score Percentile Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
1336
+ ax = plt.subplot(1,1,1)
1337
+
1338
+ models = list(pct_dfs.keys())
1339
+ if len(models) == 1:
1340
+ pct_df = pct_dfs[models[0]]
1341
+ __plot_single_cumpct_axes(pct_df, ax, fontdicts)
1342
+ else:
1343
+ __plot_multi_cumpct_axes(pct_dfs, ax, fontdicts)
1344
+
1345
+ if to_show:
1346
+ plt.show()
1347
+ if bool(save_path):
1348
+ plt.savefig(save_path, bbox_inches='tight')
1349
+ plt.close()
1350
+
1351
+
1352
+ def __plot_cumpct_axes_base(ax, fontdicts):
1353
+ """在axes上绘制dist图基础元素.
1354
+
1355
+ Parameters
1356
+ ----------
1357
+ ax: matplotlib.pyplot.plt.axes
1358
+ 绘图axes
1359
+ fontdicts: dict
1360
+ 绘图相关字体字典
1361
+ """
1362
+ ax.set_xlim([0,100])
1363
+ ax.set_xlabel('Cumulative Percentile %', fontdict=fontdicts['axislabel'])
1364
+ ax.set_ylabel('Target rate', fontdict=fontdicts['axislabel'])
1365
+
1366
+
1367
+ def __plot_single_cumpct_axes(pct_df, ax, fontdicts):
1368
+ """在axes上绘制单个Score分布图.
1369
+
1370
+ Parameters
1371
+ ----------
1372
+ pct_df: pandas.DataFrame
1373
+ 等分分组后各组统计量数据集
1374
+ ax: matplotlib.pyplot.plt.axes
1375
+ 绘图axes
1376
+ """
1377
+ __plot_cumpct_axes_base(ax, fontdicts)
1378
+
1379
+ X = pct_df['thresholds_percentile'].rolling(2).mean()
1380
+ X[0] = np.mean([pct_df['thresholds_percentile'][0], 0])
1381
+ ax.plot(X, pct_df['cumavg_score'], color=palette['ClassicBlueRedGrey'][0], linewidth=2, marker='.', markersize=5, label='Score')
1382
+ ax.plot(X, pct_df['cumavg_true'], color=palette['ClassicBlueRedGrey'][1], linewidth=2, marker='.', markersize=5, label='True')
1383
+
1384
+ pct_info = summarize_pct(pct_df)
1385
+ _b, _i, _top, _btm = pct_info['pct_bins'], pct_info['pct_interval'], pct_info['pct_top_avgTrue'], pct_info['pct_btm_avgTrue']
1386
+ ax.set_title("Bins={0} Top{1:.0f}%={2:.2%} Btm{1:.0f}%={3:.2%}".format(_b, _i, _top, _btm), fontdict=fontdicts['axislabel'])
1387
+ ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1388
+
1389
+
1390
+ def __plot_multi_cumpct_axes(pct_dfs, ax, fontdicts):
1391
+ """在axes上绘制多个Score分布图.
1392
+
1393
+ Parameters
1394
+ ----------
1395
+ pct_dfs: dict
1396
+ 单个或多个Score名下的等分组后各组统计量数据集. 键值对格式为: {name: pct_df}
1397
+ ax: matplotlib.pyplot.plt.axes
1398
+ 绘图axes
1399
+ """
1400
+ __plot_cumpct_axes_base(ax, fontdicts)
1401
+
1402
+ models = list(pct_dfs.keys())
1403
+ for i in range(len(models)):
1404
+ md = models[i]
1405
+ pct_df = pct_dfs[md]
1406
+ X = pct_df['thresholds'].rolling(2).mean()
1407
+ X[0] = np.mean([pct_df['thresholds'][0], 0])
1408
+ ax.plot(X, pct_df['cumavg_true'], color=palette['MorandiDark'][i], linewidth=2, marker='.', markersize=5, label='{0} True'.format(md))
1409
+ _n, _t = pct_df['cumsum_n'].iloc[pct_df.shape[0]-1], pct_df['cumavg_true'].iloc[pct_df.shape[0]-1]
1410
+ ax.set_title('N={0:,} True={1:.2%}'.format(_n, _t), fontdict=fontdicts['subtitle'])
1411
+ ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1412
+
1413
+
1414
+ # Gain Curve
1415
+ @timeit_decorator
1416
+ def plot_gain_curve(pct_dfs, square_figsize=8, fontdicts=fontdicts['main'], to_show=True, save_path=None):
1417
+ """绘制Score分布曲线图.
1418
+
1419
+ Parameters
1420
+ ----------
1421
+ pct_dfs: dict
1422
+ 单个或多个Score名下的等分组后各组统计量数据集. 键值对格式为: {name: pct_df}
1423
+ square_figsize: float
1424
+ 正方形图边英寸. 默认值为8
1425
+ to_show: bool
1426
+ 是否展示图片. 默认为True
1427
+ save_path: str
1428
+ 结果图片存放文件地址. 默认值为None, 即不保存
1429
+ """
1430
+ plt.figure(figsize=(square_figsize, square_figsize))
1431
+ plt.suptitle('Gain Curve', fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
1432
+ ax = plt.subplot(1,1,1)
1433
+
1434
+ models = list(pct_dfs.keys())
1435
+ if len(models) == 1:
1436
+ pct_df = pct_dfs[models[0]]
1437
+ __plot_single_gain_axes(pct_df, ax, fontdicts)
1438
+ else:
1439
+ __plot_multi_gain_axes(pct_dfs, ax, fontdicts)
1440
+ ax.legend(loc=2, fontsize=fontdicts['legend']['size'])
1441
+
1442
+ if to_show:
1443
+ plt.show()
1444
+ if bool(save_path):
1445
+ plt.savefig(save_path, bbox_inches='tight')
1446
+ plt.close()
1447
+
1448
+
1449
+ def __plot_gain_axes_base(ax, fontdicts):
1450
+ """在axes上绘制Score分布图基础.
1451
+
1452
+ Parameters
1453
+ ----------
1454
+ ax: matplotlib.pyplot.plt.axes
1455
+ 绘图axes
1456
+ fontdicts: dict
1457
+ 绘图相关字体字典
1458
+ """
1459
+ ax.plot([0,100], [0,1], color='k', linestyle='--', linewidth=1)
1460
+ ax.set_xlim([0,100])
1461
+ ax.set_ylim([0,1])
1462
+ ax.set_xlabel('Percentile % (Score Descending)', fontdict=fontdicts['axislabel'])
1463
+ ax.set_ylabel('gain', fontdict=fontdicts['axislabel'])
1464
+
1465
+
1466
+ def __plot_single_gain_axes(pct_df, ax, fontdicts):
1467
+ """在axes上绘制单个Score分布图.
1468
+
1469
+ Parameters
1470
+ ----------
1471
+ pct_df: pandas.DataFrame
1472
+ 等分分组后各组统计量数据集
1473
+ ax: matplotlib.pyplot.plt.axes
1474
+ 绘图axes
1475
+ fontdicts: dict
1476
+ 绘图相关字体字典
1477
+ """
1478
+ __plot_gain_axes_base(ax, fontdicts)
1479
+
1480
+ plot_df = pct_df.copy()
1481
+ if {'avg_score', 'proportion', 'capture_rate'}.issubset(plot_df.columns):
1482
+ plot_df = plot_df.sort_values('avg_score', ascending=False).reset_index(drop=True)
1483
+ plot_df['thresholds'] = plot_df['proportion'].cumsum() * 100
1484
+ plot_df['gain'] = plot_df['capture_rate'].cumsum()
1485
+
1486
+ X = np.array(plot_df['thresholds'])
1487
+ X = np.insert(X, 0, 0)
1488
+ Y = np.array(plot_df['gain'])
1489
+ Y = np.insert(Y, 0, 0)
1490
+ ax.plot(X, Y, color=palette['ClassicBlueRedGrey'][0], linewidth=2, marker='.', markersize=5, label='avgScore')
1491
+
1492
+ pct_info = summarize_pct(plot_df, ascending=False)
1493
+ _b, _i = pct_info['pct_bins'], pct_info['pct_interval']
1494
+ _top = plot_df['capture_rate'].iloc[0] if plot_df.shape[0] > 0 else np.nan
1495
+ _btm = plot_df['capture_rate'].iloc[-1] if plot_df.shape[0] > 0 else np.nan
1496
+ ax.set_title("Bins={0} Top{1:.0f}%={2:.2%} BTM{1:.0f}%={3:.2%}".format(_b, _i, _top, _btm), fontdict=fontdicts['subtitle'])
1497
+
1498
+
1499
+ def __plot_multi_gain_axes(pct_dfs, ax, fontdicts):
1500
+ """在axes上绘制多个Score分布图.
1501
+
1502
+ Parameters
1503
+ ----------
1504
+ pct_dfs: dict
1505
+ 单个或多个Score名下的等分组后各组统计量数据集. 键值对格式为: {name: pct_df}
1506
+ ax: matplotlib.pyplot.plt.axes
1507
+ 绘图axes
1508
+ fontdicts: dict
1509
+ 绘图相关字体字典
1510
+ """
1511
+ __plot_gain_axes_base(ax, fontdicts)
1512
+
1513
+ models = list(pct_dfs.keys())
1514
+ for i in range(len(models)):
1515
+ md = models[i]
1516
+ pct_df = pct_dfs[md].copy()
1517
+ if {'avg_score', 'proportion', 'capture_rate'}.issubset(pct_df.columns):
1518
+ pct_df = pct_df.sort_values('avg_score', ascending=False).reset_index(drop=True)
1519
+ pct_df['thresholds'] = pct_df['proportion'].cumsum() * 100
1520
+ pct_df['gain'] = pct_df['capture_rate'].cumsum()
1521
+ X = np.array(pct_df['thresholds'])
1522
+ X = np.insert(X, 0, 0)
1523
+ Y = np.array(pct_df['gain'])
1524
+ Y = np.insert(Y, 0, 0)
1525
+ ax.plot(X, Y, color=palette['MorandiDark'][i], linewidth=2, marker='.', markersize=5, label='{0} avgTrue'.format(md))
1526
+
1527
+ @timeit_decorator
1528
+ def evaluate_performance(datasets, dist_bins=20, pct_bins=10, square_figsize=5, fontdicts=fontdicts['sub'], to_show=True, save_path=None, gains_table = True, equal_freq = True, pct_bin_edges = None, sample_weight=None):
1529
+ """绘制单模型预测效果评价图.
1530
+
1531
+ Parameters
1532
+ ----------
1533
+ datasets: pandas.DataFrame
1534
+ 数据集字典. 键值对格式为: {dataname: {'y_true': y_true, 'y_score': y_score}}
1535
+ dist_bins: int
1536
+ 等距分组数. 默认值为20
1537
+ pct_bins: int
1538
+ 等分分组数. 默认值为10
1539
+ square_figsize: float
1540
+ 正方形图边英寸. 默认值为8
1541
+ fontdicts: dict
1542
+ 绘图相关字体字典. 默认值为fontdicts['sub']
1543
+ to_show: bool
1544
+ 是否展示图片. 默认为True
1545
+ save_path: str
1546
+ 结果图片存放文件地址. 默认值为None, 即不保存
1547
+
1548
+ Returns
1549
+ -------
1550
+ result_df: pandas.DataFrame
1551
+ 模型评价指标汇总数据集
1552
+ """
1553
+ if pct_bin_edges is not None:
1554
+ pct_bin_edges = list(pct_bin_edges)
1555
+
1556
+ # 检查是否有足够数据
1557
+ for d, data_dict in datasets.items():
1558
+ if len(data_dict['y_true']) < 2:
1559
+ # 返回一个包含默认列的空 DataFrame
1560
+ return pd.DataFrame()
1561
+
1562
+ datas = list(datasets.keys())
1563
+ nrow = len(datas)
1564
+ ncol = 4
1565
+ width = ncol * (square_figsize+1)
1566
+ height = nrow * (square_figsize+1)
1567
+ plt.figure(figsize=(width, height))
1568
+ plt.subplots_adjust(top=1-1/height, wspace=0.2, hspace=0.2)
1569
+ title = 'Model Evaluation (Row Dataset: {0})'.format(', '.join(datas)) if nrow > 1 else 'Model Evaluation'
1570
+ plt.suptitle(title, fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
1571
+
1572
+ result = {}
1573
+ for i in range(len(datas)):
1574
+ d = datas[i]
1575
+ y_true = datasets[d]['y_true']
1576
+ y_score = datasets[d]['y_score']
1577
+ dataset_weight = datasets[d].get('sample_weight', sample_weight)
1578
+ result.update({d: __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins, fontdicts, gains_table, equal_freq, pct_bin_edges, sample_weight=dataset_weight)})
1579
+
1580
+ result_df = pd.DataFrame.from_dict(result, orient='index').reset_index()
1581
+
1582
+ if bool(save_path):
1583
+ plt.savefig(save_path, bbox_inches='tight')
1584
+
1585
+ if to_show:
1586
+ plt.show()
1587
+
1588
+ plt.close('all')
1589
+
1590
+ return result_df
1591
+
1592
+ def resturct_gains(gains_table):
1593
+
1594
+ rename_dict = {"_bin_num": "thresholds",
1595
+ "MIN": "min_score",
1596
+ "MAX": "max_score",
1597
+ "N": "n",
1598
+ "PROP": 'proportion',
1599
+ "N_BAD": "sum_true",
1600
+ "AVG_BAD": "avg_true",
1601
+ "AVG_SCORE": "avg_score",
1602
+ "BAD_PCT_IN_EACH_BIN": 'capture_rate',
1603
+ "N_CUM_BAD": 'cumsum_true',
1604
+ "LIFT": 'lift',
1605
+ "CUM_BAD_PCT": 'gain'}
1606
+ gains_table = gains_table.reset_index().rename(columns = rename_dict)[rename_dict.values()]
1607
+ gains_table['cumavg_true'] = gains_table['cumsum_true']/gains_table['n'].cumsum()
1608
+
1609
+ return gains_table
1610
+
1611
+ def __evaluate_performance(y_true, y_score, nrow, ncol, i, dist_bins, pct_bins, fontdicts, gains_table = True, equal_freq = True, pct_bin_edges = None, sample_weight=None):
1612
+ """绘制单模型在单样本集上预测效果评价图.
1613
+ 包括: ROC、KDE、PCT、Gain四图.
1614
+
1615
+ Parameters
1616
+ ----------
1617
+ y_true: array like
1618
+ 实际样本标签序列, 只接受0-1
1619
+ y_score: array like
1620
+ 预测概率值序列
1621
+ nrow: int
1622
+ 行数
1623
+ ncol: int
1624
+ 列数
1625
+ i: int
1626
+ 行序号
1627
+ dist_bins: int
1628
+ 等距分组数
1629
+ pct_bins: int
1630
+ 等分分组数
1631
+ fontdicts: dict
1632
+ 绘图相关字体字典
1633
+ """
1634
+
1635
+ # from Model_Eval_Tool import get_gains_table
1636
+ from .Model_Eval_Tool import get_gains_table
1637
+
1638
+ # 清理数据
1639
+ mask = np.isfinite(y_score) & np.isfinite(y_true)
1640
+ if sample_weight is not None:
1641
+ sample_weight = np.asarray(sample_weight, dtype=float)
1642
+ mask = mask & np.isfinite(sample_weight)
1643
+ y_true = y_true[mask]
1644
+ y_score = y_score[mask]
1645
+ sample_weight = sample_weight[mask] if sample_weight is not None else None
1646
+
1647
+ y_true = np.array(y_true)
1648
+ y_score = np.array(y_score)
1649
+
1650
+ if gains_table:
1651
+ y_df = pd.DataFrame([y_true, y_score]).T
1652
+ y_df.columns = ['y_true', 'y_score']
1653
+ if sample_weight is not None:
1654
+ y_df['_sample_weight'] = sample_weight
1655
+ y_gains = get_gains_table(
1656
+ data = y_df,
1657
+ dep = 'y_true',
1658
+ nbins = pct_bin_edges if pct_bin_edges is not None else pct_bins,
1659
+ precision = 5,
1660
+ min_bin_prop = 0.05,
1661
+ include_missing = False,
1662
+ score = 'y_score',
1663
+ equal_freq = equal_freq,
1664
+ ascending = True ,
1665
+ withSummary = False,
1666
+ weight_col = '_sample_weight' if sample_weight is not None else None,
1667
+ )
1668
+
1669
+ roc_df = calc_roc(y_true, y_score, sample_weight=sample_weight)
1670
+ roc_info = summarize_roc(roc_df)
1671
+ if sample_weight is not None:
1672
+ bin_count = len(pct_bin_edges) - 1 if pct_bin_edges is not None else pct_bins
1673
+ weighted_gains = _weighted_eval.get_gains_table(
1674
+ pd.DataFrame({"y_true": y_true, "y_score": y_score, "w": sample_weight}),
1675
+ "y_true",
1676
+ "y_score",
1677
+ nbins=bin_count,
1678
+ weight_col="w",
1679
+ )
1680
+ pct_df = weighted_gains
1681
+ pct_desc_df = weighted_gains.iloc[::-1].reset_index(drop=True)
1682
+ if gains_table:
1683
+ y_gains = weighted_gains
1684
+ pct_info = {
1685
+ "pct_bins": weighted_gains.shape[0],
1686
+ "pct_interval": 100.0 / weighted_gains.shape[0] if weighted_gains.shape[0] else np.nan,
1687
+ "pct_top_avgTrue": float(weighted_gains["AVG_BAD"].iloc[-1]) if len(weighted_gains) else np.nan,
1688
+ "pct_btm_avgTrue": float(weighted_gains["AVG_BAD"].iloc[0]) if len(weighted_gains) else np.nan,
1689
+ }
1690
+ else:
1691
+ if pct_bin_edges is not None:
1692
+ pct_df = calc_fixed_pct(y_true, y_score, None, bin_edges=pct_bin_edges, ascending=True, sample_weight=sample_weight)
1693
+ else:
1694
+ pct_df = calc_equid_pct(y_true, y_score, None, bins=pct_bins, sample_weight=sample_weight)
1695
+
1696
+ if gains_table:
1697
+ pct_index = pct_df["thresholds"]
1698
+ pct_df = resturct_gains(y_gains)
1699
+ pct_df["thresholds"] = pct_index
1700
+
1701
+ pct_info = summarize_pct(pct_df, ascending=True)
1702
+
1703
+ if len(y_true) < 2 or len(np.unique(y_true)) < 2:
1704
+ # 返回默认性能指标(全部为NaN)
1705
+ return {
1706
+ 'N': np.nan,
1707
+ 'avgTrue': np.nan,
1708
+ 'avgScore': np.nan,
1709
+ 'KS': np.nan,
1710
+ 'AUC': np.nan,
1711
+ 'Btm{0:.0f}%_TargetRate'.format(pct_info['pct_interval']): np.nan,
1712
+ 'Top{0:.0f}%_TargetRate'.format(pct_info['pct_interval']): np.nan
1713
+ }
1714
+
1715
+ if sample_weight is None:
1716
+ if pct_bin_edges is not None:
1717
+ pct_desc_df = calc_fixed_pct(y_true, y_score, None, bin_edges=pct_bin_edges, ascending=False, sample_weight=sample_weight)
1718
+ else:
1719
+ pct_desc_df = calc_equid_pct(y_true, y_score, None, bins=pct_bins, ascending=False, sample_weight=sample_weight)
1720
+
1721
+ __plot_single_roc_axes(roc_df, plt.subplot(nrow, ncol, i*ncol+1), fontdicts)
1722
+ __plot_single_kde_axes(y_true, y_score, dist_bins, plt.subplot(nrow, ncol, i*ncol+2), fontdicts)
1723
+ if sample_weight is None:
1724
+ __plot_single_pct_axes(pct_df, plt.subplot(nrow, ncol, i*ncol+3), fontdicts)
1725
+ __plot_single_gain_axes(pct_desc_df, plt.subplot(nrow, ncol, i*ncol+4), fontdicts)
1726
+ else:
1727
+ ax_pct = plt.subplot(nrow, ncol, i * ncol + 3)
1728
+ ax_pct.set_title("Percentile Chart (Weighted)", fontdict=fontdicts["subtitle"])
1729
+ ax_gain = plt.subplot(nrow, ncol, i * ncol + 4)
1730
+ ax_gain.set_title("Gain Chart (Weighted)", fontdict=fontdicts["subtitle"])
1731
+
1732
+ info = {
1733
+ 'N': float(np.sum(sample_weight)) if sample_weight is not None else len(y_true),
1734
+ 'avgTrue': _weighted_eval.safe_weighted_average(y_true, sample_weight),
1735
+ 'avgScore': _weighted_eval.safe_weighted_average(y_score, sample_weight),
1736
+ 'KS': roc_info['ks'],
1737
+ 'AUC': roc_info['auc'],
1738
+ 'Btm{0:.0f}%_TargetRate'.format(pct_info['pct_interval']): pct_info['pct_btm_avgTrue'],
1739
+ 'Top{0:.0f}%_TargetRate'.format(pct_info['pct_interval']): pct_info['pct_top_avgTrue'],
1740
+ }
1741
+
1742
+ return info
1743
+
1744
+ @timeit_decorator
1745
+ def evaluate_distribution(datasets, dist_bins=10, square_figsize=5, fontdicts=fontdicts['sub'], toplot=True, save_path=None):
1746
+ """绘制单模型在多样本集上模型分分布.
1747
+
1748
+ Parameters
1749
+ ----------
1750
+ datasets: pandas.DataFrame
1751
+ 数据集字典. 键值对格式为: {dataname: {'y_true': y_true, 'y_score': y_score, 'y_group': y_group}}
1752
+ dist_bins: int
1753
+ 等距分组数. 默认值为20
1754
+ square_figsize: float
1755
+ 正方形图边英寸. 默认值为8
1756
+ fontdicts: dict
1757
+ 绘图相关字体字典. 默认值为fontdicts['sub']
1758
+ toplot: bool
1759
+ 是否展示图片. 默认为True
1760
+ save_path: str
1761
+ 结果图片存放文件地址. 默认值为None, 即不保存
1762
+
1763
+ Returns
1764
+ -------
1765
+ result_df: pandas.DataFrame
1766
+ 模型评价指标汇总数据集
1767
+ """
1768
+ datas = list(datasets.keys())
1769
+ nrow = len(datas)
1770
+ ncol = 2
1771
+ width = ncol * (square_figsize+1)
1772
+ height = nrow * (square_figsize+1)
1773
+ plt.figure(figsize=(width, height))
1774
+ plt.subplots_adjust(top=1-1/height, wspace=0.2, hspace=0.2)
1775
+ if nrow > 1:
1776
+ title = 'Distribution (Row Dataset: {0})'.format(', '.join(datas))
1777
+ else:
1778
+ title = 'Distribution'
1779
+ plt.suptitle(title, fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
1780
+ for i in range(len(datas)):
1781
+ d = datas[i]
1782
+ y_true = datasets[d]['y_true']
1783
+ y_score = datasets[d]['y_score']
1784
+ y_group = datasets[d]['y_group']
1785
+ __evaluate_distribution(y_true, y_score, y_group, nrow, ncol, i, dist_bins, fontdicts)
1786
+ plt.tight_layout()
1787
+
1788
+ if bool(save_path):
1789
+ plt.savefig(save_path)
1790
+
1791
+ if toplot:
1792
+ plt.show()
1793
+
1794
+ plt.close('all')
1795
+
1796
+
1797
+ def __evaluate_distribution(y_true, y_score, y_group, nrow, ncol, i, dist_bins, fontdicts):
1798
+ """绘制单模型在单样本集上模型分分布.
1799
+ 包括: ROC、KDE、PCT、Gain四图.
1800
+
1801
+ Parameters
1802
+ ----------
1803
+ y_true: array like
1804
+ 实际样本标签序列, 只接受0-1
1805
+ y_score: array like
1806
+ 预测概率值序列
1807
+ y_group: array like
1808
+ 数据组别序列
1809
+ nrow: int
1810
+ 行数
1811
+ ncol: int
1812
+ 列数
1813
+ i: int
1814
+ 行序号
1815
+ dist_bins: int
1816
+ 等距分组数
1817
+ fontdicts: dict
1818
+ 绘图相关字体字典
1819
+ """
1820
+ dist_df = calc_equid_dist(y_true, y_score, y_group, bins=dist_bins)
1821
+ if y_group is not None:
1822
+ __plot_single_stack_dist_axes(dist_df, plt.subplot(nrow, ncol, i*ncol+1), fontdicts)
1823
+ __plot_single_stack_cumdist_axes(dist_df, plt.subplot(nrow, ncol, i*ncol+2), fontdicts)
1824
+ else:
1825
+ __plot_single_dist_axes(dist_df, plt.subplot(nrow, ncol, i*ncol+1), fontdicts)
1826
+ __plot_single_cumdist_axes(dist_df, plt.subplot(nrow, ncol, i*ncol+2), fontdicts)
1827
+
1828
+ @timeit_decorator
1829
+ def comparison_performance(datasets, pct_bins=10, square_figsize=5, fontdicts=fontdicts['sub'], to_show=True, save_path=None):
1830
+ """绘制多个模型预测效果对比图.
1831
+
1832
+ Parameters
1833
+ ----------
1834
+ datasets: pandas.DataFrame
1835
+ 数据集字典. 键值对格式为: {dataname: {'y_true': y_true, 'y_score': y_score, 'y_group': y_group}}
1836
+ pct_bins: int
1837
+ 等分分组数. 默认值为10
1838
+ square_figsize: float
1839
+ 正方形图边英寸. 默认值为5
1840
+ fontdicts: dict
1841
+ 绘图相关字体字典. 默认值为fontdicts['sub']
1842
+ to_show: bool
1843
+ 是否展示图片. 默认为True
1844
+ save_path: str
1845
+ 结果图片存放文件地址. 默认值为None, 即不保存
1846
+
1847
+ Returns
1848
+ -------
1849
+ result_df: pandas.DataFrame
1850
+ 模型评价指标汇总数据集
1851
+ """
1852
+ datas = list(datasets.keys())
1853
+ models = list(datasets[datas[0]]['y_score_dict'].keys())
1854
+ nrow = len(datas)
1855
+ ncol = 3
1856
+ width = ncol * (square_figsize+1)
1857
+ height = nrow * (square_figsize+1)
1858
+ plt.figure(figsize=(width, height))
1859
+ plt.subplots_adjust(top=1-1/height, wspace=0.2, hspace=0.2)
1860
+ title = title = '{0} Comparison (Row Dataset: {1})'.format(' vs '.join(models), ', '.join(datas)) if nrow > 1 else '{0} Comparison'.format(' vs '.join(models))
1861
+ plt.suptitle(title, fontsize=fontdicts['suptitle']['size'], fontweight=fontdicts['suptitle']['weight']) #, findfont=zhfont)
1862
+
1863
+ result_dfs = []
1864
+ for i in range(len(datas)):
1865
+ d = datas[i]
1866
+ y_true = datasets[d]['y_true']
1867
+ y_score_dict = datasets[d]['y_score_dict']
1868
+ _result = __comparison_performance(y_true, y_score_dict, nrow, ncol, i, pct_bins, fontdicts)
1869
+ _result.loc[:, 'dataset'] = d
1870
+ result_dfs.append(_result)
1871
+
1872
+ result_df = pd.concat(result_dfs)
1873
+
1874
+ if bool(save_path):
1875
+ plt.savefig(save_path, bbox_inches='tight')
1876
+
1877
+ if to_show:
1878
+ plt.show()
1879
+
1880
+ plt.close('all')
1881
+
1882
+ return result_df
1883
+
1884
+
1885
+ def __comparison_performance(y_true, y_score_dict, nrow, ncol, i, pct_bins, fontdicts):
1886
+
1887
+ y_true = np.array(y_true)
1888
+ models = list(y_score_dict.keys())
1889
+ roc_dfs = {}
1890
+ pct_dfs = {}
1891
+ info = {
1892
+ 'N': len(y_true),
1893
+ 'avgTrue': np.mean(y_true),
1894
+ 'result': [],
1895
+ }
1896
+ for k in range(len(models)):
1897
+ md = models[k]
1898
+ y_score = np.array(y_score_dict[md])
1899
+ roc_df = calc_roc(y_true, y_score)
1900
+ roc_info = summarize_roc(roc_df)
1901
+ pct_df = calc_equid_pct(y_true, y_score, bins=pct_bins)
1902
+ pct_info = summarize_pct(pct_df, ascending=True)
1903
+ info['result'].append({
1904
+ 'model': md,
1905
+ 'KS': roc_info['ks'],
1906
+ 'AUC': roc_info['auc'],
1907
+ 'Btm{0:.0f}%_TargetRate'.format(pct_info['pct_interval']): pct_info['pct_btm_avgTrue'],
1908
+ 'Top{0:.0f}%_TargetRate'.format(pct_info['pct_interval']): pct_info['pct_top_avgTrue'],
1909
+ })
1910
+ roc_dfs.update({md: roc_df})
1911
+ pct_dfs.update({md: pct_df})
1912
+
1913
+ __plot_multi_roc_axes(roc_dfs, plt.subplot(nrow, ncol, i*ncol+1), fontdicts)
1914
+ __plot_multi_pct_axes(pct_dfs, plt.subplot(nrow, ncol, i*ncol+2), fontdicts)
1915
+ __plot_multi_cumpct_axes(pct_dfs, plt.subplot(nrow, ncol, i*ncol+3), fontdicts)
1916
+
1917
+ result_df = pd.json_normalize(info, ['result'], ['N', 'avgTrue'])
1918
+
1919
+ return result_df
1920
+
1921
+
1922
+ # lift table apt
1923
+ @timeit_decorator
1924
+ def calc_lift_apt(y_true, y_score, start, stop, step, score_ascending=True, sample_weight=None):
1925
+ """给定Lift取值范围, 求解Lift表.
1926
+
1927
+ Parameters
1928
+ ----------
1929
+ y_true: array like
1930
+ 实际样本标签序列, 只接受0-1
1931
+ y_score: array like
1932
+ 预测概率值序列
1933
+ start: numerical
1934
+ 起始值
1935
+ stop: numerical
1936
+ 终止值
1937
+ step: numerical
1938
+ 步长
1939
+ score_ascending: bool
1940
+ y_score为升序序列, 即数值越大y_true=1可能性越大. 默认为True
1941
+
1942
+ Returns
1943
+ -------
1944
+ lift_df: pandas.DataFrame
1945
+ Lift表
1946
+ """
1947
+ if sample_weight is not None:
1948
+ weight = np.asarray(sample_weight, dtype=float)
1949
+ int_weight = np.rint(weight).astype(int)
1950
+ if np.allclose(weight, int_weight) and np.all(int_weight >= 0):
1951
+ repeat_idx = np.repeat(np.arange(len(int_weight)), int_weight)
1952
+ y_true = np.asarray(y_true)[repeat_idx]
1953
+ y_score = np.asarray(y_score)[repeat_idx]
1954
+ else:
1955
+ return _weighted_eval.calc_lift_apt(
1956
+ y_true=y_true,
1957
+ y_score=y_score,
1958
+ start=start,
1959
+ stop=stop,
1960
+ step=step,
1961
+ sample_weight=sample_weight,
1962
+ )
1963
+
1964
+ # 计算初始等分分组数, 在200组与LiftTable长度中取大
1965
+ init_bins = np.max([200, int((stop - start + step) / step)])
1966
+
1967
+ # 根据start和stop值, 判断lift的升降(lift_ascending)
1968
+ # 升则stop=1, 降则start=1
1969
+ if start < 1:
1970
+ stop = 1
1971
+ elif start >= 1:
1972
+ start = 1
1973
+ lift_ascending = start < 1
1974
+ lift_df = pd.DataFrame({'lift': np.arange(start, stop + step, step)})
1975
+
1976
+ # 根据分数与Y=1的升降关系(score_ascending)与lift的升降(lift_ascending)判断分数分组的升降序
1977
+ # 一致则为升、不一致则为降
1978
+ ascending = score_ascending == lift_ascending
1979
+ equid_df = calc_equid_pct(y_true=y_true, y_score=y_score, y_group=None, bins=init_bins, ascending=ascending)
1980
+
1981
+ # 根据分数升降序, 修正上下组限值, 遵循上组限不在内原则
1982
+ if ascending:
1983
+ equid_df['lower_limit'] = [-np.inf, ] + list(equid_df['min_score'][1:])
1984
+ equid_df['upper_limit'] = list(equid_df['min_score'][1:]) + [np.inf, ]
1985
+ else:
1986
+ equid_df['lower_limit'] = list(equid_df['min_score'][:-1]) + [-np.inf, ]
1987
+ equid_df['upper_limit'] = [np.inf, ] + list(equid_df['min_score'][:-1])
1988
+ lift_df = pd.merge(lift_df.assign(key=1), equid_df.assign(key=1), how='left', on='key', suffixes=('', '_actual')).drop(columns=['key'])
1989
+
1990
+ # 根据lift的升降(lift_ascending)求解切分数据
1991
+ # 升则取不高于lift的最大值, 降则取不低于lift的最小值
1992
+ if lift_ascending:
1993
+ cond = lift_df['lift'] >= lift_df['lift_actual']
1994
+ else:
1995
+ cond = lift_df['lift'] <= lift_df['lift_actual']
1996
+ lift_df = lift_df.loc[cond, ].reset_index(drop=True)
1997
+ lift_df = lift_df.sort_values(by=['lift', 'thresholds'], ascending=[lift_ascending, False])
1998
+ lift_df = lift_df.groupby(['lift']).first().reset_index()
1999
+ lift_df = lift_df.sort_values(by=['lift'], ascending=lift_ascending)
2000
+
2001
+ # 根据分数排序, 升序时取组上限, 降序时取组下限
2002
+ if ascending:
2003
+ cols = ['lift', 'lift_actual', 'upper_limit', 'cumsum_n', 'cumsum_proportion', 'cumsum_true', 'cumavg_true']
2004
+ else:
2005
+ cols = ['lift', 'lift_actual', 'lower_limit', 'cumsum_n', 'cumsum_proportion', 'cumsum_true', 'cumavg_true']
2006
+ lift_df = lift_df[cols]
2007
+
2008
+ return lift_df