explainiverse 0.1.1a1__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- explainiverse/__init__.py +45 -1
- explainiverse/adapters/__init__.py +9 -0
- explainiverse/adapters/base_adapter.py +25 -25
- explainiverse/adapters/sklearn_adapter.py +32 -32
- explainiverse/core/__init__.py +22 -0
- explainiverse/core/explainer.py +31 -31
- explainiverse/core/explanation.py +24 -24
- explainiverse/core/registry.py +545 -0
- explainiverse/engine/__init__.py +8 -0
- explainiverse/engine/suite.py +142 -142
- explainiverse/evaluation/__init__.py +8 -0
- explainiverse/evaluation/metrics.py +232 -232
- explainiverse/explainers/__init__.py +38 -0
- explainiverse/explainers/attribution/__init__.py +9 -0
- explainiverse/explainers/attribution/lime_wrapper.py +90 -63
- explainiverse/explainers/attribution/shap_wrapper.py +89 -66
- explainiverse/explainers/counterfactual/__init__.py +8 -0
- explainiverse/explainers/counterfactual/dice_wrapper.py +302 -0
- explainiverse/explainers/global_explainers/__init__.py +23 -0
- explainiverse/explainers/global_explainers/ale.py +191 -0
- explainiverse/explainers/global_explainers/partial_dependence.py +192 -0
- explainiverse/explainers/global_explainers/permutation_importance.py +123 -0
- explainiverse/explainers/global_explainers/sage.py +164 -0
- explainiverse/explainers/rule_based/__init__.py +8 -0
- explainiverse/explainers/rule_based/anchors_wrapper.py +350 -0
- explainiverse-0.2.0.dist-info/METADATA +264 -0
- explainiverse-0.2.0.dist-info/RECORD +29 -0
- explainiverse-0.1.1a1.dist-info/METADATA +0 -128
- explainiverse-0.1.1a1.dist-info/RECORD +0 -19
- {explainiverse-0.1.1a1.dist-info → explainiverse-0.2.0.dist-info}/LICENSE +0 -0
- {explainiverse-0.1.1a1.dist-info → explainiverse-0.2.0.dist-info}/WHEEL +0 -0
explainiverse/engine/suite.py
CHANGED
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
# src/explainiverse/engine/suite.py
|
|
2
|
-
|
|
3
|
-
from explainiverse.core.explanation import Explanation
|
|
4
|
-
from explainiverse.explainers.attribution.lime_wrapper import LimeExplainer
|
|
5
|
-
from explainiverse.explainers.attribution.shap_wrapper import ShapExplainer
|
|
6
|
-
from explainiverse.evaluation.metrics import compute_roar
|
|
7
|
-
from sklearn.metrics import accuracy_score
|
|
8
|
-
from sklearn.linear_model import LogisticRegression
|
|
9
|
-
|
|
10
|
-
class ExplanationSuite:
|
|
11
|
-
"""
|
|
12
|
-
Runs multiple explainers on a single instance and compares their outputs.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
def __init__(self, model, explainer_configs, data_meta=None):
|
|
16
|
-
"""
|
|
17
|
-
Args:
|
|
18
|
-
model: a model adapter (e.g., SklearnAdapter)
|
|
19
|
-
explainer_configs: list of (name, kwargs) tuples for explainers
|
|
20
|
-
data_meta: optional metadata about the task, scope, or preference
|
|
21
|
-
"""
|
|
22
|
-
self.model = model
|
|
23
|
-
self.configs = explainer_configs
|
|
24
|
-
self.data_meta = data_meta or {}
|
|
25
|
-
self.explanations = {}
|
|
26
|
-
|
|
27
|
-
def run(self, instance):
|
|
28
|
-
"""
|
|
29
|
-
Run all configured explainers on a single instance.
|
|
30
|
-
"""
|
|
31
|
-
for name, params in self.configs:
|
|
32
|
-
explainer = self._load_explainer(name, **params)
|
|
33
|
-
explanation = explainer.explain(instance)
|
|
34
|
-
self.explanations[name] = explanation
|
|
35
|
-
return self.explanations
|
|
36
|
-
|
|
37
|
-
def compare(self):
|
|
38
|
-
"""
|
|
39
|
-
Print attribution scores side-by-side.
|
|
40
|
-
"""
|
|
41
|
-
keys = set()
|
|
42
|
-
for explanation in self.explanations.values():
|
|
43
|
-
keys.update(explanation.explanation_data.get("feature_attributions", {}).keys())
|
|
44
|
-
|
|
45
|
-
print("\nSide-by-Side Comparison:")
|
|
46
|
-
for key in sorted(keys):
|
|
47
|
-
row = [f"{key}"]
|
|
48
|
-
for name in self.explanations:
|
|
49
|
-
value = self.explanations[name].explanation_data.get("feature_attributions", {}).get(key, "—")
|
|
50
|
-
row.append(f"{name}: {value:.4f}" if isinstance(value, float) else f"{name}: {value}")
|
|
51
|
-
print(" | ".join(row))
|
|
52
|
-
|
|
53
|
-
def suggest_best(self):
|
|
54
|
-
"""
|
|
55
|
-
Suggest the best explainer based on model type, output structure, and task metadata.
|
|
56
|
-
"""
|
|
57
|
-
if "task" in self.data_meta:
|
|
58
|
-
task = self.data_meta["task"]
|
|
59
|
-
else:
|
|
60
|
-
task = "unknown"
|
|
61
|
-
|
|
62
|
-
model = self.model.model
|
|
63
|
-
|
|
64
|
-
# 1. Regression: SHAP preferred due to consistent output
|
|
65
|
-
if task == "regression":
|
|
66
|
-
return "shap"
|
|
67
|
-
|
|
68
|
-
# 2. Model with `predict_proba` → SHAP handles probabilistic outputs well
|
|
69
|
-
if hasattr(model, "predict_proba"):
|
|
70
|
-
try:
|
|
71
|
-
output = self.model.predict([[0] * model.n_features_in_])
|
|
72
|
-
if output.shape[1] > 2:
|
|
73
|
-
return "shap" # Multi-class, SHAP more stable
|
|
74
|
-
else:
|
|
75
|
-
return "lime" # Binary, both are okay
|
|
76
|
-
except Exception:
|
|
77
|
-
return "shap"
|
|
78
|
-
|
|
79
|
-
# 3. Tree-based models → prefer SHAP (TreeSHAP if available)
|
|
80
|
-
if "tree" in str(type(model)).lower():
|
|
81
|
-
return "shap"
|
|
82
|
-
|
|
83
|
-
# 4. Default fallback
|
|
84
|
-
return "lime"
|
|
85
|
-
|
|
86
|
-
def _load_explainer(self, name, **kwargs):
|
|
87
|
-
if name == "lime":
|
|
88
|
-
return LimeExplainer(model=self.model, **kwargs)
|
|
89
|
-
elif name == "shap":
|
|
90
|
-
return ShapExplainer(model=self.model, **kwargs)
|
|
91
|
-
else:
|
|
92
|
-
raise ValueError(f"Unknown explainer: {name}")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def evaluate_roar(
|
|
97
|
-
self,
|
|
98
|
-
X_train,
|
|
99
|
-
y_train,
|
|
100
|
-
X_test,
|
|
101
|
-
y_test,
|
|
102
|
-
top_k: int = 2,
|
|
103
|
-
model_class=None,
|
|
104
|
-
model_kwargs: dict = None
|
|
105
|
-
):
|
|
106
|
-
"""
|
|
107
|
-
Evaluate each explainer using ROAR (Remove And Retrain).
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
X_train, y_train: training data
|
|
111
|
-
X_test, y_test: test data
|
|
112
|
-
top_k: number of features to mask
|
|
113
|
-
model_class: model constructor with .fit() and .predict() (default: same as current model)
|
|
114
|
-
model_kwargs: optional keyword args for new model instance
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
Dict of {explainer_name: accuracy drop (baseline - retrained)}
|
|
118
|
-
"""
|
|
119
|
-
from explainiverse.evaluation.metrics import compute_roar
|
|
120
|
-
|
|
121
|
-
model_kwargs = model_kwargs or {}
|
|
122
|
-
|
|
123
|
-
# Default to type(self.model.model) if not provided
|
|
124
|
-
if model_class is None:
|
|
125
|
-
model_class = type(self.model.model)
|
|
126
|
-
|
|
127
|
-
roar_scores = {}
|
|
128
|
-
|
|
129
|
-
for name, explanation in self.explanations.items():
|
|
130
|
-
print(f"[ROAR] Evaluating explainer: {name}")
|
|
131
|
-
roar = compute_roar(
|
|
132
|
-
model_class=model_class,
|
|
133
|
-
X_train=X_train,
|
|
134
|
-
y_train=y_train,
|
|
135
|
-
X_test=X_test,
|
|
136
|
-
y_test=y_test,
|
|
137
|
-
explanations=[explanation], # single-instance for now
|
|
138
|
-
top_k=top_k,
|
|
139
|
-
model_kwargs=model_kwargs
|
|
140
|
-
)
|
|
141
|
-
roar_scores[name] = roar
|
|
142
|
-
|
|
1
|
+
# src/explainiverse/engine/suite.py
|
|
2
|
+
|
|
3
|
+
from explainiverse.core.explanation import Explanation
|
|
4
|
+
from explainiverse.explainers.attribution.lime_wrapper import LimeExplainer
|
|
5
|
+
from explainiverse.explainers.attribution.shap_wrapper import ShapExplainer
|
|
6
|
+
from explainiverse.evaluation.metrics import compute_roar
|
|
7
|
+
from sklearn.metrics import accuracy_score
|
|
8
|
+
from sklearn.linear_model import LogisticRegression
|
|
9
|
+
|
|
10
|
+
class ExplanationSuite:
|
|
11
|
+
"""
|
|
12
|
+
Runs multiple explainers on a single instance and compares their outputs.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, model, explainer_configs, data_meta=None):
|
|
16
|
+
"""
|
|
17
|
+
Args:
|
|
18
|
+
model: a model adapter (e.g., SklearnAdapter)
|
|
19
|
+
explainer_configs: list of (name, kwargs) tuples for explainers
|
|
20
|
+
data_meta: optional metadata about the task, scope, or preference
|
|
21
|
+
"""
|
|
22
|
+
self.model = model
|
|
23
|
+
self.configs = explainer_configs
|
|
24
|
+
self.data_meta = data_meta or {}
|
|
25
|
+
self.explanations = {}
|
|
26
|
+
|
|
27
|
+
def run(self, instance):
|
|
28
|
+
"""
|
|
29
|
+
Run all configured explainers on a single instance.
|
|
30
|
+
"""
|
|
31
|
+
for name, params in self.configs:
|
|
32
|
+
explainer = self._load_explainer(name, **params)
|
|
33
|
+
explanation = explainer.explain(instance)
|
|
34
|
+
self.explanations[name] = explanation
|
|
35
|
+
return self.explanations
|
|
36
|
+
|
|
37
|
+
def compare(self):
|
|
38
|
+
"""
|
|
39
|
+
Print attribution scores side-by-side.
|
|
40
|
+
"""
|
|
41
|
+
keys = set()
|
|
42
|
+
for explanation in self.explanations.values():
|
|
43
|
+
keys.update(explanation.explanation_data.get("feature_attributions", {}).keys())
|
|
44
|
+
|
|
45
|
+
print("\nSide-by-Side Comparison:")
|
|
46
|
+
for key in sorted(keys):
|
|
47
|
+
row = [f"{key}"]
|
|
48
|
+
for name in self.explanations:
|
|
49
|
+
value = self.explanations[name].explanation_data.get("feature_attributions", {}).get(key, "—")
|
|
50
|
+
row.append(f"{name}: {value:.4f}" if isinstance(value, float) else f"{name}: {value}")
|
|
51
|
+
print(" | ".join(row))
|
|
52
|
+
|
|
53
|
+
def suggest_best(self):
|
|
54
|
+
"""
|
|
55
|
+
Suggest the best explainer based on model type, output structure, and task metadata.
|
|
56
|
+
"""
|
|
57
|
+
if "task" in self.data_meta:
|
|
58
|
+
task = self.data_meta["task"]
|
|
59
|
+
else:
|
|
60
|
+
task = "unknown"
|
|
61
|
+
|
|
62
|
+
model = self.model.model
|
|
63
|
+
|
|
64
|
+
# 1. Regression: SHAP preferred due to consistent output
|
|
65
|
+
if task == "regression":
|
|
66
|
+
return "shap"
|
|
67
|
+
|
|
68
|
+
# 2. Model with `predict_proba` → SHAP handles probabilistic outputs well
|
|
69
|
+
if hasattr(model, "predict_proba"):
|
|
70
|
+
try:
|
|
71
|
+
output = self.model.predict([[0] * model.n_features_in_])
|
|
72
|
+
if output.shape[1] > 2:
|
|
73
|
+
return "shap" # Multi-class, SHAP more stable
|
|
74
|
+
else:
|
|
75
|
+
return "lime" # Binary, both are okay
|
|
76
|
+
except Exception:
|
|
77
|
+
return "shap"
|
|
78
|
+
|
|
79
|
+
# 3. Tree-based models → prefer SHAP (TreeSHAP if available)
|
|
80
|
+
if "tree" in str(type(model)).lower():
|
|
81
|
+
return "shap"
|
|
82
|
+
|
|
83
|
+
# 4. Default fallback
|
|
84
|
+
return "lime"
|
|
85
|
+
|
|
86
|
+
def _load_explainer(self, name, **kwargs):
|
|
87
|
+
if name == "lime":
|
|
88
|
+
return LimeExplainer(model=self.model, **kwargs)
|
|
89
|
+
elif name == "shap":
|
|
90
|
+
return ShapExplainer(model=self.model, **kwargs)
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(f"Unknown explainer: {name}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def evaluate_roar(
|
|
97
|
+
self,
|
|
98
|
+
X_train,
|
|
99
|
+
y_train,
|
|
100
|
+
X_test,
|
|
101
|
+
y_test,
|
|
102
|
+
top_k: int = 2,
|
|
103
|
+
model_class=None,
|
|
104
|
+
model_kwargs: dict = None
|
|
105
|
+
):
|
|
106
|
+
"""
|
|
107
|
+
Evaluate each explainer using ROAR (Remove And Retrain).
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
X_train, y_train: training data
|
|
111
|
+
X_test, y_test: test data
|
|
112
|
+
top_k: number of features to mask
|
|
113
|
+
model_class: model constructor with .fit() and .predict() (default: same as current model)
|
|
114
|
+
model_kwargs: optional keyword args for new model instance
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dict of {explainer_name: accuracy drop (baseline - retrained)}
|
|
118
|
+
"""
|
|
119
|
+
from explainiverse.evaluation.metrics import compute_roar
|
|
120
|
+
|
|
121
|
+
model_kwargs = model_kwargs or {}
|
|
122
|
+
|
|
123
|
+
# Default to type(self.model.model) if not provided
|
|
124
|
+
if model_class is None:
|
|
125
|
+
model_class = type(self.model.model)
|
|
126
|
+
|
|
127
|
+
roar_scores = {}
|
|
128
|
+
|
|
129
|
+
for name, explanation in self.explanations.items():
|
|
130
|
+
print(f"[ROAR] Evaluating explainer: {name}")
|
|
131
|
+
roar = compute_roar(
|
|
132
|
+
model_class=model_class,
|
|
133
|
+
X_train=X_train,
|
|
134
|
+
y_train=y_train,
|
|
135
|
+
X_test=X_test,
|
|
136
|
+
y_test=y_test,
|
|
137
|
+
explanations=[explanation], # single-instance for now
|
|
138
|
+
top_k=top_k,
|
|
139
|
+
model_kwargs=model_kwargs
|
|
140
|
+
)
|
|
141
|
+
roar_scores[name] = roar
|
|
142
|
+
|
|
143
143
|
return roar_scores
|