diff --git a/src/main/java/ru/ulstu/controllers/TimeSeriesController.java b/src/main/java/ru/ulstu/controllers/TimeSeriesController.java index 835cbea..19b58bc 100644 --- a/src/main/java/ru/ulstu/controllers/TimeSeriesController.java +++ b/src/main/java/ru/ulstu/controllers/TimeSeriesController.java @@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import ru.ulstu.configurations.ApiConfiguration; -import ru.ulstu.models.Forecast; import ru.ulstu.models.ForecastParams; import ru.ulstu.models.TimeSeries; import ru.ulstu.models.exceptions.ModelingException; @@ -52,7 +51,7 @@ public class TimeSeriesController { @PostMapping("getForecast") @ApiOperation("Получить прогноз временного ряда") - public ResponseEntity getForecastTimeSeries(@RequestBody ForecastParams forecastParams) throws ModelingException { + public ResponseEntity getForecastTimeSeries(@RequestBody ForecastParams forecastParams) throws ModelingException { return new ResponseEntity<>(timeSeriesService.getForecast(forecastParams.getOriginalTimeSeries(), forecastParams.getCountForecast()), HttpStatus.OK); } diff --git a/src/main/java/ru/ulstu/models/Forecast.java b/src/main/java/ru/ulstu/models/Forecast.java deleted file mode 100644 index 383e8fc..0000000 --- a/src/main/java/ru/ulstu/models/Forecast.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.ulstu.models; - -public class Forecast { - private Model model; - private TimeSeries forecast; - - public Forecast(Model model) { - this.model = model; - this.forecast = new TimeSeries("Forecast time series of '" + model.getOriginalTimeSeries().getName() + "'"); - } - - public Model getModel() { - return model; - } - - public TimeSeries getForecastTimeSeries() { - return forecast; - } - - public void addValue(TimeSeriesValue timeSeriesValue) { - forecast.addValue(timeSeriesValue); - } - - @Override - public String toString() { - return "Forecast{" + - "model=" + model + - ", forecast=" + forecast + - '}'; - } -} diff --git a/src/main/java/ru/ulstu/models/Model.java b/src/main/java/ru/ulstu/models/Model.java deleted file mode 100644 index 534e112..0000000 --- a/src/main/java/ru/ulstu/models/Model.java +++ /dev/null @@ -1,27 +0,0 @@ -package ru.ulstu.models; - -public class Model { - private TimeSeries originalTimeSeries; - private TimeSeries modelTimeSeries; - - public Model(TimeSeries originalTimeSeries) { - this.originalTimeSeries = originalTimeSeries; - this.modelTimeSeries = new TimeSeries("Model time series of '" + originalTimeSeries.getName() + "'"); - } - - public TimeSeries getOriginalTimeSeries() { - return originalTimeSeries; - } - - public TimeSeries getModelTimeSeries() { - return modelTimeSeries; - } - - public void addValue(TimeSeriesValue timeSeriesValue) { - modelTimeSeries.addValue(timeSeriesValue); - } - - public void addValue(TimeSeriesValue basedOnValue, double value) { - modelTimeSeries.getValues().add(new TimeSeriesValue(basedOnValue.getDate(), value)); - } -} diff --git a/src/main/java/ru/ulstu/models/TimeSeries.java b/src/main/java/ru/ulstu/models/TimeSeries.java index 68d0b1f..4659225 100644 --- a/src/main/java/ru/ulstu/models/TimeSeries.java +++ b/src/main/java/ru/ulstu/models/TimeSeries.java @@ -53,7 +53,7 @@ public class TimeSeries { } public void addValue(TimeSeriesValue basedOnValue, Double value) { - values.add(new TimeSeriesValue(basedOnValue.getDate().plusDays(1), value)); + values.add(new TimeSeriesValue(basedOnValue.getDate(), value)); } public TimeSeriesValue getLastValue() { diff --git a/src/main/java/ru/ulstu/pages/IndexView.java b/src/main/java/ru/ulstu/pages/IndexView.java index be8bfb4..d65865b 100644 --- a/src/main/java/ru/ulstu/pages/IndexView.java +++ b/src/main/java/ru/ulstu/pages/IndexView.java @@ -48,7 +48,7 @@ public class IndexView implements Serializable { LineChartSeries series2 = new LineChartSeries(); series2.setLabel("Сглаженный ряд"); try { - for (TimeSeriesValue value : timeSeriesService.getForecast(timeSeries, 10).getModel().getModelTimeSeries().getValues()) { + for (TimeSeriesValue value : timeSeriesService.smoothTimeSeries(timeSeries).getValues()) { series2.set(DateTimeFormatter.ISO_LOCAL_DATE.format(value.getDate()), value.getValue()); } } catch (ModelingException ex) { @@ -59,7 +59,7 @@ public class IndexView implements Serializable { LineChartSeries series3 = new LineChartSeries(); series3.setLabel("Прогноз"); try { - for (TimeSeriesValue value : timeSeriesService.getForecast(timeSeries, 10).getForecastTimeSeries().getValues()) { + for (TimeSeriesValue value : timeSeriesService.getForecast(timeSeries, 5).getValues()) { series3.set(DateTimeFormatter.ISO_LOCAL_DATE.format(value.getDate()), value.getValue()); } } catch (ModelingException ex) { diff --git a/src/main/java/ru/ulstu/services/TimeSeriesService.java b/src/main/java/ru/ulstu/services/TimeSeriesService.java index f45b343..f2165e6 100644 --- a/src/main/java/ru/ulstu/services/TimeSeriesService.java +++ b/src/main/java/ru/ulstu/services/TimeSeriesService.java @@ -4,19 +4,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import ru.ulstu.TimeSeriesUtils; -import ru.ulstu.models.Forecast; import ru.ulstu.models.TimeSeries; import ru.ulstu.models.TimeSeriesValue; import ru.ulstu.models.exceptions.ModelingException; +import ru.ulstu.tsMethods.exponential.AddTrendNoSeason; import ru.ulstu.tsMethods.exponential.ExponentialMethodParams; -import ru.ulstu.tsMethods.exponential.ExponentialParamName; -import ru.ulstu.tsMethods.exponential.NoTrendNoSeason; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import static ru.ulstu.tsMethods.exponential.ExponentialParamName.ALPHA; +import static ru.ulstu.tsMethods.exponential.ExponentialParamName.BETA; + @Service public class TimeSeriesService { @@ -32,9 +33,16 @@ public class TimeSeriesService { return ts; } - public Forecast getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException { - NoTrendNoSeason nn = new NoTrendNoSeason(ExponentialMethodParams.of(ExponentialParamName.ALPHA, 0.8)); - return nn.getForecast(timeSeries, countPoints); + public TimeSeries getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException { + //NoTrendNoSeason nn = new NoTrendNoSeason(ExponentialMethodParams.of(ExponentialParamName.ALPHA, 0.8)); + AddTrendNoSeason an = new AddTrendNoSeason(timeSeries, ExponentialMethodParams.of(ALPHA, 0.8, BETA, 0.8)); + return an.getForecast(countPoints); + } + + public TimeSeries smoothTimeSeries(TimeSeries timeSeries) throws ModelingException { + //NoTrendNoSeason nn = new NoTrendNoSeason(timeSeries, ExponentialMethodParams.of(ExponentialParamName.ALPHA, 0.8)); + AddTrendNoSeason an = new AddTrendNoSeason(timeSeries, ExponentialMethodParams.of(ALPHA, 0.8, BETA, 0.8)); + return an.getModel(); } public TimeSeries getTimeSeriesFromString(String tsString) { diff --git a/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java b/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java index 23adffb..969acae 100644 --- a/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java +++ b/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java @@ -1,8 +1,6 @@ package ru.ulstu.tsMethods; import ru.ulstu.TimeSeriesUtils; -import ru.ulstu.models.Forecast; -import ru.ulstu.models.Model; import ru.ulstu.models.TimeSeries; import ru.ulstu.models.TimeSeriesValue; import ru.ulstu.models.exceptions.ForecastValidateException; @@ -15,44 +13,44 @@ import java.time.temporal.ChronoUnit; * Наиболее общая логика моделировани и прогнозирования временных рядов */ public abstract class TimeSeriesMethod { + protected TimeSeries originalTimeSeries; + private TimeSeries model; + + public TimeSeriesMethod(TimeSeries originalTimeSeries) throws ModelingException { + this.originalTimeSeries = originalTimeSeries; + } + /** * Возвращает модельное представление временного ряда: для тех же точек времени что и в параметре timeSeries * строится модель. Количество точек может быть изменено: сокращено при сжатии ряда, увеличено при интерполяции. * Метод является шаблонным, выполняет операции валидации исходного ряда и потом его моделирование * - * @param timeSeries исходный временной ряд подлежащий моделированию * @return модель временного ряда * @throws TimeSeriesValidateException */ - public Model getModel(TimeSeries timeSeries) throws ModelingException { - validateTimeSeries(timeSeries); - return getModelOfValidTimeSeries(timeSeries); + protected void makeModel() throws ModelingException { + validateTimeSeries(); + model = getModelOfValidTimeSeries(); } /** * Возвращает модельное представление валидного временного ряда: для тех же точек времени что и в параметре timeSeries * строится модель. Количество точек может быть изменено: сокращено при сжатии ряда, увеличено при интерполяции. * - * @param timeSeries исходный временной ряд подлежащий моделированию * @return */ - protected abstract Model getModelOfValidTimeSeries(TimeSeries timeSeries) throws ModelingException; + protected abstract TimeSeries getModelOfValidTimeSeries() throws ModelingException; /** * Выполняет построение прогноза временного ряда. Даты спрогнозированных точек будут сгенерированы по модельным точкам. * - * @param model модель временного ряда * @param countPoints количество точек для прогнозирования * @return прогноз временного ряда */ - public Forecast getForecast(Model model, int countPoints) throws ModelingException { - Forecast forecast = new Forecast(model); - forecast = generateEmptyForecastPoints(forecast, countPoints); + public TimeSeries getForecastWithValidParams(int countPoints) throws ModelingException { + TimeSeries forecast = generateEmptyForecastPoints(originalTimeSeries, countPoints); + forecast.getFirstValue().setValue(getModel().getLastValue().getValue()); forecast = makeForecast(forecast); - if (!forecast.getForecastTimeSeries().getFirstValue() - .equals(forecast.getModel().getModelTimeSeries().getLastValue())) { - throw new ForecastValidateException("Первая точка прогноза должна совпадать с последней модельной точкой"); - } return forecast; } @@ -62,15 +60,14 @@ public abstract class TimeSeriesMethod { * @param forecast Заготовка прогноза временного ряда с пустыми значениями * @return */ - protected abstract Forecast makeForecast(Forecast forecast); + protected abstract TimeSeries makeForecast(TimeSeries forecast) throws ModelingException; - protected Forecast generateEmptyForecastPoints(Forecast forecast, int countPointForecast) { - long diffMilliseconds = TimeSeriesUtils.getTimeDifferenceInMilliseconds(forecast.getModel().getOriginalTimeSeries()); - forecast.getForecastTimeSeries() - .addValue(new TimeSeriesValue(forecast.getModel().getModelTimeSeries().getLastValue().getDate())); + protected TimeSeries generateEmptyForecastPoints(TimeSeries modelTimeSeries, int countPointForecast) { + long diffMilliseconds = TimeSeriesUtils.getTimeDifferenceInMilliseconds(originalTimeSeries); + TimeSeries forecast = new TimeSeries("Forecast of " + originalTimeSeries.getName()); + forecast.addValue(new TimeSeriesValue(modelTimeSeries.getLastValue().getDate())); for (int i = 1; i < countPointForecast + 1; i++) { - forecast.getForecastTimeSeries() - .addValue(new TimeSeriesValue(forecast.getForecastTimeSeries().getValues().get(i - 1).getDate().plus(diffMilliseconds, ChronoUnit.MILLIS))); + forecast.addValue(new TimeSeriesValue(forecast.getValues().get(i - 1).getDate().plus(diffMilliseconds, ChronoUnit.MILLIS))); } return forecast; } @@ -79,13 +76,12 @@ public abstract class TimeSeriesMethod { * Выполняет построение модели и прогноза временного ряда. Даты спрогнозированных точек будут сгенерированы * по модельным точкам. * - * @param timeSeries временной ряда * @param countPoints количество точек для прогнозирования * @return прогноз временного ряда */ - public Forecast getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException { + public TimeSeries getForecast(int countPoints) throws ModelingException { validateForecastParams(countPoints); - return getForecast(getModel(timeSeries), countPoints); + return getForecastWithValidParams(countPoints); } private void validateForecastParams(int countPoints) throws ForecastValidateException { @@ -94,18 +90,25 @@ public abstract class TimeSeriesMethod { } } - private void validateTimeSeries(TimeSeries timeSeries) throws TimeSeriesValidateException { - if (timeSeries == null || timeSeries.isEmpty()) { + private void validateTimeSeries() throws TimeSeriesValidateException { + if (originalTimeSeries == null || originalTimeSeries.isEmpty()) { throw new TimeSeriesValidateException("Временной ряд должен быть не пустым"); } - if (timeSeries.getLength() < 2) { + if (originalTimeSeries.getLength() < 2) { throw new TimeSeriesValidateException("Временной ряд должен содержать хотя бы 2 точки"); } - if (timeSeries.getValues().stream().anyMatch(val -> val == null || val.getValue() == null)) { + if (originalTimeSeries.getValues().stream().anyMatch(val -> val == null || val.getValue() == null)) { throw new TimeSeriesValidateException("Временной ряд содержит пустые значения"); } - if (timeSeries.getValues().stream().anyMatch(val -> val.getDate() == null)) { + if (originalTimeSeries.getValues().stream().anyMatch(val -> val.getDate() == null)) { throw new TimeSeriesValidateException("Временной ряд должен иметь отметки времени"); } } + + public TimeSeries getModel() throws ModelingException { + if (model == null) { + makeModel(); + } + return model; + } } diff --git a/src/main/java/ru/ulstu/tsMethods/exponential/AddTrendNoSeason.java b/src/main/java/ru/ulstu/tsMethods/exponential/AddTrendNoSeason.java new file mode 100644 index 0000000..b61f394 --- /dev/null +++ b/src/main/java/ru/ulstu/tsMethods/exponential/AddTrendNoSeason.java @@ -0,0 +1,61 @@ +package ru.ulstu.tsMethods.exponential; + +import ru.ulstu.models.TimeSeries; +import ru.ulstu.models.exceptions.ModelingException; +import ru.ulstu.tsMethods.TimeSeriesMethod; + +import java.util.ArrayList; +import java.util.List; + +import static ru.ulstu.tsMethods.exponential.ExponentialParamName.ALPHA; +import static ru.ulstu.tsMethods.exponential.ExponentialParamName.BETA; + +public class AddTrendNoSeason extends TimeSeriesMethod { + private final ExponentialMethodParams exponentialMethodParams; + private final List sComponent = new ArrayList<>(); + private final List tComponent = new ArrayList<>(); + + public AddTrendNoSeason(TimeSeries timeSeries, ExponentialMethodParams exponentialMethodParams) throws ModelingException { + super(timeSeries); + this.exponentialMethodParams = exponentialMethodParams; + } + + @Override + protected TimeSeries getModelOfValidTimeSeries() throws ModelingException { + sComponent.clear(); + tComponent.clear(); + sComponent.add(originalTimeSeries.getFirstValue().getValue()); + tComponent.add(originalTimeSeries.getValues().get(1).getValue() - originalTimeSeries.getValues().get(0).getValue()); + TimeSeries model = new TimeSeries("Model of " + originalTimeSeries.getName()); + model.addValue(originalTimeSeries.getFirstValue()); + //выполняется проход модели по сглаживанию + for (int t = 1; t < originalTimeSeries.getValues().size(); t++) { + sComponent.add(exponentialMethodParams.getValue(ALPHA) * originalTimeSeries.getNumericValue(t) + + (1 - exponentialMethodParams.getValue(ALPHA)) + * (sComponent.get(t - 1) - tComponent.get(t - 1))); + + tComponent.add(exponentialMethodParams.getValue(BETA) + * (sComponent.get(t) - sComponent.get(t - 1)) + + (1 - exponentialMethodParams.getValue(BETA)) * tComponent.get(t - 1)); + model.addValue(originalTimeSeries.getValues().get(t), sComponent.get(sComponent.size() - 1)); + } + return model; + } + + @Override + protected TimeSeries makeForecast(TimeSeries forecast) throws ModelingException { + for (int t = 1; t < forecast.getLength(); t++) { + /*int indexOffsetForModel = t + getModel().getLength() - 2; + sComponent.add(exponentialMethodParams.getValue(ALPHA) * forecast.getNumericValue(t-1) + + (1 - exponentialMethodParams.getValue(ALPHA)) + * (sComponent.get(indexOffsetForModel) - tComponent.get(indexOffsetForModel - 1))); + + tComponent.add(exponentialMethodParams.getValue(BETA) + * (sComponent.get(indexOffsetForModel) - sComponent.get(indexOffsetForModel - 1)) + + (1 - exponentialMethodParams.getValue(BETA)) * tComponent.get(indexOffsetForModel - 1));*/ + + forecast.getValues().get(t).setValue(sComponent.get(sComponent.size() - 1) + tComponent.get(tComponent.size() - 1) * t); + } + return forecast; + } +} diff --git a/src/main/java/ru/ulstu/tsMethods/exponential/ExponentialMethodParams.java b/src/main/java/ru/ulstu/tsMethods/exponential/ExponentialMethodParams.java index e3364e6..a14a827 100644 --- a/src/main/java/ru/ulstu/tsMethods/exponential/ExponentialMethodParams.java +++ b/src/main/java/ru/ulstu/tsMethods/exponential/ExponentialMethodParams.java @@ -23,4 +23,8 @@ public class ExponentialMethodParams { public static ExponentialMethodParams of(ExponentialParamName param1, Double value1) { return new ExponentialMethodParams(ImmutableMap.of(param1, value1)); } + + public static ExponentialMethodParams of(ExponentialParamName param1, Double value1, ExponentialParamName param2, Double value2) { + return new ExponentialMethodParams(ImmutableMap.of(param1, value1, param2, value2)); + } } diff --git a/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java b/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java index 176c65b..3464385 100644 --- a/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java +++ b/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java @@ -1,43 +1,43 @@ package ru.ulstu.tsMethods.exponential; -import ru.ulstu.models.Forecast; -import ru.ulstu.models.Model; import ru.ulstu.models.TimeSeries; import ru.ulstu.models.exceptions.ModelingException; import ru.ulstu.tsMethods.TimeSeriesMethod; +import java.util.ArrayList; +import java.util.List; + import static ru.ulstu.tsMethods.exponential.ExponentialParamName.ALPHA; public class NoTrendNoSeason extends TimeSeriesMethod { - private ExponentialMethodParams exponentialMethodParams; + private final ExponentialMethodParams exponentialMethodParams; + private final List sComponent = new ArrayList<>(); - public NoTrendNoSeason(ExponentialMethodParams exponentialMethodParams) { + public NoTrendNoSeason(TimeSeries timeSeries, ExponentialMethodParams exponentialMethodParams) throws ModelingException { + super(timeSeries); this.exponentialMethodParams = exponentialMethodParams; } @Override - protected Model getModelOfValidTimeSeries(TimeSeries timeSeries) throws ModelingException { - Model model = new Model(timeSeries); - model.addValue(timeSeries.getFirstValue()); + protected TimeSeries getModelOfValidTimeSeries() throws ModelingException { + sComponent.clear(); + sComponent.add(originalTimeSeries.getFirstValue().getValue()); + TimeSeries model = new TimeSeries("Model of " + originalTimeSeries.getName()); + model.addValue(originalTimeSeries.getFirstValue()); //выполняется проход модели по сглаживанию - for (int t = 1; t < timeSeries.getValues().size(); t++) { - model.addValue(timeSeries.getValues().get(t), - (1 - exponentialMethodParams.getValue(ALPHA)) * timeSeries.getNumericValue(t) - + exponentialMethodParams.getValue(ALPHA) * model.getModelTimeSeries().getValues().get(t - 1).getValue()); + for (int t = 1; t < originalTimeSeries.getValues().size(); t++) { + sComponent.add(sComponent.get(t - 1) + + exponentialMethodParams.getValue(ALPHA) + * (originalTimeSeries.getNumericValue(t) - sComponent.get(t - 1))); + model.addValue(originalTimeSeries.getValues().get(t), sComponent.get(sComponent.size() - 1)); } return model; } @Override - protected Forecast makeForecast(Forecast forecast) { - forecast.getForecastTimeSeries() - .getFirstValue() - .setValue(forecast.getModel().getModelTimeSeries().getLastValue().getValue()); - for (int t = 1; t < forecast.getForecastTimeSeries().getLength(); t++) { - forecast.getForecastTimeSeries() - .getValues() - .get(t) - .setValue(forecast.getForecastTimeSeries().getValues().get(t - 1).getValue()); + protected TimeSeries makeForecast(TimeSeries forecast) { + for (int t = 1; t < forecast.getLength(); t++) { + forecast.getValues().get(t).setValue(forecast.getValues().get(t - 1).getValue()); } return forecast; }