Releases: wlazlod/treecf
Release list
treecf 0.3.2
The exact backend now searches coarse-to-fine by default, which certifies far more of the
measured matrix in the same budget; a budget that ran out no longer costs seconds more for
its own warning; certificates of exact solves always name the engine that ran; and a packaged
credit demo makes every example in the documentation and the README run as written. The
documentation is restructured so that each page has one job, and its claims are sized to
what the benchmarks measure.
Full record: the changelog; the measured comparison against DiCE and NICE is on the benchmarks page.
treecf 0.3.1
The exact backend gains an opt-in coarse-to-fine search (search="refine") that
certifies far larger models in the same time, a maximal region mode that proves where a
certified box ends, a certification trace you can plot, and a search profile that sizes a
solve before it runs. Auditors get a one-page portfolio report; analysts get certified
recourse menus over lever sets and lever-diverse plans. Two correctness fixes reach every
user: XGBoost and CatBoost models now route float64 inputs exactly as the native model
does, and a value policy no longer withdraws an exact certificate up front. Regions stop at
the observed data range instead of running to infinity, and region phrases never overstate
the box. Everything else is byte-identical to 0.3.0.
Full record: the changelog and the benchmarks.
treecf 0.3.0
Added
- Native categorical splits. Models trained with native categorical support now parse
exactly into set-membership IR nodes: LightGBM (categorical_feature), XGBoost
(enable_categorical), scikit-learnHistGradientBoosting(categorical_features,
including string categories viacategories=), and CatBoost (cat_features; one-hot and
single-feature-statistic splits, hashing reproduced bit-exactly). CatBoost models built with
categorical feature combinations raiseParserErrornaming themax_ctr_complexity=1
retraining recipe; the newParserErrortype covers "recognized but unparseable as given". Explainer(categories=...). Display names (and, where useful, declared cardinalities
beyond training) for categorical features; required for CatBoost with native categorical
features and for HistGradientBoosting trained on string categories, optional elsewhere.- Category blocks. Every backend searches categorical features over routing-equivalence
classes of codes, so search cost scales with how finely the ensemble partitions the feature,
not its cardinality. Categorical distance is flat: any change of code costs one weighted
unit. AllowedCategoriesconstraint. Whitelist a categorical feature's codes by display name
or raw code; order- and arithmetic-shaped constraints (Range,Monotone,Linear,
Equals,Implies,OneHot) are rejected on categorical features at construction.- Categorical exact search and regions.
backend="exact"proves optimality and certified
infeasibility over the block grid;region=Truecertifies category sets per categorical
feature (RecourseRegion.feature_categories/.category_names/.cat_sets), stored in
certificates as schema version 2. Schema version 1 certificates still verify, pinned by a
committed golden file. - Presolve. The exact backend filters each feature's candidate states by reachable score
and plausibility brackets before branching;solver_statsgainspresolve_removedand
presolve_certified, and an emptied domain certifies infeasibility with zero nodes
expanded. Results are bit-identical with presolve on; only node counts drop. - Visualization.
plot_region(the certified box, with per-bound cap markers and
categorical tiles) andplot_recourse_burden/recourse_burden_table(feasible share and
cost distribution by segment, kept side by side). - Docs. Reader-oriented navigation (workflow guides, grouped concepts, split API pages,
benchmarks and changelog pages); every fenced snippet executed in CI against a committed
docs model; a structure test pins that no published URL disappears and every plot function
ships a committed figure. SECURITY.md,CONTRIBUTING.md,scripts/bump_version.py(with a version-consistency
test), and#![forbid(unsafe_code)]in the Rust core.
Changed
-
Exact-search performance. Presolve, a feature-to-trees index, and per-tree bracket
caching in region growth. Measured before/after (same machine, same seeds, medians):Scenario (exact backend, warm start, 5 s / 2M-node budgets, 10 seeds, 4-core x86_64) 0.2.4 median 0.3.0 median 30 trees / depth 4 / 8 features — every solve proved optimal 0.292 s 0.295 s 60 trees / depth 5 / 12 features — budget-capped, best-found 5.014 s 5.012 s 300 trees / depth 6 / 50 features — budget-capped, best-found 5.208 s 5.133 s No legacy case regresses (the largest change is +1.0% on the small case, within run
noise); on the large budget-capped case the search now expands 308,822 nodes in the same
budget where 0.2.4 expanded 205,755. New certification measurements (60 s budget,
3 seeds): the 200-tree / depth-5 / 12-feature reference case does not certify within
60 s on the 4-core benchmark machine; the native-categorical suite (4 numeric levers plus
cardinality-3/8/15 categoricals, LightGBM) certifies in 0.022 s median even at 200 trees.
Full tables: the docs benchmarks page, generated from the same measured JSON.
Invariants
- Numeric-model results are byte-identical to the previous release: fingerprints, solves,
regions, and stored fixtures are unchanged, pinned by a dedicated invariance suite. - No genetic or parity fixture was regenerated; exact fixtures were regenerated only under an
equality guard asserting identical plans, distances, and proofs. - The Python and Rust engines remain byte-identical on every solve, domain, and region,
including the new categorical paths.
treecf 0.2.4
Added
- Calibrator provenance in certificates. For calibrated-space targets the certificate's
target.calibratorblock is now structured —{embedded: false, fingerprint, type, buffer_logit}— with the fingerprint duck-read from the calibrator's ownfingerprint()
(nullwhen absent; probcal calibrators provide one).check_certificateaccepts an
optionalcalibrator=keyword: when given, the report gainscalibrator_match, true only
if the fingerprints agree and re-inverting the stored calibrated bounds through the passed
calibrator reproduces the stored raw interval. - Calibrator provenance in batch records.
BatchRecord.calibrator_fingerprintrepeats the
target calibrator's fingerprint on every row, so each JSON line stays self-contained. score_calibratedread-out.Counterfactual,BatchRecord, and the certificate's
factualblock now carry the calibrator's probability at the result (and at the factual)
for calibrated targets whose calibrator exposespredict_proba;Noneotherwise.
Presentational only: the engine still optimizes and verifies against the raw interval.- Plateau-aware exactness tests. Calibrated targets on and one float above step-calibrator
plateau levels, cross-checked against brute-force enumeration in calibrated space and
against real probcal isotonic/centered-isotonic fits. - probcal test matrix. New optional
testextra (and probcal in thedevextra):
7 fitted probcal calibrators x target ops x buffer levels on sklearn and LightGBM models,
every plan re-verified through the model and calibrator; dedicated CI job with pinned
probcal + lightgbm.src/never imports probcal — the duck-typed protocol is unchanged. - Docs.
concepts/calibration.mdgains provenance, read-out, and worked-example
sections, and pins the guarantee thatexplain_batchcallsinterval_inverseexactly
once per call (once per band for ladders), backed by counting tests.
Compatibility
- Strictly additive. All new dataclass fields default to
None; 0.2.x batch JSON and
certificates load with the new fields defaulted.check_certificatewithoutcalibrator=
produces byte-identical reports to 0.2.3. Calibrators missing optional duck members
(fingerprint,predict_proba) degrade tonull/None, never an error.
treecf 0.2.3
Fixed
- sklearn
tree_-based ensembles (RandomForest, GradientBoosting, IsolationForest) routed
differently from sklearn itself at split boundaries, because sklearn casts inputs to
float32 before comparing against the float64 threshold while the IR evaluates in float64.
A counterfactual whose coordinate landed exactly on a split threshold — the natural optimum
of a smallest-change search, since<=cells are closed on the left — could flip through
many trees at once: in the reproducing case (GradientBoostingClassifier,
subsample=0.8), the exact backend stampedproof="optimal"on anx_cfwhose true
decision_functionmargin was 3.09 raw-score units away from the reportedscore_raw,
silently violating the target. Thresholds are now re-expressed at parse time as the exact
float64 boundary of the float32 cast (largest float64Twithfloat32(T) <= t,
round-half-to-even handled), so float64 IR routing reproduces sklearn bit-for-bit for
every input — search, certificates, andscore_rawincluded. Verified by a 138k-probe
property sweep, new unquantized conformance tests (exact-threshold and float64-neighbour
probes; the old harness quantized all probes to the float32 grid, which is exactly why
this never surfaced), and probcal's joint recourse scenarios. HistGradientBoosting
predicts on the float64 grid and is unchanged; XGBoost also casts features to float32
natively and should get the same treatment once a reproducing case is confirmed
(follow-up).
Internal
- Restructured a late-initialized binding in the exact search's proof/lower-bound
epilogue (behavior-identical) — clippy 1.98'sneedless_late_initbegan rejecting
the old form under-D warningson the freshly installed stable toolchain in CI.
v0.2.2 — batch proof fields and audit certificates
Added
- Audit certificates:
Explainer.certificate(x, result, target)turns any storedCounterfactualorInfeasible(the certified "no" included) into a strict-JSON-serializable audit record — a reproducibility record plus a fresh verification. It binds the claim to a model fingerprint, a constraint fingerprint, and the solve parameters, and re-verifies the returned plan (score, target membership, constraint check, plausibility, sampled region points) at issue time. It does not cryptographically prove that a search ran or that aproof="optimal"claim is true — re-running with the recorded seed/budgets on a fingerprint-matching model is how a validator checks that.Explainer.check_certificate(cert)recomputes both fingerprints against the current explainer, re-runs the verification block, and reports (model_match/constraints_match/verification_ok/mismatches) without ever raising on a mismatch. The newtreecf.auditmodule exposes the underlyingir_fingerprintandconstraints_fingerprint; a callablevalue_policyhas no canonical encoding and marks the certificate"reproducible": falsewith a reason. BatchRecord.proofandBatchRecord.solver_stats: every batch record now carries the claim and (for exact solves) the diagnostics of the single-instance result that produced it.BatchResult.to_framegains aproofcolumn;save/loadround-trip both fields, and files from earlier versions load with feasibility-based defaults.
Fixed
- The batch aggregate degraded-result warning pointed at "each result's own proof/solver_stats" while
BatchRecordexposed neither field; the fields now exist, so the message is true as written.
Notes
- No solver behavior changes; no fixtures touched; no Rust source changes (only the mirrored version in
rust/Cargo.toml/Cargo.lock).
See CHANGELOG.md for details.
treecf 0.1.1
Added
TreecfWarning, emitted when a factual violates its constraints — once per
explaincall, and as a single per-constraint aggregate inexplain_batch.
The warning spells out that the returned plan includes changes made solely
to satisfy the violated constraints.- Derived per-feature bounds for single-feature
Linearconstraints
(constraint("income >= 100")now clips candidates like the equivalent
Range); vacuous zero-coefficient linears are dropped, unsatisfiable ones
rejected at compile time. - Declared Rust MSRV (rustc 1.86) in
rust/Cargo.tomlwith an enforcing CI
job; building from the sdist needs 1.86+, wheels need no toolchain. - Wheel smoke tests in the release workflow: every runnable wheel target is
installed into a fresh venv (musllinux inside an Alpine container) and runs
oneexplainper backend before upload. CITATION.cffversion is now checked againsttreecf.__version__in the
test suite.
Fixed
- Satisfiable
Linearconstraints whose feasible set lies far from the
factual no longer come backInfeasible: single-feature linears lower into
bounds, and multi-feature linears get halfspace-projection repair. apply_linkno longer raisesOverflowErrorfor raw scores below ≈ −710;
mid-range sigmoid outputs are bit-for-bit unchanged.CITATION.cffandrust/Cargo.tomlversion drift (both said 0.0.1 while
the released package was 0.1.0).
Changed
- Repair for non-canonical linear constraints now runs a 3-round cyclic
halfspace projection; seeded results from 0.1.0 that involve such
constraints are not reproducible in 0.1.1. The canonical order-pair
repair (a - b <= 0) is unchanged, and the existing parity fixtures
regenerated byte-identical; a new11-linear-projectionfixture pins the
projection behavior.
treecf 0.1.0
Added
- Calibrated targets:
Target.calibrated(calibrator, ...)expresses the
target on the post-hoc calibrated probability scale and lazily inverts it
through the calibrator's duck-typed generalized inverse
(interval_inverse(lo, hi, *, space="logit", buffer_logit=...)+
is_monotone_) — no calibration-library dependency.Target.bandsaccepts
space="calibrated"withcalibrator=/buffer_logit=for masterscales
defined on calibrated PD.Target.probabilitynow documents that it targets
the uncalibrated model probability.
Fixed
-
Target.band_intervalsfield propagation: per-band targets were rebuilt
from(space, lo, hi)only, silently dropping any other field — now all
fields propagate (surfaced by the calibrated-bands work). -
Competitor benchmark:
scripts/bench_vs_competitors.py(PEP 723,
self-contained viauv run) compares treecf with DiCE and NICE on two
model scales; results published in Backends and proofs — 8–3400× faster
than DiCE with far cheaper plans, cheapest plans overall, 157 rows/s batch
production on the medium model; NICE's per-instance speed and treecf's own
misses reported as-is. -
Post-solve pruning: every returned plan now drops changes that
verification proves unnecessary (cheapest first, each revert re-verified in
float space). The search's revert-to-factual mutation is stochastic, so a
stalled run could ship a residual micro-change that crossed no decision
threshold — pure distance cost with zero score effect. -
CITATION.cff.
Changed
- Compiled extensions are no longer tracked in git (history rewritten to drop
the committed.so; wheels come from CI, local builds via maturin). - Publish steps skip files already on the index, making tag-triggered
re-releases idempotent; retroactivev0.0.1tag and GitHub release created. - PyPI keywords no longer mention the removed CP-SAT backend; README/docs
state the published version (0.0.1) consistently.
treecf 0.0.1
First published release — on PyPI.
Highlights (full details in CHANGELOG.md):
- Constrained genetic counterfactual search on a bundled Rust core (44–58× the numpy reference); every result float-verified against the model IR.
- Targets as intervals on the raw output; declarative constraints; NaN as a first-class value; optional isolation-forest plausibility; constraint mining.
- Batch production with parallel solves, portable JSON storage, and batch-level visualizations; coalitions mode for grouped recourse.
- Wheels for linux (x86_64, aarch64, musl), macOS (arm64, x86_64), and Windows; docs at https://wlazlod.github.io/treecf/.