treecf 0.2.2__tar.gz → 0.2.3__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.2.2 → treecf-0.2.3}/PKG-INFO +1 -1
- {treecf-0.2.2 → treecf-0.2.3}/pyproject.toml +1 -1
- {treecf-0.2.2 → treecf-0.2.3}/rust/Cargo.lock +1 -1
- {treecf-0.2.2 → treecf-0.2.3}/rust/Cargo.toml +1 -1
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/search.rs +9 -8
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/__init__.py +1 -1
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/sklearn.py +35 -3
- {treecf-0.2.2 → treecf-0.2.3}/LICENSE +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/README.md +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/cells.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/constraints.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/domains.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/mod.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/orderpairs.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/propagation.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/test_support.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/ga.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/interrupt.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/ir.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/lib.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/py.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/rust/src/regions.rs +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/_errors.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/_json.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/aim/__init__.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/aim/cells.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/api.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/audit.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/__init__.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_bounds.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_domains.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_orderpairs.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_propagation.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/exact.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/exact_rust.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/genetic.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/genetic_rust.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/regions_rust.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/batch.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/__init__.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/compile.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/flatten.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/objects.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/parser.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/__init__.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/conformance.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/evaluate.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/flatten.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/model.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/__init__.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/catboost.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/json_dump.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/lightgbm.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/xgboost.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/mining.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/objective.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/plausibility.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/py.typed +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/regions.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/targets.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/viz.py +0 -0
- {treecf-0.2.2 → treecf-0.2.3}/src/treecf/viz_batch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "treecf"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
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" }
|
|
@@ -1053,19 +1053,18 @@ pub fn solve_exact(
|
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
1055
|
completed = completed && dropped_floor >= incumbent_cost;
|
|
1056
|
-
let lower_bound
|
|
1057
|
-
|
|
1058
|
-
if completed {
|
|
1059
|
-
lower_bound = match incumbent_row {
|
|
1056
|
+
let (lower_bound, proof) = if completed {
|
|
1057
|
+
let bound = match incumbent_row {
|
|
1060
1058
|
None => f64::INFINITY,
|
|
1061
1059
|
Some(_) if gap == 0.0 => incumbent_cost,
|
|
1062
1060
|
Some(_) => incumbent_cost / (1.0 + gap),
|
|
1063
1061
|
};
|
|
1064
|
-
|
|
1062
|
+
let label = if gap > 0.0 && gap_prune_fired {
|
|
1065
1063
|
"optimal_within_gap"
|
|
1066
1064
|
} else {
|
|
1067
1065
|
"optimal"
|
|
1068
1066
|
};
|
|
1067
|
+
(bound, label)
|
|
1069
1068
|
} else {
|
|
1070
1069
|
let mut open_view = f64::INFINITY;
|
|
1071
1070
|
if !ctx.order.is_empty() {
|
|
@@ -1081,9 +1080,11 @@ pub fn solve_exact(
|
|
|
1081
1080
|
} else {
|
|
1082
1081
|
dropped_floor
|
|
1083
1082
|
};
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1083
|
+
(
|
|
1084
|
+
py_min(py_min(open_view, incumbent_cost), set_aside_view),
|
|
1085
|
+
"heuristic",
|
|
1086
|
+
)
|
|
1087
|
+
};
|
|
1087
1088
|
|
|
1088
1089
|
let mut snapped: Vec<usize> = Vec::new();
|
|
1089
1090
|
for (level, chosen_state) in incumbent_states.iter().flatten().enumerate() {
|
|
@@ -96,8 +96,40 @@ def _parse_hist_gradient_boosting(model: Any) -> EnsembleIR:
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def _effective_le_threshold(t: float) -> float:
|
|
100
|
+
"""The float64 boundary of sklearn's float32 input cast, exactly.
|
|
101
|
+
|
|
102
|
+
sklearn ``tree_``-based ensembles route ``float32(x) <= float64(t)`` —
|
|
103
|
+
the input is cast to float32 (round-to-nearest-even) before the
|
|
104
|
+
comparison. The IR evaluates in float64, so the stored threshold must be
|
|
105
|
+
the largest float64 ``T`` with ``float32(T) <= t``; then ``x <= T``
|
|
106
|
+
reproduces the native routing for *every* float64 ``x``, including points
|
|
107
|
+
exactly on split boundaries — where a counterfactual search naturally
|
|
108
|
+
lands. (Verified by a 138k-probe property sweep and the unquantized
|
|
109
|
+
conformance tests.)
|
|
110
|
+
|
|
111
|
+
Construction: let ``f`` be the largest float32 with ``f <= t`` and ``s``
|
|
112
|
+
its float32 successor; every ``x`` below their float64 midpoint rounds to
|
|
113
|
+
``<= f``. The midpoint itself rounds half-to-even: it belongs to the left
|
|
114
|
+
side exactly when it rounds back to ``f``.
|
|
115
|
+
"""
|
|
116
|
+
f32 = np.float32(t)
|
|
117
|
+
if float(f32) > t:
|
|
118
|
+
f32 = np.nextafter(f32, np.float32(-np.inf))
|
|
119
|
+
succ = np.nextafter(f32, np.float32(np.inf))
|
|
120
|
+
mid = (float(f32) + float(succ)) / 2.0
|
|
121
|
+
if float(np.float32(mid)) == float(f32):
|
|
122
|
+
return mid
|
|
123
|
+
return float(np.nextafter(mid, -np.inf))
|
|
124
|
+
|
|
125
|
+
|
|
99
126
|
def _tree_from_arrays(tree: Any, scale: float, classifier: bool) -> Tree:
|
|
100
|
-
"""Convert a fitted ``sklearn.tree._tree.Tree`` to IR nodes (LE convention).
|
|
127
|
+
"""Convert a fitted ``sklearn.tree._tree.Tree`` to IR nodes (LE convention).
|
|
128
|
+
|
|
129
|
+
Thresholds are re-expressed on the float64 grid via
|
|
130
|
+
:func:`_effective_le_threshold` so the IR's float64 routing matches
|
|
131
|
+
sklearn's float32-cast routing bit-for-bit.
|
|
132
|
+
"""
|
|
101
133
|
left = tree.children_left
|
|
102
134
|
right = tree.children_right
|
|
103
135
|
feature = tree.feature
|
|
@@ -120,7 +152,7 @@ def _tree_from_arrays(tree: Any, scale: float, classifier: bool) -> Tree:
|
|
|
120
152
|
Node(
|
|
121
153
|
node_id=i,
|
|
122
154
|
feature=int(feature[i]),
|
|
123
|
-
threshold=float(threshold[i]),
|
|
155
|
+
threshold=_effective_le_threshold(float(threshold[i])),
|
|
124
156
|
op=SplitOp.LE,
|
|
125
157
|
missing_left=missing_left,
|
|
126
158
|
left=int(left[i]),
|
|
@@ -183,7 +215,7 @@ def parse_isolation_forest(model: Any) -> EnsembleIR:
|
|
|
183
215
|
Node(
|
|
184
216
|
node_id=i,
|
|
185
217
|
feature=int(tree.feature[i]),
|
|
186
|
-
threshold=float(tree.threshold[i]),
|
|
218
|
+
threshold=_effective_le_threshold(float(tree.threshold[i])),
|
|
187
219
|
op=SplitOp.LE,
|
|
188
220
|
missing_left=None,
|
|
189
221
|
left=int(tree.children_left[i]),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|