concordiahub 0.2.1__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.
Files changed (66) hide show
  1. concordiahub/__init__.py +6 -0
  2. concordiahub/_version.py +2 -0
  3. concordiahub/adapters/__init__.py +0 -0
  4. concordiahub/adapters/explainers/__init__.py +1 -0
  5. concordiahub/adapters/explainers/coefficients_adapter.py +90 -0
  6. concordiahub/adapters/explainers/llm_reasoning_adapter.py +325 -0
  7. concordiahub/adapters/explainers/treeshap_adapter.py +155 -0
  8. concordiahub/adapters/explainers/xaimethod_base.py +145 -0
  9. concordiahub/adapters/models/__init__.py +0 -0
  10. concordiahub/adapters/models/llm_adapter.py +27 -0
  11. concordiahub/adapters/models/logisticregression_adapter.py +15 -0
  12. concordiahub/adapters/models/model_base.py +30 -0
  13. concordiahub/adapters/models/xgboost_adapter.py +16 -0
  14. concordiahub/adapters/viz/__init__.py +0 -0
  15. concordiahub/adapters/viz/barchart_viz_adapter.py +73 -0
  16. concordiahub/adapters/viz/tabular_viz_adapter.py +67 -0
  17. concordiahub/adapters/viz/viz_base.py +55 -0
  18. concordiahub/artifacts.py +133 -0
  19. concordiahub/cli.py +31 -0
  20. concordiahub/config.py +103 -0
  21. concordiahub/dashboard/__init__.py +0 -0
  22. concordiahub/dashboard/app.py +23 -0
  23. concordiahub/dashboard/assets/icons.py +55 -0
  24. concordiahub/dashboard/explorer.py +18 -0
  25. concordiahub/dashboard/pages/__init__.py +0 -0
  26. concordiahub/dashboard/pages/dataset.py +168 -0
  27. concordiahub/dashboard/pages/home.py +24 -0
  28. concordiahub/dashboard/pages/instancedetail.py +131 -0
  29. concordiahub/dashboard/pages/instances.py +123 -0
  30. concordiahub/dashboard/settings.py +16 -0
  31. concordiahub/dashboard/utils/__init__.py +0 -0
  32. concordiahub/dashboard/utils/common.py +142 -0
  33. concordiahub/dashboard/utils/dataset_helpers.py +82 -0
  34. concordiahub/dashboard/utils/display_dataframe.py +83 -0
  35. concordiahub/dashboard/utils/display_table.py +104 -0
  36. concordiahub/dashboard/utils/heatmap.py +103 -0
  37. concordiahub/dashboard/utils/home_helpers.py +37 -0
  38. concordiahub/dashboard/utils/instancedetail_helpers.py +148 -0
  39. concordiahub/dashboard/utils/instances_table.py +83 -0
  40. concordiahub/dashboard/utils/layout.py +102 -0
  41. concordiahub/dashboard/utils/matrix_set.py +243 -0
  42. concordiahub/dashboard/utils/plotly_theme.py +211 -0
  43. concordiahub/dashboard/utils/sidebar.py +47 -0
  44. concordiahub/dashboard/utils/styles.py +15 -0
  45. concordiahub/entities/__init__.py +0 -0
  46. concordiahub/entities/context.py +58 -0
  47. concordiahub/entities/dataset.py +334 -0
  48. concordiahub/entities/model.py +127 -0
  49. concordiahub/entities/model_explorer.py +150 -0
  50. concordiahub/exceptions.py +21 -0
  51. concordiahub/registry/__init__.py +0 -0
  52. concordiahub/registry/autodiscover.py +33 -0
  53. concordiahub/registry/model_registry.py +57 -0
  54. concordiahub/registry/viz_registry.py +58 -0
  55. concordiahub/registry/xai_registry.py +57 -0
  56. concordiahub/runner.py +165 -0
  57. concordiahub/storage/__init__.py +0 -0
  58. concordiahub/storage/filesystem_store.py +163 -0
  59. concordiahub/storage/store_base.py +114 -0
  60. concordiahub/strings.py +267 -0
  61. concordiahub-0.2.1.dist-info/METADATA +149 -0
  62. concordiahub-0.2.1.dist-info/RECORD +66 -0
  63. concordiahub-0.2.1.dist-info/WHEEL +5 -0
  64. concordiahub-0.2.1.dist-info/entry_points.txt +2 -0
  65. concordiahub-0.2.1.dist-info/licenses/LICENSE +674 -0
  66. concordiahub-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,6 @@
1
+ from concordiahub.registry.autodiscover import autodiscover_adapters
2
+ from concordiahub.entities.context import ModelContext, ModelData, DataSchema, ResponseSchema
3
+ from concordiahub.runner import run
4
+
5
+ # Register on package import
6
+ autodiscover_adapters()
@@ -0,0 +1,2 @@
1
+ """Package version identifier."""
2
+ __version__ = "0.2.1"
File without changes
@@ -0,0 +1,90 @@
1
+ """
2
+ CoefficientsAdapter class - for model with native interpretability
3
+ """
4
+
5
+ import pandas as pd
6
+ import numpy as np
7
+
8
+ from typing import Any, Optional, Protocol
9
+
10
+ from concordiahub.exceptions import MissingDataError
11
+ from concordiahub.registry.xai_registry import register_xai
12
+ from concordiahub.adapters.explainers.xaimethod_base import XAIMethodAdapter
13
+
14
+ from concordiahub import strings, config
15
+ from concordiahub.entities.context import ModelContext
16
+
17
+ class HasCoefficients(Protocol):
18
+ """
19
+ Helper class for type hinting - the model used for the CoefficientsAdapter
20
+ must have a "coef_" attribute
21
+ """
22
+ coef_: np.ndarray
23
+
24
+ @register_xai("coefficients")
25
+ class CoefficientsAdapter(XAIMethodAdapter):
26
+ """For models with native interpretability"""
27
+
28
+ display_name = strings.COEFFICIENTS_DISPLAY_NAME
29
+ display_notes = strings.COEFFICIENTS_NOTES
30
+ default_visualization = config.COEFFS_DEFAULT_VIZ
31
+ compatible_visualizations = config.COEFFS_COMPATIBLE_VIZ
32
+
33
+
34
+ def explain_local_df(self, ctx: ModelContext, model: Optional[HasCoefficients] = None ) -> pd.DataFrame:
35
+ """Computes the local explanation from the coefficients of the model
36
+
37
+ Args:
38
+ ctx (ModelContext) : ModelContext object
39
+ model (Optional[HasCoefficients]): model exposing a coef_ attribute
40
+
41
+ Returns:
42
+ pd.DataFrame: dataframe containing local explanations
43
+
44
+ Raises:
45
+ MissingDataError: if data.x is not provided
46
+ """
47
+ if ctx.data.x_test is None:
48
+ raise MissingDataError(strings.ERROR_MISSING_MODELDATA.format(method="cls.key", data="x_test"))
49
+ coef = model.coef_[0]
50
+ instance_id = ctx.data.data_schema.instance_id_column
51
+ x_test = ctx.data.x_test.drop(columns=instance_id)
52
+ local_explanations = x_test * coef
53
+ local_explanations.insert(0, instance_id, ctx.data.x_test[instance_id].values)
54
+ return local_explanations
55
+
56
+ def explain_global_df(self, ctx: ModelContext, model: Optional[HasCoefficients] = None) -> pd.DataFrame:
57
+ """
58
+ Computes the global explanation from the coefficients of the model
59
+
60
+ Args:
61
+ ctx (ModelContext): ModelContext object
62
+ model (Optional[HasCoefficients]): model exposing a coef_ attribute
63
+
64
+ Returns:
65
+ pd.DataFrame: dataframe containing global explanation, with two columns, feature and importance
66
+ """
67
+
68
+ coef = model.coef_[0]
69
+ feature_names = ctx.data.x_test.drop(columns=ctx.data.data_schema.instance_id_column).columns
70
+ importance = pd.DataFrame({
71
+ "feature": feature_names,
72
+ "importance": coef,
73
+ })
74
+ return importance
75
+
76
+
77
+ def explain_local_json(self, ctx: ModelContext) -> dict[str, Any]:
78
+ """
79
+ Not implemented for CoefficientsAdapter
80
+
81
+ Args:
82
+ ctx (ModelContext): ModelContext object
83
+
84
+ Returns:
85
+ dict[str, Any]:
86
+
87
+ Raises:
88
+ NotImplementedError:
89
+ """
90
+ raise NotImplementedError
@@ -0,0 +1,325 @@
1
+ """
2
+ LLMReasoningAdapter class - for processing explanations from LLMs
3
+ """
4
+
5
+ import json
6
+
7
+ import pandas as pd
8
+ from typing import Any, Optional
9
+ from dataclasses import dataclass
10
+
11
+ from concordiahub.entities.context import ModelContext, ResponseSchema
12
+ from concordiahub.exceptions import MissingDataError
13
+ from concordiahub.registry.xai_registry import register_xai
14
+ from concordiahub.adapters.explainers.xaimethod_base import XAIMethodAdapter
15
+ from concordiahub import strings, config
16
+ from concordiahub.entities.model import Model
17
+ from concordiahub.config import TreeModel
18
+
19
+ @dataclass
20
+ class ContribScore:
21
+ """
22
+ Dataclass for a score and reasoning contribution
23
+ """
24
+ score: int
25
+ reasoning: str
26
+
27
+ @dataclass
28
+ class LLMResponse:
29
+ """
30
+ Dataclass for a response from LLMs
31
+ """
32
+ reasoning: str
33
+ prediction: int | str
34
+ confidence: float
35
+ contributions: dict[str, ContribScore]
36
+ raw: dict
37
+
38
+ def to_dict(self, instance_id_col: str, instance_id: str) -> dict:
39
+ """
40
+ Formats the response as a dictionary
41
+
42
+ Args:
43
+ instance_id_col (str): name of the instance id column
44
+ instance_id (str): the instance id
45
+
46
+ Returns:
47
+ dict: formatted to a common structure
48
+ """
49
+ return {
50
+ instance_id_col: instance_id,
51
+ "reasoning": self.reasoning,
52
+ "prediction": self.prediction,
53
+ "confidence": self.confidence,
54
+ "contributions": {
55
+ feature: {"score": c.score, "reasoning": c.reasoning}
56
+ for feature, c in self.contributions.items()
57
+ }
58
+ }
59
+
60
+ @register_xai("llm_reasoning")
61
+ class LLMReasoningAdapter(XAIMethodAdapter):
62
+ """
63
+ LLMReasoningAdapter class - for processing local and global explanations
64
+ from LLMs
65
+ """
66
+
67
+ display_name = strings.LLM_REASONING_DISPLAY_NAME
68
+ display_notes = strings.LLM_REASONING_NOTES
69
+ default_visualization = config.LLM_REASONING_DEFAULT_VIZ
70
+ compatible_visualizations = config.LLM_REASONING_COMPATIBLE_VIZ
71
+
72
+ # uses a json file for explanation storage
73
+ explanation_file_type: str = "json"
74
+
75
+ @classmethod
76
+ def load_global_explanation(cls, model: Model) -> pd.DataFrame:
77
+ """
78
+ Returns the global explanation for the specified model
79
+
80
+ Args:
81
+ model (Model): a Model object to get the explanation for
82
+
83
+ Returns:
84
+ pd.DataFrame: dataframe containing the global explanation, with the
85
+ following columns: feature_name, score, score_normalized_max
86
+ """
87
+ df = model.store.load_dataframe(f"exp_global_{cls.key}")
88
+ abs_importance = df["importance"].abs()
89
+ abs_max = abs_importance.max()
90
+ return pd.DataFrame({
91
+ "feature_name": df["feature"],
92
+ "score": df["importance"],
93
+ "score_normalized_max": abs_importance / abs_max if abs_max != 0 else abs_importance,
94
+ })
95
+
96
+ @classmethod
97
+ def load_local_explanation(cls,
98
+ model: Model,
99
+ instance_id: str) -> pd.DataFrame:
100
+ """
101
+ Returns the local explanation for a given instance
102
+
103
+ Args:
104
+ model (Model): a Model object
105
+ instance_id (str): identifier for the instance to get the local
106
+ explanation for
107
+
108
+ Returns:
109
+ pd.DataFrame: dataframe containing the local explanation, with the
110
+ following columns: feature_name, score, score_normalized_sum,
111
+ score_normalized_max, reasoning, overall_reasoning,
112
+ overall_confidence
113
+
114
+ Raises:
115
+ ValueError: if instance_id is not provided
116
+ """
117
+ records = model.store.load_json(f"exp_local_{cls.key}")
118
+ instance_id_col = model.get(config.DATA_SCHEMA_ID_COLUMN)
119
+
120
+ row = next((r for r in records if r[instance_id_col] == instance_id),
121
+ None)
122
+ if row is None:
123
+ raise MissingDataError(strings.ERROR_INSTANCE_NOT_FOUND.format(
124
+ instance_id=instance_id, key=cls.key))
125
+
126
+
127
+ # make sure data[score] treaded as a proper float value
128
+ contributions = list(row["contributions"].items())
129
+ scores = [float(data["score"]) for _, data in contributions]
130
+
131
+ abs_max = max(abs(s) for s in scores)
132
+
133
+ overall_reasoning = row["reasoning"]
134
+ overall_confidence = row["confidence"]
135
+
136
+ return pd.DataFrame([
137
+ {
138
+ "feature_name": feature,
139
+ "score": score,
140
+ "score_normalized_max": score / abs_max if abs_max != 0 else score,
141
+ "reasoning": data["reasoning"],
142
+ "overall_reasoning": overall_reasoning,
143
+ "overall_confidence": overall_confidence
144
+ }
145
+ for (feature, data), score in zip(contributions, scores)
146
+ ])
147
+
148
+ def explain_global_json(self,
149
+ ctx: ModelContext,
150
+ _model: Optional[TreeModel] = None) -> pd.DataFrame:
151
+ """Generates the global explanations
152
+
153
+ Importance is the mean absolute contribution score across all
154
+ instances: each feature's absolute scores are summed and divided by
155
+ the total instance count, so features not named in an instance count
156
+ as zero. This matches the aggregation SHAP applies to its own values.
157
+ Occurrence counts are returned alongside but do not affect importance.
158
+
159
+ Args:
160
+ ctx (ModelContext) : ModelContext object
161
+ _model (Optional[TreeModel]) : Unused
162
+
163
+ Returns:
164
+ pd.DataFrame: dataframe containing global explanations, with the
165
+ following columns: feature, importance, occurrence_count,
166
+ occurrence_rate, n_instances
167
+
168
+ Raises:
169
+ MissingDataError: if data.y_exp is not provided
170
+ """
171
+ if ctx.data.y_exp is None:
172
+ raise MissingDataError(
173
+ "LLMReasoningAdapter requires y_exp in ModelData")
174
+ if ctx.response_schema is None:
175
+ raise MissingDataError(
176
+ "LLMReasoningAdapter requires a ResponseSchema in ModelContext")
177
+
178
+ results = {}
179
+ n_instances = 0
180
+
181
+ for _, row in ctx.data.y_exp.iterrows():
182
+ raw = json.loads(row["explanation"])
183
+ n_instances += 1
184
+
185
+ response = self._reformat(raw, ctx.response_schema, prediction=None)
186
+
187
+ for feature, contrib in response.contributions.items():
188
+ if feature not in results:
189
+ results[feature] = {"sum_abs_score": 0,
190
+ "occurrence_count": 0}
191
+ results[feature]["sum_abs_score"] += abs(float(contrib.score))
192
+ results[feature]["occurrence_count"] += 1
193
+
194
+ rows = []
195
+ for feature, vals in results.items():
196
+ rows.append({
197
+ "feature": feature,
198
+ "importance": vals["sum_abs_score"] / n_instances,
199
+ "occurrence_count": vals["occurrence_count"],
200
+ "occurrence_rate": vals["occurrence_count"] / n_instances,
201
+ "n_instances": n_instances,
202
+ })
203
+
204
+ return pd.DataFrame(rows)
205
+
206
+
207
+ def explain_global_df(self,
208
+ ctx: ModelContext,
209
+ model: Optional[TreeModel] = None) -> pd.DataFrame:
210
+ """
211
+ Function is not implemented for this class instance
212
+
213
+ Args:
214
+ ctx (ModelContext): unused
215
+ model (Optional[TreeModel]): unused
216
+
217
+ Returns:
218
+ pd.DataFrame:
219
+
220
+ Raises:
221
+ NotImplementedError: always returns this as not implemented
222
+
223
+ """
224
+ raise NotImplementedError
225
+
226
+ def explain_local_df(self,
227
+ ctx: ModelContext,
228
+ model: Optional[TreeModel] = None) -> pd.DataFrame:
229
+ """
230
+ Function is not implemented for this class instance
231
+
232
+ Args:
233
+ ctx (ModelContext): unused
234
+ model (Optional[TreeModel]): unused
235
+
236
+ Returns:
237
+ pd.DataFrame:
238
+
239
+ Raises:
240
+ NotImplementedError: always returns this as not implemented
241
+
242
+ """
243
+ raise NotImplementedError
244
+
245
+ def explain_local_json(self,
246
+ ctx: ModelContext) -> list[dict[str, Any]]:
247
+ """
248
+ Processes the local explanations
249
+
250
+ Args:
251
+ ctx (ModelContext): ModelContext object
252
+
253
+ Returns:
254
+ list[dict[str, Any]]: list of dicts containing local explanations
255
+
256
+ Raises:
257
+ MissingDataError: if data.x is not provided
258
+ """
259
+ if ctx.data.y_exp is None or ctx.data.y_pred is None:
260
+ raise MissingDataError(strings.ERROR_MISSING_MODELDATA.format(
261
+ method="cls.key", data="y_exp, y_pred"))
262
+ if ctx.response_schema is None:
263
+ raise MissingDataError(
264
+ "LLMReasoningAdapter requires a ResponseSchema in ModelContext")
265
+
266
+ instance_id_col = ctx.data.data_schema.instance_id_column
267
+ prediction_col = ctx.data.data_schema.prediction_column
268
+ y_pred = ctx.data.y_pred
269
+ output = []
270
+
271
+ for _, row in ctx.data.y_exp.iterrows():
272
+ raw = json.loads(row["explanation"])
273
+ instance_id = row[instance_id_col]
274
+ match = y_pred.loc[y_pred[instance_id_col] == instance_id, prediction_col]
275
+ # if a prediction for particular instance is missing, then just continue
276
+ if match.empty:
277
+ continue
278
+ prediction = match.iloc[0].item() if hasattr(
279
+ match.iloc[0], 'item') else match.iloc[0]
280
+ response = self._reformat(raw, ctx.response_schema, prediction)
281
+ output.append(response.to_dict(instance_id_col, instance_id))
282
+ return output
283
+
284
+ @staticmethod
285
+ def _reformat(raw: dict,
286
+ schema: ResponseSchema,
287
+ prediction: str | None) -> LLMResponse:
288
+ """
289
+ Private helper function to reformat an explanation in raw dict format
290
+ (from JSON) into an LLMResponse object.
291
+ This uses the DataSchema definition to process the specific dict
292
+ structure into a common LLMResponse object structure.
293
+
294
+ Args:
295
+ raw (dict): dict
296
+ schema (ResponseSchema): response schema:
297
+ prediction (str|None): prediction, if defined
298
+
299
+ Returns:
300
+ LLMResponse:
301
+ """
302
+ raw_contribs = raw[schema.contributions_field]
303
+ if schema.contribution_key is not None:
304
+ contributions = {
305
+ item[schema.contribution_key]: ContribScore(
306
+ score=item["score"],
307
+ reasoning=item["reasoning"],
308
+ )
309
+ for item in raw_contribs
310
+ }
311
+ else:
312
+ contributions = {
313
+ feature: ContribScore(
314
+ score=values["score"],
315
+ reasoning=values.get("reasoning", ""),
316
+ )
317
+ for feature, values in raw_contribs.items()
318
+ }
319
+ return LLMResponse(
320
+ reasoning=raw["reasoning"],
321
+ prediction=prediction,
322
+ confidence=raw["confidence"],
323
+ contributions=contributions,
324
+ raw=raw,
325
+ )
@@ -0,0 +1,155 @@
1
+ """
2
+ TreeSHAPAdapter class - for processing SHAP explanations for tree based models
3
+ """
4
+
5
+ import shap
6
+ import pandas as pd
7
+ from typing import Any, Optional
8
+
9
+ from concordiahub.exceptions import MissingDataError
10
+ from concordiahub.entities.context import ModelContext
11
+ from concordiahub.registry.xai_registry import register_xai
12
+ from concordiahub.adapters.explainers.xaimethod_base import XAIMethodAdapter
13
+ from concordiahub import strings, config
14
+ from concordiahub.entities.model import Model
15
+ from concordiahub.config import TreeModel
16
+
17
+
18
+ @register_xai("tree_shap")
19
+ class TreeSHAPAdapter(XAIMethodAdapter):
20
+ """
21
+ Tree SHAP Explainer
22
+ """
23
+
24
+ display_name = strings.TREE_SHAP_DISPLAY_NAME
25
+ display_notes = strings.TREE_SHAP_NOTES
26
+ default_visualization = config.TREE_SHAP_DEFAULT_VIZ
27
+ compatible_visualizations = config.TREE_SHAP_COMPATIBLE_VIZ
28
+
29
+ # SHAP adds a "class" column, so this needs to be removed
30
+ EXCLUDED_CONTRIBUTION_KEYS = {"class"}
31
+
32
+ @classmethod
33
+ def load_local_explanation(cls, model: Model, instance_id: str) -> pd.DataFrame:
34
+ """
35
+ Loads local explanation for given instance_id
36
+
37
+ Args:
38
+ model (Model): Model object
39
+ instance_id (str): Instance to get the explanation for
40
+
41
+ Returns:
42
+ pd.DataFrame: dataframe containing local explanations
43
+ """
44
+
45
+ df = super().load_local_explanation(model, instance_id)
46
+ return df[~df["feature_name"].isin(cls.EXCLUDED_CONTRIBUTION_KEYS)]
47
+
48
+ def explain_global_df(self, ctx: ModelContext, model: Optional[TreeModel] = None) -> pd.DataFrame:
49
+ """
50
+ Computes the global explanation from SHAP values
51
+
52
+ Args:
53
+ ctx (ModelContext): ModelContext object
54
+ model (Optional[TreeModel]): trained tree-based model object
55
+
56
+ Returns:
57
+ pd.DataFrame: dataframe containing global explanations
58
+
59
+ Raises:
60
+ MissingDataError: if data.x_test is not provided
61
+ DataProcessingError: if errors in processing SHAP values
62
+ """
63
+ if not hasattr(ctx.data, "x_test") or ctx.data.x_test is None:
64
+ raise MissingDataError(strings.ERROR_MISSING_MODELDATA.format(
65
+ method="cls.key", data="x_test"))
66
+ instance_id = ctx.data.data_schema.instance_id_column
67
+ data = ctx.data.x_test.copy()
68
+ data = data.drop(columns=[instance_id]).select_dtypes(include=['number'])
69
+ explainer = shap.TreeExplainer(model)
70
+ shap_values = explainer.shap_values(data)
71
+
72
+ if shap_values.ndim == 2:
73
+ importance = pd.DataFrame(shap_values, columns=data.columns).abs().mean(axis=0)
74
+ result = importance.reset_index()
75
+ result.columns = ['feature', 'importance']
76
+ return result
77
+
78
+ else:
79
+ n_classes = shap_values.shape[2]
80
+ dfs = []
81
+ for i in range(n_classes):
82
+ importance = pd.DataFrame(shap_values[:, :, i], columns=data.columns).abs().mean(axis=0)
83
+ df = importance.reset_index()
84
+ df.columns = ['feature', 'importance']
85
+ dfs.append(df)
86
+ return pd.concat(dfs, ignore_index=True)
87
+
88
+
89
+ def explain_local_df(self, ctx: ModelContext, model: Optional[TreeModel] = None) -> pd.DataFrame:
90
+ """
91
+ Computes the local explanations from SHAP values of all the instances in the test set
92
+
93
+ Args:
94
+ ctx (ModelContext): ModelContext object
95
+ model (Optional[TreeModel]): trained tree-based model object
96
+
97
+ Returns:
98
+ pd.DataFrame: dataframe containing local explanations
99
+
100
+ Raises:
101
+ MissingDataError: if data.x_test is not provided
102
+ DataProcessingError: if errors in processing SHAP values
103
+ """
104
+
105
+ if not hasattr(ctx.data, "x_test") or ctx.data.x_test is None:
106
+ raise MissingDataError(strings.ERROR_MISSING_MODELDATA.format(
107
+ method="cls.key", data="x_test"))
108
+
109
+ instance_id = ctx.data.data_schema.instance_id_column
110
+ data = ctx.data.x_test.copy()
111
+ uuids = data[instance_id].values
112
+ data = data.drop(columns=[instance_id]).select_dtypes(include=['number'])
113
+
114
+ explainer = shap.TreeExplainer(model)
115
+ shap_values = explainer.shap_values(data)
116
+
117
+ # (n_instances, 1, n_features)
118
+ if shap_values.ndim == 2:
119
+ result = pd.DataFrame(shap_values, columns=data.columns)
120
+ result.insert(0, instance_id, uuids)
121
+ result['class'] = 0
122
+
123
+ # SHAP returns (n_instances, n_features, n_classes) for multiclass tree models
124
+ # slice last axis per class to get (n_instances, n_features) per DataFrame
125
+ else:
126
+ n_classes = shap_values.shape[2]
127
+ dfs = []
128
+ keys = ctx.data.data_schema.class_names if ctx.data.data_schema.class_names is not None else range(
129
+ n_classes)
130
+ for i in range(n_classes):
131
+ df = pd.DataFrame(shap_values[:, :, i], columns=data.columns)
132
+ df.insert(0, instance_id, uuids)
133
+ df['class'] = keys[i]
134
+ dfs.append(df)
135
+ result = pd.concat(dfs, ignore_index=True)
136
+
137
+
138
+ # filter by abs value
139
+ return result
140
+
141
+
142
+ def explain_local_json(self, ctx: ModelContext) -> dict[str, Any]:
143
+ """
144
+ Not implemented for TreeSHAPAdapter
145
+
146
+ Args:
147
+ ctx (ModelContext): ModelContext object
148
+
149
+ Returns:
150
+ dict[str, Any]:
151
+
152
+ Raises:
153
+ NotImplementedError: will always return this as not implemented
154
+ """
155
+ raise NotImplementedError