xailens 0.1.2__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 (68) hide show
  1. xai_compare/__init__.py +6 -0
  2. xai_compare/_version.py +2 -0
  3. xai_compare/adapters/__init__.py +0 -0
  4. xai_compare/adapters/comparison/__init__.py +0 -0
  5. xai_compare/adapters/comparison/coefficient_treeshap_adapter.py +56 -0
  6. xai_compare/adapters/comparison/comparison_base.py +28 -0
  7. xai_compare/adapters/explainers/__init__.py +1 -0
  8. xai_compare/adapters/explainers/coefficients_adapter.py +49 -0
  9. xai_compare/adapters/explainers/cor_adapter.py +107 -0
  10. xai_compare/adapters/explainers/kernelshap_adapter.py +37 -0
  11. xai_compare/adapters/explainers/treeshap_adapter.py +77 -0
  12. xai_compare/adapters/explainers/xaimethod_base.py +64 -0
  13. xai_compare/adapters/models/__init__.py +0 -0
  14. xai_compare/adapters/models/llm_adapter.py +11 -0
  15. xai_compare/adapters/models/logisticregression_adapter.py +8 -0
  16. xai_compare/adapters/models/model_base.py +15 -0
  17. xai_compare/adapters/models/xgboost_adapter.py +8 -0
  18. xai_compare/adapters/viz/__init__.py +0 -0
  19. xai_compare/adapters/viz/forceplot_viz_adapter.py +40 -0
  20. xai_compare/adapters/viz/viz_base.py +15 -0
  21. xai_compare/adapters/viz/waterfall_viz_adapter.py +38 -0
  22. xai_compare/artifacts.py +70 -0
  23. xai_compare/cli.py +10 -0
  24. xai_compare/config.py +39 -0
  25. xai_compare/constants.py +12 -0
  26. xai_compare/context.py +74 -0
  27. xai_compare/dashboard/__init__.py +0 -0
  28. xai_compare/dashboard/app.py +16 -0
  29. xai_compare/dashboard/components/__init__.py +0 -0
  30. xai_compare/dashboard/components/breadcrumb.py +24 -0
  31. xai_compare/dashboard/components/formatting.py +39 -0
  32. xai_compare/dashboard/components/layout.py +21 -0
  33. xai_compare/dashboard/components/run_display.py +62 -0
  34. xai_compare/dashboard/components/sidebar.py +62 -0
  35. xai_compare/dashboard/components/styles.py +9 -0
  36. xai_compare/dashboard/pages/__init__.py +0 -0
  37. xai_compare/dashboard/pages/detail.py +83 -0
  38. xai_compare/dashboard/pages/home.py +16 -0
  39. xai_compare/dashboard/pages/overview.py +49 -0
  40. xai_compare/dashboard/services/__init__.py +1 -0
  41. xai_compare/dashboard/services/explorer.py +8 -0
  42. xai_compare/dashboard/utils/__init__.py +0 -0
  43. xai_compare/dashboard/utils/comparison.py +66 -0
  44. xai_compare/dashboard/utils/params.py +21 -0
  45. xai_compare/encoder.py +18 -0
  46. xai_compare/exceptions.py +12 -0
  47. xai_compare/registry/__init__.py +0 -0
  48. xai_compare/registry/autodiscover.py +29 -0
  49. xai_compare/registry/comparison_registry.py +22 -0
  50. xai_compare/registry/model_registry.py +19 -0
  51. xai_compare/registry/viz_registry.py +18 -0
  52. xai_compare/registry/xai_registry.py +18 -0
  53. xai_compare/run.py +62 -0
  54. xai_compare/run_explorer.py +117 -0
  55. xai_compare/runner.py +59 -0
  56. xai_compare/services/__init__.py +0 -0
  57. xai_compare/services/comparison.py +100 -0
  58. xai_compare/services/preprocess.py +12 -0
  59. xai_compare/storage/__init__.py +0 -0
  60. xai_compare/storage/filesystem_store.py +40 -0
  61. xai_compare/storage/store_base.py +23 -0
  62. xai_compare/strings.py +38 -0
  63. xailens-0.1.2.dist-info/METADATA +79 -0
  64. xailens-0.1.2.dist-info/RECORD +68 -0
  65. xailens-0.1.2.dist-info/WHEEL +5 -0
  66. xailens-0.1.2.dist-info/entry_points.txt +2 -0
  67. xailens-0.1.2.dist-info/licenses/LICENSE +674 -0
  68. xailens-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,6 @@
1
+ from xai_compare.registry.autodiscover import autodiscover_adapters
2
+ from xai_compare.context import RunContext, RunData, DataSchema, ResponseSchema
3
+ from xai_compare.runner import run
4
+
5
+ # Register on package import
6
+ autodiscover_adapters()
@@ -0,0 +1,2 @@
1
+ # ./_version.py
2
+ __version__ = "0.1.2"
File without changes
File without changes
@@ -0,0 +1,56 @@
1
+ import plotly.graph_objects as go
2
+
3
+ from xai_compare.context import ComparisonContext
4
+ from xai_compare.registry.comparison_registry import register_comparison
5
+ from xai_compare.adapters.comparison.comparison_base import ComparisonAdapter
6
+ from xai_compare.dashboard.components.formatting import format_model_name
7
+ from xai_compare import config, context
8
+
9
+
10
+ @register_comparison("coefficients", "shap_tree")
11
+ class CoefficientTreeShapComparison(ComparisonAdapter):
12
+ """For comparing coefficients with shap tree"""
13
+
14
+
15
+ def _viz_diff_bar(self, features_a, features_b, run_a, run_b, axis_range=None):
16
+ raise NotImplementedError
17
+
18
+ def _viz_dumbbell(self, features_a, features_b, run_a, run_b, axis_range=None):
19
+ raise NotImplementedError
20
+
21
+ def _viz_alignment_scale(self, ctx: ComparisonContext ):
22
+ diff = ctx.features_a.sub(ctx.features_b, fill_value=0)
23
+ diff = diff.sort_values()
24
+
25
+ colors = [config.PLOT_LEFT_COLOR if v < 0 else config.PLOT_RIGHT_COLOR for v in diff]
26
+
27
+ fig = go.Figure(go.Bar(
28
+ x=diff.values,
29
+ y=diff.index,
30
+ orientation='h',
31
+ marker_color=colors,
32
+ ))
33
+
34
+ label_a = format_model_name(ctx.run_a)
35
+ label_b = format_model_name(ctx.run_b)
36
+
37
+ fig.update_layout(
38
+ title=f"Explanation Difference: {label_a} − {label_b}",
39
+ xaxis_title=f"Attribution difference ({label_a} − {label_b})",
40
+ xaxis=config.PLOT_ZERO_LINE,
41
+ yaxis_title="Feature",
42
+ height=400 + len(diff) * 15,
43
+ )
44
+ if ctx.axis_range:
45
+ fig.update_layout(xaxis={"range": ctx.axis_range})
46
+ return fig
47
+
48
+ visualisations_available = {
49
+ "Difference (bar)": _viz_diff_bar,
50
+ "Dumbbell": _viz_dumbbell,
51
+ "Alignment Scale": _viz_alignment_scale
52
+ }
53
+
54
+ def render_comparison(self, ctx: context.ComparisonContext, viz_type):
55
+
56
+ return self.visualisations_available[viz_type](self, ctx)
@@ -0,0 +1,28 @@
1
+ import pandas as pd
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any
4
+
5
+ from xai_compare.context import ComparisonContext
6
+ from xai_compare.services.preprocess import normalise_values
7
+
8
+
9
+ class ComparisonAdapter(ABC):
10
+ """Handles comparison between two XAI explanations."""
11
+
12
+ compatible_pairs: set[frozenset[str]]
13
+ visualisations_available = []
14
+
15
+ @classmethod
16
+ def get_abs_max(cls, explanation_a: pd.Series, explanation_b: pd.Series, sample_id_column) -> float:
17
+ """ calculate absolute max value from explanation
18
+ required for ensuring charts use the same scales """
19
+ features_a = normalise_values(explanation_a, sample_id_column)
20
+ features_b = normalise_values(explanation_b, sample_id_column)
21
+ a = pd.to_numeric(features_a, errors='coerce')
22
+ b = pd.to_numeric(features_b, errors='coerce')
23
+ diff = a.sub(b, fill_value=0)
24
+ return diff.abs().max()
25
+
26
+ @abstractmethod
27
+ def render_comparison(self, ctx: ComparisonContext, viz_type) -> Any:
28
+ raise NotImplementedError
@@ -0,0 +1,49 @@
1
+
2
+ import pandas as pd
3
+ from typing import Any
4
+
5
+ from xai_compare.context import RunData
6
+ from xai_compare.exceptions import MissingDataError
7
+ from xai_compare.registry.xai_registry import register_xai
8
+ from xai_compare.adapters.explainers.xaimethod_base import XAIMethodAdapter
9
+ from xai_compare.run import Run
10
+ from xai_compare import constants, strings, config
11
+ from xai_compare.context import RunContext
12
+
13
+
14
+ @register_xai("coefficients")
15
+ class CoefficientsAdapter(XAIMethodAdapter):
16
+ """For models with native interpretability"""
17
+
18
+ display_name = strings.COEFFICIENTS_DISPLAY_NAME
19
+ default_visualisation = config.COEFFICIENTS_DEFAULT_VIZ
20
+ compatible_visualisations = config.COEFFICIENTS_COMPATIBLE_VIZ
21
+
22
+ @classmethod
23
+ def load_explanation(cls, run: Run, sample_id: str):
24
+ df = run.store.load_dataframe(f"explanations_{cls.key}")
25
+ row = df[df[run.get(constants.DATA_SCHEMA_ID_COLUMN)] == sample_id]
26
+ if row.empty:
27
+ raise MissingDataError(f"Sample '{sample_id}' not found in explanations for {cls.key}")
28
+ return row.iloc[0]
29
+
30
+ def explain_local_df(self, ctx: RunContext, model=None) -> pd.DataFrame:
31
+ """Computes the local explanation from the coefficients of the model
32
+ Args:
33
+ ctx: RunContext object
34
+ model: Trained model object
35
+
36
+ Returns:
37
+ pd.DataFrame: dataframe containing local explanations
38
+
39
+ Raises:
40
+ MissingDataError: if data.x is not provided
41
+ """
42
+ if ctx.data.x is None:
43
+ raise MissingDataError("CoefficientsAdapter requires x in RunData")
44
+ coef = model.coef_[0]
45
+ local_explanations = ctx.data.x * coef
46
+ return local_explanations
47
+
48
+ def explain_local_json(self, ctx: RunContext) -> dict[str, Any]:
49
+ raise NotImplementedError
@@ -0,0 +1,107 @@
1
+ import json
2
+
3
+ import pandas as pd
4
+ from typing import Any
5
+ from dataclasses import dataclass
6
+
7
+ from xai_compare.context import RunContext, ResponseSchema
8
+ from xai_compare.exceptions import MissingDataError
9
+ from xai_compare.registry.xai_registry import register_xai
10
+ from xai_compare.adapters.explainers.xaimethod_base import XAIMethodAdapter
11
+ from xai_compare import strings, config
12
+
13
+
14
+ @dataclass
15
+ class ContribScore:
16
+ score: int
17
+ reasoning: str
18
+
19
+ @dataclass
20
+ class LLMResponse:
21
+ reasoning: str
22
+ prediction: int | str
23
+ confidence: float
24
+ contributions: dict[str, ContribScore]
25
+ raw: dict
26
+
27
+ def to_dict(self, sample_id_col: str, sample_id: str) -> dict:
28
+ return {
29
+ sample_id_col: sample_id,
30
+ "reasoning": self.reasoning,
31
+ "prediction": self.prediction,
32
+ "confidence": self.confidence,
33
+ "contributions": {
34
+ feature: {"score": c.score, "reasoning": c.reasoning}
35
+ for feature, c in self.contributions.items()
36
+ }
37
+ }
38
+
39
+ @register_xai("cor")
40
+ class CoRAdapter(XAIMethodAdapter):
41
+ """For chain-of-reasoning/chain-of-thought explanations"""
42
+
43
+ display_name = strings.COR_DISPLAY_NAME
44
+ default_visualisation = config.COR_DEFAULT_VIZ
45
+ compatible_visualisations = config.COR_COMPATIBLE_VIZ
46
+
47
+ # no features as such so will process the explanations in this adapter
48
+ requires_encoded_features: bool = False
49
+
50
+ # uses a json file for explanation storage
51
+ explanation_file_type: str = "json"
52
+
53
+ def explain_local_df(self, ctx: RunContext, model=None) -> pd.DataFrame:
54
+ raise NotImplementedError
55
+
56
+ def explain_local_json(self, ctx: RunContext) -> list[dict[str, Any]]:
57
+ """Processes the CoR local explanations
58
+ Args:
59
+ ctx: RunContext object
60
+
61
+ Returns:
62
+ list[dict[str, Any]]: list of dicts containing local explanations
63
+
64
+ Raises:
65
+ MissingDataError: if data.x is not provided
66
+ """
67
+ if ctx.data.y_exp is None:
68
+ raise MissingDataError("CoRAdapter requires y_exp in RunData")
69
+ if ctx.response_schema is None:
70
+ raise MissingDataError(
71
+ "CoRAdapter requires a ResponseSchema on RunContext")
72
+
73
+ sample_id_col = ctx.data.data_schema.sample_id_column
74
+ output = []
75
+ for idx, row in ctx.data.y_exp.iterrows():
76
+ raw = json.loads(row["explanation"])
77
+ response = self._normalise(raw, ctx.response_schema)
78
+ output.append(response.to_dict(sample_id_col, row[sample_id_col]))
79
+ return output
80
+
81
+ def _normalise(self, raw: dict, schema: ResponseSchema) -> LLMResponse:
82
+ raw_contribs = raw[schema.contributions_field]
83
+
84
+ if schema.contribution_key is not None:
85
+ contributions = {
86
+ item[schema.contribution_key]: ContribScore(
87
+ score=item["score"],
88
+ reasoning=item["reasoning"],
89
+ )
90
+ for item in raw_contribs
91
+ }
92
+ else:
93
+ contributions = {
94
+ feature: ContribScore(
95
+ score=values["score"],
96
+ reasoning=values["reasoning"],
97
+ )
98
+ for feature, values in raw_contribs.items()
99
+ }
100
+
101
+ return LLMResponse(
102
+ reasoning=raw["reasoning"],
103
+ prediction=raw["prediction"],
104
+ confidence=raw["confidence"],
105
+ contributions=contributions,
106
+ raw=raw,
107
+ )
@@ -0,0 +1,37 @@
1
+ import shap
2
+ import pandas as pd
3
+ from typing import Any
4
+
5
+ from xai_compare.exceptions import MissingDataError
6
+ from xai_compare.context import RunContext
7
+ from xai_compare.registry.xai_registry import register_xai
8
+ from xai_compare.adapters.explainers.xaimethod_base import XAIMethodAdapter
9
+ from xai_compare import config, strings
10
+
11
+ @register_xai("shap_kernel")
12
+ class KernalSHAPAdapter(XAIMethodAdapter):
13
+ """Model-agnostic Kernel Shap Explainer"""
14
+
15
+ display_name = strings.KERNEL_SHAP_DISPLAY_NAME
16
+ default_visualisation = config.KERNEL_SHAP_DEFAULT_VIZ
17
+ compatible_visualisations = config.KERNEL_SHAP_COMPATIBLE_VIZ
18
+
19
+ def explain_local_df(self, ctx: RunContext, model=None) -> pd.DataFrame:
20
+ """Computes the local explanation using KernelShap method
21
+ Args:
22
+ ctx: RunContext object
23
+ model: Trained model object
24
+
25
+ Returns:
26
+ pd.DataFrame: dataframe containing local explanations
27
+
28
+ Raises:
29
+ MissingDataError: if x_train or x_test is not provided
30
+ """
31
+ if ctx.data.x_train is None or ctx.data.x_test is None:
32
+ raise MissingDataError("KernalSHAPAdapter requires x_train and x_test in RunData")
33
+ explainer = shap.KernelExplainer(model.predict_proba, ctx.data.x_train)
34
+ return explainer.shap_values(ctx.data.x_test)
35
+
36
+ def explain_local_json(self, data: RunContext) -> dict[str, Any]:
37
+ raise NotImplementedError
@@ -0,0 +1,77 @@
1
+ import shap
2
+ import pandas as pd
3
+ from typing import Any
4
+
5
+ from xai_compare.exceptions import MissingDataError
6
+ from xai_compare.context import RunContext
7
+ from xai_compare.registry.xai_registry import register_xai
8
+ from xai_compare.adapters.explainers.xaimethod_base import XAIMethodAdapter
9
+ from xai_compare.run import Run
10
+ from xai_compare import constants, strings, config
11
+
12
+ @register_xai("shap_tree")
13
+ class TreeSHAPAdapter(XAIMethodAdapter):
14
+ """Tree Shap Explainer"""
15
+
16
+ display_name = strings.TREE_SHAP_DISPLAY_NAME
17
+ default_visualisation = config.TREE_SHAP_DEFAULT_VIZ
18
+ compatible_visualisations = config.TREE_SHAP_COMPATIBLE_VIZ
19
+
20
+ @classmethod
21
+ def load_explanation(cls, run: Run, sample_id: str):
22
+ df = run.store.load_dataframe(f"explanations_{cls.key}")
23
+ row = df[df[run.get(constants.DATA_SCHEMA_ID_COLUMN)] == sample_id]
24
+ if row.empty:
25
+ raise ValueError(f"Sample '{sample_id}' not found in explanations for {cls.key}")
26
+ return row.iloc[0]
27
+
28
+
29
+ def explain_local_df(self, ctx: RunContext, model=None) -> pd.DataFrame:
30
+ """Computes the local explanation from the coefficients of the model
31
+ Args:
32
+ ctx: RunContext object
33
+ model: Trained model object
34
+
35
+ Returns:
36
+ pd.DataFrame: dataframe containing local explanations
37
+
38
+ Raises:
39
+ MissingDataError: if data.x_test is not provided
40
+ """
41
+ if not hasattr(ctx.data, "x_test") or ctx.data.x_test is None:
42
+ raise MissingDataError("CoefficientsAdapter requires x_test in RunData")
43
+
44
+ data = ctx.data.x_test
45
+ if ctx.data.data_schema.sample_id_column in data.columns:
46
+ data = data.set_index(ctx.data.data_schema.sample_id_column)
47
+ data = data.select_dtypes(include=['number'])
48
+
49
+ explainer = shap.TreeExplainer(model)
50
+ shap_values = explainer.shap_values(data)
51
+
52
+ # (n_samples, 1, n_features)
53
+ if shap_values.ndim == 2:
54
+ dfs = [pd.DataFrame(shap_values, index=data.index, columns=ctx.data.x_test.columns)]
55
+ dfs[0]['class'] = 0
56
+ return dfs[0].set_index('class', append=True)
57
+
58
+ # SHAP returns (n_samples, n_features, n_classes) for multiclass tree models
59
+ # slice last axis per class to get (n_samples, n_features) per DataFrame
60
+ if shap_values.ndim == 3:
61
+ n_classes = shap_values.shape[2]
62
+ dfs = []
63
+
64
+ for i in range(n_classes):
65
+ # Slice the last axis [:, :, i] to get (n_samples, n_features)
66
+ df = pd.DataFrame(
67
+ shap_values[:, :, i],
68
+ index=data.index,
69
+ columns=data.columns
70
+ )
71
+ dfs.append(df)
72
+
73
+ keys = ctx.data.data_schema.class_names if ctx.data.data_schema.class_names is not None else range(n_classes)
74
+ return pd.concat(dfs, keys=keys, names=['class', data.index.name])
75
+
76
+ def explain_local_json(self, ctx: RunContext) -> dict[str, Any]:
77
+ raise NotImplementedError
@@ -0,0 +1,64 @@
1
+ import pandas as pd
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any
4
+
5
+ from xai_compare.context import RunContext, RunData
6
+ from xai_compare.services.preprocess import normalise_values
7
+
8
+
9
+ class XAIMethodAdapter(ABC):
10
+ """
11
+ Abstract class for XAI method adapter.
12
+ """
13
+ display_name: str | None = None
14
+ default_visualisation: str | None = None
15
+ compatible_visualisations: list[str] = []
16
+
17
+ # default to true for most explanation adapters
18
+ requires_encoded_features: bool = True
19
+
20
+ # type of explanation storage - df or json
21
+ explanation_file_type: str = "df"
22
+
23
+ @classmethod
24
+ def load_explanation(cls, store, sample_id: str):
25
+ raise NotImplementedError
26
+
27
+ @classmethod
28
+ def get_abs_max(cls, explanation: pd.Series, sample_id_column) -> float:
29
+ """ calculate absolute max value from explanation
30
+ required for ensuring charts use the same scales """
31
+ features = normalise_values(explanation, sample_id_column)
32
+ return pd.to_numeric(features, errors='coerce').abs().max()
33
+
34
+ @abstractmethod
35
+ def explain_local_df(self, ctx: RunContext, model=None) -> pd.DataFrame:
36
+ """Computes the local explanation for the model
37
+ Args:
38
+ ctx: RunContext object
39
+ model: Trained model object
40
+
41
+ Returns:
42
+ pd.DataFrame: dataframe containing local explanations
43
+
44
+ Raises:
45
+ MissingDataError: if required data is not provided
46
+ """
47
+ pass
48
+
49
+ @abstractmethod
50
+ def explain_local_json(self, ctx: RunContext) -> list[dict[str, Any]]:
51
+ """Computes the local explanation for the model
52
+ Args:
53
+ ctx: RunContext object
54
+
55
+ Returns:
56
+ list of dicts - json containing local explanations
57
+
58
+ Raises:
59
+ MissingDataError: if required data is not provided
60
+ """
61
+ pass
62
+
63
+
64
+
File without changes
@@ -0,0 +1,11 @@
1
+ from xai_compare.registry.model_registry import register_model
2
+ from xai_compare.adapters.models.model_base import ModelAdapter
3
+ from xai_compare import strings
4
+
5
+ @register_model("llm")
6
+ class LLMAdapter(ModelAdapter):
7
+
8
+ display_name = strings.LLM_DISPLAY_NAME
9
+
10
+ def prepare(self):
11
+ pass
@@ -0,0 +1,8 @@
1
+ from xai_compare.registry.model_registry import register_model
2
+ from xai_compare.adapters.models.model_base import ModelAdapter
3
+ from xai_compare import strings
4
+
5
+ @register_model("logistic_regression")
6
+ class LogisticRegressionAdapter(ModelAdapter):
7
+
8
+ display_name = strings.LOGISTIC_REGRESSION_DISPLAY_NAME
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class ModelAdapter(ABC):
5
+
6
+ display_name: str | None = None
7
+
8
+ def __init__(self, model=None):
9
+ self.model = model
10
+
11
+ def prepare(self):
12
+ return self.model
13
+
14
+
15
+
@@ -0,0 +1,8 @@
1
+ from xai_compare.registry.model_registry import register_model
2
+ from xai_compare.adapters.models.model_base import ModelAdapter
3
+ from xai_compare import strings
4
+
5
+ @register_model("xgboost")
6
+ class XGBoostAdapter(ModelAdapter):
7
+
8
+ display_name = strings.XGBOOST_DISPLAY_NAME
File without changes
@@ -0,0 +1,40 @@
1
+ import plotly.graph_objects as go
2
+
3
+ from xai_compare.adapters.viz.viz_base import VizAdapter
4
+ from xai_compare.registry.viz_registry import register_viz
5
+ from xai_compare import strings, config
6
+ from xai_compare.services.preprocess import normalise_values
7
+
8
+
9
+ @register_viz("forceplot")
10
+ class ForcePlotVizAdapter(VizAdapter):
11
+
12
+ display_name = strings.FORCE_PLOT_DISPLAY_NAME
13
+ compatible_explanations = config.FORCE_PLOT_COMPATIBLE_XAI
14
+
15
+ def render_local(self, explanation, sample_id_col, axis_range=None):
16
+ features = normalise_values(explanation, sample_id_col)
17
+
18
+ colors = [config.PLOT_LEFT_COLOR if v < 0 else config.PLOT_RIGHT_COLOR for v in features.values]
19
+
20
+ fig = go.Figure(go.Bar(
21
+ orientation="h",
22
+ y=features.index.tolist(),
23
+ x=features.values.tolist(),
24
+ marker_color=colors,
25
+ ))
26
+
27
+ fig.update_layout(
28
+ title=strings.FORCE_PLOT_TITLE,
29
+ xaxis_title=strings.FORCE_PLOT_X_AXIS,
30
+ xaxis=config.PLOT_ZERO_LINE,
31
+ showlegend=False,
32
+ )
33
+ if axis_range:
34
+ fig.update_layout(xaxis={"range":axis_range})
35
+
36
+ return fig
37
+
38
+ def render_global(self, explanations):
39
+ # Placeholder function
40
+ return
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+ from abc import ABC, abstractmethod
3
+
4
+ class VizAdapter(ABC):
5
+
6
+ display_name: str | None = None
7
+ compatible_explanations: list[str] = []
8
+
9
+ @abstractmethod
10
+ def render_local(self, explanation, sample_id_col, axis_range=None) -> Any:
11
+ raise NotImplementedError
12
+
13
+ @abstractmethod
14
+ def render_global(self, explanations) -> Any:
15
+ raise NotImplementedError
@@ -0,0 +1,38 @@
1
+ import plotly.graph_objects as go
2
+ from xai_compare.adapters.viz.viz_base import VizAdapter
3
+ from xai_compare.registry.viz_registry import register_viz
4
+ from xai_compare import strings, config
5
+
6
+ @register_viz("waterfall")
7
+ class WaterfallVizAdapter(VizAdapter):
8
+
9
+ display_name = strings.WATERFALL_DISPLAY_NAME
10
+ compatible_explanations = config.WATERFALL_COMPATIBLE_XAI
11
+
12
+ def render_local(self, explanation, sample_id_col, axis_range=None) -> go.Figure:
13
+ features = explanation.drop([sample_id_col, "class"], errors="ignore")
14
+
15
+ # Sort by absolute value descending
16
+ features = features.reindex(features.abs().sort_values(ascending=True).index)
17
+
18
+ # Normalise by sum of absolute values
19
+ features = features / features.abs().sum()
20
+
21
+ fig = go.Figure(go.Waterfall(
22
+ orientation="h",
23
+ measure=["relative"] * len(features),
24
+ y=features.index.tolist(),
25
+ x=features.values.tolist(),
26
+ connector={"line": {"color": "rgb(63, 63, 63)"}},
27
+ ))
28
+ fig.update_layout(
29
+ title=strings.WATERFALL_TITLE,
30
+ xaxis_title=strings.WATERFALL_X_AXIS,
31
+ )
32
+ if axis_range:
33
+ fig.update_layout(xaxis={"range": axis_range})
34
+ return fig
35
+
36
+ def render_global(self, explanations):
37
+ # Placeholder function
38
+ return
@@ -0,0 +1,70 @@
1
+ import os
2
+ import pandas as pd
3
+ import numpy as np
4
+ import uuid
5
+
6
+ from datetime import datetime
7
+ from dataclasses import asdict
8
+
9
+ from xai_compare import RunContext
10
+ from xai_compare import config
11
+ from xai_compare.storage.store_base import DataStore
12
+ from xai_compare.storage.filesystem_store import FilesystemStore
13
+
14
+
15
+ class ArtifactStore:
16
+ """ArtifactStore - handles the data storage
17
+
18
+ Args:
19
+ ctx (RunContext): The current run configuration.
20
+ store (DataStore): Storage backend. Defaults to FilesystemStore.
21
+ """
22
+
23
+ def __init__(self, ctx: RunContext, store: DataStore = None):
24
+ self.ctx = ctx
25
+ self.run_datetime = datetime.now().strftime("%Y%m%d-%H%M%S")
26
+ dirname = self._resolve_dirname(ctx)
27
+ self.store = store or FilesystemStore(dirname)
28
+
29
+
30
+ def _resolve_dirname(self, ctx):
31
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
32
+ if ctx.run_name:
33
+ return os.path.join(BASE_DIR, config.RUNS_DIR_NAME, ctx.run_name)
34
+ return os.path.join(BASE_DIR, config.RUNS_DIR_NAME, self.run_datetime)
35
+
36
+
37
+ def write_metadata(self):
38
+ """Writes the artifact metadata directly from the context dataclass
39
+ """
40
+ excluded = {'data', 'model'}
41
+ obj = {k: v for k, v in asdict(self.ctx).items() if k not in excluded}
42
+ obj['datetime'] = self.run_datetime
43
+ obj['run_uuid'] = str(uuid.uuid4())
44
+
45
+ # Add the dataschema info
46
+ obj['data_schema'] = asdict(self.ctx.data.data_schema)
47
+
48
+ self.store.save_json(config.META_INFO_FILENAME, obj)
49
+
50
+ def write_model(self):
51
+ if self.ctx.model:
52
+ self.store.save_model(config.MODEL_FILENAME ,self.ctx.model)
53
+
54
+ def write_data(self, name, data):
55
+ if data is None:
56
+ return
57
+ if isinstance(data, pd.Series):
58
+ data = data.to_frame()
59
+ elif isinstance(data, np.ndarray):
60
+ data = pd.DataFrame(data)
61
+ elif isinstance(data, list):
62
+ data = pd.DataFrame(data)
63
+ self.store.save_dataframe(name, data)
64
+
65
+ def write_explanation(self, name, data, type="df"):
66
+ self.store.save_explanation(name, data, type)
67
+
68
+ def load_dataframe(self, name:str) -> pd.DataFrame:
69
+ return self.store.load_dataframe(name)
70
+
xai_compare/cli.py ADDED
@@ -0,0 +1,10 @@
1
+ import os
2
+ import subprocess
3
+
4
+ def main():
5
+ base_dir = os.path.dirname(os.path.abspath(__file__))
6
+ app_path = os.path.join(base_dir, "dashboard", "app.py")
7
+ subprocess.run(["streamlit", "run", app_path])
8
+
9
+ if __name__ == "__main__":
10
+ main()