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.
Files changed (105) hide show
  1. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/Model_Explainer.py +159 -4
  2. supermodelingfactory-0.3.7/Modeling_Tool/Pipeline/_common.py +366 -0
  3. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/credit_model.py +116 -8
  4. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/feature_validation.py +17 -1
  5. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/__init__.py +1 -1
  6. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/PKG-INFO +7 -4
  7. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/README.md +6 -3
  8. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/PKG-INFO +7 -4
  9. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/pyproject.toml +1 -1
  10. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/setup.py +1 -1
  11. supermodelingfactory-0.3.5/Modeling_Tool/Pipeline/_common.py +0 -132
  12. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/ExcelFormatTool.py +0 -0
  13. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/ExcelMaster.py +0 -0
  14. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/Template.py +0 -0
  15. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/Utility.py +0 -0
  16. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/ExcelMaster/__init__.py +0 -0
  17. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/LICENSE +0 -0
  18. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/MANIFEST.in +0 -0
  19. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  20. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  21. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  22. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  23. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  24. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  25. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  26. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  27. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  28. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  29. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  30. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  31. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  32. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/__init__.py +0 -0
  33. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/kDataFrame.py +0 -0
  34. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  35. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  36. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Core/utils.py +0 -0
  37. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  38. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  39. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  40. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  41. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/__init__.py +0 -0
  42. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  43. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  44. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  45. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  46. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Explainability/__init__.py +0 -0
  47. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  48. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  49. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  50. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  51. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  52. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  53. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  54. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Feature/__init__.py +0 -0
  55. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  56. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  57. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  58. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  59. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  60. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  61. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  62. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Model/__init__.py +0 -0
  63. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/__init__.py +0 -0
  64. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  65. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  66. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  67. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  68. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  69. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  70. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  71. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  72. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  73. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  74. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  75. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/Sample/__init__.py +0 -0
  76. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  77. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/UAT/__init__.py +0 -0
  78. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  79. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  80. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  81. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  82. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  83. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  84. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  85. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  86. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  87. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  88. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  89. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  90. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/__init__.py +0 -0
  91. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  92. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  93. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  94. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  95. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/__init__.py +0 -0
  96. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  97. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Report/Report_Tool.py +0 -0
  98. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/Report/__init__.py +0 -0
  99. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  100. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  101. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  102. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/requires.txt +0 -0
  103. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  104. {supermodelingfactory-0.3.5 → supermodelingfactory-0.3.7}/requirements.txt +0 -0
  105. {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(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)
@@ -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