package net.iponweb.disthene.reader.graphite.utils; import net.iponweb.disthene.reader.beans.TimeSeries; import net.iponweb.disthene.reader.exceptions.EvaluationException; import net.iponweb.disthene.reader.exceptions.TimeSeriesNotAlignedException; import net.iponweb.disthene.reader.graphite.Target; import net.iponweb.disthene.reader.graphite.evaluation.TargetEvaluator; import net.iponweb.disthene.reader.utils.TimeSeriesUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; /** * @author Andrei Ivanov * * This implementation is probably suboptimal. * Will probably be improved some day. */ public class HoltWinters { private static final long SEASON = 604800; // 7 days private static final long BOOTSTRAP = SEASON * 2; private static final double ALPHA = 0.2; private static final double GAMMA = 0.2; private static final double BETA = 0.0035; private Target target; private TargetEvaluator evaluator; List<TimeSeries> original = new ArrayList<>(); private List<TimeSeries> forecasts = new ArrayList<>(); private List<Double> deviations = new ArrayList<>(); public HoltWinters(Target target, TargetEvaluator evaluator) { this.target = target; this.evaluator = evaluator; } public static HoltWinters analyze(Target target, TargetEvaluator evaluator) throws EvaluationException { HoltWinters holtWinters = new HoltWinters(target, evaluator); holtWinters.analyze(); return holtWinters; } private void analyze() throws EvaluationException { // Below assumes the results from evaluator will come in the same order // Firstly, let's get original series original.addAll(evaluator.eval(target)); if (original.size() == 0) return; if (!TimeSeriesUtils.checkAlignment(original)) { throw new TimeSeriesNotAlignedException(); } long from = original.get(0).getFrom(); long to = original.get(0).getTo(); int length = original.get(0).getValues().length; List<TimeSeries> bootstrapped = evaluator.bootstrap(target, original, BOOTSTRAP); for (TimeSeries ts : bootstrapped) { analyzeSingleSeries(ts, from, to, length); } } /** * Based on https://www.otexts.org/fpp/7/5 * (Forecasting: principles and practice by Rob J Hyndman, George Athanasopoulos */ private void analyzeSingleSeries(TimeSeries ts, long originalFrom, long originalTo, int originalLength) { int seasonLength = (int) (SEASON / ts.getStep()); Double[] values = ts.getValues(); Double[] forecast = new Double[values.length]; int knownLength = ts.getValues().length - originalLength; // initialize List<Double> seasonal = new LinkedList<>(); double baseline = values[1] != null ? values[1] : 0; double slope = baseline - (values[0] != null ? values[0] : 0); for (int i = 0; i < seasonLength; i++) { seasonal.add(values[i] != null ? values[i] : 0); forecast[i] = values[i] != null ? values[i] : 0; } for (int i = seasonLength; i < knownLength; i++) { forecast[i] = baseline + slope + seasonal.get(seasonLength - 1); double value = values[i] != null ? values[i] : 0; double previousBaseline = baseline; double previousSlope = slope; double previousSeasonal = seasonal.remove(0); baseline = ALPHA * (value - previousSeasonal) + (1.0 - ALPHA) * (previousBaseline + previousSlope); slope = BETA * (baseline - previousBaseline) + (1.0 - BETA) * previousSlope; seasonal.add(GAMMA * (value - baseline) + (1.0 - GAMMA) * previousSeasonal); } for (int i = knownLength; i < values.length; i++) { forecast[i] = baseline + slope + seasonal.get((seasonLength - 1 + (i - knownLength) % seasonLength) % seasonLength); } double sum = 0; for (int i = seasonLength; i < knownLength; i++) { double value = values[i] != null ? values[i] : 0; sum += (forecast[i] - value) * (forecast[i] - value); } double deviation = Math.sqrt(sum / (knownLength - seasonLength)); TimeSeries forecastTimeSeries = new TimeSeries(ts.getName(), ts.getFrom(), ts.getTo(), ts.getStep()); forecastTimeSeries.setValues(forecast); forecasts.add(revert(forecastTimeSeries, originalFrom, originalTo, originalLength)); deviations.add(deviation); } private TimeSeries revert(TimeSeries ts, long originalFrom, long originalTo, int originalLength) { ts.setFrom(originalFrom); ts.setTo(originalTo); ts.setValues(Arrays.copyOfRange(ts.getValues(), ts.getValues().length - originalLength, ts.getValues().length)); return ts; } public List<TimeSeries> getForecasts() { return forecasts; } public List<Double> getDeviations() { return deviations; } public List<TimeSeries> getOriginal() { return original; } }