DiCEx: Directional Counterfactual Explanations
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)
| File | Size | Uploaded | |
|---|---|---|---|
| dicex-0.1.0.tar.gz | 140.7 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| 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 logRelease 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 logRelease 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 logRelease 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 logRelease 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 logRelease 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