package com.linkedin.thirdeye.anomalydetection.function; import com.linkedin.thirdeye.anomalydetection.model.data.DataModel; import com.linkedin.thirdeye.anomalydetection.model.data.NoopDataModel; import com.linkedin.thirdeye.anomalydetection.model.data.SeasonalDataModel; import com.linkedin.thirdeye.anomalydetection.model.detection.DetectionModel; import com.linkedin.thirdeye.anomalydetection.model.detection.NoopDetectionModel; import com.linkedin.thirdeye.anomalydetection.model.detection.SimpleThresholdDetectionModel; import com.linkedin.thirdeye.anomalydetection.model.merge.MergeModel; import com.linkedin.thirdeye.anomalydetection.model.merge.NoopMergeModel; import com.linkedin.thirdeye.anomalydetection.model.merge.SimplePercentageMergeModel; import com.linkedin.thirdeye.anomalydetection.model.prediction.NoopPredictionModel; import com.linkedin.thirdeye.anomalydetection.model.prediction.PredictionModel; import com.linkedin.thirdeye.anomalydetection.model.prediction.SeasonalAveragePredictionModel; import com.linkedin.thirdeye.anomalydetection.model.transform.MovingAverageSmoothingFunction; import com.linkedin.thirdeye.anomalydetection.model.transform.TotalCountThresholdRemovalFunction; import com.linkedin.thirdeye.anomalydetection.model.transform.TransformationFunction; import com.linkedin.thirdeye.anomalydetection.model.transform.ZeroRemovalFunction; import com.linkedin.thirdeye.datalayer.dto.AnomalyFunctionDTO; import java.util.ArrayList; import java.util.List; import java.util.Properties; import org.apache.commons.lang3.StringUtils; public class WeekOverWeekRuleFunction extends AbstractModularizedAnomalyFunction { public static final String BASELINE = "baseline"; public static final String ENABLE_SMOOTHING = "enableSmoothing"; private DataModel dataModel = new NoopDataModel(); private List<TransformationFunction> currentTimeSeriesTransformationChain = new ArrayList<>(); private List<TransformationFunction> baselineTimeSeriesTransformationChain = new ArrayList<>(); private PredictionModel predictionModel = new NoopPredictionModel(); private DetectionModel detectionModel = new NoopDetectionModel(); private MergeModel mergeModel = new NoopMergeModel(); public String[] getPropertyKeys() { return new String[] { BASELINE, ENABLE_SMOOTHING, MovingAverageSmoothingFunction.MOVING_AVERAGE_SMOOTHING_WINDOW_SIZE, SimpleThresholdDetectionModel.AVERAGE_VOLUME_THRESHOLD, SimpleThresholdDetectionModel.CHANGE_THRESHOLD }; } @Override public void init(AnomalyFunctionDTO spec) throws Exception { super.init(spec); this.init(this.properties); } public void init(Properties properties) { this.properties = properties; String baselineProp = this.properties.getProperty(BASELINE); if (StringUtils.isNotBlank(baselineProp)) { this.initPropertiesForDataModel(baselineProp); } dataModel = new SeasonalDataModel(); dataModel.init(this.properties); // Removes zeros from time series, which currently mean empty values in ThirdEye. TransformationFunction zeroRemover = new ZeroRemovalFunction(); currentTimeSeriesTransformationChain.add(zeroRemover); baselineTimeSeriesTransformationChain.add(zeroRemover); // Add total count threshold transformation if (this.properties.containsKey(TotalCountThresholdRemovalFunction.TOTAL_COUNT_METRIC_NAME)) { TransformationFunction totalCountThresholdFunction = new TotalCountThresholdRemovalFunction(); totalCountThresholdFunction.init(this.properties); currentTimeSeriesTransformationChain.add(totalCountThresholdFunction); } // Add moving average smoothing transformation if (this.properties.containsKey(ENABLE_SMOOTHING)) { TransformationFunction movingAverageSoothingFunction = new MovingAverageSmoothingFunction(); movingAverageSoothingFunction.init(this.properties); currentTimeSeriesTransformationChain.add(movingAverageSoothingFunction); baselineTimeSeriesTransformationChain.add(movingAverageSoothingFunction); } predictionModel = new SeasonalAveragePredictionModel(); predictionModel.init(this.properties); detectionModel = new SimpleThresholdDetectionModel(); detectionModel.init(this.properties); mergeModel = new SimplePercentageMergeModel(); mergeModel.init(this.properties); } /** * Parses the human readable string of baseline property and sets up SEASONAL_PERIOD and * SEASONAL_SIZE. * * The string should be given in this regex format: [wW][/o][0-9]?[wW]. For example, this string * "Wo2W" means comparing the current week with the 2 week prior. * * If the string ends with "Avg", then the property becomes week-over-weeks-average. For instance, * "W/4wAvg" means comparing the current week with the average of the past 4 weeks. * * @param baselineProp The human readable string of baseline property. */ private void initPropertiesForDataModel(String baselineProp) { // The basic settings for w/w this.properties.setProperty(SeasonalDataModel.SEASONAL_PERIOD, "1"); this.properties.setProperty(SeasonalDataModel.SEASONAL_SIZE, "7"); this.properties.setProperty(SeasonalDataModel.SEASONAL_UNIT, "DAYS"); // Change the setting for different w/w types if (StringUtils.isBlank(baselineProp)) { return; } String intString = parseWowString(baselineProp); if (baselineProp.endsWith("Avg")) { // Week-Over-Weeks_Average // example: "w/4wAvg" --> SeasonalDataModel.SEASONAL_PERIOD = "4" this.properties.setProperty(SeasonalDataModel.SEASONAL_PERIOD, intString); } else { // Week-Over-Week // example: "w/2w" --> SeasonalDataModel.SEASONAL_SIZE = "14" int seasonalSize = Integer.valueOf(intString) * 7; this.properties.setProperty(SeasonalDataModel.SEASONAL_SIZE, Integer.toString(seasonalSize)); } } /** * Returns the first integer of a string; returns 1 if no integer could be found. * * Examples: * 1. "w/w": returns 1 * 2. "Wo4W": returns 4 * 3. "W/343wABCD": returns 343 * 4. "2abc": returns 2 * 5. "A Random string 34 and it is 54 a long one": returns 34 * * @param wowString a string. * @return the integer of a WoW string. */ public static String parseWowString(String wowString) { if (StringUtils.isBlank(wowString)) { return "1"; } char[] chars = wowString.toCharArray(); int head = -1; for (int idx = 0; idx < chars.length; ++idx) { if ('0' <= chars[idx] && chars[idx] <= '9') { head = idx; break; } } if (head < 0) { return "1"; } int tail = head + 1; for (; tail < chars.length; ++tail) { if (chars[tail] <= '0' || '9' <= chars[tail]) { break; } } return wowString.substring(head, tail); } @Override public DataModel getDataModel() { return dataModel; } @Override public List<TransformationFunction> getCurrentTimeSeriesTransformationChain() { return currentTimeSeriesTransformationChain; } @Override public List<TransformationFunction> getBaselineTimeSeriesTransformationChain() { return baselineTimeSeriesTransformationChain; } @Override public PredictionModel getPredictionModel() { return predictionModel; } @Override public DetectionModel getDetectionModel() { return detectionModel; } @Override public MergeModel getMergeModel() { return mergeModel; } }