Skip to content

Make BaseTree(Regressor) and BaseForest(Regressor) generic over criterion - #502

Closed
slievens wants to merge 19 commits into
smartcorelib:mainfrom
slievens:make-generic
Closed

slievens wants to merge 19 commits into
smartcorelib:mainfrom
slievens:make-generic

Conversation

@slievens

@slievens slievens commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #501

Follows up on PR#497

Checklist

  • [] My branch is up-to-date with main branch. --> Follows on PR#497
  • Everything works and tested on latest stable Rust.
  • Coverage and Linting have been applied

Current behaviour

BaseTreeRegressor and BaseForestRegressor are hard-coded for regression: MSE splits, f64 leaf values and mean predictions. The classification trees and forests cannot reuse them.

New expected behaviour

The base tree and base forest are generic over the output type of the leaves. A split Criterion is passed to the fit functions. The two structs have no regression-specific code:

  • BaseTree<.., Out: LeafOut>: training uses only the criterion passed to fit (target, add, leaf_value, ...), and prediction returns the leaf output (leaf_output_for_row).
  • BaseForest<.., Out: LeafOutput>: uses the criterion in fit and gives access to the trees and the bootstrap samples.
  • The regression code (convert the leaf to TY, mean over the trees, OOB mean) is now in DecisionTreeRegressor, predict_mean and predict_mean_oob. RandomForestRegressor and ExtraTreesRegressor use MSECriterion.

No public API or behaviour change: the modules are internal, and the predictions are identical. The created trees are identical and the performance is the same.

Change logs

Changed

  • Rename BaseTreeRegressor → BaseTree and BaseForestRegressor → BaseForest (with their parameter structs and files).
  • Make both generic over LeafOutput, and move the regression prediction code to the regressors.

This prepares DecisionTreeClassifier and RandomForestClassifier to use the same base types.

This is the first step in making base tree "regressor" be used by all the
tree classifiers. Next step is to make the implementation generic over the
used Criterion with callers deciding it.

Add a Criterion trait with MSE and Gini implementations in
src/tree/criterion.rs. Use MeanStats in BaseTreeRegressor in place of
explicit sum/mass bookkeeping.

- Store node stats in NodeVisitor; remove true/false_child_output
- Keep scratch stats in SplitWorkspace to avoid new allocations
- Rank splits by proxy improvement; store the real impurity
  improvement in split_score
- Pass the sample count to find_best_cutoff and check
  min_samples_split at the caller
Make sure no leaf has a split_core.
Add tests to check this.

Also check that no "proxy" values are leftover by verifying that
the sum of all split_scores adds up to the total impurity decrease.
- Compute the MSE improvement as (S_T·M_F − S_F·M_T)² / (M_T·M_F·M_P).
  This prevents cancellation for targets with a large offset.
- Remove rows with mass 0 in grow(). Zero-weight rows no longer count
  toward min_samples_leaf/min_samples_split and do not move thresholds.
- Remove the MASS_EPS clamp from difference().
- In split(), add the false side directly instead of parent − true.
- Add tests for target-offset invariance and zero-weight rows.
Add compute_feature_importances (mean decrease in impurity, as in
scikit-learn) to DecisionTreeRegressor, RandomForestRegressor and
ExtraTreesRegressor. The forests normalize the importances of each
tree, take the mean and normalize again.

The fitted models store new fields (root_mass, num_features), so
models serialized with an earlier version do not deserialize.
BaseTree only finds the leaf. DecisionTreeRegressor and
BaseForestRegressor convert the leaf output to TY.
The forest passes a Criterion to its trees. The predict methods
are restricted to Output = f64. The RF and ET regressors use MSECriterion.
BaseForest only gives access to its trees and samples. The mean of
the tree predictions is in predict_mean and predict_mean_oob.
@slievens
slievens requested a review from Mec-iS as a code owner October 23, 2026 13:05
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.98208% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.70%. Comparing base (9eaae9e) to head (0a62442).
⚠️ Report is 206 commits behind head on main.

Files with missing lines Patch % Lines
src/ensemble/base_forest.rs 89.23% 7 Missing ⚠️
src/tree/decision_tree_regressor.rs 75.00% 4 Missing ⚠️
src/ensemble/extra_trees_regressor.rs 77.77% 2 Missing ⚠️
src/ensemble/random_forest_regressor.rs 88.88% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #502       +/-   ##
===========================================
+ Coverage   43.97%   85.70%   +41.72%     
===========================================
  Files          85       97       +12     
  Lines        7281     9490     +2209     
===========================================
+ Hits         3202     8133     +4931     
+ Misses       4079     1357     -2722     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

slievens and others added 3 commits October 23, 2026 21:00
BaseTree and BaseForest do not store the criterion. Only the type of
the leaf value is kept in the tree. Make the types generic over this
output type and move the Criterion type parameter to the fit functions.

Move output_approximate_eq from Criterion to a new LeafOutput trait
on the output type.
@Mec-iS
Mec-iS changed the base branch from main to docs/breaking-changes-0.7 October 24, 2026 04:42
@Mec-iS
Mec-iS changed the base branch from docs/breaking-changes-0.7 to main October 24, 2026 04:43
@Mec-iS Mec-iS mentioned this pull request Oct 24, 2026
@Mec-iS

Mec-iS commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Superseded by #504 (v0.7 Release): these changes are merged into the v0.7 release candidate on main (commit cd217dd). The branch-level conflict resolutions between the v0.7 PRs are documented in #504. Benchmarks for the candidate are running — results will follow in a comment and in the measurement report issue.

@Mec-iS

Mec-iS commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

@slievens The v0.7 candidate benchmarks are in: #506. The generics refactor may affect code generation in the measured tree-fit path (+4–6% instructions, above the 4% gate). The issue suggests per-PR attribution benches to split the cost between #492, #497 and this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make BaseTree(Regressor) and BaseForest(Regressor) generic over critetion

2 participants