/*
* Licensed 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 com.addthis.hydra.data.query.op;
import com.addthis.basis.util.LessStrings;
import com.addthis.bundle.core.Bundle;
import com.addthis.bundle.core.BundleField;
import com.addthis.bundle.core.list.ListBundle;
import com.addthis.bundle.core.list.ListBundleFormat;
import com.addthis.bundle.table.DataTable;
import com.addthis.bundle.table.DataTableFactory;
import com.addthis.bundle.util.BundleColumnBinder;
import com.addthis.bundle.value.ValueFactory;
import com.addthis.bundle.value.ValueObject;
import com.addthis.hydra.data.query.AbstractTableOp;
import com.addthis.hydra.data.util.HoltWinters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.ChannelProgressivePromise;
/**
* <p>This query operation <span class="hydra-summary"> applies Holt-Winters algorithm</span>.
* <p/>
* <p>Holt-Winters is an expontnetial smoothing function that works well for forecasting
* time series data sets</p>
*
* <p>See {@link com.addthis.hydra.data.util.HoltWinters} for more information on the algorithm</p>
*
* <p>The operation takes a colon separated list of optional parameters:</p>
*
* <ul>
* <li>timeColumn</li>
* <li>valColumn</li>
* <li>alpha - Exponential smoothing coefficients for level, trend, seasonal components.</li>
* <li>beta - Exponential smoothing coefficients for level, trend, seasonal components.</li>
* <li>gamma - Exponential smoothing coefficients for level, trend, seasonal components.</li>
* <li>period - A complete season's data consists of L periods. And we need
* to estimate the trend factor from one period to the next. To
* accomplish this, it is advisable to use two complete seasons;
* that is, 2L periods.</li>
* <li>m - extrapolated future data points</li>
* </ul>
*
* <p>The operation returns a {@link com.addthis.bundle.table.DataTable} with 4 columns.</p>
*
* <ul>
* <li>timeColumn</li>
* <li>valColumn</li>
* <li>forecast</li>
* <li>error percentage</li>
* </ul>
*
* <p/>
* <pre>
*
* Example:
*
* holtwinters=0:1:.1:.1:.1:7:3
*
* </pre>
*
* @user-reference
* @hydra-name holwinters
*/
public class OpHoltWinters extends AbstractTableOp {
private Logger log = LoggerFactory.getLogger(OpHoltWinters.class);
private int timeColumn = 0;
private int valColumn = 1;
private double alpha = .1;
private double beta = .1;
private double gamma = .1;
private int period = 7;
private int futureDataPoints = 3;
public OpHoltWinters(DataTableFactory tableFactory, String args, ChannelProgressivePromise queryPromise) {
super(tableFactory, queryPromise);
try {
if (args.length() > 0) {
String[] opt = args.split(":");
timeColumn = opt.length >= 1 ? Integer.parseInt(opt[0]) : 0;
valColumn = opt.length >= 2 ? Integer.parseInt(opt[1]) : 1;
alpha = opt.length >= 3 ? Double.parseDouble(opt[2]) : .1;
beta = opt.length >= 4 ? Double.parseDouble(opt[3]) : .1;
gamma = opt.length >= 5 ? Double.parseDouble(opt[4]) : .1;
period = opt.length >= 6 ? Integer.parseInt(opt[5]) : 7;
futureDataPoints = opt.length >= 7 ? Integer.parseInt(opt[6]) : 3;
}
log.info("Initiated HoltWinters with parameters " +
LessStrings.join(new Object[]{timeColumn, valColumn, alpha, beta, gamma, period, futureDataPoints},
","));
} catch (Exception ex) {
log.error("", ex);
}
}
@Override
public DataTable tableOp(DataTable result) {
if (result == null || result.size() == 0) {
return result;
}
if (result.size() < 2*period) {
throw new RuntimeException("Input rows must be > 2*period, rows=" + result.size() + " period=" + period);
}
long[] data = new long[result.size()];
BundleField[] fields = new BundleColumnBinder(result.get(0)).getFields();
BundleField timeField = fields[timeColumn];
for (int i = 0; i < result.size(); i++) {
data[i] = result.get(i).getValue(fields[valColumn]).asLong().getLong();
}
double[] forecast = HoltWinters.forecast(data, alpha, beta, gamma,
period, futureDataPoints, false);
DataTable table = createTable(forecast.length);
ListBundleFormat format = new ListBundleFormat();
BundleField outTimeField = format.getField("time");
BundleField actualField = format.getField("actual");
BundleField outSizeField = format.getField("forecast");
BundleField error = format.getField("error");
int index = 0;
for (double f : forecast) {
if (index + 1 > result.size()) {
break;
}
Bundle row = new ListBundle(format);
Bundle b = result.get(index++);
ValueObject actual = b.getValue(fields[valColumn]);
long actualLong = actual.asLong().getLong();
row.setValue(outTimeField, b.getValue(timeField));
row.setValue(actualField, actual);
row.setValue(outSizeField, ValueFactory.create(f));
row.setValue(error, ValueFactory.create(((f - actualLong)/Math.max(f, actualLong)) * 100));
table.append(row);
}
return table;
}
}