package com.linkedin.thirdeye.anomalydetection.model.detection;
import com.linkedin.thirdeye.anomalydetection.context.AnomalyDetectionContext;
import com.linkedin.thirdeye.anomalydetection.context.TimeSeries;
import com.linkedin.thirdeye.api.DimensionMap;
import com.linkedin.thirdeye.datalayer.dto.RawAnomalyResultDTO;
import java.util.ArrayList;
import java.util.List;
import org.joda.time.Interval;
public class MinMaxThresholdDetectionModel extends AbstractDetectionModel {
public static final String DEFAULT_MESSAGE_TEMPLATE = "change : %.2f %%, currentVal : %.2f, min : %.2f, max : %.2f";
public static final String MIN_VAL = "min";
public static final String MAX_VAL = "max";
@Override
public List<RawAnomalyResultDTO> detect(String metricName,
AnomalyDetectionContext anomalyDetectionContext) {
List<RawAnomalyResultDTO> anomalyResults = new ArrayList<>();
// Get min / max props
Double min = null;
if (properties.containsKey(MIN_VAL)) {
min = Double.valueOf(properties.getProperty(MIN_VAL));
}
Double max = null;
if (properties.containsKey(MAX_VAL)) {
max = Double.valueOf(properties.getProperty(MAX_VAL));
}
TimeSeries timeSeries = anomalyDetectionContext.getTransformedCurrent(metricName);
// Compute the weight of this time series (average across whole)
double averageValue = 0;
for (long time : timeSeries.timestampSet()) {
averageValue += timeSeries.get(time);
}
// Compute the bucket size, so we can iterate in those steps
long bucketMillis = anomalyDetectionContext.getBucketSizeInMS();
Interval timeSeriesInterval = timeSeries.getTimeSeriesInterval();
long numBuckets = Math.abs(timeSeriesInterval.getEndMillis() - timeSeriesInterval.getStartMillis()) / bucketMillis;
// avg value of this time series
averageValue /= numBuckets;
DimensionMap dimensionMap = anomalyDetectionContext.getTimeSeriesKey().getDimensionMap();
for (long timeBucket : timeSeries.timestampSet()) {
double value = timeSeries.get(timeBucket);
double deviationFromThreshold = getDeviationFromThreshold(value, min, max);
if (deviationFromThreshold != 0) {
RawAnomalyResultDTO anomalyResult = new RawAnomalyResultDTO();
anomalyResult.setProperties(properties.toString());
anomalyResult.setStartTime(timeBucket);
anomalyResult.setEndTime(timeBucket + bucketMillis); // point-in-time
anomalyResult.setDimensions(dimensionMap);
anomalyResult.setScore(averageValue);
anomalyResult.setWeight(deviationFromThreshold); // higher change, higher the severity
anomalyResult.setAvgCurrentVal(value);
String message =
String.format(DEFAULT_MESSAGE_TEMPLATE, deviationFromThreshold, value, min, max);
anomalyResult.setMessage(message);
if (value == 0.0) {
anomalyResult.setDataMissing(true);
}
anomalyResults.add(anomalyResult);
}
}
return anomalyResults;
}
public static double getDeviationFromThreshold(double currentValue, Double min, Double max) {
if ((min != null && currentValue < min && min != 0d)) {
return calculateChange(currentValue, min);
} else if (max != null && currentValue > max && max != 0d) {
return calculateChange(currentValue, max);
}
return 0;
}
protected static double calculateChange(double currentValue, double baselineValue) {
return (currentValue - baselineValue) / baselineValue;
}
}