Skip to content

check_roc_auc passes for a detector that returns a constant score #1992

Description

@mateenali66

check_roc_auc is the detection-quality check that runs for every AnomalyDetector via check_estimator, and it asserts roc_auc_score(labels, scores) >= 0.5. A detector that returns the same score for every sample gets exactly 0.5, so it passes.

from river import datasets
from river.base import AnomalyDetector
from river.checks.anomaly import check_roc_auc

class Constant(AnomalyDetector):
    def learn_one(self, x):
        pass
    def score_one(self, x):
        return 0.5

check_roc_auc(Constant(), datasets.CreditCard().take(1000))  # passes

Two things compound here.

The bar is inclusive and the degenerate case lands exactly on it. sklearn returns 0.5 for all-tied scores, so the check cannot tell "rank anomalies above normal points" (its docstring) from "ignores the input".

The fixture is CreditCard().take(1000), which holds 2 positive labels. With 2 positives the check has almost no power against a random scorer either: uniform random scores pass on 108 of 200 seeds (river 0.26.1, numpy default_rng, seeds 0 to 199). CreditCard's prevalence is 0.173%, and its first 20 positives happen to sit inside the first 6,821 rows, so take(7000) is enough to give the assertion some power.

Two changes would close this:

  1. Make the bound strict. A constant scorer lands on exactly 0.5, so > 0.5 already excludes the degenerate case. On its own that stays brittle with 2 positives, which is why the fixture matters more.
  2. Enlarge the fixture so it holds enough positives. Runtime is not a concern, 6,821 rows run in well under a second per detector, and the memory check already streams 10,000.

One caveat to be upfront about. On the first 6,821 rows a bare OneClassSVM() scores AUC 0.16 while HalfSpaceTrees scores 0.85, so the longer fixture surfaces a real below-chance case rather than staying green. That is arguably the check doing its job, but it means the change is not a pure test tweak. This also does not collide with #1970, which rewrites the file but keeps both the >= 0.5 bound and the 1,000-row fixture.

This is the anomaly-module version of the baseline point I raised on #1914. I benchmark anomaly detectors on OpenTelemetry infrastructure data (writeup: https://doi.org/10.1109/ACCESS.2026.3705430) and the recurring result is that detectors which never beat a data-ignoring baseline still look fine on evaluations without one.

Happy to open a PR if this direction works for you.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions