Skip to main content

DiCEx: Directional Counterfactual Explanations

CI Wheels Docs Coverage
PyPI Python Preprint License: MIT
uv Ruff Checked with pyright (strict) pre-commit Rust + maturin Renovate

DiCEx tells you in which direction to change the features of a record to improve the prediction of a machine learning model, choosing the direction that remains reliable when the change is carried out imprecisely.

Classical counterfactual explanations prescribe a precise endpoint ("raise your income to exactly 52,300"). In practice, people execute recommendations with an uncertain magnitude, and an endpoint that sits next to a decision boundary can easily backfire. DiCEx instead prescribes a direction of change and evaluates it by the improvement it delivers when the length of the move is random, using a risk-averse criterion (the lower-tail CVaR) that rewards reliable gains rather than best-case ones. When no direction is reliably better than doing nothing, DiCEx recommends not acting.

DiCEx works with any fitted model that exposes predict (regression) or predict_proba (classification): scikit-learn estimators, gradient-boosted trees, neural networks, or your own black box. The formulation and the optimization algorithm (vMF-VNS, a derivative-free search on the sphere of directions with a Rust core) are described in the preprint.

Contents

Installation

DiCEx requires Python 3.12 or later. Prebuilt wheels are provided for Linux (x86_64, aarch64), macOS (Apple silicon, Intel), and Windows (x86_64):

pip install dicex

or, with uv:

uv add dicex

To install from source (this compiles the Rust extension and needs a Rust toolchain):

pip install git+https://github.com/javiermartinch/dicex.git

The only runtime dependencies are NumPy and SciPy.

Quick start

A company's loan application has been rejected. In which direction should it improve its figures so that approval becomes likely, even if the improvements turn out larger or smaller than planned?

import numpy as np
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

from dicex import Dicex, GaussianPerturbation

# Synthetic loan applications from companies. The three features are levers the company
# can act on, and they are all continuous, signed, and measured in percentage points.
features = ["operating margin (%)", "sales growth (%)", "working capital (% of sales)"]
rng = np.random.default_rng(0)
n = 3000
x_train = np.column_stack([rng.normal(5.0, 8.0, n), rng.normal(3.0, 10.0, n), rng.normal(10.0, 8.0, n)])
score = x_train @ np.array([0.10, 0.05, 0.07]) + rng.normal(0, 0.5, n)
y_train = (score > 1.2).astype(int)  # 1 = loan approved
model = make_pipeline(StandardScaler(), MLPClassifier(hidden_layer_sizes=(32, 32), max_iter=2000, random_state=0))
model.fit(x_train, y_train)

company = np.array([2.0, 0.0, 8.0])
print(f"P(approved) today: {model.predict_proba(company[None])[0, 1]:.2f}")

explainer = Dicex(
    model,
    task="classification",
    target_class=1,  # raise the probability of approval
    # The company will move along the recommended direction, but by an uncertain
    # amount: about 0.5 +/- 0.15 standard deviations of each feature.
    perturbation=GaussianPerturbation(mu=0.5, sigma=0.15),
    alpha=0.1,  # optimize the worst 10% of outcomes
    seed=42,
    verbose="none",
).fit(x_train)

result = explainer.explain(company)
# Recommended direction of change, in the units of each feature.
for name, c in zip(features, result.direction, strict=True):
    print(f"{name:>28}: {c:+.2f}")
print(f"Gain in P(approved) in the worst 10% of executions: {result.robust_value:+.2f}")
P(approved) today: 0.15
        operating margin (%): +0.60
            sales growth (%): +0.57
working capital (% of sales): +0.56
Gain in P(approved) in the worst 10% of executions: +0.30

The direction gives the proportions of the change in the units of each feature, here percentage points: DiCEx recommends improving the three together, in similar measure, rather than betting on a single one. Moving along that direction with a random step raises the probability of approval by at least 0.30 in 90% of the executions.

Inputs and outputs

What you provide:

Input Description
model Any fitted model with predict(X) (regression) or predict_proba(X) (classification): scikit-learn, XGBoost, a neural network, or your own function wrapped in a class.
task, target_class "regression" raises the prediction; "classification" raises the probability of the class target_class.
perturbation How imprecisely the change will be carried out: the random step length T along the direction and, optionally, additional noise. GaussianPerturbation(mu, sigma), UniformPerturbation(mu, delta) or CustomPerturbation(sample_fn).
alpha Risk level of the criterion: 1.0 maximizes the expected gain, 0.1 the mean of the worst 10% of outcomes.
fit(x_train) Data used to standardize the features, so that the perturbation is expressed in standard deviations of each feature (scaler="auto", the default). Use scaler=None to work in the original units instead.
preset, seed Computational budget of the optimizer ("low", "mid", "high") and a seed for reproducible results.
explain(x0) The record to explain, as a 1-D array. explain_batch(X) explains each row of a 2-D array.

What you get: an ExplanationResult with

Field Description
direction Recommended direction of change: a unit vector in the original feature space. The zero vector means that no direction reliably beats not acting.
robust_value Lower-tail CVaR, at level alpha, of the gain in the prediction when moving along direction with a random step.
alpha Risk level used.
metadata Details of the search, e.g. beats_baseline (whether acting beats not acting), baseline_cvar (the value of not acting), direction_scaled, and n_model_evals.

The components of direction are proportions in the units of each feature, so when the features have different units, those measured on larger scales get larger components; metadata["direction_scaled"] gives the same direction in standard deviations of each feature, the space in which the optimizer works.

The preprint describes the formulation and the algorithm, and the API reference documents every option.

Reproducing the paper

The folder experiments/ contains the scripts, configurations, raw results, and output logs behind every figure and table of the paper and its electronic companion. It has its own locked environment; the dicex package installed from PyPI is only the library in src/.

cd experiments
uv sync              # see experiments/README.md for options without Rust or uv
./run_all.sh example figures

This regenerates the figures and tables from the included results in a few minutes; the collected figures are written to experiments/paper_figures/ under the file names of the manuscript. experiments/README.md describes the full pipeline (runtimes, memory, random seeds) and maps each figure and table of the paper to the script that produces it.

Development

The project uses uv and a pinned Rust toolchain (rust-toolchain.toml):

git clone https://github.com/javiermartinch/dicex.git
cd dicex
uv sync --dev          # builds the Rust extension
uv run pytest          # tests
uv run pre-commit run --all-files   # ruff, pyright (strict), cargo fmt and clippy

See CONTRIBUTING.md for the contribution workflow.

Citation

If you use DiCEx in your work, please cite:

E. Carrizosa, J. Martín-Chávez, and C. Molero-Río. Directional Counterfactual Explanations. Preprint, 2026. https://www.researchgate.net/publication/415152503_Directional_Counterfactual_Explanations

@misc{carrizosa2026directional,
  title        = {Directional Counterfactual Explanations},
  author       = {Carrizosa, Emilio and Mart{\'\i}n-Ch{\'a}vez, Javier and Molero-R{\'\i}o, Cristina},
  howpublished = {Preprint},
  year         = {2026},
  url          = {https://www.researchgate.net/publication/415152503_Directional_Counterfactual_Explanations}
}

GitHub also shows this citation from CITATION.cff.

License

DiCEx is released under the MIT License.

Metadata

Release files for dicex 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 dicex 0.1.0
File Size Uploaded
dicex-0.1.0.tar.gz 140.7 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for dicex 0.1.0
File
dicex-0.1.0-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
dicex-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.12 abi3 Linux glibc 2.17+ x86-64 Details
dicex-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.12 abi3 Linux glibc 2.17+ ARM64 Details
dicex-0.1.0-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
dicex-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl CPython 3.12 abi3 macOS 10.12+ x86-64 Details

Total release size: 2.0 MB

Release files / dicex-0.1.0.tar.gz

Download URL dicex-0.1.0.tar.gz
Size 140.7 kB
Tags Source
SHA-256 checksum
How to use checksums
f9dbbfbdb2f9deb268a31cfb61df366690f8bcdc12896042c7cd8324cfb97937
BLAKE2b-256 checksum
How to use checksums
1f3c984a4b0a624648d2c5ae0aabbce7bc8f9483b66ac0f94d4cc87819925ba2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / dicex-0.1.0-cp312-abi3-win_amd64.whl

Download URL dicex-0.1.0-cp312-abi3-win_amd64.whl
Size 260.3 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
14475b52a0d33f0e3f880ac2f81f868c463e5d67b00d0446fd6612dbd0d87211
BLAKE2b-256 checksum
How to use checksums
6e653df98f9404c7b24a039f32007b1b05cb6476c03a3b38f47e273dc010e8e5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / dicex-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL dicex-0.1.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 428.1 kB
Tags CPython 3.12 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
cdd3bf8b7e5f6c1c1d66e8b6d3881edfd8816e5cfa3127a469ecfa1bd3bb8110
BLAKE2b-256 checksum
How to use checksums
35915d9ab10d3dcf5369539172d62b75f5d231d87672f00e2a5282caee05edd8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / dicex-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL dicex-0.1.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 419.7 kB
Tags CPython 3.12 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
1d804f8dbb7fda75f307b5f05d4fb4707b1d2c481a33667145ad679d9c97e496
BLAKE2b-256 checksum
How to use checksums
4dcbdfe72cae1d5a2288649e38ecd1823f01163cf4695afec2bc340671cc7a6b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / dicex-0.1.0-cp312-abi3-macosx_11_0_arm64.whl

Download URL dicex-0.1.0-cp312-abi3-macosx_11_0_arm64.whl
Size 369.0 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
883096185b1fee7751762c6b63e1013ed9cabdca6b02db029f4b861c35245eeb
BLAKE2b-256 checksum
How to use checksums
d00f20d2b278e19214ad612cd387f628696f26e7db4d8f1060b9f4fe50b4d159
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / dicex-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl

Download URL dicex-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl
Size 372.6 kB
Tags CPython 3.12 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
936f5184f5e527ddf7814256e6d567839b2cbd8e1767d31bdbb9b7f99250ce29
BLAKE2b-256 checksum
How to use checksums
6a57d35e66e8e726bb3312f5396a6ff49fe9736e47a9743ffc3b5fa5a0fd622d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

6 release files

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