SuperModelingFactory 0.3.7__tar.gz → 0.3.8__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.
- supermodelingfactory-0.3.8/Modeling_Tool/Feature/Weighted_Screen.py +548 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/__init__.py +8 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/credit_model.py +68 -67
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/__init__.py +5 -1
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/PKG-INFO +1 -1
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/PKG-INFO +1 -1
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/SOURCES.txt +1 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/pyproject.toml +1 -1
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/setup.py +1 -1
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/ExcelMaster/ExcelFormatTool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/ExcelMaster/ExcelMaster.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/ExcelMaster/Template.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/ExcelMaster/Utility.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/ExcelMaster/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/LICENSE +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/MANIFEST.in +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Binning_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Proc_Compare.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Slope_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/kDataFrame.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/utils.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/evaluate_model.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Explainability/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/Backward_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/GBM_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/LRM_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/_common.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/feature_validation.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Sample_Split.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/UAT/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Master.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/ref_font/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/ref_font/simsun.ttc +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/README.md +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Report/Report_Tool.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Report/__init__.py +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/requires.txt +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/top_level.txt +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/requirements.txt +0 -0
- {supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/setup.cfg +0 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""Weighted feature screening: PSI -> IV -> correlation dedup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight, weighted_rate
|
|
13
|
+
from Modeling_Tool.Core.utils import calc_iv
|
|
14
|
+
|
|
15
|
+
_MISSING_BIN = "__MISSING__"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class WeightedScreenResult:
|
|
20
|
+
selected_features: list[str]
|
|
21
|
+
iv_table: pd.DataFrame
|
|
22
|
+
psi_table: pd.DataFrame
|
|
23
|
+
corr_dropped: pd.DataFrame
|
|
24
|
+
summary: pd.DataFrame
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _summary_row(stage: str, n_in: int, n_out: int, threshold: Any, weight_col: str | None) -> dict:
|
|
28
|
+
return {
|
|
29
|
+
"stage": stage,
|
|
30
|
+
"n_in": n_in,
|
|
31
|
+
"n_out": n_out,
|
|
32
|
+
"threshold": threshold,
|
|
33
|
+
"weight_col": weight_col,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_splits(data: pd.DataFrame, split_col: str) -> dict[str, pd.DataFrame]:
|
|
38
|
+
if split_col not in data.columns:
|
|
39
|
+
raise KeyError(f"Missing split_col {split_col!r}")
|
|
40
|
+
ins = data[data[split_col] == "ins"].copy()
|
|
41
|
+
oos = data[data[split_col] == "oos"].copy()
|
|
42
|
+
oot = data[data[split_col] == "oot"].copy()
|
|
43
|
+
if len(ins) == 0:
|
|
44
|
+
raise ValueError("No rows with split_col == 'ins'.")
|
|
45
|
+
return {"ins": ins, "oos": oos, "oot": oot}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _max_nbins(n_obs: int, nbins: int, min_bin_prop: float) -> int:
|
|
49
|
+
if min_bin_prop <= 0 or n_obs <= 0:
|
|
50
|
+
return max(1, nbins)
|
|
51
|
+
return max(1, min(nbins, int(1 / min_bin_prop)))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _weighted_equal_freq_edges(
|
|
55
|
+
x: np.ndarray,
|
|
56
|
+
w: np.ndarray,
|
|
57
|
+
n_bins: int,
|
|
58
|
+
min_bin_prop: float,
|
|
59
|
+
precision: int = 5,
|
|
60
|
+
) -> list[float] | None:
|
|
61
|
+
mask = np.isfinite(x)
|
|
62
|
+
xv = x[mask].astype(float)
|
|
63
|
+
wv = w[mask].astype(float)
|
|
64
|
+
if xv.size == 0 or wv.sum() <= 0:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
n_bins = _max_nbins(xv.size, n_bins, min_bin_prop)
|
|
68
|
+
order = np.argsort(xv)
|
|
69
|
+
xv = xv[order]
|
|
70
|
+
wv = wv[order]
|
|
71
|
+
cumw = np.cumsum(wv)
|
|
72
|
+
total_w = float(cumw[-1])
|
|
73
|
+
targets = [total_w * i / n_bins for i in range(1, n_bins)]
|
|
74
|
+
|
|
75
|
+
edges: list[float] = []
|
|
76
|
+
for target in targets:
|
|
77
|
+
idx = int(np.searchsorted(cumw, target, side="right"))
|
|
78
|
+
idx = min(max(idx, 1), xv.size) - 1
|
|
79
|
+
edges.append(round(float(xv[idx]), precision))
|
|
80
|
+
|
|
81
|
+
uniq = sorted(set(edges))
|
|
82
|
+
if not uniq:
|
|
83
|
+
return [-np.inf, np.inf]
|
|
84
|
+
return [-np.inf, *uniq, np.inf]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _assign_bins(x: np.ndarray, edges: list[float]) -> np.ndarray:
|
|
88
|
+
out = np.empty(len(x), dtype=object)
|
|
89
|
+
missing = ~np.isfinite(x)
|
|
90
|
+
out[missing] = _MISSING_BIN
|
|
91
|
+
if edges is None:
|
|
92
|
+
out[~missing] = "all"
|
|
93
|
+
return out
|
|
94
|
+
valid = x[~missing].astype(float)
|
|
95
|
+
# pd.cut for consistent interval labels
|
|
96
|
+
cats = pd.cut(valid, bins=edges, include_lowest=True, duplicates="drop")
|
|
97
|
+
out[~missing] = np.asarray(cats.astype(str))
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _weighted_bin_distribution(
|
|
102
|
+
bins: np.ndarray,
|
|
103
|
+
w: np.ndarray,
|
|
104
|
+
content: float,
|
|
105
|
+
) -> pd.Series:
|
|
106
|
+
df = pd.DataFrame({"bin": bins, "w": w})
|
|
107
|
+
dist = df.groupby("bin", dropna=False)["w"].sum()
|
|
108
|
+
total = float(dist.sum()) or 1.0
|
|
109
|
+
dist = dist / total
|
|
110
|
+
return dist.reindex(dist.index.union([_MISSING_BIN]), fill_value=0.0).clip(lower=content)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _psi_from_distributions(expected: pd.Series, actual: pd.Series, content: float) -> float:
|
|
114
|
+
all_bins = expected.index.union(actual.index)
|
|
115
|
+
e = expected.reindex(all_bins, fill_value=content).astype(float)
|
|
116
|
+
a = actual.reindex(all_bins, fill_value=content).astype(float)
|
|
117
|
+
e = e.clip(lower=content)
|
|
118
|
+
a = a.clip(lower=content)
|
|
119
|
+
return float(((a - e) * np.log(a / e)).sum())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _weighted_iv_for_var(
|
|
123
|
+
x: np.ndarray,
|
|
124
|
+
y: np.ndarray,
|
|
125
|
+
w: np.ndarray,
|
|
126
|
+
n_bins: int,
|
|
127
|
+
min_bin_prop: float,
|
|
128
|
+
precision: int,
|
|
129
|
+
) -> tuple[float, int, float]:
|
|
130
|
+
edges = _weighted_equal_freq_edges(x, w, n_bins, min_bin_prop, precision=precision)
|
|
131
|
+
bins = _assign_bins(x, edges)
|
|
132
|
+
total_bad = float(np.sum(w * y))
|
|
133
|
+
total_good = float(np.sum(w * (1.0 - y)))
|
|
134
|
+
if total_bad <= 0 or total_good <= 0:
|
|
135
|
+
missing_rate = 1.0 - float(np.sum(w[np.isfinite(x)])) / float(np.sum(w) or 1.0)
|
|
136
|
+
return 0.0, 0, missing_rate
|
|
137
|
+
|
|
138
|
+
rows = []
|
|
139
|
+
for b in pd.unique(bins):
|
|
140
|
+
m = bins == b
|
|
141
|
+
bad_w = float(np.sum(w[m] * y[m]))
|
|
142
|
+
good_w = float(np.sum(w[m] * (1.0 - y[m])))
|
|
143
|
+
rows.append({
|
|
144
|
+
"bad_pct": bad_w / total_bad,
|
|
145
|
+
"good_pct": good_w / total_good,
|
|
146
|
+
})
|
|
147
|
+
if not rows:
|
|
148
|
+
missing_rate = 1.0 - float(np.sum(w[np.isfinite(x)])) / float(np.sum(w) or 1.0)
|
|
149
|
+
return 0.0, 0, missing_rate
|
|
150
|
+
|
|
151
|
+
stats = pd.DataFrame(rows)
|
|
152
|
+
iv = float(calc_iv(stats, "bad_pct", "good_pct").sum())
|
|
153
|
+
missing_rate = 1.0 - float(np.sum(w[np.isfinite(x)])) / float(np.sum(w) or 1.0)
|
|
154
|
+
return iv, len(rows), missing_rate
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _weighted_pearson_corr_matrix(X: np.ndarray, w: np.ndarray) -> np.ndarray:
|
|
158
|
+
if X.shape[1] == 0:
|
|
159
|
+
return np.empty((0, 0))
|
|
160
|
+
w = np.asarray(w, dtype=float)
|
|
161
|
+
w_sum = w.sum()
|
|
162
|
+
if w_sum <= 0:
|
|
163
|
+
return np.corrcoef(X, rowvar=False)
|
|
164
|
+
w_norm = w / w_sum
|
|
165
|
+
mean = np.average(X, axis=0, weights=w_norm)
|
|
166
|
+
Xc = X - mean
|
|
167
|
+
cov = np.cov(Xc, rowvar=False, aweights=w, ddof=0)
|
|
168
|
+
std = np.sqrt(np.diag(cov))
|
|
169
|
+
std[std == 0] = np.nan
|
|
170
|
+
corr = cov / np.outer(std, std)
|
|
171
|
+
return np.nan_to_num(corr, nan=0.0)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _high_corr_pairs(varlist: list[str], corr: np.ndarray, threshold: float) -> pd.DataFrame:
|
|
175
|
+
rows = []
|
|
176
|
+
for i in range(len(varlist)):
|
|
177
|
+
for j in range(i + 1, len(varlist)):
|
|
178
|
+
c = corr[i, j]
|
|
179
|
+
if abs(c) > threshold:
|
|
180
|
+
rows.append({"var_a": varlist[i], "var_b": varlist[j], "corr": float(c)})
|
|
181
|
+
return pd.DataFrame(rows)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _corr_dedup_weighted(
|
|
185
|
+
varlist: list[str],
|
|
186
|
+
corr: np.ndarray,
|
|
187
|
+
iv_map: dict[str, float],
|
|
188
|
+
threshold: float,
|
|
189
|
+
max_iterations: int,
|
|
190
|
+
) -> tuple[list[str], pd.DataFrame]:
|
|
191
|
+
dropped_rows: list[dict] = []
|
|
192
|
+
current = list(varlist)
|
|
193
|
+
idx = {v: i for i, v in enumerate(varlist)}
|
|
194
|
+
|
|
195
|
+
for _ in range(max_iterations):
|
|
196
|
+
if len(current) <= 1:
|
|
197
|
+
break
|
|
198
|
+
sub_idx = [idx[v] for v in current]
|
|
199
|
+
sub_corr = corr[np.ix_(sub_idx, sub_idx)]
|
|
200
|
+
pairs = _high_corr_pairs(current, sub_corr, threshold)
|
|
201
|
+
if pairs.empty:
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
remove: set[str] = set()
|
|
205
|
+
for _, row in pairs.iterrows():
|
|
206
|
+
v1, v2 = row["var_a"], row["var_b"]
|
|
207
|
+
if v1 in remove or v2 in remove:
|
|
208
|
+
continue
|
|
209
|
+
iv1 = iv_map.get(v1, 0.0)
|
|
210
|
+
iv2 = iv_map.get(v2, 0.0)
|
|
211
|
+
if iv1 >= iv2:
|
|
212
|
+
kept, dropped = v1, v2
|
|
213
|
+
else:
|
|
214
|
+
kept, dropped = v2, v1
|
|
215
|
+
remove.add(dropped)
|
|
216
|
+
dropped_rows.append({
|
|
217
|
+
"var_a": v1,
|
|
218
|
+
"var_b": v2,
|
|
219
|
+
"corr": row["corr"],
|
|
220
|
+
"iv_a": iv1,
|
|
221
|
+
"iv_b": iv2,
|
|
222
|
+
"kept": kept,
|
|
223
|
+
"dropped": dropped,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
if not remove:
|
|
227
|
+
break
|
|
228
|
+
current = [v for v in current if v not in remove]
|
|
229
|
+
|
|
230
|
+
return current, pd.DataFrame(dropped_rows)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _legacy_unweighted_screen(
|
|
234
|
+
splits: dict[str, pd.DataFrame],
|
|
235
|
+
feature_cols: list[str],
|
|
236
|
+
target_col: str,
|
|
237
|
+
*,
|
|
238
|
+
psi_enabled: bool,
|
|
239
|
+
psi_threshold: float,
|
|
240
|
+
psi_compare_splits: list[str],
|
|
241
|
+
iv_enabled: bool,
|
|
242
|
+
iv_threshold: float,
|
|
243
|
+
iv_bins: int,
|
|
244
|
+
iv_min_bin_prop: float,
|
|
245
|
+
corr_enabled: bool,
|
|
246
|
+
corr_threshold: float,
|
|
247
|
+
corr_max_iterations: int,
|
|
248
|
+
psi_buckets: int,
|
|
249
|
+
plot_path: str | None,
|
|
250
|
+
plot_outputs: bool,
|
|
251
|
+
iv_equal_freq: bool = True,
|
|
252
|
+
) -> WeightedScreenResult:
|
|
253
|
+
from Modeling_Tool import CorrelationFilter, PSICalculator, VarExtractionInsights
|
|
254
|
+
|
|
255
|
+
ins, oos, oot = splits["ins"], splits["oos"], splits["oot"]
|
|
256
|
+
current = list(feature_cols)
|
|
257
|
+
summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, None)]
|
|
258
|
+
|
|
259
|
+
psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
|
|
260
|
+
if psi_enabled:
|
|
261
|
+
psi_frames = []
|
|
262
|
+
if "oos" in psi_compare_splits and len(oos) > 0:
|
|
263
|
+
psi_oos = PSICalculator(buckets=psi_buckets).calculate(
|
|
264
|
+
expected_df=ins, current_data=oos, varlist=current,
|
|
265
|
+
)
|
|
266
|
+
psi_oos = psi_oos.rename(columns={"psi": "psi_ins_oos"})[["var", "psi_ins_oos"]]
|
|
267
|
+
psi_frames.append(psi_oos)
|
|
268
|
+
if "oot" in psi_compare_splits and len(oot) > 0:
|
|
269
|
+
psi_oot = PSICalculator(buckets=psi_buckets).calculate(
|
|
270
|
+
expected_df=ins, current_data=oot, varlist=current,
|
|
271
|
+
)
|
|
272
|
+
psi_oot = psi_oot.rename(columns={"psi": "psi_ins_oot"})[["var", "psi_ins_oot"]]
|
|
273
|
+
psi_frames.append(psi_oot)
|
|
274
|
+
|
|
275
|
+
if psi_frames:
|
|
276
|
+
psi_table = psi_frames[0]
|
|
277
|
+
for frame in psi_frames[1:]:
|
|
278
|
+
psi_table = psi_table.merge(frame, on="var", how="outer")
|
|
279
|
+
for col in ("psi_ins_oos", "psi_ins_oot"):
|
|
280
|
+
if col not in psi_table.columns:
|
|
281
|
+
psi_table[col] = np.nan
|
|
282
|
+
compare_cols = [c for c in ("psi_ins_oos", "psi_ins_oot") if c in psi_table.columns]
|
|
283
|
+
psi_table["psi_max"] = psi_table[compare_cols].max(axis=1, skipna=True)
|
|
284
|
+
keep = psi_table.loc[psi_table["psi_max"] < psi_threshold, "var"].tolist()
|
|
285
|
+
n_before = len(current)
|
|
286
|
+
current = keep or current
|
|
287
|
+
summary_rows.append(_summary_row("psi", n_before, len(current), psi_threshold, None))
|
|
288
|
+
|
|
289
|
+
iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
|
|
290
|
+
if iv_enabled:
|
|
291
|
+
vi = VarExtractionInsights(
|
|
292
|
+
data=ins,
|
|
293
|
+
dep=target_col,
|
|
294
|
+
plot_path=plot_path or "",
|
|
295
|
+
nbins=iv_bins,
|
|
296
|
+
equal_freq=iv_equal_freq,
|
|
297
|
+
min_bin_prop=iv_min_bin_prop,
|
|
298
|
+
)
|
|
299
|
+
iv = vi.get_var_analysis_report(data=ins, varlist=current, dep=target_col, iv_cut=0.0)
|
|
300
|
+
if plot_path and plot_outputs and current:
|
|
301
|
+
Path(plot_path, "overall").mkdir(parents=True, exist_ok=True)
|
|
302
|
+
plot_data = ins.copy()
|
|
303
|
+
plot_data["_smf_plot_group"] = "overall"
|
|
304
|
+
vi.plot_woe(
|
|
305
|
+
data=plot_data,
|
|
306
|
+
varlist=current,
|
|
307
|
+
plot_group="_smf_plot_group",
|
|
308
|
+
plot_dirname="overall",
|
|
309
|
+
plot_path=plot_path,
|
|
310
|
+
)
|
|
311
|
+
iv_table = iv.rename(columns={"iv": "iv_weighted"})[["var", "iv_weighted", "n_bins", "missing_rate"]]
|
|
312
|
+
keep = iv.loc[iv["iv"] >= iv_threshold, "var"].tolist()
|
|
313
|
+
n_before = len(current)
|
|
314
|
+
current = keep or current
|
|
315
|
+
summary_rows.append(_summary_row("iv", n_before, len(current), iv_threshold, None))
|
|
316
|
+
|
|
317
|
+
corr_dropped = pd.DataFrame(columns=["var_a", "var_b", "corr", "iv_a", "iv_b", "kept", "dropped"])
|
|
318
|
+
if corr_enabled and len(current) > 1:
|
|
319
|
+
cf = CorrelationFilter(
|
|
320
|
+
data=ins[current + [target_col]],
|
|
321
|
+
dep=target_col,
|
|
322
|
+
corr_cutpoint=corr_threshold,
|
|
323
|
+
)
|
|
324
|
+
n_before = len(current)
|
|
325
|
+
current = cf.remove_highly_correlated(current, max_iterations=corr_max_iterations)
|
|
326
|
+
summary_rows.append(_summary_row("corr", n_before, len(current), corr_threshold, None))
|
|
327
|
+
|
|
328
|
+
summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, None))
|
|
329
|
+
return WeightedScreenResult(
|
|
330
|
+
selected_features=list(current),
|
|
331
|
+
iv_table=iv_table,
|
|
332
|
+
psi_table=psi_table,
|
|
333
|
+
corr_dropped=corr_dropped,
|
|
334
|
+
summary=pd.DataFrame(summary_rows),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _weighted_screen_impl(
|
|
339
|
+
splits: dict[str, pd.DataFrame],
|
|
340
|
+
feature_cols: list[str],
|
|
341
|
+
target_col: str,
|
|
342
|
+
weight_col: str,
|
|
343
|
+
*,
|
|
344
|
+
psi_enabled: bool,
|
|
345
|
+
psi_threshold: float,
|
|
346
|
+
psi_compare_splits: list[str],
|
|
347
|
+
iv_enabled: bool,
|
|
348
|
+
iv_threshold: float,
|
|
349
|
+
iv_bins: int,
|
|
350
|
+
min_bin_prop: float,
|
|
351
|
+
corr_enabled: bool,
|
|
352
|
+
corr_threshold: float,
|
|
353
|
+
corr_max_iterations: int,
|
|
354
|
+
content: float,
|
|
355
|
+
precision: int,
|
|
356
|
+
) -> WeightedScreenResult:
|
|
357
|
+
ins = splits["ins"]
|
|
358
|
+
oos = splits["oos"]
|
|
359
|
+
oot = splits["oot"]
|
|
360
|
+
w_ins = resolve_sample_weight(data=ins, weight_col=weight_col, expected_len=len(ins))
|
|
361
|
+
|
|
362
|
+
current = list(feature_cols)
|
|
363
|
+
summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, weight_col)]
|
|
364
|
+
|
|
365
|
+
# Precompute bin edges on ins for PSI / IV
|
|
366
|
+
edges_cache: dict[str, list[float] | None] = {}
|
|
367
|
+
y_ins = ins[target_col].astype(float).to_numpy()
|
|
368
|
+
|
|
369
|
+
psi_records: list[dict] = []
|
|
370
|
+
if psi_enabled:
|
|
371
|
+
for var in feature_cols:
|
|
372
|
+
if var not in ins.columns or ins[var].nunique(dropna=False) <= 1:
|
|
373
|
+
continue
|
|
374
|
+
x_ins = ins[var].to_numpy(dtype=float)
|
|
375
|
+
if var not in edges_cache:
|
|
376
|
+
edges_cache[var] = _weighted_equal_freq_edges(
|
|
377
|
+
x_ins, w_ins, iv_bins, min_bin_prop, precision=precision,
|
|
378
|
+
)
|
|
379
|
+
edges = edges_cache[var]
|
|
380
|
+
bins_ins = _assign_bins(x_ins, edges) if edges else _assign_bins(x_ins, None)
|
|
381
|
+
exp_dist = _weighted_bin_distribution(bins_ins, w_ins, content)
|
|
382
|
+
|
|
383
|
+
row: dict[str, Any] = {"var": var, "psi_ins_oos": np.nan, "psi_ins_oot": np.nan}
|
|
384
|
+
if "oos" in psi_compare_splits and len(oos) > 0:
|
|
385
|
+
w_oos = resolve_sample_weight(data=oos, weight_col=weight_col, expected_len=len(oos))
|
|
386
|
+
bins_oos = _assign_bins(oos[var].to_numpy(dtype=float), edges)
|
|
387
|
+
act = _weighted_bin_distribution(bins_oos, w_oos, content)
|
|
388
|
+
row["psi_ins_oos"] = _psi_from_distributions(exp_dist, act, content)
|
|
389
|
+
if "oot" in psi_compare_splits and len(oot) > 0:
|
|
390
|
+
w_oot = resolve_sample_weight(data=oot, weight_col=weight_col, expected_len=len(oot))
|
|
391
|
+
bins_oot = _assign_bins(oot[var].to_numpy(dtype=float), edges)
|
|
392
|
+
act = _weighted_bin_distribution(bins_oot, w_oot, content)
|
|
393
|
+
row["psi_ins_oot"] = _psi_from_distributions(exp_dist, act, content)
|
|
394
|
+
psi_records.append(row)
|
|
395
|
+
|
|
396
|
+
psi_table = pd.DataFrame(psi_records) if psi_records else pd.DataFrame(
|
|
397
|
+
columns=["var", "psi_ins_oos", "psi_ins_oot"],
|
|
398
|
+
)
|
|
399
|
+
if not psi_table.empty:
|
|
400
|
+
compare_cols = [c for c in ("psi_ins_oos", "psi_ins_oot") if c in psi_table.columns]
|
|
401
|
+
psi_table["psi_max"] = psi_table[compare_cols].max(axis=1, skipna=True)
|
|
402
|
+
keep = psi_table.loc[psi_table["psi_max"] < psi_threshold, "var"].tolist()
|
|
403
|
+
n_before = len(current)
|
|
404
|
+
current = [v for v in current if v in keep] or current
|
|
405
|
+
summary_rows.append(_summary_row("psi", n_before, len(current), psi_threshold, weight_col))
|
|
406
|
+
else:
|
|
407
|
+
psi_table["psi_max"] = pd.Series(dtype=float)
|
|
408
|
+
else:
|
|
409
|
+
psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
|
|
410
|
+
|
|
411
|
+
iv_records: list[dict] = []
|
|
412
|
+
if iv_enabled:
|
|
413
|
+
for var in current:
|
|
414
|
+
if var not in ins.columns or ins[var].nunique(dropna=False) <= 1:
|
|
415
|
+
continue
|
|
416
|
+
iv_val, n_b, miss = _weighted_iv_for_var(
|
|
417
|
+
ins[var].to_numpy(dtype=float),
|
|
418
|
+
y_ins,
|
|
419
|
+
w_ins,
|
|
420
|
+
iv_bins,
|
|
421
|
+
min_bin_prop,
|
|
422
|
+
precision,
|
|
423
|
+
)
|
|
424
|
+
iv_records.append({
|
|
425
|
+
"var": var,
|
|
426
|
+
"iv_weighted": iv_val,
|
|
427
|
+
"n_bins": n_b,
|
|
428
|
+
"missing_rate": miss,
|
|
429
|
+
})
|
|
430
|
+
iv_table = pd.DataFrame(iv_records) if iv_records else pd.DataFrame(
|
|
431
|
+
columns=["var", "iv_weighted", "n_bins", "missing_rate"],
|
|
432
|
+
)
|
|
433
|
+
if not iv_table.empty:
|
|
434
|
+
keep = iv_table.loc[iv_table["iv_weighted"] >= iv_threshold, "var"].tolist()
|
|
435
|
+
n_before = len(current)
|
|
436
|
+
current = [v for v in current if v in keep] or current
|
|
437
|
+
summary_rows.append(_summary_row("iv", n_before, len(current), iv_threshold, weight_col))
|
|
438
|
+
else:
|
|
439
|
+
iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
|
|
440
|
+
|
|
441
|
+
corr_dropped = pd.DataFrame(columns=["var_a", "var_b", "corr", "iv_a", "iv_b", "kept", "dropped"])
|
|
442
|
+
if corr_enabled and len(current) > 1:
|
|
443
|
+
X = ins[current].astype(float).to_numpy()
|
|
444
|
+
corr = _weighted_pearson_corr_matrix(X, w_ins)
|
|
445
|
+
iv_map = dict(zip(iv_table["var"], iv_table["iv_weighted"])) if not iv_table.empty else {}
|
|
446
|
+
n_before = len(current)
|
|
447
|
+
current, corr_dropped = _corr_dedup_weighted(
|
|
448
|
+
current, corr, iv_map, corr_threshold, corr_max_iterations,
|
|
449
|
+
)
|
|
450
|
+
summary_rows.append(_summary_row("corr", n_before, len(current), corr_threshold, weight_col))
|
|
451
|
+
|
|
452
|
+
summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, weight_col))
|
|
453
|
+
return WeightedScreenResult(
|
|
454
|
+
selected_features=list(current),
|
|
455
|
+
iv_table=iv_table,
|
|
456
|
+
psi_table=psi_table,
|
|
457
|
+
corr_dropped=corr_dropped,
|
|
458
|
+
summary=pd.DataFrame(summary_rows),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def weighted_feature_screen(
|
|
463
|
+
data: pd.DataFrame,
|
|
464
|
+
feature_cols: list[str],
|
|
465
|
+
target_col: str,
|
|
466
|
+
split_col: str,
|
|
467
|
+
weight_col: str | None = None,
|
|
468
|
+
psi_enabled: bool = True,
|
|
469
|
+
psi_threshold: float = 0.2,
|
|
470
|
+
psi_compare_splits: list[str] | None = None,
|
|
471
|
+
iv_enabled: bool = True,
|
|
472
|
+
iv_threshold: float = 0.02,
|
|
473
|
+
corr_enabled: bool = True,
|
|
474
|
+
corr_threshold: float = 0.75,
|
|
475
|
+
iv_bins: int = 10,
|
|
476
|
+
min_bin_prop: float = 0.05,
|
|
477
|
+
iv_min_bin_prop: float | None = None,
|
|
478
|
+
corr_max_iterations: int = 10,
|
|
479
|
+
content: float = 1e-6,
|
|
480
|
+
precision: int = 5,
|
|
481
|
+
psi_buckets: int | None = None,
|
|
482
|
+
plot_path: str | None = None,
|
|
483
|
+
plot_outputs: bool = False,
|
|
484
|
+
iv_equal_freq: bool = True,
|
|
485
|
+
) -> WeightedScreenResult:
|
|
486
|
+
"""Run PSI -> IV -> correlation screening with optional sample weights.
|
|
487
|
+
|
|
488
|
+
Bin edges are fit on ``ins`` only. When ``weight_col`` is None the legacy
|
|
489
|
+
unweighted tools are used for backward-compatible results with
|
|
490
|
+
``CreditModelPipeline`` (IV uses tree binning via ``VarExtractionInsights``).
|
|
491
|
+
Weighted screening uses equal-frequency bins on weighted quantiles.
|
|
492
|
+
"""
|
|
493
|
+
if iv_min_bin_prop is None:
|
|
494
|
+
iv_min_bin_prop = min_bin_prop
|
|
495
|
+
if psi_buckets is None:
|
|
496
|
+
psi_buckets = iv_bins
|
|
497
|
+
if psi_compare_splits is None:
|
|
498
|
+
psi_compare_splits = ["oos", "oot"]
|
|
499
|
+
|
|
500
|
+
exclude = {target_col, split_col}
|
|
501
|
+
if weight_col:
|
|
502
|
+
exclude.add(weight_col)
|
|
503
|
+
features = [c for c in feature_cols if c not in exclude and c in data.columns]
|
|
504
|
+
|
|
505
|
+
splits = _resolve_splits(data, split_col)
|
|
506
|
+
|
|
507
|
+
if weight_col is None:
|
|
508
|
+
return _legacy_unweighted_screen(
|
|
509
|
+
splits,
|
|
510
|
+
features,
|
|
511
|
+
target_col,
|
|
512
|
+
psi_enabled=psi_enabled,
|
|
513
|
+
psi_threshold=psi_threshold,
|
|
514
|
+
psi_compare_splits=list(psi_compare_splits),
|
|
515
|
+
iv_enabled=iv_enabled,
|
|
516
|
+
iv_threshold=iv_threshold,
|
|
517
|
+
iv_bins=iv_bins,
|
|
518
|
+
iv_min_bin_prop=iv_min_bin_prop,
|
|
519
|
+
corr_enabled=corr_enabled,
|
|
520
|
+
corr_threshold=corr_threshold,
|
|
521
|
+
corr_max_iterations=corr_max_iterations,
|
|
522
|
+
psi_buckets=psi_buckets,
|
|
523
|
+
plot_path=plot_path,
|
|
524
|
+
plot_outputs=plot_outputs,
|
|
525
|
+
iv_equal_freq=iv_equal_freq,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
if weight_col not in data.columns:
|
|
529
|
+
raise KeyError(f"Missing weight_col {weight_col!r}")
|
|
530
|
+
|
|
531
|
+
return _weighted_screen_impl(
|
|
532
|
+
splits,
|
|
533
|
+
features,
|
|
534
|
+
target_col,
|
|
535
|
+
weight_col,
|
|
536
|
+
psi_enabled=psi_enabled,
|
|
537
|
+
psi_threshold=psi_threshold,
|
|
538
|
+
psi_compare_splits=list(psi_compare_splits),
|
|
539
|
+
iv_enabled=iv_enabled,
|
|
540
|
+
iv_threshold=iv_threshold,
|
|
541
|
+
iv_bins=iv_bins,
|
|
542
|
+
min_bin_prop=min_bin_prop,
|
|
543
|
+
corr_enabled=corr_enabled,
|
|
544
|
+
corr_threshold=corr_threshold,
|
|
545
|
+
corr_max_iterations=corr_max_iterations,
|
|
546
|
+
content=content,
|
|
547
|
+
precision=precision,
|
|
548
|
+
)
|
|
@@ -25,6 +25,11 @@ from .WOE_Engine_Feature_Patch import (
|
|
|
25
25
|
CorrelationFilter,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
from .Weighted_Screen import (
|
|
29
|
+
WeightedScreenResult,
|
|
30
|
+
weighted_feature_screen,
|
|
31
|
+
)
|
|
32
|
+
|
|
28
33
|
__all__ = [
|
|
29
34
|
# Distribution_Tool
|
|
30
35
|
'proc_means', 'proc_means_by_grp', 'get_distribution_shift',
|
|
@@ -37,4 +42,7 @@ __all__ = [
|
|
|
37
42
|
# PSI_Tool
|
|
38
43
|
'PSICalculator', 'calculate_psi', 'calculate_within_psi',
|
|
39
44
|
'calculate_psi_within_dataset', 'calculate_multivar_psi_two_sets',
|
|
45
|
+
|
|
46
|
+
# Weighted_Screen
|
|
47
|
+
'WeightedScreenResult', 'weighted_feature_screen',
|
|
40
48
|
]
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/credit_model.py
RENAMED
|
@@ -45,10 +45,12 @@ class CreditModelPipelineConfig:
|
|
|
45
45
|
default_factory=lambda: {
|
|
46
46
|
"psi_enabled": True,
|
|
47
47
|
"psi_threshold": 0.2,
|
|
48
|
+
"psi_compare_splits": ["oos"],
|
|
48
49
|
"iv_enabled": True,
|
|
49
50
|
"iv_threshold": 0.02,
|
|
50
51
|
"corr_enabled": True,
|
|
51
52
|
"corr_threshold": 0.75,
|
|
53
|
+
"corr_max_iterations": 10,
|
|
52
54
|
}
|
|
53
55
|
)
|
|
54
56
|
woe_engine: str = "equal_freq"
|
|
@@ -427,79 +429,78 @@ class CreditModelPipeline:
|
|
|
427
429
|
splits: dict[str, pd.DataFrame],
|
|
428
430
|
feature_cols: list[str],
|
|
429
431
|
) -> tuple[dict[str, Any], list[str]]:
|
|
430
|
-
from Modeling_Tool import
|
|
432
|
+
from Modeling_Tool.Feature.Weighted_Screen import weighted_feature_screen
|
|
431
433
|
|
|
432
434
|
cfg = self.config
|
|
433
435
|
fs_cfg = cfg.feature_selection
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
varlist=current,
|
|
444
|
-
)
|
|
445
|
-
keep = psi.loc[psi["psi"] < float(fs_cfg.get("psi_threshold", 0.2)), "var"].tolist()
|
|
446
|
-
current = keep or current
|
|
447
|
-
summary["psi"] = psi
|
|
448
|
-
except Exception as exc:
|
|
449
|
-
summary["psi_error"] = repr(exc)
|
|
450
|
-
|
|
436
|
+
split_col = "_smf_split"
|
|
437
|
+
screen_frames = []
|
|
438
|
+
for name, df in splits.items():
|
|
439
|
+
part = df.copy()
|
|
440
|
+
part[split_col] = name
|
|
441
|
+
screen_frames.append(part)
|
|
442
|
+
screen_data = pd.concat(screen_frames, ignore_index=True)
|
|
443
|
+
|
|
444
|
+
plot_path = None
|
|
451
445
|
if fs_cfg.get("iv_enabled", True):
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
make_dirs(plot_path)
|
|
455
|
-
vi = VarExtractionInsights(
|
|
456
|
-
data=ins,
|
|
457
|
-
dep=cfg.target_col,
|
|
458
|
-
plot_path=plot_path,
|
|
459
|
-
nbins=int(fs_cfg.get("iv_nbins", 10)),
|
|
460
|
-
equal_freq=bool(fs_cfg.get("iv_equal_freq", True)),
|
|
461
|
-
min_bin_prop=float(fs_cfg.get("iv_min_bin_prop", 0.05)),
|
|
462
|
-
)
|
|
463
|
-
iv = vi.get_var_analysis_report(
|
|
464
|
-
data=ins,
|
|
465
|
-
varlist=current,
|
|
466
|
-
dep=cfg.target_col,
|
|
467
|
-
iv_cut=0.0,
|
|
468
|
-
)
|
|
469
|
-
if cfg.write_outputs and cfg.plot_outputs and current:
|
|
470
|
-
make_dirs(Path(plot_path) / "overall")
|
|
471
|
-
plot_data = ins.copy()
|
|
472
|
-
plot_data["_smf_plot_group"] = "overall"
|
|
473
|
-
vi.plot_woe(
|
|
474
|
-
data=plot_data,
|
|
475
|
-
varlist=current,
|
|
476
|
-
plot_group="_smf_plot_group",
|
|
477
|
-
plot_dirname="overall",
|
|
478
|
-
plot_path=plot_path,
|
|
479
|
-
)
|
|
480
|
-
keep = iv.loc[iv["iv"] >= float(fs_cfg.get("iv_threshold", 0.02)), "var"].tolist()
|
|
481
|
-
current = keep or current
|
|
482
|
-
summary["iv"] = iv
|
|
483
|
-
except Exception as exc:
|
|
484
|
-
summary["iv_error"] = repr(exc)
|
|
446
|
+
plot_path = str(Path(cfg.output_dir) / "figs" / "var_analysis")
|
|
447
|
+
if cfg.write_outputs and cfg.plot_outputs:
|
|
448
|
+
make_dirs(plot_path, Path(plot_path) / "overall")
|
|
485
449
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
450
|
+
summary: dict[str, Any] = {"initial_features": list(feature_cols)}
|
|
451
|
+
try:
|
|
452
|
+
result = weighted_feature_screen(
|
|
453
|
+
data=screen_data,
|
|
454
|
+
feature_cols=feature_cols,
|
|
455
|
+
target_col=cfg.target_col,
|
|
456
|
+
split_col=split_col,
|
|
457
|
+
weight_col=cfg.weight_col,
|
|
458
|
+
psi_enabled=bool(fs_cfg.get("psi_enabled", True)),
|
|
459
|
+
psi_threshold=float(fs_cfg.get("psi_threshold", 0.2)),
|
|
460
|
+
psi_compare_splits=list(fs_cfg.get("psi_compare_splits", ["oos"])),
|
|
461
|
+
iv_enabled=bool(fs_cfg.get("iv_enabled", True)),
|
|
462
|
+
iv_threshold=float(fs_cfg.get("iv_threshold", 0.02)),
|
|
463
|
+
corr_enabled=bool(fs_cfg.get("corr_enabled", True)),
|
|
464
|
+
corr_threshold=float(fs_cfg.get("corr_threshold", 0.75)),
|
|
465
|
+
iv_bins=int(fs_cfg.get("iv_nbins", 10)),
|
|
466
|
+
iv_min_bin_prop=float(fs_cfg.get("iv_min_bin_prop", 0.05)),
|
|
467
|
+
corr_max_iterations=int(fs_cfg.get("corr_max_iterations", 10)),
|
|
468
|
+
psi_buckets=int(fs_cfg.get("psi_buckets", fs_cfg.get("iv_nbins", 10))),
|
|
469
|
+
plot_path=plot_path,
|
|
470
|
+
plot_outputs=bool(cfg.write_outputs and cfg.plot_outputs),
|
|
471
|
+
iv_equal_freq=bool(fs_cfg.get("iv_equal_freq", True)),
|
|
472
|
+
)
|
|
473
|
+
summary = self._screen_result_to_summary(result, feature_cols)
|
|
474
|
+
return summary, list(result.selected_features)
|
|
475
|
+
except Exception as exc:
|
|
476
|
+
summary["error"] = repr(exc)
|
|
477
|
+
summary["final_features"] = list(feature_cols)
|
|
478
|
+
return summary, list(feature_cols)
|
|
500
479
|
|
|
501
|
-
|
|
502
|
-
|
|
480
|
+
def _screen_result_to_summary(
|
|
481
|
+
self,
|
|
482
|
+
result: Any,
|
|
483
|
+
initial_features: list[str],
|
|
484
|
+
) -> dict[str, Any]:
|
|
485
|
+
summary: dict[str, Any] = {"initial_features": list(initial_features)}
|
|
486
|
+
if not result.psi_table.empty:
|
|
487
|
+
psi = result.psi_table.copy()
|
|
488
|
+
if "psi_ins_oos" in psi.columns:
|
|
489
|
+
psi["psi"] = psi["psi_ins_oos"]
|
|
490
|
+
elif "psi_max" in psi.columns:
|
|
491
|
+
psi["psi"] = psi["psi_max"]
|
|
492
|
+
summary["psi"] = psi
|
|
493
|
+
if not result.iv_table.empty:
|
|
494
|
+
iv = result.iv_table.copy()
|
|
495
|
+
if "iv_weighted" in iv.columns:
|
|
496
|
+
iv = iv.rename(columns={"iv_weighted": "iv"})
|
|
497
|
+
summary["iv"] = iv
|
|
498
|
+
if not result.corr_dropped.empty:
|
|
499
|
+
summary["corr_dropped"] = result.corr_dropped
|
|
500
|
+
summary["corr_features"] = list(result.selected_features)
|
|
501
|
+
summary["screen_summary"] = result.summary
|
|
502
|
+
summary["final_features"] = list(result.selected_features)
|
|
503
|
+
return summary
|
|
503
504
|
|
|
504
505
|
def _transform_extra_eval_datasets(
|
|
505
506
|
self,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# encoding: utf-8
|
|
2
2
|
|
|
3
3
|
__author__ = "Jingkai Sun"
|
|
4
|
-
__version__ = "0.3.
|
|
4
|
+
__version__ = "0.3.8"
|
|
5
5
|
|
|
6
6
|
# Core module - base data structures
|
|
7
7
|
from .Core import (
|
|
@@ -87,6 +87,8 @@ from .Feature import (
|
|
|
87
87
|
CorrelationFilter,
|
|
88
88
|
PSICalculator,
|
|
89
89
|
calculate_psi_within_dataset,
|
|
90
|
+
WeightedScreenResult,
|
|
91
|
+
weighted_feature_screen,
|
|
90
92
|
)
|
|
91
93
|
|
|
92
94
|
_MODEL_EXPORTS = frozenset({
|
|
@@ -228,4 +230,6 @@ __all__ = [
|
|
|
228
230
|
'CorrelationFilter',
|
|
229
231
|
'PSICalculator',
|
|
230
232
|
'calculate_psi_within_dataset',
|
|
233
|
+
'WeightedScreenResult',
|
|
234
|
+
'weighted_feature_screen',
|
|
231
235
|
]
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/SOURCES.txt
RENAMED
|
@@ -46,6 +46,7 @@ Modeling_Tool/Feature/Feature_Insights.pyi
|
|
|
46
46
|
Modeling_Tool/Feature/PSI_Tool.py
|
|
47
47
|
Modeling_Tool/Feature/PSI_Tool.pyi
|
|
48
48
|
Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py
|
|
49
|
+
Modeling_Tool/Feature/Weighted_Screen.py
|
|
49
50
|
Modeling_Tool/Feature/__init__.py
|
|
50
51
|
Modeling_Tool/Model/Backward_Tool.py
|
|
51
52
|
Modeling_Tool/Model/Backward_Tool.pyi
|
|
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
|
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
14
|
name = "SuperModelingFactory"
|
|
15
|
-
version = "0.3.
|
|
15
|
+
version = "0.3.8"
|
|
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.
|
|
18
|
+
version=os.environ.get("SMF_VERSION", "0.3.8"),
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Binning_Tool.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Binning_Tool.pyi
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Json_Data_Converter.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Model_Registry_Tool.py
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Parallel_Engine.py
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/Proc_Compare.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/XOR_Encryptor.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/XOR_Encryptor.pyi
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Core/sample_weight_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Evaluation_Tool.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Evaluation_Tool.pyi
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Model_Eval_Tool.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/Model_Eval_Tool.pyi
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/evaluate_model.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/evaluate_model.pyi
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Eval/weighted_eval_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Explainability/__init__.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/Distribution_Tool.py
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/Feature_Insights.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/Feature_Insights.pyi
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Feature/PSI_Tool.pyi
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/Backward_Tool.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/Backward_Tool.pyi
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Model/GBM_Search_Tool.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/mock_sample.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/reject_inference.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/sample_analysis.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Pipeline/score_comparison.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Reject_Infer.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Reject_Infer.pyi
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Sample_Split.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/Sample/Sample_Split.pyi
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Plot_Tool.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Report_Builder.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/WOE_Report_Builder.pyi
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/plot_woe_tool.py
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/WOE/plot_woe_tool.pyi
RENAMED
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/Modeling_Tool/ref_font/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{supermodelingfactory-0.3.7 → supermodelingfactory-0.3.8}/SuperModelingFactory.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|