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:
objectGeneral-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 muchmetadata (
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 ofmodel(which must be a fittedMLPClassifier).A strategy object:
HiddenActivations(...),CustomActivations(...),TreeLeaf(...),ForestProximity(...), orBlend(strategy, features=...)to mix an activation space with features.
The
modellives on the strategy, so an invalid combination (e.g. activations without a model) cannot be expressed.activation_extractor (
Optional[ActivationExtractor]) – Deprecated. Usesimilarity=CustomActivations(model=..., extractor=...).model (
Optional[Predictor]) – Trained model. Required only for the string shorthandsimilarity="activations"; otherwise the model lives on the strategy object.blend_alpha (
float) – Deprecated. Usesimilarity=Blend(strategy, features=...).activation_layer (
Union[str,int,None]) – Deprecated. Usesimilarity=HiddenActivations(model=..., layer=...).use_output_weights (
bool) – Deprecated. Usesimilarity=HiddenActivations(..., output_weighting=...)where"none"replacesuse_output_weights=False.retrieval (
Union[HiddenActivationRetrieval,CustomActivationRetrieval,TreeLeafRetrieval,ForestProximityRetrieval,None]) – Deprecated. Usesimilarity=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 explanationdistance_weighted (
bool) – Use distance weighting for correspondence
- Return type:
- 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)k (
Optional[int]) – Number of neighbors (optional, uses default from init)return_provenance (
bool) – Include metadatadistance_weighted (
bool) – Use distance weighting
- Return type:
- 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 nosimilarityat 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 duringexplain_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-timemodel=orpredicted_class=still overrides it.
- 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 fittedMLPClassifier.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
Blendto mix activation space with feature space.
- 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) – AnActivationExtractorimplementation.
-
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 fittedDecisionTreeClassifier.overflow (
str) –"truncate"(default) returns only same-leaf cases;"nearest"backfills from the closest out-of-leaf cases when a leaf holds fewer thanktraining samples.within_leaf (
str) – How same-leaf cases are ordered. Only"feature_distance"is supported.
- class case_explainer.ForestProximity(model)[source]
Retrieve training cases by random-forest shared-leaf proximity.
- Parameters:
model (
Any) – A fittedRandomForestClassifier.
- class case_explainer.Blend(strategy, features=0.3)[source]
Blend an activation strategy with raw feature space.
- Parameters:
strategy (
Any) – AHiddenActivationsorCustomActivationsinstance 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], sofeatures=0.0is pure activations andfeatures=1.0is pure features.
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(withBlend).output_weightingaccepts"mean_abs"(the compatibility default),"predicted_class", or"none". Setting the legacyuse_output_weightsfield to false also selects unweighted activations.
- class case_explainer.CustomActivationRetrieval(model, extractor, blend_alpha=0.0)[source]
Deprecated. Use
CustomActivations(withBlend).-
extractor:
ActivationExtractor
-
extractor:
- class case_explainer.TreeLeafRetrieval(model, overflow='truncate', within_leaf='feature_distance')[source]
Deprecated. Use
TreeLeaf.
- class case_explainer.ForestProximityRetrieval(model)[source]
Deprecated. Use
ForestProximity.
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=-1to 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\).
intSelects 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
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 byexplain_instancecase_explainer.metrics: Correspondence and distance metrics