Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 10 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,29 +108,22 @@ exp.plot() # bar chart with one marker per method
For classifiers, pass `target="benign"` (label or index) to explain a specific class;
the default is the predicted class.

### Explain the whole model (global)

### Explain multiple predictions (batch)
```python
glob = explain(model, X_test, y=y_test) # coalition mean|phi| + permutation importance
glob.to_text()
from xai_framework import explain, summarize
explanations = explain(model, X_train, instances=X_test.head(20))
summary = summarize(explanations)
print(summary)
```
### Examples

Without `y`, only the coalition explainer runs. Use held-out data for permutation
importance: on the training rows of a fully grown ensemble every feature looks
unimportant.

### Pick methods explicitly
### Explain the whole model (global)

```python
explain(model, X, instance=0, methods="surrogate") # single method
explain(model, X, instance=0, methods=["coalition", "surrogate"],
weights={"coalition": 2, "surrogate": 1}, aggregation="rank") # weighted Borda
explain(model, X, instance=0,
explainer_kwargs={"surrogate": {"n_samples": 2000},
"coalition": {"n_background": 100}})
```
### Examples
glob = explain(model, X_test, y=y_test) # coalition mean|phi| + permutation importance

glob.to_text()
```
- [Consensus Explanation Walkthrough](examples/01_consensus_walkthrough.ipynb) — demonstrates local consensus explanations, agreement between explanation methods, disagreement cases, and global explanations.

## Documentation
Expand Down
2 changes: 2 additions & 0 deletions src/xai_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .explanation import ConsensusExplanation, Explanation
from .model import ModelAdapter
from .registry import available_explainers, get_explainer, register_explainer
from .summary import summarize

__version__ = "0.1.0"

Expand All @@ -41,4 +42,5 @@
"explain_global",
"get_explainer",
"register_explainer",
"summarize",
]
59 changes: 49 additions & 10 deletions src/xai_framework/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def explain(
X: Any,
instance: Any = None,
*,
instances: Any = None,
methods: str | Sequence[str] = "auto",
target: Any = None,
y: Any = None,
Expand All @@ -97,7 +98,7 @@ def explain(
weights: dict[str, float] | None = None,
aggregation: str = "mean",
explainer_kwargs: dict[str, dict[str, Any]] | None = None,
) -> Explanation:
) -> Explanation | list[Explanation]:
"""Explain a model with one call.

Parameters
Expand All @@ -108,26 +109,61 @@ def explain(
X
Background data (training rows work well). A DataFrame supplies feature names.
instance
The row to explain: an array/Series, or an integer index into ``X``.
Omit it for a global explanation of the whole dataset.
One row to explain: an array/Series, or an integer index into ``X``.
Kept for backward compatibility.
instances
Multiple rows to explain. Accepts a DataFrame, a 2-D array, or a list
of integer row indices into ``X``.
methods
``"auto"`` picks ``coalition + surrogate`` locally and ``coalition + permutation`` globally
(permutation only when ``y`` is given). Or pass one name or a list of names.
``"auto"`` picks ``coalition + surrogate`` locally and
``coalition + permutation`` globally.
target
Class to explain for classifiers (label or index). Defaults to the predicted
class for local explanations and all classes for global ones.
Class to explain for classifiers. Defaults to the predicted class.
y
Ground-truth labels for ``X``; enables permutation importance globally.
weights, aggregation, explainer_kwargs
Forwarded to :class:`~xai_framework.explainers.consensus.ConsensusExplainer`.

Returns
-------
Explanation
A :class:`~xai_framework.explanation.ConsensusExplanation` when several
methods ran, otherwise the single method's :class:`Explanation`.
Explanation or list[Explanation]
A single explanation for ``instance`` or a list of explanations for
``instances``.
"""

if instance is not None and instances is not None:
raise ValueError("provide either 'instance' or 'instances', not both")

# Multiple local instances
if instances is not None:
explainer = build_explainer(
model,
X,
methods,
scope="local",
has_y=y is not None,
feature_names=feature_names,
class_names=class_names,
task=task,
random_state=random_state,
weights=weights,
aggregation=aggregation,
explainer_kwargs=explainer_kwargs,
)

# A list of integers means row indices into X.
if isinstance(instances, (list, tuple)) and all(
isinstance(i, (int, np.integer)) and not isinstance(i, bool) for i in instances
):
selected = to_numpy(X)[list(instances)]
return explainer.explain(selected, target=target)

# Otherwise instances is expected to be a DataFrame or 2-D array.
return explainer.explain(instances, target=target)

# Existing single/global behavior
scope = "global" if instance is None else "local"

explainer = build_explainer(
model,
X,
Expand All @@ -142,10 +178,13 @@ class for local explanations and all classes for global ones.
aggregation=aggregation,
explainer_kwargs=explainer_kwargs,
)

if scope == "global":
return explainer.explain_global(X, y, target=target)

if isinstance(instance, (int, np.integer)) and not isinstance(instance, bool):
instance = to_numpy(X)[int(instance)]

return explainer.explain_instance(instance, target=target)


Expand Down
44 changes: 44 additions & 0 deletions src/xai_framework/summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

from collections.abc import Sequence
from typing import Any

import pandas as pd

from .explanation import ConsensusExplanation, Explanation


def summarize(explanations: Sequence[Explanation]) -> pd.DataFrame:
"""Summarize a collection of explanations as a DataFrame.

Each row represents one explanation and contains its prediction,
target, top-3 features with their attributions, and agreement
when the explanation is a consensus explanation.
"""
rows: list[dict[str, Any]] = []

for explanation in explanations:
top_features = explanation.top(3)

row: dict[str, Any] = {
"prediction": explanation.prediction,
"target": explanation.target,
}

for i in range(3):
if i < len(top_features):
feature, attribution = top_features[i]
row[f"feature_{i + 1}"] = feature
row[f"attribution_{i + 1}"] = attribution
else:
row[f"feature_{i + 1}"] = None
row[f"attribution_{i + 1}"] = None

if isinstance(explanation, ConsensusExplanation):
row["agreement"] = explanation.agreement
else:
row["agreement"] = float("nan")

rows.append(row)

return pd.DataFrame(rows)
104 changes: 104 additions & 0 deletions tests/test_auto_and_registry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import pandas as pd
import pytest

from xai_framework import (
Expand All @@ -11,6 +12,7 @@
explain_global,
get_explainer,
register_explainer,
summarize,
)
from xai_framework.auto import resolve_methods

Expand Down Expand Up @@ -98,3 +100,105 @@ def test_feature_and_class_names_passthrough(binary_gb, binary):
)
assert exp.feature_names == ["alpha", "beta", "gamma"]
assert exp.target in {"no", "yes"}


def test_explain_multiple_instances_dataframe(iris_rf, iris):
X, _ = iris

explanations = explain(
iris_rf,
X,
instances=X.iloc[:3],
methods="coalition",
random_state=0,
)

assert isinstance(explanations, list)
assert len(explanations) == 3
assert all(isinstance(exp, Explanation) for exp in explanations)


def test_explain_multiple_instances_numpy(iris_rf, iris):
X, _ = iris

explanations = explain(
iris_rf,
X,
instances=X.iloc[:3].to_numpy(),
methods="coalition",
random_state=0,
)

assert len(explanations) == 3
np.testing.assert_array_equal(
explanations[0].feature_values,
X.iloc[0].to_numpy(),
)


def test_explain_multiple_instances_indices(iris_rf, iris):
X, _ = iris

explanations = explain(
iris_rf,
X,
instances=[0, 2, 4],
methods="coalition",
random_state=0,
)

assert len(explanations) == 3

np.testing.assert_array_equal(
explanations[0].feature_values,
X.iloc[0].to_numpy(),
)

np.testing.assert_array_equal(
explanations[1].feature_values,
X.iloc[2].to_numpy(),
)

np.testing.assert_array_equal(
explanations[2].feature_values,
X.iloc[4].to_numpy(),
)


def test_explain_rejects_instance_and_instances(iris_rf, iris):
X, _ = iris

with pytest.raises(ValueError, match="either 'instance' or 'instances'"):
explain(
iris_rf,
X,
instance=0,
instances=[1, 2],
methods="coalition",
)


def test_summarize_batch_explanations(iris_rf, iris):
X, _ = iris

explanations = explain(
iris_rf,
X,
instances=X.iloc[:3],
random_state=0,
)

summary = summarize(explanations)

assert isinstance(summary, pd.DataFrame)
assert len(summary) == 3

assert "prediction" in summary.columns
assert "target" in summary.columns
assert "feature_1" in summary.columns
assert "attribution_1" in summary.columns
assert "feature_2" in summary.columns
assert "attribution_2" in summary.columns
assert "feature_3" in summary.columns
assert "attribution_3" in summary.columns
assert "agreement" in summary.columns
Loading