SuperModelingFactory 0.3.13__tar.gz → 0.3.15__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 (108) hide show
  1. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/Distribution_Tool.py +34 -0
  2. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/Distribution_Tool.pyi +1 -0
  3. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/Feature_Insights.py +6 -5
  4. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/Feature_Screen.py +67 -8
  5. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +3 -3
  6. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/Weighted_Screen.py +314 -18
  7. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/feature_validation.py +17 -3
  8. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/screening_artifact.py +4 -0
  9. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/__init__.py +1 -1
  10. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/PKG-INFO +1 -1
  11. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/SuperModelingFactory.egg-info/PKG-INFO +1 -1
  12. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/pyproject.toml +1 -1
  13. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/setup.py +1 -1
  14. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/ExcelMaster/ExcelFormatTool.py +0 -0
  15. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/ExcelMaster/ExcelMaster.py +0 -0
  16. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/ExcelMaster/Template.py +0 -0
  17. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/ExcelMaster/Utility.py +0 -0
  18. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/ExcelMaster/__init__.py +0 -0
  19. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/LICENSE +0 -0
  20. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/MANIFEST.in +0 -0
  21. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  22. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  23. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  24. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  25. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  26. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  27. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  28. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  29. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  30. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  31. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  32. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  33. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  34. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/__init__.py +0 -0
  35. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/kDataFrame.py +0 -0
  36. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  37. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  38. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Core/utils.py +0 -0
  39. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  40. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  41. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  42. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  43. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/__init__.py +0 -0
  44. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  45. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  46. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  47. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  48. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  49. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Explainability/__init__.py +0 -0
  50. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  51. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  52. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  53. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Feature/__init__.py +0 -0
  54. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  55. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  56. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  57. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  58. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  59. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  60. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  61. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Model/__init__.py +0 -0
  62. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/__init__.py +0 -0
  63. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/_common.py +0 -0
  64. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/credit_model.py +0 -0
  65. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  66. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  67. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  68. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  69. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  70. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  71. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  72. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  73. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  74. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  75. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  76. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  77. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/Sample/__init__.py +0 -0
  78. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  79. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/UAT/__init__.py +0 -0
  80. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  81. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  82. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  83. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  84. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  85. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  86. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  87. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  88. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  89. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  90. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  91. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  92. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/__init__.py +0 -0
  93. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  94. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  95. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  96. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  97. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/ref_font/__init__.py +0 -0
  98. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  99. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/README.md +0 -0
  100. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Report/Report_Tool.py +0 -0
  101. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/Report/__init__.py +0 -0
  102. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  103. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  104. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  105. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/SuperModelingFactory.egg-info/requires.txt +0 -0
  106. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  107. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/requirements.txt +0 -0
  108. {supermodelingfactory-0.3.13 → supermodelingfactory-0.3.15}/setup.cfg +0 -0
@@ -186,6 +186,40 @@ def proc_means_by_grp(data, varlist, groupby=None, spec_missing_value=None, q=No
186
186
  return means_rpt
187
187
 
188
188
 
189
+ _SCREENING_MEANS_COLS = ["attribute", "N_ALL", "N", "MISSING_RATE", "MIN", "MEAN", "MAX"]
190
+
191
+
192
+ def proc_means_for_screening(data, varlist, spec_missing_value=None, q=None):
193
+ """Build a uniform means table for IV screening reports.
194
+
195
+ ``proc_means_by_grp`` returns object stats (unique/top/freq) when numeric and
196
+ categorical columns are melted together. This helper splits by dtype so MIN,
197
+ MEAN, and MAX are always present (NaN for non-numeric features).
198
+ """
199
+ if not varlist:
200
+ return pd.DataFrame(columns=_SCREENING_MEANS_COLS)
201
+
202
+ numeric = [v for v in varlist if v in data.columns and pd.api.types.is_numeric_dtype(data[v])]
203
+ other = [v for v in varlist if v not in numeric and v in data.columns]
204
+ frames = []
205
+
206
+ if numeric:
207
+ frames.append(proc_means_by_grp(data, numeric, spec_missing_value=spec_missing_value, q=q))
208
+
209
+ for var in other:
210
+ part = proc_means_by_grp(data, [var], spec_missing_value=spec_missing_value, q=q)
211
+ for col in ("MIN", "MEAN", "MAX"):
212
+ if col not in part.columns:
213
+ part[col] = np.nan
214
+ frames.append(part)
215
+
216
+ if not frames:
217
+ return pd.DataFrame(columns=_SCREENING_MEANS_COLS)
218
+
219
+ out = pd.concat(frames, ignore_index=True)
220
+ return out[_SCREENING_MEANS_COLS]
221
+
222
+
189
223
  class DistributionShiftAnalyzer:
190
224
  """分布偏移分析器。
191
225
 
@@ -25,6 +25,7 @@ class proc_means:
25
25
  def group_sum(self): ...
26
26
  def __call__(self, q = None): ...
27
27
  def proc_means_by_grp(data, varlist, groupby = None, spec_missing_value = None, q = None): ...
28
+ def proc_means_for_screening(data, varlist, spec_missing_value = None, q = None): ...
28
29
 
29
30
  class DistributionShiftAnalyzer:
30
31
  def __init__(self, data, grp_name, benchmark_value): ...
@@ -6,7 +6,7 @@
6
6
  import pandas as pd
7
7
  from tqdm import tqdm
8
8
 
9
- from .Distribution_Tool import proc_means_by_grp
9
+ from .Distribution_Tool import proc_means_by_grp, proc_means_for_screening
10
10
  import logging
11
11
  logger = logging.getLogger(__name__)
12
12
 
@@ -208,14 +208,15 @@ class VarExtractionInsights:
208
208
  high_iv_summary = iv_info_res.query(f"IV >= {iv_cut}").round(4)
209
209
  high_iv_varlist = high_iv_summary['var'].tolist()
210
210
 
211
- means = proc_means_by_grp(data, high_iv_varlist, spec_missing_value=self.missing_rate_ref)
212
-
213
211
  if len(high_iv_varlist) == 0:
214
212
  logger.info(f"WARNING: No variable with IV >= {iv_cut}")
215
- means = means.rename(columns={"index": "attribute"})
213
+
214
+ means = proc_means_for_screening(
215
+ data, high_iv_varlist, spec_missing_value=self.missing_rate_ref,
216
+ )
216
217
 
217
218
  fnl_summary = high_iv_summary.merge(
218
- means[['attribute', 'N_ALL', 'N', 'MISSING_RATE', 'MIN', 'MEAN', 'MAX']],
219
+ means,
219
220
  left_on=['var'],
220
221
  right_on=['attribute'],
221
222
  how='left'
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
6
  from pathlib import Path
7
- from typing import Any, Mapping
7
+ from typing import Any, Literal, Mapping
8
8
 
9
9
  import numpy as np
10
10
  import pandas as pd
@@ -13,14 +13,16 @@ from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
13
13
 
14
14
  from .Weighted_Screen import (
15
15
  WeightedScreenResult,
16
+ _apply_missing_rate_stage,
17
+ _apply_stage_keep,
16
18
  _corr_dedup_weighted,
17
19
  _legacy_unweighted_screen,
18
20
  _psi_from_distributions,
19
21
  _resolve_splits,
20
22
  _summary_row,
21
23
  _weighted_bin_distribution,
24
+ _weighted_corr_for_screen,
22
25
  _weighted_iv_from_assigned_bins,
23
- _weighted_pearson_corr_matrix,
24
26
  _weighted_screen_impl,
25
27
  )
26
28
 
@@ -55,6 +57,10 @@ class FeatureScreenConfig:
55
57
  corr_threshold: float = 0.75
56
58
  corr_max_iterations: int = 10
57
59
  corr_use_woe_bins: bool = False
60
+ corr_nan_policy: Literal["pairwise", "median_fill", "raise"] = "pairwise"
61
+ on_empty_stage: Literal["keep_all_warn", "raise"] = "keep_all_warn"
62
+ missing_rate_threshold: float | None = None
63
+ missing_rate_ref: float | int = -999999
58
64
  woe_engine: str = "equal_freq"
59
65
  woe_fit_query: str | None = None
60
66
  woe_params: dict[str, Any] = field(
@@ -100,6 +106,10 @@ def screen_config_from_mapping(
100
106
  corr_threshold=float(cfg.get("corr_threshold", 0.75)),
101
107
  corr_max_iterations=int(cfg.get("corr_max_iterations", 10)),
102
108
  corr_use_woe_bins=bool(cfg.get("corr_use_woe_bins", False)),
109
+ corr_nan_policy=str(cfg.get("corr_nan_policy", "pairwise")), # type: ignore[arg-type]
110
+ on_empty_stage=str(cfg.get("on_empty_stage", "keep_all_warn")), # type: ignore[arg-type]
111
+ missing_rate_threshold=cfg.get("missing_rate_threshold"),
112
+ missing_rate_ref=cfg.get("missing_rate_ref", -999999),
103
113
  woe_engine=str(woe_engine or cfg.get("woe_engine", "equal_freq")),
104
114
  woe_fit_query=woe_fit_query if woe_fit_query is not None else cfg.get("woe_fit_query"),
105
115
  woe_params=dict(FeatureScreenConfig().woe_params | dict(woe_params or cfg.get("woe_params") or {})),
@@ -206,6 +216,14 @@ def _woe_bins_unweighted_screen(
206
216
  ins, oos, oot = splits["ins"], splits["oos"], splits["oot"]
207
217
  current = list(feature_cols)
208
218
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, None)]
219
+ current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
220
+ ins,
221
+ current,
222
+ summary_rows,
223
+ missing_rate_threshold=config.missing_rate_threshold,
224
+ missing_rate_ref=config.missing_rate_ref,
225
+ on_empty_stage=config.on_empty_stage,
226
+ )
209
227
  binner = _resolve_screening_binner(splits, feature_cols, target_col, config, prefit_woe_engine)
210
228
 
211
229
  psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
@@ -236,7 +254,10 @@ def _woe_bins_unweighted_screen(
236
254
  psi_table["psi_max"] = psi_table[compare_cols].max(axis=1, skipna=True)
237
255
  keep = psi_table.loc[psi_table["psi_max"] < config.psi_threshold, "var"].tolist()
238
256
  n_before = len(current)
239
- current = keep or current
257
+ current = _apply_stage_keep(
258
+ current, keep, "psi", summary_rows,
259
+ on_empty_stage=config.on_empty_stage, threshold=config.psi_threshold,
260
+ )
240
261
  summary_rows.append(_summary_row("psi", n_before, len(current), config.psi_threshold, None))
241
262
 
242
263
  iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
@@ -268,7 +289,10 @@ def _woe_bins_unweighted_screen(
268
289
  iv_table = iv.rename(columns={"iv": "iv_weighted"})[["var", "iv_weighted", "n_bins", "missing_rate"]]
269
290
  keep = iv.loc[iv["iv"] >= config.iv_threshold, "var"].tolist()
270
291
  n_before = len(current)
271
- current = keep or current
292
+ current = _apply_stage_keep(
293
+ current, keep, "iv", summary_rows,
294
+ on_empty_stage=config.on_empty_stage, threshold=config.iv_threshold,
295
+ )
272
296
  summary_rows.append(_summary_row("iv", n_before, len(current), config.iv_threshold, None))
273
297
 
274
298
  corr_dropped = pd.DataFrame(columns=["var_a", "var_b", "corr", "iv_a", "iv_b", "kept", "dropped"])
@@ -292,6 +316,8 @@ def _woe_bins_unweighted_screen(
292
316
  psi_table=psi_table,
293
317
  corr_dropped=corr_dropped,
294
318
  summary=pd.DataFrame(summary_rows),
319
+ missing_rate_table=missing_rate_table,
320
+ missing_rate_dropped=missing_rate_dropped,
295
321
  )
296
322
 
297
323
 
@@ -317,6 +343,15 @@ def _weighted_woe_bins_screen(
317
343
  y_ins = ins[target_col].astype(float).to_numpy()
318
344
  current = list(feature_cols)
319
345
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, weight_col)]
346
+ current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
347
+ ins,
348
+ current,
349
+ summary_rows,
350
+ missing_rate_threshold=config.missing_rate_threshold,
351
+ missing_rate_ref=config.missing_rate_ref,
352
+ weight_col=weight_col,
353
+ on_empty_stage=config.on_empty_stage,
354
+ )
320
355
 
321
356
  binner = prefit_woe_engine
322
357
  if binner is None:
@@ -360,7 +395,11 @@ def _weighted_woe_bins_screen(
360
395
  psi_table["psi_max"] = psi_table[compare_cols].max(axis=1, skipna=True)
361
396
  keep = psi_table.loc[psi_table["psi_max"] < config.psi_threshold, "var"].tolist()
362
397
  n_before = len(current)
363
- current = [v for v in current if v in keep] or current
398
+ current = _apply_stage_keep(
399
+ current, keep, "psi", summary_rows,
400
+ on_empty_stage=config.on_empty_stage, weight_col=weight_col,
401
+ threshold=config.psi_threshold, intersect=True,
402
+ )
364
403
  summary_rows.append(_summary_row("psi", n_before, len(current), config.psi_threshold, weight_col))
365
404
  else:
366
405
  psi_table["psi_max"] = pd.Series(dtype=float)
@@ -387,15 +426,24 @@ def _weighted_woe_bins_screen(
387
426
  if not iv_table.empty:
388
427
  keep = iv_table.loc[iv_table["iv_weighted"] >= config.iv_threshold, "var"].tolist()
389
428
  n_before = len(current)
390
- current = [v for v in current if v in keep] or current
429
+ current = _apply_stage_keep(
430
+ current, keep, "iv", summary_rows,
431
+ on_empty_stage=config.on_empty_stage, weight_col=weight_col,
432
+ threshold=config.iv_threshold, intersect=True,
433
+ )
391
434
  summary_rows.append(_summary_row("iv", n_before, len(current), config.iv_threshold, weight_col))
392
435
  else:
393
436
  iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
394
437
 
395
438
  corr_dropped = pd.DataFrame(columns=["var_a", "var_b", "corr", "iv_a", "iv_b", "kept", "dropped"])
396
439
  if config.corr_enabled and len(current) > 1:
397
- X = ins[current].astype(float).to_numpy()
398
- corr = _weighted_pearson_corr_matrix(X, w_ins)
440
+ corr = _weighted_corr_for_screen(
441
+ ins, current, w_ins,
442
+ corr_use_woe_bins=config.corr_use_woe_bins,
443
+ corr_nan_policy=config.corr_nan_policy,
444
+ adapter=adapter,
445
+ binner=binner,
446
+ )
399
447
  iv_map = dict(zip(iv_table["var"], iv_table["iv_weighted"])) if not iv_table.empty else {}
400
448
  n_before = len(current)
401
449
  current, corr_dropped = _corr_dedup_weighted(
@@ -414,6 +462,8 @@ def _weighted_woe_bins_screen(
414
462
  psi_table=psi_table,
415
463
  corr_dropped=corr_dropped,
416
464
  summary=pd.DataFrame(summary_rows),
465
+ missing_rate_table=missing_rate_table,
466
+ missing_rate_dropped=missing_rate_dropped,
417
467
  )
418
468
 
419
469
 
@@ -457,6 +507,12 @@ def feature_screen(
457
507
  corr_max_iterations=cfg.corr_max_iterations,
458
508
  content=cfg.content,
459
509
  precision=cfg.precision,
510
+ corr_use_woe_bins=cfg.corr_use_woe_bins,
511
+ corr_nan_policy=cfg.corr_nan_policy,
512
+ on_empty_stage=cfg.on_empty_stage,
513
+ prefit_woe_engine=prefit_woe_engine,
514
+ missing_rate_threshold=cfg.missing_rate_threshold,
515
+ missing_rate_ref=cfg.missing_rate_ref,
460
516
  )
461
517
 
462
518
  if use_woe_bins or prefit_woe_engine is not None:
@@ -486,6 +542,9 @@ def feature_screen(
486
542
  plot_path=cfg.plot_path,
487
543
  plot_outputs=cfg.plot_outputs,
488
544
  iv_equal_freq=cfg.iv_equal_freq,
545
+ on_empty_stage=cfg.on_empty_stage,
546
+ missing_rate_threshold=cfg.missing_rate_threshold,
547
+ missing_rate_ref=cfg.missing_rate_ref,
489
548
  )
490
549
 
491
550
 
@@ -19,7 +19,7 @@ from .Feature_Insights import (
19
19
  VarExtractionInsights as _BaseVarExtractionInsights,
20
20
  var_corr_filter,
21
21
  )
22
- from .Distribution_Tool import proc_means_by_grp
22
+ from .Distribution_Tool import proc_means_for_screening
23
23
 
24
24
 
25
25
  def _psi_from_bins(expected_bins: pd.Series, current_bins: pd.Series, content: float) -> float:
@@ -110,9 +110,9 @@ def _screening_summary_from_engine(
110
110
  high_iv_summary = pd.DataFrame(rows).query(f"iv >= {iv_cut}").round(4)
111
111
  high_iv_varlist = high_iv_summary["var"].tolist()
112
112
  if high_iv_varlist:
113
- means = proc_means_by_grp(data, high_iv_varlist, spec_missing_value=missing_rate_ref)
113
+ means = proc_means_for_screening(data, high_iv_varlist, spec_missing_value=missing_rate_ref)
114
114
  summary = high_iv_summary.merge(
115
- means[["attribute", "N_ALL", "N", "MISSING_RATE", "MIN", "MEAN", "MAX"]],
115
+ means,
116
116
  left_on="var",
117
117
  right_on="attribute",
118
118
  how="left",
@@ -2,9 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import dataclass
5
+ import warnings
6
+ from dataclasses import dataclass, field
6
7
  from pathlib import Path
7
- from typing import Any
8
+ from typing import Any, Literal
8
9
 
9
10
  import numpy as np
10
11
  import pandas as pd
@@ -15,6 +16,9 @@ from Modeling_Tool.Core.utils import calc_iv
15
16
  _MISSING_BIN = "__MISSING__"
16
17
 
17
18
 
19
+ _MISSING_RATE_COLS = ["var", "missing_rate"]
20
+
21
+
18
22
  @dataclass
19
23
  class WeightedScreenResult:
20
24
  selected_features: list[str]
@@ -22,6 +26,12 @@ class WeightedScreenResult:
22
26
  psi_table: pd.DataFrame
23
27
  corr_dropped: pd.DataFrame
24
28
  summary: pd.DataFrame
29
+ missing_rate_table: pd.DataFrame = field(
30
+ default_factory=lambda: pd.DataFrame(columns=_MISSING_RATE_COLS),
31
+ )
32
+ missing_rate_dropped: pd.DataFrame = field(
33
+ default_factory=lambda: pd.DataFrame(columns=_MISSING_RATE_COLS),
34
+ )
25
35
 
26
36
 
27
37
  def _summary_row(stage: str, n_in: int, n_out: int, threshold: Any, weight_col: str | None) -> dict:
@@ -185,21 +195,255 @@ def _weighted_iv_for_var(
185
195
  return iv, len(rows), missing_rate
186
196
 
187
197
 
188
- def _weighted_pearson_corr_matrix(X: np.ndarray, w: np.ndarray) -> np.ndarray:
198
+ def _weighted_median(x: np.ndarray, w: np.ndarray) -> float:
199
+ mask = np.isfinite(x)
200
+ if not mask.any():
201
+ return np.nan
202
+ xv = x[mask].astype(float)
203
+ wv = w[mask].astype(float)
204
+ order = np.argsort(xv)
205
+ xv = xv[order]
206
+ wv = wv[order]
207
+ cumw = np.cumsum(wv)
208
+ total = float(cumw[-1])
209
+ if total <= 0:
210
+ return float(np.median(xv))
211
+ idx = min(int(np.searchsorted(cumw, total / 2.0, side="right")), len(xv) - 1)
212
+ return float(xv[idx])
213
+
214
+
215
+ def _weighted_corr_pair(xi: np.ndarray, xj: np.ndarray, w: np.ndarray) -> float:
216
+ mask = np.isfinite(xi) & np.isfinite(xj)
217
+ if mask.sum() < 2:
218
+ return 0.0
219
+ wi = w[mask].astype(float)
220
+ xi = xi[mask].astype(float)
221
+ xj = xj[mask].astype(float)
222
+ w_sum = wi.sum()
223
+ if w_sum <= 0:
224
+ c = np.corrcoef(xi, xj)
225
+ return float(c[0, 1]) if np.isfinite(c[0, 1]) else 0.0
226
+ w_norm = wi / w_sum
227
+ mi = np.average(xi, weights=w_norm)
228
+ mj = np.average(xj, weights=w_norm)
229
+ di = xi - mi
230
+ dj = xj - mj
231
+ cov = np.average(di * dj, weights=w_norm)
232
+ si = np.sqrt(np.average(di * di, weights=w_norm))
233
+ sj = np.sqrt(np.average(dj * dj, weights=w_norm))
234
+ if si == 0 or sj == 0:
235
+ return 0.0
236
+ return float(cov / (si * sj))
237
+
238
+
239
+ def _weighted_pearson_corr_matrix(
240
+ X: np.ndarray,
241
+ w: np.ndarray,
242
+ *,
243
+ nan_policy: Literal["pairwise", "median_fill", "raise"] = "pairwise",
244
+ ) -> np.ndarray:
189
245
  if X.shape[1] == 0:
190
246
  return np.empty((0, 0))
191
247
  w = np.asarray(w, dtype=float)
248
+ k = X.shape[1]
249
+
250
+ if nan_policy == "raise" and np.any(~np.isfinite(X)):
251
+ raise ValueError(
252
+ "weighted correlation input contains NaN; set corr_nan_policy to "
253
+ "'pairwise' or 'median_fill' to proceed"
254
+ )
255
+
256
+ nan_mask = ~np.isfinite(X)
257
+ nan_fraction = float(nan_mask.sum()) / float(X.size) if X.size else 0.0
258
+
259
+ if nan_policy == "pairwise":
260
+ corr = np.eye(k, dtype=float)
261
+ insufficient_pairs = 0
262
+ total_pairs = k * (k - 1) // 2
263
+ for i in range(k):
264
+ for j in range(i + 1, k):
265
+ c = _weighted_corr_pair(X[:, i], X[:, j], w)
266
+ corr[i, j] = c
267
+ corr[j, i] = c
268
+ overlap = int((np.isfinite(X[:, i]) & np.isfinite(X[:, j])).sum())
269
+ if overlap < 2:
270
+ insufficient_pairs += 1
271
+ if total_pairs > 0 and insufficient_pairs / total_pairs > 0.01:
272
+ warnings.warn(
273
+ f"[feature_screen] weighted corr: >1% of feature pairs have insufficient "
274
+ f"overlapping observations ({insufficient_pairs}/{total_pairs})",
275
+ stacklevel=3,
276
+ )
277
+ return corr
278
+
279
+ X_use = X.astype(float).copy()
280
+ if nan_policy == "median_fill":
281
+ for j in range(k):
282
+ col = X_use[:, j]
283
+ bad = ~np.isfinite(col)
284
+ if bad.any():
285
+ fill = _weighted_median(col, w)
286
+ if np.isfinite(fill):
287
+ col[bad] = fill
288
+ X_use[:, j] = col
289
+
192
290
  w_sum = w.sum()
193
291
  if w_sum <= 0:
194
- return np.corrcoef(X, rowvar=False)
195
- w_norm = w / w_sum
196
- mean = np.average(X, axis=0, weights=w_norm)
197
- Xc = X - mean
198
- cov = np.cov(Xc, rowvar=False, aweights=w, ddof=0)
199
- std = np.sqrt(np.diag(cov))
200
- std[std == 0] = np.nan
201
- corr = cov / np.outer(std, std)
202
- return np.nan_to_num(corr, nan=0.0)
292
+ out = np.corrcoef(X_use, rowvar=False)
293
+ out = np.nan_to_num(out, nan=0.0)
294
+ else:
295
+ w_norm = w / w_sum
296
+ mean = np.average(X_use, axis=0, weights=w_norm)
297
+ Xc = X_use - mean
298
+ cov = np.cov(Xc, rowvar=False, aweights=w, ddof=0)
299
+ std = np.sqrt(np.diag(cov))
300
+ std[std == 0] = np.nan
301
+ out = cov / np.outer(std, std)
302
+ out = np.nan_to_num(out, nan=0.0)
303
+
304
+ if nan_fraction > 0.01:
305
+ warnings.warn(
306
+ f"[feature_screen] weighted corr: {nan_fraction:.1%} of matrix entries were NaN "
307
+ f"before computation (policy={nan_policy!r})",
308
+ stacklevel=3,
309
+ )
310
+ return out
311
+
312
+
313
+ def _weighted_corr_for_screen(
314
+ ins: pd.DataFrame,
315
+ current: list[str],
316
+ w_ins: np.ndarray,
317
+ *,
318
+ corr_use_woe_bins: bool,
319
+ corr_nan_policy: Literal["pairwise", "median_fill", "raise"],
320
+ adapter: Any | None = None,
321
+ binner: Any | None = None,
322
+ ) -> np.ndarray:
323
+ """Build weighted correlation matrix for screening (WOE or raw-value path)."""
324
+ from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
325
+
326
+ if corr_use_woe_bins:
327
+ eng = adapter if adapter is not None else (as_woe_engine(binner) if binner is not None else None)
328
+ if eng is not None:
329
+ suffix = getattr(eng, "woe_suffix", "_woe")
330
+ woe_ins = eng.transform(ins, varlist=current)
331
+ cols = [f"{v}{suffix}" for v in current if f"{v}{suffix}" in woe_ins.columns]
332
+ if cols:
333
+ X = woe_ins[cols].to_numpy(dtype=float)
334
+ return _weighted_pearson_corr_matrix(X, w_ins, nan_policy="pairwise")
335
+ X = ins[current].astype(float).to_numpy()
336
+ return _weighted_pearson_corr_matrix(X, w_ins, nan_policy=corr_nan_policy)
337
+
338
+
339
+ def _missing_rate_for_series(
340
+ series: pd.Series,
341
+ *,
342
+ w: np.ndarray | None = None,
343
+ missing_rate_ref: Any = None,
344
+ ) -> float:
345
+ values = series.copy()
346
+ if missing_rate_ref is not None:
347
+ values = values.replace(missing_rate_ref, np.nan)
348
+ if w is None:
349
+ total = len(values)
350
+ if total == 0:
351
+ return 1.0
352
+ valid = int(values.notna().sum())
353
+ return 1.0 - float(valid) / float(total)
354
+ arr = values.to_numpy(dtype=float, copy=False)
355
+ finite = np.isfinite(arr) & values.notna().to_numpy()
356
+ total_w = float(np.sum(w))
357
+ if total_w <= 0:
358
+ return 1.0
359
+ return 1.0 - float(np.sum(w[finite])) / total_w
360
+
361
+
362
+ def _apply_missing_rate_stage(
363
+ ins: pd.DataFrame,
364
+ current: list[str],
365
+ summary_rows: list[dict],
366
+ *,
367
+ missing_rate_threshold: float | None,
368
+ missing_rate_ref: Any = None,
369
+ weight_col: str | None = None,
370
+ on_empty_stage: Literal["keep_all_warn", "raise"] = "keep_all_warn",
371
+ ) -> tuple[list[str], pd.DataFrame, pd.DataFrame]:
372
+ empty = pd.DataFrame(columns=_MISSING_RATE_COLS)
373
+ if missing_rate_threshold is None:
374
+ return current, empty.copy(), empty.copy()
375
+
376
+ w = (
377
+ resolve_sample_weight(data=ins, weight_col=weight_col, expected_len=len(ins))
378
+ if weight_col is not None
379
+ else None
380
+ )
381
+ records: list[dict] = []
382
+ for var in current:
383
+ if var not in ins.columns:
384
+ continue
385
+ records.append({
386
+ "var": var,
387
+ "missing_rate": _missing_rate_for_series(
388
+ ins[var], w=w, missing_rate_ref=missing_rate_ref,
389
+ ),
390
+ })
391
+ missing_rate_table = pd.DataFrame(records) if records else empty.copy()
392
+ keep = missing_rate_table.loc[
393
+ missing_rate_table["missing_rate"] <= missing_rate_threshold, "var"
394
+ ].tolist()
395
+ missing_rate_dropped = missing_rate_table.loc[
396
+ missing_rate_table["missing_rate"] > missing_rate_threshold, _MISSING_RATE_COLS
397
+ ].copy()
398
+
399
+ n_before = len(current)
400
+ current = _apply_stage_keep(
401
+ current,
402
+ keep,
403
+ "missing_rate",
404
+ summary_rows,
405
+ on_empty_stage=on_empty_stage,
406
+ weight_col=weight_col,
407
+ threshold=missing_rate_threshold,
408
+ intersect=True,
409
+ )
410
+ summary_rows.append(
411
+ _summary_row("missing_rate", n_before, len(current), missing_rate_threshold, weight_col),
412
+ )
413
+ return current, missing_rate_table, missing_rate_dropped
414
+
415
+
416
+ def _apply_stage_keep(
417
+ current: list[str],
418
+ keep: list[str],
419
+ stage: str,
420
+ summary_rows: list[dict],
421
+ *,
422
+ on_empty_stage: Literal["keep_all_warn", "raise"] = "keep_all_warn",
423
+ weight_col: str | None = None,
424
+ threshold: Any = None,
425
+ intersect: bool = False,
426
+ ) -> list[str]:
427
+ if intersect:
428
+ keep_set = set(keep)
429
+ new_current = [v for v in current if v in keep_set]
430
+ else:
431
+ new_current = list(keep) if keep else []
432
+ if new_current:
433
+ return new_current
434
+ if on_empty_stage == "raise":
435
+ raise ValueError(
436
+ f"feature screen stage {stage!r} eliminated all {len(current)} variables; "
437
+ f"on_empty_stage='raise'"
438
+ )
439
+ warnings.warn(
440
+ f"[feature_screen] stage {stage!r} 全军覆没, 按 keep_all_warn 保留全部 {len(current)} 个",
441
+ stacklevel=3,
442
+ )
443
+ summary_rows.append(
444
+ _summary_row(f"{stage}_fallback", len(current), len(current), threshold, weight_col)
445
+ )
446
+ return current
203
447
 
204
448
 
205
449
  def _high_corr_pairs(varlist: list[str], corr: np.ndarray, threshold: float) -> pd.DataFrame:
@@ -280,12 +524,23 @@ def _legacy_unweighted_screen(
280
524
  plot_path: str | None,
281
525
  plot_outputs: bool,
282
526
  iv_equal_freq: bool = True,
527
+ on_empty_stage: Literal["keep_all_warn", "raise"] = "keep_all_warn",
528
+ missing_rate_threshold: float | None = None,
529
+ missing_rate_ref: Any = None,
283
530
  ) -> WeightedScreenResult:
284
531
  from Modeling_Tool import CorrelationFilter, PSICalculator, VarExtractionInsights
285
532
 
286
533
  ins, oos, oot = splits["ins"], splits["oos"], splits["oot"]
287
534
  current = list(feature_cols)
288
535
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, None)]
536
+ current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
537
+ ins,
538
+ current,
539
+ summary_rows,
540
+ missing_rate_threshold=missing_rate_threshold,
541
+ missing_rate_ref=missing_rate_ref,
542
+ on_empty_stage=on_empty_stage,
543
+ )
289
544
 
290
545
  psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
291
546
  if psi_enabled:
@@ -314,7 +569,10 @@ def _legacy_unweighted_screen(
314
569
  psi_table["psi_max"] = psi_table[compare_cols].max(axis=1, skipna=True)
315
570
  keep = psi_table.loc[psi_table["psi_max"] < psi_threshold, "var"].tolist()
316
571
  n_before = len(current)
317
- current = keep or current
572
+ current = _apply_stage_keep(
573
+ current, keep, "psi", summary_rows,
574
+ on_empty_stage=on_empty_stage, threshold=psi_threshold,
575
+ )
318
576
  summary_rows.append(_summary_row("psi", n_before, len(current), psi_threshold, None))
319
577
 
320
578
  iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
@@ -342,7 +600,10 @@ def _legacy_unweighted_screen(
342
600
  iv_table = iv.rename(columns={"iv": "iv_weighted"})[["var", "iv_weighted", "n_bins", "missing_rate"]]
343
601
  keep = iv.loc[iv["iv"] >= iv_threshold, "var"].tolist()
344
602
  n_before = len(current)
345
- current = keep or current
603
+ current = _apply_stage_keep(
604
+ current, keep, "iv", summary_rows,
605
+ on_empty_stage=on_empty_stage, threshold=iv_threshold,
606
+ )
346
607
  summary_rows.append(_summary_row("iv", n_before, len(current), iv_threshold, None))
347
608
 
348
609
  corr_dropped = pd.DataFrame(columns=["var_a", "var_b", "corr", "iv_a", "iv_b", "kept", "dropped"])
@@ -363,6 +624,8 @@ def _legacy_unweighted_screen(
363
624
  psi_table=psi_table,
364
625
  corr_dropped=corr_dropped,
365
626
  summary=pd.DataFrame(summary_rows),
627
+ missing_rate_table=missing_rate_table,
628
+ missing_rate_dropped=missing_rate_dropped,
366
629
  )
367
630
 
368
631
 
@@ -384,6 +647,12 @@ def _weighted_screen_impl(
384
647
  corr_max_iterations: int,
385
648
  content: float,
386
649
  precision: int,
650
+ corr_use_woe_bins: bool = False,
651
+ corr_nan_policy: Literal["pairwise", "median_fill", "raise"] = "pairwise",
652
+ on_empty_stage: Literal["keep_all_warn", "raise"] = "keep_all_warn",
653
+ prefit_woe_engine: Any | None = None,
654
+ missing_rate_threshold: float | None = None,
655
+ missing_rate_ref: Any = None,
387
656
  ) -> WeightedScreenResult:
388
657
  ins = splits["ins"]
389
658
  oos = splits["oos"]
@@ -392,6 +661,15 @@ def _weighted_screen_impl(
392
661
 
393
662
  current = list(feature_cols)
394
663
  summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, weight_col)]
664
+ current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
665
+ ins,
666
+ current,
667
+ summary_rows,
668
+ missing_rate_threshold=missing_rate_threshold,
669
+ missing_rate_ref=missing_rate_ref,
670
+ weight_col=weight_col,
671
+ on_empty_stage=on_empty_stage,
672
+ )
395
673
 
396
674
  # Precompute bin edges on ins for PSI / IV
397
675
  edges_cache: dict[str, list[float] | None] = {}
@@ -432,7 +710,11 @@ def _weighted_screen_impl(
432
710
  psi_table["psi_max"] = psi_table[compare_cols].max(axis=1, skipna=True)
433
711
  keep = psi_table.loc[psi_table["psi_max"] < psi_threshold, "var"].tolist()
434
712
  n_before = len(current)
435
- current = [v for v in current if v in keep] or current
713
+ current = _apply_stage_keep(
714
+ current, keep, "psi", summary_rows,
715
+ on_empty_stage=on_empty_stage, weight_col=weight_col,
716
+ threshold=psi_threshold, intersect=True,
717
+ )
436
718
  summary_rows.append(_summary_row("psi", n_before, len(current), psi_threshold, weight_col))
437
719
  else:
438
720
  psi_table["psi_max"] = pd.Series(dtype=float)
@@ -464,15 +746,23 @@ def _weighted_screen_impl(
464
746
  if not iv_table.empty:
465
747
  keep = iv_table.loc[iv_table["iv_weighted"] >= iv_threshold, "var"].tolist()
466
748
  n_before = len(current)
467
- current = [v for v in current if v in keep] or current
749
+ current = _apply_stage_keep(
750
+ current, keep, "iv", summary_rows,
751
+ on_empty_stage=on_empty_stage, weight_col=weight_col,
752
+ threshold=iv_threshold, intersect=True,
753
+ )
468
754
  summary_rows.append(_summary_row("iv", n_before, len(current), iv_threshold, weight_col))
469
755
  else:
470
756
  iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
471
757
 
472
758
  corr_dropped = pd.DataFrame(columns=["var_a", "var_b", "corr", "iv_a", "iv_b", "kept", "dropped"])
473
759
  if corr_enabled and len(current) > 1:
474
- X = ins[current].astype(float).to_numpy()
475
- corr = _weighted_pearson_corr_matrix(X, w_ins)
760
+ corr = _weighted_corr_for_screen(
761
+ ins, current, w_ins,
762
+ corr_use_woe_bins=corr_use_woe_bins,
763
+ corr_nan_policy=corr_nan_policy,
764
+ binner=prefit_woe_engine,
765
+ )
476
766
  iv_map = dict(zip(iv_table["var"], iv_table["iv_weighted"])) if not iv_table.empty else {}
477
767
  n_before = len(current)
478
768
  current, corr_dropped = _corr_dedup_weighted(
@@ -487,6 +777,8 @@ def _weighted_screen_impl(
487
777
  psi_table=psi_table,
488
778
  corr_dropped=corr_dropped,
489
779
  summary=pd.DataFrame(summary_rows),
780
+ missing_rate_table=missing_rate_table,
781
+ missing_rate_dropped=missing_rate_dropped,
490
782
  )
491
783
 
492
784
 
@@ -521,6 +813,8 @@ def weighted_feature_screen(
521
813
  woe_params: dict[str, Any] | None = None,
522
814
  monotone_woe_params: dict[str, Any] | None = None,
523
815
  prefit_woe_engine: Any | None = None,
816
+ corr_nan_policy: Literal["pairwise", "median_fill", "raise"] = "pairwise",
817
+ on_empty_stage: Literal["keep_all_warn", "raise"] = "keep_all_warn",
524
818
  ) -> WeightedScreenResult:
525
819
  """Run PSI -> IV -> correlation screening with optional sample weights.
526
820
 
@@ -563,6 +857,8 @@ def weighted_feature_screen(
563
857
  content=content,
564
858
  precision=precision,
565
859
  min_bin_prop=min_bin_prop,
860
+ corr_nan_policy=corr_nan_policy,
861
+ on_empty_stage=on_empty_stage,
566
862
  )
567
863
  return feature_screen_from_dataframe(
568
864
  data,
@@ -97,6 +97,7 @@ class FeatureValidationPipelineConfig:
97
97
  }
98
98
  )
99
99
 
100
+ missing_rate_threshold: float | None = None
100
101
  selection_enabled: bool = False
101
102
  selection_params: dict[str, Any] = field(default_factory=dict)
102
103
  weight_col: str | None = None
@@ -1174,6 +1175,7 @@ class FeatureValidationPipeline:
1174
1175
  params = dict(cfg.selection_params or {})
1175
1176
  corr_params = dict(cfg.corr_params or {})
1176
1177
  psi_params = dict(cfg.psi_params or {})
1178
+ missing_rate_threshold = params.get("missing_rate_threshold", cfg.missing_rate_threshold)
1177
1179
  mapping = {
1178
1180
  "psi_enabled": params.get("psi_enabled", cfg.psi_enabled),
1179
1181
  "psi_threshold": params.get("psi_threshold", 0.2),
@@ -1196,6 +1198,11 @@ class FeatureValidationPipeline:
1196
1198
  corr_params.get("max_iterations", 10),
1197
1199
  ),
1198
1200
  "corr_use_woe_bins": params.get("corr_use_woe_bins", cfg.corr_use_woe_bins),
1201
+ "missing_rate_threshold": missing_rate_threshold,
1202
+ "missing_rate_ref": params.get(
1203
+ "missing_rate_ref",
1204
+ cfg.woe_params.get("missing_ref_value", -999999),
1205
+ ),
1199
1206
  }
1200
1207
  return screen_config_from_mapping(
1201
1208
  mapping,
@@ -1238,6 +1245,7 @@ class FeatureValidationPipeline:
1238
1245
  config_snapshot = {
1239
1246
  "selection_enabled": True,
1240
1247
  "selection_params": dict(cfg.selection_params or {}),
1248
+ "missing_rate_threshold": screen_cfg.missing_rate_threshold,
1241
1249
  "weight_col": cfg.weight_col,
1242
1250
  "psi_use_woe_bins": screen_cfg.psi_use_woe_bins,
1243
1251
  "iv_use_woe_bins": screen_cfg.iv_use_woe_bins,
@@ -1673,9 +1681,15 @@ class FeatureValidationPipeline:
1673
1681
  }
1674
1682
  if selected_features:
1675
1683
  tables["selected_features"] = pd.DataFrame({"feature": selected_features})
1676
- screen_summary = (selection_summary or {}).get("screen_summary")
1677
- if isinstance(screen_summary, pd.DataFrame) and not screen_summary.empty:
1678
- tables["selection_screen_summary"] = screen_summary
1684
+ if selection_summary:
1685
+ for key in ("missing_rate", "missing_rate_dropped", "screen_summary"):
1686
+ value = selection_summary.get(key)
1687
+ if isinstance(value, pd.DataFrame) and not value.empty:
1688
+ tables[f"selection_{key}"] = value
1689
+ if "final_features" in selection_summary:
1690
+ tables["selection_final_features"] = pd.DataFrame(
1691
+ {"feature": list(selection_summary["final_features"])},
1692
+ )
1679
1693
  for name, df in distribution_summary.items():
1680
1694
  tables[f"distribution_{name}"] = df
1681
1695
  return tables
@@ -31,6 +31,10 @@ def screen_result_to_summary(
31
31
  summary["iv"] = iv
32
32
  if not result.corr_dropped.empty:
33
33
  summary["corr_dropped"] = result.corr_dropped
34
+ if not result.missing_rate_table.empty:
35
+ summary["missing_rate"] = result.missing_rate_table
36
+ if not result.missing_rate_dropped.empty:
37
+ summary["missing_rate_dropped"] = result.missing_rate_dropped
34
38
  summary["corr_features"] = list(result.selected_features)
35
39
  summary["screen_summary"] = result.summary
36
40
  summary["final_features"] = list(result.selected_features)
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  __author__ = "Jingkai Sun"
4
- __version__ = "0.3.13"
4
+ __version__ = "0.3.15"
5
5
 
6
6
  # Core module - base data structures
7
7
  from .Core import (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.3.13
3
+ Version: 0.3.15
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.3.13
3
+ Version: 0.3.15
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "SuperModelingFactory"
15
- version = "0.3.13"
15
+ version = "0.3.15"
16
16
  description = "Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting."
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.10"
@@ -15,7 +15,7 @@ def _read(path: str) -> str:
15
15
 
16
16
  setup(
17
17
  name="SuperModelingFactory",
18
- version=os.environ.get("SMF_VERSION", "0.3.13"),
18
+ version=os.environ.get("SMF_VERSION", "0.3.15"),
19
19
  description="Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.",
20
20
  long_description=_read("README.md"),
21
21
  long_description_content_type="text/markdown",