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,1452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model Evaluation Tool - Optimized Version
|
|
3
|
+
|
|
4
|
+
This module provides classes and functions for model evaluation, performance comparison,
|
|
5
|
+
and gains analysis. It includes utilities for calculating various metrics and generating
|
|
6
|
+
cross-risk summaries.
|
|
7
|
+
|
|
8
|
+
Author: Matrix Agent
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import numpy as np
|
|
13
|
+
import inspect
|
|
14
|
+
from typing import List, Dict, Optional, Callable, Any, Union
|
|
15
|
+
from .Model_Eval_Tool import get_perf_summary, get_gains_table, get_gains_table_by_cust_metrics, cross_risk
|
|
16
|
+
import logging
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class EvaluationPipeline:
|
|
20
|
+
"""
|
|
21
|
+
链式调用的流水线对象,支持 .group_by() 和 .subset_by() 链式添加条件,
|
|
22
|
+
最后通过 .apply(func) 执行。
|
|
23
|
+
支持 func 返回 pandas.DataFrame 或 dict(值为 DataFrame)。
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, m_eval, data=None):
|
|
26
|
+
self.m_eval = m_eval
|
|
27
|
+
self.original_data = m_eval.data if data is None else data
|
|
28
|
+
self.steps = []
|
|
29
|
+
self._expected_columns = None # 用于 DataFrame 返回时的列名缓存
|
|
30
|
+
self._expected_dict_keys = None # 用于 dict 返回时的键缓存
|
|
31
|
+
|
|
32
|
+
def group_by(self, group_name, min_size=100, group_var_name=None):
|
|
33
|
+
if group_var_name is None:
|
|
34
|
+
group_var_name = group_name
|
|
35
|
+
self.steps.append({
|
|
36
|
+
'type': 'group',
|
|
37
|
+
'group_name': group_name,
|
|
38
|
+
'group_var_name': group_var_name,
|
|
39
|
+
'min_size': min_size
|
|
40
|
+
})
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def subset_by(self, condition_dict, name='eval_subset', min_size=100):
|
|
44
|
+
self.steps.append({
|
|
45
|
+
'type': 'subset',
|
|
46
|
+
'condition_dict': condition_dict,
|
|
47
|
+
'subset_var_name': name,
|
|
48
|
+
'min_size': min_size
|
|
49
|
+
})
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def apply(self, func, **kwargs):
|
|
53
|
+
|
|
54
|
+
import inspect
|
|
55
|
+
sig = inspect.signature(func)
|
|
56
|
+
|
|
57
|
+
def _execute(step_idx, current_data, current_meta):
|
|
58
|
+
if step_idx >= len(self.steps):
|
|
59
|
+
old_data = self.m_eval.data
|
|
60
|
+
self.m_eval.data = current_data
|
|
61
|
+
try:
|
|
62
|
+
# 自动注入当前数据(如果函数接受 'current_data' 参数)
|
|
63
|
+
if 'current_data' in sig.parameters:
|
|
64
|
+
kwargs['current_data'] = current_data
|
|
65
|
+
res = func(**kwargs)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.info(f"Error in group {current_meta}: {e}")
|
|
68
|
+
if self._expected_dict_keys is not None:
|
|
69
|
+
res = {k: pd.DataFrame() for k in self._expected_dict_keys}
|
|
70
|
+
else:
|
|
71
|
+
res = pd.DataFrame()
|
|
72
|
+
finally:
|
|
73
|
+
self.m_eval.data = old_data
|
|
74
|
+
|
|
75
|
+
# (处理 dict 和 DataFrame 的逻辑)
|
|
76
|
+
if isinstance(res, dict):
|
|
77
|
+
if self._expected_dict_keys is None and res:
|
|
78
|
+
self._expected_dict_keys = list(res.keys())
|
|
79
|
+
for key, df in res.items():
|
|
80
|
+
if df is not None and not df.empty:
|
|
81
|
+
for col_name, col_val in current_meta.items():
|
|
82
|
+
df[col_name] = col_val
|
|
83
|
+
else:
|
|
84
|
+
empty_df = pd.DataFrame()
|
|
85
|
+
for col_name, col_val in current_meta.items():
|
|
86
|
+
empty_df[col_name] = [col_val]
|
|
87
|
+
res[key] = empty_df
|
|
88
|
+
return res
|
|
89
|
+
else:
|
|
90
|
+
if self._expected_columns is None and res is not None and not res.empty:
|
|
91
|
+
self._expected_columns = res.columns.tolist()
|
|
92
|
+
if res is not None and not res.empty:
|
|
93
|
+
res = res.copy()
|
|
94
|
+
for key, val in current_meta.items():
|
|
95
|
+
res[key] = val
|
|
96
|
+
else:
|
|
97
|
+
if self._expected_columns is not None:
|
|
98
|
+
res = pd.DataFrame(columns=self._expected_columns)
|
|
99
|
+
else:
|
|
100
|
+
res = pd.DataFrame()
|
|
101
|
+
return res
|
|
102
|
+
|
|
103
|
+
step = self.steps[step_idx]
|
|
104
|
+
if step['type'] == 'group':
|
|
105
|
+
group_name = step['group_name']
|
|
106
|
+
group_var_name = step['group_var_name']
|
|
107
|
+
min_size = step['min_size']
|
|
108
|
+
results = []
|
|
109
|
+
for gval in current_data[group_name].unique():
|
|
110
|
+
sub_data = current_data[current_data[group_name] == gval]
|
|
111
|
+
if len(sub_data) >= min_size:
|
|
112
|
+
new_meta = current_meta.copy()
|
|
113
|
+
new_meta[group_var_name] = gval
|
|
114
|
+
sub_res = _execute(step_idx + 1, sub_data, new_meta)
|
|
115
|
+
if sub_res is not None:
|
|
116
|
+
results.append(sub_res)
|
|
117
|
+
if not results:
|
|
118
|
+
if self._expected_dict_keys is not None:
|
|
119
|
+
return {k: pd.DataFrame() for k in self._expected_dict_keys}
|
|
120
|
+
else:
|
|
121
|
+
return pd.DataFrame()
|
|
122
|
+
# 合并结果
|
|
123
|
+
if isinstance(results[0], dict):
|
|
124
|
+
merged = {k: pd.concat([r[k] for r in results], ignore_index=True) for k in results[0].keys()}
|
|
125
|
+
return merged
|
|
126
|
+
else:
|
|
127
|
+
return pd.concat(results, ignore_index=True)
|
|
128
|
+
|
|
129
|
+
elif step['type'] == 'subset':
|
|
130
|
+
condition_dict = step['condition_dict']
|
|
131
|
+
subset_var_name = step['subset_var_name']
|
|
132
|
+
min_size = step['min_size']
|
|
133
|
+
results = []
|
|
134
|
+
for label, query in condition_dict.items():
|
|
135
|
+
if query == "":
|
|
136
|
+
sub_data = current_data.copy()
|
|
137
|
+
else:
|
|
138
|
+
sub_data = current_data.query(query)
|
|
139
|
+
if len(sub_data) >= min_size:
|
|
140
|
+
new_meta = current_meta.copy()
|
|
141
|
+
new_meta[subset_var_name] = label
|
|
142
|
+
sub_res = _execute(step_idx + 1, sub_data, new_meta)
|
|
143
|
+
if sub_res is not None:
|
|
144
|
+
results.append(sub_res)
|
|
145
|
+
if not results:
|
|
146
|
+
if self._expected_dict_keys is not None:
|
|
147
|
+
return {k: pd.DataFrame() for k in self._expected_dict_keys}
|
|
148
|
+
else:
|
|
149
|
+
return pd.DataFrame()
|
|
150
|
+
if isinstance(results[0], dict):
|
|
151
|
+
merged = {k: pd.concat([r[k] for r in results], ignore_index=True) for k in results[0].keys()}
|
|
152
|
+
return merged
|
|
153
|
+
else:
|
|
154
|
+
return pd.concat(results, ignore_index=True)
|
|
155
|
+
|
|
156
|
+
return _execute(0, self.original_data, {})
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Utility_Functions:
|
|
160
|
+
"""Utility functions for data processing and calculations."""
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def valid_average(x):
|
|
164
|
+
"""
|
|
165
|
+
Calculate the average of positive values in a Series.
|
|
166
|
+
|
|
167
|
+
This function filters out non-positive values (including zeros and negatives)
|
|
168
|
+
before calculating the mean, useful for financial or count data where
|
|
169
|
+
zero or negative values should not contribute to the average.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
x : pandas.Series
|
|
174
|
+
Input series of numeric values.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
float
|
|
179
|
+
Rounded average of positive values, or 0 if no valid values exist.
|
|
180
|
+
|
|
181
|
+
Examples
|
|
182
|
+
--------
|
|
183
|
+
>>> import pandas as pd
|
|
184
|
+
>>> Utility_Functions.valid_average(pd.Series([1, 2, -1, 0, 3]))
|
|
185
|
+
2.0
|
|
186
|
+
"""
|
|
187
|
+
valid_mask = x > 0
|
|
188
|
+
valid_count = valid_mask.sum()
|
|
189
|
+
valid_values = x[valid_mask]
|
|
190
|
+
value_sum = valid_values.sum()
|
|
191
|
+
|
|
192
|
+
if valid_count > 0:
|
|
193
|
+
return round(value_sum / valid_count, 2)
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Model_Evaluation_Tool:
|
|
198
|
+
"""
|
|
199
|
+
A comprehensive tool for model evaluation, performance comparison, and gains analysis.
|
|
200
|
+
|
|
201
|
+
This class provides methods for:
|
|
202
|
+
- Base score calculation and correlation analysis
|
|
203
|
+
- Model performance comparison across multiple metrics
|
|
204
|
+
- Gains table generation and analysis
|
|
205
|
+
- Custom metric evaluation
|
|
206
|
+
- Cross-risk analysis
|
|
207
|
+
- Multi-dimensional evaluation with subsets and groupings
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
data : pandas.DataFrame
|
|
212
|
+
Input data containing features and target variables.
|
|
213
|
+
dep : str
|
|
214
|
+
Name of the dependent/target variable.
|
|
215
|
+
comp_scrlist : list
|
|
216
|
+
List of comparison score column names.
|
|
217
|
+
model : object, optional
|
|
218
|
+
Trained model with predict_proba method. Default is None.
|
|
219
|
+
base_score : str, optional
|
|
220
|
+
Name of the base score column. Default is None.
|
|
221
|
+
min_data_size : int, optional
|
|
222
|
+
Minimum data size threshold. Default is 500.
|
|
223
|
+
equal_freq : bool, optional
|
|
224
|
+
Whether to use equal frequency binning. Default is True.
|
|
225
|
+
precision : int, optional
|
|
226
|
+
Decimal precision for rounding. Default is 5.
|
|
227
|
+
chi2_method : bool, optional
|
|
228
|
+
Whether to use chi-square method. Default is False.
|
|
229
|
+
chi2_p : float, optional
|
|
230
|
+
Chi-square p-value threshold. Default is 0.999.
|
|
231
|
+
init_equi_bins : int, optional
|
|
232
|
+
Initial number of equal frequency bins. Default is 500.
|
|
233
|
+
tree_binning : bool, optional
|
|
234
|
+
Whether to use tree-based binning. Default is False.
|
|
235
|
+
random_seed : int, optional
|
|
236
|
+
Random seed for reproducibility. Default is 3407.
|
|
237
|
+
nbins : int, optional
|
|
238
|
+
Number of bins for analysis. Default is 10.
|
|
239
|
+
min_bin_prop : float, optional
|
|
240
|
+
Minimum proportion for each bin. Default is 0.05.
|
|
241
|
+
include_missing : bool, optional
|
|
242
|
+
Whether to include missing values. Default is True.
|
|
243
|
+
missing_rate_ref : int or float, optional
|
|
244
|
+
Reference value for missing rate. Default is -99999999.
|
|
245
|
+
excel_path : str, optional
|
|
246
|
+
Path for Excel output. Default is None.
|
|
247
|
+
|
|
248
|
+
Attributes
|
|
249
|
+
----------
|
|
250
|
+
data : pandas.DataFrame
|
|
251
|
+
The input data (may be modified during processing).
|
|
252
|
+
dep : str
|
|
253
|
+
Target variable name.
|
|
254
|
+
base_score : str or None
|
|
255
|
+
Name of the base score column.
|
|
256
|
+
comp_scrlist : list
|
|
257
|
+
List of comparison score names.
|
|
258
|
+
eval_metrics : list
|
|
259
|
+
List of evaluation metrics.
|
|
260
|
+
eval_ylabels : list
|
|
261
|
+
Labels for y-axis evaluation.
|
|
262
|
+
gains_display_metric_list : list
|
|
263
|
+
Metrics to display in gains summary.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(
|
|
267
|
+
self,
|
|
268
|
+
data: pd.DataFrame,
|
|
269
|
+
dep: str,
|
|
270
|
+
comp_scrlist: List[str],
|
|
271
|
+
model: Any = None,
|
|
272
|
+
base_score: Optional[str] = None,
|
|
273
|
+
min_data_size: int = 500,
|
|
274
|
+
equal_freq: bool = True,
|
|
275
|
+
precision: int = 5,
|
|
276
|
+
chi2_method: bool = False,
|
|
277
|
+
chi2_p: float = 0.999,
|
|
278
|
+
init_equi_bins: int = 500,
|
|
279
|
+
tree_binning: bool = False,
|
|
280
|
+
random_seed: int = 3407,
|
|
281
|
+
nbins: int = 10,
|
|
282
|
+
min_bin_prop: float = 0.05,
|
|
283
|
+
include_missing: bool = True,
|
|
284
|
+
missing_rate_ref: Union[int, float] = -99999999,
|
|
285
|
+
excel_path: Optional[str] = None,
|
|
286
|
+
fillna = -999999,
|
|
287
|
+
spec_values = []
|
|
288
|
+
):
|
|
289
|
+
"""
|
|
290
|
+
Initialize the Model_Evaluation_Tool with configuration parameters.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
data : pandas.DataFrame
|
|
295
|
+
Input dataset for evaluation.
|
|
296
|
+
dep : str
|
|
297
|
+
Name of the target/dependent variable.
|
|
298
|
+
comp_scrlist : list
|
|
299
|
+
List of comparison score column names.
|
|
300
|
+
model : object, optional
|
|
301
|
+
Trained model with predict_proba method.
|
|
302
|
+
base_score : str, optional
|
|
303
|
+
Base score column name.
|
|
304
|
+
min_data_size : int, optional
|
|
305
|
+
Minimum data size threshold.
|
|
306
|
+
equal_freq : bool, optional
|
|
307
|
+
Use equal frequency binning if True.
|
|
308
|
+
precision : int, optional
|
|
309
|
+
Decimal precision for calculations.
|
|
310
|
+
chi2_method : bool, optional
|
|
311
|
+
Use chi-square method if True.
|
|
312
|
+
chi2_p : float, optional
|
|
313
|
+
Chi-square p-value threshold.
|
|
314
|
+
init_equi_bins : int, optional
|
|
315
|
+
Initial equal bins count.
|
|
316
|
+
tree_binning : bool, optional
|
|
317
|
+
Use tree-based binning if True.
|
|
318
|
+
random_seed : int, optional
|
|
319
|
+
Random seed value.
|
|
320
|
+
nbins : int, optional
|
|
321
|
+
Number of bins.
|
|
322
|
+
min_bin_prop : float, optional
|
|
323
|
+
Minimum bin proportion.
|
|
324
|
+
include_missing : bool, optional
|
|
325
|
+
Include missing values if True.
|
|
326
|
+
missing_rate_ref : int or float, optional
|
|
327
|
+
Missing rate reference value.
|
|
328
|
+
excel_path : str, optional
|
|
329
|
+
Path for Excel output file.
|
|
330
|
+
"""
|
|
331
|
+
self.data = data
|
|
332
|
+
self.dep = dep
|
|
333
|
+
self.model = model
|
|
334
|
+
self.comp_scrlist = comp_scrlist
|
|
335
|
+
self.base_score = base_score
|
|
336
|
+
self.min_data_size = min_data_size
|
|
337
|
+
self.equal_freq = equal_freq
|
|
338
|
+
self.precision = precision
|
|
339
|
+
self.nbins = nbins
|
|
340
|
+
self.min_bin_prop = min_bin_prop
|
|
341
|
+
self.include_missing = include_missing
|
|
342
|
+
self.tree_binning = tree_binning
|
|
343
|
+
self.seed = random_seed
|
|
344
|
+
self.excel_path = excel_path
|
|
345
|
+
self.missing_rate_ref = missing_rate_ref
|
|
346
|
+
self.fillna = fillna
|
|
347
|
+
self.spec_values = spec_values
|
|
348
|
+
|
|
349
|
+
self.chi2_method = chi2_method
|
|
350
|
+
self.chi2_p = chi2_p
|
|
351
|
+
self.init_equi_bins = init_equi_bins
|
|
352
|
+
|
|
353
|
+
self.eval_metrics = [
|
|
354
|
+
'age',
|
|
355
|
+
'monthlyincome',
|
|
356
|
+
'education',
|
|
357
|
+
'credit_limit'
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
self.udf = Utility_Functions()
|
|
361
|
+
self.cross_agg_dict = {
|
|
362
|
+
'is_dpd7': ['count', lambda x: (x.sum() / x.count()).round(4)],
|
|
363
|
+
'credit_limit': ['count', lambda x: self.udf.valid_average(x)],
|
|
364
|
+
'monthlyincome': ['count', lambda x: self.udf.valid_average(x)],
|
|
365
|
+
'education': ['count', lambda x: self.udf.valid_average(x)],
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
self.subset_condition_dict = {
|
|
369
|
+
"Overall": "",
|
|
370
|
+
"AE": "aprvvrsn_2 in ('AE')",
|
|
371
|
+
"NON_AE": "aprvvrsn_2 not in ('AE')",
|
|
372
|
+
"FT": "aprvvrsn_2 in ('FT2.0')",
|
|
373
|
+
"NON_FT": "aprvvrsn_2 not in ('FT2.0')",
|
|
374
|
+
"NON_FT_AE": "aprvvrsn_2 not in ('FT2.0') and aprvvrsn_2 not in ('AE')"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
self.eval_ylabels = ['is_dpd7']
|
|
378
|
+
self.grp_namelist = ['sample_ind_fnl', 'aprvvrsn_2', 'week_start_date']
|
|
379
|
+
self.gains_display_metric_list = ['MIN', 'MAX', 'N', 'PROP', 'AVG_SCORE', 'AVG_BAD', 'CUM_BAD_PCT', 'KS_PER_BIN', 'LIFT', 'RANK_ORDER_BUMP']
|
|
380
|
+
|
|
381
|
+
def __init_excel_master(self):
|
|
382
|
+
"""
|
|
383
|
+
Initialize Excel Master for report generation.
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
ExcelMaster
|
|
388
|
+
Initialized ExcelMaster instance.
|
|
389
|
+
|
|
390
|
+
Notes
|
|
391
|
+
-----
|
|
392
|
+
Requires ExcelMaster package to be installed.
|
|
393
|
+
"""
|
|
394
|
+
from ExcelMaster.ExcelMaster import ExcelMaster
|
|
395
|
+
|
|
396
|
+
em = ExcelMaster(filepath=self.excel_path, verbose=False)
|
|
397
|
+
return em
|
|
398
|
+
|
|
399
|
+
def get_base_score(self, scorename: str = '_base_model_score_', disp: bool = False):
|
|
400
|
+
"""
|
|
401
|
+
Calculate base model scores using the provided model.
|
|
402
|
+
|
|
403
|
+
This method uses the model's predict_proba method to generate scores
|
|
404
|
+
for the input data and optionally displays correlation analysis.
|
|
405
|
+
|
|
406
|
+
Parameters
|
|
407
|
+
----------
|
|
408
|
+
scorename : str, optional
|
|
409
|
+
Name for the score column. Default is '_base_model_score_'.
|
|
410
|
+
disp : bool, optional
|
|
411
|
+
Whether to display intermediate results. Default is False.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
Model_Evaluation_Tool
|
|
416
|
+
Returns self for method chaining.
|
|
417
|
+
|
|
418
|
+
Raises
|
|
419
|
+
------
|
|
420
|
+
AttributeError
|
|
421
|
+
If model is None or lacks required attributes.
|
|
422
|
+
"""
|
|
423
|
+
if self.model is None:
|
|
424
|
+
raise AttributeError("Model must be provided to calculate base score.")
|
|
425
|
+
|
|
426
|
+
data = self.data.copy()
|
|
427
|
+
model = self.model
|
|
428
|
+
|
|
429
|
+
inputs = model.feature_names_in_.tolist()
|
|
430
|
+
data[scorename] = model.predict_proba(data[inputs])[:, 1]
|
|
431
|
+
self.data = data
|
|
432
|
+
self.base_score = scorename
|
|
433
|
+
|
|
434
|
+
scrlist = [scorename] + self.comp_scrlist
|
|
435
|
+
if disp:
|
|
436
|
+
from IPython.display import display
|
|
437
|
+
from Modeling_Tool.Feature.Distribution_Tool import proc_means_by_grp
|
|
438
|
+
display(proc_means_by_grp(data, scrlist))
|
|
439
|
+
|
|
440
|
+
tmp_data = data[data[scrlist] > 0]
|
|
441
|
+
|
|
442
|
+
if disp:
|
|
443
|
+
from IPython.display import display
|
|
444
|
+
display(tmp_data[scrlist].corr())
|
|
445
|
+
|
|
446
|
+
return self
|
|
447
|
+
|
|
448
|
+
def get_score_correlation(
|
|
449
|
+
self,
|
|
450
|
+
score_list: Optional[List[str]] = None,
|
|
451
|
+
method: str = 'pearson'
|
|
452
|
+
) -> pd.DataFrame:
|
|
453
|
+
"""
|
|
454
|
+
Calculate correlation matrix between score columns.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
score_list : list, optional
|
|
459
|
+
List of score column names. If None, uses base_score and comp_scrlist.
|
|
460
|
+
method : str, optional
|
|
461
|
+
Correlation method ('pearson', 'spearman', 'kendall'). Default is 'pearson'.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
pandas.DataFrame
|
|
466
|
+
Correlation summary in long format with columns: 'base', 'compare', 'corr'.
|
|
467
|
+
"""
|
|
468
|
+
if score_list is None:
|
|
469
|
+
score_list = [self.base_score] + self.comp_scrlist
|
|
470
|
+
|
|
471
|
+
if self.base_score is None:
|
|
472
|
+
raise ValueError("Base score must be set before calculating correlation.")
|
|
473
|
+
|
|
474
|
+
data = self.data.copy()
|
|
475
|
+
|
|
476
|
+
score_query = " and ".join([s + " > 0" for s in score_list])
|
|
477
|
+
data = data.query(score_query)
|
|
478
|
+
|
|
479
|
+
corr_summary = data[score_list]\
|
|
480
|
+
.corr(method=method)\
|
|
481
|
+
.reset_index(drop=False)\
|
|
482
|
+
.melt(id_vars=['index'], var_name='compare', value_name='corr')\
|
|
483
|
+
.rename(columns={"index": "base"})
|
|
484
|
+
|
|
485
|
+
return corr_summary
|
|
486
|
+
|
|
487
|
+
def model_perf_compare(
|
|
488
|
+
self,
|
|
489
|
+
data: Optional[pd.DataFrame] = None,
|
|
490
|
+
grp_name: Optional[str] = None,
|
|
491
|
+
dist_bins: int = 100,
|
|
492
|
+
pct_bins: int = 10,
|
|
493
|
+
min_data_size: int = 50,
|
|
494
|
+
sync_data_size: bool = True
|
|
495
|
+
) -> pd.DataFrame:
|
|
496
|
+
"""
|
|
497
|
+
Compare model performance across different scores.
|
|
498
|
+
|
|
499
|
+
This method calculates performance metrics (AUC, KS, etc.) for the base score
|
|
500
|
+
and all comparison scores, optionally filtering by group.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
data : pandas.DataFrame, optional
|
|
505
|
+
Data to use. If None, uses self.data.
|
|
506
|
+
grp_name : str, optional
|
|
507
|
+
Group name column for stratification.
|
|
508
|
+
dist_bins : int, optional
|
|
509
|
+
Number of distribution bins. Default is 100.
|
|
510
|
+
pct_bins : int, optional
|
|
511
|
+
Number of percentile bins. Default is 10.
|
|
512
|
+
min_data_size : int, optional
|
|
513
|
+
Minimum data size per bin. Default is 50.
|
|
514
|
+
sync_data_size : bool, optional
|
|
515
|
+
Whether to filter out zero/negative scores. Default is True.
|
|
516
|
+
|
|
517
|
+
Returns
|
|
518
|
+
-------
|
|
519
|
+
pandas.DataFrame
|
|
520
|
+
Performance comparison results sorted by score order.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
data = self.data.copy() if data is None else data.copy()
|
|
524
|
+
score_list = self.comp_scrlist
|
|
525
|
+
dep = self.dep
|
|
526
|
+
base_score = self.base_score
|
|
527
|
+
precision = self.precision
|
|
528
|
+
|
|
529
|
+
# 过滤掉无效分数和目标值
|
|
530
|
+
valid_mask = (
|
|
531
|
+
data[dep].notna() &
|
|
532
|
+
data[base_score].notna() &
|
|
533
|
+
np.isfinite(data[base_score])
|
|
534
|
+
)
|
|
535
|
+
clean_data = data[valid_mask]
|
|
536
|
+
|
|
537
|
+
if len(clean_data) == 0:
|
|
538
|
+
return pd.DataFrame() # 返回空结果
|
|
539
|
+
# 后续使用 clean_data 进行计算
|
|
540
|
+
|
|
541
|
+
oot_df = clean_data.copy()
|
|
542
|
+
sample_name = 'oot'
|
|
543
|
+
oot_df_ = clean_data.copy()
|
|
544
|
+
|
|
545
|
+
if sync_data_size:
|
|
546
|
+
if len(score_list) > 0:
|
|
547
|
+
score_query = " and ".join([s + " > 0" for s in score_list])
|
|
548
|
+
oot_df_ = oot_df.query(score_query) if oot_df is not None else None
|
|
549
|
+
|
|
550
|
+
perf_comp_dict = {}
|
|
551
|
+
|
|
552
|
+
if base_score is not None:
|
|
553
|
+
base_perf = get_perf_summary(
|
|
554
|
+
train=None,
|
|
555
|
+
validation=None,
|
|
556
|
+
oot=oot_df_.query(f"{base_score} > 0"),
|
|
557
|
+
tgt_name=dep,
|
|
558
|
+
scr_name=base_score,
|
|
559
|
+
to_show=False,
|
|
560
|
+
display=False,
|
|
561
|
+
dist_bins=dist_bins,
|
|
562
|
+
pct_bins=pct_bins,
|
|
563
|
+
precision=precision,
|
|
564
|
+
min_bin_prop=0.05,
|
|
565
|
+
include_missing=False,
|
|
566
|
+
equal_freq=True,
|
|
567
|
+
oot_grp_name=grp_name,
|
|
568
|
+
min_data_size=min_data_size
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if not base_perf.empty and 'index' in base_perf.columns:
|
|
572
|
+
base_perf = base_perf.query(f"index == '{sample_name}'")
|
|
573
|
+
|
|
574
|
+
perf_comp_dict[base_score] = base_perf
|
|
575
|
+
|
|
576
|
+
if len(score_list) > 0:
|
|
577
|
+
for score in score_list:
|
|
578
|
+
"""Loop Score List."""
|
|
579
|
+
perf_comp_dict[score] = get_perf_summary(
|
|
580
|
+
train=None,
|
|
581
|
+
validation=None,
|
|
582
|
+
oot=oot_df_.query(f"{score} > 0"),
|
|
583
|
+
scr_name=score,
|
|
584
|
+
tgt_name=dep,
|
|
585
|
+
to_show=False,
|
|
586
|
+
display=False,
|
|
587
|
+
dist_bins=dist_bins,
|
|
588
|
+
pct_bins=pct_bins,
|
|
589
|
+
precision=precision,
|
|
590
|
+
min_bin_prop=0.05,
|
|
591
|
+
include_missing=False,
|
|
592
|
+
equal_freq=True,
|
|
593
|
+
oot_grp_name=grp_name,
|
|
594
|
+
min_data_size=min_data_size
|
|
595
|
+
).query(f"index == '{sample_name}'")
|
|
596
|
+
|
|
597
|
+
if not perf_comp_dict:
|
|
598
|
+
return pd.DataFrame()
|
|
599
|
+
|
|
600
|
+
perf_comp_res = []
|
|
601
|
+
for score_name, perf in perf_comp_dict.items():
|
|
602
|
+
perf = perf.copy()
|
|
603
|
+
perf['score_name'] = score_name
|
|
604
|
+
perf_comp_res.append(perf)
|
|
605
|
+
|
|
606
|
+
perf_comp_res = pd.concat(perf_comp_res)
|
|
607
|
+
|
|
608
|
+
drop_cols = ['AUC_Shift', 'KS_Shift']
|
|
609
|
+
for col in drop_cols:
|
|
610
|
+
if col in perf_comp_res.columns:
|
|
611
|
+
perf_comp_res = perf_comp_res.drop(columns=[col])
|
|
612
|
+
|
|
613
|
+
custom_order = [base_score] + score_list if base_score else score_list
|
|
614
|
+
order_map = {val: i for i, val in enumerate(custom_order) if val in perf_comp_dict}
|
|
615
|
+
perf_comp_res['sort_key'] = perf_comp_res['score_name'].map(order_map)
|
|
616
|
+
perf_comp_res_sorted = perf_comp_res.sort_values('sort_key').drop('sort_key', axis=1)
|
|
617
|
+
|
|
618
|
+
return perf_comp_res_sorted
|
|
619
|
+
|
|
620
|
+
def get_gains_summary(
|
|
621
|
+
self,
|
|
622
|
+
data: Optional[pd.DataFrame] = None,
|
|
623
|
+
grp_name: Optional[str] = None,
|
|
624
|
+
disp: bool = True,
|
|
625
|
+
grp_disp_metric: List[str] = None,
|
|
626
|
+
grp_nbins: int = 5,
|
|
627
|
+
withSummary: bool = True,
|
|
628
|
+
add_func: Optional[Callable] = None,
|
|
629
|
+
sync_range: bool = True,
|
|
630
|
+
spec_values = [],
|
|
631
|
+
include_missing = False,
|
|
632
|
+
fillna = None
|
|
633
|
+
) -> pd.DataFrame:
|
|
634
|
+
"""
|
|
635
|
+
Generate gains summary table for score analysis.
|
|
636
|
+
|
|
637
|
+
This method creates gains tables showing the distribution of targets
|
|
638
|
+
across score bins, with optional group stratification.
|
|
639
|
+
|
|
640
|
+
Parameters
|
|
641
|
+
----------
|
|
642
|
+
data : pandas.DataFrame, optional
|
|
643
|
+
Data to use. If None, uses self.data.
|
|
644
|
+
grp_name : str, optional
|
|
645
|
+
Group name for stratified analysis.
|
|
646
|
+
disp : bool, optional
|
|
647
|
+
Whether to display results. Default is True.
|
|
648
|
+
grp_disp_metric : list, optional
|
|
649
|
+
Metrics to display for grouped analysis.
|
|
650
|
+
grp_nbins : int, optional
|
|
651
|
+
Number of bins for grouped analysis. Default is 5.
|
|
652
|
+
withSummary : bool, optional
|
|
653
|
+
Include summary row. Default is True.
|
|
654
|
+
add_func : callable, optional
|
|
655
|
+
Additional function to apply to gains table.
|
|
656
|
+
sync_range : bool, optional
|
|
657
|
+
Synchronize bin ranges. Default is True.
|
|
658
|
+
|
|
659
|
+
Returns
|
|
660
|
+
-------
|
|
661
|
+
pandas.DataFrame
|
|
662
|
+
Gains summary results with score names.
|
|
663
|
+
"""
|
|
664
|
+
if grp_disp_metric is None:
|
|
665
|
+
grp_disp_metric = ['N', 'PROP', 'AVG_BAD', 'LIFT']
|
|
666
|
+
|
|
667
|
+
if fillna is None:
|
|
668
|
+
fillna = self.fillna
|
|
669
|
+
|
|
670
|
+
new_oot_df = self.data.copy() if data is None else data.copy()
|
|
671
|
+
|
|
672
|
+
dep = self.dep
|
|
673
|
+
tree_binning = self.tree_binning
|
|
674
|
+
digit = self.precision
|
|
675
|
+
base_score = self.base_score
|
|
676
|
+
equal_freq = self.equal_freq
|
|
677
|
+
min_data_size = self.min_data_size
|
|
678
|
+
nbins = self.nbins if grp_name is None else grp_nbins
|
|
679
|
+
min_bin_prop = self.min_bin_prop
|
|
680
|
+
|
|
681
|
+
score_list = [base_score] + self.comp_scrlist
|
|
682
|
+
display_metric_list = self.gains_display_metric_list if add_func is None else None
|
|
683
|
+
|
|
684
|
+
if self.data is None or len(self.data) == 0:
|
|
685
|
+
empty_df = pd.DataFrame()
|
|
686
|
+
empty_df.index = pd.MultiIndex.from_tuples([], names=['_bin_num', '_bin_range'])
|
|
687
|
+
return empty_df # 或返回空结果
|
|
688
|
+
|
|
689
|
+
gains_table_dict = {}
|
|
690
|
+
for score in score_list:
|
|
691
|
+
if grp_name is None:
|
|
692
|
+
### Overall Gains
|
|
693
|
+
gains_table_dict[score] = get_gains_table(
|
|
694
|
+
# data=new_oot_df.query(f"{score} > 0"),
|
|
695
|
+
data=new_oot_df,
|
|
696
|
+
dep=dep,
|
|
697
|
+
ascending=True,
|
|
698
|
+
tree_binning=tree_binning,
|
|
699
|
+
nbins=nbins,
|
|
700
|
+
precision=digit,
|
|
701
|
+
min_bin_prop=min_bin_prop,
|
|
702
|
+
include_missing=False,
|
|
703
|
+
score=score,
|
|
704
|
+
fillna = fillna,
|
|
705
|
+
equal_freq=equal_freq,
|
|
706
|
+
withSummary=withSummary,
|
|
707
|
+
add_func=add_func,
|
|
708
|
+
spec_values=spec_values
|
|
709
|
+
)
|
|
710
|
+
if display_metric_list is not None:
|
|
711
|
+
gains_table_dict[score] = gains_table_dict[score][display_metric_list]
|
|
712
|
+
gains_table_dict[score] = gains_table_dict[score].reset_index(drop=False)
|
|
713
|
+
|
|
714
|
+
else:
|
|
715
|
+
if new_oot_df.query(f"{score} > 0").shape[0] > 10:
|
|
716
|
+
oot_by_group = get_gains_table(
|
|
717
|
+
data=new_oot_df,
|
|
718
|
+
dep=dep,
|
|
719
|
+
ascending=True,
|
|
720
|
+
tree_binning=tree_binning,
|
|
721
|
+
nbins=grp_nbins,
|
|
722
|
+
precision=digit,
|
|
723
|
+
min_bin_prop=min_bin_prop,
|
|
724
|
+
include_missing=False,
|
|
725
|
+
score=score,
|
|
726
|
+
fillna = fillna,
|
|
727
|
+
equal_freq=equal_freq,
|
|
728
|
+
sync_range=sync_range,
|
|
729
|
+
retSummary=False,
|
|
730
|
+
grp_name=grp_name,
|
|
731
|
+
min_data_size=min_data_size,
|
|
732
|
+
withSummary=withSummary,
|
|
733
|
+
spec_values=spec_values,
|
|
734
|
+
add_func=add_func
|
|
735
|
+
)
|
|
736
|
+
if display_metric_list is not None:
|
|
737
|
+
oot_by_group = oot_by_group[[*display_metric_list, grp_name]]
|
|
738
|
+
oot_by_group = oot_by_group.reset_index(drop=False)
|
|
739
|
+
|
|
740
|
+
grp_metric_dict = {}
|
|
741
|
+
for value in [x for x in grp_disp_metric if x not in ['PROP']]:
|
|
742
|
+
grp_metric_dict[value] = oot_by_group\
|
|
743
|
+
.reset_index(drop=False)\
|
|
744
|
+
.pivot_table(index=['_bin_num', '_bin_range'], columns=[grp_name], values=value, margins=True, margins_name="Grand_Total")
|
|
745
|
+
|
|
746
|
+
if 'PROP' in grp_disp_metric:
|
|
747
|
+
tot_cnt = oot_by_group\
|
|
748
|
+
.reset_index(drop=False)\
|
|
749
|
+
.pivot_table(index=['_bin_num', '_bin_range'], columns=[grp_name], values=['N'], margins=True, margins_name="Grand_Total", aggfunc='sum')
|
|
750
|
+
|
|
751
|
+
if tot_cnt.shape[0] > 0:
|
|
752
|
+
grp_metric_dict['PROP'] = tot_cnt.iloc[0:tot_cnt.shape[0]:] / np.array(tot_cnt.iloc[-1:].T['Grand_Total'].tolist())
|
|
753
|
+
grp_metric_dict['PROP'].columns = grp_metric_dict['PROP'].rename(columns={'N': ''}).columns.map("".join)
|
|
754
|
+
|
|
755
|
+
fnl_grp_gains_res = []
|
|
756
|
+
for colname, gains in grp_metric_dict.items():
|
|
757
|
+
gains = gains.copy()
|
|
758
|
+
gains['variable'] = colname
|
|
759
|
+
# gains = gains.reset_index(drop=False)
|
|
760
|
+
if disp:
|
|
761
|
+
from IPython.display import display
|
|
762
|
+
display(gains)
|
|
763
|
+
fnl_grp_gains_res.append(gains)
|
|
764
|
+
fnl_grp_gains_res = pd.concat(fnl_grp_gains_res)
|
|
765
|
+
|
|
766
|
+
gains_table_dict[score] = oot_by_group
|
|
767
|
+
|
|
768
|
+
fnl_gains_res = []
|
|
769
|
+
for score_name, gains_res in gains_table_dict.items():
|
|
770
|
+
gains_res = gains_res.copy()
|
|
771
|
+
gains_res['score_name'] = score_name
|
|
772
|
+
fnl_gains_res.append(gains_res)
|
|
773
|
+
|
|
774
|
+
fnl_gains_res = pd.concat(fnl_gains_res)
|
|
775
|
+
|
|
776
|
+
return fnl_gains_res
|
|
777
|
+
|
|
778
|
+
def get_cust_metric_summary(
|
|
779
|
+
self,
|
|
780
|
+
eval_metrics: Optional[List[str]] = None,
|
|
781
|
+
metric_agg_func: Union[str, Callable] = 'mean',
|
|
782
|
+
withSummary: bool = True,
|
|
783
|
+
spec_values = []
|
|
784
|
+
) -> pd.DataFrame:
|
|
785
|
+
"""
|
|
786
|
+
Generate summary for custom evaluation metrics.
|
|
787
|
+
|
|
788
|
+
This method calculates gains tables for custom metrics defined in
|
|
789
|
+
self.eval_metrics, allowing analysis of various feature distributions
|
|
790
|
+
across score bins.
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
eval_metrics : list, optional
|
|
795
|
+
List of metrics to evaluate. If None, uses self.eval_metrics.
|
|
796
|
+
metric_agg_func : str or callable, optional
|
|
797
|
+
Aggregation function for metrics. Default is 'mean'.
|
|
798
|
+
withSummary : bool, optional
|
|
799
|
+
Include summary row. Default is True.
|
|
800
|
+
|
|
801
|
+
Returns
|
|
802
|
+
-------
|
|
803
|
+
pandas.DataFrame
|
|
804
|
+
Custom metric summary with score names.
|
|
805
|
+
"""
|
|
806
|
+
data = self.data.copy()
|
|
807
|
+
tgt_name = self.dep
|
|
808
|
+
nbins = self.nbins
|
|
809
|
+
precision = self.precision
|
|
810
|
+
min_bin_prop = self.min_bin_prop
|
|
811
|
+
include_missing = self.include_missing
|
|
812
|
+
equal_freq = self.equal_freq
|
|
813
|
+
|
|
814
|
+
score_list = [self.base_score] + self.comp_scrlist
|
|
815
|
+
|
|
816
|
+
if eval_metrics is None:
|
|
817
|
+
eval_metrics = self.eval_metrics
|
|
818
|
+
|
|
819
|
+
cust_metric_res = {}
|
|
820
|
+
for score in score_list:
|
|
821
|
+
cust_metric_res[score] = get_gains_table_by_cust_metrics(
|
|
822
|
+
data=data,
|
|
823
|
+
dep=tgt_name,
|
|
824
|
+
nbins=nbins,
|
|
825
|
+
precision=precision,
|
|
826
|
+
min_bin_prop=min_bin_prop,
|
|
827
|
+
include_missing=include_missing,
|
|
828
|
+
score=score,
|
|
829
|
+
tree_binning=self.tree_binning,
|
|
830
|
+
equal_freq=equal_freq,
|
|
831
|
+
fillna=-999999,
|
|
832
|
+
spec_values=spec_values,
|
|
833
|
+
ascending=True,
|
|
834
|
+
eval_metrics=eval_metrics,
|
|
835
|
+
metric_agg_func=metric_agg_func,
|
|
836
|
+
withSummary=withSummary
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
cust_gains_fnl_res = []
|
|
840
|
+
for score_name, res in cust_metric_res.items():
|
|
841
|
+
res = res.copy()
|
|
842
|
+
res['score_name'] = score_name
|
|
843
|
+
cust_gains_fnl_res.append(res)
|
|
844
|
+
|
|
845
|
+
cust_gains_fnl_res = pd.concat(cust_gains_fnl_res)
|
|
846
|
+
|
|
847
|
+
return cust_gains_fnl_res
|
|
848
|
+
|
|
849
|
+
def get_cross_risk_summary(
|
|
850
|
+
self,
|
|
851
|
+
cross_agg_dict: Optional[Dict] = None,
|
|
852
|
+
nbins: int = 5,
|
|
853
|
+
equal_freq: Optional[bool] = None,
|
|
854
|
+
disp: bool = True,
|
|
855
|
+
spec_values = []
|
|
856
|
+
) -> pd.DataFrame:
|
|
857
|
+
"""
|
|
858
|
+
Generate cross-risk analysis summary between base and comparison scores.
|
|
859
|
+
|
|
860
|
+
This method creates a comprehensive cross-tabulation showing risk metrics
|
|
861
|
+
across different score ranges for both base and comparison scores.
|
|
862
|
+
|
|
863
|
+
Parameters
|
|
864
|
+
----------
|
|
865
|
+
cross_agg_dict : dict, optional
|
|
866
|
+
Dictionary of column names and aggregation functions.
|
|
867
|
+
nbins : int, optional
|
|
868
|
+
Number of bins. Default is 5.
|
|
869
|
+
equal_freq : bool, optional
|
|
870
|
+
Use equal frequency binning. If None, uses self.equal_freq.
|
|
871
|
+
disp : bool, optional
|
|
872
|
+
Whether to display results. Default is True.
|
|
873
|
+
|
|
874
|
+
Returns
|
|
875
|
+
-------
|
|
876
|
+
pandas.DataFrame
|
|
877
|
+
Cross-risk summary with base score range, comparison score range, and metrics.
|
|
878
|
+
"""
|
|
879
|
+
data = self.data.copy()
|
|
880
|
+
dep = self.dep
|
|
881
|
+
min_bin_prop = self.min_bin_prop
|
|
882
|
+
precision = self.precision
|
|
883
|
+
tree_binning = self.tree_binning
|
|
884
|
+
equal_freq = equal_freq if equal_freq is not None else self.equal_freq
|
|
885
|
+
fillna = self.missing_rate_ref
|
|
886
|
+
include_missing = self.include_missing
|
|
887
|
+
|
|
888
|
+
base_score = self.base_score
|
|
889
|
+
compare_scrlist = self.comp_scrlist
|
|
890
|
+
|
|
891
|
+
if cross_agg_dict is None:
|
|
892
|
+
cross_agg_dict = self.cross_agg_dict.copy()
|
|
893
|
+
|
|
894
|
+
multi_scr_res = {}
|
|
895
|
+
for compare_scr in compare_scrlist:
|
|
896
|
+
logger.info(compare_scr)
|
|
897
|
+
score_list = [base_score, compare_scr]
|
|
898
|
+
score_query = " and ".join([s + " > 0" for s in score_list])
|
|
899
|
+
|
|
900
|
+
prepared_data = data.query(score_query)
|
|
901
|
+
|
|
902
|
+
if prepared_data.empty:
|
|
903
|
+
return pd.DataFrame() # 或跳过该组
|
|
904
|
+
|
|
905
|
+
tot_cnt = prepared_data[score_list].shape[0]
|
|
906
|
+
cross_agg_dict_copy = cross_agg_dict.copy()
|
|
907
|
+
cross_agg_dict_copy.update({'flow_id': ['count', lambda x: x.count() / tot_cnt]})
|
|
908
|
+
|
|
909
|
+
cross_res = {}
|
|
910
|
+
for colname, aggfunc in cross_agg_dict_copy.items():
|
|
911
|
+
if data.shape[0] > nbins:
|
|
912
|
+
cross_res[colname] = cross_risk(
|
|
913
|
+
data=prepared_data,
|
|
914
|
+
score_list=[base_score, compare_scr],
|
|
915
|
+
dep=dep,
|
|
916
|
+
nbins=nbins,
|
|
917
|
+
agg_col=colname,
|
|
918
|
+
precision=precision,
|
|
919
|
+
min_bin_prop=min_bin_prop,
|
|
920
|
+
include_missing=include_missing,
|
|
921
|
+
equal_freq=equal_freq,
|
|
922
|
+
binning_numeric=[True, True],
|
|
923
|
+
tree_binning=tree_binning,
|
|
924
|
+
agg_func=aggfunc,
|
|
925
|
+
fillna=fillna,
|
|
926
|
+
spec_values=spec_values
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
fnl_res = []
|
|
930
|
+
for colname, res in cross_res.items():
|
|
931
|
+
res = res.copy()
|
|
932
|
+
res[('', 'eval_metric', '')] = colname
|
|
933
|
+
res = res.rename(columns={"<lambda>": "", "count": "n"})
|
|
934
|
+
if disp:
|
|
935
|
+
from IPython.display import display
|
|
936
|
+
display(res)
|
|
937
|
+
fnl_res.append(res)
|
|
938
|
+
|
|
939
|
+
fnl_res = pd.concat(fnl_res)
|
|
940
|
+
|
|
941
|
+
fnl_res[('', 'score_name', '')] = compare_scr
|
|
942
|
+
multi_scr_res[compare_scr] = fnl_res
|
|
943
|
+
|
|
944
|
+
combined_cross_res = []
|
|
945
|
+
for score_name, cross_data in multi_scr_res.items():
|
|
946
|
+
cross_data = cross_data.copy()
|
|
947
|
+
cross_data.columns = cross_data.columns.map(
|
|
948
|
+
lambda x: f"{x[0]}_{x[1] if len(str(x[1])) >= 2 else ('0' + str(x[1]))}_{x[2]}".strip("_")
|
|
949
|
+
)
|
|
950
|
+
cross_data.index = cross_data.index.map(
|
|
951
|
+
lambda x: f"{x[0] if len(str(x[0])) >= 2 else ('0' + str(x[0]))}_{x[1]}".strip("_")
|
|
952
|
+
)
|
|
953
|
+
cross_data = cross_data.reset_index(drop=False).melt(
|
|
954
|
+
id_vars=['index', 'eval_metric', 'score_name'],
|
|
955
|
+
var_name='comp_scr_range'
|
|
956
|
+
).rename(columns={"index": "base_scr_range"})
|
|
957
|
+
combined_cross_res.append(cross_data)
|
|
958
|
+
combined_cross_res = pd.concat(combined_cross_res)
|
|
959
|
+
|
|
960
|
+
return combined_cross_res
|
|
961
|
+
|
|
962
|
+
def multi_subset_wrapper(
|
|
963
|
+
self,
|
|
964
|
+
condition_dict: Optional[Dict[str, str]] = None,
|
|
965
|
+
subset_var_name: str = 'eval_subset',
|
|
966
|
+
func: Optional[Callable] = None,
|
|
967
|
+
min_subset_size = 10,
|
|
968
|
+
**kwargs
|
|
969
|
+
) -> pd.DataFrame:
|
|
970
|
+
"""
|
|
971
|
+
Apply evaluation function across multiple data subsets.
|
|
972
|
+
|
|
973
|
+
This method iterates over predefined subset conditions, filters the data
|
|
974
|
+
accordingly, and applies the evaluation function to each subset.
|
|
975
|
+
|
|
976
|
+
Parameters
|
|
977
|
+
----------
|
|
978
|
+
condition_dict : dict, optional
|
|
979
|
+
Dictionary mapping subset names to query conditions.
|
|
980
|
+
If None, uses self.subset_condition_dict.
|
|
981
|
+
subset_var_name : str, optional
|
|
982
|
+
Column name for subset identifier in results. Default is 'eval_subset'.
|
|
983
|
+
func : callable, optional
|
|
984
|
+
Function to apply to each subset. If None, uses model_perf_compare.
|
|
985
|
+
**kwargs
|
|
986
|
+
Additional keyword arguments passed to the evaluation function.
|
|
987
|
+
|
|
988
|
+
Returns
|
|
989
|
+
-------
|
|
990
|
+
pandas.DataFrame
|
|
991
|
+
Combined results from all subsets.
|
|
992
|
+
"""
|
|
993
|
+
if condition_dict is None:
|
|
994
|
+
condition_dict = self.subset_condition_dict
|
|
995
|
+
|
|
996
|
+
original_data = self.data.copy()
|
|
997
|
+
if func is None:
|
|
998
|
+
func = self.model_perf_compare
|
|
999
|
+
|
|
1000
|
+
fnl_subset_results = []
|
|
1001
|
+
|
|
1002
|
+
if condition_dict:
|
|
1003
|
+
for group_value, query in condition_dict.items():
|
|
1004
|
+
logger.info(f"INFO:: Multi_Subset_Eval: Running Subset {group_value}")
|
|
1005
|
+
|
|
1006
|
+
subset_data = self.data.query(query).copy() if query != "" else self.data.copy()
|
|
1007
|
+
subset_data_size = subset_data.shape[0]
|
|
1008
|
+
|
|
1009
|
+
if subset_data_size > min_subset_size:
|
|
1010
|
+
self.data = subset_data
|
|
1011
|
+
|
|
1012
|
+
subset_result = func(**kwargs)
|
|
1013
|
+
if subset_result is not None and not subset_result.empty:
|
|
1014
|
+
subset_result = subset_result.copy()
|
|
1015
|
+
subset_result[subset_var_name] = group_value
|
|
1016
|
+
fnl_subset_results.append(subset_result)
|
|
1017
|
+
else:
|
|
1018
|
+
# fnl_subset_results.append(pd.DataFrame({subset_var_name: [group_value]}))
|
|
1019
|
+
self.data = original_data
|
|
1020
|
+
continue
|
|
1021
|
+
|
|
1022
|
+
self.data = original_data
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
if fnl_subset_results:
|
|
1026
|
+
return pd.concat(fnl_subset_results)
|
|
1027
|
+
|
|
1028
|
+
return pd.DataFrame({subset_var_name: [group_value]})
|
|
1029
|
+
|
|
1030
|
+
def multi_ylabel_wrapper(
|
|
1031
|
+
self,
|
|
1032
|
+
ylabels: Optional[List[str]] = None,
|
|
1033
|
+
ylabel_var_name: str = 'eval_ylabel',
|
|
1034
|
+
eval_func: Optional[Callable] = None,
|
|
1035
|
+
**kwargs
|
|
1036
|
+
) -> pd.DataFrame:
|
|
1037
|
+
"""
|
|
1038
|
+
Apply evaluation function across multiple target labels.
|
|
1039
|
+
|
|
1040
|
+
This method iterates over different target/label columns, temporarily
|
|
1041
|
+
updating the dependent variable for each iteration.
|
|
1042
|
+
|
|
1043
|
+
Parameters
|
|
1044
|
+
----------
|
|
1045
|
+
ylabels : list, optional
|
|
1046
|
+
List of target column names. If None, uses self.eval_ylabels.
|
|
1047
|
+
ylabel_var_name : str, optional
|
|
1048
|
+
Column name for label identifier in results. Default is 'eval_ylabel'.
|
|
1049
|
+
eval_func : callable, optional
|
|
1050
|
+
Function to apply for each label. If None, uses model_perf_compare.
|
|
1051
|
+
**kwargs
|
|
1052
|
+
Additional keyword arguments passed to the evaluation function.
|
|
1053
|
+
|
|
1054
|
+
Returns
|
|
1055
|
+
-------
|
|
1056
|
+
pandas.DataFrame
|
|
1057
|
+
Combined results from all labels.
|
|
1058
|
+
"""
|
|
1059
|
+
original_dep = self.dep
|
|
1060
|
+
original_data = self.data.copy()
|
|
1061
|
+
if eval_func is None:
|
|
1062
|
+
eval_func = self.model_perf_compare
|
|
1063
|
+
|
|
1064
|
+
fnl_ylabel_results = []
|
|
1065
|
+
|
|
1066
|
+
if ylabels is None:
|
|
1067
|
+
ylabels = self.eval_ylabels
|
|
1068
|
+
|
|
1069
|
+
for current_dep in ylabels:
|
|
1070
|
+
logger.info(f"INFO:: Multi_yLabel_Eval: Running Label {current_dep}")
|
|
1071
|
+
|
|
1072
|
+
input_data = self.data.query(f"{current_dep} == {current_dep}").copy()
|
|
1073
|
+
subset_data_size = input_data.shape[0]
|
|
1074
|
+
|
|
1075
|
+
if subset_data_size > 0:
|
|
1076
|
+
self.dep = current_dep
|
|
1077
|
+
self.data = input_data
|
|
1078
|
+
|
|
1079
|
+
subset_result = eval_func(**kwargs)
|
|
1080
|
+
subset_result = subset_result.copy()
|
|
1081
|
+
subset_result[ylabel_var_name] = current_dep
|
|
1082
|
+
fnl_ylabel_results.append(subset_result)
|
|
1083
|
+
|
|
1084
|
+
self.dep = original_dep
|
|
1085
|
+
self.data = original_data
|
|
1086
|
+
|
|
1087
|
+
if fnl_ylabel_results:
|
|
1088
|
+
return pd.concat(fnl_ylabel_results)
|
|
1089
|
+
return pd.DataFrame()
|
|
1090
|
+
|
|
1091
|
+
def multi_group_wrapper(
|
|
1092
|
+
self,
|
|
1093
|
+
group_name: Optional[str] = None,
|
|
1094
|
+
group_var_name: str = 'group_name',
|
|
1095
|
+
group_eval_func: Optional[Callable] = None,
|
|
1096
|
+
min_subset_size: int = 10,
|
|
1097
|
+
**kwargs
|
|
1098
|
+
) -> pd.DataFrame:
|
|
1099
|
+
"""
|
|
1100
|
+
Apply evaluation function across multiple groups.
|
|
1101
|
+
|
|
1102
|
+
This method splits data by a grouping column and applies the evaluation
|
|
1103
|
+
function to each group separately.
|
|
1104
|
+
|
|
1105
|
+
Parameters
|
|
1106
|
+
----------
|
|
1107
|
+
group_name : str, optional
|
|
1108
|
+
Column name to group by. If None, processes all data together.
|
|
1109
|
+
group_var_name : str, optional
|
|
1110
|
+
Column name for group identifier in results. Default is 'group_name'.
|
|
1111
|
+
group_eval_func : callable, optional
|
|
1112
|
+
Function to apply to each group. If None, uses model_perf_compare.
|
|
1113
|
+
min_subset_size : int, optional
|
|
1114
|
+
Minimum data size required to process a group. Default is 10.
|
|
1115
|
+
**kwargs
|
|
1116
|
+
Additional keyword arguments passed to the evaluation function.
|
|
1117
|
+
|
|
1118
|
+
Returns
|
|
1119
|
+
-------
|
|
1120
|
+
pandas.DataFrame
|
|
1121
|
+
Combined results from all groups.
|
|
1122
|
+
"""
|
|
1123
|
+
original_data = self.data.copy()
|
|
1124
|
+
|
|
1125
|
+
if group_name is None:
|
|
1126
|
+
return group_eval_func(**kwargs) if group_eval_func else pd.DataFrame()
|
|
1127
|
+
|
|
1128
|
+
group_value_list = list(set(original_data[group_name].unique().tolist()))
|
|
1129
|
+
|
|
1130
|
+
if group_eval_func is None:
|
|
1131
|
+
group_eval_func = self.model_perf_compare
|
|
1132
|
+
|
|
1133
|
+
fnl_group_results = []
|
|
1134
|
+
for group_value in group_value_list:
|
|
1135
|
+
logger.info(f"INFO:: Multi_Group_Eval: Running Group {group_value}")
|
|
1136
|
+
|
|
1137
|
+
input_data = original_data.query(f"{group_name} == '{group_value}'").copy()
|
|
1138
|
+
subset_data_size = input_data.shape[0]
|
|
1139
|
+
|
|
1140
|
+
if subset_data_size > min_subset_size:
|
|
1141
|
+
self.data = input_data
|
|
1142
|
+
|
|
1143
|
+
subset_result = group_eval_func(**kwargs)
|
|
1144
|
+
subset_result = subset_result.copy()
|
|
1145
|
+
subset_result[group_var_name] = group_value
|
|
1146
|
+
fnl_group_results.append(subset_result)
|
|
1147
|
+
|
|
1148
|
+
self.data = original_data
|
|
1149
|
+
|
|
1150
|
+
if fnl_group_results:
|
|
1151
|
+
return pd.concat(fnl_group_results)
|
|
1152
|
+
return pd.DataFrame()
|
|
1153
|
+
|
|
1154
|
+
def multi_dim_eval(
|
|
1155
|
+
self,
|
|
1156
|
+
condition_dict: Optional[Dict[str, str]] = None,
|
|
1157
|
+
eval_ylabels: Optional[List[str]] = None,
|
|
1158
|
+
grp_namelist: Optional[List[str]] = None,
|
|
1159
|
+
eval_func: Optional[Callable] = None,
|
|
1160
|
+
subset_var_name: str = 'eval_subset',
|
|
1161
|
+
ylabel_var_name: str = 'eval_ylabel',
|
|
1162
|
+
var_name: str = 'group_name',
|
|
1163
|
+
value_name: str = 'group_value',
|
|
1164
|
+
**kwargs
|
|
1165
|
+
) -> pd.DataFrame:
|
|
1166
|
+
"""
|
|
1167
|
+
Perform multi-dimensional evaluation across subsets, labels, and groups.
|
|
1168
|
+
|
|
1169
|
+
This method combines multi_subset_wrapper and multi_ylabel_wrapper to
|
|
1170
|
+
evaluate model performance across different dimensions.
|
|
1171
|
+
|
|
1172
|
+
Parameters
|
|
1173
|
+
----------
|
|
1174
|
+
condition_dict : dict, optional
|
|
1175
|
+
Subset conditions. If None, uses self.subset_condition_dict.
|
|
1176
|
+
eval_ylabels : list, optional
|
|
1177
|
+
Target labels. If None, uses self.eval_ylabels.
|
|
1178
|
+
grp_namelist : list, optional
|
|
1179
|
+
Group columns. If None, uses self.grp_namelist.
|
|
1180
|
+
eval_func : callable, optional
|
|
1181
|
+
Evaluation function. If None, uses model_perf_compare.
|
|
1182
|
+
subset_var_name : str, optional
|
|
1183
|
+
Column name for subset identifier.
|
|
1184
|
+
ylabel_var_name : str, optional
|
|
1185
|
+
Column name for label identifier.
|
|
1186
|
+
var_name : str, optional
|
|
1187
|
+
Column name for group name in results.
|
|
1188
|
+
value_name : str, optional
|
|
1189
|
+
Column name for group value in results.
|
|
1190
|
+
**kwargs
|
|
1191
|
+
Additional keyword arguments.
|
|
1192
|
+
|
|
1193
|
+
Returns
|
|
1194
|
+
-------
|
|
1195
|
+
pandas.DataFrame
|
|
1196
|
+
Multi-dimensional evaluation results.
|
|
1197
|
+
"""
|
|
1198
|
+
if condition_dict is None:
|
|
1199
|
+
condition_dict = self.subset_condition_dict
|
|
1200
|
+
|
|
1201
|
+
if eval_ylabels is None:
|
|
1202
|
+
eval_ylabels = self.eval_ylabels
|
|
1203
|
+
|
|
1204
|
+
if grp_namelist is None:
|
|
1205
|
+
grp_namelist = self.grp_namelist
|
|
1206
|
+
|
|
1207
|
+
if eval_func is None:
|
|
1208
|
+
eval_func = self.model_perf_compare
|
|
1209
|
+
|
|
1210
|
+
fnl_perf_res = []
|
|
1211
|
+
for group_name in grp_namelist:
|
|
1212
|
+
logger.info(f"INFO:: Multi_Dim_Eval: Running Group {group_name}")
|
|
1213
|
+
|
|
1214
|
+
sig = inspect.signature(eval_func)
|
|
1215
|
+
parameters = sig.parameters
|
|
1216
|
+
|
|
1217
|
+
if 'grp_name' in parameters:
|
|
1218
|
+
sub_perf_res = self.multi_subset_wrapper(
|
|
1219
|
+
condition_dict=condition_dict,
|
|
1220
|
+
subset_var_name=subset_var_name,
|
|
1221
|
+
func=self.multi_ylabel_wrapper,
|
|
1222
|
+
ylabels=eval_ylabels,
|
|
1223
|
+
ylabel_var_name=ylabel_var_name,
|
|
1224
|
+
eval_func=eval_func,
|
|
1225
|
+
grp_name=group_name,
|
|
1226
|
+
**kwargs
|
|
1227
|
+
)
|
|
1228
|
+
else:
|
|
1229
|
+
sub_perf_res = self.multi_subset_wrapper(
|
|
1230
|
+
condition_dict=condition_dict,
|
|
1231
|
+
subset_var_name=subset_var_name,
|
|
1232
|
+
func=self.multi_ylabel_wrapper,
|
|
1233
|
+
ylabels=eval_ylabels,
|
|
1234
|
+
ylabel_var_name=ylabel_var_name,
|
|
1235
|
+
eval_func=eval_func,
|
|
1236
|
+
**kwargs
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
sub_perf_res = sub_perf_res.copy()
|
|
1240
|
+
sub_perf_res[var_name] = group_name
|
|
1241
|
+
fnl_perf_res.append(sub_perf_res)
|
|
1242
|
+
|
|
1243
|
+
fnl_perf_res = pd.concat(fnl_perf_res)
|
|
1244
|
+
|
|
1245
|
+
if 'grp_name' in parameters:
|
|
1246
|
+
if all(x in fnl_perf_res.columns for x in grp_namelist):
|
|
1247
|
+
fnl_perf_res[value_name] = fnl_perf_res[grp_namelist].bfill(axis=1).iloc[:, 0]
|
|
1248
|
+
fnl_perf_res = fnl_perf_res.drop(columns=grp_namelist)
|
|
1249
|
+
|
|
1250
|
+
return fnl_perf_res
|
|
1251
|
+
|
|
1252
|
+
def cross_perf_eval(
|
|
1253
|
+
self,
|
|
1254
|
+
bad_list: List[str],
|
|
1255
|
+
scr_list: List[str],
|
|
1256
|
+
data: Optional[pd.DataFrame] = None,
|
|
1257
|
+
eval_metric: List[str] = None,
|
|
1258
|
+
melt: bool = True
|
|
1259
|
+
) -> pd.DataFrame:
|
|
1260
|
+
"""
|
|
1261
|
+
Cross-evaluate model performance across multiple bad flags and scores.
|
|
1262
|
+
|
|
1263
|
+
This method calculates performance metrics for all combinations of
|
|
1264
|
+
bad flags and scores, useful for comparing model behavior across
|
|
1265
|
+
different definitions of "bad" outcomes.
|
|
1266
|
+
|
|
1267
|
+
Parameters
|
|
1268
|
+
----------
|
|
1269
|
+
bad_list : list
|
|
1270
|
+
List of bad/negative outcome column names.
|
|
1271
|
+
scr_list : list
|
|
1272
|
+
List of score column names.
|
|
1273
|
+
data : pandas.DataFrame, optional
|
|
1274
|
+
Data to use. If None, uses self.data.
|
|
1275
|
+
eval_metric : list, optional
|
|
1276
|
+
Metrics to extract. If None, uses ['AUC', 'KS', 'N'].
|
|
1277
|
+
melt : bool, optional
|
|
1278
|
+
Whether to melt the pivot table. Default is True.
|
|
1279
|
+
|
|
1280
|
+
Returns
|
|
1281
|
+
-------
|
|
1282
|
+
pandas.DataFrame
|
|
1283
|
+
Cross performance evaluation results.
|
|
1284
|
+
"""
|
|
1285
|
+
if eval_metric is None:
|
|
1286
|
+
eval_metric = ['AUC', 'KS', 'N']
|
|
1287
|
+
|
|
1288
|
+
if data is None:
|
|
1289
|
+
data = self.data.copy()
|
|
1290
|
+
|
|
1291
|
+
score_query = " and ".join([s + " > 0" for s in scr_list])
|
|
1292
|
+
bad_query = " and ".join([s + " == " + s for s in bad_list])
|
|
1293
|
+
|
|
1294
|
+
data = data.query(bad_query)
|
|
1295
|
+
data = data.query(score_query)
|
|
1296
|
+
|
|
1297
|
+
if data.shape[0] > self.nbins:
|
|
1298
|
+
fnl_res = []
|
|
1299
|
+
for bad in bad_list:
|
|
1300
|
+
for scr in scr_list:
|
|
1301
|
+
perf = get_perf_summary(
|
|
1302
|
+
None,
|
|
1303
|
+
None,
|
|
1304
|
+
data,
|
|
1305
|
+
tgt_name=bad,
|
|
1306
|
+
scr_name=scr,
|
|
1307
|
+
to_show=False,
|
|
1308
|
+
display=False,
|
|
1309
|
+
dist_bins=self.nbins,
|
|
1310
|
+
pct_bins=self.nbins,
|
|
1311
|
+
precision=self.precision,
|
|
1312
|
+
min_bin_prop=self.min_bin_prop,
|
|
1313
|
+
equal_freq=True
|
|
1314
|
+
)
|
|
1315
|
+
perf = perf.copy()
|
|
1316
|
+
perf['eval_ylabel'] = bad
|
|
1317
|
+
perf['score_name'] = scr
|
|
1318
|
+
fnl_res.append(perf)
|
|
1319
|
+
|
|
1320
|
+
fnl_res = pd.concat(fnl_res)
|
|
1321
|
+
|
|
1322
|
+
summary = fnl_res.pivot_table(index="score_name", columns='eval_ylabel', values=eval_metric)
|
|
1323
|
+
summary.columns = summary.columns.map(lambda x: f"{x[0]}_{x[1]}")
|
|
1324
|
+
|
|
1325
|
+
if melt:
|
|
1326
|
+
summary = summary.reset_index(drop=False).melt(id_vars=['score_name'])
|
|
1327
|
+
|
|
1328
|
+
return summary
|
|
1329
|
+
|
|
1330
|
+
return pd.DataFrame(columns=['eval_ylabel', 'score_name'])
|
|
1331
|
+
|
|
1332
|
+
def run_variable_analysis_summary(
|
|
1333
|
+
self,
|
|
1334
|
+
varlist: List[str],
|
|
1335
|
+
data: Optional[pd.DataFrame] = None,
|
|
1336
|
+
dep: Optional[str] = None,
|
|
1337
|
+
nbins: Optional[int] = None,
|
|
1338
|
+
equal_freq: Optional[bool] = None,
|
|
1339
|
+
min_bin_prop: Optional[float] = None,
|
|
1340
|
+
precision: Optional[int] = None,
|
|
1341
|
+
chi2_method: Optional[bool] = None,
|
|
1342
|
+
chi2_p: Optional[float] = None,
|
|
1343
|
+
init_equi_bins: Optional[int] = None,
|
|
1344
|
+
tree_binning: Optional[bool] = None,
|
|
1345
|
+
include_missing: Optional[bool] = None,
|
|
1346
|
+
missing_rate_ref: Optional[Union[int, float]] = None,
|
|
1347
|
+
seed: Optional[int] = None,
|
|
1348
|
+
spec_values = []
|
|
1349
|
+
) -> pd.DataFrame:
|
|
1350
|
+
"""
|
|
1351
|
+
Run comprehensive variable analysis and generate summary report.
|
|
1352
|
+
|
|
1353
|
+
This method performs binning analysis on specified variables,
|
|
1354
|
+
calculating metrics like IV (Information Value) and chi-square statistics.
|
|
1355
|
+
|
|
1356
|
+
Parameters
|
|
1357
|
+
----------
|
|
1358
|
+
varlist : list
|
|
1359
|
+
List of variable names to analyze.
|
|
1360
|
+
data : pandas.DataFrame, optional
|
|
1361
|
+
Data to use. If None, uses self.data.
|
|
1362
|
+
dep : str, optional
|
|
1363
|
+
Target variable name. If None, uses self.dep.
|
|
1364
|
+
nbins : int, optional
|
|
1365
|
+
Number of bins. If None, uses self.nbins.
|
|
1366
|
+
equal_freq : bool, optional
|
|
1367
|
+
Use equal frequency binning. If None, uses self.equal_freq.
|
|
1368
|
+
min_bin_prop : float, optional
|
|
1369
|
+
Minimum bin proportion. If None, uses self.min_bin_prop.
|
|
1370
|
+
precision : int, optional
|
|
1371
|
+
Decimal precision. If None, uses self.precision.
|
|
1372
|
+
chi2_method : bool, optional
|
|
1373
|
+
Use chi-square method. If None, uses self.chi2_method.
|
|
1374
|
+
chi2_p : float, optional
|
|
1375
|
+
Chi-square p-value. If None, uses self.chi2_p.
|
|
1376
|
+
init_equi_bins : int, optional
|
|
1377
|
+
Initial equal bins. If None, uses self.init_equi_bins.
|
|
1378
|
+
tree_binning : bool, optional
|
|
1379
|
+
Use tree binning. If None, uses self.tree_binning.
|
|
1380
|
+
include_missing : bool, optional
|
|
1381
|
+
Include missing values. If None, uses self.include_missing.
|
|
1382
|
+
missing_rate_ref : int or float, optional
|
|
1383
|
+
Missing reference. If None, uses self.missing_rate_ref.
|
|
1384
|
+
seed : int, optional
|
|
1385
|
+
Random seed. If None, uses self.seed.
|
|
1386
|
+
|
|
1387
|
+
Returns
|
|
1388
|
+
-------
|
|
1389
|
+
pandas.DataFrame
|
|
1390
|
+
Variable analysis summary with IV and other metrics.
|
|
1391
|
+
"""
|
|
1392
|
+
if data is None:
|
|
1393
|
+
data = self.data.copy()
|
|
1394
|
+
|
|
1395
|
+
if dep is None:
|
|
1396
|
+
dep = self.dep
|
|
1397
|
+
|
|
1398
|
+
nbins = nbins if nbins is not None else self.nbins
|
|
1399
|
+
equal_freq = equal_freq if equal_freq is not None else self.equal_freq
|
|
1400
|
+
min_bin_prop = min_bin_prop if min_bin_prop is not None else self.min_bin_prop
|
|
1401
|
+
precision = precision if precision is not None else self.precision
|
|
1402
|
+
chi2_method = chi2_method if chi2_method is not None else self.chi2_method
|
|
1403
|
+
chi2_p = chi2_p if chi2_p is not None else self.chi2_p
|
|
1404
|
+
init_equi_bins = init_equi_bins if init_equi_bins is not None else self.init_equi_bins
|
|
1405
|
+
tree_binning = tree_binning if tree_binning is not None else self.tree_binning
|
|
1406
|
+
include_missing = include_missing if include_missing is not None else self.include_missing
|
|
1407
|
+
missing_rate_ref = missing_rate_ref if missing_rate_ref is not None else self.missing_rate_ref
|
|
1408
|
+
seed = seed if seed is not None else self.seed
|
|
1409
|
+
|
|
1410
|
+
from Modeling_Tool.Feature.Feature_Insights import VarExtractionInsights
|
|
1411
|
+
|
|
1412
|
+
varInsights = VarExtractionInsights(
|
|
1413
|
+
data=data,
|
|
1414
|
+
dep=dep,
|
|
1415
|
+
plot_path=None,
|
|
1416
|
+
nbins=nbins,
|
|
1417
|
+
equal_freq=equal_freq,
|
|
1418
|
+
min_bin_prop=min_bin_prop,
|
|
1419
|
+
precision=precision,
|
|
1420
|
+
chi2_method=chi2_method,
|
|
1421
|
+
chi2_p=chi2_p,
|
|
1422
|
+
init_equi_bins=init_equi_bins,
|
|
1423
|
+
tree_binning=tree_binning,
|
|
1424
|
+
include_missing=include_missing,
|
|
1425
|
+
seed=seed,
|
|
1426
|
+
missing_rate_ref=missing_rate_ref,
|
|
1427
|
+
spec_values=spec_values
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
data = data.copy()
|
|
1431
|
+
for var in varlist:
|
|
1432
|
+
data[var] = data[var].fillna(missing_rate_ref)
|
|
1433
|
+
|
|
1434
|
+
var_summary = varInsights.get_var_analysis_report(data=data, varlist=varlist, dep=dep, iv_cut=0)
|
|
1435
|
+
|
|
1436
|
+
return var_summary
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def pipe(self, data=None):
|
|
1440
|
+
"""
|
|
1441
|
+
返回一个 EvaluationPipeline 对象,用于链式分组和子集评估。
|
|
1442
|
+
|
|
1443
|
+
Parameters
|
|
1444
|
+
----------
|
|
1445
|
+
data : pd.DataFrame, optional
|
|
1446
|
+
指定数据,默认使用 self.data
|
|
1447
|
+
|
|
1448
|
+
Returns
|
|
1449
|
+
-------
|
|
1450
|
+
EvaluationPipeline
|
|
1451
|
+
"""
|
|
1452
|
+
return EvaluationPipeline(self, data)
|