Skip to main content

PerCE: Hierarchical Perturbation-Based Counterfactual Explanations

PyPI version Python 3.9+ License: MIT CI IEEE Access

PerCE generates plausible counterfactual explanations for multivariate time series classification.

"What minimal change to this ECG would cause the model to predict a different diagnosis?"

PerCE answers that question using a hierarchical perturbation strategy guided by permutation-based feature importance, operating at both temporal-segment and channel levels. It anchors every counterfactual to a real training instance (InSample), enabling physiologically plausible results.


Paper

Bayrak, B. & Bach, K. (2025). PerCE: Hierarchical Perturbation-Based Counterfactual Explanations for Multivariate Time Series Classification. IEEE Access. DOI: 10.1109/ACCESS.2025.3639125

If you use PerCE in your work, please cite:

@article{bayrak2025perce,
  title   = {{PerCE}: Hierarchical Perturbation-Based Counterfactual Explanations
             for Multivariate Time Series Classification},
  author  = {Bayrak, Bet\"{u}l and Bach, Kerstin},
  journal = {IEEE Access},
  year    = {2025},
  doi     = {10.1109/ACCESS.2025.3639125}
}

Key Results

Evaluated on the open CODE-test ECG dataset (827 12-lead recordings, cardiologist-annotated), PerCE substantially outperforms the InSample baseline:

from perce import evaluate_batch

summary = evaluate_batch(results)
print(f"Validity:  {summary['validity_rate']:.2%}")
print(f"Proximity: {summary['proximity_mean']:.3f} ± {summary['proximity_std']:.3f}")
print(f"Sparsity:  {summary['sparsity_mean']:.3f} ± {summary['sparsity_std']:.3f}")
print(f"Diversity: {summary['diversity']:.3f}")
Metric InSample baseline PerCE Improvement
Validity ↑ 0.65 ± 0.35 0.98 ± 0.05 +51%
Proximity ↓ 200 ± 150 50 ± 25 −75%
Sparsity ↑ 0.70 ± 0.15 0.40 ± 0.12 +43%
Diversity 0.30 ± 0.10 0.20 ± 0.08 (tunable)

Validity: 1.0 = always achieves class change. Proximity: lower = closer to original. Sparsity: lower = fewer segments modified.


Installation

pip install perce

With ECG demo dependencies (h5py, matplotlib):

pip install "perce[ecg]"

Requirements: Python ≥ 3.9, NumPy ≥ 1.24, SciPy ≥ 1.10, scikit-learn ≥ 1.3. No Java. No external REST API.


Quick Start

import numpy as np
from perce import PerCEExplainer

# 1. Wrap your model
#    Must accept (N, C, T) and return (N,) class predictions
def my_model(X):
    # PyTorch example:
    # import torch
    # with torch.no_grad():
    #     return net(torch.tensor(X)).argmax(dim=1).numpy()
    return np.zeros(len(X), dtype=int)   # stub

# 2. Fit (store training data for InSample candidate selection) 
exp = PerCEExplainer(
    model=my_model,
    n_segments=10,   # divide each time series into 10 segments
    alpha=0.5,       # segment-level interpolation strength
    beta=0.6,        # channel-level fallback strength
    k=5,             # k-nearest neighbours for candidate selection
)
exp.fit(X_train, y_train)   # X_train shape: (N, C, T)

# 3. Explain a single instance
result = exp.explain(X_query, target_class=1)   # X_query shape: (C, T)
print(result.summary())

# 4. Access everything 
cf  = result.counterfactual       # shape (C, T) — the explanation
print("Valid?    ", result.is_valid)
print("Proximity:", result.proximity_score)
print("Sparsity: ", result.sparsity_score)
print("Channels modified:", result.channels_modified)

Batch explanation

results = exp.explain_batch(X_test, target_classes=1, verbose=True)

from perce import evaluate_batch
summary = evaluate_batch(results)
print(f"Validity rate: {summary['validity_rate']:.2%}")
print(f"Proximity:     {summary['proximity_mean']:.3f} ± {summary['proximity_std']:.3f}")

How It Works

PerCE follows a three-stage hierarchical algorithm:

Query X (C channels × T time points)
         │
         ▼
① Feature Importance          ② InSample Candidate
  • Channel-level (Ich)          k-NN from target class
  • Segment-level (Iseg)         via DTW distance
  Both: permutation-based,
  model-agnostic
         │                              │
         └──────────────┬───────────────┘
                        ▼
             ③ Hierarchical Perturbation
               For each channel c (most→least important):
                 For each segment s (most→least important):
                   X'[c,s] = (1−α)·X[c,s] + α·Xcand[c,s]
                   If model predicts target → return X'
               Fallback: full-channel replacement (beta)
                        │
                        ▼
             Counterfactual X' ← real ECG pattern,
             minimal changes, high validity

Why hierarchical?

Traditional perturbation methods modify all features blindly. PerCE focuses on the most informative channels and time windows first, this facilitates producing sparser, more clinically meaningful explanations.

Why InSample?

By anchoring to real training instances, every generated counterfactual is guaranteed to be within the data distribution. No out-of-distribution artifacts.


Notebooks

Notebook Description
01_ECG_demo.ipynb Full pipeline on CODE-test — reproduces Table 1 from paper
02_custom_model.ipynb How to plug in your own PyTorch / sklearn model
03_evaluation.ipynb Comprehensive evaluation

Related Packages

This package is part of a growing XAI ecosystem from NTNU's NorwAI Centre:

Package What it does Paper
PerCE Counterfactual explanations for time series IEEE Access 2025
PertCF Counterfactual explanations for tabular data SGAI 2023
CEval Evaluation framework for any counterfactual method IEEE Access 2024

Repository Structure

perce/
├── perce/
│   ├── __init__.py          # Public API
│   ├── explainer.py         # PerCEExplainer + CounterfactualResult
│   ├── importance.py        # Channel- and segment-level permutation importance
│   ├── neighbors.py         # DTW-based k-NN candidate selection
│   ├── perturbation.py      # Hierarchical interpolation (Algorithm 1)
│   └── metrics.py           # Proximity, Sparsity, Validity, Diversity
├── notebooks/
│   ├── 01_ECG_demo.ipynb
│   ├── 02_custom_model.ipynb
│   └── 03_evaluation.ipynb
├── tests/
│   └── test_perce.py
├── pyproject.toml
└── README.md

API Reference

PerCEExplainer

Parameter Default Description
model — Callable: (N, C, T) → (N,) class predictions
n_segments 10 Number of temporal segments
alpha 0.5 Segment-level interpolation weight
beta 0.6 Channel-level fallback interpolation weight
k 5 Nearest neighbours for candidate selection
dtw_window 0.1 Sakoe-Chiba band (fraction of T)
random_state 42 Reproducibility seed

CounterfactualResult

Attribute Type Description
.counterfactual ndarray (C,T) The generated explanation
.is_valid bool Did it achieve the target class?
.proximity_score float Normalised DTW distance (lower=better)
.sparsity_score float Fraction of segments unchanged (higher=better)
.channels_modified list[int] Which channels were touched
.candidate ndarray (C,T) InSample anchor used

Running Tests

git clone https://github.com/b-bayrak/PerCE
cd PerCE
pip install -e ".[dev]"
pytest tests/ -v

Acknowledgements

This work was supported by the Research Council of Norway through the SFI NorwAI (Norwegian Research Center for AI Innovation), grant number 309834.


License

MIT © Betül Bayrak

Metadata

Release files for perce 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for perce 0.1.0
File Size Uploaded Provenance Actions
perce-0.1.0.tar.gz 19.5 kB Without provenance Details for perce-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for perce 0.1.0
File Interpreter ABI Platform Actions
perce-0.1.0-py3-none-any.whl Python 3 none any Details for perce-0.1.0-py3-none-any.whl

Total release size: 36.3 kB

Release files / perce-0.1.0.tar.gz

Download URL perce-0.1.0.tar.gz
Size 19.5 kB
Tags Source
SHA-256 checksum
How to use checksums
18f6be73457f38dcc8b756e423976b2de7a66aabe33644c890605fc32924a84f
BLAKE2b-256 checksum
How to use checksums
f126cab0857729daedfa6d23238188b7f483a78e706f59d22de9f3fc32bc5af3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.16

Release files / perce-0.1.0-py3-none-any.whl

Download URL perce-0.1.0-py3-none-any.whl
Size 16.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
496a9295f68dfe15407da36959b84c939fc5fb8bfd5e7999745461c2bdb2a325
BLAKE2b-256 checksum
How to use checksums
33b820f62054e8d260c04934bd181fa0bd81c69399f6981c35aea042c4501404
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.16

Security for perce 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page