package com.linkedin.thirdeye.anomalydetection.model.merge; import com.linkedin.thirdeye.anomalydetection.context.AnomalyDetectionContext; import com.linkedin.thirdeye.anomalydetection.context.TimeSeries; import com.linkedin.thirdeye.anomalydetection.model.prediction.ExpectedTimeSeriesPredictionModel; import com.linkedin.thirdeye.anomalydetection.model.prediction.PredictionModel; import com.linkedin.thirdeye.datalayer.dto.MergedAnomalyResultDTO; import com.linkedin.thirdeye.datalayer.dto.RawAnomalyResultDTO; import java.util.List; import org.apache.commons.collections.CollectionUtils; import org.joda.time.Interval; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SimplePercentageMergeModel extends AbstractMergeModel { private static final Logger LOGGER = LoggerFactory.getLogger(SimplePercentageMergeModel.class); private static final String DEFAULT_MESSAGE_TEMPLATE = "change : %.2f %%, currentVal : %.2f, baseLineVal : %.2f, score : %.2f"; /** * The weight of the merged anomaly is calculated by this equation: * weight = (avg. observed value) / (avg. expected value) - 1; * * Note that the values of the holes in the time series are not included in the computation. * Considering the observed and expected time series: * observed: 1 2 x 4 x 6 * expected: 1 x x 4 5 6 * The values that are included in the computation are those at slots 1, 4, and 6. * * @param anomalyDetectionContext the context that provided a trained * ExpectedTimeSeriesPredictionModel for computing the weight. * Moreover, the data range of the time series should equals the * range of anomaly to be updated. * * @param anomalyToUpdated the anomaly of which the information is updated. */ @Override public void update(AnomalyDetectionContext anomalyDetectionContext, MergedAnomalyResultDTO anomalyToUpdated) { String mainMetric = anomalyDetectionContext.getAnomalyDetectionFunction().getSpec().getTopicMetric(); PredictionModel predictionModel = anomalyDetectionContext.getTrainedPredictionModel(mainMetric); if (!(predictionModel instanceof ExpectedTimeSeriesPredictionModel)) { LOGGER.error("SimplePercentageMergeModel expects an ExpectedTimeSeriesPredictionModel but the trained model is not one."); return; } ExpectedTimeSeriesPredictionModel expectedTimeSeriesPredictionModel = (ExpectedTimeSeriesPredictionModel) predictionModel; TimeSeries expectedTimeSeries = expectedTimeSeriesPredictionModel.getExpectedTimeSeries(); long expectedStartTime = expectedTimeSeries.getTimeSeriesInterval().getStartMillis(); TimeSeries observedTimeSeries = anomalyDetectionContext.getTransformedCurrent(mainMetric); long observedStartTime = observedTimeSeries.getTimeSeriesInterval().getStartMillis(); double avgCurrent = 0d; double avgBaseline = 0d; int count = 0; Interval anomalyInterval = new Interval(anomalyToUpdated.getStartTime(), anomalyToUpdated.getEndTime()); for (long observedTimestamp : observedTimeSeries.timestampSet()) { if (anomalyInterval.contains(observedTimestamp)) { long offset = observedTimestamp - observedStartTime; long expectedTimestamp = expectedStartTime + offset; if (expectedTimeSeries.hasTimestamp(expectedTimestamp)) { avgCurrent += observedTimeSeries.get(observedTimestamp); avgBaseline += expectedTimeSeries.get(expectedTimestamp); ++count; } } } double weight = 0d; if (count != 0 && avgBaseline != 0d) { weight = (avgCurrent - avgBaseline) / avgBaseline; avgCurrent /= count; avgBaseline /= count; } else { weight = 0d; } // Average score of raw anomalies List<RawAnomalyResultDTO> rawAnomalyResultDTOs = anomalyToUpdated.getAnomalyResults(); double score = 0d; if (CollectionUtils.isNotEmpty(rawAnomalyResultDTOs)) { for (RawAnomalyResultDTO rawAnomaly : rawAnomalyResultDTOs) { score += rawAnomaly.getScore(); } score /= rawAnomalyResultDTOs.size(); } else { score = anomalyToUpdated.getScore(); } anomalyToUpdated.setWeight(weight); anomalyToUpdated.setScore(score); anomalyToUpdated.setAvgCurrentVal(avgCurrent); anomalyToUpdated.setAvgBaselineVal(avgBaseline); anomalyToUpdated.setMessage( String.format(DEFAULT_MESSAGE_TEMPLATE, weight * 100, avgCurrent, avgBaseline, score)); } }