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.
Files changed (62) hide show
  1. {treecf-0.2.2 → treecf-0.2.3}/PKG-INFO +1 -1
  2. {treecf-0.2.2 → treecf-0.2.3}/pyproject.toml +1 -1
  3. {treecf-0.2.2 → treecf-0.2.3}/rust/Cargo.lock +1 -1
  4. {treecf-0.2.2 → treecf-0.2.3}/rust/Cargo.toml +1 -1
  5. {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/search.rs +9 -8
  6. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/__init__.py +1 -1
  7. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/sklearn.py +35 -3
  8. {treecf-0.2.2 → treecf-0.2.3}/LICENSE +0 -0
  9. {treecf-0.2.2 → treecf-0.2.3}/README.md +0 -0
  10. {treecf-0.2.2 → treecf-0.2.3}/rust/src/cells.rs +0 -0
  11. {treecf-0.2.2 → treecf-0.2.3}/rust/src/constraints.rs +0 -0
  12. {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/domains.rs +0 -0
  13. {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/mod.rs +0 -0
  14. {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/orderpairs.rs +0 -0
  15. {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/propagation.rs +0 -0
  16. {treecf-0.2.2 → treecf-0.2.3}/rust/src/exact/test_support.rs +0 -0
  17. {treecf-0.2.2 → treecf-0.2.3}/rust/src/ga.rs +0 -0
  18. {treecf-0.2.2 → treecf-0.2.3}/rust/src/interrupt.rs +0 -0
  19. {treecf-0.2.2 → treecf-0.2.3}/rust/src/ir.rs +0 -0
  20. {treecf-0.2.2 → treecf-0.2.3}/rust/src/lib.rs +0 -0
  21. {treecf-0.2.2 → treecf-0.2.3}/rust/src/py.rs +0 -0
  22. {treecf-0.2.2 → treecf-0.2.3}/rust/src/regions.rs +0 -0
  23. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/_errors.py +0 -0
  24. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/_json.py +0 -0
  25. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/aim/__init__.py +0 -0
  26. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/aim/cells.py +0 -0
  27. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/api.py +0 -0
  28. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/audit.py +0 -0
  29. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/__init__.py +0 -0
  30. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_bounds.py +0 -0
  31. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_domains.py +0 -0
  32. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_orderpairs.py +0 -0
  33. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/_exact_propagation.py +0 -0
  34. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/exact.py +0 -0
  35. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/exact_rust.py +0 -0
  36. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/genetic.py +0 -0
  37. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/genetic_rust.py +0 -0
  38. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/backends/regions_rust.py +0 -0
  39. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/batch.py +0 -0
  40. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/__init__.py +0 -0
  41. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/compile.py +0 -0
  42. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/flatten.py +0 -0
  43. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/objects.py +0 -0
  44. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/constraints/parser.py +0 -0
  45. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/__init__.py +0 -0
  46. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/conformance.py +0 -0
  47. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/evaluate.py +0 -0
  48. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/flatten.py +0 -0
  49. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/model.py +0 -0
  50. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/__init__.py +0 -0
  51. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/catboost.py +0 -0
  52. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/json_dump.py +0 -0
  53. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/lightgbm.py +0 -0
  54. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/ir/parsers/xgboost.py +0 -0
  55. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/mining.py +0 -0
  56. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/objective.py +0 -0
  57. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/plausibility.py +0 -0
  58. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/py.typed +0 -0
  59. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/regions.py +0 -0
  60. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/targets.py +0 -0
  61. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/viz.py +0 -0
  62. {treecf-0.2.2 → treecf-0.2.3}/src/treecf/viz_batch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: treecf
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "treecf"
3
- version = "0.2.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" }
@@ -419,7 +419,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
419
419
 
420
420
  [[package]]
421
421
  name = "treecf-core"
422
- version = "0.2.2"
422
+ version = "0.2.3"
423
423
  dependencies = [
424
424
  "numpy",
425
425
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "treecf-core"
3
- version = "0.2.2"
3
+ version = "0.2.3"
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"
@@ -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
- let proof;
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
- proof = if gap > 0.0 && gap_prune_fired {
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
- lower_bound = py_min(py_min(open_view, incumbent_cost), set_aside_view);
1085
- proof = "heuristic";
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() {
@@ -28,7 +28,7 @@ from treecf.plausibility import Plausibility
28
28
  from treecf.regions import RecourseRegion
29
29
  from treecf.targets import Target
30
30
 
31
- __version__ = "0.2.2"
31
+ __version__ = "0.2.3"
32
32
 
33
33
  __all__ = [
34
34
  "AllowMissing",
@@ -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