SuperModelingFactory 0.3.6__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.
Files changed (104) hide show
  1. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/Model_Explainer.py +159 -4
  2. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/_common.py +126 -0
  3. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/credit_model.py +43 -3
  4. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/__init__.py +1 -1
  5. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/PKG-INFO +2 -2
  6. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/README.md +1 -1
  7. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  8. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/pyproject.toml +1 -1
  9. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/setup.py +1 -1
  10. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/ExcelMaster/ExcelFormatTool.py +0 -0
  11. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/ExcelMaster/ExcelMaster.py +0 -0
  12. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/ExcelMaster/Template.py +0 -0
  13. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/ExcelMaster/Utility.py +0 -0
  14. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/ExcelMaster/__init__.py +0 -0
  15. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/LICENSE +0 -0
  16. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/MANIFEST.in +0 -0
  17. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  18. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  19. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  20. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  21. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  22. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  23. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  24. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  25. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  26. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  27. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  28. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  29. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  30. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/__init__.py +0 -0
  31. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/kDataFrame.py +0 -0
  32. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  33. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  34. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/utils.py +0 -0
  35. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  36. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  37. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  38. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  39. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/__init__.py +0 -0
  40. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  41. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  42. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  43. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  44. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/__init__.py +0 -0
  45. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  46. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  47. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  48. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  49. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  50. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  51. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  52. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/__init__.py +0 -0
  53. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  54. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  55. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  56. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  57. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  58. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  59. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  60. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/__init__.py +0 -0
  61. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/__init__.py +0 -0
  62. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/feature_validation.py +0 -0
  63. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  64. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  65. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  66. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  67. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  68. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  69. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  70. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  71. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  72. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  73. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  74. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/__init__.py +0 -0
  75. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  76. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/UAT/__init__.py +0 -0
  77. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  78. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  79. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  80. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  81. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  82. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  83. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  84. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  85. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  86. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  87. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  88. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  89. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/__init__.py +0 -0
  90. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  91. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  92. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  93. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  94. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/__init__.py +0 -0
  95. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  96. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Report/Report_Tool.py +0 -0
  97. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/Report/__init__.py +0 -0
  98. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  99. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  100. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  101. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/requires.txt +0 -0
  102. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  103. {supermodelingfactory-0.3.6 → supermodelingfactory-0.3.7}/requirements.txt +0 -0
  104. {supermodelingfactory-0.3.6 → 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(self, x_row, X_train=None, num_features=10, num_samples=5000, random_state=None, **lime_kwargs):
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
- train = X_train if X_train is not None else self.background_data
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(self, X, X_train=None, num_features=10, num_samples=2000, sample_size=100, random_state=None, **lime_kwargs):
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=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)
@@ -150,6 +150,132 @@ def safe_to_csv(df: pd.DataFrame | None, path: str | os.PathLike, index: bool =
150
150
  df.to_csv(path, index=index)
151
151
 
152
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
+
153
279
  def get_raw_model(model: Any) -> Any:
154
280
  if hasattr(model, "_model") and hasattr(model._model, "model"):
155
281
  return model._model.model
@@ -13,6 +13,7 @@ from ._common import (
13
13
  as_list,
14
14
  make_dirs,
15
15
  merge_dict,
16
+ persist_explain_outputs,
16
17
  predict_positive,
17
18
  safe_to_csv,
18
19
  validate_woe_fit_query_columns,
@@ -106,6 +107,7 @@ class CreditModelPipelineResult:
106
107
  optuna_results: dict[str, pd.DataFrame] = field(default_factory=dict)
107
108
  perf_results: dict[str, pd.DataFrame] = field(default_factory=dict)
108
109
  explain_outputs: dict[str, Any] = field(default_factory=dict)
110
+ explain_paths: dict[str, dict[str, str]] = field(default_factory=dict)
109
111
  report_path: str | None = None
110
112
  lr_search_results: pd.DataFrame | None = None
111
113
  warm_start_summary: pd.DataFrame | None = None
@@ -173,13 +175,15 @@ class CreditModelPipeline:
173
175
 
174
176
  output_dir = Path(cfg.output_dir)
175
177
  if cfg.write_outputs or cfg.write_excel:
176
- make_dirs(
178
+ dirs = [
177
179
  output_dir,
178
180
  output_dir / "figs" / "woe",
179
181
  output_dir / "figs" / "mono_woe",
180
182
  output_dir / "figs" / "perf",
181
- output_dir / "explain",
182
- )
183
+ ]
184
+ if cfg.write_outputs and self._will_run_explainability():
185
+ dirs.append(output_dir / "explain")
186
+ make_dirs(*dirs)
183
187
  if cfg.save_models:
184
188
  make_dirs(self._model_output_dir(), output_dir / "artifacts")
185
189
 
@@ -219,6 +223,9 @@ class CreditModelPipeline:
219
223
  woe_artifacts.get("extra_eval"),
220
224
  )
221
225
  explain_outputs = self._run_explainability(model_inputs, models)
226
+ explain_paths: dict[str, dict[str, str]] = {}
227
+ if cfg.write_outputs and explain_outputs:
228
+ explain_paths = persist_explain_outputs(explain_outputs, output_dir / "explain")
222
229
  model_paths: dict[str, str] = {}
223
230
  artifact_paths: dict[str, str] = {}
224
231
  model_paths_frame = None
@@ -261,6 +268,7 @@ class CreditModelPipeline:
261
268
  }
262
269
  for name, perf in perf_results.items():
263
270
  sheets[f"Perf_{name.upper()}"] = perf
271
+ sheets.update(self._explain_excel_sheets(explain_outputs))
264
272
  write_basic_excel(report_path, sheets, title="SuperModelingFactory Credit Model Pipeline Report")
265
273
 
266
274
  return CreditModelPipelineResult(
@@ -273,6 +281,7 @@ class CreditModelPipeline:
273
281
  optuna_results=optuna_results,
274
282
  perf_results=perf_results,
275
283
  explain_outputs=explain_outputs,
284
+ explain_paths=explain_paths,
276
285
  report_path=report_path,
277
286
  lr_search_results=lr_search_results,
278
287
  warm_start_summary=warm_start_summary,
@@ -977,6 +986,28 @@ class CreditModelPipeline:
977
986
  return wrapper.predict_with_base_margin(data[feature_cols], init_score, return_prob=True)
978
987
  return predict_positive(wrapper, data, feature_cols)
979
988
 
989
+ def _will_run_explainability(self) -> bool:
990
+ cfg = self.config
991
+ return bool(as_list(cfg.explain_models)) or bool(cfg.owen_enabled)
992
+
993
+ def _explain_excel_sheets(self, explain_outputs: dict[str, Any]) -> dict[str, pd.DataFrame]:
994
+ sheets: dict[str, pd.DataFrame] = {}
995
+ for model_name, payload in explain_outputs.items():
996
+ if model_name == "import_error" or not isinstance(payload, dict):
997
+ continue
998
+ fi = payload.get("feature_importance")
999
+ if isinstance(fi, pd.DataFrame) and not fi.empty:
1000
+ sheets[f"Explain_{str(model_name).upper()}_FI"] = fi
1001
+ owen = payload.get("owen")
1002
+ if isinstance(owen, dict):
1003
+ owen_fi = owen.get("feature_importance")
1004
+ if isinstance(owen_fi, pd.DataFrame) and not owen_fi.empty:
1005
+ sheets[f"Explain_{str(model_name).upper()}_Owen_FI"] = owen_fi
1006
+ owen_grp = owen.get("group_importance")
1007
+ if isinstance(owen_grp, pd.DataFrame) and not owen_grp.empty:
1008
+ sheets[f"Explain_{str(model_name).upper()}_Owen_Group"] = owen_grp
1009
+ return sheets
1010
+
980
1011
  def _run_explainability(
981
1012
  self,
982
1013
  model_inputs: dict[str, dict[str, Any]],
@@ -987,6 +1018,7 @@ class CreditModelPipeline:
987
1018
  if not explain_models and not cfg.owen_enabled:
988
1019
  return {}
989
1020
  outputs: dict[str, Any] = {}
1021
+ explain_dir = Path(cfg.output_dir) / "explain"
990
1022
  try:
991
1023
  from Modeling_Tool import ModelExplainer
992
1024
  except Exception as exc:
@@ -1005,6 +1037,14 @@ class CreditModelPipeline:
1005
1037
  item: dict[str, Any] = {}
1006
1038
  if name in explain_models:
1007
1039
  item["feature_importance"] = exp.feature_importance(X=eval_x, normalize=True)
1040
+ if cfg.write_outputs and cfg.plot_outputs:
1041
+ plot_path = explain_dir / name / "shap_summary.png"
1042
+ plot_path.parent.mkdir(parents=True, exist_ok=True)
1043
+ try:
1044
+ exp.summary_plot(X=eval_x, show=False, save_path=str(plot_path))
1045
+ item["shap_summary"] = str(plot_path)
1046
+ except Exception as plot_exc:
1047
+ item["plot_error"] = repr(plot_exc)
1008
1048
  if cfg.owen_enabled and name != "xgb":
1009
1049
  item["owen"] = self._run_owen(exp, eval_x, feature_cols)
1010
1050
  outputs[name] = item
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  __author__ = "Jingkai Sun"
4
- __version__ = "0.3.6"
4
+ __version__ = "0.3.7"
5
5
 
6
6
  # Core module - base data structures
7
7
  from .Core import (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.3.6
268
+ - **Version**: 0.3.7
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -208,7 +208,7 @@ em.close_workbook()
208
208
 
209
209
  ## 版本
210
210
 
211
- - **Version**: 0.3.6
211
+ - **Version**: 0.3.7
212
212
  - **Author**: Jingkai Sun
213
213
 
214
214
  ## 许可证
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.3.6
268
+ - **Version**: 0.3.7
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "SuperModelingFactory"
15
- version = "0.3.6"
15
+ version = "0.3.7"
16
16
  description = "Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting."
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.10"
@@ -15,7 +15,7 @@ def _read(path: str) -> str:
15
15
 
16
16
  setup(
17
17
  name="SuperModelingFactory",
18
- version=os.environ.get("SMF_VERSION", "0.3.6"),
18
+ version=os.environ.get("SMF_VERSION", "0.3.7"),
19
19
  description="Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.",
20
20
  long_description=_read("README.md"),
21
21
  long_description_content_type="text/markdown",