treecf 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- treecf-0.3.2/PKG-INFO +188 -0
- treecf-0.3.2/README.md +121 -0
- {treecf-0.3.0 → treecf-0.3.2}/pyproject.toml +1 -1
- {treecf-0.3.0 → treecf-0.3.2}/rust/Cargo.lock +1 -1
- {treecf-0.3.0 → treecf-0.3.2}/rust/Cargo.toml +1 -1
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/domains.rs +1 -1
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/mod.rs +6 -1
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/propagation.rs +32 -1
- treecf-0.3.2/rust/src/exact/refine.rs +825 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/search.rs +415 -132
- treecf-0.3.2/rust/src/exact/trace.rs +108 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/lib.rs +1 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/py.rs +103 -26
- treecf-0.3.2/rust/src/region_slab.rs +344 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/regions.rs +559 -68
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/__init__.py +4 -1
- treecf-0.3.2/src/treecf/_menu.py +629 -0
- treecf-0.3.2/src/treecf/_portfolio.py +619 -0
- treecf-0.3.2/src/treecf/_region_slab.py +278 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/api.py +415 -39
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/audit.py +34 -11
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_bounds.py +36 -2
- treecf-0.3.2/src/treecf/backends/_exact_profile.py +106 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_propagation.py +26 -1
- treecf-0.3.2/src/treecf/backends/_exact_refine.py +653 -0
- treecf-0.3.2/src/treecf/backends/_exact_trace.py +50 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/exact.py +211 -46
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/exact_rust.py +18 -1
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/regions_rust.py +41 -8
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/batch.py +124 -20
- treecf-0.3.2/src/treecf/datasets/__init__.py +59 -0
- treecf-0.3.2/src/treecf/datasets/credit_model.json +1 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/model.py +92 -1
- treecf-0.3.2/src/treecf/ir/parsers/_float32.py +49 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/catboost.py +7 -2
- treecf-0.3.2/src/treecf/ir/parsers/json_dump.py +95 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/lightgbm.py +3 -1
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/sklearn.py +4 -30
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/xgboost.py +4 -1
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/regions.py +442 -80
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/viz.py +359 -6
- treecf-0.3.0/PKG-INFO +0 -143
- treecf-0.3.0/README.md +0 -76
- treecf-0.3.0/src/treecf/ir/parsers/json_dump.py +0 -39
- {treecf-0.3.0 → treecf-0.3.2}/LICENSE +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/cells.rs +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/constraints.rs +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/orderpairs.rs +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/test_support.rs +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/ga.rs +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/interrupt.rs +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/rust/src/ir.rs +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/_errors.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/_json.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/aim/__init__.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/aim/cells.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/__init__.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_domains.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_orderpairs.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/genetic.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/genetic_rust.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/__init__.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/compile.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/flatten.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/objects.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/parser.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/__init__.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/conformance.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/evaluate.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/flatten.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/__init__.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/_catboost_cat.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/mining.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/objective.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/plausibility.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/py.typed +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/targets.py +0 -0
- {treecf-0.3.0 → treecf-0.3.2}/src/treecf/viz_batch.py +0 -0
treecf-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: treecf
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Science/Research
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Dist: numpy>=1.24
|
|
15
|
+
Requires-Dist: xgboost>=2.0 ; extra == 'all'
|
|
16
|
+
Requires-Dist: lightgbm>=4.3 ; extra == 'all'
|
|
17
|
+
Requires-Dist: catboost>=1.2 ; extra == 'all'
|
|
18
|
+
Requires-Dist: scikit-learn>=1.4 ; extra == 'all'
|
|
19
|
+
Requires-Dist: matplotlib>=3.8 ; extra == 'all'
|
|
20
|
+
Requires-Dist: catboost>=1.2 ; extra == 'catboost'
|
|
21
|
+
Requires-Dist: maturin>=1.7 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-cov>=5.0 ; extra == 'dev'
|
|
24
|
+
Requires-Dist: hypothesis>=6.100 ; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.5 ; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.10 ; extra == 'dev'
|
|
27
|
+
Requires-Dist: xgboost>=2.0 ; extra == 'dev'
|
|
28
|
+
Requires-Dist: lightgbm>=4.3 ; extra == 'dev'
|
|
29
|
+
Requires-Dist: catboost>=1.2 ; extra == 'dev'
|
|
30
|
+
Requires-Dist: scikit-learn>=1.4 ; extra == 'dev'
|
|
31
|
+
Requires-Dist: matplotlib>=3.8 ; extra == 'dev'
|
|
32
|
+
Requires-Dist: probcal>=0.2 ; extra == 'dev'
|
|
33
|
+
Requires-Dist: mkdocs>=1.6 ; extra == 'docs'
|
|
34
|
+
Requires-Dist: mkdocs-material>=9.5 ; extra == 'docs'
|
|
35
|
+
Requires-Dist: mkdocstrings[python]>=0.27 ; extra == 'docs'
|
|
36
|
+
Requires-Dist: pymdown-extensions>=10.9 ; extra == 'docs'
|
|
37
|
+
Requires-Dist: mkdocs-jupyter>=0.24 ; extra == 'docs'
|
|
38
|
+
Requires-Dist: ipykernel>=6.29 ; extra == 'docs'
|
|
39
|
+
Requires-Dist: lightgbm>=4.3 ; extra == 'lightgbm'
|
|
40
|
+
Requires-Dist: scikit-learn>=1.4 ; extra == 'sklearn'
|
|
41
|
+
Requires-Dist: pytest>=8.0 ; extra == 'test'
|
|
42
|
+
Requires-Dist: hypothesis>=6.100 ; extra == 'test'
|
|
43
|
+
Requires-Dist: probcal>=0.2 ; extra == 'test'
|
|
44
|
+
Requires-Dist: matplotlib>=3.8 ; extra == 'viz'
|
|
45
|
+
Requires-Dist: xgboost>=2.0 ; extra == 'xgboost'
|
|
46
|
+
Provides-Extra: all
|
|
47
|
+
Provides-Extra: catboost
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Provides-Extra: docs
|
|
50
|
+
Provides-Extra: lightgbm
|
|
51
|
+
Provides-Extra: sklearn
|
|
52
|
+
Provides-Extra: test
|
|
53
|
+
Provides-Extra: viz
|
|
54
|
+
Provides-Extra: xgboost
|
|
55
|
+
License-File: LICENSE
|
|
56
|
+
Summary: Constrained, threshold-aware counterfactual explanations for tree ensembles (XGBoost, LightGBM, CatBoost, sklearn) — fast Rust genetic search, exact optimality proofs, certified infeasibility, and recourse regions.
|
|
57
|
+
Keywords: counterfactual,xai,interpretability,recourse,gbdt,xgboost,lightgbm,catboost,credit-risk
|
|
58
|
+
Author-email: Daniel Wlazlo <wlazlo.daniel@gmail.com>
|
|
59
|
+
License: MIT
|
|
60
|
+
Requires-Python: >=3.11
|
|
61
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
62
|
+
Project-URL: Changelog, https://github.com/wlazlod/treecf/blob/main/CHANGELOG.md
|
|
63
|
+
Project-URL: Documentation, https://wlazlod.github.io/treecf/
|
|
64
|
+
Project-URL: Homepage, https://github.com/wlazlod/treecf
|
|
65
|
+
Project-URL: Issues, https://github.com/wlazlod/treecf/issues
|
|
66
|
+
|
|
67
|
+
# treecf
|
|
68
|
+
|
|
69
|
+
[](https://doi.org/10.5281/zenodo.22069503)
|
|
70
|
+
[](https://pypi.org/project/treecf/)
|
|
71
|
+
[](https://pypi.org/project/treecf/)
|
|
72
|
+
[](https://github.com/wlazlod/treecf/actions/workflows/ci.yml)
|
|
73
|
+
[](LICENSE)
|
|
74
|
+
|
|
75
|
+
**Constrained, threshold-aware counterfactual explanations for tree ensembles.**
|
|
76
|
+
|
|
77
|
+
`treecf` answers the question: *"what is the minimal, feasible change to this instance such
|
|
78
|
+
that the model's output lands in a target interval?"* — for XGBoost, LightGBM, CatBoost and
|
|
79
|
+
scikit-learn tree ensembles.
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
> On [PyPI](https://pypi.org/project/treecf/). See the [documentation](https://wlazlod.github.io/treecf/) for concepts and tutorials.
|
|
84
|
+
|
|
85
|
+
## Why another counterfactual package?
|
|
86
|
+
|
|
87
|
+
- **Tree-native and fast.** Models are parsed into a shared tree IR and the constrained
|
|
88
|
+
search runs on a bundled Rust core, typically in milliseconds; every result is
|
|
89
|
+
float-verified against the parsed model before it is returned, and the parsers are
|
|
90
|
+
conformance-tested against the native library.
|
|
91
|
+
- **Optional proofs, inside a measured envelope.** `backend="exact"` returns
|
|
92
|
+
`proof="optimal"` when no cheaper plan exists under the declared objective (weighted
|
|
93
|
+
distance, plus a per-feature term only if you set `sparsity_weight`), and a completed
|
|
94
|
+
search that finds nothing returns `Infeasible(proof="certified")`. Proofs scale with the
|
|
95
|
+
number of levers the search may move, not with the model's width: on the measured matrix
|
|
96
|
+
the refine search certifies up to 200 trees with 12 free features inside 60 s and no
|
|
97
|
+
20-feature model beyond the smallest one (50 trees at depth 3) — while on the 300-tree,
|
|
98
|
+
50-feature model a coalition of up to
|
|
99
|
+
three levers certifies in under half a second with `search="refine"`, so wide models get
|
|
100
|
+
proofs once the levers are restricted with `Freeze`, coalitions, or a `recourse_menu`. A
|
|
101
|
+
search that runs out of budget returns its best plan labelled `heuristic` and warns; it
|
|
102
|
+
never claims more.
|
|
103
|
+
- **Recourse regions.** Any verified counterfactual widens into a certified box — "reduce
|
|
104
|
+
utilization below 0.40", not "to 0.3972" — with every point in the box provably in-target
|
|
105
|
+
and constraint-feasible; works with every backend.
|
|
106
|
+
- **Real constraints.** Immutability, directionality, ranges, one-hot consistency, linear
|
|
107
|
+
inter-feature rules such as `max_dpd_30d <= max_dpd_12m`, and NaN as a legitimate value
|
|
108
|
+
with its own transition cost — declared once, enforced by every backend.
|
|
109
|
+
- **Menus, diverse plans, certificates.** `recourse_menu` solves every lever set up to a
|
|
110
|
+
size and says which combinations provably cannot work; `explain_diverse` returns the
|
|
111
|
+
cheapest plans with distinct lever sets; `certificate` turns any result into a
|
|
112
|
+
self-contained JSON record a validator re-checks later.
|
|
113
|
+
|
|
114
|
+
On a 120-tree model and 100 declined rows, treecf's plans cost a seventh of DiCE's at a fifth
|
|
115
|
+
of the time; NICE is four times faster per instance, and its plans cost 2.7 times more and
|
|
116
|
+
cannot take constraints. The measured tables and the honest reading are on the
|
|
117
|
+
[benchmarks page](https://wlazlod.github.io/treecf/concepts/backends/#against-other-cf-libraries).
|
|
118
|
+
|
|
119
|
+
Not for you if: the model is not a tree ensemble; you want sets of plans diverse by distance
|
|
120
|
+
rather than by the levers they use (DiCE does that); you need a proof over dozens of free
|
|
121
|
+
levers at once without restricting them (see the [proof envelope](https://wlazlod.github.io/treecf/concepts/certification/#the-proof-envelope-measured));
|
|
122
|
+
or you need a frozen API — treecf is in beta, see
|
|
123
|
+
[API stability](https://wlazlod.github.io/treecf/api-stability/).
|
|
124
|
+
|
|
125
|
+
## Installation
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install "treecf[xgboost,viz]" # wheels for Linux, macOS, Windows; no Rust toolchain needed
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
numpy is the only Python dependency; the extras add a parser for your training library and
|
|
132
|
+
the plots. JSON model dumps parse without the training library, so explanations can be
|
|
133
|
+
generated on a scoring host that has neither it nor a solver.
|
|
134
|
+
|
|
135
|
+
## Quick look
|
|
136
|
+
|
|
137
|
+
`credit_demo()` returns a packaged credit model, background rows, and one declined
|
|
138
|
+
applicant. It is new in this version; on an older installed package, substitute your own
|
|
139
|
+
model, as the quickstart notebook does.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from treecf import Explainer, Freeze, Monotone, Target
|
|
143
|
+
from treecf.datasets import credit_demo
|
|
144
|
+
|
|
145
|
+
model, X, x = credit_demo()
|
|
146
|
+
target = Target.probability(range=(0.0, 0.05))
|
|
147
|
+
exp = Explainer(
|
|
148
|
+
model, background=X,
|
|
149
|
+
constraints=[Freeze("occupation"), Monotone("tenure_months", "increase")],
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
res = exp.explain(x, target=target, seed=0)
|
|
153
|
+
res.changes # {'income': (4678.0, 6932.4)}
|
|
154
|
+
res.proof # 'heuristic'
|
|
155
|
+
|
|
156
|
+
proved = exp.explain(x, target=target, backend="exact", region=True, seed=0)
|
|
157
|
+
proved.proof # 'optimal'
|
|
158
|
+
proved.region.describe() # {'income': 'in [6.58e+03, 7.81e+03] (data-limited)',
|
|
159
|
+
# 'utilization': 'in [0.428, 0.541)', ...}
|
|
160
|
+
|
|
161
|
+
menu = exp.recourse_menu(x, target=target, max_levers=2)
|
|
162
|
+
menu.describe()["dpd_12m"] # 'no acceptance is reachable by changing only dpd_12m'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Learn more
|
|
166
|
+
|
|
167
|
+
- [How it works](https://wlazlod.github.io/treecf/how-it-works/) — the pipeline from objective to verified answer.
|
|
168
|
+
- [Certification](https://wlazlod.github.io/treecf/concepts/certification/) — what a proof covers and where it stops.
|
|
169
|
+
- [Credit-risk walkthrough](https://wlazlod.github.io/treecf/notebooks/02-credit-risk-tutorial/) — a batch workflow end to end.
|
|
170
|
+
- [probcal integration](https://wlazlod.github.io/treecf/guide/probcal/) — recourse against calibrated cutoffs.
|
|
171
|
+
|
|
172
|
+
## Cite
|
|
173
|
+
|
|
174
|
+
```bibtex
|
|
175
|
+
@software{wlazlo_treecf,
|
|
176
|
+
author = {Wlazło, Daniel},
|
|
177
|
+
title = {treecf: constrained, threshold-aware counterfactual explanations for tree ensembles},
|
|
178
|
+
doi = {10.5281/zenodo.22069503},
|
|
179
|
+
url = {https://github.com/wlazlod/treecf},
|
|
180
|
+
license = {MIT}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Contributing and license
|
|
185
|
+
|
|
186
|
+
MIT. See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, the test layers, and the
|
|
187
|
+
project's hard invariants; report security issues privately per [SECURITY.md](SECURITY.md).
|
|
188
|
+
|
treecf-0.3.2/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# treecf
|
|
2
|
+
|
|
3
|
+
[](https://doi.org/10.5281/zenodo.22069503)
|
|
4
|
+
[](https://pypi.org/project/treecf/)
|
|
5
|
+
[](https://pypi.org/project/treecf/)
|
|
6
|
+
[](https://github.com/wlazlod/treecf/actions/workflows/ci.yml)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
**Constrained, threshold-aware counterfactual explanations for tree ensembles.**
|
|
10
|
+
|
|
11
|
+
`treecf` answers the question: *"what is the minimal, feasible change to this instance such
|
|
12
|
+
that the model's output lands in a target interval?"* — for XGBoost, LightGBM, CatBoost and
|
|
13
|
+
scikit-learn tree ensembles.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
> On [PyPI](https://pypi.org/project/treecf/). See the [documentation](https://wlazlod.github.io/treecf/) for concepts and tutorials.
|
|
18
|
+
|
|
19
|
+
## Why another counterfactual package?
|
|
20
|
+
|
|
21
|
+
- **Tree-native and fast.** Models are parsed into a shared tree IR and the constrained
|
|
22
|
+
search runs on a bundled Rust core, typically in milliseconds; every result is
|
|
23
|
+
float-verified against the parsed model before it is returned, and the parsers are
|
|
24
|
+
conformance-tested against the native library.
|
|
25
|
+
- **Optional proofs, inside a measured envelope.** `backend="exact"` returns
|
|
26
|
+
`proof="optimal"` when no cheaper plan exists under the declared objective (weighted
|
|
27
|
+
distance, plus a per-feature term only if you set `sparsity_weight`), and a completed
|
|
28
|
+
search that finds nothing returns `Infeasible(proof="certified")`. Proofs scale with the
|
|
29
|
+
number of levers the search may move, not with the model's width: on the measured matrix
|
|
30
|
+
the refine search certifies up to 200 trees with 12 free features inside 60 s and no
|
|
31
|
+
20-feature model beyond the smallest one (50 trees at depth 3) — while on the 300-tree,
|
|
32
|
+
50-feature model a coalition of up to
|
|
33
|
+
three levers certifies in under half a second with `search="refine"`, so wide models get
|
|
34
|
+
proofs once the levers are restricted with `Freeze`, coalitions, or a `recourse_menu`. A
|
|
35
|
+
search that runs out of budget returns its best plan labelled `heuristic` and warns; it
|
|
36
|
+
never claims more.
|
|
37
|
+
- **Recourse regions.** Any verified counterfactual widens into a certified box — "reduce
|
|
38
|
+
utilization below 0.40", not "to 0.3972" — with every point in the box provably in-target
|
|
39
|
+
and constraint-feasible; works with every backend.
|
|
40
|
+
- **Real constraints.** Immutability, directionality, ranges, one-hot consistency, linear
|
|
41
|
+
inter-feature rules such as `max_dpd_30d <= max_dpd_12m`, and NaN as a legitimate value
|
|
42
|
+
with its own transition cost — declared once, enforced by every backend.
|
|
43
|
+
- **Menus, diverse plans, certificates.** `recourse_menu` solves every lever set up to a
|
|
44
|
+
size and says which combinations provably cannot work; `explain_diverse` returns the
|
|
45
|
+
cheapest plans with distinct lever sets; `certificate` turns any result into a
|
|
46
|
+
self-contained JSON record a validator re-checks later.
|
|
47
|
+
|
|
48
|
+
On a 120-tree model and 100 declined rows, treecf's plans cost a seventh of DiCE's at a fifth
|
|
49
|
+
of the time; NICE is four times faster per instance, and its plans cost 2.7 times more and
|
|
50
|
+
cannot take constraints. The measured tables and the honest reading are on the
|
|
51
|
+
[benchmarks page](https://wlazlod.github.io/treecf/concepts/backends/#against-other-cf-libraries).
|
|
52
|
+
|
|
53
|
+
Not for you if: the model is not a tree ensemble; you want sets of plans diverse by distance
|
|
54
|
+
rather than by the levers they use (DiCE does that); you need a proof over dozens of free
|
|
55
|
+
levers at once without restricting them (see the [proof envelope](https://wlazlod.github.io/treecf/concepts/certification/#the-proof-envelope-measured));
|
|
56
|
+
or you need a frozen API — treecf is in beta, see
|
|
57
|
+
[API stability](https://wlazlod.github.io/treecf/api-stability/).
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install "treecf[xgboost,viz]" # wheels for Linux, macOS, Windows; no Rust toolchain needed
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
numpy is the only Python dependency; the extras add a parser for your training library and
|
|
66
|
+
the plots. JSON model dumps parse without the training library, so explanations can be
|
|
67
|
+
generated on a scoring host that has neither it nor a solver.
|
|
68
|
+
|
|
69
|
+
## Quick look
|
|
70
|
+
|
|
71
|
+
`credit_demo()` returns a packaged credit model, background rows, and one declined
|
|
72
|
+
applicant. It is new in this version; on an older installed package, substitute your own
|
|
73
|
+
model, as the quickstart notebook does.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from treecf import Explainer, Freeze, Monotone, Target
|
|
77
|
+
from treecf.datasets import credit_demo
|
|
78
|
+
|
|
79
|
+
model, X, x = credit_demo()
|
|
80
|
+
target = Target.probability(range=(0.0, 0.05))
|
|
81
|
+
exp = Explainer(
|
|
82
|
+
model, background=X,
|
|
83
|
+
constraints=[Freeze("occupation"), Monotone("tenure_months", "increase")],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
res = exp.explain(x, target=target, seed=0)
|
|
87
|
+
res.changes # {'income': (4678.0, 6932.4)}
|
|
88
|
+
res.proof # 'heuristic'
|
|
89
|
+
|
|
90
|
+
proved = exp.explain(x, target=target, backend="exact", region=True, seed=0)
|
|
91
|
+
proved.proof # 'optimal'
|
|
92
|
+
proved.region.describe() # {'income': 'in [6.58e+03, 7.81e+03] (data-limited)',
|
|
93
|
+
# 'utilization': 'in [0.428, 0.541)', ...}
|
|
94
|
+
|
|
95
|
+
menu = exp.recourse_menu(x, target=target, max_levers=2)
|
|
96
|
+
menu.describe()["dpd_12m"] # 'no acceptance is reachable by changing only dpd_12m'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Learn more
|
|
100
|
+
|
|
101
|
+
- [How it works](https://wlazlod.github.io/treecf/how-it-works/) — the pipeline from objective to verified answer.
|
|
102
|
+
- [Certification](https://wlazlod.github.io/treecf/concepts/certification/) — what a proof covers and where it stops.
|
|
103
|
+
- [Credit-risk walkthrough](https://wlazlod.github.io/treecf/notebooks/02-credit-risk-tutorial/) — a batch workflow end to end.
|
|
104
|
+
- [probcal integration](https://wlazlod.github.io/treecf/guide/probcal/) — recourse against calibrated cutoffs.
|
|
105
|
+
|
|
106
|
+
## Cite
|
|
107
|
+
|
|
108
|
+
```bibtex
|
|
109
|
+
@software{wlazlo_treecf,
|
|
110
|
+
author = {Wlazło, Daniel},
|
|
111
|
+
title = {treecf: constrained, threshold-aware counterfactual explanations for tree ensembles},
|
|
112
|
+
doi = {10.5281/zenodo.22069503},
|
|
113
|
+
url = {https://github.com/wlazlod/treecf},
|
|
114
|
+
license = {MIT}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Contributing and license
|
|
119
|
+
|
|
120
|
+
MIT. See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, the test layers, and the
|
|
121
|
+
project's hard invariants; report security issues privately per [SECURITY.md](SECURITY.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "treecf"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Constrained, threshold-aware counterfactual explanations for tree ensembles (XGBoost, LightGBM, CatBoost, sklearn) — fast Rust genetic search, exact optimality proofs, certified infeasibility, and recourse regions."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -12,7 +12,7 @@ use crate::ir::Ensemble;
|
|
|
12
12
|
/// Python's `<`-based ordering: `-0.0` and `0.0` compare equal, so a stable sort
|
|
13
13
|
/// leaves them in insertion order. Costs and sort values are never NaN here.
|
|
14
14
|
#[inline]
|
|
15
|
-
fn py_cmp(a: f64, b: f64) -> std::cmp::Ordering {
|
|
15
|
+
pub(crate) fn py_cmp(a: f64, b: f64) -> std::cmp::Ordering {
|
|
16
16
|
use std::cmp::Ordering;
|
|
17
17
|
if a < b {
|
|
18
18
|
Ordering::Less
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
//! | `treecf.backends._exact_domains` (+ the `treecf.api._snap` it calls) | `domains` |
|
|
12
12
|
//! | `treecf.backends._exact_propagation` | `propagation` |
|
|
13
13
|
//! | `treecf.backends._exact_orderpairs` | `orderpairs` |
|
|
14
|
+
//! | `treecf.backends._exact_refine` | `refine` |
|
|
15
|
+
//! | `treecf.backends._exact_trace` | `trace` |
|
|
14
16
|
//!
|
|
15
17
|
//! Those five Python files carry the bit-parity contract in their own headers;
|
|
16
18
|
//! every module here follows its counterpart line for line, so the operation
|
|
@@ -72,13 +74,16 @@
|
|
|
72
74
|
pub(crate) mod domains;
|
|
73
75
|
pub(crate) mod orderpairs;
|
|
74
76
|
pub(crate) mod propagation;
|
|
77
|
+
pub(crate) mod refine;
|
|
75
78
|
pub(crate) mod search;
|
|
76
79
|
#[cfg(test)]
|
|
77
80
|
pub(crate) mod test_support;
|
|
81
|
+
pub(crate) mod trace;
|
|
78
82
|
|
|
79
83
|
pub use crate::interrupt::SearchOutcome;
|
|
80
84
|
pub use domains::constraint_cells;
|
|
81
|
-
pub use search::{solve_exact, ExactParams, ExactResult, ExactStats};
|
|
85
|
+
pub use search::{solve_exact, ExactParams, ExactResult, ExactStats, SearchMode};
|
|
86
|
+
pub use trace::TraceSample;
|
|
82
87
|
|
|
83
88
|
/// Per-feature snapping rule for values that move. Mirrors `treecf.api.ValuePolicy`
|
|
84
89
|
/// minus the callable case (rejected before marshaling) and minus `"raw"` (`None`).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//! port of `treecf.backends._exact_propagation`. Parity rules in the module
|
|
3
3
|
//! header of `super` govern this file too.
|
|
4
4
|
|
|
5
|
+
use crate::cells::Cell;
|
|
5
6
|
use crate::constraints::Constraints;
|
|
6
7
|
use crate::exact::domains::State;
|
|
7
8
|
|
|
@@ -29,15 +30,30 @@ pub(crate) struct Propagation<'a> {
|
|
|
29
30
|
pub(crate) zeros: Vec<usize>,
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/// Whether a demanded value can still be met inside an interval; a missing
|
|
34
|
+
/// value never can, since an interval holds numbers only.
|
|
35
|
+
fn range_contains(rng: &Cell, value: f64) -> bool {
|
|
36
|
+
!value.is_nan() && rng.contains(value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// A feature held to a whole interval (`ranges[f]` set) has no value to compare
|
|
40
|
+
/// a demand against: the demand is checked for containment and then recorded,
|
|
41
|
+
/// exactly as for an undecided feature, so the point the feature is later
|
|
42
|
+
/// narrowed to is held to it.
|
|
32
43
|
fn force(
|
|
33
44
|
forced_value: &mut [Option<f64>],
|
|
34
45
|
frame: &mut PropFrame,
|
|
35
46
|
assigned: &[bool],
|
|
36
47
|
values: &[f64],
|
|
48
|
+
ranges: &[Option<Cell>],
|
|
37
49
|
f: usize,
|
|
38
50
|
value: f64,
|
|
39
51
|
) -> bool {
|
|
40
|
-
if
|
|
52
|
+
if let Some(rng) = ranges.get(f).copied().flatten() {
|
|
53
|
+
if !range_contains(&rng, value) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
} else if assigned[f] {
|
|
41
57
|
return values[f] == value;
|
|
42
58
|
}
|
|
43
59
|
if let Some(current) = forced_value[f] {
|
|
@@ -82,6 +98,19 @@ impl<'a> Propagation<'a> {
|
|
|
82
98
|
v: f64,
|
|
83
99
|
assigned: &[bool],
|
|
84
100
|
values: &[f64],
|
|
101
|
+
) -> (PropFrame, bool) {
|
|
102
|
+
self.apply_with(j, v, assigned, values, &[])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// `apply` for a search that also holds features to whole intervals; an
|
|
106
|
+
/// empty `ranges` slice is what the classic search passes.
|
|
107
|
+
pub(crate) fn apply_with(
|
|
108
|
+
&mut self,
|
|
109
|
+
j: usize,
|
|
110
|
+
v: f64,
|
|
111
|
+
assigned: &[bool],
|
|
112
|
+
values: &[f64],
|
|
113
|
+
ranges: &[Option<Cell>],
|
|
85
114
|
) -> (PropFrame, bool) {
|
|
86
115
|
let mut frame = PropFrame::default();
|
|
87
116
|
if let Some(forced) = self.forced_value[j] {
|
|
@@ -117,6 +146,7 @@ impl<'a> Propagation<'a> {
|
|
|
117
146
|
&mut frame,
|
|
118
147
|
assigned,
|
|
119
148
|
values,
|
|
149
|
+
ranges,
|
|
120
150
|
last,
|
|
121
151
|
1.0,
|
|
122
152
|
) {
|
|
@@ -132,6 +162,7 @@ impl<'a> Propagation<'a> {
|
|
|
132
162
|
&mut frame,
|
|
133
163
|
assigned,
|
|
134
164
|
values,
|
|
165
|
+
ranges,
|
|
135
166
|
cons_index as usize,
|
|
136
167
|
cons_value,
|
|
137
168
|
)
|