treecf 0.3.1__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.
Files changed (77) hide show
  1. treecf-0.3.2/PKG-INFO +188 -0
  2. treecf-0.3.2/README.md +121 -0
  3. {treecf-0.3.1 → treecf-0.3.2}/pyproject.toml +1 -1
  4. {treecf-0.3.1 → treecf-0.3.2}/rust/Cargo.lock +1 -1
  5. {treecf-0.3.1 → treecf-0.3.2}/rust/Cargo.toml +1 -1
  6. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/__init__.py +1 -1
  7. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/_menu.py +1 -1
  8. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/api.py +20 -13
  9. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/audit.py +5 -0
  10. treecf-0.3.2/src/treecf/datasets/__init__.py +59 -0
  11. treecf-0.3.2/src/treecf/datasets/credit_model.json +1 -0
  12. treecf-0.3.1/PKG-INFO +0 -143
  13. treecf-0.3.1/README.md +0 -76
  14. {treecf-0.3.1 → treecf-0.3.2}/LICENSE +0 -0
  15. {treecf-0.3.1 → treecf-0.3.2}/rust/src/cells.rs +0 -0
  16. {treecf-0.3.1 → treecf-0.3.2}/rust/src/constraints.rs +0 -0
  17. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/domains.rs +0 -0
  18. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/mod.rs +0 -0
  19. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/orderpairs.rs +0 -0
  20. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/propagation.rs +0 -0
  21. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/refine.rs +0 -0
  22. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/search.rs +0 -0
  23. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/test_support.rs +0 -0
  24. {treecf-0.3.1 → treecf-0.3.2}/rust/src/exact/trace.rs +0 -0
  25. {treecf-0.3.1 → treecf-0.3.2}/rust/src/ga.rs +0 -0
  26. {treecf-0.3.1 → treecf-0.3.2}/rust/src/interrupt.rs +0 -0
  27. {treecf-0.3.1 → treecf-0.3.2}/rust/src/ir.rs +0 -0
  28. {treecf-0.3.1 → treecf-0.3.2}/rust/src/lib.rs +0 -0
  29. {treecf-0.3.1 → treecf-0.3.2}/rust/src/py.rs +0 -0
  30. {treecf-0.3.1 → treecf-0.3.2}/rust/src/region_slab.rs +0 -0
  31. {treecf-0.3.1 → treecf-0.3.2}/rust/src/regions.rs +0 -0
  32. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/_errors.py +0 -0
  33. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/_json.py +0 -0
  34. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/_portfolio.py +0 -0
  35. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/_region_slab.py +0 -0
  36. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/aim/__init__.py +0 -0
  37. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/aim/cells.py +0 -0
  38. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/__init__.py +0 -0
  39. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/_exact_bounds.py +0 -0
  40. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/_exact_domains.py +0 -0
  41. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/_exact_orderpairs.py +0 -0
  42. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/_exact_profile.py +0 -0
  43. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/_exact_propagation.py +0 -0
  44. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/_exact_refine.py +0 -0
  45. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/_exact_trace.py +0 -0
  46. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/exact.py +0 -0
  47. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/exact_rust.py +0 -0
  48. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/genetic.py +0 -0
  49. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/genetic_rust.py +0 -0
  50. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/backends/regions_rust.py +0 -0
  51. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/batch.py +0 -0
  52. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/constraints/__init__.py +0 -0
  53. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/constraints/compile.py +0 -0
  54. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/constraints/flatten.py +0 -0
  55. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/constraints/objects.py +0 -0
  56. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/constraints/parser.py +0 -0
  57. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/__init__.py +0 -0
  58. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/conformance.py +0 -0
  59. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/evaluate.py +0 -0
  60. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/flatten.py +0 -0
  61. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/model.py +0 -0
  62. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/__init__.py +0 -0
  63. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/_catboost_cat.py +0 -0
  64. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/_float32.py +0 -0
  65. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/catboost.py +0 -0
  66. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/json_dump.py +0 -0
  67. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/lightgbm.py +0 -0
  68. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/sklearn.py +0 -0
  69. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/ir/parsers/xgboost.py +0 -0
  70. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/mining.py +0 -0
  71. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/objective.py +0 -0
  72. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/plausibility.py +0 -0
  73. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/py.typed +0 -0
  74. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/regions.py +0 -0
  75. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/targets.py +0 -0
  76. {treecf-0.3.1 → treecf-0.3.2}/src/treecf/viz.py +0 -0
  77. {treecf-0.3.1 → 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
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22069503.svg)](https://doi.org/10.5281/zenodo.22069503)
70
+ [![PyPI](https://img.shields.io/pypi/v/treecf.svg)](https://pypi.org/project/treecf/)
71
+ [![Python](https://img.shields.io/pypi/pyversions/treecf.svg)](https://pypi.org/project/treecf/)
72
+ [![CI](https://github.com/wlazlod/treecf/actions/workflows/ci.yml/badge.svg)](https://github.com/wlazlod/treecf/actions/workflows/ci.yml)
73
+ [![License](https://img.shields.io/pypi/l/treecf.svg)](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
+ ![Lever-set by feature matrix of a recourse menu: filled cells where a plan changes that lever, a square for a proved-optimal plan, a cross for a lever set certified unable to reach the target](https://raw.githubusercontent.com/wlazlod/treecf/main/docs/guide/img/plot_recourse_menu.png)
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
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22069503.svg)](https://doi.org/10.5281/zenodo.22069503)
4
+ [![PyPI](https://img.shields.io/pypi/v/treecf.svg)](https://pypi.org/project/treecf/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/treecf.svg)](https://pypi.org/project/treecf/)
6
+ [![CI](https://github.com/wlazlod/treecf/actions/workflows/ci.yml/badge.svg)](https://github.com/wlazlod/treecf/actions/workflows/ci.yml)
7
+ [![License](https://img.shields.io/pypi/l/treecf.svg)](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
+ ![Lever-set by feature matrix of a recourse menu: filled cells where a plan changes that lever, a square for a proved-optimal plan, a cross for a lever set certified unable to reach the target](https://raw.githubusercontent.com/wlazlod/treecf/main/docs/guide/img/plot_recourse_menu.png)
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.1"
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" }
@@ -419,7 +419,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
419
419
 
420
420
  [[package]]
421
421
  name = "treecf-core"
422
- version = "0.3.1"
422
+ version = "0.3.2"
423
423
  dependencies = [
424
424
  "numpy",
425
425
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "treecf-core"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  edition = "2021"
5
5
  # f64::next_down (cells.rs) stabilized in 1.86; pyo3 0.29 needs 1.83
6
6
  rust-version = "1.86"
@@ -31,7 +31,7 @@ from treecf.plausibility import Plausibility
31
31
  from treecf.regions import RecourseRegion
32
32
  from treecf.targets import Target
33
33
 
34
- __version__ = "0.3.1"
34
+ __version__ = "0.3.2"
35
35
 
36
36
  __all__ = [
37
37
  "AllowMissing",
@@ -42,7 +42,7 @@ _MODES = ("minimal", "all")
42
42
  _CRITERIA = ("levers", "coalitions")
43
43
  _MENU_OPTIONS: dict[str, Any] = {
44
44
  "backend": "exact",
45
- "search": "classic",
45
+ "search": "refine",
46
46
  "seed": None,
47
47
  "time_budget_s": None,
48
48
  "total_budget_s": None,
@@ -221,7 +221,7 @@ def _calibrated_readout(target: Target, score_raw: float) -> float | None:
221
221
  _DEFAULT_WARM_START = True
222
222
  _DEFAULT_NODE_BUDGET = 2_000_000
223
223
  _DEFAULT_GAP = 0.0
224
- _DEFAULT_SEARCH = "classic"
224
+ _DEFAULT_SEARCH = "refine"
225
225
  _DEFAULT_TIME_BUDGET_S = 10.0
226
226
  _SEARCH_MODES = ("classic", "refine")
227
227
 
@@ -574,12 +574,13 @@ class Explainer:
574
574
  additive rather than deducted from the budget. ``gap`` lets the exact
575
575
  search settle for a counterfactual within that relative fraction of
576
576
  the true optimum, reported through ``proof="optimal_within_gap"``.
577
- ``search`` picks the exact engine: ``"classic"`` (the default)
578
- assigns one candidate value per feature at a time; ``"refine"``
577
+ ``search`` picks the exact engine: ``"refine"`` (the default)
579
578
  first holds each numeric feature to a range of routing cells and
580
- descends only where the score bound forces it, which proves the same
581
- optimum and the same infeasibility certificates — often in far fewer
582
- nodes on models with many thresholds per feature — though the row it
579
+ descends only where the score bound forces it; ``"classic"``
580
+ assigns one candidate value per feature at a time. Both prove the
581
+ same optimum and the same infeasibility certificates — the refine
582
+ search often in far fewer nodes on models with many thresholds per
583
+ feature, which is why it is the default — though the row the two
583
584
  returns may be a different argmin of the same cost.
584
585
 
585
586
  An exact search can return a feasible row with ``proof="heuristic"``
@@ -1196,11 +1197,13 @@ class Explainer:
1196
1197
  elapsed = time.monotonic() - start
1197
1198
 
1198
1199
  if res.stats["completed"] is False:
1199
- # the size of the space is computed only on this path: it costs a
1200
- # domain build, cheap next to an exhausted search
1200
+ # the size of the space is computed only on this path, and without
1201
+ # the presolve pass: sizing the presolved space means bracketing
1202
+ # every candidate state through every tree in Python, which on a
1203
+ # wide, deep model costs seconds after the budget already ended
1201
1204
  log10_states: float | None = None
1202
1205
  if cast(int, res.stats["nodes_expanded"]) >= node_budget or elapsed >= time_budget_s:
1203
- log10_states = cast(float, self._search_profile(x, interval)["log10_states"])
1206
+ log10_states = cast(float, self._search_profile(x, None)["log10_states"])
1204
1207
  degradation = _degradation_for(
1205
1208
  res, node_budget, time_budget_s, elapsed, seed, log10_states
1206
1209
  )
@@ -1453,7 +1456,7 @@ class Explainer:
1453
1456
  max_levers: int = 3,
1454
1457
  mode: str = "minimal",
1455
1458
  backend: str = "exact",
1456
- search: str = "classic",
1459
+ search: str = "refine",
1457
1460
  seed: int | None = None,
1458
1461
  time_budget_s: float | None = None,
1459
1462
  total_budget_s: float | None = None,
@@ -1502,7 +1505,7 @@ class Explainer:
1502
1505
  backend
1503
1506
  ``"exact"`` (certifies) or ``"genetic"``.
1504
1507
  search
1505
- Exact search mode, ``"classic"`` or ``"refine"``.
1508
+ Exact search mode, ``"refine"`` (the default) or ``"classic"``.
1506
1509
  seed
1507
1510
  Passed to every solve.
1508
1511
  time_budget_s
@@ -1733,7 +1736,11 @@ class Explainer:
1733
1736
  ``seed``/``node_budget``/``gap``/``time_budget_s``/``warm_start``/
1734
1737
  ``search`` are recorded under ``solve.declared`` when given: the
1735
1738
  result object does not carry them, so they are caller-supplied, and
1736
- the block's name makes that provenance explicit.
1739
+ the block's name makes that provenance explicit. ``search`` is the one
1740
+ exception: an exact result reports the engine that ran in its own
1741
+ ``solver_stats``, so it is recorded there even when not given, and a
1742
+ certificate never depends on the default of the release that issued
1743
+ it.
1737
1744
 
1738
1745
  Parameters
1739
1746
  ----------
@@ -1758,7 +1765,7 @@ class Explainer:
1758
1765
  The warm-start setting the solve ran with, likewise.
1759
1766
  search
1760
1767
  The exact search mode (``"classic"`` or ``"refine"``) the solve
1761
- ran with, likewise.
1768
+ ran with; taken from the result's ``solver_stats`` when omitted.
1762
1769
 
1763
1770
  Returns
1764
1771
  -------
@@ -485,6 +485,11 @@ def build_certificate(
485
485
  declared["warm_start"] = warm_start
486
486
  if search is not None:
487
487
  declared["search"] = search
488
+ elif "search" in result.solver_stats:
489
+ # an exact solve reports which engine ran; record it, so a certificate
490
+ # never leaves the engine to be inferred from the issuing release's
491
+ # default
492
+ declared["search"] = str(result.solver_stats["search"])
488
493
  solve: dict[str, object] = {
489
494
  "backend": _backend_of(result.solver_stats),
490
495
  "proof": result.proof,
@@ -0,0 +1,59 @@
1
+ """A packaged credit-shaped demo: a committed model, a background sample, one declined row.
2
+
3
+ Every example in the documentation starts from ``credit_demo()``, so it runs
4
+ as written on a fresh install without a training library. The model is a
5
+ committed LightGBM JSON dump of a small synthetic credit-risk classifier over
6
+ five features — ``income``, ``utilization``, ``dpd_12m``, ``tenure_months``,
7
+ and a native categorical ``occupation`` — and the background rows follow a
8
+ fixed synthetic recipe, so a given seed always returns the same data.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from importlib import resources
14
+
15
+ import numpy as np
16
+ import numpy.typing as npt
17
+
18
+ from treecf.ir.model import EnsembleIR, apply_categories
19
+ from treecf.ir.parsers import parse_model
20
+
21
+ FloatArray = npt.NDArray[np.float64]
22
+
23
+ OCCUPATIONS = ("student", "clerk", "manager", "retired")
24
+ """Display names of the demo model's ``occupation`` codes, in code order."""
25
+
26
+
27
+ def credit_demo(seed: int = 7, n: int = 400) -> tuple[EnsembleIR, FloatArray, FloatArray]:
28
+ """The demo model, a background sample, and one declined applicant.
29
+
30
+ Parameters
31
+ ----------
32
+ seed
33
+ Seed of the background sample; the model itself is fixed.
34
+ n
35
+ Number of background rows.
36
+
37
+ Returns
38
+ -------
39
+ ``(model, X, x)``: the parsed model (an ``EnsembleIR`` with the
40
+ occupation names installed, accepted by ``Explainer`` directly), an
41
+ ``(n, 5)`` background matrix, and its second row, an applicant the
42
+ model scores well above a 5% default probability.
43
+ """
44
+ with resources.as_file(resources.files(__name__).joinpath("credit_model.json")) as path:
45
+ model = apply_categories(parse_model(str(path)), {"occupation": OCCUPATIONS})
46
+ rng = np.random.default_rng(seed)
47
+ X = np.column_stack(
48
+ [
49
+ rng.normal(loc=4200.0, scale=1600.0, size=n), # income
50
+ np.clip(rng.beta(2.0, 3.5, size=n), 0.0, 1.0), # utilization
51
+ np.floor(rng.exponential(scale=6.0, size=n)), # dpd_12m
52
+ np.floor(rng.uniform(3, 240, size=n)), # tenure_months
53
+ rng.integers(0, 4, size=n).astype(np.float64), # occupation codes
54
+ ]
55
+ )
56
+ return model, X, X[1].copy()
57
+
58
+
59
+ __all__ = ["OCCUPATIONS", "credit_demo"]