CaseExplainer

The main class for creating case-based explanations.

class case_explainer.CaseExplainer(X_train, y_train, k=5, feature_names=None, class_names=None, metric='euclidean', algorithm='auto', scale_data=True, class_weights=None, metadata=None, n_jobs=-1, similarity=None, activation_extractor=None, model=None, blend_alpha=0.0, activation_layer=None, use_output_weights=True, retrieval=None)[source]

Bases: object

General-purpose case-based explainability module.

Provides model-agnostic explanations through training set precedent and nearest neighbor correspondence. Builds k-NN index during initialization for fast lookups during explanation.

Based on refined Method 2 from hardware trojan detection pipeline: - Pre-builds NearestNeighbors index on training data - Uses distance-weighted correspondence: weight = 1 / (distance + 1)^3 - Supports class weights for imbalanced datasets - Compatible with any classifier (sklearn, XGBoost, etc.)

Example

>>> from case_explainer import CaseExplainer
>>> explainer = CaseExplainer(X_train, y_train, k=5)
>>> explanation = explainer.explain_instance(test_sample, model=clf)
>>> print(f"Correspondence: {explanation.correspondence:.2%}")
>>> explanation.plot()

Core Methods

Building the Explainer

CaseExplainer.__init__(X_train, y_train, k=5, feature_names=None, class_names=None, metric='euclidean', algorithm='auto', scale_data=True, class_weights=None, metadata=None, n_jobs=-1, similarity=None, activation_extractor=None, model=None, blend_alpha=0.0, activation_layer=None, use_output_weights=True, retrieval=None)[source]

Initialize CaseExplainer with training data and build k-NN index.

Parameters:
  • X_train (Union[ndarray, DataFrame]) – Training features (n_samples, n_features)

  • y_train (Union[ndarray, Series, List]) – Training labels (n_samples,)

  • k (int) – Number of nearest neighbors for explanations (default: 5)

  • feature_names (Optional[List[str]]) – Names of features (optional)

  • class_names (Optional[Dict[Any, str]]) – Mapping from class labels to names (optional)

  • metric (str) – Distance metric (default: ‘euclidean’)

  • algorithm (str) – k-NN algorithm - ‘auto’, ‘ball_tree’, ‘kd_tree’, ‘brute’ (default: ‘auto’)

  • scale_data (bool) – Whether to standardize features (recommended: True)

  • class_weights (Optional[Dict[Any, float]]) – Optional weights for each class in correspondence computation e.g., {0: 1.0, 1: 2.0} to weight class 1 twice as much

  • metadata (Optional[Dict[str, List]]) – Optional dict with metadata for each training sample e.g., {‘sample_id’: […], ‘source’: […], …}

  • n_jobs (int) – Number of parallel jobs for k-NN search (-1 = all CPUs)

  • similarity (Union[str, Features, HiddenActivations, CustomActivations, TreeLeaf, ForestProximity, Blend, None]) –

    How neighbor similarity is computed. This is the single knob for retrieval mode. Accepts:

    • None (default) or "features" / Features() — raw feature space.

    • "activations" — hidden-activation space of model (which must be a fitted MLPClassifier).

    • A strategy object: HiddenActivations(...), CustomActivations(...), TreeLeaf(...), ForestProximity(...), or Blend(strategy, features=...) to mix an activation space with features.

    The model lives on the strategy, so an invalid combination (e.g. activations without a model) cannot be expressed.

  • activation_extractor (Optional[ActivationExtractor]) – Deprecated. Use similarity=CustomActivations(model=..., extractor=...).

  • model (Optional[Predictor]) – Trained model. Required only for the string shorthand similarity="activations"; otherwise the model lives on the strategy object.

  • blend_alpha (float) – Deprecated. Use similarity=Blend(strategy, features=...).

  • activation_layer (Union[str, int, None]) – Deprecated. Use similarity=HiddenActivations(model=..., layer=...).

  • use_output_weights (bool) – Deprecated. Use similarity=HiddenActivations(..., output_weighting=...) where "none" replaces use_output_weights=False.

  • retrieval (Union[HiddenActivationRetrieval, CustomActivationRetrieval, TreeLeafRetrieval, ForestProximityRetrieval, None]) – Deprecated. Use similarity= with the renamed strategy classes (HiddenActivations, CustomActivations, TreeLeaf, ForestProximity).

Explaining Predictions

CaseExplainer.explain_instance(test_sample, test_index=None, true_class=None, predicted_class=None, model=None, k=None, return_provenance=True, distance_weighted=True)[source]

Explain a prediction using case-based reasoning with k-NN precedent.

This method: 1. Finds k nearest neighbors in the pre-built index 2. Computes weighted correspondence based on neighbor labels 3. Returns explanation with neighbor details and correspondence score

Parameters:
  • test_sample (Union[ndarray, Series, List]) – Sample to explain (n_features,)

  • test_index (Optional[int]) – Index in test set (optional, for tracking)

  • true_class (Optional[Any]) – True class label (optional, for validation)

  • predicted_class (Optional[Any]) – Predicted class (optional, will use model if not provided)

  • model (Optional[Any]) – Trained model with predict() method (optional)

  • k (Optional[int]) – Number of neighbors (optional, uses default from init if not provided)

  • return_provenance (bool) – Include metadata in explanation

  • distance_weighted (bool) – Use distance weighting for correspondence

Return type:

Explanation

Returns:

Explanation object with neighbors and correspondence

CaseExplainer.explain_batch(X_test, y_test=None, predictions=None, model=None, k=None, return_provenance=True, distance_weighted=True)[source]

Explain multiple predictions efficiently.

Parameters:
  • X_test (Union[ndarray, DataFrame]) – Test samples (n_samples, n_features)

  • y_test (Union[ndarray, Series, List, None]) – True labels (optional)

  • predictions (Union[ndarray, List, None]) – Predicted labels (optional, will use model if not provided)

  • model (Optional[Any]) – Trained model (optional)

  • k (Optional[int]) – Number of neighbors (optional, uses default from init)

  • return_provenance (bool) – Include metadata

  • distance_weighted (bool) – Use distance weighting

Return type:

List[Explanation]

Returns:

List of Explanation objects

Similarity Strategies

class case_explainer.Features(model=None)[source]

Retrieve neighbors in raw (scaled) feature space.

This is the default behaviour and the feature-space half of the Caruana et al. (1999) comparison. Passing similarity=Features() is equivalent to passing no similarity at all; it exists so the two halves of the comparison read symmetrically in user code.

Parameters:

model (Optional[Any]) – Optional fitted classifier used to supply the predicted class during explain_instance. Feature-space distance itself needs no model, but binding one here lets the input-space and activation-space branches share an identical call signature. A call-time model= or predicted_class= still overrides it.

model: Optional[Any] = None
class case_explainer.HiddenActivations(model, layer='last_hidden', output_weighting='mean_abs', input_transform=None)[source]

Retrieve neighbors in the hidden-activation space of an sklearn MLP.

Implements Caruana et al. (1999): the trained network’s internal representation becomes the distance metric for case retrieval.

Parameters:
  • model (Any) – A fitted MLPClassifier.

  • layer (Union[str, int]) – "last_hidden" (default), "all_hidden", or an integer hidden-layer index.

  • output_weighting (str) – How to weight hidden units by their contribution to the output. One of "mean_abs" (default), "predicted_class", or "none".

  • input_transform (Optional[Any]) – Optional preprocessing applied before the forward pass.

Wrap this in Blend to mix activation space with feature space.

model: Any
layer: Union[str, int] = 'last_hidden'
output_weighting: str = 'mean_abs'
input_transform: Optional[Any] = None
class case_explainer.CustomActivations(model, extractor)[source]

Retrieve neighbors using a user-provided activation extractor.

Parameters:
  • model (Any) – The fitted model the extractor reads activations from.

  • extractor (ActivationExtractor) – An ActivationExtractor implementation.

model: Any
extractor: ActivationExtractor
class case_explainer.TreeLeaf(model, overflow='truncate', within_leaf='feature_distance')[source]

Retrieve training cases that share a decision-tree leaf.

Parameters:
  • model (Any) – A fitted DecisionTreeClassifier.

  • overflow (str) – "truncate" (default) returns only same-leaf cases; "nearest" backfills from the closest out-of-leaf cases when a leaf holds fewer than k training samples.

  • within_leaf (str) – How same-leaf cases are ordered. Only "feature_distance" is supported.

model: Any
overflow: str = 'truncate'
within_leaf: str = 'feature_distance'
class case_explainer.ForestProximity(model)[source]

Retrieve training cases by random-forest shared-leaf proximity.

Parameters:

model (Any) – A fitted RandomForestClassifier.

model: Any
class case_explainer.Blend(strategy, features=0.3)[source]

Blend an activation strategy with raw feature space.

Parameters:
  • strategy (Any) – A HiddenActivations or CustomActivations instance whose activation space is mixed with features.

  • features (float) – Weight given to feature space, in [0, 1]. The remainder goes to the wrapped strategy’s activation space. The index is built on [sqrt(features) * X, sqrt(1 - features) * A], so features=0.0 is pure activations and features=1.0 is pure features.

strategy: Any
features: float = 0.3

Deprecated Retrieval Configurations

class case_explainer.HiddenActivationRetrieval(model, layer='last_hidden', use_output_weights=True, output_weighting='mean_abs', blend_alpha=0.0, input_transform=None)[source]

Deprecated. Use HiddenActivations (with Blend).

output_weighting accepts "mean_abs" (the compatibility default), "predicted_class", or "none". Setting the legacy use_output_weights field to false also selects unweighted activations.

model: Any
layer: Union[str, int] = 'last_hidden'
use_output_weights: bool = True
output_weighting: str = 'mean_abs'
blend_alpha: float = 0.0
input_transform: Optional[Any] = None
class case_explainer.CustomActivationRetrieval(model, extractor, blend_alpha=0.0)[source]

Deprecated. Use CustomActivations (with Blend).

model: Any
extractor: ActivationExtractor
blend_alpha: float = 0.0
class case_explainer.TreeLeafRetrieval(model, overflow='truncate', within_leaf='feature_distance')[source]

Deprecated. Use TreeLeaf.

model: Any
overflow: str = 'truncate'
within_leaf: str = 'feature_distance'
class case_explainer.ForestProximityRetrieval(model)[source]

Deprecated. Use ForestProximity.

model: Any

Example Usage

Basic Example

from case_explainer import CaseExplainer
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier

# Load and split data
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.3, random_state=42
)

# Train classifier
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)

# Create explainer
explainer = CaseExplainer(
    X_train=X_train,
    y_train=y_train,
    feature_names=data.feature_names,
    algorithm='ball_tree',
    scale_data=True
)

# Explain a prediction
explanation = explainer.explain_instance(
    test_sample=X_test[0],
    k=5,
    model=clf,
    true_class=y_test[0]
)

print(f"Correspondence: {explanation.correspondence:.2%}")
print(f"Predicted class: {explanation.predicted_class}")
print(f"Correct: {explanation.is_correct()}")

Batch Explanations

# Explain multiple predictions at once
explanations = explainer.explain_batch(
    X_test[:100],
    k=5,
    y_test=y_test[:100],
    model=clf
)

# Analyze correspondence distribution
correspondences = [exp.correspondence for exp in explanations]
correct_corr = [exp.correspondence for exp in explanations if exp.is_correct()]
incorrect_corr = [exp.correspondence for exp in explanations if not exp.is_correct()]

print(f"Mean correspondence: {sum(correspondences)/len(correspondences):.2%}")
print(f"Correct predictions: {sum(correct_corr)/len(correct_corr):.2%}")
print(f"Incorrect predictions: {sum(incorrect_corr)/len(incorrect_corr):.2%}")

Working with Metadata

# Attach metadata to training samples
metadata = {
    'sample_id': [f"patient_{i}" for i in range(len(X_train))],
    'date': ['2024-01-01'] * len(X_train),
    'source': ['hospital_A'] * len(X_train)
}

explainer = CaseExplainer(
    X_train=X_train,
    y_train=y_train,
    metadata=metadata,
    algorithm='ball_tree'
)

# Access metadata in explanations
explanation = explainer.explain_instance(X_test[0], k=5, model=clf)
for neighbor in explanation.neighbors:
    print(f"Neighbor {neighbor.index}: {neighbor.metadata}")

Configuration Options

Algorithm Selection

Choose the indexing algorithm based on your data characteristics:

  • kd_tree: Best for low-dimensional data (<20 features), fastest for small to medium datasets

  • ball_tree: Better for high-dimensional data (>20 features), good all-around choice

  • brute: Exact search, only recommended for small datasets (<5k samples)

  • auto: Let scikit-learn choose based on data characteristics (default)

# For low-dimensional data
explainer = CaseExplainer(X_train, y_train, algorithm='kd_tree')

# For high-dimensional data
explainer = CaseExplainer(X_train, y_train, algorithm='ball_tree')

# For very small datasets
explainer = CaseExplainer(X_train, y_train, algorithm='brute')

Feature Scaling

Feature scaling is recommended to prevent features with large ranges from dominating distance calculations:

# With scaling (recommended)
explainer = CaseExplainer(X_train, y_train, scale_data=True)

# Without scaling (if features are already normalized)
explainer = CaseExplainer(X_train, y_train, scale_data=False)

Class Weights

For imbalanced datasets, you can weight classes differently in correspondence computation:

# Weight minority class more heavily
explainer = CaseExplainer(
    X_train, y_train,
    class_weights={0: 1.0, 1: 5.0}  # Weight class 1 five times more
)

Notes

Performance Considerations

  • Index building time is O(n log n) for tree-based methods

  • Query time is O(log n) for tree-based methods, O(n) for brute force

  • Memory usage scales with dataset size and dimensionality

  • Use n_jobs=-1 to parallelize nearest neighbor search

Correspondence Interpretation

  • High (≥85%): Strong agreement with training precedent, high confidence

  • Medium (70-85%): Moderate agreement, reasonable confidence

  • Low (<70%): Weak agreement, prediction may be uncertain or unusual

Activation-Based Similarity

When your model is a neural network, you can optionally build the k-NN index on the model’s hidden-layer activations instead of raw input features, an approach described by Caruana et al. (1999). The retrieved cases show consequences of the model’s learned representation. They do not expose its complete internal reasoning.

Quick Start

from sklearn.neural_network import MLPClassifier
from case_explainer import CaseExplainer, HiddenActivations

mlp = MLPClassifier(hidden_layer_sizes=(64, 32, 16), random_state=42)
mlp.fit(X_train, y_train)

# Pure activation-based similarity, using the last hidden layer
explainer = CaseExplainer(
    X_train, y_train,
    similarity=HiddenActivations(
        model=mlp,
        layer="last_hidden",
    ),
)

# All-layer aggregation is a library extension.
explainer_deep = CaseExplainer(
    X_train, y_train,
    similarity=HiddenActivations(model=mlp, layer="all_hidden"),
)

explanation = explainer.explain_instance(X_test[0])

Output weighting

output_weighting="mean_abs" is the compatibility default. For multiclass models, it averages each hidden unit’s absolute connections across output classes and uses one retrieval index for every query.

output_weighting="predicted_class" weights hidden units using only the predicted class’s output connections. The explainer builds one index per class and selects the matching index after prediction. sklearn class labels are mapped through model.classes_, so labels do not need to be contiguous or zero-based. Binary sklearn MLP classifiers have one output column and therefore use the same connection magnitudes for both labels.

similarity = HiddenActivations(
     model=mlp,
     output_weighting="predicted_class",
)
explainer = CaseExplainer(X_train, y_train, similarity=similarity)

Set output_weighting="none" to use standardized activations without output connection weighting.

Layer modes

'last_hidden'

Uses the last hidden layer, based on Caruana (1999). output_weighting="mean_abs" applies Section 5 scaling.

'all_hidden'

Concatenates independently standardized hidden layers and scales layer \(i\) by \(w_i = \sqrt{(i+1)/n}\). This is a library extension with \(d^2 = \sum_i \frac{i+1}{n} \|\Delta a_i\|^2\).

int

Selects a specific hidden layer by 0-based index. Out-of-range indices raise ValueError.

Using an explicit extractor

For full control, wrap a SklearnMLPActivationExtractor in CustomActivations:

from case_explainer import (
    CaseExplainer, CustomActivations, SklearnMLPActivationExtractor,
)

ext = SklearnMLPActivationExtractor(layer="all_hidden", use_output_weights=True)
explainer = CaseExplainer(
    X_train, y_train,
    similarity=CustomActivations(model=mlp, extractor=ext),
)

Feature/activation hybrid

Blend mixes feature-space and activation-space distances:

from case_explainer import Blend, HiddenActivations

# 30% features, 70% activations
explainer = CaseExplainer(
    X_train, y_train,
    similarity=Blend(HiddenActivations(model=mlp), features=0.3),
)

\(d^2 = \alpha\,d_\text{feat}^2 + (1-\alpha)\,d_\text{act}^2\)

features=0.0 is pure activations; features=1.0 is pure features (identical to feature retrieval). Hybrid retrieval is a library extension and is not defined by Caruana et al. (1999).

Model preprocessing

When an MLP was trained on transformed features, provide the fitted transformer:

similarity = HiddenActivations(
    model=mlp,
    input_transform=fitted_scaler,
)
explainer = CaseExplainer(X_train, y_train, similarity=similarity)

The transform is applied before activation extraction and prediction. The scale_data parameter remains responsible only for feature-space retrieval.

Decision-tree leaves

For a fitted sklearn decision tree, paper-faithful retrieval returns only cases from the query’s leaf. If fewer than k cases occupy that leaf, the default behavior returns fewer cases rather than crossing the leaf boundary. If the retained case base contains no case from the query leaf, the explanation contains no neighbors and reports correspondence as undefined.

from case_explainer import TreeLeaf

explainer = CaseExplainer(
    X_train, y_train,
    similarity=TreeLeaf(model=tree, overflow="truncate"),
)

Set overflow="nearest" to explicitly fill the remaining positions with feature-nearest cases from other leaves.

Random-forest proximity

For a fitted sklearn random forest, proximity is the fraction of trees in which two samples reach the same leaf. ForestProximity retrieves the cases with the smallest distance

\[d(x,z) = 1 - \frac{1}{T}\sum_{t=1}^{T} \mathbf{1}\{\ell_t(x)=\ell_t(z)\}.\]
from case_explainer import ForestProximity

explainer = CaseExplainer(
     X_train, y_train,
     similarity=ForestProximity(model=forest),
)

This strategy compares leaf identities directly. It does not treat tree node identifiers as numeric coordinates.

Convenience aliases

The activation_extractor, activation_layer, use_output_weights, and blend_alpha constructor path, along with the retrieval= parameter, remains supported but emits DeprecationWarning. New code should use similarity= with the strategy classes (Features, HiddenActivations, CustomActivations, TreeLeaf, ForestProximity, Blend) because it keeps one similarity strategy together and makes preprocessing ownership explicit.

See Also

  • Explanation: The explanation object returned by explain_instance

  • case_explainer.metrics: Correspondence and distance metrics