shap-analyzer 0.1.3__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.
- shap_analyzer-0.1.3/PKG-INFO +10 -0
- shap_analyzer-0.1.3/pyproject.toml +16 -0
- shap_analyzer-0.1.3/setup.cfg +4 -0
- shap_analyzer-0.1.3/shap_analyzer/__init__.py +26 -0
- shap_analyzer-0.1.3/shap_analyzer/core.py +252 -0
- shap_analyzer-0.1.3/shap_analyzer/visualizer.py +69 -0
- shap_analyzer-0.1.3/shap_analyzer.egg-info/PKG-INFO +10 -0
- shap_analyzer-0.1.3/shap_analyzer.egg-info/SOURCES.txt +10 -0
- shap_analyzer-0.1.3/shap_analyzer.egg-info/dependency_links.txt +1 -0
- shap_analyzer-0.1.3/shap_analyzer.egg-info/requires.txt +4 -0
- shap_analyzer-0.1.3/shap_analyzer.egg-info/top_level.txt +1 -0
- shap_analyzer-0.1.3/tests/test_core.py +65 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shap_analyzer
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A lightweight wrapper for quick SHAP model explainability analysis.
|
|
5
|
+
Author-email: Abu Saad <abusaadd44@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Requires-Dist: shap>=0.42.0
|
|
8
|
+
Requires-Dist: pandas>=1.5.0
|
|
9
|
+
Requires-Dist: numpy>=1.21.0
|
|
10
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shap_analyzer"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "A lightweight wrapper for quick SHAP model explainability analysis."
|
|
9
|
+
authors = [{ name = "Abu Saad", email = "abusaadd44@gmail.com" }]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"shap>=0.42.0",
|
|
12
|
+
"pandas>=1.5.0",
|
|
13
|
+
"numpy>=1.21.0",
|
|
14
|
+
"matplotlib>=3.5.0"
|
|
15
|
+
]
|
|
16
|
+
requires-python = ">=3.8"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
SHAP Analyzer Package Initializer
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core import ModelExplainer, analyze
|
|
8
|
+
from .visualizer import (
|
|
9
|
+
plot_bar,
|
|
10
|
+
plot_decision,
|
|
11
|
+
plot_dependence,
|
|
12
|
+
plot_heatmap,
|
|
13
|
+
plot_local,
|
|
14
|
+
plot_summary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ModelExplainer",
|
|
19
|
+
"analyze",
|
|
20
|
+
"plot_summary",
|
|
21
|
+
"plot_bar",
|
|
22
|
+
"plot_local",
|
|
23
|
+
"plot_dependence",
|
|
24
|
+
"plot_heatmap",
|
|
25
|
+
"plot_decision"
|
|
26
|
+
]
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# # """
|
|
2
|
+
# # Core analysis engine wrapping SHAP explainers.
|
|
3
|
+
# # """
|
|
4
|
+
|
|
5
|
+
# # from typing import Union, Optional
|
|
6
|
+
# # import pandas as pd
|
|
7
|
+
# # import numpy as np
|
|
8
|
+
# # import shap
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# # class ModelExplainer:
|
|
12
|
+
# # """
|
|
13
|
+
# # Main wrapper class for initializing SHAP analysis on trained models.
|
|
14
|
+
# # """
|
|
15
|
+
|
|
16
|
+
# # def __init__(self, model, X_data: pd.DataFrame, sample_size: Optional[int] = None):
|
|
17
|
+
# # """
|
|
18
|
+
# # Parameters
|
|
19
|
+
# # ----------
|
|
20
|
+
# # model : object
|
|
21
|
+
# # Trained Machine Learning model (Scikit-Learn, XGBoost, LightGBM, etc.).
|
|
22
|
+
# # X_data : pd.DataFrame
|
|
23
|
+
# # Dataset used for generating explanations.
|
|
24
|
+
# # sample_size : int, optional
|
|
25
|
+
# # If provided, subsamples X_data for faster computation on large datasets.
|
|
26
|
+
# # """
|
|
27
|
+
# # self.model = model
|
|
28
|
+
# # self.feature_names = list(X_data.columns)
|
|
29
|
+
|
|
30
|
+
# # if sample_size and sample_size < len(X_data):
|
|
31
|
+
# # self.X_data = X_data.sample(n=sample_size, random_state=42)
|
|
32
|
+
# # else:
|
|
33
|
+
# # self.X_data = X_data.copy()
|
|
34
|
+
|
|
35
|
+
# # # Automatically picks the best explainer (TreeExplainer, LinearExplainer, etc.)
|
|
36
|
+
# # self.explainer = shap.Explainer(self.model, self.X_data)
|
|
37
|
+
# # self.shap_values = self.explainer(self.X_data)
|
|
38
|
+
|
|
39
|
+
# # def get_feature_importance(self) -> pd.DataFrame:
|
|
40
|
+
# # """
|
|
41
|
+
# # Calculates global feature importance based on mean absolute SHAP values.
|
|
42
|
+
|
|
43
|
+
# # Returns
|
|
44
|
+
# # -------
|
|
45
|
+
# # pd.DataFrame
|
|
46
|
+
# # DataFrame with 'feature' and 'mean_shap_value' sorted in descending order.
|
|
47
|
+
# # """
|
|
48
|
+
# # # Multi-class output handling
|
|
49
|
+
# # if len(self.shap_values.values.shape) == 3:
|
|
50
|
+
# # vals = np.abs(self.shap_values.values).mean(axis=(0, 2))
|
|
51
|
+
# # else:
|
|
52
|
+
# # vals = np.abs(self.shap_values.values).mean(axis=0)
|
|
53
|
+
|
|
54
|
+
# # df = pd.DataFrame({
|
|
55
|
+
# # 'feature': self.feature_names,
|
|
56
|
+
# # 'mean_shap_value': vals
|
|
57
|
+
# # })
|
|
58
|
+
# # return df.sort_values(by='mean_shap_value', ascending=False).reset_index(drop=True)
|
|
59
|
+
|
|
60
|
+
# # def get_instance_explanation(self, index: int) -> pd.DataFrame:
|
|
61
|
+
# # """
|
|
62
|
+
# # Extracts feature contributions for a single observation/row.
|
|
63
|
+
|
|
64
|
+
# # Parameters
|
|
65
|
+
# # ----------
|
|
66
|
+
# # index : int
|
|
67
|
+
# # Row index position in X_data.
|
|
68
|
+
|
|
69
|
+
# # Returns
|
|
70
|
+
# # -------
|
|
71
|
+
# # pd.DataFrame
|
|
72
|
+
# # DataFrame containing feature names, raw values, and instance SHAP impact.
|
|
73
|
+
# # """
|
|
74
|
+
# # if index < 0 or index >= len(self.X_data):
|
|
75
|
+
# # raise IndexError(f"Index {index} is out of bounds for data length {len(self.X_data)}")
|
|
76
|
+
|
|
77
|
+
# # single_shap = self.shap_values.values[index]
|
|
78
|
+
# # single_val = self.X_data.iloc[index].values
|
|
79
|
+
|
|
80
|
+
# # # If multi-class, select first class for simplicity
|
|
81
|
+
# # if len(single_shap.shape) > 1:
|
|
82
|
+
# # single_shap = single_shap[:, 0]
|
|
83
|
+
|
|
84
|
+
# # df = pd.DataFrame({
|
|
85
|
+
# # 'feature': self.feature_names,
|
|
86
|
+
# # 'feature_value': single_val,
|
|
87
|
+
# # 'shap_impact': single_shap
|
|
88
|
+
# # })
|
|
89
|
+
# # return df.sort_values(by='shap_impact', key=abs, ascending=False).reset_index(drop=True)
|
|
90
|
+
|
|
91
|
+
# from typing import Optional
|
|
92
|
+
# import pandas as pd
|
|
93
|
+
# import numpy as np
|
|
94
|
+
# import shap
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# class ModelExplainer:
|
|
98
|
+
# def __init__(self, model, X_data: pd.DataFrame, sample_size: Optional[int] = None):
|
|
99
|
+
# self.model = model
|
|
100
|
+
# self.feature_names = list(X_data.columns)
|
|
101
|
+
|
|
102
|
+
# if sample_size and sample_size < len(X_data):
|
|
103
|
+
# self.X_data = X_data.sample(n=sample_size, random_state=42)
|
|
104
|
+
# else:
|
|
105
|
+
# self.X_data = X_data.copy()
|
|
106
|
+
|
|
107
|
+
# self.explainer = shap.Explainer(self.model, self.X_data)
|
|
108
|
+
# raw_shap = self.explainer(self.X_data)
|
|
109
|
+
|
|
110
|
+
# # Fix: For classification models (3D matrix [samples, features, classes]), select Class 1
|
|
111
|
+
# if len(raw_shap.shape) == 3:
|
|
112
|
+
# self.shap_values = raw_shap[:, :, 1]
|
|
113
|
+
# else:
|
|
114
|
+
# self.shap_values = raw_shap
|
|
115
|
+
|
|
116
|
+
# def get_feature_importance(self) -> pd.DataFrame:
|
|
117
|
+
# vals = np.abs(self.shap_values.values).mean(axis=0)
|
|
118
|
+
# return pd.DataFrame({
|
|
119
|
+
# 'feature': self.feature_names,
|
|
120
|
+
# 'mean_shap_value': vals
|
|
121
|
+
# }).sort_values(by='mean_shap_value', ascending=False).reset_index(drop=True)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# def get_instance_explanation(self, index: int) -> pd.DataFrame:
|
|
126
|
+
# if index < 0 or index >= len(self.X_data):
|
|
127
|
+
# raise IndexError(f"Index {index} out of bounds.")
|
|
128
|
+
|
|
129
|
+
# return pd.DataFrame({
|
|
130
|
+
# 'feature': self.feature_names,
|
|
131
|
+
# 'feature_value': self.X_data.iloc[index].values,
|
|
132
|
+
# 'shap_impact': self.shap_values.values[index]
|
|
133
|
+
# }).sort_values(by='shap_impact', key=abs, ascending=False).reset_index(drop=True)
|
|
134
|
+
|
|
135
|
+
# def get_top_drivers(self, index: int, top_n: int = 3) -> dict:
|
|
136
|
+
# df = self.get_instance_explanation(index)
|
|
137
|
+
|
|
138
|
+
# # 1. Filter and sort positive drivers (largest positive impacts first)
|
|
139
|
+
# pos_drivers = (
|
|
140
|
+
# df[df['shap_impact'] > 0]
|
|
141
|
+
# .sort_values(by='shap_impact', ascending=False)
|
|
142
|
+
# .head(top_n)
|
|
143
|
+
# )
|
|
144
|
+
|
|
145
|
+
# # 2. Filter and sort negative drivers (most negative impacts first)
|
|
146
|
+
# neg_drivers = (
|
|
147
|
+
# df[df['shap_impact'] < 0]
|
|
148
|
+
# .sort_values(by='shap_impact', ascending=True)
|
|
149
|
+
# .head(top_n)
|
|
150
|
+
# )
|
|
151
|
+
|
|
152
|
+
# # 3. Missing Return Block
|
|
153
|
+
# return {
|
|
154
|
+
# "positive_drivers": pos_drivers[['feature', 'feature_value', 'shap_impact']].to_dict(orient='records'),
|
|
155
|
+
# "negative_drivers": neg_drivers[['feature', 'feature_value', 'shap_impact']].to_dict(orient='records')
|
|
156
|
+
# }
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
Core explainability module for shap_analyzer.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
from typing import Optional
|
|
163
|
+
import pandas as pd
|
|
164
|
+
import numpy as np
|
|
165
|
+
import shap
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ModelExplainer:
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
model,
|
|
172
|
+
X_data: pd.DataFrame,
|
|
173
|
+
sample_size: Optional[int] = None,
|
|
174
|
+
class_index: int = 1
|
|
175
|
+
):
|
|
176
|
+
self.model = model
|
|
177
|
+
self.class_index = class_index
|
|
178
|
+
self.feature_names = list(X_data.columns)
|
|
179
|
+
|
|
180
|
+
if sample_size and sample_size < len(X_data):
|
|
181
|
+
self.X_data = X_data.sample(n=sample_size, random_state=42)
|
|
182
|
+
else:
|
|
183
|
+
self.X_data = X_data.copy()
|
|
184
|
+
|
|
185
|
+
self.explainer = shap.Explainer(self.model, self.X_data)
|
|
186
|
+
raw_shap = self.explainer(self.X_data)
|
|
187
|
+
|
|
188
|
+
# Handle 3D SHAP outputs (multi-class or binary classification)
|
|
189
|
+
if len(raw_shap.shape) == 3:
|
|
190
|
+
num_classes = raw_shap.shape[2]
|
|
191
|
+
if self.class_index < 0 or self.class_index >= num_classes:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"class_index={self.class_index} is out of bounds. "
|
|
194
|
+
f"This model has {num_classes} classes (indexes 0 to {num_classes - 1})."
|
|
195
|
+
)
|
|
196
|
+
self.shap_values = raw_shap[:, :, self.class_index]
|
|
197
|
+
else:
|
|
198
|
+
self.shap_values = raw_shap
|
|
199
|
+
|
|
200
|
+
def get_feature_importance(self) -> pd.DataFrame:
|
|
201
|
+
"""Returns global feature importance sorted by mean absolute SHAP value."""
|
|
202
|
+
vals = np.abs(self.shap_values.values).mean(axis=0)
|
|
203
|
+
return pd.DataFrame({
|
|
204
|
+
'feature': self.feature_names,
|
|
205
|
+
'mean_shap_value': vals
|
|
206
|
+
}).sort_values(by='mean_shap_value', ascending=False).reset_index(drop=True)
|
|
207
|
+
|
|
208
|
+
def get_instance_explanation(self, index: int) -> pd.DataFrame:
|
|
209
|
+
"""Returns local feature impacts for a specific instance row."""
|
|
210
|
+
if index < 0 or index >= len(self.X_data):
|
|
211
|
+
raise IndexError(f"Index {index} out of bounds.")
|
|
212
|
+
|
|
213
|
+
return pd.DataFrame({
|
|
214
|
+
'feature': self.feature_names,
|
|
215
|
+
'feature_value': self.X_data.iloc[index].values,
|
|
216
|
+
'shap_impact': self.shap_values.values[index]
|
|
217
|
+
}).sort_values(by='shap_impact', key=abs, ascending=False).reset_index(drop=True)
|
|
218
|
+
|
|
219
|
+
def get_top_drivers(self, index: int, top_n: int = 3) -> dict:
|
|
220
|
+
"""Returns top N positive and negative features influencing a single prediction."""
|
|
221
|
+
df = self.get_instance_explanation(index)
|
|
222
|
+
|
|
223
|
+
pos_drivers = (
|
|
224
|
+
df[df['shap_impact'] > 0]
|
|
225
|
+
.sort_values(by='shap_impact', ascending=False)
|
|
226
|
+
.head(top_n)
|
|
227
|
+
)
|
|
228
|
+
neg_drivers = (
|
|
229
|
+
df[df['shap_impact'] < 0]
|
|
230
|
+
.sort_values(by='shap_impact', ascending=True)
|
|
231
|
+
.head(top_n)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"positive_drivers": pos_drivers[['feature', 'feature_value', 'shap_impact']].to_dict(orient='records'),
|
|
236
|
+
"negative_drivers": neg_drivers[['feature', 'feature_value', 'shap_impact']].to_dict(orient='records')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def analyze(
|
|
241
|
+
model,
|
|
242
|
+
X_data: pd.DataFrame,
|
|
243
|
+
sample_size: Optional[int] = None,
|
|
244
|
+
class_index: int = 1
|
|
245
|
+
) -> ModelExplainer:
|
|
246
|
+
"""Helper function to initialize ModelExplainer directly."""
|
|
247
|
+
return ModelExplainer(
|
|
248
|
+
model,
|
|
249
|
+
X_data,
|
|
250
|
+
sample_size=sample_size,
|
|
251
|
+
class_index=class_index
|
|
252
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plotting module built on top of SHAP visual outputs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import shap
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def plot_summary(explainer_obj, max_display: int = 10):
|
|
11
|
+
"""Generates a SHAP beeswarm summary plot showing global feature impact."""
|
|
12
|
+
plt.figure()
|
|
13
|
+
shap.plots.beeswarm(explainer_obj.shap_values, max_display=max_display)
|
|
14
|
+
plt.tight_layout()
|
|
15
|
+
plt.show()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def plot_bar(explainer_obj, max_display: int = 10):
|
|
19
|
+
"""Generates a standard bar plot of mean absolute SHAP values."""
|
|
20
|
+
plt.figure()
|
|
21
|
+
shap.plots.bar(explainer_obj.shap_values, max_display=max_display)
|
|
22
|
+
plt.tight_layout()
|
|
23
|
+
plt.show()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def plot_local(explainer_obj, index: int = 0):
|
|
27
|
+
"""Generates a waterfall plot for explaining a specific single row prediction."""
|
|
28
|
+
plt.figure()
|
|
29
|
+
shap.plots.waterfall(explainer_obj.shap_values[index])
|
|
30
|
+
plt.tight_layout()
|
|
31
|
+
plt.show()
|
|
32
|
+
|
|
33
|
+
def plot_dependence(explainer_obj, feature_name: str):
|
|
34
|
+
"""
|
|
35
|
+
Shows how a single feature's value affects SHAP values
|
|
36
|
+
and auto-colors by its strongest interacting feature.
|
|
37
|
+
"""
|
|
38
|
+
plt.figure()
|
|
39
|
+
shap.plots.scatter(explainer_obj.shap_values[:, feature_name], color=explainer_obj.shap_values)
|
|
40
|
+
plt.tight_layout()
|
|
41
|
+
plt.show()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def plot_heatmap(explainer_obj, max_display: int = 10, num_samples: int = 100):
|
|
45
|
+
"""
|
|
46
|
+
Visualizes SHAP values across multiple rows simultaneously as a heatmap.
|
|
47
|
+
"""
|
|
48
|
+
plt.figure()
|
|
49
|
+
shap.plots.heatmap(explainer_obj.shap_values[:num_samples], max_display=max_display)
|
|
50
|
+
plt.tight_layout()
|
|
51
|
+
plt.show()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def plot_decision(explainer_obj, index: int = 0):
|
|
55
|
+
"""
|
|
56
|
+
Shows the step-by-step decision trajectory from base value to final model prediction.
|
|
57
|
+
"""
|
|
58
|
+
plt.figure()
|
|
59
|
+
expected_val = explainer_obj.explainer.expected_value
|
|
60
|
+
if isinstance(expected_val, (list, np.ndarray)):
|
|
61
|
+
expected_val = expected_val[1] if len(expected_val) > 1 else expected_val[0]
|
|
62
|
+
|
|
63
|
+
shap.decision_plot(
|
|
64
|
+
expected_val,
|
|
65
|
+
explainer_obj.shap_values.values[index],
|
|
66
|
+
feature_names=explainer_obj.feature_names
|
|
67
|
+
)
|
|
68
|
+
plt.tight_layout()
|
|
69
|
+
plt.show()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shap_analyzer
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A lightweight wrapper for quick SHAP model explainability analysis.
|
|
5
|
+
Author-email: Abu Saad <abusaadd44@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Requires-Dist: shap>=0.42.0
|
|
8
|
+
Requires-Dist: pandas>=1.5.0
|
|
9
|
+
Requires-Dist: numpy>=1.21.0
|
|
10
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
shap_analyzer/__init__.py
|
|
3
|
+
shap_analyzer/core.py
|
|
4
|
+
shap_analyzer/visualizer.py
|
|
5
|
+
shap_analyzer.egg-info/PKG-INFO
|
|
6
|
+
shap_analyzer.egg-info/SOURCES.txt
|
|
7
|
+
shap_analyzer.egg-info/dependency_links.txt
|
|
8
|
+
shap_analyzer.egg-info/requires.txt
|
|
9
|
+
shap_analyzer.egg-info/top_level.txt
|
|
10
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shap_analyzer
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pytest
|
|
3
|
+
from sklearn.datasets import make_classification
|
|
4
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
5
|
+
|
|
6
|
+
from shap_analyzer import analyze, ModelExplainer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def binary_data_and_model():
|
|
11
|
+
X, y = make_classification(n_samples=100, n_features=4, n_classes=2, random_state=42)
|
|
12
|
+
X_df = pd.DataFrame(X, columns=[f"feat_{i}" for i in range(4)])
|
|
13
|
+
model = RandomForestClassifier(random_state=42).fit(X_df, y)
|
|
14
|
+
return model, X_df
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def multiclass_data_and_model():
|
|
19
|
+
X, y = make_classification(n_samples=100, n_features=4, n_classes=3, n_informative=3, random_state=42)
|
|
20
|
+
X_df = pd.DataFrame(X, columns=[f"feat_{i}" for i in range(4)])
|
|
21
|
+
model = RandomForestClassifier(random_state=42).fit(X_df, y)
|
|
22
|
+
return model, X_df
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_explainer_initialization(binary_data_and_model):
|
|
26
|
+
model, X_df = binary_data_and_model
|
|
27
|
+
exp = analyze(model, X_df)
|
|
28
|
+
assert isinstance(exp, ModelExplainer)
|
|
29
|
+
assert len(exp.feature_names) == 4
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_feature_importance(binary_data_and_model):
|
|
33
|
+
model, X_df = binary_data_and_model
|
|
34
|
+
exp = analyze(model, X_df)
|
|
35
|
+
df_imp = exp.get_feature_importance()
|
|
36
|
+
|
|
37
|
+
assert isinstance(df_imp, pd.DataFrame)
|
|
38
|
+
assert list(df_imp.columns) == ["feature", "mean_shap_value"]
|
|
39
|
+
assert len(df_imp) == 4
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_top_drivers(binary_data_and_model):
|
|
43
|
+
model, X_df = binary_data_and_model
|
|
44
|
+
exp = analyze(model, X_df)
|
|
45
|
+
drivers = exp.get_top_drivers(index=0, top_n=2)
|
|
46
|
+
|
|
47
|
+
assert "positive_drivers" in drivers
|
|
48
|
+
assert "negative_drivers" in drivers
|
|
49
|
+
assert len(drivers["positive_drivers"]) <= 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Currently working on multiclass Models
|
|
54
|
+
# def test_multiclass_handling(multiclass_data_and_model):
|
|
55
|
+
# model, X_df = multiclass_data_and_model
|
|
56
|
+
# exp = analyze(model, X_df, class_index=2)
|
|
57
|
+
|
|
58
|
+
# assert exp.class_index == 2
|
|
59
|
+
# assert exp.shap_values.values.shape[0] == 100
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# def test_out_of_bounds_class_index(multiclass_data_and_model):
|
|
63
|
+
# model, X_df = multiclass_data_and_model
|
|
64
|
+
# with pytest.raises(ValueError):
|
|
65
|
+
# analyze(model, X_df, class_index=99)
|