SuperModelingFactory 0.5.10__tar.gz → 0.6.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.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Proc_Compare.py +189 -36
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/kDataFrame.py +8 -16
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/utils.py +10 -7
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Model_Eval_Tool.py +26 -46
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/evaluate_model.py +48 -19
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/weighted_eval_utils.py +13 -2
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Explainability/Coalition_Structure.py +5 -5
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Explainability/Model_Explainer.py +106 -26
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Distribution_Tool.py +105 -29
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Distribution_Tool.pyi +3 -3
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Feature_Insights.py +82 -36
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Feature_Screen.py +18 -4
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/PSI_Tool.py +244 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/PSI_Tool.pyi +1 -1
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +181 -61
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Weighted_Screen.py +94 -20
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/feature_validation.py +322 -96
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/field_meta.py +9 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/sample_analysis.py +124 -101
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/score_consistency_uat.py +2 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Sample_Split.py +32 -3
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Sample_Split.pyi +1 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +73 -50
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Adapter.py +45 -6
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Adapter.pyi +1 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +30 -9
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +1 -1
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Tool.py +82 -35
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/__init__.py +1 -1
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/PKG-INFO +2 -2
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/README.md +1 -1
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/pyproject.toml +1 -1
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/setup.py +1 -1
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/ExcelMaster/ExcelFormatTool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/ExcelMaster/ExcelMaster.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/ExcelMaster/Template.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/ExcelMaster/Utility.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/ExcelMaster/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/LICENSE +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/MANIFEST.in +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Binning_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Slope_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Explainability/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Feature/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/Backward_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/GBM_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/LRM_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Model/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/_common.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/credit_model.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Sample/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/UAT/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Master.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/nan_guard.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/robust.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/_utils/sentinels.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/ref_font/simsun.ttc +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Report/Report_Tool.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Report/__init__.py +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/requires.txt +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/SuperModelingFactory.egg-info/top_level.txt +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/requirements.txt +0 -0
- {supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/setup.cfg +0 -0
{supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Core/Proc_Compare.py
RENAMED
|
@@ -34,6 +34,7 @@ class ProcCompareConfig:
|
|
|
34
34
|
chunk_size: int = 200_000
|
|
35
35
|
n_partitions: int = 16
|
|
36
36
|
backend: CompareBackend = "sequential"
|
|
37
|
+
compare_block_size: int = 64
|
|
37
38
|
|
|
38
39
|
numeric_tol: float = 1e-8
|
|
39
40
|
numeric_rtol: float = 0.0
|
|
@@ -107,6 +108,8 @@ class ProcCompareEngine:
|
|
|
107
108
|
raise ValueError("chunk_size must be a positive integer.")
|
|
108
109
|
if cfg.n_partitions <= 0:
|
|
109
110
|
raise ValueError("n_partitions must be a positive integer.")
|
|
111
|
+
if cfg.compare_block_size <= 0:
|
|
112
|
+
raise ValueError("compare_block_size must be a positive integer.")
|
|
110
113
|
if cfg.numeric_tol < 0 or cfg.numeric_rtol < 0 or cfg.datetime_tol_seconds < 0:
|
|
111
114
|
raise ValueError("tolerance values must be non-negative.")
|
|
112
115
|
if cfg.top_n <= 0:
|
|
@@ -294,44 +297,54 @@ class ProcCompareEngine:
|
|
|
294
297
|
mismatch_frames = []
|
|
295
298
|
mismatch_cols_by_row: dict[int, list[str]] = {}
|
|
296
299
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
300
|
+
common_values = common_mask.to_numpy(dtype=bool)
|
|
301
|
+
n_compared = int(common_values.sum())
|
|
302
|
+
block_size = int(self.config.compare_block_size)
|
|
303
|
+
for start in range(0, len(compare_cols), block_size):
|
|
304
|
+
block_cols = compare_cols[start : start + block_size]
|
|
305
|
+
comp = self._compare_column_block(merged, block_cols, common_values)
|
|
306
|
+
mismatch_matrix = comp["mismatch"]
|
|
307
|
+
one_null_matrix = comp["one_null"]
|
|
308
|
+
both_null_matrix = comp["both_null"]
|
|
309
|
+
diff_matrix = comp["diff"]
|
|
310
|
+
abs_diff_matrix = comp["abs_diff"]
|
|
311
|
+
left_values = comp["left_values"]
|
|
312
|
+
right_values = comp["right_values"]
|
|
313
|
+
|
|
314
|
+
for col_idx, col in enumerate(block_cols):
|
|
315
|
+
mismatch_positions = np.flatnonzero(mismatch_matrix[:, col_idx])
|
|
316
|
+
n_mismatch = int(len(mismatch_positions))
|
|
317
|
+
for idx in mismatch_positions:
|
|
306
318
|
mismatch_cols_by_row.setdefault(int(idx), []).append(col)
|
|
307
|
-
if self.config.detail_mode != "none":
|
|
308
|
-
detail = merged.
|
|
319
|
+
if n_mismatch and self.config.detail_mode != "none":
|
|
320
|
+
detail = merged.iloc[mismatch_positions][key_cols].copy()
|
|
309
321
|
detail["column"] = col
|
|
310
|
-
detail["left_value"] =
|
|
311
|
-
detail["right_value"] =
|
|
312
|
-
detail["diff"] =
|
|
313
|
-
detail["abs_diff"] =
|
|
322
|
+
detail["left_value"] = left_values[mismatch_positions, col_idx]
|
|
323
|
+
detail["right_value"] = right_values[mismatch_positions, col_idx]
|
|
324
|
+
detail["diff"] = diff_matrix[mismatch_positions, col_idx]
|
|
325
|
+
detail["abs_diff"] = abs_diff_matrix[mismatch_positions, col_idx]
|
|
314
326
|
mismatch_frames.append(detail)
|
|
315
327
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
328
|
+
diff_values = diff_matrix[:, col_idx]
|
|
329
|
+
abs_diff_values = abs_diff_matrix[:, col_idx]
|
|
330
|
+
diff_valid = np.isfinite(diff_values)
|
|
331
|
+
diff_sum = float(np.nansum(diff_values)) if diff_valid.any() else 0.0
|
|
332
|
+
diff_count = int(diff_valid.sum())
|
|
333
|
+
column_rows.append(
|
|
334
|
+
{
|
|
335
|
+
"column": col,
|
|
336
|
+
"n_compared": n_compared,
|
|
337
|
+
"n_equal": n_compared - n_mismatch,
|
|
338
|
+
"n_mismatch": n_mismatch,
|
|
339
|
+
"pct_mismatch": round(n_mismatch / n_compared * 100, 6) if n_compared else 0.0,
|
|
340
|
+
"n_one_side_null": int(one_null_matrix[:, col_idx].sum()),
|
|
341
|
+
"n_both_null": int(both_null_matrix[:, col_idx].sum()),
|
|
342
|
+
"mean_diff": diff_sum / diff_count if diff_count else np.nan,
|
|
343
|
+
"max_abs_diff": float(np.nanmax(abs_diff_values)) if np.isfinite(abs_diff_values).any() else np.nan,
|
|
344
|
+
"_diff_sum": diff_sum,
|
|
345
|
+
"_diff_count": diff_count,
|
|
346
|
+
}
|
|
347
|
+
)
|
|
335
348
|
|
|
336
349
|
for idx, cols in mismatch_cols_by_row.items():
|
|
337
350
|
row_summary.loc[idx, "n_cell_mismatch"] = len(cols)
|
|
@@ -375,6 +388,146 @@ class ProcCompareEngine:
|
|
|
375
388
|
duplicate_key_summary=duplicate_key_summary,
|
|
376
389
|
)
|
|
377
390
|
|
|
391
|
+
def _compare_column_block(
|
|
392
|
+
self,
|
|
393
|
+
merged: pd.DataFrame,
|
|
394
|
+
columns: list[str],
|
|
395
|
+
common_mask: np.ndarray,
|
|
396
|
+
) -> dict[str, np.ndarray]:
|
|
397
|
+
"""Compare a bounded field block with vectorized type-specific kernels."""
|
|
398
|
+
n_rows = len(merged)
|
|
399
|
+
n_cols = len(columns)
|
|
400
|
+
left_series = [self._normalize_series_missing(merged[f"{col}_left"]) for col in columns]
|
|
401
|
+
right_series = [self._normalize_series_missing(merged[f"{col}_right"]) for col in columns]
|
|
402
|
+
left_values = np.column_stack([series.to_numpy(dtype=object) for series in left_series])
|
|
403
|
+
right_values = np.column_stack([series.to_numpy(dtype=object) for series in right_series])
|
|
404
|
+
mismatch = np.zeros((n_rows, n_cols), dtype=bool)
|
|
405
|
+
one_null = np.zeros((n_rows, n_cols), dtype=bool)
|
|
406
|
+
both_null = np.zeros((n_rows, n_cols), dtype=bool)
|
|
407
|
+
diff = np.full((n_rows, n_cols), np.nan, dtype=float)
|
|
408
|
+
abs_diff = np.full((n_rows, n_cols), np.nan, dtype=float)
|
|
409
|
+
|
|
410
|
+
datetime_idx = [
|
|
411
|
+
idx
|
|
412
|
+
for idx, col in enumerate(columns)
|
|
413
|
+
if self._should_compare_datetime(col, left_series[idx], right_series[idx])
|
|
414
|
+
]
|
|
415
|
+
datetime_set = set(datetime_idx)
|
|
416
|
+
numeric_idx = [
|
|
417
|
+
idx
|
|
418
|
+
for idx in range(n_cols)
|
|
419
|
+
if idx not in datetime_set
|
|
420
|
+
and self._should_compare_numeric(left_series[idx], right_series[idx])
|
|
421
|
+
]
|
|
422
|
+
numeric_set = set(numeric_idx)
|
|
423
|
+
string_idx = [
|
|
424
|
+
idx
|
|
425
|
+
for idx in range(n_cols)
|
|
426
|
+
if idx not in datetime_set and idx not in numeric_set
|
|
427
|
+
]
|
|
428
|
+
common = common_mask[:, None]
|
|
429
|
+
|
|
430
|
+
if datetime_idx:
|
|
431
|
+
left_dt = np.column_stack(
|
|
432
|
+
[self._datetime_seconds(left_series[idx]) for idx in datetime_idx]
|
|
433
|
+
)
|
|
434
|
+
right_dt = np.column_stack(
|
|
435
|
+
[self._datetime_seconds(right_series[idx]) for idx in datetime_idx]
|
|
436
|
+
)
|
|
437
|
+
block_diff = left_dt - right_dt
|
|
438
|
+
block_abs = np.abs(block_diff)
|
|
439
|
+
left_null = ~np.isfinite(left_dt)
|
|
440
|
+
right_null = ~np.isfinite(right_dt)
|
|
441
|
+
tolerances = np.asarray(
|
|
442
|
+
[self._column_datetime_tol(columns[idx]) for idx in datetime_idx],
|
|
443
|
+
dtype=float,
|
|
444
|
+
)
|
|
445
|
+
block_one = (left_null ^ right_null) & common
|
|
446
|
+
block_both = left_null & right_null & common
|
|
447
|
+
block_mismatch = (block_abs > tolerances[None, :]) | block_one
|
|
448
|
+
if not self.config.both_null_equal:
|
|
449
|
+
block_mismatch |= block_both
|
|
450
|
+
block_mismatch &= common
|
|
451
|
+
diff[:, datetime_idx] = block_diff
|
|
452
|
+
abs_diff[:, datetime_idx] = block_abs
|
|
453
|
+
one_null[:, datetime_idx] = block_one
|
|
454
|
+
both_null[:, datetime_idx] = block_both
|
|
455
|
+
mismatch[:, datetime_idx] = block_mismatch
|
|
456
|
+
|
|
457
|
+
if numeric_idx:
|
|
458
|
+
left_num = np.column_stack(
|
|
459
|
+
[pd.to_numeric(left_series[idx], errors="coerce").to_numpy(dtype=float, na_value=np.nan) for idx in numeric_idx]
|
|
460
|
+
)
|
|
461
|
+
right_num = np.column_stack(
|
|
462
|
+
[pd.to_numeric(right_series[idx], errors="coerce").to_numpy(dtype=float, na_value=np.nan) for idx in numeric_idx]
|
|
463
|
+
)
|
|
464
|
+
block_diff = left_num - right_num
|
|
465
|
+
block_abs = np.abs(block_diff)
|
|
466
|
+
left_null = ~np.isfinite(left_num)
|
|
467
|
+
right_null = ~np.isfinite(right_num)
|
|
468
|
+
tolerances = np.asarray(
|
|
469
|
+
[self._column_numeric_tol(columns[idx]) for idx in numeric_idx],
|
|
470
|
+
dtype=float,
|
|
471
|
+
)
|
|
472
|
+
limit = tolerances[:, 0][None, :] + tolerances[:, 1][None, :] * np.abs(right_num)
|
|
473
|
+
block_one = (left_null ^ right_null) & common
|
|
474
|
+
block_both = left_null & right_null & common
|
|
475
|
+
block_mismatch = (block_abs > limit) | block_one
|
|
476
|
+
if not self.config.both_null_equal:
|
|
477
|
+
block_mismatch |= block_both
|
|
478
|
+
block_mismatch &= common
|
|
479
|
+
diff[:, numeric_idx] = block_diff
|
|
480
|
+
abs_diff[:, numeric_idx] = block_abs
|
|
481
|
+
one_null[:, numeric_idx] = block_one
|
|
482
|
+
both_null[:, numeric_idx] = block_both
|
|
483
|
+
mismatch[:, numeric_idx] = block_mismatch
|
|
484
|
+
|
|
485
|
+
if string_idx:
|
|
486
|
+
left_null = np.column_stack([left_series[idx].isna().to_numpy() for idx in string_idx])
|
|
487
|
+
right_null = np.column_stack([right_series[idx].isna().to_numpy() for idx in string_idx])
|
|
488
|
+
left_text = np.column_stack(
|
|
489
|
+
[left_series[idx].astype("string").fillna("").astype(str).to_numpy() for idx in string_idx]
|
|
490
|
+
)
|
|
491
|
+
right_text = np.column_stack(
|
|
492
|
+
[right_series[idx].astype("string").fillna("").astype(str).to_numpy() for idx in string_idx]
|
|
493
|
+
)
|
|
494
|
+
block_one = (left_null ^ right_null) & common
|
|
495
|
+
block_both = left_null & right_null & common
|
|
496
|
+
block_mismatch = (left_text != right_text) | block_one
|
|
497
|
+
if self.config.both_null_equal:
|
|
498
|
+
block_mismatch &= ~block_both
|
|
499
|
+
else:
|
|
500
|
+
block_mismatch |= block_both
|
|
501
|
+
block_mismatch &= common
|
|
502
|
+
one_null[:, string_idx] = block_one
|
|
503
|
+
both_null[:, string_idx] = block_both
|
|
504
|
+
mismatch[:, string_idx] = block_mismatch
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
"mismatch": mismatch,
|
|
508
|
+
"one_null": one_null,
|
|
509
|
+
"both_null": both_null,
|
|
510
|
+
"diff": diff,
|
|
511
|
+
"abs_diff": abs_diff,
|
|
512
|
+
"left_values": left_values,
|
|
513
|
+
"right_values": right_values,
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
@staticmethod
|
|
517
|
+
def _datetime_seconds(series: pd.Series) -> np.ndarray:
|
|
518
|
+
values = ProcCompareEngine._to_datetime_series(series)
|
|
519
|
+
missing = values.isna().to_numpy()
|
|
520
|
+
result = values.astype("int64").to_numpy(dtype=float) / 1_000_000_000.0
|
|
521
|
+
result[missing] = np.nan
|
|
522
|
+
return result
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _to_datetime_series(series: pd.Series) -> pd.Series:
|
|
526
|
+
try:
|
|
527
|
+
return pd.to_datetime(series, errors="coerce", format="mixed")
|
|
528
|
+
except (TypeError, ValueError):
|
|
529
|
+
return pd.to_datetime(series, errors="coerce")
|
|
530
|
+
|
|
378
531
|
def _compare_series(
|
|
379
532
|
self,
|
|
380
533
|
col: str,
|
|
@@ -391,8 +544,8 @@ class ProcCompareEngine:
|
|
|
391
544
|
abs_diff = pd.Series(np.nan, index=left.index, dtype="float64")
|
|
392
545
|
|
|
393
546
|
if self._should_compare_datetime(col, left, right):
|
|
394
|
-
left_dt =
|
|
395
|
-
right_dt =
|
|
547
|
+
left_dt = self._to_datetime_series(left)
|
|
548
|
+
right_dt = self._to_datetime_series(right)
|
|
396
549
|
diff = (left_dt - right_dt).dt.total_seconds()
|
|
397
550
|
abs_diff = diff.abs()
|
|
398
551
|
tol = self._column_datetime_tol(col)
|
|
@@ -57,13 +57,7 @@ class kSeries(Series):
|
|
|
57
57
|
"""
|
|
58
58
|
Scale the model scores (for internt segment of MCI model)
|
|
59
59
|
"""
|
|
60
|
-
|
|
61
|
-
def app_func(x):
|
|
62
|
-
scores = x * 1.112
|
|
63
|
-
if (scores > 0.9999999):
|
|
64
|
-
return 0.9999999
|
|
65
|
-
return scores
|
|
66
|
-
return data.apply(app_func)
|
|
60
|
+
return (self * 1.112).clip(upper=0.9999999)
|
|
67
61
|
|
|
68
62
|
def proc_freq(self) -> pd.DataFrame:
|
|
69
63
|
"""
|
|
@@ -128,8 +122,12 @@ class kDataFrame(DataFrame):
|
|
|
128
122
|
data = self
|
|
129
123
|
else:
|
|
130
124
|
data = self.copy()
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
data[vintage_colname] = (
|
|
126
|
+
data[by]
|
|
127
|
+
.astype("string")
|
|
128
|
+
.str.extract(r"(\d{4}-\d{2})", expand=False)
|
|
129
|
+
.str.replace("-", "", regex=False)
|
|
130
|
+
)
|
|
133
131
|
if inplace:
|
|
134
132
|
self = data
|
|
135
133
|
return data
|
|
@@ -165,13 +163,7 @@ class kDataFrame(DataFrame):
|
|
|
165
163
|
"""
|
|
166
164
|
Scale the model scores (for internt segment of MCI model)
|
|
167
165
|
"""
|
|
168
|
-
|
|
169
|
-
def app_func(x):
|
|
170
|
-
scores = x * 1.112
|
|
171
|
-
if (scores > 0.9999999):
|
|
172
|
-
return 0.9999999
|
|
173
|
-
return scores
|
|
174
|
-
return data[pb_score].apply(app_func)
|
|
166
|
+
return (self[pb_score] * 1.112).clip(upper=0.9999999)
|
|
175
167
|
|
|
176
168
|
def proc_freq(self, var: str):
|
|
177
169
|
"""
|
|
@@ -342,8 +342,12 @@ def convert_to_vintage(data, vintage_colname = 'VINTAGE', by = 'TRAN_TMS', retur
|
|
|
342
342
|
>>> df = pd.DataFrame({'TRAN_TMS': ['2025-03-15 10:00:00', '2025-03-20 11:00:00']})
|
|
343
343
|
>>> convert_to_vintage(df)
|
|
344
344
|
"""
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
data[vintage_colname] = (
|
|
346
|
+
data[by]
|
|
347
|
+
.astype("string")
|
|
348
|
+
.str.extract(r"(\d{4}-\d{2})", expand=False)
|
|
349
|
+
.str.replace("-", "", regex=False)
|
|
350
|
+
)
|
|
347
351
|
|
|
348
352
|
if return_kDF:
|
|
349
353
|
return kDataFrame(data)
|
|
@@ -1052,10 +1056,9 @@ def bool_to_str(data):
|
|
|
1052
1056
|
>>> bool_to_str(df)
|
|
1053
1057
|
"""
|
|
1054
1058
|
dfc = data.copy()
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
dfc = dfc.astype({k: str})
|
|
1059
|
+
bool_cols = [col for col in data.columns if pd.api.types.is_bool_dtype(data[col])]
|
|
1060
|
+
if bool_cols:
|
|
1061
|
+
dfc[bool_cols] = dfc[bool_cols].astype(str)
|
|
1059
1062
|
return dfc
|
|
1060
1063
|
|
|
1061
1064
|
|
|
@@ -2670,4 +2673,4 @@ def get_feature_names_batch(models, model_type=None):
|
|
|
2670
2673
|
elif isinstance(models, list):
|
|
2671
2674
|
return [get_feature_names(model, model_type=model_type) for model in models]
|
|
2672
2675
|
else:
|
|
2673
|
-
raise TypeError("models参数应为dict或list类型")
|
|
2676
|
+
raise TypeError("models参数应为dict或list类型")
|
{supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/Model_Eval_Tool.py
RENAMED
|
@@ -88,51 +88,31 @@ def _get_gains_table_scr(data, score, dep, nbins = 10, precision = 5,
|
|
|
88
88
|
bin_colnames = ("_bin_num", "_bin_range"),
|
|
89
89
|
ascending = ascending)
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
lift = avg_bad / res[dep].mean()
|
|
117
|
-
prop = n / grand_total
|
|
118
|
-
|
|
119
|
-
# 返回Series
|
|
120
|
-
return pd.Series({
|
|
121
|
-
'MIN': min_val,
|
|
122
|
-
'MAX': max_val,
|
|
123
|
-
'N': n,
|
|
124
|
-
'PROP': prop,
|
|
125
|
-
'PERF_CNT': perf_cnt,
|
|
126
|
-
'AVG_SCORE': avg_score,
|
|
127
|
-
'UNIQUE_SCORE': unique_score,
|
|
128
|
-
'AVG_BAD': avg_bad,
|
|
129
|
-
'AVG_GOOD': avg_good,
|
|
130
|
-
'N_BAD': n_bad,
|
|
131
|
-
'N_GOOD': n_good,
|
|
132
|
-
'LIFT': lift
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
gains_table = res.groupby(["_bin_num", "_bin_range"], dropna=False).apply(_compute_gains_tmp)
|
|
91
|
+
res = res.copy()
|
|
92
|
+
res["_smf_bad_ind"] = res[dep].eq(1).astype(np.int64)
|
|
93
|
+
res["_smf_good_ind"] = res[dep].eq(0).astype(np.int64)
|
|
94
|
+
grouped = res.groupby(["_bin_num", "_bin_range"], dropna=False)
|
|
95
|
+
gains_table = grouped.agg(
|
|
96
|
+
MIN=(score, "min"),
|
|
97
|
+
MAX=(score, "max"),
|
|
98
|
+
N=(dep, "size"),
|
|
99
|
+
PERF_CNT=(dep, "count"),
|
|
100
|
+
AVG_SCORE=(score, "mean"),
|
|
101
|
+
UNIQUE_SCORE=(score, "nunique"),
|
|
102
|
+
N_BAD=("_smf_bad_ind", "sum"),
|
|
103
|
+
N_GOOD=("_smf_good_ind", "sum"),
|
|
104
|
+
)
|
|
105
|
+
perf_denom = gains_table["PERF_CNT"].replace(0, np.nan)
|
|
106
|
+
gains_table["PROP"] = gains_table["N"] / max(len(res), 1)
|
|
107
|
+
gains_table["AVG_BAD"] = (gains_table["N_BAD"] / perf_denom).fillna(0.0)
|
|
108
|
+
gains_table["AVG_GOOD"] = (gains_table["N_GOOD"] / perf_denom).fillna(0.0)
|
|
109
|
+
gains_table["LIFT"] = gains_table["AVG_BAD"] / res[dep].mean()
|
|
110
|
+
gains_table = gains_table[
|
|
111
|
+
[
|
|
112
|
+
"MIN", "MAX", "N", "PROP", "PERF_CNT", "AVG_SCORE",
|
|
113
|
+
"UNIQUE_SCORE", "AVG_BAD", "AVG_GOOD", "N_BAD", "N_GOOD", "LIFT",
|
|
114
|
+
]
|
|
115
|
+
]
|
|
136
116
|
|
|
137
117
|
gains_table["BAD_PCT_IN_EACH_BIN"] = gains_table["N_BAD"] / gains_table["N_BAD"].sum()
|
|
138
118
|
gains_table["GOOD_PCT_IN_EACH_BIN"] = gains_table["N_GOOD"] / gains_table["N_GOOD"].sum()
|
|
@@ -146,7 +126,7 @@ def _get_gains_table_scr(data, score, dep, nbins = 10, precision = 5,
|
|
|
146
126
|
|
|
147
127
|
|
|
148
128
|
gains_table["TRUE_BAD_SHIFT"] = (gains_table['AVG_BAD'].shift(1) / gains_table['AVG_BAD'] - 1) if not ascending else (gains_table['AVG_BAD'] / gains_table['AVG_BAD'].shift(1) - 1)
|
|
149
|
-
gains_table["RANK_ORDER_BUMP"] = gains_table["TRUE_BAD_SHIFT"].
|
|
129
|
+
gains_table["RANK_ORDER_BUMP"] = gains_table["TRUE_BAD_SHIFT"].lt(0).astype(int)
|
|
150
130
|
|
|
151
131
|
gains_table["WOE"] = calc_woe(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")
|
|
152
132
|
gains_table["IV"] = calc_iv(data = gains_table, bad_pct = "BAD_PCT_IN_EACH_BIN", good_pct = "GOOD_PCT_IN_EACH_BIN")
|
{supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/evaluate_model.py
RENAMED
|
@@ -648,25 +648,46 @@ def __agg(df):
|
|
|
648
648
|
sum_score=pd.NamedAgg(column='y_score', aggfunc='sum'),
|
|
649
649
|
).reset_index()
|
|
650
650
|
df_agg[['n', 'sum_true', 'sum_score']] = df_agg[['n', 'sum_true', 'sum_score']].fillna(0)
|
|
651
|
-
df_agg.loc[:, 'proportion'] =
|
|
652
|
-
|
|
651
|
+
df_agg.loc[:, 'proportion'] = np.divide(
|
|
652
|
+
df_agg['n'].to_numpy(dtype=float),
|
|
653
|
+
float(N),
|
|
654
|
+
out=np.full(len(df_agg), np.nan),
|
|
655
|
+
where=N > 0,
|
|
656
|
+
)
|
|
657
|
+
df_agg.loc[:, 'capture_rate'] = np.divide(
|
|
658
|
+
df_agg['sum_true'].to_numpy(dtype=float),
|
|
659
|
+
float(N1),
|
|
660
|
+
out=np.full(len(df_agg), np.nan),
|
|
661
|
+
where=N1 > 0,
|
|
662
|
+
)
|
|
653
663
|
|
|
654
664
|
if 'y_group' in df_agg.columns:
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
665
|
+
cumulative = df_agg.groupby('y_group', sort=False)[
|
|
666
|
+
['n', 'proportion', 'sum_true', 'sum_score']
|
|
667
|
+
].cumsum()
|
|
668
|
+
cumulative.columns = [
|
|
669
|
+
'cumsum_n', 'cumsum_proportion', 'cumsum_true', 'cumsum_score'
|
|
670
|
+
]
|
|
671
|
+
df_agg.loc[:, cumulative.columns] = cumulative.to_numpy()
|
|
662
672
|
else:
|
|
663
673
|
df_agg.loc[:, 'cumsum_n'] = np.cumsum(df_agg['n'])
|
|
664
674
|
df_agg.loc[:, 'cumsum_proportion'] = np.cumsum(df_agg['proportion'])
|
|
665
675
|
df_agg.loc[:, 'cumsum_true'] = np.cumsum(df_agg['sum_true'])
|
|
666
676
|
df_agg.loc[:, 'cumsum_score'] = np.cumsum(df_agg['sum_score'])
|
|
667
677
|
|
|
668
|
-
|
|
669
|
-
df_agg.loc[:, '
|
|
678
|
+
cumulative_n = df_agg['cumsum_n'].to_numpy(dtype=float)
|
|
679
|
+
df_agg.loc[:, 'cumavg_true'] = np.divide(
|
|
680
|
+
df_agg['cumsum_true'].to_numpy(dtype=float),
|
|
681
|
+
cumulative_n,
|
|
682
|
+
out=np.full(len(df_agg), np.nan),
|
|
683
|
+
where=cumulative_n > 0,
|
|
684
|
+
)
|
|
685
|
+
df_agg.loc[:, 'cumavg_score'] = np.divide(
|
|
686
|
+
df_agg['cumsum_score'].to_numpy(dtype=float),
|
|
687
|
+
cumulative_n,
|
|
688
|
+
out=np.full(len(df_agg), np.nan),
|
|
689
|
+
where=cumulative_n > 0,
|
|
690
|
+
)
|
|
670
691
|
|
|
671
692
|
columns = group_cols + ['min_score', 'max_score', 'n', 'proportion', 'sum_true', 'sum_score', 'avg_true', 'avg_score', 'capture_rate',
|
|
672
693
|
'cumsum_n', 'cumsum_proportion', 'cumsum_true', 'cumsum_score', 'cumavg_true', 'cumavg_score', ]
|
|
@@ -794,20 +815,28 @@ def calc_equid_pct(y_true, y_score, y_group=None, bins=10, ascending=True, sampl
|
|
|
794
815
|
binsize = int(size / bins) # 向下取整
|
|
795
816
|
indices = np.argsort(y_score) if ascending else np.argsort(y_score)[::-1]
|
|
796
817
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
818
|
+
# Keep the historical integer percentile labels while assigning them in
|
|
819
|
+
# one pass. For bin counts that do not divide 100, the legacy code
|
|
820
|
+
# truncated labels during assignment to its integer array.
|
|
821
|
+
thresholds = np.zeros(size, dtype=int)
|
|
822
|
+
if binsize > 0:
|
|
823
|
+
sorted_bin = np.minimum(np.arange(size) // binsize, bins - 1)
|
|
824
|
+
else:
|
|
825
|
+
sorted_bin = np.full(size, bins - 1, dtype=int)
|
|
826
|
+
thresholds[indices] = 100.0 * (sorted_bin + 1) / bins
|
|
827
|
+
if y_group is not None:
|
|
804
828
|
df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'y_group': y_group, 'thresholds': thresholds})
|
|
805
829
|
else:
|
|
806
830
|
df = pd.DataFrame({'y_true': y_true, 'y_score': y_score, 'thresholds': thresholds})
|
|
807
831
|
pct_df = __agg(df)
|
|
808
832
|
|
|
809
833
|
avg_true = np.mean(y_true)
|
|
810
|
-
pct_df['lift'] =
|
|
834
|
+
pct_df['lift'] = np.divide(
|
|
835
|
+
pct_df['cumavg_true'].to_numpy(dtype=float),
|
|
836
|
+
float(avg_true),
|
|
837
|
+
out=np.full(len(pct_df), np.nan),
|
|
838
|
+
where=avg_true != 0,
|
|
839
|
+
)
|
|
811
840
|
pct_df['gain'] = np.cumsum(pct_df['capture_rate'])
|
|
812
841
|
|
|
813
842
|
return pct_df
|
{supermodelingfactory-0.5.10 → supermodelingfactory-0.6.0}/Modeling_Tool/Eval/weighted_eval_utils.py
RENAMED
|
@@ -124,6 +124,7 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
|
|
|
124
124
|
df["_w"] = weight
|
|
125
125
|
df["_bad_w"] = weight * y
|
|
126
126
|
df["_good_w"] = weight * (1.0 - y)
|
|
127
|
+
df["_score_w"] = weight * s
|
|
127
128
|
grouped = df.groupby(["_bin_num", "_bin_range"], sort=True, dropna=False)
|
|
128
129
|
out = grouped.agg(
|
|
129
130
|
MIN=(score, "min"),
|
|
@@ -133,9 +134,19 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
|
|
|
133
134
|
PERF_CNT=("_w", "sum"),
|
|
134
135
|
N_BAD=("_bad_w", "sum"),
|
|
135
136
|
N_GOOD=("_good_w", "sum"),
|
|
136
|
-
|
|
137
|
+
SCORE_W=("_score_w", "sum"),
|
|
138
|
+
SCORE_COUNT=(score, "count"),
|
|
137
139
|
UNIQUE_SCORE=(score, "nunique"),
|
|
138
140
|
)
|
|
141
|
+
out["AVG_SCORE"] = out["SCORE_W"] / out["N"].replace(0, np.nan)
|
|
142
|
+
out.loc[out["SCORE_COUNT"].ne(out["N_RAW"]), "AVG_SCORE"] = np.nan
|
|
143
|
+
out = out.drop(columns=["SCORE_W", "SCORE_COUNT"])
|
|
144
|
+
out = out[
|
|
145
|
+
[
|
|
146
|
+
"MIN", "MAX", "N", "N_RAW", "PERF_CNT", "N_BAD",
|
|
147
|
+
"N_GOOD", "AVG_SCORE", "UNIQUE_SCORE",
|
|
148
|
+
]
|
|
149
|
+
]
|
|
139
150
|
|
|
140
151
|
total_weight = float(out["N"].sum()) or 1.0
|
|
141
152
|
total_bad = float(out["N_BAD"].sum()) or 1.0
|
|
@@ -155,7 +166,7 @@ def get_gains_table(data, dep, score, nbins=10, weight_col=None, weighted_binnin
|
|
|
155
166
|
out["KS"] = out["KS_PER_BIN"]
|
|
156
167
|
out["LIFT"] = out["AVG_BAD"] / overall_bad_rate if overall_bad_rate else np.nan
|
|
157
168
|
out["TRUE_BAD_SHIFT"] = out["AVG_BAD"].shift(1) / out["AVG_BAD"] - 1
|
|
158
|
-
out["RANK_ORDER_BUMP"] = out["TRUE_BAD_SHIFT"].
|
|
169
|
+
out["RANK_ORDER_BUMP"] = out["TRUE_BAD_SHIFT"].lt(0).astype(int)
|
|
159
170
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
160
171
|
out["WOE"] = np.log(out["BAD_PCT_IN_EACH_BIN"] / out["GOOD_PCT_IN_EACH_BIN"])
|
|
161
172
|
out["WOE"] = out["WOE"].replace([np.inf, -np.inf], 0).fillna(0)
|
|
@@ -8,6 +8,7 @@ accepted by ``shap.maskers.Partition``.
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
from collections import Counter
|
|
11
12
|
from typing import Dict, Iterable, List, Mapping, Optional, Sequence
|
|
12
13
|
|
|
13
14
|
import numpy as np
|
|
@@ -248,7 +249,7 @@ def validate_groups(groups: Mapping[str, Sequence[str]], features: Sequence[str]
|
|
|
248
249
|
feature_set = set(features)
|
|
249
250
|
missing = feature_set - set(all_assigned)
|
|
250
251
|
unknown = set(all_assigned) - feature_set
|
|
251
|
-
duplicated = {feat for feat in all_assigned if
|
|
252
|
+
duplicated = {feat for feat, count in Counter(all_assigned).items() if count > 1}
|
|
252
253
|
ok = not missing and not duplicated and not unknown
|
|
253
254
|
if not ok and raise_error:
|
|
254
255
|
parts = []
|
|
@@ -316,10 +317,9 @@ def groups_to_shap_clustering(
|
|
|
316
317
|
|
|
317
318
|
for feats in groups.values():
|
|
318
319
|
idxs = [feat_idx[feat] for feat in feats if feat in feat_idx]
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
dist_mat[i, j] = float(intra_dist)
|
|
320
|
+
if idxs:
|
|
321
|
+
dist_mat[np.ix_(idxs, idxs)] = float(intra_dist)
|
|
322
|
+
dist_mat[idxs, idxs] = 0.0
|
|
323
323
|
|
|
324
324
|
condensed = squareform(dist_mat, checks=False)
|
|
325
325
|
return linkage(condensed, method="complete")
|