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.
- concordiahub/__init__.py +6 -0
- concordiahub/_version.py +2 -0
- concordiahub/adapters/__init__.py +0 -0
- concordiahub/adapters/explainers/__init__.py +1 -0
- concordiahub/adapters/explainers/coefficients_adapter.py +90 -0
- concordiahub/adapters/explainers/llm_reasoning_adapter.py +325 -0
- concordiahub/adapters/explainers/treeshap_adapter.py +155 -0
- concordiahub/adapters/explainers/xaimethod_base.py +145 -0
- concordiahub/adapters/models/__init__.py +0 -0
- concordiahub/adapters/models/llm_adapter.py +27 -0
- concordiahub/adapters/models/logisticregression_adapter.py +15 -0
- concordiahub/adapters/models/model_base.py +30 -0
- concordiahub/adapters/models/xgboost_adapter.py +16 -0
- concordiahub/adapters/viz/__init__.py +0 -0
- concordiahub/adapters/viz/barchart_viz_adapter.py +73 -0
- concordiahub/adapters/viz/tabular_viz_adapter.py +67 -0
- concordiahub/adapters/viz/viz_base.py +55 -0
- concordiahub/artifacts.py +133 -0
- concordiahub/cli.py +31 -0
- concordiahub/config.py +103 -0
- concordiahub/dashboard/__init__.py +0 -0
- concordiahub/dashboard/app.py +23 -0
- concordiahub/dashboard/assets/icons.py +55 -0
- concordiahub/dashboard/explorer.py +18 -0
- concordiahub/dashboard/pages/__init__.py +0 -0
- concordiahub/dashboard/pages/dataset.py +168 -0
- concordiahub/dashboard/pages/home.py +24 -0
- concordiahub/dashboard/pages/instancedetail.py +131 -0
- concordiahub/dashboard/pages/instances.py +123 -0
- concordiahub/dashboard/settings.py +16 -0
- concordiahub/dashboard/utils/__init__.py +0 -0
- concordiahub/dashboard/utils/common.py +142 -0
- concordiahub/dashboard/utils/dataset_helpers.py +82 -0
- concordiahub/dashboard/utils/display_dataframe.py +83 -0
- concordiahub/dashboard/utils/display_table.py +104 -0
- concordiahub/dashboard/utils/heatmap.py +103 -0
- concordiahub/dashboard/utils/home_helpers.py +37 -0
- concordiahub/dashboard/utils/instancedetail_helpers.py +148 -0
- concordiahub/dashboard/utils/instances_table.py +83 -0
- concordiahub/dashboard/utils/layout.py +102 -0
- concordiahub/dashboard/utils/matrix_set.py +243 -0
- concordiahub/dashboard/utils/plotly_theme.py +211 -0
- concordiahub/dashboard/utils/sidebar.py +47 -0
- concordiahub/dashboard/utils/styles.py +15 -0
- concordiahub/entities/__init__.py +0 -0
- concordiahub/entities/context.py +58 -0
- concordiahub/entities/dataset.py +334 -0
- concordiahub/entities/model.py +127 -0
- concordiahub/entities/model_explorer.py +150 -0
- concordiahub/exceptions.py +21 -0
- concordiahub/registry/__init__.py +0 -0
- concordiahub/registry/autodiscover.py +33 -0
- concordiahub/registry/model_registry.py +57 -0
- concordiahub/registry/viz_registry.py +58 -0
- concordiahub/registry/xai_registry.py +57 -0
- concordiahub/runner.py +165 -0
- concordiahub/storage/__init__.py +0 -0
- concordiahub/storage/filesystem_store.py +163 -0
- concordiahub/storage/store_base.py +114 -0
- concordiahub/strings.py +267 -0
- concordiahub-0.2.1.dist-info/METADATA +149 -0
- concordiahub-0.2.1.dist-info/RECORD +66 -0
- concordiahub-0.2.1.dist-info/WHEEL +5 -0
- concordiahub-0.2.1.dist-info/entry_points.txt +2 -0
- concordiahub-0.2.1.dist-info/licenses/LICENSE +674 -0
- concordiahub-0.2.1.dist-info/top_level.txt +1 -0
concordiahub/__init__.py
ADDED
concordiahub/_version.py
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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
|