Add anomaly.RobustRandomCutForest - #1975
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
|
Very cool! I'll take a look sometime this week. Out of curiosity, may I know what's your interest with River? Are you maybe using it for work? |
|
Thanks Max! My background is in payments / fraud ML, so online and streaming anomaly detection is squarely in my area of interest. RRCF is a well-known streaming detector and I noticed river didn't have it yet, so it seemed like a natural thing to contribute. It's not wired into a specific production system, more that this is the kind of tooling I like to work with and learn from. Happy to iterate on the PR however you'd like. |
| self.l = left | ||
| self.r = right | ||
| self.u = u | ||
| self.q = q | ||
| self.p = p | ||
| self.n = n | ||
| self.b = b |
There was a problem hiding this comment.
I am not a fan of using acronyms, and even less so single letter variables. Please use explicit names
| class _RCTree: | ||
| """A single robust random cut tree. | ||
|
|
||
| This is a faithful port of the ``RCTree`` data structure from the ``rrcf`` package | ||
| (https://github.com/kLabUM/rrcf, MIT license), restricted to the incremental streaming | ||
| interface used by River: an empty tree grown one point at a time via ``insert_point`` and | ||
| trimmed via ``forget_point``. | ||
| """ |
There was a problem hiding this comment.
I'm not sure porting rrcf is the right approach. Instead, I would rather consider leveraging the existing HalfSpaceTrees, and seeing what code can be shared with it. And regardless, the implementation should be dictionary based, and should not necessitate using NumPy.
Add an online Robust Random Cut Forest anomaly detector (Guha et al., 2016). It maintains an ensemble of robust random cut trees over a sliding window and scores points by their average collusive displacement (CoDisp); score_one is side-effect-free. The tree logic is a faithful port of the reference rrcf package (its CoDisp matches bit-for-bit) exposed through river's dict-native AnomalyDetector API. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop NumPy entirely: bounding boxes are now plain dicts keyed by feature name, and the branch and leaf types subclass tree.base.Branch and tree.base.Leaf. Rename the single-letter node attributes to feature, threshold, left, right, parent, n_points, and lower/upper. Remove the per-insert scan over every leaf that computed the maximum depth, along with the depth attribute it required, since walking parent pointers gives CoDisp the same answer. Eviction is now O(depth) rather than O(subtree), and the detector is several times faster than the previous NumPy implementation. CoDisp matches the reference rrcf at master bit-for-bit when both are driven by the same sequence of uniform draws. Note this follows master rather than the released 0.4.4, whose _insert_point_cut collapses the bounding box span to zero when the insertion target is a leaf.
98a6d28 to
80b298f
Compare
|
Thanks Max, and sorry for the slow turnaround. I've reworked both points. Names. The Dict based, no NumPy. I've also rebased onto current main, which moved the tests to On sharing with HST builds a complete fixed-depth tree once, up front, from a declared hypercube, and never changes its shape afterwards: learning bumps a mass counter along one root-to-leaf path, and the window is a wholesale That said, you've already pulled HST's nodes out into How I checked correctness. Going pure Python meant I lost the old bit-for-bit comparison, since a Python RNG can't reproduce a NumPy stream. So I did it the other way round: I recorded every uniform variate the reference They do, across five configurations (1 to 12 features, sliding window and pure growth, 2050 stream points and 200 scored queries):
One thing I should flag, because it's the single place this deliberately diverges from what you'd get off PyPI. That agreement is against This implementation follows master. Speed. Dropping NumPy made it faster, not slower. While I was in there I also removed a
To be straight about the absolute numbers rather than just the ratios: at the current defaults of 40 trees over a 256 point window it's about 1.1k records/s to learn and about 530/s to score and learn, which is under the 5k/s guideline in the contributing docs. That's the cost of doing 40 trees of work per record rather than anything specific to this implementation, and it's why the docstring example uses 15. Say the word and I'll lower the defaults so it lands closer to the guideline out of the box.
|
|
You're answering by asking Claude to write the answer for you. You're putting in little effort to use your own voice. I'm sorry but I do not feel inclined to pursue the review of this PR. |
|
Hi @MaxHalford you're right, and I'm sorry. I used Claude to write that reply instead of answering you myself. The PR was up front about being agent-assisted from the start, but that doesn't make it okay to hand you generated prose to read when you'd put real time into reviewing it. I'll write my own replies from here. Completely understand if you'd rather not pick this back up. Thanks :) |
Closes #1393.
What this adds
anomaly.RobustRandomCutForest, an online implementation of the Robust Random Cut Forest (Guha, Mishra, Roy, Schrijvers, ICML 2016).river/anomaly/has half-space trees and LODA but no random-cut method, and RRCF is the canonical streaming random-cut detector (the method behind AWS Kinesis / CloudWatch anomaly detection).How it works
The forest is an ensemble of robust random cut trees, each holding a bounded sliding window of the most recent
tree_sizepoints. Cuts are drawn in proportion to each feature's span within the bounding box (scale-aware, unlike the uniform dimension choice of an isolation tree). A point's score is its collusive displacement (CoDisp), averaged over the trees; higher means more anomalous.learn_one/score_one). The feature ordering is fixed from the first observation; missing features are treated as0.0.score_oneis side-effect-free: it inserts the query point into each tree, reads its CoDisp, then removes it and restores the tree's RNG state, so scoring never mutates the model.n_trees=40,tree_size=256,seed.Implementation and verification
The tree logic (
insert_point/forget_point/codispand the span-proportional cut) is a faithful port of the referencerrcfpackage 1. I verified:rrcf.RCTreewith the same seed and inserting the same points, the CoDisp matches bit-for-bit (max abs diff0.0over all points).score_onepurity: the pickled model is byte-identical before and after scoring, and repeated scores of the same point are identical.seedproduce identical score sequences on the same stream.datasets.CreditCard().take(1000), paired with aStandardScaler,metrics.RollingROCAUCreaches 95.64% (this is a doctest in the module).check_estimatorpasses;mypyandruffare clean;test_rrcf.pyand the doctests pass.A changelog entry is included in
docs/releases/unreleased.md.Footnotes
Bartos, Mullapudi, Troutman, rrcf: Implementation of the Robust Random Cut Forest algorithm, JOSS 2019, https://github.com/kLabUM/rrcf ↩