Predict Sales Spikes With C# and ML.NET Machine Learning
By: Mark Farragher
Tags: Machine Learning, Artificial Intelligence, Deep Learning, Data Science, Programming

It’s a very simple CSV file with only two columns:

The date for the sales record

The number of shampoo bottles sold on that date

I will build a machine learning model that reads in each sales record, and then identifies every anomalous spike in the sales data.

Let’s get started. Here’s how to set up a new console project in NET Core:

$ dotnet new console -o Spike

$ cd Spike

Next, I need to install the ML.NET base package, the time series extensions, and a plotting library:

$ dotnet add package Microsoft.ML

$ dotnet add package Microsoft.ML.TimeSeries

$ dotnet add package plplot

We C# programmers unfortunately do not get access to the awesome matplotlib library that Python programmers get to use. But there’s a really nice alternative for us: PLplot. It’s an advanced scientific plotting library and it comes with full C# bindings.

If you’re on Windows, you can use the Nuget package out of the box. But if you use a Mac like me, you’ll have to install plplot with homebrew first:

$ brew install plplot

Linux users also need to install plplot. Here’s how you do it:

$ sudo apt install libplplot15 plplot-driver-cairo

How cool is that? This is a multi-platform NET Core plotting app that runs on Windows, OS/X, and Linux!

Now I’m ready to add some classes. I’ll need one to hold a sales record, and one to hold my model’s predictions.

I will modify the Program.cs file like this:

The SalesRecord class holds one single sales record. Note how each field is tagged with a Column attribute that tell the CSV data loading code which column to import data from.

I’m also declaring an SalesPrediction class which will hold a single sales prediction. Note that the prediction field is tagged with a VectorType attribute that tells ML.NET that each prediction will consist of 3 numbers.

Now I’m going to load the sales data in memory:

This code uses the method LoadFromTextFile to load the CSV data directly into memory. The class field annotations tell the method how to store the loaded data in the SalesRecord class.

The data is now stored in a data view, but I want to work with the sales records directly. So I’m calling CreateEnumerable() to convert the data view to an enumeration of SalesRecord instances.

Let’s start by plotting the sales records to get an idea of what my data looks like. I’ll add the following code:

You can see that plplot has an API with very short method names. I’ve added some comments to describe what each method does.

The line() method draws a line from two array of x- and y-coordinates. My code uses two LINQ queries to provide a range of 0..35 for x, and the corresponding sales numbers for y.

The final eop() method closes the plot and saves it to disk.

When I run the app, I get the following plot:

Looks good! That’s a nicely increasing shampoo sales trend.

Now I will identify all anomalies in the data. I will use an ML.NET algorithm called IID Spike Estimator.

‘IID’ refers to Independent and Identically Distributed. It means that each sales record is independent from all other sales records, and the probability distribution of all sales records are the same.

Anomalies are outliers in the data. These are points on the input time-series at which the data behaves differently from what is expected. These deviations are usually indicative of some interesting events in the problem domain that we want to focus on.

There are two types of anomalies: spikes which are sudden yet temporary bursts in the values of the input time-series, and change points which indicate the beginning of persistent changes in the system

Let’s start with spikes. Here’s how to perform spike estimation in ML.NET.

First remove the pl.eop() call, and then add the following code:

Machine learning models in ML.NET are built with pipelines, which are sequences of data-loading, transformation, and learning components.

My pipeline has only one component:

DetectIidSpike which reads the sales records and estimates all anomalous spikes in the data. I have to provide the input and output column names, a confidence threshold, and the size of the sliding window used during estimation.

I’m configuring my spike estimator to use a 95% confidence threshold and a window that spans 25% of my x-value range.

With the pipeline fully assembled, I can train the model on the data with a call to Fit(…) and then call Transform(…) to make spike predictions for every sales record in the data set.

Finally I call CreateEnumerable() to convert my transformed variable to an enumeration of SalesPrediction instances.

Each sales prediction instance now holds a vector with three values:

An ‘alert’ value that is equal to 1 for a sales spike (that exceeded the specified threshold) and 0 otherwise.

The predicted sales value.

The p-value. This is a metric between zero and one. The lower the value, the larger the probability that we’re looking at a spike.

I can highlight the spikes in the plot with the following code:

I use a LINQ query to select all spike predictions with the ‘alert’ value equal to 1, and call the pl.string2() method to highlight these locations in the graph with a down-arrow symbol.

Here’s what that looks like. When I run the app now, I get the following plot:

The algorithm has discovered four spikes in the data.

Now let’s try and find the change points. Remove the final pl.eop() and add the following code:

My new pipeline has only one component:

DetectIidChangePoint which reads the sales records and estimates all change points in the data. I have to provide the input and output column names, a confidence threshold, and the size of the sliding window used during estimation.

I’m configuring my change point estimator with the same values I used before: a 95% confidence threshold and a window that spans 25% of my x-value range.

With the pipeline fully assembled, I can train the model on the data with a call to Fit(…) and then call Transform(…) to make change point predictions for every sales record in the data set.

Finally I call CreateEnumerable() to convert my transformed variable to an enumeration of SalesPrediction instances.

Each sales prediction instance now holds a vector with four(!) values:

An ‘alert’ value that is equal to 1 for a change point (that exceeded the specified threshold) and 0 otherwise.

The predicted sales value.

The p-value. This is a metric between zero and one. The lower the value, the larger the probability that we’re looking at a spike.

The Martingale value. This represents the deviation of the current value from the previous trend. High values indicate a change point.

I can highlight the change points in the plot with the following code:

I use a LINQ query to select all change point predictions with the ‘alert’ value equal to 1, and call the pl.line() method to draw a vertical red line in these locations in the graph.

Here’s what that looks like. When I run the app now, I get the following plot:

The algorithm has discovered one change point at day #8. This is where the horizontal sales trend stops, and shampoo sales start trending upward.

So what do you think? Are you ready to start writing C# machine learning apps with ML.NET?