package com.github.servicenow.ds.stats.stl;
/**
* Encapsulate the complexity of smoothing the cyclic sub-series in a separate class.
*
* Created by Jim Crotinger on 13-May-2016.
*/
@SuppressWarnings("WeakerAccess")
public class CyclicSubSeriesSmoother {
private final double[][] fRawCyclicSubSeries;
private final double[][] fSmoothedCyclicSubSeries;
private final double[][] fSubSeriesWeights;
private final int fPeriodLength;
private final int fNumPeriods;
private final int fRemainder;
private final int fNumPeriodsToExtrapolateBackward;
private final int fNumPeriodsToExtrapolateForward;
private final int fWidth;
private final LoessSmoother.Builder fLoessSmootherFactory;
/**
* Use Builder to simplify complex construction patterns.
*/
public static class Builder {
private Integer fWidth = null;
private Integer fDataLength = null;
private Integer fPeriodicity = null;
private Integer fNumPeriodsBackward = null;
private Integer fNumPeriodsForward = null;
private int fDegree = 1;
private int fJump = 1;
/**
* Set the width of the LOESS smoother used to smooth each seasonal sub-series.
*
* @param width width of the LOESS smoother
* @return this
*/
public Builder setWidth(int width) {
fWidth = width;
return this;
}
/**
* Set the degree of the LOESS smoother used to smooth each seasonal sub-series.
*
* @param degree degree of the LOESS smoother
* @return this
*/
public Builder setDegree(int degree) {
if (degree < 0 || degree > 2)
throw new IllegalArgumentException("Degree must be 0, 1 or 2");
fDegree = degree;
return this;
}
/**
* Set the jump (number of points to skip) between LOESS interpolations when smoothing the seasonal sub-series.
*
* Defaults to 1 (computes LOESS interpolation at each point).
*
* @param jump jump (number of points to skip) in the LOESS smoother
* @return this
*/
public Builder setJump(int jump) {
fJump = jump;
return this;
}
/**
* Set the total length of the data that will be deconstructed into cyclic sub-series.
*
* @param dataLength total length of the data
* @return this
*/
public Builder setDataLength(int dataLength) {
fDataLength = dataLength;
return this;
}
/**
* Set the period of the data's seasonality.
*
* @param periodicity number of data points in each season or period
* @return this
*/
public Builder setPeriodicity(int periodicity) {
fPeriodicity = periodicity;
return this;
}
/**
* Construct a smoother that will extrapolate forward only by the specified number of periods.
*
* @param periods number of periods to extrapolate
* @return this
*/
public Builder extrapolateForwardOnly(int periods) {
fNumPeriodsForward = periods;
fNumPeriodsBackward = 0;
return this;
}
/**
* Construct a smoother that extrapolates forward and backward by the specified number of periods.
*
* @param periods number of periods to extrapolate
* @return this
*/
public Builder extrapolateForwardAndBack(int periods) {
fNumPeriodsForward = periods;
fNumPeriodsBackward = periods;
return this;
}
/**
* Set the number of periods to extrapolate forward.
*
* Defaults to 1.
*
* @param periods number of periods to extrapolate
* @return this
*/
public Builder setNumPeriodsForward(int periods) {
fNumPeriodsForward = periods;
return this;
}
/**
* Set the number of periods to extrapolate backward.
*
* Defaults to 1.
*
* @param periods number of periods to extrapolate
* @return this
*/
public Builder setNumPeriodsBackward(int periods) {
fNumPeriodsBackward = periods;
return this;
}
/**
* Build the sub-series smoother.
*
* @return new CyclicSubSeriesSmoother
*/
public CyclicSubSeriesSmoother build() {
checkSanity();
return new CyclicSubSeriesSmoother(fWidth, fDegree, fJump, fDataLength, fPeriodicity,
fNumPeriodsBackward, fNumPeriodsForward);
}
private void checkSanity() {
if (fWidth == null)
throw new IllegalArgumentException(
"CyclicSubSeriesSmoother.Builder: setWidth must be called before building the smoother.");
if (fPeriodicity == null)
throw new IllegalArgumentException(
"CyclicSubSeriesSmoother.Builder: setPeriodicity must be called before building the smoother.");
if (fDataLength == null)
throw new IllegalArgumentException(
"CyclicSubSeriesSmoother.Builder: setDataLength must be called before building the smoother.");
if (fNumPeriodsBackward == null || fNumPeriodsForward == null)
throw new IllegalArgumentException(
"CyclicSubSeriesSmoother.Builder: Extrapolation settings must be provided.");
}
}
/**
* Create a cyclic sub-series smoother with the specified properties.
*
* @param width width of the LOESS smoother
* @param degree degree of the LOESS smoother
* @param jump jump to use in LOESS smoothing
* @param dataLength length of the input data
* @param periodicity length of the cyclic period
* @param numPeriodsToExtrapolateBackward number of periods to extrapolate backward
* @param numPeriodsToExtrapolateForward numbers of periods to extrapolate forward
*/
CyclicSubSeriesSmoother(int width, int degree, int jump,
int dataLength, int periodicity,
int numPeriodsToExtrapolateBackward, int numPeriodsToExtrapolateForward) {
fWidth = width;
fLoessSmootherFactory = new LoessSmoother.Builder().setWidth(width).setJump(jump).setDegree(degree);
fPeriodLength = periodicity;
fNumPeriods = dataLength / periodicity;
fRemainder = dataLength % periodicity;
fNumPeriodsToExtrapolateBackward = numPeriodsToExtrapolateBackward;
fNumPeriodsToExtrapolateForward = numPeriodsToExtrapolateForward;
fRawCyclicSubSeries = new double[periodicity][];
fSmoothedCyclicSubSeries = new double[periodicity][];
fSubSeriesWeights = new double[periodicity][];
// Bookkeeping: Write the data length as
//
// n = m * periodicity + r
//
// where r < periodicity. The first r sub-series will have length m + 1 and the remaining will have length m.
// Another way to look at this is that the cycle length is
//
// cycleLength = (n - p - 1) / periodicity + 1
//
// where p is the index of the cycle that we're currently in.
for (int period = 0; period < periodicity; ++period) {
int seriesLength = (period < fRemainder) ? (fNumPeriods + 1) : fNumPeriods;
fRawCyclicSubSeries[period] = new double[seriesLength];
fSmoothedCyclicSubSeries[period] = new double[fNumPeriodsToExtrapolateBackward + seriesLength
+ fNumPeriodsToExtrapolateForward];
fSubSeriesWeights[period] = new double[seriesLength];
}
}
/**
* Run the cyclic sub-series smoother on the specified data, with the specified weights (ignored if null). The
* sub-series are reconstructed into a single series in smoothedData.
*
* @param rawData input data
* @param smoothedData output data
* @param weights weights to use in the underlying interpolator; ignored if null.
*/
public void smoothSeasonal(double[] rawData, double[] smoothedData, double[] weights) {
extractRawSubSeriesAndWeights(rawData, weights);
computeSmoothedSubSeries(weights != null);
reconstructExtendedDataFromSubSeries(smoothedData);
// SeasonalTrendLoess.dumpDebugData("extended seasonal", smoothedData);
}
private void computeSmoothedSubSeries(boolean useResidualWeights) {
for (int period = 0; period < fPeriodLength; ++period) {
double[] weights = useResidualWeights ? fSubSeriesWeights[period] : null;
double[] rawData = fRawCyclicSubSeries[period];
double[] smoothedData = fSmoothedCyclicSubSeries[period];
smoothOneSubSeries(weights, rawData, smoothedData);
// dumpCyclicSubseriesDebugData(period, rawData.length, smoothedData, rawData);
}
}
private void extractRawSubSeriesAndWeights(double[] data, double[] weights) {
for (int period = 0; period < fPeriodLength; ++period) {
final int cycleLength = (period < fRemainder) ? (fNumPeriods + 1) : fNumPeriods;
for (int i = 0; i < cycleLength; ++i) {
fRawCyclicSubSeries[period][i] = data[i * fPeriodLength + period];
if (weights != null) {
fSubSeriesWeights[period][i] = weights[i * fPeriodLength + period];
}
}
}
}
private void reconstructExtendedDataFromSubSeries(double[] data) {
// Copy this smoothed cyclic sub-series to the extendedSeasonal work array.
for (int period = 0; period < fPeriodLength; ++period) {
final int cycleLength = (period < fRemainder) ? (fNumPeriods + 1) : fNumPeriods;
for (int i = 0; i < fNumPeriodsToExtrapolateBackward + cycleLength + fNumPeriodsToExtrapolateForward; ++i) {
data[i * fPeriodLength + period] = fSmoothedCyclicSubSeries[period][i];
}
}
}
/**
* Use LOESS interpolation on each of the cyclic sub-series (e.g. in monthly data, smooth the Januaries, Februaries,
* etc.).
*
* @param weights external weights for interpolation
* @param rawData input data to be smoothed
* @param smoothedData output smoothed data
*/
private void smoothOneSubSeries(double[] weights, double[] rawData, double[] smoothedData) {
final int cycleLength = rawData.length;
// Smooth the cyclic sub-series with LOESS and then extrapolate one place beyond each end.
LoessSmoother smoother = fLoessSmootherFactory.setData(rawData).setExternalWeights(weights).build();
// Copy, shifting by 1 to leave room for the extrapolated point at the beginning.
System.arraycopy(smoother.smooth(), 0, smoothedData, fNumPeriodsToExtrapolateBackward, cycleLength);
LoessInterpolator interpolator = smoother.getInterpolator();
// Extrapolate from the leftmost "width" points to the "-1" position
int left = 0;
int right = left + fWidth - 1;
right = Math.min(right, cycleLength - 1);
int leftValue = fNumPeriodsToExtrapolateBackward;
for (int i = 1; i <= fNumPeriodsToExtrapolateBackward; i++) {
Double ys = interpolator.smoothOnePoint(-i, left, right);
smoothedData[leftValue - i] = ys == null ? smoothedData[leftValue] : ys;
}
// Extrapolate from the rightmost "width" points to the "length" position (one past the array end).
right = cycleLength - 1;
left = right - fWidth + 1;
left = Math.max(0, left);
int rightValue = fNumPeriodsToExtrapolateBackward + right;
for (int i = 1; i <= fNumPeriodsToExtrapolateForward; i++) {
Double ys = interpolator.smoothOnePoint(right + i, left, right);
smoothedData[rightValue + i] = ys == null ? smoothedData[rightValue] : ys;
}
}
// @SuppressWarnings("unused")
// private static void dumpCyclicSubseriesDebugData(int p, int cycleLength, double[] smoothedData, double[] inputData) {
// System.out.println(String.format("subcycle smoother at j = %d", p));
// System.out.println(String.format(" , smoothed(%2d) = %22.15e", 0, smoothedData[0]));
// for (int i = 0; i < cycleLength; ++i) {
// System.out.println(String.format("y(%2d) = %22.15e, smoothed(%2d) = %22.15e", i, inputData[i], i + 1,
// smoothedData[i + 1]));
// }
// System.out.println(String.format(" , smoothed(%2d) = %22.15e", cycleLength + 1,
// smoothedData[cycleLength + 1]));
// }
}
Ads help maintain this website.