Let's say you have a time series and an anomaly detection algorithm to detect anomalies. If you have a labeled dataset, the performance of your anomaly detection algorithm can be measured using metrics like precision, recall, or F1-score. But what if your dataset doesn't include any labels, meaning you don't know where the anomalies are? How do you measure performance then?
Synthetic Anomalies Detection
A way to assess the performance of your method is to manually inject synthetic anomalies into your time series. Anomalies can take many different forms. Throughout this article, we considered an anomaly to be a localized spike in the input time series, as shown in the image below:
A successful outcome of the anomaly detection algorithm is shown in the following image: the algorithm correctly identifies the point where the anomaly was injected.
Of course, an unsuccessful outcome would be when the anomaly detector fails to identify the injected anomaly.
Because now we have a clear definition of what counts as successful or unsuccessful detection, these synthetic anomalies can be used to build a labeled dataset. This labeled dataset can then be used to train, validate, and benchmark anomaly detection models in a controlled and repeatable setting.
Synthetic Anomalies Parameters
In the setup of synthetic anomalies, multiple parameters can influence anomaly detection:
- The kind of injected anomalies: what do our injected anomalies look like?
- The specific anomaly detection algorithm we are adopting: how are we detecting the anomalies?
- The size of our anomalies: how "big" do we assume our anomalies to be?
- The location of our anomalies in the time series: where is the anomaly in the time series?
The first two parameters (kind and anomaly detection algorithm) are fixed in this blog post.
As stated earlier, a reasonable assumption for the "kind" of anomaly is a localized spike. For example, in a weather dataset, where the amplitude (y-axis) represents temperature in Kelvin and the x-axis represents time in hours, the spike corresponds to a temperature that is significantly higher than average.
The anomaly detection algorithm that we will be testing is the TimeGPT-1 model, developed by the Nixtla team. The idea behind TimeGPT-1 is to use the transformer algorithm and conformal probabilities to get accurate predictions and uncertainty boundaries. You can read more about it in the original paper, while another application of anomaly detection through TimeGPT-1 can be found in this blog post.
The size and location parameters are not fixed and will be considered as variables. A visual representation of injected anomalies at varying sizes and locations can be seen below:
And the corresponding effect on the input time series is shown below:
Performance Evaluation Method
Now, if you think about it, when the anomaly size is huge, any anomaly detection model (even a very simple one) would be able to easily spot it. Nonetheless, when the anomaly size is almost zero, even a very powerful anomaly detection model would struggle to detect it.
The question we want to ask ourselves is the following: "What is the smallest anomaly that we can detect through our algorithm?"
The evaluation algorithm to detect the smallest detectable anomaly, which we are going to define as the minimum detectable anomaly, is the following:
- We fix the largest size of the anomaly (e.g. size = 0.1 × the average of the time series).
- We inject the anomaly, using the same size, at multiple locations in the time series.
- For each time series with an injected anomaly, we run the anomaly detection algorithm and check whether the anomaly at the injected location is detected.
- We measure performance across all locations using a metric such as:
- If the accuracy is satisfactory, we can reduce the size of the anomaly and repeat from step 2. If not, we interrupt the loop.
A visual representation of this algorithm can be seen in the following flowchart:
At the end of this loop, the smallest size that yields satisfactory accuracy will be defined as the minimum detectable anomaly.
Data Setup and Anomaly Injection
We are going to implement the Performance Evaluation's algorithm described above on a real world dataset. The dataset and all the scripts that you need to run the code below can be found in this PieroPaialungaAI/AnomalyDetectionNixtla folder, which you can clone using:
git clone https://github.com/PieroPaialungaAI/AnomalyDetectionNixtla.git
The time series that we will be using come from a Kaggle open-source dataset, which can be found here. Note that you don't need to download anything, as the dataset is already available in the RawData folder.
In this dataset, the x-axis is the hourly recorded time, and the y-axis is the temperature, measured in K. The loader for this dataset can be found in the data.py script. The dataset provides multiple time series as columns (i.e., one column per city). For example, the last entries for the time series for Denver look like this:
from data import *
data = Data(datetime_column='datetime')
data.raw_data[['Denver', 'datetime']].tail()
| Index | Denver | datetime |
|---|---|---|
| 45248 | 289.56 | 2017-11-29 20:00:00 |
| 45249 | 290.70 | 2017-11-29 21:00:00 |
| 45250 | 289.71 | 2017-11-29 22:00:00 |
| 45251 | 289.17 | 2017-11-29 23:00:00 |
| 45252 | 285.18 | 2017-11-30 00:00:00 |
Multiple preprocessing steps are now applied:
- We only deal with one time series, so we can pick the city. The default city (selected throughout the blog post) is
Phoenix, but theisolate_city(city)allows you to pick whatever city you like. - The time series is pretty long, so we isolate a portion of the time series to reduce time and power complexity. The default portion is between
index = 41253andindex = 45253. Again, feel free to change this however you'd like using theisolate_portion(start,end)function. Keep in mind that the full time series has lengthl = 45253(so don't exceed the boundaries) - In order to run our
TimeGPT-1model onNixtla, the last preprocessing step is to rename the datetime columns todsand the timeseries amplitude toy.
The entire data preprocessing pipeline can be run using the following block of code:
data.isolate_city()
data.isolate_portion()
preprocessed_data = data.prepare_for_nixtla()
preprocessed_data.head()
| Index | y | ds | unique_id |
|---|---|---|---|
| 0 | 297.96 | 2017-06-16 09:00:00 | 0 |
| 1 | 297.15 | 2017-06-16 10:00:00 | 0 |
| 2 | 295.80 | 2017-06-16 11:00:00 | 0 |
| 3 | 295.00 | 2017-06-16 12:00:00 | 0 |
| 4 | 294.33 | 2017-06-16 13:00:00 | 0 |
This is the preprocessed_data that we will use for the rest of the blog post.
Everything related to anomaly injection and detection is implemented in the AnomalyCalibrator class. Its input is the preprocessed_data object, which is the result of the data processing steps from the Data class described above:
from anomaly_calibrator import *
anomaly_calibrator = AnomalyCalibrator(processed_data = preprocessed_data)
The inject_anomaly function allows us to place an anomaly of any size (threshold) wherever we like (location). For example, we can inject an anomaly at location = 300 with size = 0.1 (times the time series average in a small window around location = 300) using the following line of code:
anomaly_data = anomaly_calibrator.inject_anomaly(location = 300, threshold = 0.1)
plot_normal_and_anomalous_signal(anomaly_data['normal_signal'], anomaly_data['anomalous_signal'])
