Repository navigation
Fix SR-CNN missing anomalies when Period > 0 - #7610
Conversation
In the period > 0 path, two domain-mismatch bugs caused real anomalies to be suppressed: 1. SrCnnEntireModeler.Train computed _mean / _std from the raw input series before deseasonality ran. SpectralResidual then compared the deseasonalized residuals in _seriesToDetect against those raw-domain statistics, producing meaningless z-scores in the false-anomaly filter. Move the sum/mean/std computation below the deseasonality call and compute the statistics against _seriesToDetect. 2. SrCnnEntireModeler.GetMarginPeriod passed _ifftRe[i] to CalculateAnomalyScore as the expected value. _ifftRe in this path is the SR saliency map's real component computed from the deseasonalized residual series, not a raw-domain expected value. CalculateAnomalyScore expects exp and value in the same domain, so the resulting score was proportional to raw magnitude rather than deviation from expectation. Pass results[i][3] (the raw-domain expected value produced by GetExpectedValuePeriod above) instead. The no-period path (Train with _period == 0 and GetMargin) is correct as written and is unchanged.
Adds TestSrCnnAnomalyDetectorPhoneCalls, a regression test based on
the dotnet/samples PhoneCallsAnomalyDetection tutorial. The test data
file is taken verbatim from the dotnet/samples repository.
DetectSeasonality is asserted to return period 7 (the tutorial's
documented output). DetectEntireAnomalyBySrCnn is then expected to
flag indices {28, 44, 56, 70} and only those. Prior to the
SrCnnEntireModeler period-path fix this test detects no anomalies
on the same input.
Sensitivity = 87.0 is used to obtain the boundary width the tutorial
was originally calibrated for under the v1.5.2 _factors table; the
table was rewritten in a later release so the tutorial's literal
Sensitivity = 64.0 now produces a much wider boundary.
|
@dotnet-policy-service agree company="Microsoft" |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7610 +/- ##
==========================================
+ Coverage 69.88% 69.90% +0.02%
==========================================
Files 1487 1487
Lines 276253 276294 +41
Branches 28291 28294 +3
==========================================
+ Hits 193053 193138 +85
+ Misses 75710 75671 -39
+ Partials 7490 7485 -5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Assert anomaly scores, normal-point scores, output width, and row count so the test protects both domain fixes. Explain the sensitivity calibration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
matouskozak
left a comment
There was a problem hiding this comment.
LGTM! I updated the test a bit to test the fixed behavior.
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The domain mismatches are corrected consistently and covered by a targeted end-to-end regression test.
Review effort: Balanced
Findings: None
What changed in this PR
Fixes SR-CNN anomaly suppression for seasonal data by keeping statistical comparisons and anomaly scoring within consistent domains.
Changes:
- Computes z-score statistics from deseasonalized data.
- Scores anomalies against the raw-domain expected value.
- Adds a phone-call dataset regression test.
| File | Description |
|---|---|
src/Microsoft.ML.TimeSeries/SrCnnEntireAnomalyDetector.cs |
Corrects seasonal anomaly filtering and scoring. |
test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs |
Adds regression coverage for detected anomalies and scores. |
test/data/Timeseries/phone-calls.csv |
Provides seasonal test data. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
We are excited to review your PR.
So we can do the best job, please check:
Fixes #nnnnin your description to cause GitHub to automatically close the issue(s) when your PR is merged.Fixes #5891 - DetectEntireAnomalyBySrCnn - No anomalies detected as of version 1.5.4 or greater
Root cause
Two domain-mismatch bugs in the
Period > 0code path ofSrCnnEntireModeler:1.
Train()— z-score statistics computed in the wrong domain._mean/_stdwere computed from the raw input series before deseasonality ran. The z-score "false anomaly" filter insideSpectralResidualthen compared_seriesToDetect[i](the deseasonalized residual) against those raw-domain stats, producing meaningless z-scores that suppressed real anomalies.2.
GetMarginPeriod()—CalculateAnomalyScorecalled withexpin the wrong domain._ifftRe[i]was passed as the expected-value argument.CalculateAnomalyScorecomputes|exp - value| / unit, which is only meaningful when both arguments share a domain.In the no-period sibling
GetMargin(), this call is correct:CalculateExpectedValueByFft(_deAnomalyData)is invoked immediately beforehand and overwrites_ifftRewith the raw-domain expected value of the de-anomalized series — so_ifftRe[i]andvalues[i]share a domain there.GetMarginPeriod()was added later (#5202) and reused the same call shape without that prior overwrite. In the period > 0 path_ifftReholds the SR saliency map's real component computed on the deseasonalized residual series, not a raw-domain expected value. The resulting score collapses to roughly|raw_value| / unit— proportional to raw magnitude rather than deviation from expectation — so real anomalies often score near zero. This looks like a copy-paste of the no-period call site without re-deriving what_ifftReactually holds in the new path.Fix
sum/_mean/_stdcomputation inTrain()to after the deseasonality call and compute the statistics against_seriesToDetect(the same series SR will actually score).GetMarginPeriod()passresults[i][3](the raw-domain expected value just produced byGetExpectedValuePeriod) instead of_ifftRe[i].The no-period path (
Trainwith_period == 0andGetMargin) is correct as written and is unchanged.Test
Adds
TestSrCnnAnomalyDetectorPhoneCalls, ported from the dotnet/samples PhoneCallsAnomalyDetection tutorial. Data file is taken verbatim from dotnet/samples.The test asserts
DetectSeasonalityreturns 7 and thatDetectEntireAnomalyBySrCnnflags exactly indices{28, 44, 56, 70}. Prior to this fix the same call detects zero anomalies on the same input.Sensitivity = 87.0is used to obtain the boundary width the tutorial was originally calibrated for under the v1.5.2_factorstable; the table was rewritten in #5436, so the tutorial's literalSensitivity = 64.0now produces a much wider boundary that masks all but the strongest spike at index 28.