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.
Files changed (78) hide show
  1. treecf-0.3.2/PKG-INFO +188 -0
  2. treecf-0.3.2/README.md +121 -0
  3. {treecf-0.3.0 → treecf-0.3.2}/pyproject.toml +1 -1
  4. {treecf-0.3.0 → treecf-0.3.2}/rust/Cargo.lock +1 -1
  5. {treecf-0.3.0 → treecf-0.3.2}/rust/Cargo.toml +1 -1
  6. {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/domains.rs +1 -1
  7. {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/mod.rs +6 -1
  8. {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/propagation.rs +32 -1
  9. treecf-0.3.2/rust/src/exact/refine.rs +825 -0
  10. {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/search.rs +415 -132
  11. treecf-0.3.2/rust/src/exact/trace.rs +108 -0
  12. {treecf-0.3.0 → treecf-0.3.2}/rust/src/lib.rs +1 -0
  13. {treecf-0.3.0 → treecf-0.3.2}/rust/src/py.rs +103 -26
  14. treecf-0.3.2/rust/src/region_slab.rs +344 -0
  15. {treecf-0.3.0 → treecf-0.3.2}/rust/src/regions.rs +559 -68
  16. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/__init__.py +4 -1
  17. treecf-0.3.2/src/treecf/_menu.py +629 -0
  18. treecf-0.3.2/src/treecf/_portfolio.py +619 -0
  19. treecf-0.3.2/src/treecf/_region_slab.py +278 -0
  20. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/api.py +415 -39
  21. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/audit.py +34 -11
  22. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_bounds.py +36 -2
  23. treecf-0.3.2/src/treecf/backends/_exact_profile.py +106 -0
  24. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_propagation.py +26 -1
  25. treecf-0.3.2/src/treecf/backends/_exact_refine.py +653 -0
  26. treecf-0.3.2/src/treecf/backends/_exact_trace.py +50 -0
  27. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/exact.py +211 -46
  28. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/exact_rust.py +18 -1
  29. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/regions_rust.py +41 -8
  30. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/batch.py +124 -20
  31. treecf-0.3.2/src/treecf/datasets/__init__.py +59 -0
  32. treecf-0.3.2/src/treecf/datasets/credit_model.json +1 -0
  33. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/model.py +92 -1
  34. treecf-0.3.2/src/treecf/ir/parsers/_float32.py +49 -0
  35. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/catboost.py +7 -2
  36. treecf-0.3.2/src/treecf/ir/parsers/json_dump.py +95 -0
  37. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/lightgbm.py +3 -1
  38. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/sklearn.py +4 -30
  39. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/xgboost.py +4 -1
  40. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/regions.py +442 -80
  41. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/viz.py +359 -6
  42. treecf-0.3.0/PKG-INFO +0 -143
  43. treecf-0.3.0/README.md +0 -76
  44. treecf-0.3.0/src/treecf/ir/parsers/json_dump.py +0 -39
  45. {treecf-0.3.0 → treecf-0.3.2}/LICENSE +0 -0
  46. {treecf-0.3.0 → treecf-0.3.2}/rust/src/cells.rs +0 -0
  47. {treecf-0.3.0 → treecf-0.3.2}/rust/src/constraints.rs +0 -0
  48. {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/orderpairs.rs +0 -0
  49. {treecf-0.3.0 → treecf-0.3.2}/rust/src/exact/test_support.rs +0 -0
  50. {treecf-0.3.0 → treecf-0.3.2}/rust/src/ga.rs +0 -0
  51. {treecf-0.3.0 → treecf-0.3.2}/rust/src/interrupt.rs +0 -0
  52. {treecf-0.3.0 → treecf-0.3.2}/rust/src/ir.rs +0 -0
  53. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/_errors.py +0 -0
  54. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/_json.py +0 -0
  55. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/aim/__init__.py +0 -0
  56. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/aim/cells.py +0 -0
  57. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/__init__.py +0 -0
  58. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_domains.py +0 -0
  59. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/_exact_orderpairs.py +0 -0
  60. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/genetic.py +0 -0
  61. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/backends/genetic_rust.py +0 -0
  62. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/__init__.py +0 -0
  63. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/compile.py +0 -0
  64. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/flatten.py +0 -0
  65. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/objects.py +0 -0
  66. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/constraints/parser.py +0 -0
  67. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/__init__.py +0 -0
  68. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/conformance.py +0 -0
  69. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/evaluate.py +0 -0
  70. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/flatten.py +0 -0
  71. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/__init__.py +0 -0
  72. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/ir/parsers/_catboost_cat.py +0 -0
  73. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/mining.py +0 -0
  74. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/objective.py +0 -0
  75. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/plausibility.py +0 -0
  76. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/py.typed +0 -0
  77. {treecf-0.3.0 → treecf-0.3.2}/src/treecf/targets.py +0 -0
  78. {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
+ [![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.0"
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.0"
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.0"
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"
@@ -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 assigned[f] {
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
  )