From a5d00736e218fbfef4c0ae9b4f2efbf73fe4d3de Mon Sep 17 00:00:00 2001 From: ruthraguru248 Date: Mon, 5 Oct 2026 12:38:20 +0530 Subject: [PATCH 1/3] Add batch explanations and summary --- src/xai_framework/__init__.py | 2 + src/xai_framework/auto.py | 61 +++++++++++++++---- src/xai_framework/summary.py | 44 +++++++++++++ tests/test_auto_and_registry.py | 105 +++++++++++++++++++++++++++++++- 4 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 src/xai_framework/summary.py diff --git a/src/xai_framework/__init__.py b/src/xai_framework/__init__.py index caead01..79088da 100644 --- a/src/xai_framework/__init__.py +++ b/src/xai_framework/__init__.py @@ -10,6 +10,7 @@ """ from .auto import build_explainer, explain, explain_global +from .summary import summarize from .explainers import ( BaseExplainer, CoalitionExplainer, @@ -41,4 +42,5 @@ "explain_global", "get_explainer", "register_explainer", + "summarize", ] diff --git a/src/xai_framework/auto.py b/src/xai_framework/auto.py index ac3e27f..729a0d6 100644 --- a/src/xai_framework/auto.py +++ b/src/xai_framework/auto.py @@ -81,12 +81,12 @@ def build_explainer( random_state=random_state, ) - def explain( model: Any, X: Any, instance: Any = None, *, + instances: Any = None, methods: str | Sequence[str] = "auto", target: Any = None, y: Any = None, @@ -97,7 +97,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 @@ -108,14 +108,16 @@ 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 @@ -123,11 +125,45 @@ class for local explanations and all classes for global ones. 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, @@ -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) diff --git a/src/xai_framework/summary.py b/src/xai_framework/summary.py new file mode 100644 index 0000000..33cd75e --- /dev/null +++ b/src/xai_framework/summary.py @@ -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) \ No newline at end of file diff --git a/tests/test_auto_and_registry.py b/tests/test_auto_and_registry.py index 92efd79..d9f9ba7 100644 --- a/tests/test_auto_and_registry.py +++ b/tests/test_auto_and_registry.py @@ -1,6 +1,6 @@ import numpy as np +import pandas as pd import pytest - from xai_framework import ( BaseExplainer, ConsensusExplanation, @@ -11,6 +11,7 @@ explain_global, get_explainer, register_explainer, + summarize, ) from xai_framework.auto import resolve_methods @@ -98,3 +99,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 From 0ae6750399a3b22499c4f96d4ca07ef8f968e697 Mon Sep 17 00:00:00 2001 From: ruthraguru248 Date: Mon, 5 Oct 2026 12:52:38 +0530 Subject: [PATCH 2/3] Fix lint and formatting --- src/xai_framework/__init__.py | 2 +- src/xai_framework/auto.py | 4 ++-- src/xai_framework/summary.py | 2 +- tests/test_auto_and_registry.py | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/xai_framework/__init__.py b/src/xai_framework/__init__.py index 79088da..a5a995b 100644 --- a/src/xai_framework/__init__.py +++ b/src/xai_framework/__init__.py @@ -10,7 +10,6 @@ """ from .auto import build_explainer, explain, explain_global -from .summary import summarize from .explainers import ( BaseExplainer, CoalitionExplainer, @@ -22,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" diff --git a/src/xai_framework/auto.py b/src/xai_framework/auto.py index 729a0d6..a0fdb51 100644 --- a/src/xai_framework/auto.py +++ b/src/xai_framework/auto.py @@ -81,6 +81,7 @@ def build_explainer( random_state=random_state, ) + def explain( model: Any, X: Any, @@ -152,8 +153,7 @@ def explain( # 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 + 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) diff --git a/src/xai_framework/summary.py b/src/xai_framework/summary.py index 33cd75e..c53ab8c 100644 --- a/src/xai_framework/summary.py +++ b/src/xai_framework/summary.py @@ -41,4 +41,4 @@ def summarize(explanations: Sequence[Explanation]) -> pd.DataFrame: rows.append(row) - return pd.DataFrame(rows) \ No newline at end of file + return pd.DataFrame(rows) diff --git a/tests/test_auto_and_registry.py b/tests/test_auto_and_registry.py index d9f9ba7..ef6ab42 100644 --- a/tests/test_auto_and_registry.py +++ b/tests/test_auto_and_registry.py @@ -1,6 +1,7 @@ import numpy as np import pandas as pd import pytest + from xai_framework import ( BaseExplainer, ConsensusExplanation, From 914f91e9e5004e1ecaf099fb04b12678575fd7e8 Mon Sep 17 00:00:00 2001 From: ruthraguru248 Date: Tue, 6 Oct 2026 22:55:28 +0530 Subject: [PATCH 3/3] Add batch explanation README example --- README.md | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index b062938..ffecf05 100644 --- a/README.md +++ b/README.md @@ -105,46 +105,20 @@ 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) - -```python -glob = explain(model, X_test, y=y_test) # coalition mean|phi| + permutation importance -glob.to_text() -``` - -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 multiple predictions (batch) ```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}}) +from xai_framework import explain, summarize +explanations = explain(model, X_train, instances=X_test.head(20)) +summary = summarize(explanations) +print(summary) ``` -### Use the explainer classes directly +### Explain the whole model (global) ```python -from xai_framework import ( - CoalitionExplainer, LocalSurrogateExplainer, PermutationExplainer, ConsensusExplainer, -) - -coal = CoalitionExplainer(model, X_train, n_background=50) # exact/sampling chosen automatically -surr = LocalSurrogateExplainer(model, X_train, n_samples=5000, categorical_features=["sex"]) -perm = PermutationExplainer(model, X_train, metric="accuracy") - -coal.explain_instance(x) -coal.explain(X_test.head(20)) # batch -coal.explain_global(X_test) -perm.explain_global(X_test, y_test) +glob = explain(model, X_test, y=y_test) # coalition mean|phi| + permutation importance -consensus = ConsensusExplainer(model, X_train, methods=(coal, surr)) -consensus.explain_instance(x) +glob.to_text() ``` ### How the coalition explainer scales