SuperModelingFactory 0.4.2__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Feature_Insights.py +24 -5
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Feature_Screen.py +6 -5
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/PSI_Tool.py +204 -39
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/PSI_Tool.pyi +7 -7
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +101 -14
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Weighted_Screen.py +80 -42
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/reject_inference.py +31 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Master.py +39 -1
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Master.pyi +1 -1
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +76 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/__init__.py +4 -1
- supermodelingfactory-0.5.1/Modeling_Tool/_utils/__init__.py +6 -0
- supermodelingfactory-0.5.1/Modeling_Tool/_utils/robust.py +55 -0
- supermodelingfactory-0.5.1/Modeling_Tool/_utils/sentinels.py +10 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/PKG-INFO +1 -1
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/SuperModelingFactory.egg-info/PKG-INFO +1 -1
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/SuperModelingFactory.egg-info/SOURCES.txt +3 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/pyproject.toml +1 -1
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/setup.py +1 -1
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/ExcelMaster/ExcelFormatTool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/ExcelMaster/ExcelMaster.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/ExcelMaster/Template.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/ExcelMaster/Utility.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/ExcelMaster/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/LICENSE +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/MANIFEST.in +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Binning_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Proc_Compare.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Slope_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/kDataFrame.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Core/utils.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/evaluate_model.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Explainability/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/Backward_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/GBM_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/LRM_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Model/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/_common.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/credit_model.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/feature_validation.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Sample/Sample_Split.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Sample/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/UAT/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/ref_font/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/ref_font/simsun.ttc +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/README.md +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Report/Report_Tool.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Report/__init__.py +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/SuperModelingFactory.egg-info/requires.txt +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/SuperModelingFactory.egg-info/top_level.txt +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/requirements.txt +0 -0
- {supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/setup.cfg +0 -0
{supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Feature_Insights.py
RENAMED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
|
+
import warnings
|
|
7
8
|
from tqdm import tqdm
|
|
8
9
|
|
|
9
10
|
from .Distribution_Tool import proc_means_by_grp, proc_means_for_screening
|
|
11
|
+
from Modeling_Tool._utils.robust import smf_logger
|
|
10
12
|
import logging
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
@@ -108,6 +110,7 @@ class VarExtractionInsights:
|
|
|
108
110
|
self.seed = seed
|
|
109
111
|
self.missing_rate_ref = missing_rate_ref
|
|
110
112
|
self.spec_values = spec_values if spec_values is not None else []
|
|
113
|
+
self.failed_variables = []
|
|
111
114
|
|
|
112
115
|
@staticmethod
|
|
113
116
|
def remove_folder(file_path):
|
|
@@ -170,6 +173,7 @@ class VarExtractionInsights:
|
|
|
170
173
|
"""
|
|
171
174
|
if dep is None:
|
|
172
175
|
dep = self.dep
|
|
176
|
+
self.failed_variables = []
|
|
173
177
|
|
|
174
178
|
from Modeling_Tool.Eval.Model_Eval_Tool import get_gains_table
|
|
175
179
|
|
|
@@ -200,9 +204,27 @@ class VarExtractionInsights:
|
|
|
200
204
|
attr_iv['var'] = var
|
|
201
205
|
iv_info_res.append(attr_iv)
|
|
202
206
|
|
|
203
|
-
except TypeError:
|
|
207
|
+
except (TypeError, ValueError, KeyError, ZeroDivisionError) as exc:
|
|
208
|
+
row = smf_logger.record_and_continue(var, exc, stage="feature_insights")
|
|
209
|
+
self.failed_variables.append((row["feature"], row["exception_type"]))
|
|
204
210
|
continue
|
|
205
211
|
|
|
212
|
+
if self.failed_variables:
|
|
213
|
+
failed = [name for name, _ in self.failed_variables]
|
|
214
|
+
warnings.warn(
|
|
215
|
+
f"{len(failed)}/{len(varlist)} variables failed insight computation: "
|
|
216
|
+
f"{failed[:10]}{'...' if len(failed) > 10 else ''} — see smf_logger for details",
|
|
217
|
+
UserWarning,
|
|
218
|
+
stacklevel=2,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
output_cols = [
|
|
222
|
+
'var', 'n_all', 'n', 'ks_in_gains', 'lift_in_gains', 'iv',
|
|
223
|
+
'n_bump', 'missing_rate', 'min', 'mean', 'max', 'n_bins'
|
|
224
|
+
]
|
|
225
|
+
if not iv_info_res:
|
|
226
|
+
return pd.DataFrame(columns=output_cols)
|
|
227
|
+
|
|
206
228
|
iv_info_res = pd.concat(iv_info_res).sort_values("IV", ascending=False)
|
|
207
229
|
|
|
208
230
|
high_iv_summary = iv_info_res.query(f"IV >= {iv_cut}").round(4)
|
|
@@ -222,10 +244,7 @@ class VarExtractionInsights:
|
|
|
222
244
|
how='left'
|
|
223
245
|
)
|
|
224
246
|
fnl_summary.columns = [x.lower() for x in fnl_summary.columns]
|
|
225
|
-
fnl_summary = fnl_summary[
|
|
226
|
-
'var', 'n_all', 'n', 'ks_in_gains', 'lift_in_gains', 'iv',
|
|
227
|
-
'n_bump', 'missing_rate', 'min', 'mean', 'max', 'n_bins'
|
|
228
|
-
]]
|
|
247
|
+
fnl_summary = fnl_summary[output_cols]
|
|
229
248
|
|
|
230
249
|
return fnl_summary
|
|
231
250
|
|
{supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/Feature_Screen.py
RENAMED
|
@@ -10,6 +10,7 @@ import numpy as np
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
|
|
12
12
|
from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
|
|
13
|
+
from Modeling_Tool._utils.sentinels import SMF_MISSING_BIN
|
|
13
14
|
|
|
14
15
|
from .Weighted_Screen import (
|
|
15
16
|
WeightedScreenResult,
|
|
@@ -163,7 +164,7 @@ def fit_screening_woe_engine(
|
|
|
163
164
|
|
|
164
165
|
master_params = dict(woe_params or {})
|
|
165
166
|
woe_suffix = master_params.pop("woe_suffix", "_woe")
|
|
166
|
-
missing_ref_value = master_params.pop("missing_ref_value",
|
|
167
|
+
missing_ref_value = master_params.pop("missing_ref_value", SMF_MISSING_BIN)
|
|
167
168
|
master = WOE_Master(
|
|
168
169
|
train_data=fit_ins,
|
|
169
170
|
varlist=features,
|
|
@@ -224,7 +225,7 @@ def _woe_bins_unweighted_screen(
|
|
|
224
225
|
missing_rate_ref=config.missing_rate_ref,
|
|
225
226
|
on_empty_stage=config.on_empty_stage,
|
|
226
227
|
)
|
|
227
|
-
binner = _resolve_screening_binner(splits,
|
|
228
|
+
binner = _resolve_screening_binner(splits, current, target_col, config, prefit_woe_engine)
|
|
228
229
|
|
|
229
230
|
psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
|
|
230
231
|
if config.psi_enabled:
|
|
@@ -357,7 +358,7 @@ def _weighted_woe_bins_screen(
|
|
|
357
358
|
if binner is None:
|
|
358
359
|
binner = fit_screening_woe_engine(
|
|
359
360
|
ins,
|
|
360
|
-
|
|
361
|
+
current,
|
|
361
362
|
target_col,
|
|
362
363
|
woe_engine=config.woe_engine,
|
|
363
364
|
woe_fit_query=config.woe_fit_query,
|
|
@@ -369,7 +370,7 @@ def _weighted_woe_bins_screen(
|
|
|
369
370
|
|
|
370
371
|
psi_records: list[dict] = []
|
|
371
372
|
if config.psi_enabled:
|
|
372
|
-
for var in
|
|
373
|
+
for var in current:
|
|
373
374
|
if var not in ins.columns or ins[var].nunique(dropna=False) <= 1:
|
|
374
375
|
continue
|
|
375
376
|
bins_ins = _bins_to_numpy(adapter.assign_bins(ins, var))
|
|
@@ -583,4 +584,4 @@ __all__ = [
|
|
|
583
584
|
"feature_screen_from_dataframe",
|
|
584
585
|
"fit_screening_woe_engine",
|
|
585
586
|
"screen_config_from_mapping",
|
|
586
|
-
]
|
|
587
|
+
]
|
|
@@ -20,6 +20,58 @@ from Modeling_Tool.Core.Binning_Tool import quick_binning
|
|
|
20
20
|
# ``Feature.Weighted_Screen`` and ``WOE_Adapter`` so downstream aggregators can
|
|
21
21
|
# recognise it without special-casing.
|
|
22
22
|
_MISSING_BIN = "__MISSING__"
|
|
23
|
+
_PSI_BUCKET_POLICIES = {"floor_1e6", "smooth_laplace", "exclude"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validate_psi_bucket_policy(policy: str, caller: str) -> None:
|
|
27
|
+
if policy not in _PSI_BUCKET_POLICIES:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"{caller}: psi_missing_bucket_policy must be one of "
|
|
30
|
+
f"{sorted(_PSI_BUCKET_POLICIES)}; got {policy!r}."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _psi_distributions_from_counts(
|
|
35
|
+
expected_count: pd.Series,
|
|
36
|
+
actual_count: pd.Series,
|
|
37
|
+
expected_total: float,
|
|
38
|
+
actual_total: float,
|
|
39
|
+
*,
|
|
40
|
+
content: float,
|
|
41
|
+
policy: str,
|
|
42
|
+
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
|
|
43
|
+
_validate_psi_bucket_policy(policy, "_psi_distributions_from_counts")
|
|
44
|
+
all_bins = expected_count.index.union(actual_count.index)
|
|
45
|
+
expected_aligned = expected_count.reindex(all_bins, fill_value=0).astype(float)
|
|
46
|
+
actual_aligned = actual_count.reindex(all_bins, fill_value=0).astype(float)
|
|
47
|
+
one_sided = (expected_aligned == 0) | (actual_aligned == 0)
|
|
48
|
+
|
|
49
|
+
expected_total = float(expected_total) if expected_total > 0 else 1.0
|
|
50
|
+
actual_total = float(actual_total) if actual_total > 0 else 1.0
|
|
51
|
+
|
|
52
|
+
if policy == "exclude":
|
|
53
|
+
keep = ~one_sided
|
|
54
|
+
expected_aligned = expected_aligned[keep]
|
|
55
|
+
actual_aligned = actual_aligned[keep]
|
|
56
|
+
if expected_aligned.empty:
|
|
57
|
+
empty = pd.Series(dtype=float)
|
|
58
|
+
return empty, empty, empty, empty
|
|
59
|
+
expected_pct = expected_aligned / expected_total
|
|
60
|
+
actual_pct = actual_aligned / actual_total
|
|
61
|
+
elif policy == "smooth_laplace" and bool(one_sided.any()):
|
|
62
|
+
n_buckets = max(len(all_bins), 1)
|
|
63
|
+
expected_pct = (expected_aligned + 1.0) / (expected_total + n_buckets)
|
|
64
|
+
actual_pct = (actual_aligned + 1.0) / (actual_total + n_buckets)
|
|
65
|
+
else:
|
|
66
|
+
expected_pct = (expected_aligned / expected_total).clip(lower=content)
|
|
67
|
+
actual_pct = (actual_aligned / actual_total).clip(lower=content)
|
|
68
|
+
|
|
69
|
+
psi_values = (actual_pct - expected_pct) * np.log(actual_pct / expected_pct)
|
|
70
|
+
|
|
71
|
+
floor_expected = (expected_aligned / expected_total).clip(lower=content)
|
|
72
|
+
floor_actual = (actual_aligned / actual_total).clip(lower=content)
|
|
73
|
+
floor_values = (floor_actual - floor_expected) * np.log(floor_actual / floor_expected)
|
|
74
|
+
return expected_pct, actual_pct, psi_values, floor_values
|
|
23
75
|
|
|
24
76
|
# ============================================================================
|
|
25
77
|
# Classes
|
|
@@ -58,7 +110,9 @@ class PSICalculator:
|
|
|
58
110
|
equal_freq: bool = True,
|
|
59
111
|
min_bin_prop: float = 0.05,
|
|
60
112
|
content: float = 1e-6,
|
|
61
|
-
precision: int = 5
|
|
113
|
+
precision: int = 5,
|
|
114
|
+
missing_policy: str = "include",
|
|
115
|
+
psi_missing_bucket_policy: str = "smooth_laplace",
|
|
62
116
|
):
|
|
63
117
|
"""
|
|
64
118
|
Initialize PSICalculator with configuration parameters.
|
|
@@ -75,12 +129,28 @@ class PSICalculator:
|
|
|
75
129
|
Small value to prevent division by zero. Default is 1e-6.
|
|
76
130
|
precision : int, optional
|
|
77
131
|
Decimal precision for rounding. Default is 5.
|
|
132
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
133
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
134
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
135
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
136
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
137
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
138
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
139
|
+
naming the NaN counts.
|
|
78
140
|
"""
|
|
141
|
+
if missing_policy not in {"include", "drop", "warn_and_drop"}:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"PSICalculator.__init__: missing_policy must be one of "
|
|
144
|
+
f"'include', 'drop', 'warn_and_drop'; got {missing_policy!r}."
|
|
145
|
+
)
|
|
146
|
+
_validate_psi_bucket_policy(psi_missing_bucket_policy, "PSICalculator.__init__")
|
|
79
147
|
self.buckets = buckets
|
|
80
148
|
self.equal_freq = equal_freq
|
|
81
149
|
self.min_bin_prop = min_bin_prop
|
|
82
150
|
self.content = content
|
|
83
151
|
self.precision = precision
|
|
152
|
+
self.missing_policy = missing_policy
|
|
153
|
+
self.psi_missing_bucket_policy = psi_missing_bucket_policy
|
|
84
154
|
|
|
85
155
|
# def _calculate_single_psi(
|
|
86
156
|
# self,
|
|
@@ -418,7 +488,9 @@ class PSICalculator:
|
|
|
418
488
|
varlist: List[str],
|
|
419
489
|
group_by: Optional[str] = None,
|
|
420
490
|
group_name: Optional[str] = None,
|
|
421
|
-
return_details = False
|
|
491
|
+
return_details = False,
|
|
492
|
+
missing_policy: Optional[str] = None,
|
|
493
|
+
psi_missing_bucket_policy: Optional[str] = None,
|
|
422
494
|
) -> pd.DataFrame:
|
|
423
495
|
"""
|
|
424
496
|
Calculate grouped PSI comparing two datasets, using expected as benchmark.
|
|
@@ -435,12 +507,23 @@ class PSICalculator:
|
|
|
435
507
|
Column to group by in both datasets.
|
|
436
508
|
group_name : str, optional
|
|
437
509
|
Specific group column name for multi-group calculation.
|
|
510
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
511
|
+
How NaN rows are handled. If None (default), falls back to the value
|
|
512
|
+
configured on ``self.missing_policy``. Pass an explicit value to
|
|
513
|
+
override the class-level default for this call only. See
|
|
514
|
+
``PSICalculator.__init__`` for the semantics of each mode.
|
|
438
515
|
|
|
439
516
|
Returns
|
|
440
517
|
-------
|
|
441
518
|
pandas.DataFrame
|
|
442
519
|
Grouped PSI results.
|
|
443
520
|
"""
|
|
521
|
+
effective_policy = missing_policy if missing_policy is not None else self.missing_policy
|
|
522
|
+
effective_bucket_policy = (
|
|
523
|
+
psi_missing_bucket_policy
|
|
524
|
+
if psi_missing_bucket_policy is not None
|
|
525
|
+
else self.psi_missing_bucket_policy
|
|
526
|
+
)
|
|
444
527
|
return calculate_multigroup_psi_two_sets(
|
|
445
528
|
expected_df = expected_df,
|
|
446
529
|
actual_df = current_data,
|
|
@@ -452,7 +535,9 @@ class PSICalculator:
|
|
|
452
535
|
content = self.content,
|
|
453
536
|
precision = self.precision,
|
|
454
537
|
group_name = group_name,
|
|
455
|
-
return_details = return_details
|
|
538
|
+
return_details = return_details,
|
|
539
|
+
missing_policy = effective_policy,
|
|
540
|
+
psi_missing_bucket_policy = effective_bucket_policy,
|
|
456
541
|
)
|
|
457
542
|
|
|
458
543
|
|
|
@@ -469,7 +554,8 @@ def _calculate_single_psi(
|
|
|
469
554
|
min_bin_prop: float = 0.05,
|
|
470
555
|
content: float = 1e-6,
|
|
471
556
|
precision: int = 5,
|
|
472
|
-
missing_policy: str = "
|
|
557
|
+
missing_policy: str = "include",
|
|
558
|
+
psi_missing_bucket_policy: str = "smooth_laplace",
|
|
473
559
|
) -> Union[float, Tuple[float, pd.DataFrame]]:
|
|
474
560
|
"""
|
|
475
561
|
Calculate Population Stability Index (PSI) for a single variable.
|
|
@@ -536,6 +622,7 @@ def _calculate_single_psi(
|
|
|
536
622
|
f"_calculate_single_psi: missing_policy must be one of "
|
|
537
623
|
f"'include', 'drop', 'warn_and_drop'; got {missing_policy!r}."
|
|
538
624
|
)
|
|
625
|
+
_validate_psi_bucket_policy(psi_missing_bucket_policy, "_calculate_single_psi")
|
|
539
626
|
|
|
540
627
|
# Record NaN counts before splitting so we can (a) re-attach the missing
|
|
541
628
|
# bin under "include" and (b) warn under "warn_and_drop".
|
|
@@ -608,25 +695,24 @@ def _calculate_single_psi(
|
|
|
608
695
|
)
|
|
609
696
|
expected_denom = float(n_expected) if n_expected > 0 else 1.0
|
|
610
697
|
actual_denom = float(n_actual) if n_actual > 0 else 1.0
|
|
611
|
-
expected_percents = expected_count / expected_denom
|
|
612
|
-
actual_percents = actual_count / actual_denom
|
|
613
698
|
else:
|
|
614
699
|
# "drop" / "warn_and_drop": legacy behaviour, denominators are the
|
|
615
700
|
# NaN-dropped row counts, so missing-rate drift is invisible.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
701
|
+
expected_denom = float(len(expected_bins)) if len(expected_bins) > 0 else 1.0
|
|
702
|
+
actual_denom = float(len(actual_bins)) if len(actual_bins) > 0 else 1.0
|
|
703
|
+
|
|
704
|
+
all_bins = expected_count.index.union(actual_count.index)
|
|
705
|
+
expected_count = expected_count.reindex(all_bins, fill_value=0).astype(float)
|
|
706
|
+
actual_count = actual_count.reindex(all_bins, fill_value=0).astype(float)
|
|
707
|
+
|
|
708
|
+
expected_percents, actual_percents, psi_values, psi_floor_values = _psi_distributions_from_counts(
|
|
709
|
+
expected_count,
|
|
710
|
+
actual_count,
|
|
711
|
+
expected_denom,
|
|
712
|
+
actual_denom,
|
|
713
|
+
content=content,
|
|
714
|
+
policy=psi_missing_bucket_policy,
|
|
715
|
+
)
|
|
630
716
|
psi_total = psi_values.sum()
|
|
631
717
|
|
|
632
718
|
if return_details:
|
|
@@ -635,8 +721,13 @@ def _calculate_single_psi(
|
|
|
635
721
|
'actual_count': actual_count,
|
|
636
722
|
'expected_percent': expected_percents,
|
|
637
723
|
'actual_percent': actual_percents,
|
|
638
|
-
'psi_component': psi_values
|
|
724
|
+
'psi_component': psi_values,
|
|
725
|
+
'psi_component_floor_1e6': psi_floor_values,
|
|
639
726
|
})
|
|
727
|
+
details["bucket_status"] = "common"
|
|
728
|
+
details.loc[(details["expected_count"] == 0) & (details["actual_count"] > 0), "bucket_status"] = "actual_only"
|
|
729
|
+
details.loc[(details["expected_count"] > 0) & (details["actual_count"] == 0), "bucket_status"] = "expected_only"
|
|
730
|
+
details["psi_missing_bucket_policy"] = psi_missing_bucket_policy
|
|
640
731
|
return psi_total, details
|
|
641
732
|
else:
|
|
642
733
|
return psi_total
|
|
@@ -652,7 +743,9 @@ def calculate_psi(
|
|
|
652
743
|
return_details: bool = False,
|
|
653
744
|
min_bin_prop: float = 0.05,
|
|
654
745
|
content: float = 1e-6,
|
|
655
|
-
precision: int = 5
|
|
746
|
+
precision: int = 5,
|
|
747
|
+
missing_policy: str = "include",
|
|
748
|
+
psi_missing_bucket_policy: str = "smooth_laplace",
|
|
656
749
|
) -> Union[float, pd.DataFrame, Tuple[Dict, Dict]]:
|
|
657
750
|
"""
|
|
658
751
|
Calculate Population Stability Index (PSI) for a variable, optionally by groups.
|
|
@@ -682,6 +775,14 @@ def calculate_psi(
|
|
|
682
775
|
Small value to avoid division by zero. Default is 1e-6.
|
|
683
776
|
precision : int, optional
|
|
684
777
|
Decimal precision. Default is 5.
|
|
778
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
779
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
780
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
781
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
782
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
783
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
784
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
785
|
+
naming the NaN counts.
|
|
685
786
|
|
|
686
787
|
Returns
|
|
687
788
|
-------
|
|
@@ -734,7 +835,9 @@ def calculate_psi(
|
|
|
734
835
|
True,
|
|
735
836
|
min_bin_prop,
|
|
736
837
|
content,
|
|
737
|
-
precision
|
|
838
|
+
precision,
|
|
839
|
+
missing_policy,
|
|
840
|
+
psi_missing_bucket_policy,
|
|
738
841
|
)
|
|
739
842
|
results[group] = psi_value
|
|
740
843
|
details_dict[group] = detail
|
|
@@ -747,7 +850,9 @@ def calculate_psi(
|
|
|
747
850
|
False,
|
|
748
851
|
min_bin_prop,
|
|
749
852
|
content,
|
|
750
|
-
precision
|
|
853
|
+
precision,
|
|
854
|
+
missing_policy,
|
|
855
|
+
psi_missing_bucket_policy,
|
|
751
856
|
)
|
|
752
857
|
|
|
753
858
|
if return_details:
|
|
@@ -765,7 +870,9 @@ def calculate_psi(
|
|
|
765
870
|
True,
|
|
766
871
|
min_bin_prop,
|
|
767
872
|
content,
|
|
768
|
-
precision
|
|
873
|
+
precision,
|
|
874
|
+
missing_policy,
|
|
875
|
+
psi_missing_bucket_policy,
|
|
769
876
|
)
|
|
770
877
|
else:
|
|
771
878
|
return _calculate_single_psi(
|
|
@@ -776,7 +883,9 @@ def calculate_psi(
|
|
|
776
883
|
False,
|
|
777
884
|
min_bin_prop,
|
|
778
885
|
content,
|
|
779
|
-
precision
|
|
886
|
+
precision,
|
|
887
|
+
missing_policy,
|
|
888
|
+
psi_missing_bucket_policy,
|
|
780
889
|
)
|
|
781
890
|
|
|
782
891
|
|
|
@@ -791,7 +900,9 @@ def calculate_within_psi(
|
|
|
791
900
|
min_bin_prop: float = 0.05,
|
|
792
901
|
content: float = 1e-6,
|
|
793
902
|
precision: int = 5,
|
|
794
|
-
benchmark_display_name: Optional[str] = None
|
|
903
|
+
benchmark_display_name: Optional[str] = None,
|
|
904
|
+
missing_policy: str = "include",
|
|
905
|
+
psi_missing_bucket_policy: str = "smooth_laplace",
|
|
795
906
|
) -> Union[pd.DataFrame, Dict]:
|
|
796
907
|
"""
|
|
797
908
|
Calculate PSI values within a single dataset, comparing groups to a benchmark.
|
|
@@ -823,6 +934,14 @@ def calculate_within_psi(
|
|
|
823
934
|
Decimal precision. Default is 5.
|
|
824
935
|
benchmark_display_name : str, optional
|
|
825
936
|
Custom name for benchmark in results.
|
|
937
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
938
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
939
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
940
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
941
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
942
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
943
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
944
|
+
naming the NaN counts.
|
|
826
945
|
|
|
827
946
|
Returns
|
|
828
947
|
-------
|
|
@@ -858,7 +977,9 @@ def calculate_within_psi(
|
|
|
858
977
|
return_details=True,
|
|
859
978
|
min_bin_prop=min_bin_prop,
|
|
860
979
|
content=content,
|
|
861
|
-
precision=precision
|
|
980
|
+
precision=precision,
|
|
981
|
+
missing_policy=missing_policy,
|
|
982
|
+
psi_missing_bucket_policy=psi_missing_bucket_policy,
|
|
862
983
|
)
|
|
863
984
|
res_dict[obs_value] = psi
|
|
864
985
|
detail_dict[obs_value] = details
|
|
@@ -872,7 +993,9 @@ def calculate_within_psi(
|
|
|
872
993
|
return_details=False,
|
|
873
994
|
min_bin_prop=min_bin_prop,
|
|
874
995
|
content=content,
|
|
875
|
-
precision=precision
|
|
996
|
+
precision=precision,
|
|
997
|
+
missing_policy=missing_policy,
|
|
998
|
+
psi_missing_bucket_policy=psi_missing_bucket_policy,
|
|
876
999
|
)
|
|
877
1000
|
res_dict[obs_value] = psi
|
|
878
1001
|
|
|
@@ -895,7 +1018,9 @@ def calculate_psi_within_dataset(
|
|
|
895
1018
|
buckets: int = 10,
|
|
896
1019
|
min_bin_prop: float = 0.05,
|
|
897
1020
|
content: float = 1e-6,
|
|
898
|
-
precision: int = 5
|
|
1021
|
+
precision: int = 5,
|
|
1022
|
+
missing_policy: str = "include",
|
|
1023
|
+
psi_missing_bucket_policy: str = "smooth_laplace",
|
|
899
1024
|
) -> pd.DataFrame:
|
|
900
1025
|
"""
|
|
901
1026
|
Calculate PSI for multiple variables within a dataset, comparing groups to a benchmark.
|
|
@@ -923,6 +1048,14 @@ def calculate_psi_within_dataset(
|
|
|
923
1048
|
Small value to avoid division by zero. Default is 1e-6.
|
|
924
1049
|
precision : int, optional
|
|
925
1050
|
Decimal precision. Default is 5.
|
|
1051
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
1052
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
1053
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
1054
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
1055
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
1056
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
1057
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
1058
|
+
naming the NaN counts.
|
|
926
1059
|
|
|
927
1060
|
Returns
|
|
928
1061
|
-------
|
|
@@ -946,7 +1079,9 @@ def calculate_psi_within_dataset(
|
|
|
946
1079
|
return_details=False,
|
|
947
1080
|
min_bin_prop=min_bin_prop,
|
|
948
1081
|
content=content,
|
|
949
|
-
precision=precision
|
|
1082
|
+
precision=precision,
|
|
1083
|
+
missing_policy=missing_policy,
|
|
1084
|
+
psi_missing_bucket_policy=psi_missing_bucket_policy,
|
|
950
1085
|
).sort_values([grp_name]).reset_index(drop=True)
|
|
951
1086
|
|
|
952
1087
|
single_psi['var'] = var
|
|
@@ -964,7 +1099,9 @@ def calculate_multivar_psi_two_sets(
|
|
|
964
1099
|
equal_freq: bool = True,
|
|
965
1100
|
min_bin_prop: float = 0.05,
|
|
966
1101
|
content: float = 1e-6,
|
|
967
|
-
precision: int = 5
|
|
1102
|
+
precision: int = 5,
|
|
1103
|
+
missing_policy: str = "include",
|
|
1104
|
+
psi_missing_bucket_policy: str = "smooth_laplace",
|
|
968
1105
|
) -> pd.DataFrame:
|
|
969
1106
|
"""
|
|
970
1107
|
Calculate PSI for multiple variables by comparing two different datasets.
|
|
@@ -992,6 +1129,14 @@ def calculate_multivar_psi_two_sets(
|
|
|
992
1129
|
Small value to avoid division by zero. Default is 1e-6.
|
|
993
1130
|
precision : int, optional
|
|
994
1131
|
Decimal precision. Default is 5.
|
|
1132
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
1133
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
1134
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
1135
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
1136
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
1137
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
1138
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
1139
|
+
naming the NaN counts.
|
|
995
1140
|
|
|
996
1141
|
Returns
|
|
997
1142
|
-------
|
|
@@ -1011,7 +1156,13 @@ def calculate_multivar_psi_two_sets(
|
|
|
1011
1156
|
target_col=var,
|
|
1012
1157
|
group_by=group_by,
|
|
1013
1158
|
buckets=buckets,
|
|
1014
|
-
|
|
1159
|
+
equal_freq=equal_freq,
|
|
1160
|
+
min_bin_prop=min_bin_prop,
|
|
1161
|
+
content=content,
|
|
1162
|
+
precision=precision,
|
|
1163
|
+
return_details=False,
|
|
1164
|
+
missing_policy=missing_policy,
|
|
1165
|
+
psi_missing_bucket_policy=psi_missing_bucket_policy,
|
|
1015
1166
|
)
|
|
1016
1167
|
if group_by is None:
|
|
1017
1168
|
single_psi = pd.DataFrame([single_psi], columns=['psi'])
|
|
@@ -1151,7 +1302,9 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1151
1302
|
content: float = 1e-6,
|
|
1152
1303
|
precision: int = 5,
|
|
1153
1304
|
group_name: Optional[str] = None,
|
|
1154
|
-
return_details: bool = False
|
|
1305
|
+
return_details: bool = False,
|
|
1306
|
+
missing_policy: str = "include",
|
|
1307
|
+
psi_missing_bucket_policy: str = "smooth_laplace",
|
|
1155
1308
|
) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]:
|
|
1156
1309
|
"""
|
|
1157
1310
|
Calculate grouped PSI using expected DataFrame as benchmark, applied to actual DataFrame groups.
|
|
@@ -1181,6 +1334,12 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1181
1334
|
return_details : bool, optional
|
|
1182
1335
|
是否返回详细分箱信息。若为 True,返回字典 {'psi': psi_df, 'details': details_df},
|
|
1183
1336
|
details_df 包含列:['bin', 'expected_percent', 'actual_percent', 'psi_component', group_name, 'var']
|
|
1337
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
1338
|
+
NaN 行处理策略。0.5.0 起默认为 "include",将 NaN 行汇入独立的
|
|
1339
|
+
"__MISSING__" 桶,使缺失率漂移进入 PSI 计算;这是推荐的生产行为,
|
|
1340
|
+
并在 0.5.0 成为默认值(0.4.2 为 "drop" 以保持数值向后兼容)。
|
|
1341
|
+
传入 "drop" 可复现 0.5.0 之前的数值,或 "warn_and_drop" 保留旧数值
|
|
1342
|
+
同时发出 RuntimeWarning 报告两侧 NaN 数量。
|
|
1184
1343
|
"""
|
|
1185
1344
|
if group_name is not None:
|
|
1186
1345
|
if actual_df[group_name].isna().sum() > 0:
|
|
@@ -1203,7 +1362,9 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1203
1362
|
return_details=True,
|
|
1204
1363
|
min_bin_prop=min_bin_prop,
|
|
1205
1364
|
content=content,
|
|
1206
|
-
precision=precision
|
|
1365
|
+
precision=precision,
|
|
1366
|
+
missing_policy=missing_policy,
|
|
1367
|
+
psi_missing_bucket_policy=psi_missing_bucket_policy,
|
|
1207
1368
|
)
|
|
1208
1369
|
|
|
1209
1370
|
psi_records.append({group_name: group, 'var': var, 'psi': psi_val})
|
|
@@ -1221,7 +1382,7 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1221
1382
|
detail_df[group_name] = group
|
|
1222
1383
|
detail_df['var'] = var
|
|
1223
1384
|
|
|
1224
|
-
required_cols = ['bin', 'expected_count', 'actual_count', 'expected_percent', 'actual_percent', 'psi_component', group_name, 'var']
|
|
1385
|
+
required_cols = ['bin', 'expected_count', 'actual_count', 'expected_percent', 'actual_percent', 'psi_component', 'bucket_status', 'psi_missing_bucket_policy', group_name, 'var']
|
|
1225
1386
|
for col in required_cols:
|
|
1226
1387
|
if col not in detail_df.columns:
|
|
1227
1388
|
detail_df[col] = np.nan
|
|
@@ -1250,7 +1411,9 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1250
1411
|
return_details=True,
|
|
1251
1412
|
min_bin_prop=min_bin_prop,
|
|
1252
1413
|
content=content,
|
|
1253
|
-
precision=precision
|
|
1414
|
+
precision=precision,
|
|
1415
|
+
missing_policy=missing_policy,
|
|
1416
|
+
psi_missing_bucket_policy=psi_missing_bucket_policy,
|
|
1254
1417
|
)
|
|
1255
1418
|
psi_records.append({'var': var, 'psi': psi_val})
|
|
1256
1419
|
|
|
@@ -1262,7 +1425,7 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1262
1425
|
index_col = detail_df.columns[0]
|
|
1263
1426
|
detail_df = detail_df.rename(columns={index_col: 'bin'})
|
|
1264
1427
|
detail_df['var'] = var
|
|
1265
|
-
required_cols = ['bin', 'expected_count', 'actual_count', 'expected_percent', 'actual_percent', 'psi_component', 'var']
|
|
1428
|
+
required_cols = ['bin', 'expected_count', 'actual_count', 'expected_percent', 'actual_percent', 'psi_component', 'bucket_status', 'psi_missing_bucket_policy', 'var']
|
|
1266
1429
|
for col in required_cols:
|
|
1267
1430
|
if col not in detail_df.columns:
|
|
1268
1431
|
detail_df[col] = np.nan
|
|
@@ -1285,7 +1448,9 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1285
1448
|
equal_freq=equal_freq,
|
|
1286
1449
|
min_bin_prop=min_bin_prop,
|
|
1287
1450
|
content=content,
|
|
1288
|
-
precision=precision
|
|
1451
|
+
precision=precision,
|
|
1452
|
+
missing_policy=missing_policy,
|
|
1453
|
+
psi_missing_bucket_policy=psi_missing_bucket_policy,
|
|
1289
1454
|
)
|
|
1290
1455
|
|
|
1291
1456
|
return group_psi
|
{supermodelingfactory-0.4.2 → supermodelingfactory-0.5.1}/Modeling_Tool/Feature/PSI_Tool.pyi
RENAMED
|
@@ -20,10 +20,10 @@ from tqdm import tqdm
|
|
|
20
20
|
from Modeling_Tool.Core.Binning_Tool import quick_binning
|
|
21
21
|
|
|
22
22
|
class PSICalculator:
|
|
23
|
-
def __init__(self, buckets: int = 10, equal_freq: bool = True, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, binning_engine: Any = None): ...
|
|
24
|
-
def calculate(self, expected_df: pd.DataFrame, current_data: pd.DataFrame, varlist: List[str], group_by: Optional[str] = None, group_name: Optional[str] = None, return_details = False) -> pd.DataFrame: ...
|
|
25
|
-
def calculate_psi(expected: Union[pd.DataFrame, pd.Series], actual: Union[pd.DataFrame, pd.Series], target_col: str, buckets: int = 10, equal_freq: bool = True, group_by: Optional[Union[str, List[str]]] = None, return_details: bool = False, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5) -> Union[float, pd.DataFrame, Tuple[Dict, Dict]]: ...
|
|
26
|
-
def calculate_within_psi(data: pd.DataFrame, grp_name: str, target_col: str, benchmark: Optional[Any] = None, equal_freq: bool = True, buckets: int = 10, return_details: bool = False, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, benchmark_display_name: Optional[str] = None) -> Union[pd.DataFrame, Dict]: ...
|
|
27
|
-
def calculate_psi_within_dataset(data: pd.DataFrame, grp_name: str, varlist: List[str], benchmark: Optional[Any] = None, equal_freq: bool = True, buckets: int = 10, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5) -> pd.DataFrame: ...
|
|
28
|
-
def calculate_multivar_psi_two_sets(expected_df: pd.DataFrame, actual_df: pd.DataFrame, varlist: List[str], group_by: Optional[Union[str, List[str]]] = None, buckets: int = 10, equal_freq: bool = True, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5) -> pd.DataFrame: ...
|
|
29
|
-
def calculate_multigroup_psi_two_sets(expected_df: pd.DataFrame, actual_df: pd.DataFrame, varlist: List[str], group_by: Optional[Union[str, List[str]]] = None, buckets: int = 10, equal_freq: bool = True, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, group_name: Optional[str] = None, return_details: bool = False) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]: ...
|
|
23
|
+
def __init__(self, buckets: int = 10, equal_freq: bool = True, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, missing_policy: str = "include", psi_missing_bucket_policy: str = "smooth_laplace", binning_engine: Any = None): ...
|
|
24
|
+
def calculate(self, expected_df: pd.DataFrame, current_data: pd.DataFrame, varlist: List[str], group_by: Optional[str] = None, group_name: Optional[str] = None, return_details = False, missing_policy: Optional[str] = None, psi_missing_bucket_policy: Optional[str] = None) -> pd.DataFrame: ...
|
|
25
|
+
def calculate_psi(expected: Union[pd.DataFrame, pd.Series], actual: Union[pd.DataFrame, pd.Series], target_col: str, buckets: int = 10, equal_freq: bool = True, group_by: Optional[Union[str, List[str]]] = None, return_details: bool = False, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, missing_policy: str = "include", psi_missing_bucket_policy: str = "smooth_laplace") -> Union[float, pd.DataFrame, Tuple[Dict, Dict]]: ...
|
|
26
|
+
def calculate_within_psi(data: pd.DataFrame, grp_name: str, target_col: str, benchmark: Optional[Any] = None, equal_freq: bool = True, buckets: int = 10, return_details: bool = False, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, benchmark_display_name: Optional[str] = None, missing_policy: str = "include", psi_missing_bucket_policy: str = "smooth_laplace") -> Union[pd.DataFrame, Dict]: ...
|
|
27
|
+
def calculate_psi_within_dataset(data: pd.DataFrame, grp_name: str, varlist: List[str], benchmark: Optional[Any] = None, equal_freq: bool = True, buckets: int = 10, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, missing_policy: str = "include", psi_missing_bucket_policy: str = "smooth_laplace") -> pd.DataFrame: ...
|
|
28
|
+
def calculate_multivar_psi_two_sets(expected_df: pd.DataFrame, actual_df: pd.DataFrame, varlist: List[str], group_by: Optional[Union[str, List[str]]] = None, buckets: int = 10, equal_freq: bool = True, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, missing_policy: str = "include", psi_missing_bucket_policy: str = "smooth_laplace") -> pd.DataFrame: ...
|
|
29
|
+
def calculate_multigroup_psi_two_sets(expected_df: pd.DataFrame, actual_df: pd.DataFrame, varlist: List[str], group_by: Optional[Union[str, List[str]]] = None, buckets: int = 10, equal_freq: bool = True, min_bin_prop: float = 0.05, content: float = 1e-06, precision: int = 5, group_name: Optional[str] = None, return_details: bool = False, missing_policy: str = "include", psi_missing_bucket_policy: str = "smooth_laplace") -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]: ...
|