Getting started¶
Install¶
pip install treecf # bundled Rust search engine
pip install "treecf[xgboost,viz]" # parser extras, matplotlib plots
numpy is the only Python dependency; the genetic engine is a compiled Rust core shipped inside the wheel. Model parsers accept JSON dumps directly, so explanations can be generated on machines where the training framework (or any solver) is not installed. 32-bit Raspberry Pi wheels arrive via piwheels' other builders; the recurring Bookworm build failure there is an upstream toolchain issue, not a treecf packaging bug. All other platforms ship from CI as usual.
First counterfactual¶
import numpy as np
import xgboost as xgb
from treecf import Counterfactual, Explainer, Target, Freeze, Monotone, constraint
# a binary classifier trained on your data
clf = xgb.XGBClassifier(n_estimators=100, max_depth=4).fit(X_train, y_train)
exp = Explainer(
model=clf, # or "model.json", or a dump dict
background=X_train, # fits robust distance normalizers (MAD chain)
constraints=[
Freeze("age_of_bureau_file"), # immutable
Monotone("age", "increase"), # can only grow
constraint("max_dpd_30d <= max_dpd_12m"), # inter-feature consistency
],
)
res = exp.explain(
x_row,
target=Target.probability(range=(0.0, 0.04)), # get under the 4% PD cutoff
seed=0,
)
if isinstance(res, Counterfactual):
print(res.changes) # {"feature": (from, to), ...}
else:
print(res.reason) # Infeasible: why no plan was found
The search is heuristic (proof="heuristic"), feasibility-first, and
seed-deterministic; on toy suites it brackets a brute-force optimum. It runs on
the bundled Rust engine in milliseconds even on 300-tree models;
backend="python" runs the reference numpy implementation of the same
algorithm. How it works walks the whole pipeline.
Calibrated models¶
If your pipeline post-hoc calibrates the model's probabilities, express the
target on the calibrated scale — Target.probability would silently target
the uncalibrated output:
res = exp.explain(
x_row,
target=Target.calibrated(cal, range=(0.0, 0.04)), # calibrated PD ≤ 4%
seed=0,
)
cal is any monotone calibrator exposing interval_inverse and
is_monotone_; see the FAQ for the exact protocol and the buffer_logit robustness margin.
Read the result¶
| Field | Meaning |
|---|---|
x_cf |
counterfactual instance (NaN where a missing state was chosen) |
changes |
feature → (factual, counterfactual) for every changed feature |
distance, n_changed |
weighted L1 distance and L0 count |
score_raw, score_prob |
raw model output and its sigmoid when applicable |
proof |
always "heuristic" — the search never claims optimality |
snapped |
per-feature outcome of value_policy snapping |
Every result is re-verified in float space against the IR before it is returned: the target and each constraint are checked on the actual returned values.
Visualize it¶
from treecf.viz import plot_changes, plot_waterfall, plot_effort
plot_changes(res) # dumbbells: from -> to per feature
plot_waterfall(exp, res, target=t) # SHAP-style: exact score deltas, cutoff line
plot_effort(exp, res) # where the applicant's effort goes (J split)
Alternatives for one instance¶
One plan is rarely the whole story. Ask for several distinct plans for the same row and compare them side by side:
from treecf.viz import plot_alternatives, plot_tradeoff
batch = exp.explain_batch(x_row.reshape(1, -1), target=t, n_per_example=3, seed=0)
plans = batch.for_id(0) # up to 3 distinct plans for this row
plot_alternatives(plans, explainer=exp) # every plan's changes, standardized to Δ/σ
plot_tradeoff(plans, target=t) # cost vs achieved score: which plan buys what
diversity="lever-blocking" instead re-solves with each plan's biggest lever
frozen — and reports levers that turn out to be essential.
For advice grouped by what a person controls together, ask for one plan per named feature group — see Coalitions:
result = exp.explain_coalitions(
x_row, target=t,
coalitions={"debt": ["max_dpd_30d", "max_dpd_12m"], "income": ["income_monthly"]},
include_full=True, # adds the unrestricted "(all levers)" baseline
)
plot_alternatives(result, explainer=exp) # coalition names label the plans
When you need proof¶
explain never claims optimality by default — for that, ask for the exact backend and a
certified region around the answer:
res = exp.explain(x_row, target=t, backend="exact", region=True, seed=0)
res.proof # "optimal" | "optimal_within_gap" | "heuristic"
res.region.describe() # per-feature interval every point of which is also a valid plan
Certification covers exactly what that proof does and does not guarantee.
Scale to a dataset¶
batch = exp.explain_batch(
X_declined, # e.g. today's declined applications
target=Target.probability(range=(0.0, 0.30)),
n_per_example=2, # counterfactuals per example
diversity="seeds", # or "lever-blocking" (also finds essential levers)
ids=app_ids,
seed=0,
)
batch.save("counterfactuals_today.json") # compute once, store...
stored = BatchResult.load("counterfactuals_today.json")
stored.for_id("APP-00042") # ...look up any time
stored.to_frame() # or analyze as a pandas DataFrame
Solves run in parallel inside the Rust core. treecf.viz_batch plots the whole
batch — lever usage, per-plan effort, cost/sparsity/feasibility — as shown in the
credit-risk walkthrough.
Where next¶
- How it works — the pipeline from objective to verified answer.
- Concepts — one page per stage: models, targets, constraints, missing values, plausibility, backends, certification.
- Tutorials — runnable notebooks.
- API reference.