diff --git a/Microsoft.ML.sln b/Microsoft.ML.sln
index 3aa700eb02..7b7365cfd5 100644
--- a/Microsoft.ML.sln
+++ b/Microsoft.ML.sln
@@ -147,6 +147,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.OnnxTransform.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.LightGBM.StaticPipe", "src\Microsoft.ML.LightGBM.StaticPipe\Microsoft.ML.LightGBM.StaticPipe.csproj", "{22C51B08-ACAE-47B2-A312-462DC239A23B}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.TimeSeries.StaticPipe", "src\Microsoft.ML.TimeSeries.StaticPipe\Microsoft.ML.TimeSeries.StaticPipe.csproj", "{06A147ED-15EA-4106-9105-9B745125B470}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -781,6 +783,18 @@ Global
{22C51B08-ACAE-47B2-A312-462DC239A23B}.Release-Intrinsics|Any CPU.Build.0 = Release-Intrinsics|Any CPU
{22C51B08-ACAE-47B2-A312-462DC239A23B}.Release-netfx|Any CPU.ActiveCfg = Release-netfx|Any CPU
{22C51B08-ACAE-47B2-A312-462DC239A23B}.Release-netfx|Any CPU.Build.0 = Release-netfx|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Debug-Intrinsics|Any CPU.ActiveCfg = Debug-Intrinsics|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Debug-Intrinsics|Any CPU.Build.0 = Debug-Intrinsics|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Debug-netfx|Any CPU.ActiveCfg = Debug-netfx|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Debug-netfx|Any CPU.Build.0 = Debug-netfx|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Release|Any CPU.Build.0 = Release|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Release-Intrinsics|Any CPU.ActiveCfg = Release-Intrinsics|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Release-Intrinsics|Any CPU.Build.0 = Release-Intrinsics|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Release-netfx|Any CPU.ActiveCfg = Release-netfx|Any CPU
+ {06A147ED-15EA-4106-9105-9B745125B470}.Release-netfx|Any CPU.Build.0 = Release-netfx|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -843,6 +857,7 @@ Global
{2F25EF6A-C754-45BE-AD9E-7DDF46A1B51A} = {09EADF06-BE25-4228-AB53-95AE3E15B530}
{D1324668-9568-40F4-AA55-30A9A516C230} = {09EADF06-BE25-4228-AB53-95AE3E15B530}
{22C51B08-ACAE-47B2-A312-462DC239A23B} = {09EADF06-BE25-4228-AB53-95AE3E15B530}
+ {06A147ED-15EA-4106-9105-9B745125B470} = {09EADF06-BE25-4228-AB53-95AE3E15B530}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {41165AF1-35BB-4832-A189-73060F82B01D}
diff --git a/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs b/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs
index 2cc96116b6..4c76446e5a 100644
--- a/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs
@@ -45,6 +45,7 @@
[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.HalLearners.StaticPipe" + PublicKey.Value)]
[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.OnnxTransform.StaticPipe" + PublicKey.Value)]
[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.LightGBM.StaticPipe" + PublicKey.Value)]
+[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.TimeSeries.StaticPipe" + PublicKey.Value)]
[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Internal.MetaLinearLearner" + InternalPublicKey.Value)]
[assembly: InternalsVisibleTo(assemblyName: "TreeVisualizer" + InternalPublicKey.Value)]
diff --git a/src/Microsoft.ML.TimeSeries.StaticPipe/Microsoft.ML.TimeSeries.StaticPipe.csproj b/src/Microsoft.ML.TimeSeries.StaticPipe/Microsoft.ML.TimeSeries.StaticPipe.csproj
new file mode 100644
index 0000000000..08eaf4f029
--- /dev/null
+++ b/src/Microsoft.ML.TimeSeries.StaticPipe/Microsoft.ML.TimeSeries.StaticPipe.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.ML.TimeSeries.StaticPipe/TimeSeriesStatic.cs b/src/Microsoft.ML.TimeSeries.StaticPipe/TimeSeriesStatic.cs
new file mode 100644
index 0000000000..5bb29f7f99
--- /dev/null
+++ b/src/Microsoft.ML.TimeSeries.StaticPipe/TimeSeriesStatic.cs
@@ -0,0 +1,314 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.ML.Core.Data;
+using Microsoft.ML.StaticPipe.Runtime;
+using Microsoft.ML.TimeSeriesProcessing;
+
+namespace Microsoft.ML.StaticPipe
+{
+ using IidBase = Microsoft.ML.TimeSeriesProcessing.SequentialAnomalyDetectionTransformBase;
+ using SsaBase = Microsoft.ML.TimeSeriesProcessing.SequentialAnomalyDetectionTransformBase;
+
+ ///
+ /// Static API extension methods for .
+ ///
+ public static class IidChangePointStaticExtensions
+ {
+ private sealed class OutColumn : Vector
+ {
+ public PipelineColumn Input { get; }
+
+ public OutColumn(
+ Scalar input,
+ int confidence,
+ int changeHistoryLength,
+ IidBase.MartingaleType martingale,
+ double eps)
+ : base(new Reconciler(confidence, changeHistoryLength, martingale, eps), input)
+ {
+ Input = input;
+ }
+ }
+
+ private sealed class Reconciler : EstimatorReconciler
+ {
+ private readonly int _confidence;
+ private readonly int _changeHistoryLength;
+ private readonly IidBase.MartingaleType _martingale;
+ private readonly double _eps;
+
+ public Reconciler(
+ int confidence,
+ int changeHistoryLength,
+ IidBase.MartingaleType martingale,
+ double eps)
+ {
+ _confidence = confidence;
+ _changeHistoryLength = changeHistoryLength;
+ _martingale = martingale;
+ _eps = eps;
+ }
+
+ public override IEstimator Reconcile(IHostEnvironment env,
+ PipelineColumn[] toOutput,
+ IReadOnlyDictionary inputNames,
+ IReadOnlyDictionary outputNames,
+ IReadOnlyCollection usedNames)
+ {
+ Contracts.Assert(toOutput.Length == 1);
+ var outCol = (OutColumn)toOutput[0];
+ return new IidChangePointEstimator(env,
+ inputNames[outCol.Input],
+ outputNames[outCol],
+ _confidence,
+ _changeHistoryLength,
+ _martingale,
+ _eps);
+ }
+ }
+
+ ///
+ /// Perform IID change point detection over a column of time series data. See .
+ ///
+ public static Vector IidChangePointDetect(
+ this Scalar input,
+ int confidence,
+ int changeHistoryLength,
+ IidBase.MartingaleType martingale = IidBase.MartingaleType.Power,
+ double eps = 0.1) => new OutColumn(input, confidence, changeHistoryLength, martingale, eps);
+ }
+
+ ///
+ /// Static API extension methods for .
+ ///
+ public static class IidSpikeDetectorStaticExtensions
+ {
+ private sealed class OutColumn : Vector
+ {
+ public PipelineColumn Input { get; }
+
+ public OutColumn(Scalar input,
+ int confidence,
+ int pvalueHistoryLength,
+ IidBase.AnomalySide side)
+ : base(new Reconciler(confidence, pvalueHistoryLength, side), input)
+ {
+ Input = input;
+ }
+ }
+
+ private sealed class Reconciler : EstimatorReconciler
+ {
+ private readonly int _confidence;
+ private readonly int _pvalueHistoryLength;
+ private readonly IidBase.AnomalySide _side;
+
+ public Reconciler(
+ int confidence,
+ int pvalueHistoryLength,
+ IidBase.AnomalySide side)
+ {
+ _confidence = confidence;
+ _pvalueHistoryLength = pvalueHistoryLength;
+ _side = side;
+ }
+
+ public override IEstimator Reconcile(IHostEnvironment env,
+ PipelineColumn[] toOutput,
+ IReadOnlyDictionary inputNames,
+ IReadOnlyDictionary outputNames,
+ IReadOnlyCollection usedNames)
+ {
+ Contracts.Assert(toOutput.Length == 1);
+ var outCol = (OutColumn)toOutput[0];
+ return new IidSpikeEstimator(env,
+ inputNames[outCol.Input],
+ outputNames[outCol],
+ _confidence,
+ _pvalueHistoryLength,
+ _side);
+ }
+ }
+
+ ///
+ /// Perform IID spike detection over a column of time series data. See .
+ ///
+ public static Vector IidSpikeDetect(
+ this Scalar input,
+ int confidence,
+ int pvalueHistoryLength,
+ IidBase.AnomalySide side = IidBase.AnomalySide.TwoSided
+ ) => new OutColumn(input, confidence, pvalueHistoryLength, side);
+ }
+
+ ///
+ /// Static API extension methods for .
+ ///
+ public static class SsaChangePointStaticExtensions
+ {
+ private sealed class OutColumn : Vector
+ {
+ public PipelineColumn Input { get; }
+
+ public OutColumn(Scalar input,
+ int confidence,
+ int changeHistoryLength,
+ int trainingWindowSize,
+ int seasonalityWindowSize,
+ ErrorFunctionUtils.ErrorFunction errorFunction,
+ SsaBase.MartingaleType martingale,
+ double eps)
+ : base(new Reconciler(confidence, changeHistoryLength, trainingWindowSize, seasonalityWindowSize, errorFunction, martingale, eps), input)
+ {
+ Input = input;
+ }
+ }
+
+ private sealed class Reconciler : EstimatorReconciler
+ {
+ private readonly int _confidence;
+ private readonly int _changeHistoryLength;
+ private readonly int _trainingWindowSize;
+ private readonly int _seasonalityWindowSize;
+ private readonly ErrorFunctionUtils.ErrorFunction _errorFunction;
+ private readonly SsaBase.MartingaleType _martingale;
+ private readonly double _eps;
+
+ public Reconciler(
+ int confidence,
+ int changeHistoryLength,
+ int trainingWindowSize,
+ int seasonalityWindowSize,
+ ErrorFunctionUtils.ErrorFunction errorFunction,
+ SsaBase.MartingaleType martingale,
+ double eps)
+ {
+ _confidence = confidence;
+ _changeHistoryLength = changeHistoryLength;
+ _trainingWindowSize = trainingWindowSize;
+ _seasonalityWindowSize = seasonalityWindowSize;
+ _errorFunction = errorFunction;
+ _martingale = martingale;
+ _eps = eps;
+ }
+
+ public override IEstimator Reconcile(IHostEnvironment env,
+ PipelineColumn[] toOutput,
+ IReadOnlyDictionary inputNames,
+ IReadOnlyDictionary outputNames,
+ IReadOnlyCollection usedNames)
+ {
+ Contracts.Assert(toOutput.Length == 1);
+ var outCol = (OutColumn)toOutput[0];
+ return new SsaChangePointEstimator(env,
+ inputNames[outCol.Input],
+ outputNames[outCol],
+ _confidence,
+ _changeHistoryLength,
+ _trainingWindowSize,
+ _seasonalityWindowSize,
+ _errorFunction,
+ _martingale,
+ _eps);
+ }
+ }
+
+ ///
+ /// Perform SSA change point detection over a column of time series data. See .
+ ///
+ public static Vector SsaChangePointDetect(
+ this Scalar input,
+ int confidence,
+ int changeHistoryLength,
+ int trainingWindowSize,
+ int seasonalityWindowSize,
+ ErrorFunctionUtils.ErrorFunction errorFunction = ErrorFunctionUtils.ErrorFunction.SignedDifference,
+ SsaBase.MartingaleType martingale = SsaBase.MartingaleType.Power,
+ double eps = 0.1) => new OutColumn(input, confidence, changeHistoryLength, trainingWindowSize, seasonalityWindowSize, errorFunction, martingale, eps);
+ }
+
+ ///
+ /// Static API extension methods for .
+ ///
+ public static class SsaSpikeDetecotStaticExtensions
+ {
+ private sealed class OutColumn : Vector
+ {
+ public PipelineColumn Input { get; }
+
+ public OutColumn(Scalar input,
+ int confidence,
+ int pvalueHistoryLength,
+ int trainingWindowSize,
+ int seasonalityWindowSize,
+ SsaBase.AnomalySide side,
+ ErrorFunctionUtils.ErrorFunction errorFunction)
+ : base(new Reconciler(confidence, pvalueHistoryLength, trainingWindowSize, seasonalityWindowSize, side, errorFunction), input)
+ {
+ Input = input;
+ }
+ }
+
+ private sealed class Reconciler : EstimatorReconciler
+ {
+ private readonly int _confidence;
+ private readonly int _pvalueHistoryLength;
+ private readonly int _trainingWindowSize;
+ private readonly int _seasonalityWindowSize;
+ private readonly SsaBase.AnomalySide _side;
+ private readonly ErrorFunctionUtils.ErrorFunction _errorFunction;
+
+ public Reconciler(
+ int confidence,
+ int pvalueHistoryLength,
+ int trainingWindowSize,
+ int seasonalityWindowSize,
+ SsaBase.AnomalySide side,
+ ErrorFunctionUtils.ErrorFunction errorFunction)
+ {
+ _confidence = confidence;
+ _pvalueHistoryLength = pvalueHistoryLength;
+ _trainingWindowSize = trainingWindowSize;
+ _seasonalityWindowSize = seasonalityWindowSize;
+ _side = side;
+ _errorFunction = errorFunction;
+ }
+
+ public override IEstimator Reconcile(IHostEnvironment env,
+ PipelineColumn[] toOutput,
+ IReadOnlyDictionary inputNames,
+ IReadOnlyDictionary outputNames,
+ IReadOnlyCollection usedNames)
+ {
+ Contracts.Assert(toOutput.Length == 1);
+ var outCol = (OutColumn)toOutput[0];
+ return new SsaSpikeEstimator(env,
+ inputNames[outCol.Input],
+ outputNames[outCol],
+ _confidence,
+ _pvalueHistoryLength,
+ _trainingWindowSize,
+ _seasonalityWindowSize,
+ _side,
+ _errorFunction);
+ }
+ }
+
+ ///
+ /// Perform SSA spike detection over a column of time series data. See .
+ ///
+ public static Vector SsaSpikeDetect(
+ this Scalar input,
+ int confidence,
+ int changeHistoryLength,
+ int trainingWindowSize,
+ int seasonalityWindowSize,
+ SsaBase.AnomalySide side = SsaBase.AnomalySide.TwoSided,
+ ErrorFunctionUtils.ErrorFunction errorFunction = ErrorFunctionUtils.ErrorFunction.SignedDifference
+ ) => new OutColumn(input, confidence, changeHistoryLength, trainingWindowSize, seasonalityWindowSize, side, errorFunction);
+
+ }
+}
diff --git a/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj b/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj
index bc7f32d660..9b23e12fc9 100644
--- a/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj
+++ b/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj
@@ -5,6 +5,7 @@
+
diff --git a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesStaticTests.cs b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesStaticTests.cs
new file mode 100644
index 0000000000..6e78c83bdf
--- /dev/null
+++ b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesStaticTests.cs
@@ -0,0 +1,245 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.ML.Data;
+using Microsoft.ML.RunTests;
+using Microsoft.ML.StaticPipe;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.ML.Tests
+{
+ public sealed class TimeSeriesStaticTests : BaseTestBaseline
+ {
+ public TimeSeriesStaticTests(ITestOutputHelper output) : base(output)
+ {
+ }
+#pragma warning disable CS0649 // Ignore unintialized field warning
+ private sealed class ChangePointPrediction
+ {
+ // Note that this field must be named "Data"; we ultimately convert
+ // to a dynamic IDataView in order to extract AsEnumerable
+ // predictions and that process uses "Data" as the default column
+ // name for an output column from a static pipeline.
+ [VectorType(4)]
+ public double[] Data;
+ }
+
+ private sealed class SpikePrediction
+ {
+ [VectorType(3)]
+ public double[] Data;
+ }
+#pragma warning restore CS0649
+
+ private sealed class Data
+ {
+ public float Value;
+
+ public Data(float value) => Value = value;
+ }
+
+ [Fact]
+ public void ChangeDetection()
+ {
+ var env = new MLContext(conc: 1);
+ const int Size = 10;
+ var data = new List(Size);
+ var dataView = env.CreateStreamingDataView(data);
+ for (int i = 0; i < Size / 2; i++)
+ data.Add(new Data(5));
+
+ for (int i = 0; i < Size / 2; i++)
+ data.Add(new Data((float)(5 + i * 1.1)));
+
+ // Convert to statically-typed data view.
+ var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar });
+ // Build the pipeline
+ var staticLearningPipeline = staticData.MakeNewEstimator()
+ .Append(r => r.Value.IidChangePointDetect(80, Size));
+ // Train
+ var detector = staticLearningPipeline.Fit(staticData);
+ // Transform
+ var output = detector.Transform(staticData);
+
+ // Get predictions
+ var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator();
+ ChangePointPrediction row = null;
+ List expectedValues = new List() { 0, 5, 0.5, 5.1200000000000114E-08, 0, 5, 0.4999999995, 5.1200000046080209E-08, 0, 5, 0.4999999995, 5.1200000092160303E-08,
+ 0, 5, 0.4999999995, 5.12000001382404E-08};
+ int index = 0;
+ while (enumerator.MoveNext() && index < expectedValues.Count)
+ {
+ row = enumerator.Current;
+
+ Assert.Equal(expectedValues[index++], row.Data[0], precision: 7);
+ Assert.Equal(expectedValues[index++], row.Data[1], precision: 7);
+ Assert.Equal(expectedValues[index++], row.Data[2], precision: 7);
+ Assert.Equal(expectedValues[index++], row.Data[3], precision: 7);
+ }
+ }
+
+ [Fact]
+ public void ChangePointDetectionWithSeasonality()
+ {
+ var env = new MLContext(conc: 1);
+ const int ChangeHistorySize = 10;
+ const int SeasonalitySize = 10;
+ const int NumberOfSeasonsInTraining = 5;
+ const int MaxTrainingSize = NumberOfSeasonsInTraining * SeasonalitySize;
+
+ var data = new List();
+ var dataView = env.CreateStreamingDataView(data);
+
+ for (int j = 0; j < NumberOfSeasonsInTraining; j++)
+ for (int i = 0; i < SeasonalitySize; i++)
+ data.Add(new Data(i));
+
+ for (int i = 0; i < ChangeHistorySize; i++)
+ data.Add(new Data(i * 100));
+
+ // Convert to statically-typed data view.
+ var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar });
+ // Build the pipeline
+ var staticLearningPipeline = staticData.MakeNewEstimator()
+ .Append(r => r.Value.SsaChangePointDetect(95, ChangeHistorySize, MaxTrainingSize, SeasonalitySize));
+ // Train
+ var detector = staticLearningPipeline.Fit(staticData);
+ // Transform
+ var output = detector.Transform(staticData);
+
+ // Get predictions
+ var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator();
+ ChangePointPrediction row = null;
+ List expectedValues = new List() { 0, -3.31410598754883, 0.5, 5.12000000000001E-08, 0, 1.5700820684432983, 5.2001145245395008E-07,
+ 0.012414560443710681, 0, 1.2854313254356384, 0.28810801662678009, 0.02038940454467935, 0, -1.0950627326965332, 0.36663890634019225, 0.026956459625565483};
+
+ int index = 0;
+ while (enumerator.MoveNext() && index < expectedValues.Count)
+ {
+ row = enumerator.Current;
+
+ CompareNumbersWithTolerance(expectedValues[index++], row.Data[0], digitsOfPrecision: 5); // Alert
+ CompareNumbersWithTolerance(expectedValues[index++], row.Data[1], digitsOfPrecision: 5); // Raw score
+ CompareNumbersWithTolerance(expectedValues[index++], row.Data[2], digitsOfPrecision: 5); // P-Value score
+ CompareNumbersWithTolerance(expectedValues[index++], row.Data[3], digitsOfPrecision: 5); // Martingale score
+ }
+ }
+
+ [Fact]
+ public void SpikeDetection()
+ {
+ var env = new MLContext(conc: 1);
+ const int Size = 10;
+ const int PvalHistoryLength = Size / 4;
+
+ // Generate sample series data with a spike
+ List data = new List(Size);
+ var dataView = env.CreateStreamingDataView(data);
+ for (int i = 0; i < Size / 2; i++)
+ data.Add(new Data(5));
+ data.Add(new Data(10)); // This is the spike
+ for (int i = 0; i < Size / 2 - 1; i++)
+ data.Add(new Data(5));
+
+ // Convert to statically-typed data view.
+ var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar });
+ // Build the pipeline
+ var staticLearningPipeline = staticData.MakeNewEstimator()
+ .Append(r => r.Value.IidSpikeDetect(80, PvalHistoryLength));
+ // Train
+ var detector = staticLearningPipeline.Fit(staticData);
+ // Transform
+ var output = detector.Transform(staticData);
+
+ // Get predictions
+ var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator();
+ var expectedValues = new List() {
+ // Alert Score P-Value
+ new double[] {0, 5, 0.5},
+ new double[] {0, 5, 0.5},
+ new double[] {0, 5, 0.5},
+ new double[] {0, 5, 0.5},
+ new double[] {0, 5, 0.5},
+ new double[] {1, 10, 0.0}, // alert is on, predicted spike
+ new double[] {0, 5, 0.261375},
+ new double[] {0, 5, 0.261375},
+ new double[] {0, 5, 0.50},
+ new double[] {0, 5, 0.50}
+ };
+
+ SpikePrediction row = null;
+ for (var i = 0; enumerator.MoveNext() && i < expectedValues.Count; i++)
+ {
+ row = enumerator.Current;
+
+ CompareNumbersWithTolerance(expectedValues[i][0], row.Data[0], digitsOfPrecision: 7);
+ CompareNumbersWithTolerance(expectedValues[i][1], row.Data[1], digitsOfPrecision: 7);
+ CompareNumbersWithTolerance(expectedValues[i][2], row.Data[2], digitsOfPrecision: 7);
+ }
+ }
+
+ [Fact]
+ public void SsaSpikeDetection()
+ {
+ var env = new MLContext(conc: 1);
+ const int Size = 16;
+ const int ChangeHistoryLength = Size / 4;
+ const int TrainingWindowSize = Size / 2;
+ const int SeasonalityWindowSize = Size / 8;
+
+ // Generate sample series data with a spike
+ List data = new List(Size);
+ var dataView = env.CreateStreamingDataView(data);
+ for (int i = 0; i < Size / 2; i++)
+ data.Add(new Data(5));
+ data.Add(new Data(10)); // This is the spike
+ for (int i = 0; i < Size / 2 - 1; i++)
+ data.Add(new Data(5));
+
+ // Convert to statically-typed data view.
+ var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar });
+ // Build the pipeline
+ var staticLearningPipeline = staticData.MakeNewEstimator()
+ .Append(r => r.Value.SsaSpikeDetect(80, ChangeHistoryLength, TrainingWindowSize, SeasonalityWindowSize));
+ // Train
+ var detector = staticLearningPipeline.Fit(staticData);
+ // Transform
+ var output = detector.Transform(staticData);
+
+ // Get predictions
+ var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator();
+ var expectedValues = new List() {
+ // Alert Score P-Value
+ new double[] {0, 0.0, 0.5},
+ new double[] {0, 0.0, 0.5},
+ new double[] {0, 0.0, 0.5},
+ new double[] {0, 0.0, 0.5},
+ new double[] {0, 0.0, 0.5},
+ new double[] {0, 0.0, 0.5},
+ new double[] {0, 0.0, 0.5},
+ new double[] {0, 0.0, 0.5},
+ new double[] {1, 5.0, 0.0}, // alert is on, predicted spike
+ new double[] {1, -2.5, 0.093146},
+ new double[] {0, -2.5, 0.215437},
+ new double[] {0, 0.0, 0.465745},
+ new double[] {0, 0.0, 0.465745},
+ new double[] {0, 0.0, 0.261375},
+ new double[] {0, 0.0, 0.377615},
+ new double[] {0, 0.0, 0.50}
+ };
+
+ SpikePrediction row = null;
+ for (var i = 0; enumerator.MoveNext() && i < expectedValues.Count; i++)
+ {
+ row = enumerator.Current;
+
+ CompareNumbersWithTolerance(expectedValues[i][0], row.Data[0], digitsOfPrecision: 6);
+ CompareNumbersWithTolerance(expectedValues[i][1], row.Data[1], digitsOfPrecision: 6);
+ CompareNumbersWithTolerance(expectedValues[i][2], row.Data[2], digitsOfPrecision: 6);
+ }
+ }
+ }
+}