SuperModelingFactory 0.6.4__tar.gz → 0.6.6__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 (115) hide show
  1. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/Model_Eval_Tool.py +55 -8
  2. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +1 -1
  3. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/weighted_eval_utils.py +102 -13
  4. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/_common.py +63 -0
  5. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/credit_model.py +386 -29
  6. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/feature_validation.py +26 -2
  7. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/field_meta.py +16 -0
  8. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/__init__.py +1 -1
  9. {supermodelingfactory-0.6.4/SuperModelingFactory.egg-info → supermodelingfactory-0.6.6}/PKG-INFO +2 -2
  10. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/README.md +1 -1
  11. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6/SuperModelingFactory.egg-info}/PKG-INFO +2 -2
  12. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/pyproject.toml +1 -1
  13. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/setup.py +1 -1
  14. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/ExcelMaster/ExcelFormatTool.py +0 -0
  15. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/ExcelMaster/ExcelMaster.py +0 -0
  16. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/ExcelMaster/Template.py +0 -0
  17. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/ExcelMaster/Utility.py +0 -0
  18. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/ExcelMaster/__init__.py +0 -0
  19. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/LICENSE +0 -0
  20. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/MANIFEST.in +0 -0
  21. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  22. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  23. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  24. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  25. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  26. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  27. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  28. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  29. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  30. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  31. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  32. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  33. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  34. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/__init__.py +0 -0
  35. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/kDataFrame.py +0 -0
  36. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  37. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  38. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Core/utils.py +0 -0
  39. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  40. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  41. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/__init__.py +0 -0
  42. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  43. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  44. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  45. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  46. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Explainability/__init__.py +0 -0
  47. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  48. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  49. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  50. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  51. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
  52. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/ODPS_Distribution_Tool.py +0 -0
  53. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/ODPS_Distribution_Tool.pyi +0 -0
  54. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  55. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  56. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  57. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/Weighted_Screen.py +0 -0
  58. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Feature/__init__.py +0 -0
  59. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  60. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  61. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  62. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  63. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  64. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  65. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  66. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Model/__init__.py +0 -0
  67. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/__init__.py +0 -0
  68. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  69. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  70. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  71. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  72. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  73. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  74. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  75. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  76. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  77. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  78. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  79. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  80. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  81. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/Sample/__init__.py +0 -0
  82. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  83. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/UAT/__init__.py +0 -0
  84. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  85. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  86. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  87. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  88. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  89. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  90. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  91. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  92. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  93. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  94. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  95. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  96. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/__init__.py +0 -0
  97. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  98. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  99. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/_utils/__init__.py +0 -0
  100. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/_utils/nan_guard.py +0 -0
  101. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/_utils/robust.py +0 -0
  102. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/_utils/sentinels.py +0 -0
  103. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  104. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  105. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/ref_font/__init__.py +0 -0
  106. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  107. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Report/Report_Tool.py +0 -0
  108. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/Report/__init__.py +0 -0
  109. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  110. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  111. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  112. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/SuperModelingFactory.egg-info/requires.txt +0 -0
  113. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  114. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/requirements.txt +0 -0
  115. {supermodelingfactory-0.6.4 → supermodelingfactory-0.6.6}/setup.cfg +0 -0
@@ -2169,7 +2169,7 @@ class PerformanceEvaluator:
2169
2169
  dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05,
2170
2170
  include_missing = False, equal_freq = True, chi2_method = False,
2171
2171
  init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42,
2172
- weight_col = None):
2172
+ weight_col = None, spec_values = None, ascending = None):
2173
2173
  """
2174
2174
  初始化性能评估器。
2175
2175
 
@@ -2225,9 +2225,29 @@ class PerformanceEvaluator:
2225
2225
  self.tree_binning = tree_binning
2226
2226
  self.random_state = random_state
2227
2227
  self.weight_col = weight_col
2228
+ # spec_values: sentinel scores (e.g. -1 for all-missing overrides)
2229
+ # that get their own evaluation bin and are excluded from quantile
2230
+ # edges and ranking metrics. ascending: None keeps every underlying
2231
+ # function's own legacy default; an explicit bool is threaded through
2232
+ # summary/gains/plot and the weighted path uniformly.
2233
+ self.spec_values = list(spec_values) if spec_values else []
2234
+ self.ascending = ascending
2228
2235
  self.datasets = {}
2229
2236
  self.dataset_weight_cols = {}
2230
2237
  self.evaluate_status = None
2238
+
2239
+ def _governed_eval_kwargs(self):
2240
+ """Kwargs to thread spec_values/ascending into underlying eval calls.
2241
+
2242
+ Nothing is passed when unset, so every legacy default stays intact
2243
+ (unweighted summary False, comparison paths True, weighted False).
2244
+ """
2245
+ kwargs = {}
2246
+ if self.spec_values:
2247
+ kwargs["spec_values"] = list(self.spec_values)
2248
+ if self.ascending is not None:
2249
+ kwargs["ascending"] = bool(self.ascending)
2250
+ return kwargs
2231
2251
 
2232
2252
  def add_dataset(self, name, data, weight_col = None, overwrite = False):
2233
2253
  """
@@ -2351,6 +2371,8 @@ class PerformanceEvaluator:
2351
2371
  scr_name,
2352
2372
  weight_col=wc,
2353
2373
  nbins=self.pct_bins,
2374
+ ascending=bool(self.ascending) if self.ascending is not None else False,
2375
+ spec_values=self.spec_values or None,
2354
2376
  )
2355
2377
  )
2356
2378
  fnl_df = pd.DataFrame(rows)
@@ -2368,15 +2390,23 @@ class PerformanceEvaluator:
2368
2390
  if scr_name is None:
2369
2391
  scr_name = "_mdl_scr"
2370
2392
  work_data[scr_name] = self.model.predict_proba(work_data.loc[:, self.feature_cols])[:, 1]
2371
- payload = {
2372
- "y_true": work_data[self.tgt_name],
2373
- "y_score": work_data[scr_name],
2374
- }
2375
2393
  resolved_weight = _weighted_eval.resolve_weights(
2376
2394
  work_data,
2377
2395
  weight_col=wc,
2378
2396
  expected_len=len(work_data),
2379
2397
  )
2398
+ y_true = work_data[self.tgt_name].to_numpy()
2399
+ y_score = work_data[scr_name].to_numpy()
2400
+ if self.spec_values:
2401
+ keep = ~pd.Series(y_score).isin(self.spec_values).to_numpy()
2402
+ y_true = y_true[keep]
2403
+ y_score = y_score[keep]
2404
+ if resolved_weight is not None:
2405
+ resolved_weight = np.asarray(resolved_weight)[keep]
2406
+ payload = {
2407
+ "y_true": y_true,
2408
+ "y_score": y_score,
2409
+ }
2380
2410
  if resolved_weight is not None:
2381
2411
  payload["sample_weight"] = resolved_weight
2382
2412
  eval_datasets[name] = payload
@@ -2445,6 +2475,21 @@ class PerformanceEvaluator:
2445
2475
  return data[self.scr_name]
2446
2476
  return self.model.predict_proba(data.loc[:, self.feature_cols])[:, 1]
2447
2477
 
2478
+ def _rank_payload(data):
2479
+ """(y_true, y_score) with special sentinel scores removed.
2480
+
2481
+ Sentinels like the all-missing override -1 carry no ordering
2482
+ information: AUC/KS/distribution figures are computed on the
2483
+ non-special rows; their bin-level accounting lives in the gains
2484
+ table via spec_values.
2485
+ """
2486
+ y_true = data[self.tgt_name]
2487
+ y_score = _get_score(data)
2488
+ if not self.spec_values:
2489
+ return y_true, y_score
2490
+ keep = ~pd.Series(np.asarray(y_score)).isin(self.spec_values).to_numpy()
2491
+ return np.asarray(y_true)[keep], np.asarray(y_score)[keep]
2492
+
2448
2493
  def _get_benchmark_bin_edges():
2449
2494
  if benchmark_dataset is None:
2450
2495
  return None
@@ -2492,9 +2537,10 @@ class PerformanceEvaluator:
2492
2537
  for name, data in dataset_dict.items():
2493
2538
  if data is None:
2494
2539
  continue
2540
+ y_true, y_score = _rank_payload(data)
2495
2541
  eval_datasets[name] = {
2496
- "y_true": data[self.tgt_name],
2497
- "y_score": _get_score(data)
2542
+ "y_true": y_true,
2543
+ "y_score": y_score
2498
2544
  }
2499
2545
 
2500
2546
  if len(eval_datasets) == 0:
@@ -2553,7 +2599,8 @@ class PerformanceEvaluator:
2553
2599
  chi2_p = self.chi2_p,
2554
2600
  retSummary = True,
2555
2601
  tree_binning = False if benchmark_bin_edges is not None else self.tree_binning,
2556
- random_state = self.random_state
2602
+ random_state = self.random_state,
2603
+ **self._governed_eval_kwargs(),
2557
2604
  )
2558
2605
  gains_res['index'] = name
2559
2606
  gains_summ_list.append(gains_res)
@@ -32,6 +32,6 @@ class GainsTableCalculator:
32
32
  def calculate(self, grp_name = None, min_data_size = 100, grp_colname = None, sync_range = True, retSummary = False, withSummary = False, wholeGroup = False, add_func = None): ...
33
33
 
34
34
  class PerformanceEvaluator:
35
- def __init__(self, tgt_name, scr_name = None, model = None, feature_cols = None, dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, chi2_method = False, init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42, weight_col = None): ...
35
+ def __init__(self, tgt_name, scr_name = None, model = None, feature_cols = None, dist_bins = 20, pct_bins = 10, precision = 5, min_bin_prop = 0.05, include_missing = False, equal_freq = True, chi2_method = False, init_equi_bins = 1000, chi2_p = 0.9, tree_binning = False, random_state = 42, weight_col = None, spec_values = None, ascending = None): ...
36
36
  def add_dataset(self, name, data, weight_col = None, overwrite = False): ...
37
37
  def evaluate(self, oot_grp_name = None, min_data_size = 100, grp_colname = None, fig_save_path = None, rpt_save_path = None, to_show = False, display = True, gains_table = False, benchmark_dataset = None, weight_col = None): ...
@@ -95,10 +95,18 @@ def calc_pr(y_true, y_score, sample_weight=None):
95
95
  )
96
96
 
97
97
 
98
- def rank_bins(score, weight, nbins):
98
+ def rank_bins(score, weight, nbins, ascending=False):
99
+ """Weighted equal-frequency rank bins.
100
+
101
+ ``ascending=False`` (default, legacy) sorts scores descending — bin 1 is
102
+ the highest-score bucket. ``ascending=True`` sorts scores ascending so
103
+ bin 1 is the lowest-score bucket, matching the unweighted
104
+ ``gains_ascending=True`` convention.
105
+ """
99
106
  score = np.asarray(score, dtype=float)
100
107
  weight = np.ones(len(score), dtype=float) if weight is None else np.asarray(weight, dtype=float)
101
- order = np.lexsort((np.arange(len(score)), -score))
108
+ sort_key = score if ascending else -score
109
+ order = np.lexsort((np.arange(len(score)), sort_key))
102
110
  bins = np.empty(len(score), dtype=int)
103
111
  total = float(weight.sum()) or float(len(score)) or 1.0
104
112
  cum_weight = np.cumsum(weight[order])
@@ -108,18 +116,35 @@ def rank_bins(score, weight, nbins):
108
116
  return bins
109
117
 
110
118
 
111
- def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binning=None, **kwargs):
119
+ def _split_special_scores(df, score, spec_values):
120
+ """Split a frame into (normal, special) rows by exact score membership.
121
+
122
+ Special score values (e.g. the all-missing override ``-1``) are business
123
+ sentinels, not model outputs: they must not participate in quantile
124
+ binning or ranking metrics.
125
+ """
126
+ if not spec_values:
127
+ return df, None
128
+ spec_mask = df[score].isin(list(spec_values))
129
+ if not bool(spec_mask.any()):
130
+ return df, None
131
+ return df[~spec_mask], df[spec_mask]
132
+
133
+
134
+ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binning=None,
135
+ ascending=False, spec_values=None, **kwargs):
112
136
  cols = [dep, score]
113
137
  if weight_col is not None and weight_col in data.columns:
114
138
  cols.append(weight_col)
115
139
  df = data[cols].copy()
140
+ df, special_df = _split_special_scores(df, score, spec_values)
116
141
  weight = resolve_weights(df, weight_col=weight_col, expected_len=len(df))
117
142
  if weight is None:
118
143
  weight = np.ones(len(df), dtype=float)
119
144
 
120
145
  y = df[dep].astype(float).to_numpy()
121
146
  s = df[score].astype(float).to_numpy()
122
- df["_bin_num"] = rank_bins(s, weight, nbins)
147
+ df["_bin_num"] = rank_bins(s, weight, nbins, ascending=ascending)
123
148
  df["_bin_range"] = df["_bin_num"]
124
149
  df["_w"] = weight
125
150
  df["_bad_w"] = weight * y
@@ -165,13 +190,56 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
165
190
  out["KS_PER_BIN"] = (out["CUM_BAD_PCT"] - out["CUM_GOOD_PCT"]).abs()
166
191
  out["KS"] = out["KS_PER_BIN"]
167
192
  out["LIFT"] = out["AVG_BAD"] / overall_bad_rate if overall_bad_rate else np.nan
168
- out["TRUE_BAD_SHIFT"] = out["AVG_BAD"].shift(1) / out["AVG_BAD"] - 1
193
+ out["TRUE_BAD_SHIFT"] = (
194
+ (out["AVG_BAD"].shift(1) / out["AVG_BAD"] - 1)
195
+ if not ascending
196
+ else (out["AVG_BAD"] / out["AVG_BAD"].shift(1) - 1)
197
+ )
169
198
  out["RANK_ORDER_BUMP"] = out["TRUE_BAD_SHIFT"].lt(0).astype(int)
170
199
  with np.errstate(divide="ignore", invalid="ignore"):
171
200
  out["WOE"] = np.log(out["BAD_PCT_IN_EACH_BIN"] / out["GOOD_PCT_IN_EACH_BIN"])
172
201
  out["WOE"] = out["WOE"].replace([np.inf, -np.inf], 0).fillna(0)
173
202
  out["IV"] = (out["BAD_PCT_IN_EACH_BIN"] - out["GOOD_PCT_IN_EACH_BIN"]) * out["WOE"]
174
203
  out["AUC"] = safe_auc(y, s, sample_weight=weight)
204
+
205
+ if special_df is not None and len(special_df):
206
+ # Special sentinel scores (e.g. -1 for all-missing rows) get their own
207
+ # descriptive rows: never part of quantile edges, cumulative columns,
208
+ # or ranking metrics (those stay NaN by construction).
209
+ spec_weight = resolve_weights(special_df, weight_col=weight_col, expected_len=len(special_df))
210
+ if spec_weight is None:
211
+ spec_weight = np.ones(len(special_df), dtype=float)
212
+ grand_total = total_weight + float(np.sum(spec_weight))
213
+ spec_rows = []
214
+ for value, part in special_df.groupby(score, sort=True):
215
+ part_w = resolve_weights(part, weight_col=weight_col, expected_len=len(part))
216
+ if part_w is None:
217
+ part_w = np.ones(len(part), dtype=float)
218
+ part_y = part[dep].astype(float).to_numpy()
219
+ n_w = float(np.sum(part_w))
220
+ n_bad = float(np.sum(part_w * part_y))
221
+ label = f"special:{value}"
222
+ spec_rows.append(
223
+ pd.DataFrame(
224
+ {
225
+ "MIN": [value],
226
+ "MAX": [value],
227
+ "N": [n_w],
228
+ "N_RAW": [int(len(part))],
229
+ "PERF_CNT": [n_w],
230
+ "N_BAD": [n_bad],
231
+ "N_GOOD": [n_w - n_bad],
232
+ "AVG_SCORE": [float(value)],
233
+ "UNIQUE_SCORE": [1],
234
+ "PROP": [n_w / grand_total if grand_total else np.nan],
235
+ "AVG_BAD": [n_bad / n_w if n_w else np.nan],
236
+ "AVG_GOOD": [(n_w - n_bad) / n_w if n_w else np.nan],
237
+ },
238
+ index=pd.MultiIndex.from_tuples([(label, label)], names=out.index.names),
239
+ )
240
+ )
241
+ out = pd.concat([out] + spec_rows)
242
+ out["AUC"] = out["AUC"].iloc[0]
175
243
  return out
176
244
 
177
245
 
@@ -212,32 +280,53 @@ def calc_fixed_pct(y_true, y_score, sample_weight=None, **kwargs):
212
280
  return calc_equid_dist(y_true, y_score, sample_weight=sample_weight, **kwargs)
213
281
 
214
282
 
215
- def dataset_summary(name, data, tgt_name, scr_name, weight_col=None, nbins=10):
283
+ def dataset_summary(name, data, tgt_name, scr_name, weight_col=None, nbins=10,
284
+ ascending=False, spec_values=None):
216
285
  weight = resolve_weights(data, weight_col=weight_col, expected_len=len(data))
217
286
  y_true = data[tgt_name].to_numpy()
218
287
  y_score = data[scr_name].to_numpy()
219
- roc_df = calc_roc(y_true, y_score, sample_weight=weight)
220
- gains = get_gains_table(data, tgt_name, scr_name, nbins=nbins, weight_col=weight_col)
221
- return {
288
+ # Ranking metrics (AUC/KS/avgScore) are computed on non-special rows only:
289
+ # sentinel scores like -1 carry no ordering information.
290
+ rank_data, special_part = _split_special_scores(data, scr_name, spec_values)
291
+ rank_weight = resolve_weights(rank_data, weight_col=weight_col, expected_len=len(rank_data))
292
+ rank_true = rank_data[tgt_name].to_numpy()
293
+ rank_score = rank_data[scr_name].to_numpy()
294
+ roc_df = calc_roc(rank_true, rank_score, sample_weight=rank_weight)
295
+ gains = get_gains_table(
296
+ data, tgt_name, scr_name, nbins=nbins, weight_col=weight_col,
297
+ ascending=ascending, spec_values=spec_values,
298
+ )
299
+ summary = {
222
300
  "index": name,
223
301
  "dataset": name,
224
302
  "DATASET": name,
225
- "AUC": safe_auc(y_true, y_score, sample_weight=weight),
303
+ "AUC": safe_auc(rank_true, rank_score, sample_weight=rank_weight),
226
304
  "KS": float(roc_df["KS"].max()) if "KS" in roc_df else np.nan,
227
305
  "LIFT": float(gains["LIFT"].max()) if "LIFT" in gains else np.nan,
228
306
  "IV": float(gains["IV"].sum()) if "IV" in gains else np.nan,
229
307
  "N": float(np.sum(weight)) if weight is not None else float(len(data)),
230
308
  "N_RAW": int(len(data)),
231
309
  "avgTrue": safe_weighted_average(y_true, weight),
232
- "avgScore": safe_weighted_average(y_score, weight),
310
+ "avgScore": safe_weighted_average(rank_score, rank_weight),
233
311
  }
312
+ if special_part is not None:
313
+ spec_weight = resolve_weights(special_part, weight_col=weight_col, expected_len=len(special_part))
314
+ summary["N_SPECIAL"] = (
315
+ float(np.sum(spec_weight)) if spec_weight is not None else float(len(special_part))
316
+ )
317
+ summary["N_SPECIAL_RAW"] = int(len(special_part))
318
+ return summary
234
319
 
235
320
 
236
- def get_perf_summary(train=None, validation=None, oot=None, tgt_name=None, scr_name=None, weight_col=None, nbins=10, **kwargs):
321
+ def get_perf_summary(train=None, validation=None, oot=None, tgt_name=None, scr_name=None, weight_col=None, nbins=10,
322
+ ascending=False, spec_values=None, **kwargs):
237
323
  rows = []
238
324
  for name, data in (("ins", train), ("oos", validation), ("oot", oot)):
239
325
  if data is not None:
240
- rows.append(dataset_summary(name, data, tgt_name, scr_name, weight_col=weight_col, nbins=nbins))
326
+ rows.append(dataset_summary(
327
+ name, data, tgt_name, scr_name, weight_col=weight_col, nbins=nbins,
328
+ ascending=ascending, spec_values=spec_values,
329
+ ))
241
330
  return pd.DataFrame(rows)
242
331
 
243
332
 
@@ -532,6 +532,69 @@ def copy_column_length_checked(
532
532
  dst[col] = src[col].to_numpy()
533
533
 
534
534
 
535
+ def all_missing_mask(data: pd.DataFrame, feature_cols: list[str]) -> np.ndarray:
536
+ """Rows where every listed feature is missing.
537
+
538
+ Same membership test as ``Core.scoring``'s all-missing branch
539
+ (``pd.isnull(data[varlist]).sum(axis=1) == len(varlist)``), so the
540
+ pipeline's evaluation override and the production scoring API agree on
541
+ which rows count as un-scorable.
542
+ """
543
+ features = list(feature_cols or [])
544
+ if not features:
545
+ return np.zeros(len(data), dtype=bool)
546
+ absent = [col for col in features if col not in data.columns]
547
+ if absent:
548
+ raise KeyError(
549
+ f"all-missing score override needs raw feature columns "
550
+ f"{absent[:5]} which are absent from the evaluation frame."
551
+ )
552
+ return (pd.isnull(data[features]).sum(axis=1) == len(features)).to_numpy()
553
+
554
+
555
+ def resolve_missing_oot(
556
+ oos: pd.DataFrame,
557
+ synthesize: bool,
558
+ pipeline: str,
559
+ ) -> pd.DataFrame | None:
560
+ """Decide what to do when a run has no real OOT rows.
561
+
562
+ ``synthesize=True`` preserves the historical behavior — the OOS frame is
563
+ copied in as a stand-in OOT — but now emits a loud ``UserWarning`` so the
564
+ placeholder can never be mistaken for a real out-of-time sample.
565
+ ``synthesize=False`` returns ``None``: the caller must then leave OOT out
566
+ of its splits (missing key or empty frame) so no downstream stage
567
+ evaluates, searches, or reports against a synthetic OOT.
568
+ """
569
+ if synthesize:
570
+ warnings.warn(
571
+ f"{pipeline}: no real OOT rows found; OOS ({len(oos)} rows) was "
572
+ f"copied in as a stand-in OOT. Metrics labeled 'oot' are OOS "
573
+ f"metrics, NOT out-of-time performance. Set "
574
+ f"synthesize_missing_oot=False (or candidate_mode=True) to drop "
575
+ f"the placeholder instead.",
576
+ UserWarning,
577
+ stacklevel=3,
578
+ )
579
+ return oos.copy()
580
+ return None
581
+
582
+
583
+ def assert_split_allowed(
584
+ split: str,
585
+ forbidden: list[str] | set[str] | None,
586
+ component: str,
587
+ ) -> None:
588
+ """Hard gate for split governance: raise if ``component`` is about to
589
+ consume a split listed in ``forbidden_splits``."""
590
+ if forbidden and split in set(forbidden):
591
+ raise ValueError(
592
+ f"{component} requested split {split!r}, but it is listed in "
593
+ f"forbidden_splits {sorted(set(forbidden))}. Remove the split from "
594
+ f"the component's configuration or lift the restriction."
595
+ )
596
+
597
+
535
598
  def split_oot_by_flag(
536
599
  data: pd.DataFrame,
537
600
  oot_col: str | None,