SuperModelingFactory 0.3.14__tar.gz → 0.4.2__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.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/ODPS_Tool.py +102 -9
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Distribution_Tool.py +34 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Distribution_Tool.pyi +1 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Feature_Insights.py +6 -5
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Feature_Screen.py +30 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/PSI_Tool.py +107 -34
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +3 -3
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Weighted_Screen.py +128 -3
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/_common.py +109 -3
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/credit_model.py +36 -8
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/feature_validation.py +81 -24
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/reject_inference.py +58 -4
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/sample_analysis.py +50 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/score_comparison.py +20 -3
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/score_consistency_uat.py +54 -6
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/screening_artifact.py +4 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Sample/Reject_Infer.py +63 -7
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Sample/Sample_Split.py +33 -22
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/__init__.py +1 -1
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/PKG-INFO +2 -2
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/README.md +1 -1
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/pyproject.toml +1 -1
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/setup.py +1 -1
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/ExcelMaster/ExcelFormatTool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/ExcelMaster/ExcelMaster.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/ExcelMaster/Template.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/ExcelMaster/Utility.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/ExcelMaster/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/LICENSE +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/MANIFEST.in +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Binning_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Proc_Compare.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Slope_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/kDataFrame.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Core/utils.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/evaluate_model.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Explainability/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/Backward_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/GBM_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/LRM_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Model/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Sample/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/UAT/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Master.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/ref_font/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/ref_font/simsun.ttc +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Report/Report_Tool.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Report/__init__.py +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/SuperModelingFactory.egg-info/requires.txt +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/SuperModelingFactory.egg-info/top_level.txt +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/requirements.txt +0 -0
- {supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/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):
|
|
@@ -186,6 +186,40 @@ def proc_means_by_grp(data, varlist, groupby=None, spec_missing_value=None, q=No
|
|
|
186
186
|
return means_rpt
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
_SCREENING_MEANS_COLS = ["attribute", "N_ALL", "N", "MISSING_RATE", "MIN", "MEAN", "MAX"]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def proc_means_for_screening(data, varlist, spec_missing_value=None, q=None):
|
|
193
|
+
"""Build a uniform means table for IV screening reports.
|
|
194
|
+
|
|
195
|
+
``proc_means_by_grp`` returns object stats (unique/top/freq) when numeric and
|
|
196
|
+
categorical columns are melted together. This helper splits by dtype so MIN,
|
|
197
|
+
MEAN, and MAX are always present (NaN for non-numeric features).
|
|
198
|
+
"""
|
|
199
|
+
if not varlist:
|
|
200
|
+
return pd.DataFrame(columns=_SCREENING_MEANS_COLS)
|
|
201
|
+
|
|
202
|
+
numeric = [v for v in varlist if v in data.columns and pd.api.types.is_numeric_dtype(data[v])]
|
|
203
|
+
other = [v for v in varlist if v not in numeric and v in data.columns]
|
|
204
|
+
frames = []
|
|
205
|
+
|
|
206
|
+
if numeric:
|
|
207
|
+
frames.append(proc_means_by_grp(data, numeric, spec_missing_value=spec_missing_value, q=q))
|
|
208
|
+
|
|
209
|
+
for var in other:
|
|
210
|
+
part = proc_means_by_grp(data, [var], spec_missing_value=spec_missing_value, q=q)
|
|
211
|
+
for col in ("MIN", "MEAN", "MAX"):
|
|
212
|
+
if col not in part.columns:
|
|
213
|
+
part[col] = np.nan
|
|
214
|
+
frames.append(part)
|
|
215
|
+
|
|
216
|
+
if not frames:
|
|
217
|
+
return pd.DataFrame(columns=_SCREENING_MEANS_COLS)
|
|
218
|
+
|
|
219
|
+
out = pd.concat(frames, ignore_index=True)
|
|
220
|
+
return out[_SCREENING_MEANS_COLS]
|
|
221
|
+
|
|
222
|
+
|
|
189
223
|
class DistributionShiftAnalyzer:
|
|
190
224
|
"""分布偏移分析器。
|
|
191
225
|
|
|
@@ -25,6 +25,7 @@ class proc_means:
|
|
|
25
25
|
def group_sum(self): ...
|
|
26
26
|
def __call__(self, q = None): ...
|
|
27
27
|
def proc_means_by_grp(data, varlist, groupby = None, spec_missing_value = None, q = None): ...
|
|
28
|
+
def proc_means_for_screening(data, varlist, spec_missing_value = None, q = None): ...
|
|
28
29
|
|
|
29
30
|
class DistributionShiftAnalyzer:
|
|
30
31
|
def __init__(self, data, grp_name, benchmark_value): ...
|
{supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Feature_Insights.py
RENAMED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
from tqdm import tqdm
|
|
8
8
|
|
|
9
|
-
from .Distribution_Tool import proc_means_by_grp
|
|
9
|
+
from .Distribution_Tool import proc_means_by_grp, proc_means_for_screening
|
|
10
10
|
import logging
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -208,14 +208,15 @@ class VarExtractionInsights:
|
|
|
208
208
|
high_iv_summary = iv_info_res.query(f"IV >= {iv_cut}").round(4)
|
|
209
209
|
high_iv_varlist = high_iv_summary['var'].tolist()
|
|
210
210
|
|
|
211
|
-
means = proc_means_by_grp(data, high_iv_varlist, spec_missing_value=self.missing_rate_ref)
|
|
212
|
-
|
|
213
211
|
if len(high_iv_varlist) == 0:
|
|
214
212
|
logger.info(f"WARNING: No variable with IV >= {iv_cut}")
|
|
215
|
-
|
|
213
|
+
|
|
214
|
+
means = proc_means_for_screening(
|
|
215
|
+
data, high_iv_varlist, spec_missing_value=self.missing_rate_ref,
|
|
216
|
+
)
|
|
216
217
|
|
|
217
218
|
fnl_summary = high_iv_summary.merge(
|
|
218
|
-
means
|
|
219
|
+
means,
|
|
219
220
|
left_on=['var'],
|
|
220
221
|
right_on=['attribute'],
|
|
221
222
|
how='left'
|
{supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/Modeling_Tool/Feature/Feature_Screen.py
RENAMED
|
@@ -13,6 +13,7 @@ from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
|
|
|
13
13
|
|
|
14
14
|
from .Weighted_Screen import (
|
|
15
15
|
WeightedScreenResult,
|
|
16
|
+
_apply_missing_rate_stage,
|
|
16
17
|
_apply_stage_keep,
|
|
17
18
|
_corr_dedup_weighted,
|
|
18
19
|
_legacy_unweighted_screen,
|
|
@@ -58,6 +59,8 @@ class FeatureScreenConfig:
|
|
|
58
59
|
corr_use_woe_bins: bool = False
|
|
59
60
|
corr_nan_policy: Literal["pairwise", "median_fill", "raise"] = "pairwise"
|
|
60
61
|
on_empty_stage: Literal["keep_all_warn", "raise"] = "keep_all_warn"
|
|
62
|
+
missing_rate_threshold: float | None = None
|
|
63
|
+
missing_rate_ref: float | int = -999999
|
|
61
64
|
woe_engine: str = "equal_freq"
|
|
62
65
|
woe_fit_query: str | None = None
|
|
63
66
|
woe_params: dict[str, Any] = field(
|
|
@@ -105,6 +108,8 @@ def screen_config_from_mapping(
|
|
|
105
108
|
corr_use_woe_bins=bool(cfg.get("corr_use_woe_bins", False)),
|
|
106
109
|
corr_nan_policy=str(cfg.get("corr_nan_policy", "pairwise")), # type: ignore[arg-type]
|
|
107
110
|
on_empty_stage=str(cfg.get("on_empty_stage", "keep_all_warn")), # type: ignore[arg-type]
|
|
111
|
+
missing_rate_threshold=cfg.get("missing_rate_threshold"),
|
|
112
|
+
missing_rate_ref=cfg.get("missing_rate_ref", -999999),
|
|
108
113
|
woe_engine=str(woe_engine or cfg.get("woe_engine", "equal_freq")),
|
|
109
114
|
woe_fit_query=woe_fit_query if woe_fit_query is not None else cfg.get("woe_fit_query"),
|
|
110
115
|
woe_params=dict(FeatureScreenConfig().woe_params | dict(woe_params or cfg.get("woe_params") or {})),
|
|
@@ -211,6 +216,14 @@ def _woe_bins_unweighted_screen(
|
|
|
211
216
|
ins, oos, oot = splits["ins"], splits["oos"], splits["oot"]
|
|
212
217
|
current = list(feature_cols)
|
|
213
218
|
summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, None)]
|
|
219
|
+
current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
|
|
220
|
+
ins,
|
|
221
|
+
current,
|
|
222
|
+
summary_rows,
|
|
223
|
+
missing_rate_threshold=config.missing_rate_threshold,
|
|
224
|
+
missing_rate_ref=config.missing_rate_ref,
|
|
225
|
+
on_empty_stage=config.on_empty_stage,
|
|
226
|
+
)
|
|
214
227
|
binner = _resolve_screening_binner(splits, feature_cols, target_col, config, prefit_woe_engine)
|
|
215
228
|
|
|
216
229
|
psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
|
|
@@ -303,6 +316,8 @@ def _woe_bins_unweighted_screen(
|
|
|
303
316
|
psi_table=psi_table,
|
|
304
317
|
corr_dropped=corr_dropped,
|
|
305
318
|
summary=pd.DataFrame(summary_rows),
|
|
319
|
+
missing_rate_table=missing_rate_table,
|
|
320
|
+
missing_rate_dropped=missing_rate_dropped,
|
|
306
321
|
)
|
|
307
322
|
|
|
308
323
|
|
|
@@ -328,6 +343,15 @@ def _weighted_woe_bins_screen(
|
|
|
328
343
|
y_ins = ins[target_col].astype(float).to_numpy()
|
|
329
344
|
current = list(feature_cols)
|
|
330
345
|
summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, weight_col)]
|
|
346
|
+
current, missing_rate_table, missing_rate_dropped = _apply_missing_rate_stage(
|
|
347
|
+
ins,
|
|
348
|
+
current,
|
|
349
|
+
summary_rows,
|
|
350
|
+
missing_rate_threshold=config.missing_rate_threshold,
|
|
351
|
+
missing_rate_ref=config.missing_rate_ref,
|
|
352
|
+
weight_col=weight_col,
|
|
353
|
+
on_empty_stage=config.on_empty_stage,
|
|
354
|
+
)
|
|
331
355
|
|
|
332
356
|
binner = prefit_woe_engine
|
|
333
357
|
if binner is None:
|
|
@@ -438,6 +462,8 @@ def _weighted_woe_bins_screen(
|
|
|
438
462
|
psi_table=psi_table,
|
|
439
463
|
corr_dropped=corr_dropped,
|
|
440
464
|
summary=pd.DataFrame(summary_rows),
|
|
465
|
+
missing_rate_table=missing_rate_table,
|
|
466
|
+
missing_rate_dropped=missing_rate_dropped,
|
|
441
467
|
)
|
|
442
468
|
|
|
443
469
|
|
|
@@ -485,6 +511,8 @@ def feature_screen(
|
|
|
485
511
|
corr_nan_policy=cfg.corr_nan_policy,
|
|
486
512
|
on_empty_stage=cfg.on_empty_stage,
|
|
487
513
|
prefit_woe_engine=prefit_woe_engine,
|
|
514
|
+
missing_rate_threshold=cfg.missing_rate_threshold,
|
|
515
|
+
missing_rate_ref=cfg.missing_rate_ref,
|
|
488
516
|
)
|
|
489
517
|
|
|
490
518
|
if use_woe_bins or prefit_woe_engine is not None:
|
|
@@ -515,6 +543,8 @@ def feature_screen(
|
|
|
515
543
|
plot_outputs=cfg.plot_outputs,
|
|
516
544
|
iv_equal_freq=cfg.iv_equal_freq,
|
|
517
545
|
on_empty_stage=cfg.on_empty_stage,
|
|
546
|
+
missing_rate_threshold=cfg.missing_rate_threshold,
|
|
547
|
+
missing_rate_ref=cfg.missing_rate_ref,
|
|
518
548
|
)
|
|
519
549
|
|
|
520
550
|
|
{supermodelingfactory-0.3.14 → supermodelingfactory-0.4.2}/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
|
# ============================================================================
|
|
@@ -460,14 +468,29 @@ def _calculate_single_psi(
|
|
|
460
468
|
return_details: bool = False,
|
|
461
469
|
min_bin_prop: float = 0.05,
|
|
462
470
|
content: float = 1e-6,
|
|
463
|
-
precision: int = 5
|
|
471
|
+
precision: int = 5,
|
|
472
|
+
missing_policy: str = "drop",
|
|
464
473
|
) -> Union[float, Tuple[float, pd.DataFrame]]:
|
|
465
474
|
"""
|
|
466
475
|
Calculate Population Stability Index (PSI) for a single variable.
|
|
467
|
-
|
|
476
|
+
|
|
468
477
|
This function bins both expected and actual series using the same breakpoints,
|
|
469
478
|
then calculates PSI based on the distribution difference between them.
|
|
470
|
-
|
|
479
|
+
|
|
480
|
+
Missing handling (0.4.2, N29)
|
|
481
|
+
------------------------------
|
|
482
|
+
Before 0.4.2, both series were unconditionally passed through ``.dropna()``
|
|
483
|
+
before binning. Any drift in the *missing rate itself* — the single most
|
|
484
|
+
common early signal of a data-pipeline break — was silently invisible in
|
|
485
|
+
the returned PSI. 0.4.2 adds the ``missing_policy`` parameter so callers
|
|
486
|
+
can opt in to the corrected behaviour. The default remains ``"drop"`` for
|
|
487
|
+
strict backward compatibility with the pre-0.4.2 numeric output; the next
|
|
488
|
+
minor release will flip the default to ``"include"``. Pass
|
|
489
|
+
``missing_policy="include"`` to route NaN rows through a dedicated
|
|
490
|
+
``"__MISSING__"`` bin on both sides so missing-rate drift contributes to
|
|
491
|
+
the PSI. Pass ``missing_policy="warn_and_drop"`` to keep the old numbers
|
|
492
|
+
but at least surface a ``RuntimeWarning`` naming the two NaN counts.
|
|
493
|
+
|
|
471
494
|
Parameters
|
|
472
495
|
----------
|
|
473
496
|
expected_series : pandas.Series
|
|
@@ -486,62 +509,112 @@ def _calculate_single_psi(
|
|
|
486
509
|
Small value to avoid division by zero. Default is 1e-6.
|
|
487
510
|
precision : int, optional
|
|
488
511
|
Decimal precision. Default is 5.
|
|
489
|
-
|
|
512
|
+
missing_policy : {"drop", "include", "warn_and_drop"}, optional
|
|
513
|
+
How NaN rows are handled. ``"drop"`` (default in 0.4.2) reproduces the
|
|
514
|
+
pre-0.4.2 behaviour and silently excludes NaN rows from both sides
|
|
515
|
+
before binning. ``"include"`` treats NaN as its own ``"__MISSING__"``
|
|
516
|
+
bin so missing-rate drift shows up in the PSI — recommended for any
|
|
517
|
+
production drift monitor, and will become the default in the next
|
|
518
|
+
minor release. ``"warn_and_drop"`` behaves like ``"drop"`` but emits
|
|
519
|
+
a ``RuntimeWarning`` naming the two NaN counts so the caller sees what
|
|
520
|
+
was dropped.
|
|
521
|
+
|
|
490
522
|
Returns
|
|
491
523
|
-------
|
|
492
524
|
float or tuple
|
|
493
525
|
If return_details is False: Returns total PSI value.
|
|
494
526
|
If return_details is True: Returns tuple of (PSI value, details DataFrame).
|
|
495
|
-
|
|
527
|
+
|
|
496
528
|
Notes
|
|
497
529
|
-----
|
|
498
530
|
PSI Formula: Σ (Actual% - Expected%) * ln(Actual% / Expected%)
|
|
499
531
|
A PSI < 0.1 indicates stable population, 0.1-0.25 suggests some change,
|
|
500
532
|
and > 0.25 indicates significant drift.
|
|
501
533
|
"""
|
|
502
|
-
|
|
534
|
+
if missing_policy not in {"include", "drop", "warn_and_drop"}:
|
|
535
|
+
raise ValueError(
|
|
536
|
+
f"_calculate_single_psi: missing_policy must be one of "
|
|
537
|
+
f"'include', 'drop', 'warn_and_drop'; got {missing_policy!r}."
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Record NaN counts before splitting so we can (a) re-attach the missing
|
|
541
|
+
# bin under "include" and (b) warn under "warn_and_drop".
|
|
542
|
+
n_expected = int(len(expected_series))
|
|
543
|
+
n_actual = int(len(actual_series))
|
|
544
|
+
n_expected_na = int(expected_series.isna().sum())
|
|
545
|
+
n_actual_na = int(actual_series.isna().sum())
|
|
546
|
+
|
|
547
|
+
if missing_policy == "warn_and_drop" and (n_expected_na or n_actual_na):
|
|
548
|
+
warnings.warn(
|
|
549
|
+
f"_calculate_single_psi: dropping NaN rows before binning. "
|
|
550
|
+
f"expected: {n_expected_na}/{n_expected} NaN, "
|
|
551
|
+
f"actual: {n_actual_na}/{n_actual} NaN. "
|
|
552
|
+
f"Pass missing_policy='include' to treat NaN as its own bin so "
|
|
553
|
+
f"missing-rate drift is captured.",
|
|
554
|
+
RuntimeWarning,
|
|
555
|
+
stacklevel=2,
|
|
556
|
+
)
|
|
557
|
+
|
|
503
558
|
expected_clean = expected_series.dropna()
|
|
504
559
|
actual_clean = actual_series.dropna()
|
|
505
|
-
|
|
560
|
+
|
|
506
561
|
# Bin the expected data to get breakpoints
|
|
507
562
|
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,
|
|
563
|
+
pd.DataFrame(expected_clean),
|
|
564
|
+
expected_clean.name,
|
|
565
|
+
labels=None,
|
|
566
|
+
nbins=buckets,
|
|
567
|
+
precision=precision,
|
|
568
|
+
equal_freq=equal_freq,
|
|
569
|
+
right=True,
|
|
515
570
|
include_lowest=False,
|
|
516
|
-
min_bin_prop=min_bin_prop,
|
|
517
|
-
tree_binning=False,
|
|
518
|
-
target=None,
|
|
571
|
+
min_bin_prop=min_bin_prop,
|
|
572
|
+
tree_binning=False,
|
|
573
|
+
target=None,
|
|
519
574
|
random_state=42
|
|
520
575
|
)
|
|
521
|
-
|
|
576
|
+
|
|
522
577
|
# Bin the actual data using the same breakpoints
|
|
523
578
|
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,
|
|
579
|
+
pd.DataFrame(actual_clean),
|
|
580
|
+
actual_clean.name,
|
|
581
|
+
labels=None,
|
|
582
|
+
nbins=list(breakpoints),
|
|
583
|
+
precision=precision,
|
|
584
|
+
equal_freq=equal_freq,
|
|
585
|
+
right=True,
|
|
531
586
|
include_lowest=False,
|
|
532
|
-
min_bin_prop=min_bin_prop,
|
|
533
|
-
tree_binning=False,
|
|
534
|
-
target=None,
|
|
587
|
+
min_bin_prop=min_bin_prop,
|
|
588
|
+
tree_binning=False,
|
|
589
|
+
target=None,
|
|
535
590
|
random_state=42
|
|
536
591
|
)
|
|
537
|
-
|
|
538
|
-
# Get bin counts
|
|
592
|
+
|
|
593
|
+
# Get bin counts (finite-value rows only, from quick_binning)
|
|
539
594
|
expected_count = expected_bins.value_counts(normalize=False, sort=False)
|
|
540
595
|
actual_count = actual_bins.value_counts(normalize=False, sort=False)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
596
|
+
|
|
597
|
+
if missing_policy == "include":
|
|
598
|
+
# Re-attach the __MISSING__ bin so that missing-rate drift contributes
|
|
599
|
+
# to the PSI on both sides. The denominators use the *original* row
|
|
600
|
+
# counts (before dropna) so the fractions are directly comparable.
|
|
601
|
+
if n_expected_na > 0:
|
|
602
|
+
expected_count = pd.concat(
|
|
603
|
+
[expected_count, pd.Series({_MISSING_BIN: n_expected_na})]
|
|
604
|
+
)
|
|
605
|
+
if n_actual_na > 0:
|
|
606
|
+
actual_count = pd.concat(
|
|
607
|
+
[actual_count, pd.Series({_MISSING_BIN: n_actual_na})]
|
|
608
|
+
)
|
|
609
|
+
expected_denom = float(n_expected) if n_expected > 0 else 1.0
|
|
610
|
+
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
|
+
else:
|
|
614
|
+
# "drop" / "warn_and_drop": legacy behaviour, denominators are the
|
|
615
|
+
# NaN-dropped row counts, so missing-rate drift is invisible.
|
|
616
|
+
expected_percents = expected_bins.value_counts(normalize=True, sort=False)
|
|
617
|
+
actual_percents = actual_bins.value_counts(normalize=True, sort=False)
|
|
545
618
|
|
|
546
619
|
# Ensure both series have the same bin indices
|
|
547
620
|
all_bins = expected_percents.index.union(actual_percents.index)
|
|
@@ -19,7 +19,7 @@ from .Feature_Insights import (
|
|
|
19
19
|
VarExtractionInsights as _BaseVarExtractionInsights,
|
|
20
20
|
var_corr_filter,
|
|
21
21
|
)
|
|
22
|
-
from .Distribution_Tool import
|
|
22
|
+
from .Distribution_Tool import proc_means_for_screening
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def _psi_from_bins(expected_bins: pd.Series, current_bins: pd.Series, content: float) -> float:
|
|
@@ -110,9 +110,9 @@ def _screening_summary_from_engine(
|
|
|
110
110
|
high_iv_summary = pd.DataFrame(rows).query(f"iv >= {iv_cut}").round(4)
|
|
111
111
|
high_iv_varlist = high_iv_summary["var"].tolist()
|
|
112
112
|
if high_iv_varlist:
|
|
113
|
-
means =
|
|
113
|
+
means = proc_means_for_screening(data, high_iv_varlist, spec_missing_value=missing_rate_ref)
|
|
114
114
|
summary = high_iv_summary.merge(
|
|
115
|
-
means
|
|
115
|
+
means,
|
|
116
116
|
left_on="var",
|
|
117
117
|
right_on="attribute",
|
|
118
118
|
how="left",
|