Skip to content

feat(drift): evaluation metrics for drift detectors - #1963

Draft
jevwithwind wants to merge 2 commits into
online-ml:mainfrom
jevwithwind:drift-evaluate
Draft

feat(drift): evaluation metrics for drift detectors#1963
jevwithwind wants to merge 2 commits into
online-ml:mainfrom
jevwithwind:drift-evaluate

Conversation

@jevwithwind

@jevwithwind jevwithwind commented Jul 27, 2026

Copy link
Copy Markdown

Hi, as discussed in #1914, this is a prototype evaluation tool for drift detection at 'river/drift/evaluate.py'. It's only a draft and needs help on the API side.

I used claude code to write the code and tests, while I directed it and reviewed its output before putting it here.

It's my first time contributing here and please let me know what you make of this.

Adds river/drift/evaluate.py: detection window matching, the metric
functions, a report, and a driver that runs a detector over a stream
with known drifts. Targets online-ml#1914.

Metric definitions follow Cerqueira et al. 2026 (arXiv:2606.07789) and
Bifet 2017 (10.1007/978-3-319-59060-8_64).

Code and tests generated with Claude Code, directed and reviewed by me.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Jul 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 96 untouched benchmarks
⏩ 16 skipped benchmarks1


Comparing jevwithwind:drift-evaluate (bdc845a) with main (df2466f)

Open in CodSpeed

Footnotes

  1. 16 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@jevwithwind

Copy link
Copy Markdown
Author

For metrics definitions, I used:

  1. Cerqueira et al. 2026 (preprint) since two of the authors are on the River team and it implements the protocol in CapyMOA, so claude matched it.
  2. Bifet 2017 (doi 10.1007/978-3-319-59060-8_64), which is where the four metrics in Drift module roadmap: broaden detector coverage + build an evaluation harness (6-month focus) #1914 come from.
  3. Lukats et al. (doi 10.1007/s41060-024-00620-y), recommended by @e10e3.

@jevwithwind

jevwithwind commented Jul 27, 2026

Copy link
Copy Markdown
Author

To continue, I want to know: does this belong in river/drift/ or river/evaluate/?
And should delta_max should keep having no default following Cerqueira et al. 2026?
Also any clue on how to proceed with the API side.

river/test_docs.py parses the docstring of every public class and
function and requires each signature parameter to appear in a
Parameters section. The metric functions only had a description and
examples, so the parse failed and CI went red on online-ml#1963.

Adds Parameters sections to the eleven functions that lacked one, and
turns Matching into a typing.NamedTuple so its fields can carry a
docstring too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@e10e3

e10e3 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Thank you for the contribution!

If all additions here are metrics, maybe they should go to the metrics module, or perhaps one of its submodule?
If you chose this option, I shall note that all metrics (and much of River's API) are implemented as classes, whereas yours are functions. To integrate them in the metrics, you will need to turn them into classes.

metrics already has metrics with the same name as yours (F1, Precision, Recall…), is there no way to reuse them?

The article from Cerqueira et al. that you mention says that $\delta_\mathrm{pre}$ should be 0 only for synthetic streams because of how they're constructed, so 0 is not a good value for all possible streams. If you need to have defaults for $\delta_\mathrm{pre}$ and $\delta_\mathrm{max}$, you could consider what other papers use. Bifet's paper appears to use $\delta_\mathrm{pre} = 0$ and $\delta_\mathrm{max} = +\infty$ (this shows that both papers have differing evaluation methodologies).

You implementation relies on inputing Iterables of results. Does this work well with River's iterative behaviour? We don't want to need to store all of a model's predictions to evaluate it. The current metrics work the following may:

for x, y in dataset:
    y_pred = model.predict_one(x)
    metric.update(y, y_pred)
   model.learn_one(x, y)

Where, at every step, we update the metric with the model's output, so we only need to store the aggregate and not the full output series of the model. This also allows us to get a reading of the metric at any time without additional computation.
This way of working is central to River, and we want to preserve it.

@jevwithwind

Copy link
Copy Markdown
Author

Thank you for the reply! Yes, metrics should be in classes not functions, and putting them in metrics makes sense. δpre default to 0 should be removed too.

Regarding the Iterables, understood, the code is fixable to fit the existing frame. The drift schedule is known in advance so each step can be classified as it arrives.

Regarding the existing metrics, F1, precision, recall can be reused but episode recall counts windows not alarms. NDT and the rest are not classification metrics. To reuse them, I would add a matching step (alarm stream -> drift schedule -> TP/FP/FN under a tolerance window) before reusing the existing arithmetic.

Claude also made a mistake with Bifet's paper on how it counts drifts. I will start with the rework shortly.

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.

2 participants