SuperModelingFactory 0.2.0__tar.gz → 0.2.1__tar.gz

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 (88) hide show
  1. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/Evaluation_Tool.py +317 -248
  2. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/Evaluation_Tool.pyi +6 -7
  3. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Explainability/Coalition_Structure.py +73 -9
  4. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Explainability/Model_Explainer.py +196 -1
  5. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/Backward_Tool.py +70 -24
  6. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/__init__.py +1 -1
  7. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/PKG-INFO +4 -2
  8. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/README.md +1 -1
  9. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/SuperModelingFactory.egg-info/PKG-INFO +4 -2
  10. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/SuperModelingFactory.egg-info/requires.txt +5 -0
  11. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/pyproject.toml +2 -1
  12. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/setup.py +1 -1
  13. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/ExcelMaster/ExcelFormatTool.py +0 -0
  14. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/ExcelMaster/ExcelMaster.py +0 -0
  15. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/ExcelMaster/Template.py +0 -0
  16. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/ExcelMaster/Utility.py +0 -0
  17. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/ExcelMaster/__init__.py +0 -0
  18. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/LICENSE +0 -0
  19. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/MANIFEST.in +0 -0
  20. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  21. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  22. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  23. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  24. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  25. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  26. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  27. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  28. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  29. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  30. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/__init__.py +0 -0
  31. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/kDataFrame.py +0 -0
  32. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  33. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  34. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Core/utils.py +0 -0
  35. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  36. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  37. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/__init__.py +0 -0
  38. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  39. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  40. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  41. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Explainability/__init__.py +0 -0
  42. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  43. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  44. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  45. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  46. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  47. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  48. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  49. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Feature/__init__.py +0 -0
  50. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  51. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  52. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  53. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  54. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  55. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  56. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Model/__init__.py +0 -0
  57. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  58. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  59. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  60. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  61. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  62. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  63. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/Sample/__init__.py +0 -0
  64. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  65. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/UAT/__init__.py +0 -0
  66. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  67. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  68. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  69. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  70. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  71. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  72. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  73. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  74. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  75. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  76. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  77. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  78. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/__init__.py +0 -0
  79. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  80. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  81. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Report/Report_Tool.py +0 -0
  82. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/Report/__init__.py +0 -0
  83. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  84. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  85. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  86. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  87. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/requirements.txt +0 -0
  88. {supermodelingfactory-0.2.0 → supermodelingfactory-0.2.1}/setup.cfg +0 -0
@@ -12,10 +12,42 @@ import pandas as pd
12
12
  import numpy as np
13
13
  import inspect
14
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
15
+ from .Model_Eval_Tool import (
16
+ cross_risk,
17
+ get_perf_summary,
18
+ GainsTableCalculator,
19
+ PerformanceEvaluator,
20
+ )
16
21
  import logging
17
22
  logger = logging.getLogger(__name__)
18
23
 
24
+ _DEFAULT_GAINS_DISPLAY_METRICS = [
25
+ 'MIN', 'MAX', 'N', 'PROP', 'AVG_SCORE', 'AVG_BAD',
26
+ 'CUM_BAD_PCT', 'KS_PER_BIN', 'LIFT', 'RANK_ORDER_BUMP',
27
+ ]
28
+ _LEGACY_EVAL_YLABELS = ['is_dpd7']
29
+ _LEGACY_GRP_NAMELIST = ['sample_ind_fnl', 'aprvvrsn_2', 'week_start_date']
30
+
31
+
32
+ def _legacy_subset_condition_dict():
33
+ return {
34
+ "Overall": "",
35
+ "AE": "aprvvrsn_2 in ('AE')",
36
+ "NON_AE": "aprvvrsn_2 not in ('AE')",
37
+ "FT": "aprvvrsn_2 in ('FT2.0')",
38
+ "NON_FT": "aprvvrsn_2 not in ('FT2.0')",
39
+ "NON_FT_AE": "aprvvrsn_2 not in ('FT2.0') and aprvvrsn_2 not in ('AE')",
40
+ }
41
+
42
+
43
+ def _legacy_cross_agg_dict(udf):
44
+ return {
45
+ 'is_dpd7': ['count', lambda x: (x.sum() / x.count()).round(4)],
46
+ 'credit_limit': ['count', lambda x: udf.valid_average(x)],
47
+ 'monthlyincome': ['count', lambda x: udf.valid_average(x)],
48
+ 'education': ['count', lambda x: udf.valid_average(x)],
49
+ }
50
+
19
51
  class EvaluationPipeline:
20
52
  """
21
53
  链式调用的流水线对象,支持 .group_by() 和 .subset_by() 链式添加条件,
@@ -202,7 +234,6 @@ class Model_Evaluation_Tool:
202
234
  - Base score calculation and correlation analysis
203
235
  - Model performance comparison across multiple metrics
204
236
  - Gains table generation and analysis
205
- - Custom metric evaluation
206
237
  - Cross-risk analysis
207
238
  - Multi-dimensional evaluation with subsets and groupings
208
239
 
@@ -255,8 +286,6 @@ class Model_Evaluation_Tool:
255
286
  Name of the base score column.
256
287
  comp_scrlist : list
257
288
  List of comparison score names.
258
- eval_metrics : list
259
- List of evaluation metrics.
260
289
  eval_ylabels : list
261
290
  Labels for y-axis evaluation.
262
291
  gains_display_metric_list : list
@@ -283,8 +312,15 @@ class Model_Evaluation_Tool:
283
312
  include_missing: bool = True,
284
313
  missing_rate_ref: Union[int, float] = -99999999,
285
314
  excel_path: Optional[str] = None,
286
- fillna = -999999,
287
- spec_values = []
315
+ fillna=-999999,
316
+ spec_values=None,
317
+ cross_agg_dict: Optional[Dict] = None,
318
+ subset_condition_dict: Optional[Dict[str, str]] = None,
319
+ eval_ylabels: Optional[List[str]] = None,
320
+ grp_namelist: Optional[List[str]] = None,
321
+ gains_display_metric_list: Optional[List[str]] = None,
322
+ weight_col: Optional[str] = None,
323
+ positive_score_only: bool = True,
288
324
  ):
289
325
  """
290
326
  Initialize the Model_Evaluation_Tool with configuration parameters.
@@ -344,39 +380,105 @@ class Model_Evaluation_Tool:
344
380
  self.excel_path = excel_path
345
381
  self.missing_rate_ref = missing_rate_ref
346
382
  self.fillna = fillna
347
- self.spec_values = spec_values
383
+ self.spec_values = [] if spec_values is None else spec_values
384
+ self.weight_col = weight_col
385
+ self.positive_score_only = positive_score_only
348
386
 
349
387
  self.chi2_method = chi2_method
350
388
  self.chi2_p = chi2_p
351
389
  self.init_equi_bins = init_equi_bins
352
390
 
353
- self.eval_metrics = [
354
- 'age',
355
- 'monthlyincome',
356
- 'education',
357
- 'credit_limit'
358
- ]
359
-
360
391
  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']
392
+ self.cross_agg_dict = (
393
+ cross_agg_dict if cross_agg_dict is not None
394
+ else _legacy_cross_agg_dict(self.udf)
395
+ )
396
+ self.subset_condition_dict = (
397
+ subset_condition_dict if subset_condition_dict is not None
398
+ else _legacy_subset_condition_dict()
399
+ )
400
+ self.eval_ylabels = (
401
+ eval_ylabels if eval_ylabels is not None else list(_LEGACY_EVAL_YLABELS)
402
+ )
403
+ self.grp_namelist = (
404
+ grp_namelist if grp_namelist is not None else list(_LEGACY_GRP_NAMELIST)
405
+ )
406
+ self.gains_display_metric_list = (
407
+ gains_display_metric_list if gains_display_metric_list is not None
408
+ else list(_DEFAULT_GAINS_DISPLAY_METRICS)
409
+ )
410
+
411
+ def _score_order(self) -> List[str]:
412
+ scores = []
413
+ if self.base_score is not None:
414
+ scores.append(self.base_score)
415
+ scores.extend(self.comp_scrlist or [])
416
+ return scores
417
+
418
+ def _filter_positive_scores(self, data: pd.DataFrame, scores: List[str]) -> pd.DataFrame:
419
+ if not self.positive_score_only or not scores:
420
+ return data
421
+ mask = pd.Series(True, index=data.index)
422
+ for score in scores:
423
+ if score in data.columns:
424
+ mask &= data[score] > 0
425
+ return data.loc[mask]
426
+
427
+ def _build_performance_evaluator(
428
+ self,
429
+ score: str,
430
+ dist_bins: int,
431
+ pct_bins: int,
432
+ min_bin_prop: Optional[float] = None,
433
+ include_missing: Optional[bool] = None,
434
+ equal_freq: Optional[bool] = None,
435
+ ) -> PerformanceEvaluator:
436
+ return PerformanceEvaluator(
437
+ tgt_name=self.dep,
438
+ scr_name=score,
439
+ dist_bins=dist_bins,
440
+ pct_bins=pct_bins,
441
+ precision=self.precision,
442
+ min_bin_prop=self.min_bin_prop if min_bin_prop is None else min_bin_prop,
443
+ include_missing=False if include_missing is None else include_missing,
444
+ equal_freq=self.equal_freq if equal_freq is None else equal_freq,
445
+ chi2_method=self.chi2_method,
446
+ init_equi_bins=self.init_equi_bins,
447
+ chi2_p=self.chi2_p,
448
+ tree_binning=self.tree_binning,
449
+ random_state=self.seed,
450
+ weight_col=self.weight_col,
451
+ )
452
+
453
+ def _build_gains_calculator(
454
+ self,
455
+ data: pd.DataFrame,
456
+ score: str,
457
+ nbins: int,
458
+ fillna,
459
+ spec_values,
460
+ include_missing: bool,
461
+ ascending: bool = True,
462
+ ) -> GainsTableCalculator:
463
+ return GainsTableCalculator(
464
+ data=data,
465
+ dep=self.dep,
466
+ score=score,
467
+ nbins=nbins,
468
+ precision=self.precision,
469
+ min_bin_prop=self.min_bin_prop,
470
+ include_missing=include_missing,
471
+ equal_freq=self.equal_freq,
472
+ chi2_method=self.chi2_method,
473
+ chi2_p=self.chi2_p,
474
+ init_equi_bins=self.init_equi_bins,
475
+ fillna=fillna,
476
+ spec_values=spec_values,
477
+ tree_binning=self.tree_binning,
478
+ random_state=self.seed,
479
+ ascending=ascending,
480
+ weight_col=self.weight_col,
481
+ )
380
482
 
381
483
  def __init_excel_master(self):
382
484
  """
@@ -491,13 +593,17 @@ class Model_Evaluation_Tool:
491
593
  dist_bins: int = 100,
492
594
  pct_bins: int = 10,
493
595
  min_data_size: int = 50,
494
- sync_data_size: bool = True
596
+ sync_data_size: bool = True,
597
+ min_bin_prop: Optional[float] = None,
598
+ include_missing: Optional[bool] = None,
599
+ equal_freq: Optional[bool] = None,
495
600
  ) -> pd.DataFrame:
496
601
  """
497
602
  Compare model performance across different scores.
498
603
 
499
604
  This method calculates performance metrics (AUC, KS, etc.) for the base score
500
- and all comparison scores, optionally filtering by group.
605
+ and all comparison scores via ``PerformanceEvaluator``, optionally filtering
606
+ by group.
501
607
 
502
608
  Parameters
503
609
  ----------
@@ -513,108 +619,90 @@ class Model_Evaluation_Tool:
513
619
  Minimum data size per bin. Default is 50.
514
620
  sync_data_size : bool, optional
515
621
  Whether to filter out zero/negative scores. Default is True.
622
+ min_bin_prop : float, optional
623
+ Minimum bin proportion. Defaults to the instance setting.
624
+ include_missing : bool, optional
625
+ Whether to include missing values in performance binning.
626
+ Defaults to ``False`` for performance comparison.
627
+ equal_freq : bool, optional
628
+ Use equal-frequency binning. Defaults to the instance setting.
516
629
 
517
630
  Returns
518
631
  -------
519
632
  pandas.DataFrame
520
633
  Performance comparison results sorted by score order.
521
634
  """
522
-
523
635
  data = self.data.copy() if data is None else data.copy()
524
636
  score_list = self.comp_scrlist
525
637
  dep = self.dep
526
638
  base_score = self.base_score
527
- precision = self.precision
528
-
529
- # 过滤掉无效分数和目标值
639
+ sample_name = 'oot'
640
+
641
+ if base_score is None:
642
+ return pd.DataFrame()
643
+
530
644
  valid_mask = (
531
- data[dep].notna() &
532
- data[base_score].notna() &
533
- np.isfinite(data[base_score])
645
+ data[dep].notna()
646
+ & data[base_score].notna()
647
+ & np.isfinite(data[base_score])
534
648
  )
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
-
649
+ clean_data = data.loc[valid_mask]
650
+ if clean_data.empty:
651
+ return pd.DataFrame()
652
+
653
+ shared_data = clean_data.copy()
654
+ if sync_data_size and score_list:
655
+ shared_data = self._filter_positive_scores(shared_data, score_list)
656
+
550
657
  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,
658
+ score_order = self._score_order()
659
+
660
+ for score in score_order:
661
+ if score not in clean_data.columns:
662
+ continue
663
+ score_data = shared_data if sync_data_size and score in (score_list or []) else clean_data
664
+ score_data = score_data.loc[score_data[score] > 0] if self.positive_score_only else score_data
665
+ if score_data.empty:
666
+ continue
667
+
668
+ evaluator = self._build_performance_evaluator(
669
+ score=score,
561
670
  dist_bins=dist_bins,
562
671
  pct_bins=pct_bins,
563
- precision=precision,
564
- min_bin_prop=0.05,
565
- include_missing=False,
566
- equal_freq=True,
672
+ min_bin_prop=min_bin_prop,
673
+ include_missing=include_missing,
674
+ equal_freq=equal_freq,
675
+ )
676
+ perf = evaluator.add_dataset(sample_name, score_data).evaluate(
567
677
  oot_grp_name=grp_name,
568
- min_data_size=min_data_size
678
+ min_data_size=min_data_size,
679
+ to_show=False,
680
+ display=False,
569
681
  )
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
-
682
+ if isinstance(perf, pd.DataFrame) and not perf.empty and 'index' in perf.columns:
683
+ perf = perf.query(f"index == '{sample_name}'")
684
+ perf_comp_dict[score] = perf
685
+
597
686
  if not perf_comp_dict:
598
687
  return pd.DataFrame()
599
-
688
+
600
689
  perf_comp_res = []
601
690
  for score_name, perf in perf_comp_dict.items():
602
691
  perf = perf.copy()
603
692
  perf['score_name'] = score_name
604
693
  perf_comp_res.append(perf)
605
-
694
+
606
695
  perf_comp_res = pd.concat(perf_comp_res)
607
-
696
+
608
697
  drop_cols = ['AUC_Shift', 'KS_Shift']
609
698
  for col in drop_cols:
610
699
  if col in perf_comp_res.columns:
611
700
  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}
701
+
702
+ order_map = {val: i for i, val in enumerate(score_order) if val in perf_comp_dict}
615
703
  perf_comp_res['sort_key'] = perf_comp_res['score_name'].map(order_map)
616
704
  perf_comp_res_sorted = perf_comp_res.sort_values('sort_key').drop('sort_key', axis=1)
617
-
705
+
618
706
  return perf_comp_res_sorted
619
707
 
620
708
  def get_gains_summary(
@@ -627,15 +715,16 @@ class Model_Evaluation_Tool:
627
715
  withSummary: bool = True,
628
716
  add_func: Optional[Callable] = None,
629
717
  sync_range: bool = True,
630
- spec_values = [],
631
- include_missing = False,
632
- fillna = None
718
+ spec_values=None,
719
+ include_missing: Optional[bool] = None,
720
+ fillna=None,
633
721
  ) -> pd.DataFrame:
634
722
  """
635
723
  Generate gains summary table for score analysis.
636
724
 
637
- This method creates gains tables showing the distribution of targets
638
- across score bins, with optional group stratification.
725
+ This method creates gains tables via ``GainsTableCalculator``, showing the
726
+ distribution of targets across score bins, with optional group stratification.
727
+ Custom metrics can be injected through ``add_func``.
639
728
 
640
729
  Parameters
641
730
  ----------
@@ -652,9 +741,15 @@ class Model_Evaluation_Tool:
652
741
  withSummary : bool, optional
653
742
  Include summary row. Default is True.
654
743
  add_func : callable, optional
655
- Additional function to apply to gains table.
744
+ Custom metric function merged into each gains table.
656
745
  sync_range : bool, optional
657
746
  Synchronize bin ranges. Default is True.
747
+ spec_values : list, optional
748
+ Special values to treat separately during binning.
749
+ include_missing : bool, optional
750
+ Whether to include missing values. Defaults to the instance setting.
751
+ fillna : any, optional
752
+ Missing-value fill for score binning. Defaults to the instance setting.
658
753
 
659
754
  Returns
660
755
  -------
@@ -663,188 +758,120 @@ class Model_Evaluation_Tool:
663
758
  """
664
759
  if grp_disp_metric is None:
665
760
  grp_disp_metric = ['N', 'PROP', 'AVG_BAD', 'LIFT']
666
-
761
+
667
762
  if fillna is None:
668
763
  fillna = self.fillna
669
-
764
+ if spec_values is None:
765
+ spec_values = self.spec_values
766
+ if include_missing is None:
767
+ include_missing = self.include_missing
768
+
670
769
  new_oot_df = self.data.copy() if data is None else data.copy()
671
-
770
+
672
771
  dep = self.dep
673
- tree_binning = self.tree_binning
674
- digit = self.precision
675
772
  base_score = self.base_score
676
- equal_freq = self.equal_freq
677
773
  min_data_size = self.min_data_size
678
774
  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
775
+
776
+ score_list = self._score_order()
682
777
  display_metric_list = self.gains_display_metric_list if add_func is None else None
683
-
778
+
684
779
  if self.data is None or len(self.data) == 0:
685
780
  empty_df = pd.DataFrame()
686
781
  empty_df.index = pd.MultiIndex.from_tuples([], names=['_bin_num', '_bin_range'])
687
- return empty_df # 或返回空结果
688
-
782
+ return empty_df
783
+
689
784
  gains_table_dict = {}
690
785
  for score in score_list:
786
+ if score not in new_oot_df.columns:
787
+ continue
788
+
789
+ calculator = self._build_gains_calculator(
790
+ data=new_oot_df,
791
+ score=score,
792
+ nbins=nbins,
793
+ fillna=fillna,
794
+ spec_values=spec_values,
795
+ include_missing=include_missing,
796
+ ascending=True,
797
+ )
798
+
691
799
  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,
800
+ gains_table_dict[score] = calculator.calculate(
706
801
  withSummary=withSummary,
707
802
  add_func=add_func,
708
- spec_values=spec_values
709
803
  )
710
804
  if display_metric_list is not None:
711
805
  gains_table_dict[score] = gains_table_dict[score][display_metric_list]
712
806
  gains_table_dict[score] = gains_table_dict[score].reset_index(drop=False)
713
-
714
807
  else:
715
808
  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,
809
+ oot_by_group = calculator.calculate(
810
+ grp_name=grp_name,
731
811
  min_data_size=min_data_size,
812
+ sync_range=sync_range,
813
+ retSummary=False,
732
814
  withSummary=withSummary,
733
- spec_values=spec_values,
734
- add_func=add_func
815
+ add_func=add_func,
735
816
  )
736
817
  if display_metric_list is not None:
737
818
  oot_by_group = oot_by_group[[*display_metric_list, grp_name]]
738
819
  oot_by_group = oot_by_group.reset_index(drop=False)
739
-
820
+
740
821
  grp_metric_dict = {}
741
822
  for value in [x for x in grp_disp_metric if x not in ['PROP']]:
742
823
  grp_metric_dict[value] = oot_by_group\
743
824
  .reset_index(drop=False)\
744
- .pivot_table(index=['_bin_num', '_bin_range'], columns=[grp_name], values=value, margins=True, margins_name="Grand_Total")
745
-
825
+ .pivot_table(
826
+ index=['_bin_num', '_bin_range'],
827
+ columns=[grp_name],
828
+ values=value,
829
+ margins=True,
830
+ margins_name="Grand_Total",
831
+ )
832
+
746
833
  if 'PROP' in grp_disp_metric:
747
834
  tot_cnt = oot_by_group\
748
835
  .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
-
836
+ .pivot_table(
837
+ index=['_bin_num', '_bin_range'],
838
+ columns=[grp_name],
839
+ values=['N'],
840
+ margins=True,
841
+ margins_name="Grand_Total",
842
+ aggfunc='sum',
843
+ )
844
+
751
845
  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
-
846
+ grp_metric_dict['PROP'] = tot_cnt.iloc[0:tot_cnt.shape[0]:] / np.array(
847
+ tot_cnt.iloc[-1:].T['Grand_Total'].tolist()
848
+ )
849
+ grp_metric_dict['PROP'].columns = grp_metric_dict['PROP'].rename(
850
+ columns={'N': ''}
851
+ ).columns.map("".join)
852
+
755
853
  fnl_grp_gains_res = []
756
854
  for colname, gains in grp_metric_dict.items():
757
855
  gains = gains.copy()
758
856
  gains['variable'] = colname
759
- # gains = gains.reset_index(drop=False)
760
857
  if disp:
761
858
  from IPython.display import display
762
859
  display(gains)
763
860
  fnl_grp_gains_res.append(gains)
764
861
  fnl_grp_gains_res = pd.concat(fnl_grp_gains_res)
765
-
862
+
766
863
  gains_table_dict[score] = oot_by_group
767
-
864
+
768
865
  fnl_gains_res = []
769
866
  for score_name, gains_res in gains_table_dict.items():
770
867
  gains_res = gains_res.copy()
771
868
  gains_res['score_name'] = score_name
772
869
  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
870
+
871
+ if not fnl_gains_res:
872
+ return pd.DataFrame()
873
+
874
+ return pd.concat(fnl_gains_res)
848
875
 
849
876
  def get_cross_risk_summary(
850
877
  self,
@@ -852,7 +879,8 @@ class Model_Evaluation_Tool:
852
879
  nbins: int = 5,
853
880
  equal_freq: Optional[bool] = None,
854
881
  disp: bool = True,
855
- spec_values = []
882
+ spec_values=None,
883
+ binning_numeric=None,
856
884
  ) -> pd.DataFrame:
857
885
  """
858
886
  Generate cross-risk analysis summary between base and comparison scores.
@@ -870,6 +898,13 @@ class Model_Evaluation_Tool:
870
898
  Use equal frequency binning. If None, uses self.equal_freq.
871
899
  disp : bool, optional
872
900
  Whether to display results. Default is True.
901
+ spec_values : list, optional
902
+ Special values to keep separate during binning.
903
+ binning_numeric : bool or list/tuple of bool, optional
904
+ Whether numeric score columns should be binned before cross-risk
905
+ aggregation. If None, defaults to [True, True] for backward
906
+ compatibility. Passing a bool applies the same setting to both
907
+ base and comparison score.
873
908
 
874
909
  Returns
875
910
  -------
@@ -890,7 +925,41 @@ class Model_Evaluation_Tool:
890
925
 
891
926
  if cross_agg_dict is None:
892
927
  cross_agg_dict = self.cross_agg_dict.copy()
893
-
928
+ if spec_values is None:
929
+ spec_values = self.spec_values
930
+ if binning_numeric is None:
931
+ binning_numeric = [True, True]
932
+ elif isinstance(binning_numeric, (bool, np.bool_)):
933
+ binning_numeric = [bool(binning_numeric), bool(binning_numeric)]
934
+ elif isinstance(binning_numeric, (list, tuple)):
935
+ if len(binning_numeric) != 2:
936
+ raise ValueError(
937
+ "binning_numeric must be a bool or a two-element list/tuple."
938
+ )
939
+ if not all(isinstance(value, (bool, np.bool_)) for value in binning_numeric):
940
+ raise TypeError("binning_numeric values must be booleans.")
941
+ binning_numeric = [bool(binning_numeric[0]), bool(binning_numeric[1])]
942
+ else:
943
+ raise TypeError("binning_numeric must be None, a bool, or a two-element list/tuple.")
944
+
945
+ def _ensure_three_level_columns(frame: pd.DataFrame) -> pd.DataFrame:
946
+ frame = frame.copy()
947
+ if isinstance(frame.columns, pd.MultiIndex):
948
+ normalized_columns = []
949
+ for col in frame.columns:
950
+ col_tuple = tuple(col)
951
+ if len(col_tuple) < 3:
952
+ col_tuple = col_tuple + ("",) * (3 - len(col_tuple))
953
+ elif len(col_tuple) > 3:
954
+ col_tuple = col_tuple[:2] + ("_".join(map(str, col_tuple[2:])),)
955
+ normalized_columns.append(col_tuple)
956
+ frame.columns = pd.MultiIndex.from_tuples(normalized_columns)
957
+ else:
958
+ frame.columns = pd.MultiIndex.from_tuples(
959
+ [(col, "", "") for col in frame.columns]
960
+ )
961
+ return frame
962
+
894
963
  multi_scr_res = {}
895
964
  for compare_scr in compare_scrlist:
896
965
  logger.info(compare_scr)
@@ -919,7 +988,7 @@ class Model_Evaluation_Tool:
919
988
  min_bin_prop=min_bin_prop,
920
989
  include_missing=include_missing,
921
990
  equal_freq=equal_freq,
922
- binning_numeric=[True, True],
991
+ binning_numeric=binning_numeric,
923
992
  tree_binning=tree_binning,
924
993
  agg_func=aggfunc,
925
994
  fillna=fillna,
@@ -928,7 +997,7 @@ class Model_Evaluation_Tool:
928
997
 
929
998
  fnl_res = []
930
999
  for colname, res in cross_res.items():
931
- res = res.copy()
1000
+ res = _ensure_three_level_columns(res)
932
1001
  res[('', 'eval_metric', '')] = colname
933
1002
  res = res.rename(columns={"<lambda>": "", "count": "n"})
934
1003
  if disp:
@@ -1449,4 +1518,4 @@ class Model_Evaluation_Tool:
1449
1518
  -------
1450
1519
  EvaluationPipeline
1451
1520
  """
1452
- return EvaluationPipeline(self, data)
1521
+ return EvaluationPipeline(self, data)