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:
- 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.
- 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.
check_roc_aucis the detection-quality check that runs for everyAnomalyDetectorviacheck_estimator, and it assertsroc_auc_score(labels, scores) >= 0.5. A detector that returns the same score for every sample gets exactly 0.5, so it 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, numpydefault_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, sotake(7000)is enough to give the assertion some power.Two changes would close this:
> 0.5already excludes the degenerate case. On its own that stays brittle with 2 positives, which is why the fixture matters more.One caveat to be upfront about. On the first 6,821 rows a bare
OneClassSVM()scores AUC 0.16 whileHalfSpaceTreesscores 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.5bound 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.