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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,4 @@
1
+ shap>=0.42.0
2
+ pandas>=1.5.0
3
+ numpy>=1.21.0
4
+ matplotlib>=3.5.0
@@ -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)