diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38c6d71..93b4594 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,3 +39,18 @@ jobs: with: files: ./coverage.xml fail_ci_if_error: false + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + - name: Install + run: | + python -m pip install --upgrade pip + pip install -e ".[dev,docs]" + - name: Build docs (strict) + run: mkdocs build --strict diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..0eee13c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,59 @@ +name: Docs + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + docs: + name: Build documentation + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev,docs]" + + - name: Build documentation + run: mkdocs build --strict + + deploy: + name: Deploy to GitHub Pages + needs: docs + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev,docs]" + + - name: Deploy documentation + run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index f36abdc..25c7816 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ env/ .coverage htmlcov/ coverage.xml +site/ # Editors / OS .vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce5db4..46c90e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project are documented here. The format follows ## [Unreleased] ### Added +- Documentation site powered by `mkdocs-material` and `mkdocstrings`, featuring getting started guides, mathematical overviews for all explainers, consensus documentation, an extending guide, and full API reference (#16). +- Automated GitHub Actions deployment workflow for GitHub Pages on push to `main` and strict doc build verification in CI (#16). - `Explanation.to_markdown()`: render explanations as Markdown tables with headlines and agreement summaries for reports, pull requests, and notebooks (#1). ## [0.1.0] - 2026-09-21 diff --git a/README.md b/README.md index f53a1f8..c31a97e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Documentation](https://img.shields.io/badge/docs-mkdocs--material-blue.svg)](https://adaumsilva.github.io/XAI-framework/) [![CI](https://github.com/adaumsilva/XAI-framework/actions/workflows/ci.yml/badge.svg)](https://github.com/adaumsilva/XAI-framework/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/adaumsilva/XAI-framework/branch/main/graph/badge.svg)](https://codecov.io/gh/adaumsilva/XAI-framework) [![GitHub issues](https://img.shields.io/github/issues/adaumsilva/XAI-framework)](https://github.com/adaumsilva/XAI-framework/issues) @@ -10,6 +11,8 @@ [![GitHub contributors](https://img.shields.io/github/contributors/adaumsilva/XAI-framework)](https://github.com/adaumsilva/XAI-framework/graphs/contributors) [![GitHub stars](https://img.shields.io/github/stars/adaumsilva/XAI-framework?style=social)](https://github.com/adaumsilva/XAI-framework/stargazers) +> 📖 **Full Documentation & API Reference:** [https://adaumsilva.github.io/XAI-framework/](https://adaumsilva.github.io/XAI-framework/) + **One call, several attribution methods, and a measure of how much they agree.** XAI-Framework is a model-agnostic explainable-AI library with its **own native @@ -129,90 +132,31 @@ explain(model, X, instance=0, ### Examples - [Consensus Explanation Walkthrough](examples/01_consensus_walkthrough.ipynb) — demonstrates local consensus explanations, agreement between explanation methods, disagreement cases, and global explanations. -### Use the explainer classes directly - -```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) - -consensus = ConsensusExplainer(model, X_train, methods=(coal, surr)) -consensus.explain_instance(x) -``` +## Documentation -### How the coalition explainer scales +Detailed guides, mathematical formulations, and API specifications are available in the [Documentation Site](https://adaumsilva.github.io/XAI-framework/): -Cost per local explanation is `n_coalitions x n_background` model evaluations, -batched into a handful of `predict` calls: - -| features | coalitions (auto) | result | -|---|---|---| -| <= 11 | all `2^n - 2` | exact Shapley values | -| 30 | 2 108 | sizes 1, 2, 28, 29 enumerated exactly; the rest importance-sampled | -| any, linear regressor | - | closed form, instant | - -A 30-feature random forest explains locally in ~0.2 s and globally (100 rows) in a few -seconds on a laptop. Tune with `n_coalitions`, `n_background`, `n_global`. +| Guide | Description | +|---|---| +| [Getting Started](https://adaumsilva.github.io/XAI-framework/getting-started/) | Step-by-step tutorial on local and global explanations, formatting, and visualization. | +| [Methods & Mathematical Foundations](https://adaumsilva.github.io/XAI-framework/methods/) | Mathematical foundations for Coalition (Shapley), Local Surrogates, and Permutation Importance. | +| [Consensus & Agreement](https://adaumsilva.github.io/XAI-framework/methods/consensus/) | How multi-method explanations are normalized, combined, and diagnosed for agreement ($\rho$). | +| [Extending the Framework](https://adaumsilva.github.io/XAI-framework/extending/) | Guide on subclassing `BaseExplainer` and packaging custom explainers via entry points. | +| [API Reference](https://adaumsilva.github.io/XAI-framework/api/) | Full class, method, and function reference generated directly from code docstrings. | -### Works with +## Compatibility * scikit-learn estimators and `Pipeline`s (anything with `predict_proba` / `predict`) * XGBoost, LightGBM, CatBoost and any other library with the same interface -* plain callables `f(X) -> predictions` (probabilities or values) +* Plain callables `f(X) -> predictions` (probabilities or values) * NumPy arrays and pandas DataFrames (column names become feature names) -Models fitted on DataFrames are handed DataFrames again, so you won't see -"X does not have valid feature names" warnings. - -## The `Explanation` object - -| Attribute / method | Meaning | -|---|---| -| `feature_names`, `values` | one attribution per feature; positive pushes towards the target class / higher value | -| `feature_values` | the explained instance (local only) | -| `prediction`, `base_value`, `target` | model output, reference value, class explained | -| `top(k)`, `ranks()`, `normalized()` | quick views | -| `to_text()`, `to_markdown()`, `to_dataframe()`, `to_dict()`, `plot()` | outputs | -| `metadata` | method-specific extras (surrogate `local_r2`, coalition `algorithm` / `exact`, permutation `importances_std`, ...) | - -`ConsensusExplanation` adds `components`, `agreement`, `agreement_matrix` and -`agreement_level()` (`"high"` >= 0.8, `"moderate"` >= 0.5, `"low"`). - -## Adding your own explainer - -Subclass `BaseExplainer`, implement `_explain_instance` and/or `_explain_global`, -and register it: - -```python -from xai_framework import BaseExplainer, register_explainer - -@register_explainer("my_method") -class MyExplainer(BaseExplainer): - supports_global = False - - def _explain_instance(self, x, target): - values = ... # one number per feature - return self._make_explanation(values, x, target, base_value=None) -``` - -It is now usable as `explain(model, X, instance=0, methods=["coalition", "my_method"])`. -Third-party packages can register through the `xai_framework.explainers` entry-point -group - see [CONTRIBUTING.md](CONTRIBUTING.md). - ## Roadmap See [ROADMAP.md](ROADMAP.md) and the issue tracker. Highlights: a fast exact path for tree ensembles, rule-based (anchor) and counterfactual explainers, text and image -support, faithfulness metrics, a CLI and an HTML report, and a docs site. +support, faithfulness metrics, and standalone HTML reports. ## Contributing diff --git a/docs/api/auto.md b/docs/api/auto.md new file mode 100644 index 0000000..360fa34 --- /dev/null +++ b/docs/api/auto.md @@ -0,0 +1,5 @@ +# Auto API (`xai_framework.auto`) + +The `auto` module provides the high-level functional interface to XAI-Framework. + +::: xai_framework.auto diff --git a/docs/api/explainers.md b/docs/api/explainers.md new file mode 100644 index 0000000..da8c93a --- /dev/null +++ b/docs/api/explainers.md @@ -0,0 +1,9 @@ +# Explainers API (`xai_framework.explainers`) + +Explainer classes and consensus combination algorithms. + +::: xai_framework.explainers.base +::: xai_framework.explainers.coalition +::: xai_framework.explainers.surrogate +::: xai_framework.explainers.permutation +::: xai_framework.explainers.consensus diff --git a/docs/api/explanation.md b/docs/api/explanation.md new file mode 100644 index 0000000..8d2a611 --- /dev/null +++ b/docs/api/explanation.md @@ -0,0 +1,5 @@ +# Explanation API (`xai_framework.explanation`) + +The data structures representing individual and consensus explanations. + +::: xai_framework.explanation diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..df36c4b --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,13 @@ +# API Reference + +Complete documentation generated directly from docstrings across the XAI-Framework codebase. + +--- + +## Core Modules + +- **[`auto`](auto.md)**: Top-level convenience functions [`explain()`][xai_framework.auto.explain] and [`explain_global()`][xai_framework.auto.explain_global], plus explainer factory [`build_explainer()`][xai_framework.auto.build_explainer]. +- **[`explainers`](explainers.md)**: Base classes and concrete explainer implementations ([`CoalitionExplainer`][xai_framework.explainers.coalition.CoalitionExplainer], [`LocalSurrogateExplainer`][xai_framework.explainers.surrogate.LocalSurrogateExplainer], [`PermutationExplainer`][xai_framework.explainers.permutation.PermutationExplainer], and [`ConsensusExplainer`][xai_framework.explainers.consensus.ConsensusExplainer]). +- **[`explanation`](explanation.md)**: Output containers [`Explanation`][xai_framework.explanation.Explanation] and [`ConsensusExplanation`][xai_framework.explanation.ConsensusExplanation]. +- **[`model`](model.md)**: Standardized wrapper [`ModelAdapter`][xai_framework.model.ModelAdapter] providing uniform inference over estimators, pipelines, and callables. +- **[`registry`](registry.md)**: Explainer registry, dynamic discovery, and entry-point plugin mechanics. diff --git a/docs/api/model.md b/docs/api/model.md new file mode 100644 index 0000000..0e14b79 --- /dev/null +++ b/docs/api/model.md @@ -0,0 +1,5 @@ +# Model Adapter API (`xai_framework.model`) + +Model introspection, prediction wrapping, and format unification. + +::: xai_framework.model diff --git a/docs/api/registry.md b/docs/api/registry.md new file mode 100644 index 0000000..8cfc36e --- /dev/null +++ b/docs/api/registry.md @@ -0,0 +1,5 @@ +# Registry API (`xai_framework.registry`) + +Dynamic explainer registration and entry point resolution. + +::: xai_framework.registry diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..786b75d --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/extending.md b/docs/extending.md new file mode 100644 index 0000000..c7c0fba --- /dev/null +++ b/docs/extending.md @@ -0,0 +1,94 @@ +# Extending XAI-Framework + +XAI-Framework is designed to be easily extensible. You can register custom attribution algorithms internally or build third-party plugins that integrate seamlessly with the framework's consensus engine. + +--- + +## The Explainer Contract + +Every explainer inherits from [`BaseExplainer`][xai_framework.explainers.base.BaseExplainer] and produces a standardized [`Explanation`][xai_framework.explanation.Explanation] object. + +### Step 1: Subclass `BaseExplainer` + +Create a new class inheriting from `BaseExplainer`: + +```python +from __future__ import annotations +import numpy as np +from xai_framework import BaseExplainer, register_explainer + +@register_explainer("custom_gradient") +class CustomGradientExplainer(BaseExplainer): + """Example custom explainer estimating gradients via finite differences.""" + + name = "custom_gradient" + supports_local = True + supports_global = False + requires_y = False + + def _explain_instance(self, x: np.ndarray, target: int | None) -> Explanation: + # x is guaranteed to be a 1D float numpy array + # Use self._predict_scalar(..., target)[0] to evaluate scalar predictions + base_pred = float(self._predict_scalar(x.reshape(1, -1), target)[0]) + + eps = 1e-4 + n_features = len(x) + attributions = np.zeros(n_features) + + for i in range(n_features): + x_perturbed = x.copy() + x_perturbed[i] += eps + pred_perturbed = float(self._predict_scalar(x_perturbed.reshape(1, -1), target)[0]) + attributions[i] = (pred_perturbed - base_pred) / eps + + # Build standard Explanation instance using helper + return self._make_explanation( + values=attributions, + x=x, + target=target, + prediction=base_pred, + base_value=None, + step_size=eps, + ) +``` + +--- + +## Core Guidelines & Invariants + +When implementing a custom explainer, adhere to the following framework conventions: + +1. **Model Communication via Helpers & `self.adapter`**: + Never call `model.predict()` or `model.predict_proba()` directly. Use `self._predict_scalar(X, target)` to obtain the scalar outcome being explained, or interact via `self.adapter` ([`ModelAdapter`][xai_framework.model.ModelAdapter]), which abstracts classification, regression, single-output functions, and feature name resolution. + +2. **Strict Determinism via `self.rng`**: + Never use global random seeds (`np.random.seed` or `np.random.*`). Always use the instance-level random generator `self.rng` initialized with the user-provided `random_state`. + +3. **Standard Sign Convention**: + Attributions must be formatted such that a positive value $\phi_i > 0$ means feature $i$ pushes the prediction towards the target outcome (higher value or higher probability for class `target`). + +4. **Optional Dependencies & Lazy Imports**: + If your explainer relies on optional external libraries (such as PyTorch or XGBoost), override `is_available()` and perform imports lazily inside `__init__`. + +--- + +## External Entry-Point Plugins + +External Python packages can register custom explainers into XAI-Framework without modifying the `xai-framework` repository. + +In your external package's `pyproject.toml`, declare entry points under `xai_framework.explainers`: + +```toml +[project.entry-points."xai_framework.explainers"] +anchors = "my_xai_package.anchors:AnchorsExplainer" +integrated_gradients = "my_xai_package.ig:IntegratedGradientsExplainer" +``` + +Once installed in the environment, your explainer will be automatically detected and available to `explain()`: + +```python +from xai_framework import explain + +# Custom explainer is loaded via entry points automatically +exp = explain(model, X_train, instance=x_query, methods=["coalition", "anchors"]) +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..fc18704 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,117 @@ +# Getting Started + +This guide walks you through installing XAI-Framework, configuring dependencies, and generating your first local and global model explanations. + +--- + +## Installation + +### From GitHub + +Install directly from GitHub using `pip`: + +```bash +pip install git+https://github.com/adaumsilva/XAI-framework.git +``` + +### With Optional Plotting Support + +To generate visual explanation plots via Matplotlib: + +```bash +pip install "xai-framework[plot] @ git+https://github.com/adaumsilva/XAI-framework.git" +``` + +### Development Installation + +To set up an editable installation for development and testing: + +```bash +git clone https://github.com/adaumsilva/XAI-framework.git +cd XAI-framework +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +pip install -e ".[dev,docs]" +pytest +``` + +--- + +## Local Explanations (Instance-Level) + +A local explanation explains a specific model prediction for an individual instance $x$. + +```python +from sklearn.datasets import load_iris +from sklearn.ensemble import RandomForestClassifier +from xai_framework import explain + +iris = load_iris(as_frame=True) +X, y = iris.data, iris.target + +clf = RandomForestClassifier(random_state=42).fit(X, y) + +# Explain the first sample +exp = explain(clf, X, instance=X.iloc[0]) + +# Inspect summary +print(exp.to_text()) +``` + +### Inspecting Results + +The returned `Explanation` (or `ConsensusExplanation`) object provides several inspection methods: + +- **Top Features**: `exp.top(3)` returns the top 3 most influential features and their attribution scores. +- **Agreement Level**: `exp.agreement_level()` indicates whether consensus methods have `"high"`, `"moderate"`, or `"low"` rank agreement. +- **Pairwise Correlation**: `exp.agreement` provides the overall Spearman $\rho$ correlation. +- **Exporting Data**: + - `exp.to_dataframe()` returns a pandas DataFrame with features, instance values, consensus values, and per-explainer attributions. + - `exp.to_markdown()` outputs a clean GitHub Flavored Markdown table. + - `exp.to_dict()` provides a serializable Python dictionary. + - `exp.plot()` displays a horizontal bar chart showing attributions and individual method points. + +```python +# Convert to Markdown table +print(exp.to_markdown()) + +# Plot explanation +exp.plot() +``` + +--- + +## Global Explanations (Model-Level) + +Global explanations describe overall feature importance across a dataset: + +```python +from xai_framework import explain_global + +# Requires true labels y for permutation importance +glob = explain_global(clf, X, y=y) +print(glob.to_text()) +``` + +When ground truth `y` is provided, `explain_global()` calculates a consensus between the mean absolute Shapley values (from `coalition`) and permutation importance. Without `y`, it reports global attributions from `coalition`. + +--- + +## Selecting Specific Methods & Weights + +You can specify which explainer methods to run, assign custom weights, and configure aggregation strategies: + +```python +# Run only surrogate +exp_surr = explain(clf, X, instance=X.iloc[0], methods="surrogate") + +# Weighted Borda count between coalition and surrogate +exp_custom = explain( + clf, + X, + instance=X.iloc[0], + methods=["coalition", "surrogate"], + weights={"coalition": 2.0, "surrogate": 1.0}, + aggregation="rank", +) +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..757456b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,59 @@ +# XAI-Framework + +**One call, several attribution methods, and a measure of how much they agree.** + +`xai-framework` is a model-agnostic explainable-AI library with its own native implementations of the ideas behind the most popular attribution methods — Shapley values over feature coalitions, local linear surrogates, and permutation importance — unified behind a single interface. + +It puts their outputs on a common scale and reports a **consensus explanation** together with an **agreement score**. When the methods agree you can trust the ranking; when they do not, the framework highlights the disagreement instead of silently picking one. + +--- + +## Key Features + +- **Consensus Explanations**: Combines multiple explainer algorithms (e.g. Shapley coalitions + local linear surrogates) into a unified, balanced attribution. +- **Agreement Diagnostics**: Measures rank agreement using Spearman's rank correlation ($\rho$) and top-$k$ sign consistency. +- **Pure Python & Zero Bloat**: Native implementations in `numpy` and `pandas`. No heavy third-party explainer dependencies. +- **Model Agnostic**: Seamlessly explains scikit-learn estimators, gradient boosters (XGBoost, LightGBM, CatBoost), and custom prediction callables. +- **Flexible Outputs**: Export explanations to plain text, Markdown tables, pandas DataFrames, Python dictionaries, or Matplotlib visualisations. + +--- + +## Quick Example + +```python +from sklearn.ensemble import RandomForestClassifier +from sklearn.datasets import load_breast_cancer +from sklearn.model_selection import train_test_split +from xai_framework import explain + +# Prepare dataset and train model +data = load_breast_cancer(as_frame=True) +X_train, X_test, y_train, y_test = train_test_split(data.data, data.target, random_state=42) +model = RandomForestClassifier(random_state=42).fit(X_train, y_train) + +# Explain a single test instance +exp = explain(model, X_train, instance=X_test.iloc[0]) +print(exp.to_text()) +``` + +Output: +```text +Predicted class 'malignant' with probability 0.980 (consensus). +Features that push towards 'malignant': + + worst concave points = 0.2051 (+0.2548) + + mean concave points = 0.08172 (+0.1159) + + worst concavity = 0.5106 (+0.1124) + + mean concavity = 0.1445 (+0.08179) + + worst texture = 29.66 (+0.06424) +Agreement between coalition + surrogate: moderate (rho = 0.76) - the top features are reliable, lower ranks less so. +``` + +--- + +## Next Steps + +- Check out the [Getting Started](getting-started.md) guide for installation and walkthrough tutorials. +- Explore [Methods Overview](methods/index.md) to understand the mathematical foundations of each explainer. +- Learn about the consensus mechanism in [Consensus & Agreement](methods/consensus.md). +- Extend the framework with custom explainers in [Extending](extending.md). +- Read the [API Reference](api/index.md) for full parameter and method documentation. diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 0000000..06dbf38 --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,16 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } +}; + +document$.subscribe(() => { + MathJax.typesetPromise() +}) diff --git a/docs/methods/coalition.md b/docs/methods/coalition.md new file mode 100644 index 0000000..fb6036b --- /dev/null +++ b/docs/methods/coalition.md @@ -0,0 +1,82 @@ +# Coalition Explainer (Shapley Values) + +The Coalition explainer computes feature attributions based on the classic **Shapley value** from cooperative game theory (Shapley, 1953; Lundberg & Lee, 2017). + +--- + +## Mathematical Formulation + +A prediction task is modeled as a cooperative game where the set of features $N = \{1, 2, \dots, n\}$ acts as players collaborating to produce the prediction $f(x)$. + +The attribution $\phi_i$ assigned to feature $i$ is its average marginal contribution across all possible coalitions $S \subseteq N \setminus \{i\}$: + +$$\phi_i(v) = \sum_{S \subseteq N \setminus \{i\}} \frac{|S|!(n - |S| - 1)!}{n!} \left( v(S \cup \{i\}) - v(S) \right)$$ + +### Characteristic Valuation Function + +For a coalition $S$, the characteristic function $v(S)$ represents the expected model prediction when features in $S$ are fixed to their values in the explained instance $x$, and features outside $S$ ($N \setminus S$) are drawn from the marginal background distribution $D_{bg}$: + +$$v(S) \approx \frac{1}{|D_{bg}|} \sum_{z \in D_{bg}} f(x_S, z_{N \setminus S})$$ + +--- + +## Axiomatic Guarantees + +Shapley values are uniquely characterized by four fundamental axioms: + +1. **Efficiency**: The sum of feature attributions equals the difference between the prediction $f(x)$ and the baseline expected value $\mathbb{E}[f(X)]$: + $$\sum_{i=1}^n \phi_i = f(x) - \mathbb{E}[f(X)]$$ +2. **Symmetry**: If two features contribute equally to all possible subsets ($v(S \cup \{i\}) = v(S \cup \{j\})$ for all $S \subseteq N \setminus \{i, j\}$), then $\phi_i = \phi_j$. +3. **Dummy / Null Player**: If a feature never changes the prediction ($v(S \cup \{i\}) = v(S)$ for all $S$), then $\phi_i = 0$. +4. **Additivity**: If a model is a linear combination of two models $f = f_1 + f_2$, the attributions add up: $\phi_i(f) = \phi_i(f_1) + \phi_i(f_2)$. + +--- + +## Computational Strategies + +Computing exact Shapley values requires evaluating all $2^n - 2$ non-trivial coalitions. `CoalitionExplainer` automatically chooses the optimal strategy based on the feature dimensionality: + +``` + +---------------------------+ + | Number of features (n) | + +-------------+-------------+ + | + +----------------+----------------+ + | | + n <= 11 n > 11 + | | + [ Exact Enumeration ] [ Kernel Shapley Sampling ] + Evaluates all 2^n - 2 Importance-samples pairs + subsets explicitly. weighted by Shapley kernel: + pi(s) = (n-1) / (comb(n,s) * s * (n-s)) +``` + +### 1. Exact Enumeration ($n \le 11$) +When $n \le 11$, all $2^n - 2$ coalitions are enumerated explicitly and weighted by the Shapley kernel: +$$\pi(s) = \frac{n - 1}{\binom{n}{s} s (n - s)}$$ +Weighted least-squares regression over all coalitions recovers the exact mathematical Shapley values. + +### 2. Importance-Sampled Approximation ($n > 11$) +When $n > 11$, the explainer samples complementary coalition pairs $(S, N \setminus S)$ according to the kernel probability distribution $\pi(s)$, prioritizing very small and very large coalitions where marginal variance is greatest. + +### 3. Linear Regressor Closed Form +For standard linear regressors ($f(x) = w^T x + b$), the marginal contribution simplifies analytically: +$$\phi_i = w_i (x_i - \mathbb{E}[X_i])$$ +Evaluating linear models requires zero sampling and runs instantaneously. + +--- + +## Usage Example + +```python +from xai_framework import CoalitionExplainer + +# Initialize explainer with background reference data +explainer = CoalitionExplainer(model, X_train, n_background=50, random_state=42) + +# Explain a single local instance +exp = explainer.explain_instance(X_test.iloc[0]) + +# Global explanation across a dataset sample +glob_exp = explainer.explain_global(X_test.iloc[:50]) +``` diff --git a/docs/methods/consensus.md b/docs/methods/consensus.md new file mode 100644 index 0000000..a74be94 --- /dev/null +++ b/docs/methods/consensus.md @@ -0,0 +1,93 @@ +# Consensus & Agreement + +Attribution methods evaluate models from distinct perspectives: Shapley values measure average marginal coalition contributions, while local surrogates measure directional gradients within a perturbed neighborhood. In practice, different explainers frequently produce differing rankings on the same model and query instance. + +Instead of forcing practitioners to arbitrarily pick a single algorithm, XAI-Framework computes a **Consensus Explanation** accompanied by an **Agreement Score**. + +--- + +## 1. Scale Normalization + +Different explainability methods output attributions on fundamentally different scales (e.g. log-odds shifts vs. raw probability differences). Before combination, raw attributions are $L_1$-normalized to represent proportional influence: + +$$\tilde{\phi}_i^{(m)} = \frac{\phi_i^{(m)}}{\sum_{j=1}^n |\phi_j^{(m)}|}$$ + +If all attributions for a method are zero, the normalized vector remains zero. + +--- + +## 2. Aggregation Strategies + +XAI-Framework provides two aggregation strategies configurable via `aggregation="mean"` or `aggregation="rank"`: + +### Weighted Mean (`"mean"`, default) +Computes the weighted linear combination of normalized attributions across all $M$ methods: + +$$\phi_i^{\text{consensus}} = \frac{\sum_{m=1}^M w_m \cdot \tilde{\phi}_i^{(m)}}{\sum_{m=1}^M w_m}$$ + +This approach preserves both the direction (sign) and relative magnitude of attributions. + +### Borda Rank (`"rank"`) +Computes a normalized Borda score for each feature from each method's magnitude ranking, where rank 1 corresponds to the most important feature (yielding 1.0) down to rank $n$ (yielding $1/n$): + +$$\text{borda}_i^{(m)} = \frac{n - \text{rank}_i^{(m)} + 1}{n}$$ + +These scores are averaged across methods using normalized weights $w_m$ ($\sum_{m=1}^M w_m = 1$), and the consensus attribution applies the direction (sign) of the weighted mean attribution $\bar{\phi}_i^{\text{mean}} = \sum_{m=1}^M w_m \tilde{\phi}_i^{(m)}$: + +$$\phi_i^{\text{consensus}} = \text{sign}\left( \bar{\phi}_i^{\text{mean}} \right) \cdot \sum_{m=1}^M w_m \cdot \text{borda}_i^{(m)}$$ + +(where $\text{sign}(0)$ defaults to $1.0$). Rank aggregation provides extra robustness against an individual method producing outsized attribution values. + +--- + +## 3. Quantifying Inter-Method Agreement + +### Pairwise Spearman Rank Correlation ($\rho$) +For each pair of methods $(u, v)$, the framework computes the Spearman rank correlation $\rho_{u, v}$ over the feature magnitude vectors $|v_u|$ and $|v_v|$: + +$$\rho_{u, v} = 1 - \frac{6 \sum_{i=1}^n d_i^2}{n(n^2 - 1)}$$ + +where $d_i$ is the rank difference between methods $u$ and $v$ for feature $i$. + +The overall consensus agreement $\rho$ is the arithmetic mean of all non-diagonal pairwise correlations: + +$$\bar{\rho} = \frac{2}{M(M-1)} \sum_{u < v} \rho_{u, v}$$ + +### Sign Agreement on Top Features +In addition to rank correlation, the framework checks whether methods agree on the *direction of effect* (positive or negative) for the top-$k$ most important features: + +$$\text{sign\_agreement} = \frac{1}{k} \sum_{i \in \text{top-k}} \mathbb{I}\left( \text{sign}(\phi_i^{(u)}) = \text{sign}(\phi_i^{(v)}) \quad \forall u, v \right)$$ + +--- + +## 4. Agreement Tiers + +The framework categorizes consensus reliability into three intuitive tiers: + +| Tier | Condition | Interpretation | +| :--- | :--- | :--- | +| **High** | $\bar{\rho} \ge 0.8$ | Methods strongly align. The feature rankings and top attributions are highly robust across algorithmic assumptions. | +| **Moderate** | $0.5 \le \bar{\rho} < 0.8$ | Top features are generally consistent and reliable, but middle-to-lower rankings may vary across methods. | +| **Low** | $\bar{\rho} < 0.5$ | Significant divergence between methods. Model predictions may rely on complex non-linear feature interactions where local approximations disagree. | + +--- + +## Usage Example + +```python +from xai_framework import explain + +exp = explain( + model, + X_train, + instance=X_test.iloc[0], + methods=["coalition", "surrogate"], + weights={"coalition": 2.0, "surrogate": 1.0}, + aggregation="mean", +) + +# Inspect agreement diagnostics +print("Agreement score (rho):", exp.agreement) +print("Agreement level:", exp.agreement_level()) +print("Pairwise matrix:\n", exp.agreement_matrix) +``` diff --git a/docs/methods/index.md b/docs/methods/index.md new file mode 100644 index 0000000..a6e1ed5 --- /dev/null +++ b/docs/methods/index.md @@ -0,0 +1,24 @@ +# Methods Overview + +XAI-Framework implements native, self-contained explainability algorithms designed from foundational principles in cooperative game theory, local surrogate modeling, and permutation sampling. + +All explainers return a standardized [`Explanation`][xai_framework.explanation.Explanation] object, making their outputs directly comparable and capable of being combined into a [`ConsensusExplanation`][xai_framework.explanation.ConsensusExplanation]. + +--- + +## Explainer Matrix + +| Explainer | Key Foundation | Primary Scope | Mathematical Intuition | +| :--- | :--- | :--- | :--- | +| **[`coalition`](coalition.md)** | Shapley values (Shapley 1953; Lundberg & Lee 2017) | Local & Global | Treats features as players in a cooperative game; calculates average marginal contributions across feature subsets. | +| **[`surrogate`](surrogate.md)** | Local Linear Surrogates (LIME, Ribeiro et al. 2016) | Local | Perturbs instances in feature space, weights them by proximity kernel, and fits a sparse weighted linear model. | +| **[`permutation`](permutation.md)** | Permutation Importance (Breiman 2001; Fisher et al. 2019) | Global | Randomly permutes feature columns on held-out data and measures performance drop. | +| **[`consensus`](consensus.md)** | Multi-method Synthesis (*ours*) | Local & Global | Normalizes disparate attributions to a shared scale, computes ensemble rankings, and quantifies inter-method agreement ($\rho$). | + +--- + +## Core Properties + +1. **Native Pure-Python Implementations**: Built using `numpy` and `pandas` without external dependencies on `shap` or `lime`. +2. **Unified Representation**: Every local explainer assigns an additive attribution value $\phi_i$ to feature $i$, where positive values push toward the target outcome. +3. **Consensus & Diagnostics**: Instead of trusting a single algorithm's assumptions, XAI-Framework computes consensus and reports whether the explainers agree or diverge. diff --git a/docs/methods/permutation.md b/docs/methods/permutation.md new file mode 100644 index 0000000..134bd82 --- /dev/null +++ b/docs/methods/permutation.md @@ -0,0 +1,97 @@ +# Permutation Feature Importance + +The Permutation explainer calculates global feature importance by assessing the degradation in model performance when the relationship between a feature and the target variable is severed through random shuffling (Breiman, 2001; Fisher et al., 2019). + +--- + +## Mathematical Formulation + +Let $X \in \mathbb{R}^{m \times n}$ denote a dataset of $m$ samples and $n$ features, with corresponding true targets $y$, and let $L(y, \hat{y})$ denote a scoring loss function. + +### 1. Baseline Performance +Compute the baseline model error on the uncorrupted evaluation dataset: + +$$L_{\text{base}} = L(y, f(X))$$ + +### 2. Feature Permutation +For each feature $j \in \{1, \dots, n\}$: +1. Construct permuted matrix $X^{\pi_j}$ by randomly shuffling column $j$ across all rows, destroying any statistical association between feature $j$ and target $y$ while preserving the marginal feature distribution. +2. Evaluate corrupted model loss: + $$L_{\text{perm}}^{(j)} = L(y, f(X^{\pi_j}))$$ +3. Compute raw importance score as the performance degradation: + $$I(j) = L_{\text{perm}}^{(j)} - L_{\text{base}}$$ + +### 3. Repeated Resampling & Uncertainty +To reduce sampling variance, shuffling is repeated across $B$ repetitions ($b = 1, \dots, B$). The explainer computes both the mean importance drop $\bar{I}(j)$ and its population standard deviation (`importances_std`, using `ddof=0`): + +$$\bar{I}(j) = \frac{1}{B} \sum_{b=1}^B I_b(j), \quad \sigma_I(j) = \sqrt{\frac{1}{B} \sum_{b=1}^B (I_b(j) - \bar{I}(j))^2}$$ + +Note that `importances_std` reflects the population standard deviation across the $B$ permutation runs, rather than a sample standard deviation or standard error of the mean. + +--- + +## Metric Selection + +`PermutationExplainer` requires metrics where higher scores represent better model performance. + +### Built-in Metrics +- **Classifiers**: + - `"log_loss"` (*default*): Smooth negative cross-entropy computed from predicted probabilities. Captures subtle shifts in class probability distributions even when top-predicted discrete labels do not flip. + - `"accuracy"`: Discrete classification accuracy. +- **Regressors**: + - `"r2"` (*default*): Coefficient of determination $R^2$. + - `"neg_mse"`: Negative mean squared error ($-MSE$). + - `"neg_mae"`: Negative mean absolute error ($-MAE$). + +> [!NOTE] +> Passing unregistered metric names such as `"mse"` or `"roc_auc"` will raise a `ValueError`. + +### Custom Scoring Callables +For non-standard metrics such as ROC AUC, pass a callable `metric(y_true, model_output) -> float` when initializing `PermutationExplainer` (where higher indicates better performance): + +```python +from sklearn.metrics import roc_auc_score + +# Classifier model_output is the array of predicted probabilities +explainer = PermutationExplainer( + model=model, + X_background=X_test, + metric=lambda y_true, y_prob: roc_auc_score(y_true, y_prob[:, 1]), + n_repeats=10, + random_state=42, +) +glob_exp = explainer.explain_global(X_test, y=y_test) +``` + +--- + +## Importance of Held-Out Data + +> [!WARNING] +> Permutation importance should always be evaluated on **held-out validation or test data**, never solely on training data. +> +> On unregularized models or deep decision trees, the model may achieve near-zero training error even with noisy, overfitted features. Evaluating permutation importance on training data can incorrectly assign high importance to spurious patterns. + +--- + +## Usage Example + +```python +from xai_framework import PermutationExplainer + +# Initialize permutation explainer with held-out validation data +explainer = PermutationExplainer( + model=model, + X_background=X_test, + metric="log_loss", + n_repeats=10, + random_state=42, +) + +# Compute global explanations using true labels y_test +glob_exp = explainer.explain_global(X_test, y=y_test) + +# Display ranked global importances +for feature, score in glob_exp.top(5): + print(f"{feature}: {score:.4f}") +``` diff --git a/docs/methods/surrogate.md b/docs/methods/surrogate.md new file mode 100644 index 0000000..647bd6d --- /dev/null +++ b/docs/methods/surrogate.md @@ -0,0 +1,71 @@ +# Local Surrogate Explainer + +The Local Surrogate explainer approximates complex black-box model decisions locally around an individual prediction using an interpretable, weighted linear model (inspired by Ribeiro et al., 2016). + +--- + +## Mathematical Formulation + +Complex nonlinear decision boundaries are often locally smooth. Around a specific query point $x$, the black box $f(z)$ can be approximated by a linear surrogate model $g(z)$: + +$$g(z) = w^T z + b$$ + +### 1. Perturbation Generation +To understand the local decision landscape, the explainer generates $K$ perturbed instances $\{z_k\}_{k=1}^K$: +- **Continuous Features**: By default, perturbations are centered on the background mean $\mu_j$ and drawn from the training distribution $\mathcal{N}(\mu_j, \sigma_j^2)$. To draw perturbations tightly centered around the query instance ($\mathcal{N}(x_j, \sigma_j^2)$), set `sample_around_instance=True`. +- **Categorical Features**: Perturbed by sampling from their empirical training distribution frequencies. +- In both cases, the query instance $x$ is explicitly preserved as the first sample ($z_0 = x$). + +### 2. Proximity Kernel Weighting +Perturbations are mapped into an interpretable standardized representation $Z$: continuous features are z-scored, and categorical features are represented via indicator encoding $1[z_{k, j} == x_j]$. + +Each sample $z_k$ is weighted by an exponential proximity kernel: + +$$\pi_x(z_k) = \exp\left( -\frac{D(x, z_k)^2}{2 \cdot \text{kernel\_width}^2} \right)$$ + +where $D(x, z_k) = \|Z_k - Z_0\|_2$ is the Euclidean distance in standardized space, and $\text{kernel\_width}$ defaults to $0.75 \sqrt{n_{\text{features}}}$. + +### 3. Weighted Linear Optimization +The surrogate weights $w$ and unpenalized intercept $b$ are solved via closed-form weighted Ridge regression: + +$$\min_{w, b} \sum_{k=1}^K \pi_x(z_k) \left( f(z_k) - (w^T z_k + b) \right)^2 + \alpha \|w\|_2^2$$ + +--- + +## Additive Attributions & Local Fidelity + +In default `mode="contribution"`, raw regression coefficients $w_i$ are converted into additive feature contributions using standardized instance values: +- **Continuous Features**: + $$\phi_i = w_i \cdot \frac{x_i - \mu_i}{\sigma_i}$$ +- **Categorical Features**: + $$\phi_j = w_j \cdot 1[x_j == x_j] = w_j$$ + +Under this formulation, $b + \sum_{i=1}^n \phi_i = g(x)$ recovers the surrogate's local prediction, ensuring direct comparability with Shapley values and consensus aggregation. Setting `mode="coefficient"` reports raw regression weights $w$. + +### Surrogate Quality Metric ($R^2$) +The explainer calculates the weighted coefficient of determination ($R_{local}^2$) of the linear surrogate on the perturbed sample: +- High $R_{local}^2$ ($\ge 0.85$) confirms that the local boundary is approximately linear and the attributions are trustworthy. +- Low $R_{local}^2$ alerts the user to high local non-linearity or complex interactions. + +--- + +## Usage Example + +```python +from xai_framework import LocalSurrogateExplainer + +# Configure surrogate explainer with 2000 perturbation samples +explainer = LocalSurrogateExplainer( + model=model, + X_background=X_train, + n_samples=2000, + kernel_width=None, # Auto-selected from feature dimensions + random_state=42, +) + +# Explain single query instance +exp = explainer.explain_instance(X_test.iloc[0]) + +# Inspect local fit goodness +print(f"Local R^2: {exp.metadata.get('local_r2'):.3f}") +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..cfe4c4f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,80 @@ +site_name: XAI-Framework +site_description: "Model-agnostic explainable AI with unified consensus explanations" +site_url: https://adaumsilva.github.io/XAI-framework/ +repo_url: https://github.com/adaumsilva/XAI-framework +repo_name: adaumsilva/XAI-framework + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: [src] + options: + docstring_style: numpy + show_root_heading: true + show_source: true + heading_level: 2 + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.tabbed: + alternate_style: true + - pymdownx.arithmatex: + generic: true + - tables + - attr_list + +extra_javascript: + - javascripts/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + +nav: + - Home: index.md + - Getting Started: getting-started.md + - Methods: + - Overview: methods/index.md + - Coalition (Shapley): methods/coalition.md + - Surrogate: methods/surrogate.md + - Permutation: methods/permutation.md + - Consensus & Agreement: methods/consensus.md + - Extending: extending.md + - API Reference: + - Overview: api/index.md + - Auto: api/auto.md + - Explainers: api/explainers.md + - Explanation: api/explanation.md + - Model Adapter: api/model.md + - Registry: api/registry.md + - Changelog: changelog.md diff --git a/pyproject.toml b/pyproject.toml index 2a66517..571540b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,15 @@ dependencies = [ [project.optional-dependencies] plot = ["matplotlib>=3.7"] +docs = [ + "mkdocs>=1.5", + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.24", + "pymdown-extensions>=10.7", +] dev = [ "xai-framework[plot]", + "xai-framework[docs]", "pytest>=7", "pytest-cov", "ruff>=0.4", @@ -41,6 +48,7 @@ dev = [ [project.urls] Homepage = "https://github.com/adaumsilva/XAI-framework" +Documentation = "https://adaumsilva.github.io/XAI-framework/" Issues = "https://github.com/adaumsilva/XAI-framework/issues" [tool.setuptools.packages.find] diff --git a/tests/test_docs_smoke.py b/tests/test_docs_smoke.py new file mode 100644 index 0000000..119129b --- /dev/null +++ b/tests/test_docs_smoke.py @@ -0,0 +1,181 @@ +"""Executable smoke tests for documentation code examples.""" + +from __future__ import annotations + +import numpy as np +import pytest +from sklearn.datasets import load_breast_cancer, load_iris +from sklearn.ensemble import RandomForestClassifier +from sklearn.metrics import roc_auc_score +from sklearn.model_selection import train_test_split + +from xai_framework import ( + BaseExplainer, + CoalitionExplainer, + Explanation, + LocalSurrogateExplainer, + PermutationExplainer, + explain, + explain_global, + register_explainer, +) + + +@pytest.fixture(scope="module") +def iris_data_and_model(): + iris = load_iris(as_frame=True) + X, y = iris.data, iris.target + model = RandomForestClassifier(n_estimators=10, random_state=42).fit(X, y) + return X, y, model + + +@pytest.fixture(scope="module") +def cancer_data_and_model(): + cancer = load_breast_cancer(as_frame=True) + X_train, X_test, y_train, y_test = train_test_split( + cancer.data, cancer.target, test_size=0.25, random_state=42 + ) + model = RandomForestClassifier(n_estimators=10, random_state=42).fit(X_train, y_train) + return X_train, X_test, y_train, y_test, model + + +def test_quickstart_index_example(cancer_data_and_model): + X_train, X_test, y_train, y_test, model = cancer_data_and_model + exp = explain(model, X_train, instance=X_test.iloc[0]) + text = exp.to_text() + assert isinstance(text, str) + assert len(text) > 0 + + +def test_getting_started_local_and_global(iris_data_and_model): + X, y, model = iris_data_and_model + + # Local explanation + exp = explain(model, X, instance=X.iloc[0]) + top_3 = exp.top(3) + assert len(top_3) == 3 + assert exp.agreement_level() in ("high", "moderate", "low") + assert isinstance(exp.agreement, float) + + df = exp.to_dataframe() + assert not df.empty + md = exp.to_markdown() + assert "|" in md + d = exp.to_dict() + assert "features" in d + assert "agreement" in d + + # Global explanation + glob = explain_global(model, X, y=y) + assert glob.scope == "global" + assert len(glob.to_text()) > 0 + + # Custom methods & aggregation + exp_surr = explain(model, X, instance=X.iloc[0], methods="surrogate") + assert exp_surr.method == "surrogate" + + exp_rank = explain( + model, + X, + instance=X.iloc[0], + methods=["coalition", "surrogate"], + weights={"coalition": 2.0, "surrogate": 1.0}, + aggregation="rank", + ) + assert exp_rank.method == "consensus" + assert exp_rank.metadata["aggregation"] == "rank" + + +def test_coalition_explainer_example(cancer_data_and_model): + X_train, X_test, _, _, model = cancer_data_and_model + explainer = CoalitionExplainer(model, X_train, n_background=20, random_state=42) + exp = explainer.explain_instance(X_test.iloc[0]) + assert exp.scope == "local" + assert len(exp.values) == X_train.shape[1] + + glob_exp = explainer.explain_global(X_test.iloc[:10]) + assert glob_exp.scope == "global" + + +def test_surrogate_explainer_example(cancer_data_and_model): + X_train, X_test, _, _, model = cancer_data_and_model + explainer = LocalSurrogateExplainer( + model=model, + X_background=X_train, + n_samples=200, + kernel_width=None, + random_state=42, + ) + exp = explainer.explain_instance(X_test.iloc[0]) + assert exp.metadata.get("local_r2") is not None + assert "coefficients" in exp.metadata + + +def test_permutation_explainer_example(cancer_data_and_model): + _, X_test, _, y_test, model = cancer_data_and_model + explainer = PermutationExplainer( + model=model, + X_background=X_test, + metric="log_loss", + n_repeats=3, + random_state=42, + ) + glob_exp = explainer.explain_global(X_test, y=y_test) + assert glob_exp.scope == "global" + assert len(glob_exp.top(5)) == 5 + assert "importances_std" in glob_exp.metadata + + # Custom metric callable example + custom_expl = PermutationExplainer( + model=model, + X_background=X_test, + metric=lambda y_true, y_prob: roc_auc_score(y_true, y_prob[:, 1]), + n_repeats=3, + random_state=42, + ) + custom_exp = custom_expl.explain_global(X_test, y=y_test) + assert custom_exp.scope == "global" + + +def test_custom_gradient_explainer_example(iris_data_and_model): + """Smoke test the exact custom explainer code published in docs/extending.md.""" + X, _, model = iris_data_and_model + + @register_explainer("custom_gradient_smoke") + class CustomGradientSmokeExplainer(BaseExplainer): + name = "custom_gradient_smoke" + supports_local = True + supports_global = False + requires_y = False + + def _explain_instance(self, x: np.ndarray, target: int | None) -> Explanation: + base_pred = float(self._predict_scalar(x.reshape(1, -1), target)[0]) + eps = 1e-4 + n_features = len(x) + attributions = np.zeros(n_features) + + for i in range(n_features): + x_perturbed = x.copy() + x_perturbed[i] += eps + pred_perturbed = float(self._predict_scalar(x_perturbed.reshape(1, -1), target)[0]) + attributions[i] = (pred_perturbed - base_pred) / eps + + return self._make_explanation( + values=attributions, + x=x, + target=target, + prediction=base_pred, + base_value=None, + step_size=eps, + ) + + expl = CustomGradientSmokeExplainer(model, X) + exp = expl.explain_instance(X.iloc[0]) + assert exp.method == "custom_gradient_smoke" + assert exp.metadata.get("step_size") == 1e-4 + assert len(exp.values) == X.shape[1] + + # Consensus with the new explainer + combined = explain(model, X, instance=X.iloc[0], methods=["coalition", "custom_gradient_smoke"]) + assert combined.method == "consensus" + assert "custom_gradient_smoke" in combined.components