package com.linkedin.thirdeye.anomalydetection.model.transform; import com.linkedin.thirdeye.anomalydetection.context.AnomalyDetectionContext; import com.linkedin.thirdeye.anomalydetection.context.TimeSeries; import com.linkedin.thirdeye.api.DimensionMap; import org.joda.time.Interval; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MovingAverageSmoothingFunction extends AbstractTransformationFunction { private static final Logger LOGGER = LoggerFactory.getLogger(MovingAverageSmoothingFunction.class); public static final String MOVING_AVERAGE_SMOOTHING_WINDOW_SIZE = "movingAverageSmoothingWindowSize"; /** * Smooths the given time series using moving average. * * If the input time series is shorter than the moving average window size, then this method * does not apply smoothing on the time series, i.e., it returns the original time series. * * The transformed time series is shorten by the size of the moving average window in * comparison to the original time series. For instance, if there are 10 consecutive data points * the a time series and the window size for moving average is 2, then the transformed time series * contains only 9 consecutive data points; The first data points has no other data point to * average and thus it is discarded. * * @param timeSeries the time series that provides the data points to be transformed. * @param anomalyDetectionContext the anomaly detection context that could provide additional * information for the transformation. * @return a time series that is smoothed using moving average. */ @Override public TimeSeries transform(TimeSeries timeSeries, AnomalyDetectionContext anomalyDetectionContext) { Interval timeSeriesInterval = timeSeries.getTimeSeriesInterval(); long startTime = timeSeriesInterval.getStartMillis(); long endTime = timeSeriesInterval.getEndMillis(); long bucketSizeInMillis = anomalyDetectionContext.getBucketSizeInMS(); int movingAverageWindowSize = Integer.valueOf(getProperties().getProperty(MOVING_AVERAGE_SMOOTHING_WINDOW_SIZE)); // Check if the moving average window size is larger than the time series itself long transformedStartTime = startTime + bucketSizeInMillis * (movingAverageWindowSize - 1); if (transformedStartTime > endTime) { String metricName = anomalyDetectionContext.getAnomalyDetectionFunction().getSpec().getTopicMetric(); DimensionMap dimensionMap = anomalyDetectionContext.getTimeSeriesKey().getDimensionMap(); LOGGER.warn( "Input time series (Metric:{}, Dimension:{}) is shorter than the moving average " + "smoothing window; therefore, smoothing is not applied on this time series.", metricName, dimensionMap); return timeSeries; } TimeSeries transformedTimeSeries = new TimeSeries(); Interval transformedInterval = new Interval(transformedStartTime, endTime); transformedTimeSeries.setTimeSeriesInterval(transformedInterval); for (long timeKeyToTransform : timeSeries.timestampSet()) { if (!transformedInterval.contains(timeKeyToTransform)) { continue; } double sum = 0d; int count = 0; for (int i = 0; i < movingAverageWindowSize; ++i) { long timeKey = timeKeyToTransform - bucketSizeInMillis * i; if (timeSeries.hasTimestamp(timeKey)) { sum += timeSeries.get(timeKey); ++count; } } double average = sum / count; // count is at least one due to the existence of timeKeyToTransform transformedTimeSeries.set(timeKeyToTransform, average); } return transformedTimeSeries; } }