SeasonalTrendLoess { public Decomposition decompose() { fDecomposition = new Decomposition(fData); int outerIteration = 0; while (true) { boolean useResidualWeights = outerIteration > 0; for (int iteration = 0; iteration < fInnerIterations; ++iteration) { smoothSeasonalSubCycles(useResidualWeights); removeSeasonality(); updateSeasonalAndTrend(useResidualWeights); } if (++outerIteration > fRobustIterations) break; fDecomposition.computeResidualWeights(); } fDecomposition.updateResiduals(); Decomposition result = fDecomposition; fDecomposition = null; return result; } @SuppressWarnings("WeakerAccess")  SeasonalTrendLoess(double[] data, int periodicity, int ni, int no, LoessSettings seasonalSettings,
	                   LoessSettings trendSettings, LoessSettings lowpassSettings); static Decomposition performPeriodicDecomposition(
			double[] data,
			int periodicity
	); static Decomposition performRobustPeriodicDecomposition(
			double[] data,
			int periodicity
	); Decomposition decompose(); @Override String toString();  }
@Test public void setPeriodicTest() { double[] data = new double[144]; for (int i = 0; i < data.length; ++i) { data[i] = fNonRobustNoisySinusoidResults[i][0]; } SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12); builder.setPeriodic(); builder.setTrendWidth(23); builder.setLowpassWidth(13); builder.setInnerIterations(2).setRobustnessIterations(0); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double epsilon = 2e-8; double[] seasonal = stl.getSeasonal(); for (int i = 0; i < 12; ++i) { for (int p = 0; p < 12; ++p) { assertEquals(seasonal[i], seasonal[i + p * 12], epsilon); } } }
@Test public void forcedPeriodicityTest2() { double[] data = new double[144]; for (int i = 0; i < data.length; ++i) { data[i] = fNonRobustNoisySinusoidResults[i][0]; } SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12); builder = builder.setSeasonalWidth(100000001).setSeasonalDegree(0).setSeasonalJump(100000001); builder = builder.setTrendWidth(23).setTrendDegree(0).setTrendJump(100000); builder = builder.setLowpassWidth(13).setLowpassDegree(0).setLowpassJump(1); builder = builder.setInnerIterations(2).setRobustnessIterations(0); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double epsilon = 1.0e-10; double[] seasonal = stl.getSeasonal(); for (int i = 0; i < 12; ++i) { for (int p = 0; p < 12; ++p) { assertEquals(seasonal[i], seasonal[i + p * 12], epsilon); } } }
@Test public void linearTrendTest() { long seed = 1234567L; double[] data = testDataGenerator.createNoisySeasonalData(144, 12, 1.0, 0.2, 0.1, seed); SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setLinearTrend().setPeriodLength(12).setSeasonalWidth(1000000).setRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double[] trend = stl.getTrend(); double dt = trend[1] - trend[0]; for (int i = 1; i < trend.length; ++i) assertEquals(dt, trend[i] - trend[i-1], 1.0e-14); double dx = 2 * Math.PI / 12; assertEquals(dt, 0.2 * dx, 1.0e-4); }
@Test public void flatTrendTest() { long seed = 1234567L; double[] data = testDataGenerator.createNoisySeasonalData(144, 12, 1.0, 0.0, 0.1, seed); SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setFlatTrend().setPeriodLength(12).setSeasonalWidth(1000000).setRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double[] trend = stl.getTrend(); for (int i = 1; i < trend.length; ++i) assertEquals(0.0, trend[i] - trend[i-1], 1.0e-13); }
@Test public void sineWithOutlierTest() { long seed = 1234567L; double[] data = testDataGenerator.createNoisySeasonalData(144, 12, 1.0, 0.0, 0.0, seed); data[100] = 1000; SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12).setSeasonalWidth(1000000).setRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double epsilon = 1.0e-4; double[] trend = stl.getTrend(); double[] seasonal = stl.getSeasonal(); double[] residuals = stl.getResidual(); for (int i = 0; i < data.length; ++i) { if (i != 100) { assertEquals(String.format("seasonal[%d]", i), data[i], seasonal[i], epsilon); assertEquals(String.format("trend[%d]", i), 0.0, trend[i], epsilon); assertEquals(String.format("residuals[%d]", i), 0.0, residuals[i], epsilon); } else { assertEquals(String.format("seasonal[%d]", i), data[i - 12], seasonal[i], epsilon); assertEquals(String.format("trend[%d]", i), 0.0, trend[i], epsilon); assertEquals(String.format("residuals[%d]", i), 1.0, residuals[i] / 1000.0, 1.0e-3); } } }
@Test public void robustRegressionTest() { double[] data = new double[144]; for (int i = 0; i < data.length; ++i) { data[i] = fRobustNoisySinusoidResults[i][0]; } SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12).setSeasonalWidth(7); builder.setInnerIterations(1).setRobustnessIterations(1); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double epsilon = 2.0e-07; double[] trend = stl.getTrend(); double[] seasonal = stl.getSeasonal(); double[] residuals = stl.getResidual(); for (int i = 0; i < data.length; ++i) { assertEquals(String.format("seasonal[%d]", i), fRobustNoisySinusoidResults[i][1], trend[i], epsilon); assertEquals(String.format("trend[%d]", i), fRobustNoisySinusoidResults[i][2], seasonal[i], epsilon); assertEquals(String.format("residuals[%d]", i), fRobustNoisySinusoidResults[i][3], residuals[i], epsilon); } }
@Test public void periodicBuilderCanBeReused() { double[] data = SimulatedWeeklyMetric.getFourWeekValues(); int periodicity = 1008; SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder(); builder.setPeriodLength(periodicity) .setRobust() .setPeriodic() .setFlatTrend(); SeasonalTrendLoess stlSmoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = stlSmoother.decompose(); assertNotNull(stl); builder.setRobustnessIterations(17); stlSmoother = builder.buildSmoother(data); stl = stlSmoother.decompose(); assertNotNull(stl); }
@Test public void linearTrendBuilderCanBeReused() { double[] data = SimulatedWeeklyMetric.getFourWeekValues(); int periodicity = 1008; SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder(); builder.setPeriodLength(periodicity) .setRobust() .setLinearTrend() .setSeasonalWidth(101); SeasonalTrendLoess stlSmoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = stlSmoother.decompose(); assertNotNull(stl); builder.setRobustnessIterations(17); stlSmoother = builder.buildSmoother(data); stl = stlSmoother.decompose(); assertNotNull(stl); }
@Test public void flatTrendBuilderCanBeReused() { double[] data = SimulatedWeeklyMetric.getFourWeekValues(); int periodicity = 1008; SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder(); builder.setPeriodLength(periodicity) .setRobust() .setFlatTrend() .setSeasonalWidth(101); SeasonalTrendLoess stlSmoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = stlSmoother.decompose(); assertNotNull(stl); builder.setRobustnessIterations(17); stlSmoother = builder.buildSmoother(data); stl = stlSmoother.decompose(); assertNotNull(stl); }
@Ignore("For manual testing only") @Test public void pureNoiseTest() { double[] data = testDataGenerator.createNoisySeasonalDataWithTimeSeed(144, 12, 0.0, 0.0, 1.0); SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12).setSeasonalWidth(7).setRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double[] trend = stl.getTrend(); double[] seasonal = stl.getSeasonal(); double[] residuals = stl.getResidual(); double rsum = 0.0; double r2sum = 0.0; double dsum = 0.0; double d2sum = 0.0; double tsum = 0.0; double t2sum = 0.0; double ssum = 0.0; double s2sum = 0.0; for (int i = 0; i < data.length; ++i) { final double r = residuals[i]; rsum += r; r2sum += r * r; final double d = data[i]; dsum += d; d2sum += d * d; final double t = trend[i]; tsum += t; t2sum += t * t; final double s = seasonal[i]; ssum += s; s2sum += s * s; } double trendMean = tsum / data.length; double trendVar = (t2sum - trendMean * trendMean) / (data.length - 1); double resMean = rsum / data.length; double resVar = (r2sum - resMean * resMean) / (data.length - 1); double dataMean = dsum / data.length; double dataVar = (d2sum - dataMean * dataMean) / (data.length - 1); double seasonalMean = ssum / data.length; double seasonalVar = (s2sum - seasonalMean * seasonalMean) / (data.length - 1); final double stlMean = seasonalMean + trendMean + resMean; assertEquals(dataMean, stlMean, 1.0e-8); final double stlVar = seasonalVar + trendVar + resVar; System.out.println(" \t\tmean\tvariance"); System.out.println(String.format("data \t%f\t%f", dataMean, dataVar)); System.out.println(String.format("seasonal \t%f\t%f", seasonalMean, seasonalVar)); System.out.println(String.format("trend \t%f\t%f", trendMean, trendVar)); System.out.println(String.format("residual \t%f\t%f", resMean, resVar)); System.out.println(String.format("stl sum \t%f\t%f", stlMean, stlVar)); }
@Ignore("This is just for data generation so we can compare the Java results in Python") @Test public void stlDataFactory() { double[] data = testDataGenerator.createNoisySeasonalDataWithTimeSeed(144, 12, 10.0, 1.0, 2.0); SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12).setSeasonalWidth(7).setNonRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double[] residuals = stl.getResidual(); double rsum = 0.0; double r2sum = 0.0; for (int i = 0; i < data.length; ++i) { double r = residuals[i]; rsum += r; r2sum += r * r; } double mean = rsum / data.length; double variance = (r2sum - rsum * rsum) / (data.length - 1); System.out.println(String.format("Residual has mean %f and variance %f", mean, variance)); }
@Test public void pureSineTest() { long seed = 1234567L; double[] data = testDataGenerator.createNoisySeasonalData(144, 12, 1.0, 0.0, 0.0, seed); SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12).setSeasonalWidth(7).setNonRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double[] trend = stl.getTrend(); double[] seasonal = stl.getSeasonal(); double[] residuals = stl.getResidual(); for (int i = 0; i < data.length; ++i) { assertEquals(data[i], seasonal[i], 1.0e-14); assertEquals(0.0, trend[i], 1.0e-14); assertEquals(0.0, residuals[i], 1.0e-14); } }
@Test public void pureTrendTest() { long seed = 1234567L; double[] data = testDataGenerator.createNoisySeasonalData(144, 12, 0.0, 1.0, 0.0, seed); SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12).setSeasonalWidth(7).setNonRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double[] trend = stl.getTrend(); double[] seasonal = stl.getSeasonal(); double[] residuals = stl.getResidual(); double[] weights = stl.getWeights(); for (int i = 0; i < data.length; ++i) { assertEquals(data[i], trend[i], 1.0e-12); assertEquals(0.0, seasonal[i], 1.0e-12); assertEquals(0.0, residuals[i], 1.0e-12); assertEquals(1.0, weights[i], 1.0e-13); } }
@Test public void squareWaveTest() { double[] data = testDataGenerator.createSquareWaveData(); SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(288).setSeasonalWidth(13).setNonRobust(); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double[] trend = stl.getTrend(); double[] seasonal = stl.getSeasonal(); double[] residuals = stl.getResidual(); double[] weights = stl.getWeights(); for (int i = 0; i < data.length; ++i) { assertEquals(42.5, trend[i], 1.0e-12); assertEquals(data[i], seasonal[i] + stl.getTrend()[i], 1.0e-12); assertEquals(0.0, residuals[i], 1.0e-12); assertEquals(1.0, weights[i], 1.0e-13); } }
@Test public void nonRobustRegressionTest() { double[] data = new double[144]; for (int i = 0; i < data.length; ++i) { data[i] = fNonRobustNoisySinusoidResults[i][0]; } SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12).setSeasonalWidth(7); builder.setInnerIterations(2).setRobustnessIterations(0); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double epsilon = 7.0e-11; double[] trend = stl.getTrend(); double[] seasonal = stl.getSeasonal(); double[] residuals = stl.getResidual(); double[] weights = stl.getWeights(); for (int i = 0; i < data.length; ++i) { assertEquals(String.format("trend[%d]", i), fNonRobustNoisySinusoidResults[i][1], trend[i], epsilon); assertEquals(String.format("seasonal[%d]", i), fNonRobustNoisySinusoidResults[i][2], seasonal[i], epsilon); assertEquals(String.format("residuals[%d]", i), fNonRobustNoisySinusoidResults[i][3], residuals[i], epsilon); assertEquals(1.0, weights[i], 1.0e-13); } }
@Test public void forcedPeriodicityTest() { double[] data = new double[144]; for (int i = 0; i < data.length; ++i) { data[i] = fNonRobustNoisySinusoidResults[i][0]; } SeasonalTrendLoess.Builder builder = new SeasonalTrendLoess.Builder().setPeriodLength(12); builder.setSeasonalWidth(100000001).setSeasonalDegree(0).setSeasonalJump(100000001); builder.setTrendWidth(23); builder.setLowpassWidth(13); builder.setInnerIterations(2).setRobustnessIterations(0); SeasonalTrendLoess smoother = builder.buildSmoother(data); SeasonalTrendLoess.Decomposition stl = smoother.decompose(); double epsilon = 0.0; double[] seasonal = stl.getSeasonal(); for (int i = 0; i < 12; ++i) { for (int p = 0; p < 12; ++p) { assertEquals(seasonal[i], seasonal[i + p * 12], epsilon); } } }