SuperModelingFactory 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. ExcelMaster/ExcelFormatTool.py +487 -0
  2. ExcelMaster/ExcelMaster.py +917 -0
  3. ExcelMaster/Template.py +525 -0
  4. ExcelMaster/Utility.py +233 -0
  5. ExcelMaster/__init__.py +3 -0
  6. Modeling_Tool/Core/Binning_Tool.py +1608 -0
  7. Modeling_Tool/Core/Binning_Tool.pyi +48 -0
  8. Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
  9. Modeling_Tool/Core/Json_Data_Converter.py +621 -0
  10. Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
  11. Modeling_Tool/Core/ODPS_Tool.py +284 -0
  12. Modeling_Tool/Core/Slope_Tool.py +356 -0
  13. Modeling_Tool/Core/Slope_Tool.pyi +31 -0
  14. Modeling_Tool/Core/XOR_Encryptor.py +207 -0
  15. Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
  16. Modeling_Tool/Core/__init__.py +99 -0
  17. Modeling_Tool/Core/kDataFrame.py +228 -0
  18. Modeling_Tool/Core/kDataFrame.pyi +43 -0
  19. Modeling_Tool/Core/sample_weight_utils.py +77 -0
  20. Modeling_Tool/Core/utils.py +2672 -0
  21. Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
  22. Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
  23. Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
  24. Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
  25. Modeling_Tool/Eval/__init__.py +54 -0
  26. Modeling_Tool/Eval/evaluate_model.py +2008 -0
  27. Modeling_Tool/Eval/evaluate_model.pyi +50 -0
  28. Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
  29. Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
  30. Modeling_Tool/Explainability/Model_Explainer.py +743 -0
  31. Modeling_Tool/Explainability/__init__.py +24 -0
  32. Modeling_Tool/Feature/Distribution_Tool.py +509 -0
  33. Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
  34. Modeling_Tool/Feature/Feature_Insights.py +762 -0
  35. Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
  36. Modeling_Tool/Feature/PSI_Tool.py +1195 -0
  37. Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
  38. Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
  39. Modeling_Tool/Feature/__init__.py +40 -0
  40. Modeling_Tool/Model/Backward_Tool.py +778 -0
  41. Modeling_Tool/Model/Backward_Tool.pyi +45 -0
  42. Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
  43. Modeling_Tool/Model/GBM_Tool.py +1610 -0
  44. Modeling_Tool/Model/GBM_Tool.pyi +90 -0
  45. Modeling_Tool/Model/LRM_Tool.py +1198 -0
  46. Modeling_Tool/Model/LRM_Tool.pyi +47 -0
  47. Modeling_Tool/Model/__init__.py +61 -0
  48. Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
  49. Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
  50. Modeling_Tool/Sample/Reject_Infer.py +413 -0
  51. Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
  52. Modeling_Tool/Sample/Sample_Split.py +520 -0
  53. Modeling_Tool/Sample/Sample_Split.pyi +43 -0
  54. Modeling_Tool/Sample/__init__.py +31 -0
  55. Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
  56. Modeling_Tool/UAT/__init__.py +19 -0
  57. Modeling_Tool/WOE/WOE_Adapter.py +204 -0
  58. Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
  59. Modeling_Tool/WOE/WOE_Master.py +491 -0
  60. Modeling_Tool/WOE/WOE_Master.pyi +40 -0
  61. Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
  62. Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
  63. Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
  64. Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
  65. Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
  66. Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
  67. Modeling_Tool/WOE/WOE_Tool.py +1094 -0
  68. Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
  69. Modeling_Tool/WOE/__init__.py +86 -0
  70. Modeling_Tool/WOE/plot_woe_tool.py +290 -0
  71. Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
  72. Modeling_Tool/__init__.py +176 -0
  73. Report/Report_Tool.py +380 -0
  74. Report/__init__.py +3 -0
  75. supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
  76. supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
  77. supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
  78. supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
  79. supermodelingfactory-0.2.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,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)