/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.search.aggregations.pipeline.moving.avg;
import org.elasticsearch.common.collect.EvictingQueue;
import org.elasticsearch.search.aggregations.pipeline.movavg.models.EwmaModel;
import org.elasticsearch.search.aggregations.pipeline.movavg.models.HoltLinearModel;
import org.elasticsearch.search.aggregations.pipeline.movavg.models.HoltWintersModel;
import org.elasticsearch.search.aggregations.pipeline.movavg.models.LinearModel;
import org.elasticsearch.search.aggregations.pipeline.movavg.models.MovAvgModel;
import org.elasticsearch.search.aggregations.pipeline.movavg.models.SimpleModel;
import org.elasticsearch.test.ESTestCase;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
public class MovAvgUnitTests extends ESTestCase {
public void testSimpleMovAvgModel() {
MovAvgModel model = new SimpleModel();
int numValues = randomIntBetween(1, 100);
int windowSize = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < numValues; i++) {
double randValue = randomDouble();
double expected = 0;
if (i == 0) {
window.offer(randValue);
continue;
}
for (double value : window) {
expected += value;
}
expected /= window.size();
double actual = model.next(window);
assertThat(Double.compare(expected, actual), equalTo(0));
window.offer(randValue);
}
}
public void testSimplePredictionModel() {
MovAvgModel model = new SimpleModel();
int windowSize = randomIntBetween(1, 50);
int numPredictions = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
double actual[] = model.predict(window, numPredictions);
double expected[] = new double[numPredictions];
double t = 0;
for (double value : window) {
t += value;
}
t /= window.size();
Arrays.fill(expected, t);
for (int i = 0; i < numPredictions; i++) {
assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
}
}
public void testLinearMovAvgModel() {
MovAvgModel model = new LinearModel();
int numValues = randomIntBetween(1, 100);
int windowSize = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < numValues; i++) {
double randValue = randomDouble();
if (i == 0) {
window.offer(randValue);
continue;
}
double avg = 0;
long totalWeight = 1;
long current = 1;
for (double value : window) {
avg += value * current;
totalWeight += current;
current += 1;
}
double expected = avg / totalWeight;
double actual = model.next(window);
assertThat(Double.compare(expected, actual), equalTo(0));
window.offer(randValue);
}
}
public void testLinearPredictionModel() {
MovAvgModel model = new LinearModel();
int windowSize = randomIntBetween(1, 50);
int numPredictions = randomIntBetween(1,50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
double actual[] = model.predict(window, numPredictions);
double expected[] = new double[numPredictions];
double avg = 0;
long totalWeight = 1;
long current = 1;
for (double value : window) {
avg += value * current;
totalWeight += current;
current += 1;
}
avg = avg / totalWeight;
Arrays.fill(expected, avg);
for (int i = 0; i < numPredictions; i++) {
assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
}
}
public void testEWMAMovAvgModel() {
double alpha = randomDouble();
MovAvgModel model = new EwmaModel(alpha);
int numValues = randomIntBetween(1, 100);
int windowSize = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < numValues; i++) {
double randValue = randomDouble();
if (i == 0) {
window.offer(randValue);
continue;
}
double avg = 0;
boolean first = true;
for (double value : window) {
if (first) {
avg = value;
first = false;
} else {
avg = (value * alpha) + (avg * (1 - alpha));
}
}
double expected = avg;
double actual = model.next(window);
assertThat(Double.compare(expected, actual), equalTo(0));
window.offer(randValue);
}
}
public void testEWMAPredictionModel() {
double alpha = randomDouble();
MovAvgModel model = new EwmaModel(alpha);
int windowSize = randomIntBetween(1, 50);
int numPredictions = randomIntBetween(1,50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
double actual[] = model.predict(window, numPredictions);
double expected[] = new double[numPredictions];
double avg = 0;
boolean first = true;
for (double value : window) {
if (first) {
avg = value;
first = false;
} else {
avg = (value * alpha) + (avg * (1 - alpha));
}
}
Arrays.fill(expected, avg);
for (int i = 0; i < numPredictions; i++) {
assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
}
}
public void testHoltLinearMovAvgModel() {
double alpha = randomDouble();
double beta = randomDouble();
MovAvgModel model = new HoltLinearModel(alpha, beta);
int numValues = randomIntBetween(1, 100);
int windowSize = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < numValues; i++) {
double randValue = randomDouble();
if (i == 0) {
window.offer(randValue);
continue;
}
double s = 0;
double last_s = 0;
// Trend value
double b = 0;
double last_b = 0;
int counter = 0;
double last;
for (double value : window) {
last = value;
if (counter == 1) {
s = value;
b = value - last;
} else {
s = alpha * value + (1.0d - alpha) * (last_s + last_b);
b = beta * (s - last_s) + (1 - beta) * last_b;
}
counter += 1;
last_s = s;
last_b = b;
}
double expected = s + (0 * b) ;
double actual = model.next(window);
assertThat(Double.compare(expected, actual), equalTo(0));
window.offer(randValue);
}
}
public void testHoltLinearPredictionModel() {
double alpha = randomDouble();
double beta = randomDouble();
MovAvgModel model = new HoltLinearModel(alpha, beta);
int windowSize = randomIntBetween(1, 50);
int numPredictions = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
double actual[] = model.predict(window, numPredictions);
double expected[] = new double[numPredictions];
double s = 0;
double last_s = 0;
// Trend value
double b = 0;
double last_b = 0;
int counter = 0;
double last;
for (double value : window) {
last = value;
if (counter == 1) {
s = value;
b = value - last;
} else {
s = alpha * value + (1.0d - alpha) * (last_s + last_b);
b = beta * (s - last_s) + (1 - beta) * last_b;
}
counter += 1;
last_s = s;
last_b = b;
}
for (int i = 0; i < numPredictions; i++) {
expected[i] = s + (i * b);
assertThat(Double.compare(expected[i], actual[i]), equalTo(0));
}
}
public void testHoltWintersMultiplicativePadModel() {
double alpha = randomDouble();
double beta = randomDouble();
double gamma = randomDouble();
int period = randomIntBetween(1,10);
MovAvgModel model = new HoltWintersModel(alpha, beta, gamma, period, HoltWintersModel.SeasonalityType.MULTIPLICATIVE, true);
int windowSize = randomIntBetween(period * 2, 50); // HW requires at least two periods of data
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
// Smoothed value
double s = 0;
double last_s = 0;
// Trend value
double b = 0;
double last_b = 0;
// Seasonal value
double[] seasonal = new double[windowSize];
int counter = 0;
double[] vs = new double[windowSize];
for (double v : window) {
vs[counter] = v + 0.0000000001;
counter += 1;
}
// Initial level value is average of first season
// Calculate the slopes between first and second season for each period
for (int i = 0; i < period; i++) {
s += vs[i];
b += (vs[i + period] - vs[i]) / period;
}
s /= period;
b /= period;
last_s = s;
// Calculate first seasonal
if (Double.compare(s, 0.0) == 0 || Double.compare(s, -0.0) == 0) {
Arrays.fill(seasonal, 0.0);
} else {
for (int i = 0; i < period; i++) {
seasonal[i] = vs[i] / s;
}
}
for (int i = period; i < vs.length; i++) {
s = alpha * (vs[i] / seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b);
b = beta * (s - last_s) + (1 - beta) * last_b;
seasonal[i] = gamma * (vs[i] / (last_s + last_b )) + (1 - gamma) * seasonal[i - period];
last_s = s;
last_b = b;
}
int idx = window.size() - period + (0 % period);
double expected = (s + (1 * b)) * seasonal[idx];
double actual = model.next(window);
assertThat(Double.compare(expected, actual), equalTo(0));
}
public void testHoltWintersMultiplicativePadPredictionModel() {
double alpha = randomDouble();
double beta = randomDouble();
double gamma = randomDouble();
int period = randomIntBetween(1,10);
MovAvgModel model = new HoltWintersModel(alpha, beta, gamma, period, HoltWintersModel.SeasonalityType.MULTIPLICATIVE, true);
int windowSize = randomIntBetween(period * 2, 50); // HW requires at least two periods of data
int numPredictions = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
double actual[] = model.predict(window, numPredictions);
double expected[] = new double[numPredictions];
// Smoothed value
double s = 0;
double last_s = 0;
// Trend value
double b = 0;
double last_b = 0;
// Seasonal value
double[] seasonal = new double[windowSize];
int counter = 0;
double[] vs = new double[windowSize];
for (double v : window) {
vs[counter] = v + 0.0000000001;
counter += 1;
}
// Initial level value is average of first season
// Calculate the slopes between first and second season for each period
for (int i = 0; i < period; i++) {
s += vs[i];
b += (vs[i + period] - vs[i]) / period;
}
s /= period;
b /= period;
last_s = s;
// Calculate first seasonal
if (Double.compare(s, 0.0) == 0 || Double.compare(s, -0.0) == 0) {
Arrays.fill(seasonal, 0.0);
} else {
for (int i = 0; i < period; i++) {
seasonal[i] = vs[i] / s;
}
}
for (int i = period; i < vs.length; i++) {
s = alpha * (vs[i] / seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b);
b = beta * (s - last_s) + (1 - beta) * last_b;
seasonal[i] = gamma * (vs[i] / (last_s + last_b )) + (1 - gamma) * seasonal[i - period];
last_s = s;
last_b = b;
}
for (int i = 1; i <= numPredictions; i++) {
int idx = window.size() - period + ((i - 1) % period);
expected[i-1] = (s + (i * b)) * seasonal[idx];
assertThat(Double.compare(expected[i-1], actual[i-1]), equalTo(0));
}
}
public void testHoltWintersAdditiveModel() {
double alpha = randomDouble();
double beta = randomDouble();
double gamma = randomDouble();
int period = randomIntBetween(1,10);
MovAvgModel model = new HoltWintersModel(alpha, beta, gamma, period, HoltWintersModel.SeasonalityType.ADDITIVE, false);
int windowSize = randomIntBetween(period * 2, 50); // HW requires at least two periods of data
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
// Smoothed value
double s = 0;
double last_s = 0;
// Trend value
double b = 0;
double last_b = 0;
// Seasonal value
double[] seasonal = new double[windowSize];
int counter = 0;
double[] vs = new double[windowSize];
for (double v : window) {
vs[counter] = v;
counter += 1;
}
// Initial level value is average of first season
// Calculate the slopes between first and second season for each period
for (int i = 0; i < period; i++) {
s += vs[i];
b += (vs[i + period] - vs[i]) / period;
}
s /= period;
b /= period;
last_s = s;
// Calculate first seasonal
if (Double.compare(s, 0.0) == 0 || Double.compare(s, -0.0) == 0) {
Arrays.fill(seasonal, 0.0);
} else {
for (int i = 0; i < period; i++) {
seasonal[i] = vs[i] / s;
}
}
for (int i = period; i < vs.length; i++) {
s = alpha * (vs[i] - seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b);
b = beta * (s - last_s) + (1 - beta) * last_b;
seasonal[i] = gamma * (vs[i] - (last_s - last_b )) + (1 - gamma) * seasonal[i - period];
last_s = s;
last_b = b;
}
int idx = window.size() - period + (0 % period);
double expected = s + (1 * b) + seasonal[idx];
double actual = model.next(window);
assertThat(Double.compare(expected, actual), equalTo(0));
}
public void testHoltWintersAdditivePredictionModel() {
double alpha = randomDouble();
double beta = randomDouble();
double gamma = randomDouble();
int period = randomIntBetween(1,10);
MovAvgModel model = new HoltWintersModel(alpha, beta, gamma, period, HoltWintersModel.SeasonalityType.ADDITIVE, false);
int windowSize = randomIntBetween(period * 2, 50); // HW requires at least two periods of data
int numPredictions = randomIntBetween(1, 50);
EvictingQueue<Double> window = new EvictingQueue<>(windowSize);
for (int i = 0; i < windowSize; i++) {
window.offer(randomDouble());
}
double actual[] = model.predict(window, numPredictions);
double expected[] = new double[numPredictions];
// Smoothed value
double s = 0;
double last_s = 0;
// Trend value
double b = 0;
double last_b = 0;
// Seasonal value
double[] seasonal = new double[windowSize];
int counter = 0;
double[] vs = new double[windowSize];
for (double v : window) {
vs[counter] = v;
counter += 1;
}
// Initial level value is average of first season
// Calculate the slopes between first and second season for each period
for (int i = 0; i < period; i++) {
s += vs[i];
b += (vs[i + period] - vs[i]) / period;
}
s /= period;
b /= period;
last_s = s;
// Calculate first seasonal
if (Double.compare(s, 0.0) == 0 || Double.compare(s, -0.0) == 0) {
Arrays.fill(seasonal, 0.0);
} else {
for (int i = 0; i < period; i++) {
seasonal[i] = vs[i] / s;
}
}
for (int i = period; i < vs.length; i++) {
s = alpha * (vs[i] - seasonal[i - period]) + (1.0d - alpha) * (last_s + last_b);
b = beta * (s - last_s) + (1 - beta) * last_b;
seasonal[i] = gamma * (vs[i] - (last_s - last_b )) + (1 - gamma) * seasonal[i - period];
last_s = s;
last_b = b;
}
for (int i = 1; i <= numPredictions; i++) {
int idx = window.size() - period + ((i - 1) % period);
expected[i-1] = s + (i * b) + seasonal[idx];
assertThat(Double.compare(expected[i-1], actual[i-1]), equalTo(0));
}
}
public void testNumericValidation() {
List<MovAvgModel.AbstractModelParser> parsers = new ArrayList<>(3);
// Simple and Linear don't have any settings to test
parsers.add(EwmaModel.PARSER);
parsers.add(HoltWintersModel.PARSER);
parsers.add(HoltLinearModel.PARSER);
Object[] values = {(byte)1, 1, 1L, (short)1, (double)1};
Map<String, Object> settings = new HashMap<>(2);
for (MovAvgModel.AbstractModelParser parser : parsers) {
for (Object v : values) {
settings.put("alpha", v);
try {
parser.parse(settings, "pipeline", 10);
} catch (ParseException e) {
fail(parser + " parser should not have thrown SearchParseException while parsing [" +
v.getClass().getSimpleName() +"]");
}
}
}
for (MovAvgModel.AbstractModelParser parser : parsers) {
settings.put("alpha", "abc");
settings.put("beta", "abc");
settings.put("gamma", "abc");
try {
parser.parse(settings, "pipeline", 10);
} catch (ParseException e) {
//all good
continue;
}
fail(parser + " parser should have thrown SearchParseException while parsing [String]");
}
}
}