diff --git a/src/main/java/ru/ulstu/controllers/TimeSeriesController.java b/src/main/java/ru/ulstu/controllers/TimeSeriesController.java index 48a4224..86c1752 100644 --- a/src/main/java/ru/ulstu/controllers/TimeSeriesController.java +++ b/src/main/java/ru/ulstu/controllers/TimeSeriesController.java @@ -13,6 +13,7 @@ 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; import ru.ulstu.services.TimeSeriesService; @RestController @@ -39,7 +40,7 @@ public class TimeSeriesController { @PostMapping("getForecast") @ApiOperation("Получить прогноз временного ряда") - public ResponseEntity getForecastTimeSeries(@RequestBody ForecastParams forecastParams) { + 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 index c3413fc..383e8fc 100644 --- a/src/main/java/ru/ulstu/models/Forecast.java +++ b/src/main/java/ru/ulstu/models/Forecast.java @@ -1,23 +1,31 @@ package ru.ulstu.models; public class Forecast { - private TimeSeries originalTimeSeries; + private Model model; private TimeSeries forecast; - public Forecast(TimeSeries originalTimeSeries) { - this.originalTimeSeries = originalTimeSeries; - this.forecast = new TimeSeries("Forecast time series of '" + originalTimeSeries.getName() + "'"); + public Forecast(Model model) { + this.model = model; + this.forecast = new TimeSeries("Forecast time series of '" + model.getOriginalTimeSeries().getName() + "'"); } - public TimeSeries getOriginalTimeSeries() { - return originalTimeSeries; + public Model getModel() { + return model; } - public TimeSeries getForecast() { + 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/ModelTimeSeries.java b/src/main/java/ru/ulstu/models/Model.java similarity index 84% rename from src/main/java/ru/ulstu/models/ModelTimeSeries.java rename to src/main/java/ru/ulstu/models/Model.java index 29b872c..534e112 100644 --- a/src/main/java/ru/ulstu/models/ModelTimeSeries.java +++ b/src/main/java/ru/ulstu/models/Model.java @@ -1,10 +1,10 @@ package ru.ulstu.models; -public class ModelTimeSeries { +public class Model { private TimeSeries originalTimeSeries; private TimeSeries modelTimeSeries; - public ModelTimeSeries(TimeSeries originalTimeSeries) { + public Model(TimeSeries originalTimeSeries) { this.originalTimeSeries = originalTimeSeries; this.modelTimeSeries = new TimeSeries("Model time series of '" + originalTimeSeries.getName() + "'"); } @@ -22,6 +22,6 @@ public class ModelTimeSeries { } public void addValue(TimeSeriesValue basedOnValue, double value) { - modelTimeSeries.getValues().add(new TimeSeriesValue(basedOnValue.getDate().plusDays(1), 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 7515cf3..619412e 100644 --- a/src/main/java/ru/ulstu/models/TimeSeries.java +++ b/src/main/java/ru/ulstu/models/TimeSeries.java @@ -59,4 +59,26 @@ public class TimeSeries { public int getLength() { return values.size(); } + + public TimeSeriesValue getFirstValue() { + if ((values.size() > 0)) { + return values.get(0); + } + throw new RuntimeException("Временной ряд пуст"); + } + + public Double getNumericValue(int t) { + if ((values.size() > t) && (t >= 0)) { + return values.get(t).getValue(); + } + throw new RuntimeException("Индекс выходит за границы временного ряда"); + } + + @Override + public String toString() { + return "TimeSeries{" + + "values=" + values + + ", name='" + name + '\'' + + '}'; + } } diff --git a/src/main/java/ru/ulstu/models/TimeSeriesValue.java b/src/main/java/ru/ulstu/models/TimeSeriesValue.java index 4fe4c0e..a96df51 100644 --- a/src/main/java/ru/ulstu/models/TimeSeriesValue.java +++ b/src/main/java/ru/ulstu/models/TimeSeriesValue.java @@ -3,23 +3,28 @@ package ru.ulstu.models; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; public class TimeSeriesValue { - private LocalDate date; + private LocalDateTime date; private Double value; @JsonCreator - public TimeSeriesValue(@JsonProperty(value="date") LocalDate date, @JsonProperty(value = "value") Double value) { + public TimeSeriesValue(@JsonProperty(value = "date") LocalDateTime date, @JsonProperty(value = "value") Double value) { this.date = date; this.value = value; } - public LocalDate getDate() { + public TimeSeriesValue(LocalDateTime date) { + this.date = date; + } + + public LocalDateTime getDate() { return date; } - public void setDate(LocalDate date) { + public void setDate(LocalDateTime date) { this.date = date; } @@ -30,4 +35,23 @@ public class TimeSeriesValue { public void setValue(Double value) { this.value = value; } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TimeSeriesValue that = (TimeSeriesValue) o; + return Objects.equals(date, that.date) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(date, value); + } } diff --git a/src/main/java/ru/ulstu/models/exceptions/ForecastValidateException.java b/src/main/java/ru/ulstu/models/exceptions/ForecastValidateException.java new file mode 100644 index 0000000..083ef0e --- /dev/null +++ b/src/main/java/ru/ulstu/models/exceptions/ForecastValidateException.java @@ -0,0 +1,7 @@ +package ru.ulstu.models.exceptions; + +public class ForecastValidateException extends ModelingException { + public ForecastValidateException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/models/exceptions/TimeSeriesValidateException.java b/src/main/java/ru/ulstu/models/exceptions/TimeSeriesValidateException.java index 7138862..1137ed7 100644 --- a/src/main/java/ru/ulstu/models/exceptions/TimeSeriesValidateException.java +++ b/src/main/java/ru/ulstu/models/exceptions/TimeSeriesValidateException.java @@ -1,6 +1,6 @@ package ru.ulstu.models.exceptions; -public class TimeSeriesValidateException extends Exception { +public class TimeSeriesValidateException extends ModelingException { public TimeSeriesValidateException(String message) { super(message); } diff --git a/src/main/java/ru/ulstu/services/TimeSeriesService.java b/src/main/java/ru/ulstu/services/TimeSeriesService.java index 4bf9a40..ccee4d0 100644 --- a/src/main/java/ru/ulstu/services/TimeSeriesService.java +++ b/src/main/java/ru/ulstu/services/TimeSeriesService.java @@ -6,10 +6,10 @@ import org.springframework.stereotype.Service; import ru.ulstu.models.Forecast; import ru.ulstu.models.TimeSeries; import ru.ulstu.models.TimeSeriesValue; -import ru.ulstu.models.exceptions.TimeSeriesValidateException; +import ru.ulstu.models.exceptions.ModelingException; import ru.ulstu.tsMethods.exponential.NoTrendNoSeason; -import java.time.LocalDate; +import java.time.LocalDateTime; @Service @@ -18,7 +18,7 @@ public class TimeSeriesService { public TimeSeries getRandomTimeSeries(int length) { TimeSeries ts = new TimeSeries("Random time series"); - LocalDate dateStart = LocalDate.now().minusDays(length); + LocalDateTime dateStart = LocalDateTime.now().minusDays(length); for (int i = 0; i < length; i++) { ts.getValues().add(new TimeSeriesValue(dateStart, Math.random())); dateStart = dateStart.plusDays(1); @@ -26,15 +26,8 @@ public class TimeSeriesService { return ts; } - public Forecast getForecast(TimeSeries timeSeries, int countForecast) { - try { - NoTrendNoSeason nn = new NoTrendNoSeason(timeSeries); - nn.setAlpa(0.1); - nn.createModel(); - return nn.getForecast(); - } catch (TimeSeriesValidateException ex) { - LOGGER.error("Некорректная инициализация метода моделирования", ex); - return null; - } + public Forecast getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException { + NoTrendNoSeason nn = new NoTrendNoSeason(0.1); + return nn.getForecast(timeSeries, countPoints); } } diff --git a/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java b/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java index 0eb6d70..5e74e89 100644 --- a/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java +++ b/src/main/java/ru/ulstu/tsMethods/TimeSeriesMethod.java @@ -1,45 +1,129 @@ package ru.ulstu.tsMethods; import ru.ulstu.models.Forecast; -import ru.ulstu.models.ModelTimeSeries; +import ru.ulstu.models.Model; import ru.ulstu.models.TimeSeries; +import ru.ulstu.models.TimeSeriesValue; +import ru.ulstu.models.exceptions.ForecastValidateException; +import ru.ulstu.models.exceptions.ModelingException; import ru.ulstu.models.exceptions.TimeSeriesValidateException; -import java.util.HashMap; -import java.util.Map; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +/** + * Наиболее общая логика моделировани и прогнозирования временных рядов + */ public abstract class TimeSeriesMethod { - protected TimeSeries originalTimeSeries; - protected ModelTimeSeries model; - protected Forecast forecast; - protected int countForecast; - protected Map parameters = new HashMap<>(); - - protected TimeSeriesMethod(TimeSeries originalTimeSeries) throws TimeSeriesValidateException { - validateTimeSeries(originalTimeSeries); - this.originalTimeSeries = originalTimeSeries; - model = new ModelTimeSeries(originalTimeSeries); - forecast = new Forecast(originalTimeSeries); + /** + * Возвращает модельное представление временного ряда: для тех же точек времени что и в параметре timeSeries + * строится модель. Количество точек может быть изменено: сокращено при сжатии ряда, увеличено при интерполяции. + * Метод является шаблонным, выполняет операции валидации исходного ряда и потом его моделирование + * + * @param timeSeries исходный временной ряд подлежащий моделированию + * @return модель временного ряда + * @throws TimeSeriesValidateException + */ + public Model getModel(TimeSeries timeSeries) throws TimeSeriesValidateException { + validateTimeSeries(timeSeries); + return getModelOfValidTimeSeries(timeSeries); } - public TimeSeries getOriginalTimeSeries() { - return originalTimeSeries; - } + /** + * Возвращает модельное представление валидного временного ряда: для тех же точек времени что и в параметре timeSeries + * строится модель. Количество точек может быть изменено: сокращено при сжатии ряда, увеличено при интерполяции. + * + * @param timeSeries исходный временной ряд подлежащий моделированию + * @return + */ + protected abstract Model getModelOfValidTimeSeries(TimeSeries timeSeries); - public ModelTimeSeries getModel() { - return model; - } - - public Forecast getForecast() { + /** + * Выполняет построение прогноза временного ряда. Даты спрогнозированных точек будут сгенерированы по модельным точкам. + * + * @param model модель временного ряда + * @param countPoints количество точек для прогнозирования + * @return прогноз временного ряда + */ + public Forecast getForecast(Model model, int countPoints) throws ModelingException { + Forecast forecast = new Forecast(model); + forecast = generateEmptyForecastPoints(forecast, countPoints); + forecast = makeForecast(forecast); + if (!forecast.getForecastTimeSeries().getFirstValue() + .equals(forecast.getModel().getModelTimeSeries().getLastValue())) { + throw new ForecastValidateException("Первая точка прогноза должна совпадать с последней модельной точкой"); + } return forecast; } - public abstract void createModel(); + /** + * Выполняет построение прогноза ждя уже сгенерированных будущих точек временного ряда. + * + * @param forecast Заготовка прогноза временного ряда с пустыми значениями + * @return + */ + protected abstract Forecast makeForecast(Forecast forecast); + + protected Forecast generateEmptyForecastPoints(Forecast forecast, int countPointForecast) { + long diffMilliseconds = getTimeDifferenceInMilliseconds(forecast); + LocalDateTime lastTimeSeriesDateTime = forecast + .getModel() + .getOriginalTimeSeries() + .getLastValue() + .getDate() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + forecast.getForecastTimeSeries() + .addValue(new TimeSeriesValue(forecast.getModel().getModelTimeSeries().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))); + } + return forecast; + } + + /** + * Вычисляет среднее значение между датами исходного временного ряда + * + * @param forecast объект, содержащий результат прогнозирования + * @return средняя разница между датами исходного временного ряда в миллисекундах + */ + protected long getTimeDifferenceInMilliseconds(Forecast forecast) { + long diffMilliseconds = 0; + for (int i = 1; i < forecast.getModel().getOriginalTimeSeries().getLength(); i++) { + diffMilliseconds += forecast.getModel().getOriginalTimeSeries().getValues().get(i - 1).getDate() + .until(forecast.getModel().getOriginalTimeSeries().getValues().get(i).getDate(), ChronoUnit.MILLIS); + } + return diffMilliseconds / (forecast.getModel().getOriginalTimeSeries().getLength() - 1); + } + + /** + * Выполняет построение модели и прогноза временного ряда. Даты спрогнозированных точек будут сгенерированы + * по модельным точкам. + * + * @param timeSeries временной ряда + * @param countPoints количество точек для прогнозирования + * @return прогноз временного ряда + */ + public Forecast getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException { + validateForecastParams(countPoints); + return getForecast(getModel(timeSeries), countPoints); + } + + private void validateForecastParams(int countPoints) throws ForecastValidateException { + if (countPoints < 1) { + throw new ForecastValidateException("Количество прогнозных точек должно быть больше 0"); + } + } private void validateTimeSeries(TimeSeries timeSeries) throws TimeSeriesValidateException { if (timeSeries == null || timeSeries.isEmpty()) { throw new TimeSeriesValidateException("Временной ряд должен быть не пустым"); } + if (timeSeries.getLength() < 2) { + throw new TimeSeriesValidateException("Временной ряд должен содержать хотя бы 2 точки"); + } if (timeSeries.getValues().stream().anyMatch(val -> val == null || val.getValue() == null)) { throw new TimeSeriesValidateException("Временной ряд содержит пустые значения"); } diff --git a/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java b/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java index cdc8d50..13ac7ef 100644 --- a/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java +++ b/src/main/java/ru/ulstu/tsMethods/exponential/NoTrendNoSeason.java @@ -1,12 +1,45 @@ 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.TimeSeriesValidateException; -import ru.ulstu.tsMethods.Param; import ru.ulstu.tsMethods.TimeSeriesMethod; public class NoTrendNoSeason extends TimeSeriesMethod { - public NoTrendNoSeason(TimeSeries originalTimeSeries) throws TimeSeriesValidateException { + private double alpha; + + public NoTrendNoSeason(double alpha) { + this.alpha = alpha; + } + + @Override + protected Model getModelOfValidTimeSeries(TimeSeries timeSeries) { + Model model = new Model(timeSeries); + model.addValue(timeSeries.getFirstValue()); + //выполняется проход модели по сглаживанию + for (int t = 1; t < timeSeries.getValues().size(); t++) { + model.addValue(timeSeries.getValues().get(t), + (1 - alpha) * timeSeries.getNumericValue(t) + + alpha * model.getModelTimeSeries().getValues().get(t - 1).getValue()); + } + 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(alpha * forecast.getForecastTimeSeries().getValues().get(t - 1).getValue()); + } + return forecast; + } + +/* public NoTrendNoSeason(TimeSeries originalTimeSeries) throws TimeSeriesValidateException { super(originalTimeSeries); model.addValue(originalTimeSeries.getValues().get(0)); forecast.addValue(originalTimeSeries.getValues().get(0)); @@ -34,4 +67,5 @@ public class NoTrendNoSeason extends TimeSeriesMethod { forecast.addValue(model.getModelTimeSeries().getLastValue()); // прогноз } } + */ }