Constrained, threshold-aware counterfactual explanations for tree ensembles.
treecf answers the question: "what is the minimal, feasible change to this instance such
that the model's output lands in a target interval?" — for XGBoost, LightGBM, CatBoost and
scikit-learn tree ensembles.
On PyPI. See the documentation for concepts and tutorials.
- Tree-native and fast. Models are parsed into a shared tree IR and the constrained search runs on a bundled Rust core, typically in milliseconds; every result is float-verified against the parsed model before it is returned, and the parsers are conformance-tested against the native library.
- Optional proofs, inside a measured envelope.
backend="exact"returnsproof="optimal"when no cheaper plan exists under the declared objective (weighted distance, plus a per-feature term only if you setsparsity_weight), and a completed search that finds nothing returnsInfeasible(proof="certified"). Proofs scale with the number of levers the search may move, not with the model's width: on the measured matrix the refine search certifies up to 200 trees with 12 free features inside 60 s and no 20-feature model beyond the smallest one (50 trees at depth 3) — while on the 300-tree, 50-feature model a coalition of up to three levers certifies in under half a second withsearch="refine", so wide models get proofs once the levers are restricted withFreeze, coalitions, or arecourse_menu. A search that runs out of budget returns its best plan labelledheuristicand warns; it never claims more. - Recourse regions. Any verified counterfactual widens into a certified box — "reduce utilization below 0.40", not "to 0.3972" — with every point in the box provably in-target and constraint-feasible; works with every backend.
- Real constraints. Immutability, directionality, ranges, one-hot consistency, linear
inter-feature rules such as
max_dpd_30d <= max_dpd_12m, and NaN as a legitimate value with its own transition cost — declared once, enforced by every backend. - Menus, diverse plans, certificates.
recourse_menusolves every lever set up to a size and says which combinations provably cannot work;explain_diversereturns the cheapest plans with distinct lever sets;certificateturns any result into a self-contained JSON record a validator re-checks later.
On a 120-tree model and 100 declined rows, treecf's plans cost a seventh of DiCE's at a fifth of the time; NICE is four times faster per instance, and its plans cost 2.7 times more and cannot take constraints. The measured tables and the honest reading are on the benchmarks page.
Not for you if: the model is not a tree ensemble; you want sets of plans diverse by distance rather than by the levers they use (DiCE does that); you need a proof over dozens of free levers at once without restricting them (see the proof envelope); or you need a frozen API — treecf is in beta, see API stability.
pip install "treecf[xgboost,viz]" # wheels for Linux, macOS, Windows; no Rust toolchain needednumpy is the only Python dependency; the extras add a parser for your training library and the plots. JSON model dumps parse without the training library, so explanations can be generated on a scoring host that has neither it nor a solver.
credit_demo() returns a packaged credit model, background rows, and one declined
applicant. It is new in this version; on an older installed package, substitute your own
model, as the quickstart notebook does.
from treecf import Explainer, Freeze, Monotone, Target
from treecf.datasets import credit_demo
model, X, x = credit_demo()
target = Target.probability(range=(0.0, 0.05))
exp = Explainer(
model, background=X,
constraints=[Freeze("occupation"), Monotone("tenure_months", "increase")],
)
res = exp.explain(x, target=target, seed=0)
res.changes # {'income': (4678.0, 6932.4)}
res.proof # 'heuristic'
proved = exp.explain(x, target=target, backend="exact", region=True, seed=0)
proved.proof # 'optimal'
proved.region.describe() # {'income': 'in [6.58e+03, 7.81e+03] (data-limited)',
# 'utilization': 'in [0.428, 0.541)', ...}
menu = exp.recourse_menu(x, target=target, max_levers=2)
menu.describe()["dpd_12m"] # 'no acceptance is reachable by changing only dpd_12m'- How it works — the pipeline from objective to verified answer.
- Certification — what a proof covers and where it stops.
- Credit-risk walkthrough — a batch workflow end to end.
- probcal integration — recourse against calibrated cutoffs.
@software{wlazlo_treecf,
author = {Wlazło, Daniel},
title = {treecf: constrained, threshold-aware counterfactual explanations for tree ensembles},
doi = {10.5281/zenodo.22069503},
url = {https://github.com/wlazlod/treecf},
license = {MIT}
}MIT. See CONTRIBUTING.md for dev setup, the test layers, and the project's hard invariants; report security issues privately per SECURITY.md.
