SPOTSET Rashomon Sets of Sparse Trees (formerly treeFARMS)¶
SPOTSET (Sparse Optimal Rashomon SET) enumerates the whole Rashomon set of sparse decision trees: instead of returning one optimal tree, it returns every tree whose regularized objective (misclassification loss plus a per-leaf penalty) lies within a configurable bound of the optimum.
Renamed from treeFARMS ("Trees FAst RashoMon Sets", ubc-systopia/treeFARMS, NeurIPS 2022). Upstream builds on the same gosdt-guesses lineage as SPOT and names nearly everything "GOSDT"; in shinrin the engine is renamed SPOTSET to keep the single-optimal-tree trainer (
SPOTClassifier, formerlyGOSDTClassifier) distinct from the set-enumerating trainer. The two engines are compiled into the same native extension (the SPOTSET engine lives behind aspotsetC++ namespace), so there is no duplicated binary or system dependency.
Why a Rashomon set?¶
Many datasets admit several almost-equally-good trees with very different structure. The Rashomon set exposes that ambiguity: you can inspect competing explanations, select for robustness/simplicity trade-offs after training, and quantify how much accuracy must be sacrificed for sparser models (see McTavish et al., Exploring the Whole Rashomon Set of Sparse Decision Trees, NeurIPS 2022).
End-to-end pipeline¶
from shinrin import SPOTSETClassifier, ThresholdGuessBinarizer
# 1) Binarize via reference-ensemble threshold guesses (same as SPOT)
enc = ThresholdGuessBinarizer(n_estimators=20, max_depth=2, random_state=0)
X_bin = enc.fit_transform(X, y)
# 2) Enumerate all trees within 5% of the optimal regularized objective
clf = SPOTSETClassifier(regularization=0.01, rashomon_bound_multiplier=0.05)
clf.fit(X_bin, y)
print(clf.n_trees_) # number of trees in the extracted set
accuracy = clf.score(X_bin, y) # first tree's accuracy (sklearn convention)
Working with the set¶
tree = clf[1] # decode the second tree of the set
print(tree.leaves()) # number of leaves
print(tree.maximum_depth()) # longest decision path
print(tree) # if-then-else pseudocode
y_hat = tree.predict(X_bin) # predictions (integer-coded labels)
metric = clf.model_set_.get_tree_metric_at_idx(1)
# {"objective": ..., "loss": ..., "complexity": ...} for that tree
trie = clf.get_decision_paths() # trie of shared decision paths across the set
Notes:
clf.predict/clf.scoreuse the first tree of the set (the lowest-objective model), mapping integer-coded labels back toclf.classes_. Trees obtained throughclf[i]predict integer-coded labels; map back withclf.classes_[prediction].- Every tree's objective is guaranteed within
(1 + rashomon_bound_multiplier)times the optimum — verified against SPOT intests/test_spotset.py. - The whole set is also a host for counterfactuals:
Minimal-Flip Feature Tweaking can
search for the smallest feature change that flips every tree in the set
at once (
scope="rashomon"), not just the first one.
SPOTSETClassifier¶
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
regularization |
float |
0.05 |
Penalty per leaf; recommend > 1 / n_samples |
rashomon_bound_multiplier |
float |
0.05 |
Set size knob: bound = (1 + multiplier) * optimum; grows the set exponentially |
rashomon |
bool |
True |
Extract the full set; False returns only the near-optimal search result |
depth_budget |
int \| None |
None |
Max tree depth (root-only tree = depth 1); None is unlimited |
time_limit |
int \| None |
None |
Seconds; on timeout the partial set is returned with a warning |
worker_limit |
int |
1 |
Parallel search workers; 0 uses one per core |
verbose |
bool |
False |
Engine progress printing |
Attributes¶
| Attribute | Description |
|---|---|
classes_ |
Unique class labels seen during fit |
model_set_ |
ModelSetContainer over the extracted set |
n_trees_ |
Number of trees in the set |
train_time_ |
Native search time in seconds |
n_features_in_ |
Number of input features |
Methods¶
| Method | Description |
|---|---|
fit(X, y) |
Extract the Rashomon set for binarized features |
predict(X) |
First-tree predictions mapped to original labels |
score(X, y) |
First-tree accuracy |
get_tree_count() |
Size of the set |
__getitem__(i) |
Decode the i-th tree as a TreeClassifier |
get_decision_paths() |
Trie of decision paths shared across the set |
Provenance¶
Vendored from ubc-systopia/treeFARMS
(BSD-3-Clause). Deviations from upstream are documented in
src/shinrin/_spotset/README.md:
the timbertrek visualization dependency and the pure-Python imbalance/OSDT
variant are not ported, and the pybind11 binding is replaced by the same
C ABI + PyO3 bridge used by the other vendored engines.