Skip to content

Speed up HalfSpaceTrees with an iterative walk#1859

Merged
MaxHalford merged 2 commits into
mainfrom
speed-up-hst
May 28, 2026
Merged

Speed up HalfSpaceTrees with an iterative walk#1859
MaxHalford merged 2 commits into
mainfrom
speed-up-hst

Conversation

@MaxHalford
Copy link
Copy Markdown
Member

Summary

Replaces the recursive generic tree.base.Branch.walk traversal in anomaly.HalfSpaceTrees.learn_one/score_one with HST-specific iterative loops that:

  • Inline the split-and-descend logic (no per-level generator allocation, no HSTBranch.next call).
  • Cache size_limit = 0.1 * window_size and self.height as locals (was a property recomputed ~1.1M times in profile).
  • Pivot node masses through a precomputed flat node list built once at tree construction, instead of iter_dfs() every window_size observations.

The public API is unchanged — no constructor changes, no behaviour changes. The docstring scores remain byte-identical and all 50 check_estimator cases for MinMaxScaler | HalfSpaceTrees pass.

Benchmarks

macOS arm64, Python 3.13, default HalfSpaceTrees(n_trees=10, height=8, window_size=250), best of 3.

Pure HST on a synthetic 10-feature uniform stream (no scaler):

Workload Before After Speedup
score+learn 27,902 obs/s 85,231 obs/s 3.05x
learn_one 64,232 obs/s 169,098 obs/s 2.63x
score_one 49,598 obs/s 188,172 obs/s 3.79x

MinMaxScaler | HalfSpaceTrees on datasets.CreditCard() (10k samples):

Workload Before After Speedup
score+learn 20,493 obs/s 40,513 obs/s 1.98x
learn_one 42,943 obs/s 74,241 obs/s 1.73x
score_one 38,590 obs/s 89,623 obs/s 2.32x

In the post-fix profile, HST itself (_walk_learn + _walk_score) is no longer the dominant cost — MinMaxScaler.transform_one/learn_one is. Further wins would need a Rust port (cf. the recent ADWIN/Mondrian/VectorDict efforts) or speeding up MinMaxScaler.

Test plan

  • uv run pytest --doctest-modules river/anomaly/hst.py — passes, docstring outputs byte-identical
  • uv run pytest river/anomaly/ -x — all 16 tests pass
  • uv run pytest river/test_estimators.py -k "HalfSpaceTrees" — all 50 estimator checks pass
  • pre-commit (ruff, mypy) — passes

🤖 Generated with Claude Code

Replace the generic recursive `tree.base.Branch.walk` traversal in
`anomaly.HalfSpaceTrees.learn_one` and `score_one` with HST-specific
iterative loops that inline the split logic, cache `size_limit` and the
tree height as locals, and pivot node masses through a precomputed flat
node list instead of `iter_dfs` each window.

Output is unchanged (doctest scores byte-identical, full estimator-check
suite for `MinMaxScaler | HalfSpaceTrees` passes).

Pure HST (no scaler, synthetic 10-feature stream):
  score+learn:  27.9k -> 85.2k obs/s (~3.05x)
  learn_one:    64.2k -> 169k  obs/s (~2.63x)
  score_one:    49.6k -> 188k  obs/s (~3.79x)

`MinMaxScaler | HalfSpaceTrees` on CreditCard:
  score+learn:  20.5k -> 40.5k obs/s (~1.98x)
@MaxHalford MaxHalford requested a review from smastelini as a code owner May 12, 2026 13:35
@MaxHalford MaxHalford merged commit c2c1fe5 into main May 28, 2026
1 check passed
@MaxHalford MaxHalford deleted the speed-up-hst branch May 28, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant