SuperModelingFactory 0.3.15__tar.gz → 0.5.0__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.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/ODPS_Tool.py +102 -9
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/PSI_Tool.py +203 -53
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Weighted_Screen.py +16 -2
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/_common.py +109 -3
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/credit_model.py +36 -8
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/feature_validation.py +64 -21
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/reject_inference.py +58 -4
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/sample_analysis.py +50 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/score_comparison.py +20 -3
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/score_consistency_uat.py +54 -6
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Reject_Infer.py +63 -7
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Sample_Split.py +33 -22
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +76 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/__init__.py +1 -1
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/PKG-INFO +2 -2
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/README.md +1 -1
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/pyproject.toml +1 -1
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/setup.py +1 -1
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/ExcelMaster/ExcelFormatTool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/ExcelMaster/ExcelMaster.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/ExcelMaster/Template.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/ExcelMaster/Utility.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/ExcelMaster/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/LICENSE +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/MANIFEST.in +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Binning_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Proc_Compare.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Slope_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/kDataFrame.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Core/utils.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/evaluate_model.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Explainability/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Feature_Screen.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/Backward_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/GBM_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/LRM_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Model/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Sample/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/UAT/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Master.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/ref_font/simsun.ttc +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Report/Report_Tool.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Report/__init__.py +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/requires.txt +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/SuperModelingFactory.egg-info/top_level.txt +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/requirements.txt +0 -0
- {supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/setup.cfg +0 -0
|
@@ -247,7 +247,7 @@ class ODPSRunner(object):
|
|
|
247
247
|
|
|
248
248
|
return table_schema
|
|
249
249
|
|
|
250
|
-
def upload_df(self, df, table_name, table_schema=None, partition=None):
|
|
250
|
+
def upload_df(self, df, table_name, table_schema=None, partition=None, atomic=True):
|
|
251
251
|
"""上传数据集至mc中创建新表
|
|
252
252
|
|
|
253
253
|
Parameters
|
|
@@ -260,20 +260,113 @@ class ODPSRunner(object):
|
|
|
260
260
|
数据集
|
|
261
261
|
partition: string
|
|
262
262
|
保存分区
|
|
263
|
+
atomic: bool, default True
|
|
264
|
+
When ``True`` (recommended, default from 0.4.2), the target table is
|
|
265
|
+
replaced through a temp-table + rename swap so that a failure between
|
|
266
|
+
``delete_table`` and the completed write never leaves the caller
|
|
267
|
+
with a dropped-and-empty table. When ``False``, the pre-0.4.2
|
|
268
|
+
behaviour is used: the existing table is dropped first and then
|
|
269
|
+
recreated, so any exception during write leaves the target table
|
|
270
|
+
missing. Only set ``atomic=False`` if you deliberately want the
|
|
271
|
+
legacy behaviour, e.g. for tables whose downstream readers expect
|
|
272
|
+
a specific object identity.
|
|
263
273
|
"""
|
|
264
274
|
if table_schema is None:
|
|
265
275
|
df.loc[:, "py_inserttime"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
266
276
|
table_schema = self.cre_table_schema(df=df, partition_name=None)
|
|
267
277
|
|
|
268
|
-
|
|
269
|
-
|
|
278
|
+
if not atomic:
|
|
279
|
+
# Legacy pre-0.4.2 non-atomic path — retained under an explicit
|
|
280
|
+
# opt-out for callers that need the old identity semantics.
|
|
281
|
+
self.o.delete_table(table_name, if_exists=True)
|
|
282
|
+
t = self.o.create_table(table_name, table_schema)
|
|
283
|
+
if bool(partition):
|
|
284
|
+
with t.open_writer(partition=partition, create_partition=True) as writer:
|
|
285
|
+
writer.write(df.values.tolist())
|
|
286
|
+
else:
|
|
287
|
+
with t.open_writer() as writer:
|
|
288
|
+
writer.write(df.values.tolist())
|
|
289
|
+
logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# Atomic swap path (default from 0.4.2). If any step before the final
|
|
293
|
+
# swap fails, the original table is untouched.
|
|
294
|
+
ts = datetime.now().strftime('%Y%m%d%H%M%S%f')
|
|
295
|
+
tmp_name = f"{table_name}__tmp_{ts}"
|
|
296
|
+
old_name = f"{table_name}__old_{ts}"
|
|
297
|
+
|
|
298
|
+
# Step 1: write data into the temp table. If this fails, drop the
|
|
299
|
+
# partial temp table and re-raise; the target table is untouched.
|
|
300
|
+
self.o.delete_table(tmp_name, if_exists=True)
|
|
301
|
+
tmp_table = self.o.create_table(tmp_name, table_schema)
|
|
302
|
+
try:
|
|
303
|
+
if bool(partition):
|
|
304
|
+
with tmp_table.open_writer(partition=partition, create_partition=True) as writer:
|
|
305
|
+
writer.write(df.values.tolist())
|
|
306
|
+
else:
|
|
307
|
+
with tmp_table.open_writer() as writer:
|
|
308
|
+
writer.write(df.values.tolist())
|
|
309
|
+
except Exception:
|
|
310
|
+
try:
|
|
311
|
+
self.o.delete_table(tmp_name, if_exists=True)
|
|
312
|
+
except Exception as cleanup_exc: # noqa: BLE001
|
|
313
|
+
logger.warning(
|
|
314
|
+
f"upload_df: failed to drop temp table {tmp_name} after write error: {cleanup_exc!r}"
|
|
315
|
+
)
|
|
316
|
+
raise
|
|
317
|
+
|
|
318
|
+
# Step 2: swap. If the original table exists, rename it aside first so
|
|
319
|
+
# that the rename of tmp -> target is guaranteed to succeed. Then drop
|
|
320
|
+
# the old copy (best-effort, warn on failure).
|
|
321
|
+
target_exists = self.o.exist_table(table_name)
|
|
322
|
+
if target_exists:
|
|
323
|
+
try:
|
|
324
|
+
self.o.run_sql(f"ALTER TABLE {table_name} RENAME TO {old_name};")
|
|
325
|
+
except Exception:
|
|
326
|
+
# Rename of the live target failed — do not touch it. Drop the
|
|
327
|
+
# tmp table so we don't leak it, then re-raise.
|
|
328
|
+
try:
|
|
329
|
+
self.o.delete_table(tmp_name, if_exists=True)
|
|
330
|
+
except Exception as cleanup_exc: # noqa: BLE001
|
|
331
|
+
logger.warning(
|
|
332
|
+
f"upload_df: failed to drop temp table {tmp_name} after rename-target failure: {cleanup_exc!r}"
|
|
333
|
+
)
|
|
334
|
+
raise
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
self.o.run_sql(f"ALTER TABLE {tmp_name} RENAME TO {table_name};")
|
|
338
|
+
except Exception:
|
|
339
|
+
# tmp -> target rename failed. If we already moved the original,
|
|
340
|
+
# try to restore it so the caller is not left with a missing table.
|
|
341
|
+
if target_exists:
|
|
342
|
+
try:
|
|
343
|
+
self.o.run_sql(f"ALTER TABLE {old_name} RENAME TO {table_name};")
|
|
344
|
+
logger.warning(
|
|
345
|
+
f"upload_df: swap of {tmp_name} -> {table_name} failed; restored original from {old_name}"
|
|
346
|
+
)
|
|
347
|
+
except Exception as restore_exc: # noqa: BLE001
|
|
348
|
+
logger.error(
|
|
349
|
+
f"upload_df: swap of {tmp_name} -> {table_name} failed AND restore of "
|
|
350
|
+
f"{old_name} -> {table_name} failed: {restore_exc!r}. "
|
|
351
|
+
f"Manual recovery required — original data is in {old_name}."
|
|
352
|
+
)
|
|
353
|
+
try:
|
|
354
|
+
self.o.delete_table(tmp_name, if_exists=True)
|
|
355
|
+
except Exception: # noqa: BLE001
|
|
356
|
+
pass
|
|
357
|
+
raise
|
|
358
|
+
|
|
359
|
+
# Step 3: drop the moved-aside original — best-effort, do not fail the
|
|
360
|
+
# call if this cleanup errors, since the swap already succeeded.
|
|
361
|
+
if target_exists:
|
|
362
|
+
try:
|
|
363
|
+
self.o.delete_table(old_name, if_exists=True)
|
|
364
|
+
except Exception as cleanup_exc: # noqa: BLE001
|
|
365
|
+
logger.warning(
|
|
366
|
+
f"upload_df: swap succeeded but failed to drop old copy {old_name}: {cleanup_exc!r}. "
|
|
367
|
+
f"Safe to delete manually."
|
|
368
|
+
)
|
|
270
369
|
|
|
271
|
-
if bool(partition):
|
|
272
|
-
with t.open_writer(partition=partition, create_partition=True) as writer:
|
|
273
|
-
writer.write(df.values.tolist())
|
|
274
|
-
else:
|
|
275
|
-
with t.open_writer() as writer:
|
|
276
|
-
writer.write(df.values.tolist())
|
|
277
370
|
logger.info(f'<<<< 完成数据入表{table_name}: shape={df.shape} >>>>')
|
|
278
371
|
|
|
279
372
|
def insert_df(self, df, table_name, overwrite=True, partition=None):
|
{supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/PSI_Tool.py
RENAMED
|
@@ -7,12 +7,20 @@ to measure the distribution drift between expected and actual datasets.
|
|
|
7
7
|
Author: Matrix Agent
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
10
12
|
import numpy as np
|
|
11
13
|
import pandas as pd
|
|
12
14
|
from typing import Union, List, Dict, Optional, Tuple, Callable, Any
|
|
13
15
|
from tqdm import tqdm
|
|
14
16
|
from Modeling_Tool.Core.Binning_Tool import quick_binning
|
|
15
17
|
|
|
18
|
+
# Missing bucket label used across PSI helpers when ``missing_policy="include"``
|
|
19
|
+
# (the 0.4.2 default). Kept identical to the sentinel already used by
|
|
20
|
+
# ``Feature.Weighted_Screen`` and ``WOE_Adapter`` so downstream aggregators can
|
|
21
|
+
# recognise it without special-casing.
|
|
22
|
+
_MISSING_BIN = "__MISSING__"
|
|
23
|
+
|
|
16
24
|
# ============================================================================
|
|
17
25
|
# Classes
|
|
18
26
|
# ============================================================================
|
|
@@ -50,7 +58,8 @@ class PSICalculator:
|
|
|
50
58
|
equal_freq: bool = True,
|
|
51
59
|
min_bin_prop: float = 0.05,
|
|
52
60
|
content: float = 1e-6,
|
|
53
|
-
precision: int = 5
|
|
61
|
+
precision: int = 5,
|
|
62
|
+
missing_policy: str = "include"
|
|
54
63
|
):
|
|
55
64
|
"""
|
|
56
65
|
Initialize PSICalculator with configuration parameters.
|
|
@@ -67,12 +76,26 @@ class PSICalculator:
|
|
|
67
76
|
Small value to prevent division by zero. Default is 1e-6.
|
|
68
77
|
precision : int, optional
|
|
69
78
|
Decimal precision for rounding. Default is 5.
|
|
79
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
80
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
81
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
82
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
83
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
84
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
85
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
86
|
+
naming the NaN counts.
|
|
70
87
|
"""
|
|
88
|
+
if missing_policy not in {"include", "drop", "warn_and_drop"}:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"PSICalculator.__init__: missing_policy must be one of "
|
|
91
|
+
f"'include', 'drop', 'warn_and_drop'; got {missing_policy!r}."
|
|
92
|
+
)
|
|
71
93
|
self.buckets = buckets
|
|
72
94
|
self.equal_freq = equal_freq
|
|
73
95
|
self.min_bin_prop = min_bin_prop
|
|
74
96
|
self.content = content
|
|
75
97
|
self.precision = precision
|
|
98
|
+
self.missing_policy = missing_policy
|
|
76
99
|
|
|
77
100
|
# def _calculate_single_psi(
|
|
78
101
|
# self,
|
|
@@ -410,7 +433,8 @@ class PSICalculator:
|
|
|
410
433
|
varlist: List[str],
|
|
411
434
|
group_by: Optional[str] = None,
|
|
412
435
|
group_name: Optional[str] = None,
|
|
413
|
-
return_details = False
|
|
436
|
+
return_details = False,
|
|
437
|
+
missing_policy: Optional[str] = None
|
|
414
438
|
) -> pd.DataFrame:
|
|
415
439
|
"""
|
|
416
440
|
Calculate grouped PSI comparing two datasets, using expected as benchmark.
|
|
@@ -427,12 +451,18 @@ class PSICalculator:
|
|
|
427
451
|
Column to group by in both datasets.
|
|
428
452
|
group_name : str, optional
|
|
429
453
|
Specific group column name for multi-group calculation.
|
|
454
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
455
|
+
How NaN rows are handled. If None (default), falls back to the value
|
|
456
|
+
configured on ``self.missing_policy``. Pass an explicit value to
|
|
457
|
+
override the class-level default for this call only. See
|
|
458
|
+
``PSICalculator.__init__`` for the semantics of each mode.
|
|
430
459
|
|
|
431
460
|
Returns
|
|
432
461
|
-------
|
|
433
462
|
pandas.DataFrame
|
|
434
463
|
Grouped PSI results.
|
|
435
464
|
"""
|
|
465
|
+
effective_policy = missing_policy if missing_policy is not None else self.missing_policy
|
|
436
466
|
return calculate_multigroup_psi_two_sets(
|
|
437
467
|
expected_df = expected_df,
|
|
438
468
|
actual_df = current_data,
|
|
@@ -444,7 +474,8 @@ class PSICalculator:
|
|
|
444
474
|
content = self.content,
|
|
445
475
|
precision = self.precision,
|
|
446
476
|
group_name = group_name,
|
|
447
|
-
return_details = return_details
|
|
477
|
+
return_details = return_details,
|
|
478
|
+
missing_policy = effective_policy
|
|
448
479
|
)
|
|
449
480
|
|
|
450
481
|
|
|
@@ -460,14 +491,29 @@ def _calculate_single_psi(
|
|
|
460
491
|
return_details: bool = False,
|
|
461
492
|
min_bin_prop: float = 0.05,
|
|
462
493
|
content: float = 1e-6,
|
|
463
|
-
precision: int = 5
|
|
494
|
+
precision: int = 5,
|
|
495
|
+
missing_policy: str = "include",
|
|
464
496
|
) -> Union[float, Tuple[float, pd.DataFrame]]:
|
|
465
497
|
"""
|
|
466
498
|
Calculate Population Stability Index (PSI) for a single variable.
|
|
467
|
-
|
|
499
|
+
|
|
468
500
|
This function bins both expected and actual series using the same breakpoints,
|
|
469
501
|
then calculates PSI based on the distribution difference between them.
|
|
470
|
-
|
|
502
|
+
|
|
503
|
+
Missing handling (0.4.2, N29)
|
|
504
|
+
------------------------------
|
|
505
|
+
Before 0.4.2, both series were unconditionally passed through ``.dropna()``
|
|
506
|
+
before binning. Any drift in the *missing rate itself* — the single most
|
|
507
|
+
common early signal of a data-pipeline break — was silently invisible in
|
|
508
|
+
the returned PSI. 0.4.2 adds the ``missing_policy`` parameter so callers
|
|
509
|
+
can opt in to the corrected behaviour. The default remains ``"drop"`` for
|
|
510
|
+
strict backward compatibility with the pre-0.4.2 numeric output; the next
|
|
511
|
+
minor release will flip the default to ``"include"``. Pass
|
|
512
|
+
``missing_policy="include"`` to route NaN rows through a dedicated
|
|
513
|
+
``"__MISSING__"`` bin on both sides so missing-rate drift contributes to
|
|
514
|
+
the PSI. Pass ``missing_policy="warn_and_drop"`` to keep the old numbers
|
|
515
|
+
but at least surface a ``RuntimeWarning`` naming the two NaN counts.
|
|
516
|
+
|
|
471
517
|
Parameters
|
|
472
518
|
----------
|
|
473
519
|
expected_series : pandas.Series
|
|
@@ -486,62 +532,112 @@ def _calculate_single_psi(
|
|
|
486
532
|
Small value to avoid division by zero. Default is 1e-6.
|
|
487
533
|
precision : int, optional
|
|
488
534
|
Decimal precision. Default is 5.
|
|
489
|
-
|
|
535
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
536
|
+
How NaN rows are handled. ``"drop"`` (default in 0.4.2) reproduces the
|
|
537
|
+
pre-0.4.2 behaviour and silently excludes NaN rows from both sides
|
|
538
|
+
before binning. ``"include"`` treats NaN as its own ``"__MISSING__"``
|
|
539
|
+
bin so missing-rate drift shows up in the PSI — recommended for any
|
|
540
|
+
production drift monitor, and will become the default in the next
|
|
541
|
+
minor release. ``"warn_and_drop"`` behaves like ``"drop"`` but emits
|
|
542
|
+
a ``RuntimeWarning`` naming the two NaN counts so the caller sees what
|
|
543
|
+
was dropped.
|
|
544
|
+
|
|
490
545
|
Returns
|
|
491
546
|
-------
|
|
492
547
|
float or tuple
|
|
493
548
|
If return_details is False: Returns total PSI value.
|
|
494
549
|
If return_details is True: Returns tuple of (PSI value, details DataFrame).
|
|
495
|
-
|
|
550
|
+
|
|
496
551
|
Notes
|
|
497
552
|
-----
|
|
498
553
|
PSI Formula: Σ (Actual% - Expected%) * ln(Actual% / Expected%)
|
|
499
554
|
A PSI < 0.1 indicates stable population, 0.1-0.25 suggests some change,
|
|
500
555
|
and > 0.25 indicates significant drift.
|
|
501
556
|
"""
|
|
502
|
-
|
|
557
|
+
if missing_policy not in {"include", "drop", "warn_and_drop"}:
|
|
558
|
+
raise ValueError(
|
|
559
|
+
f"_calculate_single_psi: missing_policy must be one of "
|
|
560
|
+
f"'include', 'drop', 'warn_and_drop'; got {missing_policy!r}."
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Record NaN counts before splitting so we can (a) re-attach the missing
|
|
564
|
+
# bin under "include" and (b) warn under "warn_and_drop".
|
|
565
|
+
n_expected = int(len(expected_series))
|
|
566
|
+
n_actual = int(len(actual_series))
|
|
567
|
+
n_expected_na = int(expected_series.isna().sum())
|
|
568
|
+
n_actual_na = int(actual_series.isna().sum())
|
|
569
|
+
|
|
570
|
+
if missing_policy == "warn_and_drop" and (n_expected_na or n_actual_na):
|
|
571
|
+
warnings.warn(
|
|
572
|
+
f"_calculate_single_psi: dropping NaN rows before binning. "
|
|
573
|
+
f"expected: {n_expected_na}/{n_expected} NaN, "
|
|
574
|
+
f"actual: {n_actual_na}/{n_actual} NaN. "
|
|
575
|
+
f"Pass missing_policy='include' to treat NaN as its own bin so "
|
|
576
|
+
f"missing-rate drift is captured.",
|
|
577
|
+
RuntimeWarning,
|
|
578
|
+
stacklevel=2,
|
|
579
|
+
)
|
|
580
|
+
|
|
503
581
|
expected_clean = expected_series.dropna()
|
|
504
582
|
actual_clean = actual_series.dropna()
|
|
505
|
-
|
|
583
|
+
|
|
506
584
|
# Bin the expected data to get breakpoints
|
|
507
585
|
expected_bins, breakpoints = quick_binning(
|
|
508
|
-
pd.DataFrame(expected_clean),
|
|
509
|
-
expected_clean.name,
|
|
510
|
-
labels=None,
|
|
511
|
-
nbins=buckets,
|
|
512
|
-
precision=precision,
|
|
513
|
-
equal_freq=equal_freq,
|
|
514
|
-
right=True,
|
|
586
|
+
pd.DataFrame(expected_clean),
|
|
587
|
+
expected_clean.name,
|
|
588
|
+
labels=None,
|
|
589
|
+
nbins=buckets,
|
|
590
|
+
precision=precision,
|
|
591
|
+
equal_freq=equal_freq,
|
|
592
|
+
right=True,
|
|
515
593
|
include_lowest=False,
|
|
516
|
-
min_bin_prop=min_bin_prop,
|
|
517
|
-
tree_binning=False,
|
|
518
|
-
target=None,
|
|
594
|
+
min_bin_prop=min_bin_prop,
|
|
595
|
+
tree_binning=False,
|
|
596
|
+
target=None,
|
|
519
597
|
random_state=42
|
|
520
598
|
)
|
|
521
|
-
|
|
599
|
+
|
|
522
600
|
# Bin the actual data using the same breakpoints
|
|
523
601
|
actual_bins, _ = quick_binning(
|
|
524
|
-
pd.DataFrame(actual_clean),
|
|
525
|
-
actual_clean.name,
|
|
526
|
-
labels=None,
|
|
527
|
-
nbins=list(breakpoints),
|
|
528
|
-
precision=precision,
|
|
529
|
-
equal_freq=equal_freq,
|
|
530
|
-
right=True,
|
|
602
|
+
pd.DataFrame(actual_clean),
|
|
603
|
+
actual_clean.name,
|
|
604
|
+
labels=None,
|
|
605
|
+
nbins=list(breakpoints),
|
|
606
|
+
precision=precision,
|
|
607
|
+
equal_freq=equal_freq,
|
|
608
|
+
right=True,
|
|
531
609
|
include_lowest=False,
|
|
532
|
-
min_bin_prop=min_bin_prop,
|
|
533
|
-
tree_binning=False,
|
|
534
|
-
target=None,
|
|
610
|
+
min_bin_prop=min_bin_prop,
|
|
611
|
+
tree_binning=False,
|
|
612
|
+
target=None,
|
|
535
613
|
random_state=42
|
|
536
614
|
)
|
|
537
|
-
|
|
538
|
-
# Get bin counts
|
|
615
|
+
|
|
616
|
+
# Get bin counts (finite-value rows only, from quick_binning)
|
|
539
617
|
expected_count = expected_bins.value_counts(normalize=False, sort=False)
|
|
540
618
|
actual_count = actual_bins.value_counts(normalize=False, sort=False)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
619
|
+
|
|
620
|
+
if missing_policy == "include":
|
|
621
|
+
# Re-attach the __MISSING__ bin so that missing-rate drift contributes
|
|
622
|
+
# to the PSI on both sides. The denominators use the *original* row
|
|
623
|
+
# counts (before dropna) so the fractions are directly comparable.
|
|
624
|
+
if n_expected_na > 0:
|
|
625
|
+
expected_count = pd.concat(
|
|
626
|
+
[expected_count, pd.Series({_MISSING_BIN: n_expected_na})]
|
|
627
|
+
)
|
|
628
|
+
if n_actual_na > 0:
|
|
629
|
+
actual_count = pd.concat(
|
|
630
|
+
[actual_count, pd.Series({_MISSING_BIN: n_actual_na})]
|
|
631
|
+
)
|
|
632
|
+
expected_denom = float(n_expected) if n_expected > 0 else 1.0
|
|
633
|
+
actual_denom = float(n_actual) if n_actual > 0 else 1.0
|
|
634
|
+
expected_percents = expected_count / expected_denom
|
|
635
|
+
actual_percents = actual_count / actual_denom
|
|
636
|
+
else:
|
|
637
|
+
# "drop" / "warn_and_drop": legacy behaviour, denominators are the
|
|
638
|
+
# NaN-dropped row counts, so missing-rate drift is invisible.
|
|
639
|
+
expected_percents = expected_bins.value_counts(normalize=True, sort=False)
|
|
640
|
+
actual_percents = actual_bins.value_counts(normalize=True, sort=False)
|
|
545
641
|
|
|
546
642
|
# Ensure both series have the same bin indices
|
|
547
643
|
all_bins = expected_percents.index.union(actual_percents.index)
|
|
@@ -579,7 +675,8 @@ def calculate_psi(
|
|
|
579
675
|
return_details: bool = False,
|
|
580
676
|
min_bin_prop: float = 0.05,
|
|
581
677
|
content: float = 1e-6,
|
|
582
|
-
precision: int = 5
|
|
678
|
+
precision: int = 5,
|
|
679
|
+
missing_policy: str = "include"
|
|
583
680
|
) -> Union[float, pd.DataFrame, Tuple[Dict, Dict]]:
|
|
584
681
|
"""
|
|
585
682
|
Calculate Population Stability Index (PSI) for a variable, optionally by groups.
|
|
@@ -609,6 +706,14 @@ def calculate_psi(
|
|
|
609
706
|
Small value to avoid division by zero. Default is 1e-6.
|
|
610
707
|
precision : int, optional
|
|
611
708
|
Decimal precision. Default is 5.
|
|
709
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
710
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
711
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
712
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
713
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
714
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
715
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
716
|
+
naming the NaN counts.
|
|
612
717
|
|
|
613
718
|
Returns
|
|
614
719
|
-------
|
|
@@ -661,7 +766,8 @@ def calculate_psi(
|
|
|
661
766
|
True,
|
|
662
767
|
min_bin_prop,
|
|
663
768
|
content,
|
|
664
|
-
precision
|
|
769
|
+
precision,
|
|
770
|
+
missing_policy
|
|
665
771
|
)
|
|
666
772
|
results[group] = psi_value
|
|
667
773
|
details_dict[group] = detail
|
|
@@ -674,7 +780,8 @@ def calculate_psi(
|
|
|
674
780
|
False,
|
|
675
781
|
min_bin_prop,
|
|
676
782
|
content,
|
|
677
|
-
precision
|
|
783
|
+
precision,
|
|
784
|
+
missing_policy
|
|
678
785
|
)
|
|
679
786
|
|
|
680
787
|
if return_details:
|
|
@@ -692,7 +799,8 @@ def calculate_psi(
|
|
|
692
799
|
True,
|
|
693
800
|
min_bin_prop,
|
|
694
801
|
content,
|
|
695
|
-
precision
|
|
802
|
+
precision,
|
|
803
|
+
missing_policy
|
|
696
804
|
)
|
|
697
805
|
else:
|
|
698
806
|
return _calculate_single_psi(
|
|
@@ -703,7 +811,8 @@ def calculate_psi(
|
|
|
703
811
|
False,
|
|
704
812
|
min_bin_prop,
|
|
705
813
|
content,
|
|
706
|
-
precision
|
|
814
|
+
precision,
|
|
815
|
+
missing_policy
|
|
707
816
|
)
|
|
708
817
|
|
|
709
818
|
|
|
@@ -718,7 +827,8 @@ def calculate_within_psi(
|
|
|
718
827
|
min_bin_prop: float = 0.05,
|
|
719
828
|
content: float = 1e-6,
|
|
720
829
|
precision: int = 5,
|
|
721
|
-
benchmark_display_name: Optional[str] = None
|
|
830
|
+
benchmark_display_name: Optional[str] = None,
|
|
831
|
+
missing_policy: str = "include"
|
|
722
832
|
) -> Union[pd.DataFrame, Dict]:
|
|
723
833
|
"""
|
|
724
834
|
Calculate PSI values within a single dataset, comparing groups to a benchmark.
|
|
@@ -750,6 +860,14 @@ def calculate_within_psi(
|
|
|
750
860
|
Decimal precision. Default is 5.
|
|
751
861
|
benchmark_display_name : str, optional
|
|
752
862
|
Custom name for benchmark in results.
|
|
863
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
864
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
865
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
866
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
867
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
868
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
869
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
870
|
+
naming the NaN counts.
|
|
753
871
|
|
|
754
872
|
Returns
|
|
755
873
|
-------
|
|
@@ -785,7 +903,8 @@ def calculate_within_psi(
|
|
|
785
903
|
return_details=True,
|
|
786
904
|
min_bin_prop=min_bin_prop,
|
|
787
905
|
content=content,
|
|
788
|
-
precision=precision
|
|
906
|
+
precision=precision,
|
|
907
|
+
missing_policy=missing_policy
|
|
789
908
|
)
|
|
790
909
|
res_dict[obs_value] = psi
|
|
791
910
|
detail_dict[obs_value] = details
|
|
@@ -799,7 +918,8 @@ def calculate_within_psi(
|
|
|
799
918
|
return_details=False,
|
|
800
919
|
min_bin_prop=min_bin_prop,
|
|
801
920
|
content=content,
|
|
802
|
-
precision=precision
|
|
921
|
+
precision=precision,
|
|
922
|
+
missing_policy=missing_policy
|
|
803
923
|
)
|
|
804
924
|
res_dict[obs_value] = psi
|
|
805
925
|
|
|
@@ -822,7 +942,8 @@ def calculate_psi_within_dataset(
|
|
|
822
942
|
buckets: int = 10,
|
|
823
943
|
min_bin_prop: float = 0.05,
|
|
824
944
|
content: float = 1e-6,
|
|
825
|
-
precision: int = 5
|
|
945
|
+
precision: int = 5,
|
|
946
|
+
missing_policy: str = "include"
|
|
826
947
|
) -> pd.DataFrame:
|
|
827
948
|
"""
|
|
828
949
|
Calculate PSI for multiple variables within a dataset, comparing groups to a benchmark.
|
|
@@ -850,6 +971,14 @@ def calculate_psi_within_dataset(
|
|
|
850
971
|
Small value to avoid division by zero. Default is 1e-6.
|
|
851
972
|
precision : int, optional
|
|
852
973
|
Decimal precision. Default is 5.
|
|
974
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
975
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
976
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
977
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
978
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
979
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
980
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
981
|
+
naming the NaN counts.
|
|
853
982
|
|
|
854
983
|
Returns
|
|
855
984
|
-------
|
|
@@ -873,7 +1002,8 @@ def calculate_psi_within_dataset(
|
|
|
873
1002
|
return_details=False,
|
|
874
1003
|
min_bin_prop=min_bin_prop,
|
|
875
1004
|
content=content,
|
|
876
|
-
precision=precision
|
|
1005
|
+
precision=precision,
|
|
1006
|
+
missing_policy=missing_policy
|
|
877
1007
|
).sort_values([grp_name]).reset_index(drop=True)
|
|
878
1008
|
|
|
879
1009
|
single_psi['var'] = var
|
|
@@ -891,7 +1021,8 @@ def calculate_multivar_psi_two_sets(
|
|
|
891
1021
|
equal_freq: bool = True,
|
|
892
1022
|
min_bin_prop: float = 0.05,
|
|
893
1023
|
content: float = 1e-6,
|
|
894
|
-
precision: int = 5
|
|
1024
|
+
precision: int = 5,
|
|
1025
|
+
missing_policy: str = "include"
|
|
895
1026
|
) -> pd.DataFrame:
|
|
896
1027
|
"""
|
|
897
1028
|
Calculate PSI for multiple variables by comparing two different datasets.
|
|
@@ -919,6 +1050,14 @@ def calculate_multivar_psi_two_sets(
|
|
|
919
1050
|
Small value to avoid division by zero. Default is 1e-6.
|
|
920
1051
|
precision : int, optional
|
|
921
1052
|
Decimal precision. Default is 5.
|
|
1053
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
1054
|
+
How NaN rows are handled. Default in 0.5.0 is "include", which routes
|
|
1055
|
+
NaN rows through a dedicated "__MISSING__" bin so missing-rate drift
|
|
1056
|
+
contributes to the PSI. This is the recommended production behaviour
|
|
1057
|
+
and became the default in 0.5.0 (previously "drop" in 0.4.2 for
|
|
1058
|
+
numeric backward-compat). Pass "drop" to reproduce pre-0.5.0
|
|
1059
|
+
numbers, or "warn_and_drop" for legacy numbers with a RuntimeWarning
|
|
1060
|
+
naming the NaN counts.
|
|
922
1061
|
|
|
923
1062
|
Returns
|
|
924
1063
|
-------
|
|
@@ -938,7 +1077,8 @@ def calculate_multivar_psi_two_sets(
|
|
|
938
1077
|
target_col=var,
|
|
939
1078
|
group_by=group_by,
|
|
940
1079
|
buckets=buckets,
|
|
941
|
-
return_details=False
|
|
1080
|
+
return_details=False,
|
|
1081
|
+
missing_policy=missing_policy
|
|
942
1082
|
)
|
|
943
1083
|
if group_by is None:
|
|
944
1084
|
single_psi = pd.DataFrame([single_psi], columns=['psi'])
|
|
@@ -1078,7 +1218,8 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1078
1218
|
content: float = 1e-6,
|
|
1079
1219
|
precision: int = 5,
|
|
1080
1220
|
group_name: Optional[str] = None,
|
|
1081
|
-
return_details: bool = False
|
|
1221
|
+
return_details: bool = False,
|
|
1222
|
+
missing_policy: str = "include"
|
|
1082
1223
|
) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]:
|
|
1083
1224
|
"""
|
|
1084
1225
|
Calculate grouped PSI using expected DataFrame as benchmark, applied to actual DataFrame groups.
|
|
@@ -1108,6 +1249,12 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1108
1249
|
return_details : bool, optional
|
|
1109
1250
|
是否返回详细分箱信息。若为 True,返回字典 {'psi': psi_df, 'details': details_df},
|
|
1110
1251
|
details_df 包含列:['bin', 'expected_percent', 'actual_percent', 'psi_component', group_name, 'var']
|
|
1252
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
1253
|
+
NaN 行处理策略。0.5.0 起默认为 "include",将 NaN 行汇入独立的
|
|
1254
|
+
"__MISSING__" 桶,使缺失率漂移进入 PSI 计算;这是推荐的生产行为,
|
|
1255
|
+
并在 0.5.0 成为默认值(0.4.2 为 "drop" 以保持数值向后兼容)。
|
|
1256
|
+
传入 "drop" 可复现 0.5.0 之前的数值,或 "warn_and_drop" 保留旧数值
|
|
1257
|
+
同时发出 RuntimeWarning 报告两侧 NaN 数量。
|
|
1111
1258
|
"""
|
|
1112
1259
|
if group_name is not None:
|
|
1113
1260
|
if actual_df[group_name].isna().sum() > 0:
|
|
@@ -1130,7 +1277,8 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1130
1277
|
return_details=True,
|
|
1131
1278
|
min_bin_prop=min_bin_prop,
|
|
1132
1279
|
content=content,
|
|
1133
|
-
precision=precision
|
|
1280
|
+
precision=precision,
|
|
1281
|
+
missing_policy=missing_policy
|
|
1134
1282
|
)
|
|
1135
1283
|
|
|
1136
1284
|
psi_records.append({group_name: group, 'var': var, 'psi': psi_val})
|
|
@@ -1177,7 +1325,8 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1177
1325
|
return_details=True,
|
|
1178
1326
|
min_bin_prop=min_bin_prop,
|
|
1179
1327
|
content=content,
|
|
1180
|
-
precision=precision
|
|
1328
|
+
precision=precision,
|
|
1329
|
+
missing_policy=missing_policy
|
|
1181
1330
|
)
|
|
1182
1331
|
psi_records.append({'var': var, 'psi': psi_val})
|
|
1183
1332
|
|
|
@@ -1212,7 +1361,8 @@ def calculate_multigroup_psi_two_sets(
|
|
|
1212
1361
|
equal_freq=equal_freq,
|
|
1213
1362
|
min_bin_prop=min_bin_prop,
|
|
1214
1363
|
content=content,
|
|
1215
|
-
precision=precision
|
|
1364
|
+
precision=precision,
|
|
1365
|
+
missing_policy=missing_policy
|
|
1216
1366
|
)
|
|
1217
1367
|
|
|
1218
1368
|
return group_psi
|
{supermodelingfactory-0.3.15 → supermodelingfactory-0.5.0}/Modeling_Tool/Feature/Weighted_Screen.py
RENAMED
|
@@ -113,11 +113,25 @@ def _weighted_bin_distribution(
|
|
|
113
113
|
w: np.ndarray,
|
|
114
114
|
content: float,
|
|
115
115
|
) -> pd.Series:
|
|
116
|
+
"""Return the weighted bin distribution as an observed-only frequency
|
|
117
|
+
Series.
|
|
118
|
+
|
|
119
|
+
Fix (0.4.2, N23): the pre-0.4.2 version unioned ``_MISSING_BIN`` into the
|
|
120
|
+
returned index and clipped every entry to ``content`` (a small positive
|
|
121
|
+
floor such as ``1e-5``). When only one side of a PSI comparison had missing
|
|
122
|
+
values, that forced a large synthetic mass on the missing bin of the side
|
|
123
|
+
that actually observed zero missings — which then propagated through
|
|
124
|
+
``_psi_from_distributions`` as a large spurious PSI component. The fix is
|
|
125
|
+
to return only the bins actually observed in ``bins``; the alignment and
|
|
126
|
+
zero-safety are done exactly once, in ``_psi_from_distributions``, so both
|
|
127
|
+
sides are treated symmetrically. The ``content`` parameter is retained for
|
|
128
|
+
signature stability but is now unused in this helper.
|
|
129
|
+
"""
|
|
130
|
+
del content # retained for signature stability; alignment is elsewhere.
|
|
116
131
|
df = pd.DataFrame({"bin": bins, "w": w})
|
|
117
132
|
dist = df.groupby("bin", dropna=False)["w"].sum()
|
|
118
133
|
total = float(dist.sum()) or 1.0
|
|
119
|
-
|
|
120
|
-
return dist.reindex(dist.index.union([_MISSING_BIN]), fill_value=0.0).clip(lower=content)
|
|
134
|
+
return dist / total
|
|
121
135
|
|
|
122
136
|
|
|
123
137
|
def _psi_from_distributions(expected: pd.Series, actual: pd.Series, content: float) -> float:
|