From d1cc0e8b90596cd64db089945777f0e1786b9166 Mon Sep 17 00:00:00 2001 From: Fan Bao Date: Tue, 5 May 2026 14:02:56 -0700 Subject: [PATCH 1/3] Fix SR-CNN missing anomalies when Period > 0 (#5891) 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. --- .../SrCnnEntireAnomalyDetector.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.ML.TimeSeries/SrCnnEntireAnomalyDetector.cs b/src/Microsoft.ML.TimeSeries/SrCnnEntireAnomalyDetector.cs index 58b5442e13..233bc84c25 100644 --- a/src/Microsoft.ML.TimeSeries/SrCnnEntireAnomalyDetector.cs +++ b/src/Microsoft.ML.TimeSeries/SrCnnEntireAnomalyDetector.cs @@ -496,9 +496,6 @@ public void Train(double[] values, ref double[][] results) _minimumOriginValue = Double.MaxValue; _maximumOriginValue = Double.MinValue; - var sum = 0.0; - var squareSum = 0.0; - Array.Resize(ref _seriesToDetect, values.Length); for (int i = 0; i < values.Length; ++i) { @@ -506,18 +503,25 @@ public void Train(double[] values, ref double[][] results) _seriesToDetect[i] = value; _minimumOriginValue = Math.Min(_minimumOriginValue, value); _maximumOriginValue = Math.Max(_maximumOriginValue, value); - sum += value; - squareSum += value * value; } - _mean = sum / values.Length; - _std = Math.Sqrt((squareSum - (sum * sum) / values.Length) / values.Length); - if (_period > 0) { _deseasonalityFunction.Deseasonality(ref values, _period, ref _seriesToDetect); } + var sum = 0.0; + var squareSum = 0.0; + for (int i = 0; i < values.Length; ++i) + { + var value = _seriesToDetect[i]; + sum += value; + squareSum += value * value; + } + + _mean = sum / values.Length; + _std = Math.Sqrt((squareSum - (sum * sum) / values.Length) / values.Length); + SpectralResidual(_seriesToDetect, results, _threshold); //Optional Steps @@ -812,7 +816,7 @@ private void GetMarginPeriod(double[] values, double[][] results, IReadOnlyList< //Step 11: Update Anomaly Score, Expected Value and Boundaries for (int i = 0; i < results.Length; ++i) { - results[i][1] = CalculateAnomalyScore(values[i], _ifftRe[i], _units[i], results[i][0] > 0); + results[i][1] = CalculateAnomalyScore(values[i], results[i][3], _units[i], results[i][0] > 0); // adjust the expected value if the point is not anomaly if (results[i][0] == 0) From 645d952767ff4eb6e416555980d1e2d3c405b382 Mon Sep 17 00:00:00 2001 From: Fan Bao Date: Tue, 5 May 2026 14:03:08 -0700 Subject: [PATCH 2/3] Add SR-CNN phone-calls regression test (#5891) 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. --- .../TimeSeriesDirectApi.cs | 58 ++++++++++++++ test/data/Timeseries/phone-calls.csv | 79 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 test/data/Timeseries/phone-calls.csv diff --git a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs index ce727c5daa..0b673ffb25 100644 --- a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs +++ b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs @@ -772,6 +772,64 @@ public void TestSrCnnAnomalyDetectorBigSpike( } + private sealed class PhoneCallsData + { + [LoadColumn(0)] + public string timestamp; + + [LoadColumn(1)] + public double value; + } + + private sealed class PhoneCallsPrediction + { + [VectorType(7)] + public double[] Prediction { get; set; } + } + + [NativeDependencyFact("MklImports")] + public void TestSrCnnAnomalyDetectorPhoneCalls() + { + var mlContext = new MLContext(1); + + var dataPath = GetDataPath("Timeseries", "phone-calls.csv"); + + IDataView dataView = mlContext.Data.LoadFromTextFile(path: dataPath, hasHeader: true, separatorChar: ','); + + int period = mlContext.AnomalyDetection.DetectSeasonality(dataView, nameof(PhoneCallsData.value)); + Assert.Equal(7, period); + + var options = new SrCnnEntireAnomalyDetectorOptions() + { + Threshold = 0.3, + Sensitivity = 87.0, + DetectMode = SrCnnDetectMode.AnomalyAndMargin, + Period = period, + }; + + var outputDataView = mlContext.AnomalyDetection.DetectEntireAnomalyBySrCnn(dataView, nameof(PhoneCallsPrediction.Prediction), nameof(PhoneCallsData.value), options); + + var predictions = mlContext.Data.CreateEnumerable( + outputDataView, reuseRowObject: false); + + var anomalyIndices = new HashSet { 28, 44, 56, 70 }; + + int k = 0; + foreach (var prediction in predictions) + { + if (anomalyIndices.Contains(k)) + { + Assert.Equal(1, prediction.Prediction[0]); + } + else + { + Assert.Equal(0, prediction.Prediction[0]); + } + + ++k; + } + } + [NativeDependencyTheory("MklImports"), CombinatorialData] public void TestSrCnnAnomalyDetectorWithSeasonalAnomalyData( [CombinatorialValues(SrCnnDeseasonalityMode.Stl, SrCnnDeseasonalityMode.Mean, SrCnnDeseasonalityMode.Median)] SrCnnDeseasonalityMode mode diff --git a/test/data/Timeseries/phone-calls.csv b/test/data/Timeseries/phone-calls.csv new file mode 100644 index 0000000000..a9d0e2da0a --- /dev/null +++ b/test/data/Timeseries/phone-calls.csv @@ -0,0 +1,79 @@ +timestamp,value +2018/9/3,36.69670857 +2018/9/4,35.74160571 +2018/9/5,34.11781143 +2018/9/6,33.53363571 +2018/9/7,29.19957714 +2018/9/8,5.18315 +2018/9/9,5.324905714 +2018/9/10,36.69670857 +2018/9/11,35.74160571 +2018/9/12,34.11781143 +2018/9/13,33.53363571 +2018/9/14,29.19957714 +2018/9/15,5.18315 +2018/9/16,5.324905714 +2018/9/17,36.69670857 +2018/9/18,35.74160571 +2018/9/19,34.11781143 +2018/9/20,33.53363571 +2018/9/21,29.19957714 +2018/9/22,5.18315 +2018/9/23,5.324905714 +2018/9/24,36.69670857 +2018/9/25,35.74160571 +2018/9/26,34.11781143 +2018/9/27,33.53363571 +2018/9/28,29.19957714 +2018/9/29,5.18315 +2018/9/30,5.324905714 +2018/10/1,31.34386429 +2018/10/2,36.18100429 +2018/10/3,34.49893429 +2018/10/4,34.18594143 +2018/10/5,29.96867143 +2018/10/6,5.240491429 +2018/10/7,5.304298571 +2018/10/8,37.94839429 +2018/10/9,36.3811 +2018/10/10,35.76107429 +2018/10/11,35.28894143 +2018/10/12,31.08465286 +2018/10/13,5.931802857 +2018/10/14,5.476382857 +2018/10/15,36.23001857 +2018/10/16,35.51714 +2018/10/17,21.95187143 +2018/10/18,31.15111714 +2018/10/19,28.21996429 +2018/10/20,4.532814286 +2018/10/21,5.376088571 +2018/10/22,36.19748286 +2018/10/23,36.05797571 +2018/10/24,35.05092286 +2018/10/25,34.78529 +2018/10/26,31.19532857 +2018/10/27,5.655607143 +2018/10/28,5.925987143 +2018/10/29,26.61867857 +2018/10/30,35.35089571 +2018/10/31,34.09542571 +2018/11/1,28.74181 +2018/11/2,28.20479429 +2018/11/3,4.849405714 +2018/11/4,5.444168571 +2018/11/5,33.21586857 +2018/11/6,35.69544714 +2018/11/7,34.79379714 +2018/11/8,34.26969714 +2018/11/9,30.07392 +2018/11/10,3.18219 +2018/11/11,3.964938571 +2018/11/12,45.21586857 +2018/11/13,35.69544714 +2018/11/14,34.79379714 +2018/11/15,34.26969714 +2018/11/16,30.07392 +2018/11/17,5.266295714 +2018/11/18,5.386695714 +2018/11/19,33.80200857 From 197a85992373ebb377bc04bab236b7db3b84a9fe Mon Sep 17 00:00:00 2001 From: Fan Bao Date: Tue, 5 May 2026 15:04:12 -0700 Subject: [PATCH 3/3] Suppress CS0649 on PhoneCallsData reflection-assigned fields --- test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs index 0b673ffb25..520e7fbab0 100644 --- a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs +++ b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesDirectApi.cs @@ -774,11 +774,13 @@ public void TestSrCnnAnomalyDetectorBigSpike( private sealed class PhoneCallsData { +#pragma warning disable CS0649 [LoadColumn(0)] public string timestamp; [LoadColumn(1)] public double value; +#pragma warning restore CS0649 } private sealed class PhoneCallsPrediction