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.
- ExcelMaster/ExcelFormatTool.py +487 -0
- ExcelMaster/ExcelMaster.py +917 -0
- ExcelMaster/Template.py +525 -0
- ExcelMaster/Utility.py +233 -0
- ExcelMaster/__init__.py +3 -0
- Modeling_Tool/Core/Binning_Tool.py +1608 -0
- Modeling_Tool/Core/Binning_Tool.pyi +48 -0
- Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
- Modeling_Tool/Core/Json_Data_Converter.py +621 -0
- Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
- Modeling_Tool/Core/ODPS_Tool.py +284 -0
- Modeling_Tool/Core/Slope_Tool.py +356 -0
- Modeling_Tool/Core/Slope_Tool.pyi +31 -0
- Modeling_Tool/Core/XOR_Encryptor.py +207 -0
- Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
- Modeling_Tool/Core/__init__.py +99 -0
- Modeling_Tool/Core/kDataFrame.py +228 -0
- Modeling_Tool/Core/kDataFrame.pyi +43 -0
- Modeling_Tool/Core/sample_weight_utils.py +77 -0
- Modeling_Tool/Core/utils.py +2672 -0
- Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
- Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
- Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
- Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
- Modeling_Tool/Eval/__init__.py +54 -0
- Modeling_Tool/Eval/evaluate_model.py +2008 -0
- Modeling_Tool/Eval/evaluate_model.pyi +50 -0
- Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
- Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
- Modeling_Tool/Explainability/Model_Explainer.py +743 -0
- Modeling_Tool/Explainability/__init__.py +24 -0
- Modeling_Tool/Feature/Distribution_Tool.py +509 -0
- Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
- Modeling_Tool/Feature/Feature_Insights.py +762 -0
- Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
- Modeling_Tool/Feature/PSI_Tool.py +1195 -0
- Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
- Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
- Modeling_Tool/Feature/__init__.py +40 -0
- Modeling_Tool/Model/Backward_Tool.py +778 -0
- Modeling_Tool/Model/Backward_Tool.pyi +45 -0
- Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
- Modeling_Tool/Model/GBM_Tool.py +1610 -0
- Modeling_Tool/Model/GBM_Tool.pyi +90 -0
- Modeling_Tool/Model/LRM_Tool.py +1198 -0
- Modeling_Tool/Model/LRM_Tool.pyi +47 -0
- Modeling_Tool/Model/__init__.py +61 -0
- Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
- Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
- Modeling_Tool/Sample/Reject_Infer.py +413 -0
- Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
- Modeling_Tool/Sample/Sample_Split.py +520 -0
- Modeling_Tool/Sample/Sample_Split.pyi +43 -0
- Modeling_Tool/Sample/__init__.py +31 -0
- Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
- Modeling_Tool/UAT/__init__.py +19 -0
- Modeling_Tool/WOE/WOE_Adapter.py +204 -0
- Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
- Modeling_Tool/WOE/WOE_Master.py +491 -0
- Modeling_Tool/WOE/WOE_Master.pyi +40 -0
- Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
- Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
- Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
- Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
- Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
- Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
- Modeling_Tool/WOE/WOE_Tool.py +1094 -0
- Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
- Modeling_Tool/WOE/__init__.py +86 -0
- Modeling_Tool/WOE/plot_woe_tool.py +290 -0
- Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
- Modeling_Tool/__init__.py +176 -0
- Report/Report_Tool.py +380 -0
- Report/__init__.py +3 -0
- supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
- supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
- supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
- supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
- 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
|