SuperModelingFactory 0.3.5__tar.gz → 0.3.7__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.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/Model_Explainer.py +159 -4
- supermodelingfactory-0.3.7/Modeling_Tool/Pipeline/_common.py +366 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/credit_model.py +116 -8
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/feature_validation.py +17 -1
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/__init__.py +1 -1
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/PKG-INFO +7 -4
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/README.md +6 -3
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/PKG-INFO +7 -4
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/pyproject.toml +1 -1
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/setup.py +1 -1
- supermodelingfactory-0.3.5/Modeling_Tool/Pipeline/_common.py +0 -132
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/ExcelFormatTool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/ExcelMaster.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/Template.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/Utility.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/LICENSE +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/MANIFEST.in +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Binning_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Proc_Compare.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Slope_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/kDataFrame.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/utils.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/evaluate_model.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/Backward_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/LRM_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Sample_Split.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/UAT/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Master.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/simsun.ttc +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Report/Report_Tool.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Report/__init__.py +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/requires.txt +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/top_level.txt +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/requirements.txt +0 -0
- {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/setup.cfg +0 -0
|
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|
|
22
22
|
import ast
|
|
23
23
|
import importlib.metadata
|
|
24
24
|
import sys
|
|
25
|
+
import warnings
|
|
25
26
|
|
|
26
27
|
import numpy as np
|
|
27
28
|
import pandas as pd
|
|
@@ -848,6 +849,125 @@ class ModelExplainer:
|
|
|
848
849
|
# ------------------------------------------------------------------ #
|
|
849
850
|
# LIME
|
|
850
851
|
# ------------------------------------------------------------------ #
|
|
852
|
+
@staticmethod
|
|
853
|
+
def _lime_missing_mask(frame: pd.DataFrame) -> pd.Series:
|
|
854
|
+
numeric = frame.select_dtypes(include=[np.number])
|
|
855
|
+
if numeric.empty:
|
|
856
|
+
return pd.Series(False, index=frame.index)
|
|
857
|
+
return numeric.isna().any(axis=1)
|
|
858
|
+
|
|
859
|
+
@classmethod
|
|
860
|
+
def _warn_lime_missing(
|
|
861
|
+
cls,
|
|
862
|
+
frame: pd.DataFrame,
|
|
863
|
+
*,
|
|
864
|
+
role: str,
|
|
865
|
+
missing_strategy: str,
|
|
866
|
+
n_before: int,
|
|
867
|
+
n_after: int,
|
|
868
|
+
fill_values: dict[str, float] | None = None,
|
|
869
|
+
) -> None:
|
|
870
|
+
numeric = frame.select_dtypes(include=[np.number])
|
|
871
|
+
if numeric.empty:
|
|
872
|
+
return
|
|
873
|
+
null_cols = [col for col in numeric.columns if numeric[col].isna().any()]
|
|
874
|
+
if not null_cols:
|
|
875
|
+
return
|
|
876
|
+
details = []
|
|
877
|
+
for col in null_cols:
|
|
878
|
+
count = int(numeric[col].isna().sum())
|
|
879
|
+
pct = 100.0 * count / len(frame) if len(frame) else 0.0
|
|
880
|
+
detail = f"{col}: {count}/{len(frame)} ({pct:.1f}%)"
|
|
881
|
+
if fill_values and col in fill_values:
|
|
882
|
+
detail += f", median_fill={fill_values[col]:.6g}"
|
|
883
|
+
details.append(detail)
|
|
884
|
+
msg = (
|
|
885
|
+
f"LIME missing_strategy={missing_strategy!r} on {role}: "
|
|
886
|
+
f"null columns={null_cols}; " + "; ".join(details)
|
|
887
|
+
)
|
|
888
|
+
if missing_strategy == "drop":
|
|
889
|
+
msg += f"; rows {n_before} -> {n_after}"
|
|
890
|
+
warnings.warn(msg, UserWarning, stacklevel=3)
|
|
891
|
+
|
|
892
|
+
@classmethod
|
|
893
|
+
def _prepare_lime_data(
|
|
894
|
+
cls,
|
|
895
|
+
train: pd.DataFrame,
|
|
896
|
+
rows: pd.DataFrame | None = None,
|
|
897
|
+
*,
|
|
898
|
+
train_role: str = "X_train",
|
|
899
|
+
rows_role: str = "x_row",
|
|
900
|
+
missing_strategy: str = "median",
|
|
901
|
+
) -> tuple[pd.DataFrame, pd.DataFrame | None]:
|
|
902
|
+
if missing_strategy not in {"median", "drop"}:
|
|
903
|
+
raise ValueError("missing_strategy must be 'median' or 'drop'")
|
|
904
|
+
|
|
905
|
+
train_clean = train.copy()
|
|
906
|
+
rows_clean = rows.copy() if rows is not None else None
|
|
907
|
+
numeric_cols = list(train_clean.select_dtypes(include=[np.number]).columns)
|
|
908
|
+
|
|
909
|
+
if missing_strategy == "median":
|
|
910
|
+
fill_values: dict[str, float] = {}
|
|
911
|
+
for col in numeric_cols:
|
|
912
|
+
if train_clean[col].isna().any() or (
|
|
913
|
+
rows_clean is not None and rows_clean[col].isna().any()
|
|
914
|
+
):
|
|
915
|
+
median_val = float(train_clean[col].median())
|
|
916
|
+
fill_values[col] = median_val
|
|
917
|
+
train_clean[col] = train_clean[col].fillna(median_val)
|
|
918
|
+
if rows_clean is not None:
|
|
919
|
+
rows_clean[col] = rows_clean[col].fillna(median_val)
|
|
920
|
+
cls._warn_lime_missing(
|
|
921
|
+
train,
|
|
922
|
+
role=train_role,
|
|
923
|
+
missing_strategy=missing_strategy,
|
|
924
|
+
n_before=len(train),
|
|
925
|
+
n_after=len(train_clean),
|
|
926
|
+
fill_values=fill_values or None,
|
|
927
|
+
)
|
|
928
|
+
if rows_clean is not None:
|
|
929
|
+
cls._warn_lime_missing(
|
|
930
|
+
rows,
|
|
931
|
+
role=rows_role,
|
|
932
|
+
missing_strategy=missing_strategy,
|
|
933
|
+
n_before=len(rows),
|
|
934
|
+
n_after=len(rows_clean),
|
|
935
|
+
fill_values=fill_values or None,
|
|
936
|
+
)
|
|
937
|
+
return train_clean, rows_clean
|
|
938
|
+
|
|
939
|
+
n_train_before = len(train_clean)
|
|
940
|
+
train_clean = train_clean.loc[~cls._lime_missing_mask(train_clean)].copy()
|
|
941
|
+
cls._warn_lime_missing(
|
|
942
|
+
train,
|
|
943
|
+
role=train_role,
|
|
944
|
+
missing_strategy=missing_strategy,
|
|
945
|
+
n_before=n_train_before,
|
|
946
|
+
n_after=len(train_clean),
|
|
947
|
+
)
|
|
948
|
+
if train_clean.empty:
|
|
949
|
+
raise ValueError(
|
|
950
|
+
f"LIME missing_strategy='drop' removed all rows from {train_role} "
|
|
951
|
+
f"({n_train_before} -> 0). Provide complete training data or use missing_strategy='median'."
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
if rows_clean is not None:
|
|
955
|
+
n_rows_before = len(rows_clean)
|
|
956
|
+
rows_clean = rows_clean.loc[~cls._lime_missing_mask(rows_clean)].copy()
|
|
957
|
+
cls._warn_lime_missing(
|
|
958
|
+
rows,
|
|
959
|
+
role=rows_role,
|
|
960
|
+
missing_strategy=missing_strategy,
|
|
961
|
+
n_before=n_rows_before,
|
|
962
|
+
n_after=len(rows_clean),
|
|
963
|
+
)
|
|
964
|
+
if rows_clean.empty:
|
|
965
|
+
raise ValueError(
|
|
966
|
+
f"LIME missing_strategy='drop' removed all rows from {rows_role} "
|
|
967
|
+
f"({n_rows_before} -> 0). Impute missing values or use missing_strategy='median'."
|
|
968
|
+
)
|
|
969
|
+
return train_clean, rows_clean
|
|
970
|
+
|
|
851
971
|
def _build_lime_explainer(self, X_train, num_features=None, random_state=None, **lime_kwargs):
|
|
852
972
|
LimeTabularExplainer = _lazy_lime()
|
|
853
973
|
train = self._as_frame(X_train if X_train is not None else self.background_data)
|
|
@@ -864,7 +984,16 @@ class ModelExplainer:
|
|
|
864
984
|
**lime_kwargs,
|
|
865
985
|
)
|
|
866
986
|
|
|
867
|
-
def lime_explain_instance(
|
|
987
|
+
def lime_explain_instance(
|
|
988
|
+
self,
|
|
989
|
+
x_row,
|
|
990
|
+
X_train=None,
|
|
991
|
+
num_features=10,
|
|
992
|
+
num_samples=5000,
|
|
993
|
+
random_state=None,
|
|
994
|
+
missing_strategy="median",
|
|
995
|
+
**lime_kwargs,
|
|
996
|
+
):
|
|
868
997
|
"""Explain one sample with LIME."""
|
|
869
998
|
if isinstance(x_row, pd.Series):
|
|
870
999
|
x_row = x_row.to_frame().T
|
|
@@ -874,7 +1003,14 @@ class ModelExplainer:
|
|
|
874
1003
|
if frame.shape[0] != 1:
|
|
875
1004
|
frame = frame.iloc[[0]]
|
|
876
1005
|
|
|
877
|
-
|
|
1006
|
+
train_raw = self._as_frame(X_train if X_train is not None else self.background_data)
|
|
1007
|
+
train, frame = self._prepare_lime_data(
|
|
1008
|
+
train_raw,
|
|
1009
|
+
frame,
|
|
1010
|
+
train_role="X_train",
|
|
1011
|
+
rows_role="x_row",
|
|
1012
|
+
missing_strategy=missing_strategy,
|
|
1013
|
+
)
|
|
878
1014
|
explainer = self._build_lime_explainer(train, num_features, random_state, **lime_kwargs)
|
|
879
1015
|
explanation = explainer.explain_instance(
|
|
880
1016
|
data_row=np.asarray(frame.iloc[0]),
|
|
@@ -896,17 +1032,36 @@ class ModelExplainer:
|
|
|
896
1032
|
out.attrs["score"] = explanation.score
|
|
897
1033
|
return out
|
|
898
1034
|
|
|
899
|
-
def lime_global_importance(
|
|
1035
|
+
def lime_global_importance(
|
|
1036
|
+
self,
|
|
1037
|
+
X,
|
|
1038
|
+
X_train=None,
|
|
1039
|
+
num_features=10,
|
|
1040
|
+
num_samples=2000,
|
|
1041
|
+
sample_size=100,
|
|
1042
|
+
random_state=None,
|
|
1043
|
+
missing_strategy="median",
|
|
1044
|
+
**lime_kwargs,
|
|
1045
|
+
):
|
|
900
1046
|
"""Aggregate LIME local weights across a sample as global importance."""
|
|
1047
|
+
train_raw = self._as_frame(X_train if X_train is not None else self.background_data)
|
|
901
1048
|
frame = self._sample_frame(X, sample_size=sample_size, random_state=random_state)
|
|
1049
|
+
train, frame = self._prepare_lime_data(
|
|
1050
|
+
train_raw,
|
|
1051
|
+
frame,
|
|
1052
|
+
train_role="X_train",
|
|
1053
|
+
rows_role="X",
|
|
1054
|
+
missing_strategy=missing_strategy,
|
|
1055
|
+
)
|
|
902
1056
|
rows = []
|
|
903
1057
|
for _, row in frame.iterrows():
|
|
904
1058
|
local = self.lime_explain_instance(
|
|
905
1059
|
row,
|
|
906
|
-
X_train=
|
|
1060
|
+
X_train=train,
|
|
907
1061
|
num_features=num_features,
|
|
908
1062
|
num_samples=num_samples,
|
|
909
1063
|
random_state=random_state,
|
|
1064
|
+
missing_strategy="median",
|
|
910
1065
|
**lime_kwargs,
|
|
911
1066
|
)
|
|
912
1067
|
rows.append(local)
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import fields, is_dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable, Mapping
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
_QUERY_COLUMN_RE = re.compile(r"(?<!['\"])\b([A-Za-z_][\w]*)\b(?![\'\"])")
|
|
13
|
+
_QUERY_RESERVED = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"and",
|
|
16
|
+
"or",
|
|
17
|
+
"not",
|
|
18
|
+
"True",
|
|
19
|
+
"False",
|
|
20
|
+
"None",
|
|
21
|
+
"in",
|
|
22
|
+
"is",
|
|
23
|
+
"where",
|
|
24
|
+
"if",
|
|
25
|
+
"else",
|
|
26
|
+
"lambda",
|
|
27
|
+
"abs",
|
|
28
|
+
"all",
|
|
29
|
+
"any",
|
|
30
|
+
"max",
|
|
31
|
+
"min",
|
|
32
|
+
"sum",
|
|
33
|
+
"len",
|
|
34
|
+
"round",
|
|
35
|
+
"float",
|
|
36
|
+
"int",
|
|
37
|
+
"str",
|
|
38
|
+
"bool",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def make_dirs(*paths: str | os.PathLike | None) -> None:
|
|
44
|
+
for path in paths:
|
|
45
|
+
if path:
|
|
46
|
+
os.makedirs(str(path), exist_ok=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def as_list(value: Any | Iterable[Any] | None, default: list[Any] | None = None) -> list[Any]:
|
|
50
|
+
if value is None:
|
|
51
|
+
return list(default or [])
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
return [value]
|
|
54
|
+
return list(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def merge_dict(base: Mapping[str, Any] | None, override: Mapping[str, Any] | None) -> dict[str, Any]:
|
|
58
|
+
merged = dict(base or {})
|
|
59
|
+
merged.update(dict(override or {}))
|
|
60
|
+
return merged
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def query_referenced_columns(expr: str) -> set[str]:
|
|
64
|
+
"""Return identifier-like column names referenced in a pandas query expression."""
|
|
65
|
+
if not expr or not str(expr).strip():
|
|
66
|
+
return set()
|
|
67
|
+
return {
|
|
68
|
+
match.group(1)
|
|
69
|
+
for match in _QUERY_COLUMN_RE.finditer(str(expr))
|
|
70
|
+
if match.group(1) not in _QUERY_RESERVED
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_woe_fit_query_syntax(data: pd.DataFrame, query: str) -> None:
|
|
75
|
+
"""Raise ValueError when a woe_fit_query expression is syntactically invalid."""
|
|
76
|
+
if not query:
|
|
77
|
+
return
|
|
78
|
+
sample = data.head(min(100, len(data)))
|
|
79
|
+
if sample.empty:
|
|
80
|
+
raise ValueError("woe_fit_query cannot be validated on an empty dataset.")
|
|
81
|
+
try:
|
|
82
|
+
sample.query(query, engine="python")
|
|
83
|
+
except SyntaxError as exc:
|
|
84
|
+
raise ValueError(f"Invalid woe_fit_query syntax: {query!r}") from exc
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
raise ValueError(f"woe_fit_query failed on sample data: {query!r} ({exc})") from exc
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_woe_fit_query_columns(
|
|
90
|
+
query: str,
|
|
91
|
+
available_cols: Iterable[str],
|
|
92
|
+
*,
|
|
93
|
+
context: str = "dataset",
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Raise KeyError when query references columns missing from available_cols."""
|
|
96
|
+
if not query:
|
|
97
|
+
return
|
|
98
|
+
available = set(available_cols)
|
|
99
|
+
missing = sorted(query_referenced_columns(query) - available)
|
|
100
|
+
if missing:
|
|
101
|
+
raise KeyError(
|
|
102
|
+
f"woe_fit_query references missing columns in {context}: {missing}. "
|
|
103
|
+
"Add them to the input data, batch_base_cols, or population_dims."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def apply_woe_fit_query(
|
|
108
|
+
train: pd.DataFrame,
|
|
109
|
+
query: str | None,
|
|
110
|
+
*,
|
|
111
|
+
target: str | None = None,
|
|
112
|
+
) -> tuple[pd.DataFrame, dict[str, Any] | None]:
|
|
113
|
+
"""Filter WOE fit rows via pandas query and return an audit row for refine_summary."""
|
|
114
|
+
if not query:
|
|
115
|
+
return train, None
|
|
116
|
+
n_before = len(train)
|
|
117
|
+
try:
|
|
118
|
+
filtered = train.query(query, engine="python").copy()
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
audit = {
|
|
121
|
+
"target": target,
|
|
122
|
+
"step": "fit_filter",
|
|
123
|
+
"status": "error",
|
|
124
|
+
"query": query,
|
|
125
|
+
"n_before": n_before,
|
|
126
|
+
"error": repr(exc),
|
|
127
|
+
}
|
|
128
|
+
return train, audit
|
|
129
|
+
audit = {
|
|
130
|
+
"target": target,
|
|
131
|
+
"step": "fit_filter",
|
|
132
|
+
"status": "ok",
|
|
133
|
+
"query": query,
|
|
134
|
+
"n_before": n_before,
|
|
135
|
+
"n_after": len(filtered),
|
|
136
|
+
}
|
|
137
|
+
return filtered, audit
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def dataclass_to_dict(obj: Any) -> dict[str, Any]:
|
|
141
|
+
if not is_dataclass(obj):
|
|
142
|
+
return dict(obj)
|
|
143
|
+
return {field.name: getattr(obj, field.name) for field in fields(obj)}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def safe_to_csv(df: pd.DataFrame | None, path: str | os.PathLike, index: bool = False) -> None:
|
|
147
|
+
if df is None:
|
|
148
|
+
return
|
|
149
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
df.to_csv(path, index=index)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def persist_explain_outputs(
|
|
154
|
+
outputs: Mapping[str, Any],
|
|
155
|
+
explain_dir: str | os.PathLike,
|
|
156
|
+
) -> dict[str, dict[str, str]]:
|
|
157
|
+
"""Write explain_outputs tables to ``explain_dir`` and return path index."""
|
|
158
|
+
explain_root = Path(explain_dir)
|
|
159
|
+
explain_root.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
manifest_rows: list[dict[str, str]] = []
|
|
161
|
+
explain_paths: dict[str, dict[str, str]] = {}
|
|
162
|
+
|
|
163
|
+
if "import_error" in outputs:
|
|
164
|
+
err_path = explain_root / "import_error.txt"
|
|
165
|
+
err_path.write_text(str(outputs["import_error"]), encoding="utf-8")
|
|
166
|
+
manifest_rows.append(
|
|
167
|
+
{"model": "_global", "artifact": "import_error", "path": str(err_path), "status": "error"}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
for model_name, payload in outputs.items():
|
|
171
|
+
if model_name == "import_error":
|
|
172
|
+
continue
|
|
173
|
+
if not isinstance(payload, dict):
|
|
174
|
+
continue
|
|
175
|
+
model_dir = explain_root / str(model_name)
|
|
176
|
+
paths: dict[str, str] = {}
|
|
177
|
+
|
|
178
|
+
if payload.get("error") and "feature_importance" not in payload:
|
|
179
|
+
err_path = model_dir / "error.txt"
|
|
180
|
+
model_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
err_path.write_text(str(payload["error"]), encoding="utf-8")
|
|
182
|
+
paths["error"] = str(err_path)
|
|
183
|
+
manifest_rows.append(
|
|
184
|
+
{
|
|
185
|
+
"model": str(model_name),
|
|
186
|
+
"artifact": "error",
|
|
187
|
+
"path": str(err_path),
|
|
188
|
+
"status": "error",
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
explain_paths[str(model_name)] = paths
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
fi = payload.get("feature_importance")
|
|
195
|
+
if isinstance(fi, pd.DataFrame) and not fi.empty:
|
|
196
|
+
fi_path = model_dir / "feature_importance.csv"
|
|
197
|
+
safe_to_csv(fi, fi_path)
|
|
198
|
+
paths["feature_importance"] = str(fi_path)
|
|
199
|
+
manifest_rows.append(
|
|
200
|
+
{
|
|
201
|
+
"model": str(model_name),
|
|
202
|
+
"artifact": "feature_importance",
|
|
203
|
+
"path": str(fi_path),
|
|
204
|
+
"status": "ok",
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
owen = payload.get("owen")
|
|
209
|
+
if isinstance(owen, dict):
|
|
210
|
+
if owen.get("error"):
|
|
211
|
+
err_path = model_dir / "owen_error.txt"
|
|
212
|
+
model_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
err_path.write_text(str(owen["error"]), encoding="utf-8")
|
|
214
|
+
paths["owen_error"] = str(err_path)
|
|
215
|
+
manifest_rows.append(
|
|
216
|
+
{
|
|
217
|
+
"model": str(model_name),
|
|
218
|
+
"artifact": "owen_error",
|
|
219
|
+
"path": str(err_path),
|
|
220
|
+
"status": "error",
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
for artifact, filename in (
|
|
224
|
+
("feature_importance", "owen_feature_importance.csv"),
|
|
225
|
+
("group_importance", "owen_group_importance.csv"),
|
|
226
|
+
):
|
|
227
|
+
table = owen.get(artifact)
|
|
228
|
+
if isinstance(table, pd.DataFrame) and not table.empty:
|
|
229
|
+
table_path = model_dir / filename
|
|
230
|
+
safe_to_csv(table, table_path)
|
|
231
|
+
key = f"owen_{artifact}"
|
|
232
|
+
paths[key] = str(table_path)
|
|
233
|
+
manifest_rows.append(
|
|
234
|
+
{
|
|
235
|
+
"model": str(model_name),
|
|
236
|
+
"artifact": key,
|
|
237
|
+
"path": str(table_path),
|
|
238
|
+
"status": "ok",
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
for artifact_key in ("shap_summary", "plot_error"):
|
|
243
|
+
plot_path = payload.get(artifact_key)
|
|
244
|
+
if plot_path and artifact_key == "shap_summary":
|
|
245
|
+
paths["shap_summary"] = str(plot_path)
|
|
246
|
+
manifest_rows.append(
|
|
247
|
+
{
|
|
248
|
+
"model": str(model_name),
|
|
249
|
+
"artifact": "shap_summary",
|
|
250
|
+
"path": str(plot_path),
|
|
251
|
+
"status": "ok",
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
elif plot_path and artifact_key == "plot_error":
|
|
255
|
+
err_path = model_dir / "plot_error.txt"
|
|
256
|
+
model_dir.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
err_path.write_text(str(plot_path), encoding="utf-8")
|
|
258
|
+
paths["plot_error"] = str(err_path)
|
|
259
|
+
manifest_rows.append(
|
|
260
|
+
{
|
|
261
|
+
"model": str(model_name),
|
|
262
|
+
"artifact": "plot_error",
|
|
263
|
+
"path": str(err_path),
|
|
264
|
+
"status": "plot_error",
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if paths:
|
|
269
|
+
explain_paths[str(model_name)] = paths
|
|
270
|
+
|
|
271
|
+
if manifest_rows:
|
|
272
|
+
manifest_df = pd.DataFrame(manifest_rows)
|
|
273
|
+
safe_to_csv(manifest_df, explain_root / "explain_manifest.csv", index=False)
|
|
274
|
+
explain_paths.setdefault("_manifest", {})["explain_manifest"] = str(explain_root / "explain_manifest.csv")
|
|
275
|
+
|
|
276
|
+
return explain_paths
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_raw_model(model: Any) -> Any:
|
|
280
|
+
if hasattr(model, "_model") and hasattr(model._model, "model"):
|
|
281
|
+
return model._model.model
|
|
282
|
+
if hasattr(model, "model"):
|
|
283
|
+
return model.model
|
|
284
|
+
return model
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def predict_positive(model: Any, data: pd.DataFrame, feature_cols: list[str]) -> np.ndarray:
|
|
288
|
+
raw_model = get_raw_model(model)
|
|
289
|
+
x = data[feature_cols]
|
|
290
|
+
|
|
291
|
+
if hasattr(model, "_model") and hasattr(raw_model, "predict_proba"):
|
|
292
|
+
pred = raw_model.predict_proba(x)
|
|
293
|
+
elif model.__class__.__name__ == "LRMaster" and hasattr(model, "predict_proba"):
|
|
294
|
+
pred = model.predict_proba(data, varlist=feature_cols)
|
|
295
|
+
elif hasattr(model, "predict_proba"):
|
|
296
|
+
try:
|
|
297
|
+
pred = model.predict_proba(x)
|
|
298
|
+
except TypeError:
|
|
299
|
+
pred = model.predict_proba(data, varlist=feature_cols)
|
|
300
|
+
elif hasattr(raw_model, "predict_proba"):
|
|
301
|
+
pred = raw_model.predict_proba(x)
|
|
302
|
+
elif hasattr(model, "predict"):
|
|
303
|
+
pred = model.predict(x)
|
|
304
|
+
elif hasattr(raw_model, "predict"):
|
|
305
|
+
pred = raw_model.predict(x)
|
|
306
|
+
else:
|
|
307
|
+
raise TypeError(f"Model {type(model)!r} does not expose predict/predict_proba")
|
|
308
|
+
|
|
309
|
+
pred_arr = np.asarray(pred)
|
|
310
|
+
if pred_arr.ndim == 2 and pred_arr.shape[1] > 1:
|
|
311
|
+
return pred_arr[:, 1]
|
|
312
|
+
return pred_arr.reshape(-1)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def add_dataset_with_optional_weight(
|
|
316
|
+
evaluator: Any,
|
|
317
|
+
name: str,
|
|
318
|
+
data: pd.DataFrame,
|
|
319
|
+
weight_col: str | None = None,
|
|
320
|
+
) -> None:
|
|
321
|
+
if weight_col:
|
|
322
|
+
try:
|
|
323
|
+
evaluator.add_dataset(name, data, weight_col=weight_col)
|
|
324
|
+
return
|
|
325
|
+
except TypeError:
|
|
326
|
+
pass
|
|
327
|
+
evaluator.add_dataset(name, data)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def write_basic_excel(
|
|
331
|
+
report_path: str,
|
|
332
|
+
sheets: Mapping[str, pd.DataFrame | None],
|
|
333
|
+
title: str | None = None,
|
|
334
|
+
) -> str:
|
|
335
|
+
from ExcelMaster.ExcelMaster import ExcelMaster
|
|
336
|
+
|
|
337
|
+
Path(report_path).parent.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
em = ExcelMaster(report_path, verbose=False)
|
|
339
|
+
used: set[str] = set()
|
|
340
|
+
|
|
341
|
+
def sheet_name(name: str) -> str:
|
|
342
|
+
clean = str(name).replace("/", "_").replace("\\", "_").replace(":", "-")
|
|
343
|
+
clean = clean.replace("[", "(").replace("]", ")").replace("*", "_").replace("?", "_")
|
|
344
|
+
clean = clean.strip("' ") or "Sheet"
|
|
345
|
+
base = clean[:31]
|
|
346
|
+
candidate = base
|
|
347
|
+
i = 2
|
|
348
|
+
while candidate.lower() in used:
|
|
349
|
+
suffix = f"_{i}"
|
|
350
|
+
candidate = f"{base[:31 - len(suffix)]}{suffix}"
|
|
351
|
+
i += 1
|
|
352
|
+
used.add(candidate.lower())
|
|
353
|
+
return candidate
|
|
354
|
+
|
|
355
|
+
if title:
|
|
356
|
+
ws = em.add_worksheet(sheet_name("Overview"))
|
|
357
|
+
em.write_text_content(ws, input_text=title)
|
|
358
|
+
|
|
359
|
+
for name, df in sheets.items():
|
|
360
|
+
if df is None:
|
|
361
|
+
continue
|
|
362
|
+
ws = em.add_worksheet(sheet_name(name))
|
|
363
|
+
em.write_dataframe(ws, df=df, title=str(name), index=False)
|
|
364
|
+
|
|
365
|
+
em.close_workbook()
|
|
366
|
+
return report_path
|