Add chance-aware detection metrics - #739
be-student wants to merge 5 commits into
Conversation
mateenali66
left a comment
There was a problem hiding this comment.
the shared helper is weaker than the deepsvdd guard it generalises, and it lets the degenerate case through about half the time.
test_deepsvdd.py::test_scores_are_not_constant counts uniques after np.round(scores, 8), and the comment there says why, "a ROC floor can still be met by floating-point noise in the ordering". assert_scores_separate_labels drops the rounding and then uses auc > 0.5, which is that floor.
constant detector, 300 samples, 10% contamination:
s = np.full(n, 0.5) + rng.normal(0, 1e-17, n)np.unique(s).size is 2, since float64 spacing near 0.5 is about 1.1e-16 and the noise nudges a couple of samples onto the next representable value. that clears > 1. auc then falls either side of 0.5 by luck. across 2000 seeds assert_scores_separate_labels accepts this detector on 970 of them, 48.5%. the deepsvdd guard rejects all 2000, because rounding to 8dp collapses it back to one unique value.
the fix is already in this PR. assert detection_lift with a margin instead of auc > 0.5. lift is prevalence-baselined, so one threshold means the same thing across all seven fixtures, and it is the metric the PR is adding. carrying over the np.round(scores, 8) from deepsvdd closes the constant check.
| from .utility import check_parameter | ||
| from .utility import precision_n_scores | ||
| from .utility import check_parameter | ||
| from .utility import detection_lift |
There was a problem hiding this comment.
noticed this file isCRLF throughout, so every untouched or removed line carries a trailing \r. Butthe 3 new import lines here and the new lines further down in evaluate_print are LF-only. Worth normalising, otherwise the diff carries mixed line endings forward, future edits to this file could show spurious changes on every line
reproduced this with afresh RNG (numpy Generator, 2000 seeds), same setup, n=300, 10% contamination, noise at 1e-17 around 0.5. Raw np.unique(scores).size > 1 passed 1114/2000, 55.7%. Rounding to 8dp first passed 0/2000, matching the deepsvdd guard. Numbers differ a bit from yours, likely RNG choice, mechanism and conclusion both hold. So yes, assert detection_lift with a margin plus the round(scores, 8) step, it's prevalence-baselined so one threshold means the same thing across all 7 |
mateenali66
left a comment
There was a problem hiding this comment.
The lift bound lets a random scorer through. Pure random scores clear > 1.1 on 54.4% of runs against the four n=300 fixtures and 36.0% against the three n=500 ones, 20k trials each. Lift is quantised by the outlier count, so > 1.1 is really "one hit in the top 15", or two in the top 25.
Measured what the detectors actually score: ts_od 3.0, kshape 6.8, sand 10.2, matrix_profile 10.9, and spectral_residual, lstm and anomaly_transformer all 20.0. So > 2.7 passes all seven and drops random to about 3%.
ts_od at 3.0 is the floor and 2.7 sits close under it. Raise the bound, or grow the fixtures so a bound has somewhere to sit?
The round(scores, 8) half is doing its job. Constant detector 0 of 2000, against 1109 and 1471 before.
|
Thanks for quantifying the false-accept rate. I raised the shared lift bound from |
|
The first new-head matrix exposed a real issue in the TimeSeriesOD quality test: on macOS/Python 3.9 and Ubuntu/Python 3.10, its 20-sample window spread isolated point anomalies enough for the lift to fall below the new |
Coverage Report for CI Build 36094929379Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.08%) to 92.657%Details
Uncovered Changes
Coverage Regressions48 previously-covered lines in 16 files lost coverage.
Coverage Stats
💛 - Coveralls |
|
The replacement run for |
Closes #736.
Problem
evaluate_printreports precision at rank n without its chance-level baseline, so the same precision value can mean different things at different outlier prevalences. Its old test passed scaled labels as scores, while seven time-series detector tests did not verify that scores separated their fixture labels.Changes
detection_lift, defined as precision at rank n divided by outlier prevalence. Return0.0with no outliers and reject empty ground truth.evaluate_print; makeprecision_n_scoresreturn zero without anUndefinedMetricWarningwhen no positives are predicted.detection_lift > 2.7.pyod/utils/data.pyline endings to avoid a mixed CRLF/LF diff.Review follow-up
The reviewer measured that the former
> 1.1threshold accepted random scores too often. Commit577f1earaises it to> 2.7. In fresh seeded simulations with 10,000 random scorers per fixture size, acceptance fell from 54.57% to 3.36% at n=300 and from 35.49% to 3.02% at n=500. The three previously tabulated subsequence detector lifts are 6.75 (KShape), 10.91 (MatrixProfile), and 10.23 (SAND).The first hosted run at
577f1eafound one further fixture mismatch:TimeSeriesOD::test_fit_univariateuses isolated point anomalies but had a 20-sample scoring window. Its lift sometimes fell below 2.7 on macOS/Python 3.9 and Ubuntu/Python 3.10. Commit0d5ba5241108ff89b87bd18ce766a49ead3e6675changes only that test's window to 10 samples, preserving the detector, labels, and strict bound. Across 100 seeded detector runs on each of local Python 3.9 and 3.10, the minimum lift was 4.0, with no bound failures. This keeps the quality assertion meaningful while matching its point-anomaly resolution.An earlier head failed the KShape, MatrixProfile, and SAND tests across the hosted matrix because point-only fixtures with 20-sample windows produced broad tied score plateaus. The corrected subsequence fixtures passed all eleven Linux, Windows, and macOS matrix jobs, packaging, and CodeQL at head
4696fbe. Those hosted results predate the threshold increase and are not claimed for the new head.Validation
0d5ba52on macOS/Python 3.14.3, 57 tests across the seven time-series detector modules and utility tests passed with PyTorch installed.git diff --checkpassed.0d5ba52: eleven Linux, Windows, and macOS matrix jobs, packaging, CodeQL, and Python analysis. The earlier577f1earun's macOS/Python 3.9 and Ubuntu/Python 3.10 failures came from the 20-sample TimeSeriesOD fixture described above.AI assistance disclosure
OpenAI Codex assisted with implementation, CI-log analysis, review-feedback remediation, and automated testing. No manual-testing or human-review claim is made.