#2 -- first implementations of date generator, modeling and forecasting

This commit is contained in:
Anton Romanov 2020-10-01 08:41:23 +04:00
parent 3cd0d77e0c
commit 84a228b06d
10 changed files with 229 additions and 56 deletions

View File

@ -13,6 +13,7 @@ import ru.ulstu.configurations.ApiConfiguration;
import ru.ulstu.models.Forecast; import ru.ulstu.models.Forecast;
import ru.ulstu.models.ForecastParams; import ru.ulstu.models.ForecastParams;
import ru.ulstu.models.TimeSeries; import ru.ulstu.models.TimeSeries;
import ru.ulstu.models.exceptions.ModelingException;
import ru.ulstu.services.TimeSeriesService; import ru.ulstu.services.TimeSeriesService;
@RestController @RestController
@ -39,7 +40,7 @@ public class TimeSeriesController {
@PostMapping("getForecast") @PostMapping("getForecast")
@ApiOperation("Получить прогноз временного ряда") @ApiOperation("Получить прогноз временного ряда")
public ResponseEntity<Forecast> getForecastTimeSeries(@RequestBody ForecastParams forecastParams) { public ResponseEntity<Forecast> getForecastTimeSeries(@RequestBody ForecastParams forecastParams) throws ModelingException {
return new ResponseEntity<>(timeSeriesService.getForecast(forecastParams.getOriginalTimeSeries(), return new ResponseEntity<>(timeSeriesService.getForecast(forecastParams.getOriginalTimeSeries(),
forecastParams.getCountForecast()), HttpStatus.OK); forecastParams.getCountForecast()), HttpStatus.OK);
} }

View File

@ -1,23 +1,31 @@
package ru.ulstu.models; package ru.ulstu.models;
public class Forecast { public class Forecast {
private TimeSeries originalTimeSeries; private Model model;
private TimeSeries forecast; private TimeSeries forecast;
public Forecast(TimeSeries originalTimeSeries) { public Forecast(Model model) {
this.originalTimeSeries = originalTimeSeries; this.model = model;
this.forecast = new TimeSeries("Forecast time series of '" + originalTimeSeries.getName() + "'"); this.forecast = new TimeSeries("Forecast time series of '" + model.getOriginalTimeSeries().getName() + "'");
} }
public TimeSeries getOriginalTimeSeries() { public Model getModel() {
return originalTimeSeries; return model;
} }
public TimeSeries getForecast() { public TimeSeries getForecastTimeSeries() {
return forecast; return forecast;
} }
public void addValue(TimeSeriesValue timeSeriesValue) { public void addValue(TimeSeriesValue timeSeriesValue) {
forecast.addValue(timeSeriesValue); forecast.addValue(timeSeriesValue);
} }
@Override
public String toString() {
return "Forecast{" +
"model=" + model +
", forecast=" + forecast +
'}';
}
} }

View File

@ -1,10 +1,10 @@
package ru.ulstu.models; package ru.ulstu.models;
public class ModelTimeSeries { public class Model {
private TimeSeries originalTimeSeries; private TimeSeries originalTimeSeries;
private TimeSeries modelTimeSeries; private TimeSeries modelTimeSeries;
public ModelTimeSeries(TimeSeries originalTimeSeries) { public Model(TimeSeries originalTimeSeries) {
this.originalTimeSeries = originalTimeSeries; this.originalTimeSeries = originalTimeSeries;
this.modelTimeSeries = new TimeSeries("Model time series of '" + originalTimeSeries.getName() + "'"); this.modelTimeSeries = new TimeSeries("Model time series of '" + originalTimeSeries.getName() + "'");
} }
@ -22,6 +22,6 @@ public class ModelTimeSeries {
} }
public void addValue(TimeSeriesValue basedOnValue, double value) { 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));
} }
} }

View File

@ -59,4 +59,26 @@ public class TimeSeries {
public int getLength() { public int getLength() {
return values.size(); 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 + '\'' +
'}';
}
} }

View File

@ -3,23 +3,28 @@ package ru.ulstu.models;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDate; import java.time.LocalDateTime;
import java.util.Objects;
public class TimeSeriesValue { public class TimeSeriesValue {
private LocalDate date; private LocalDateTime date;
private Double value; private Double value;
@JsonCreator @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.date = date;
this.value = value; this.value = value;
} }
public LocalDate getDate() { public TimeSeriesValue(LocalDateTime date) {
this.date = date;
}
public LocalDateTime getDate() {
return date; return date;
} }
public void setDate(LocalDate date) { public void setDate(LocalDateTime date) {
this.date = date; this.date = date;
} }
@ -30,4 +35,23 @@ public class TimeSeriesValue {
public void setValue(Double value) { public void setValue(Double value) {
this.value = 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);
}
} }

View File

@ -0,0 +1,7 @@
package ru.ulstu.models.exceptions;
public class ForecastValidateException extends ModelingException {
public ForecastValidateException(String message) {
super(message);
}
}

View File

@ -1,6 +1,6 @@
package ru.ulstu.models.exceptions; package ru.ulstu.models.exceptions;
public class TimeSeriesValidateException extends Exception { public class TimeSeriesValidateException extends ModelingException {
public TimeSeriesValidateException(String message) { public TimeSeriesValidateException(String message) {
super(message); super(message);
} }

View File

@ -6,10 +6,10 @@ import org.springframework.stereotype.Service;
import ru.ulstu.models.Forecast; import ru.ulstu.models.Forecast;
import ru.ulstu.models.TimeSeries; import ru.ulstu.models.TimeSeries;
import ru.ulstu.models.TimeSeriesValue; import ru.ulstu.models.TimeSeriesValue;
import ru.ulstu.models.exceptions.TimeSeriesValidateException; import ru.ulstu.models.exceptions.ModelingException;
import ru.ulstu.tsMethods.exponential.NoTrendNoSeason; import ru.ulstu.tsMethods.exponential.NoTrendNoSeason;
import java.time.LocalDate; import java.time.LocalDateTime;
@Service @Service
@ -18,7 +18,7 @@ public class TimeSeriesService {
public TimeSeries getRandomTimeSeries(int length) { public TimeSeries getRandomTimeSeries(int length) {
TimeSeries ts = new TimeSeries("Random time series"); 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++) { for (int i = 0; i < length; i++) {
ts.getValues().add(new TimeSeriesValue(dateStart, Math.random())); ts.getValues().add(new TimeSeriesValue(dateStart, Math.random()));
dateStart = dateStart.plusDays(1); dateStart = dateStart.plusDays(1);
@ -26,15 +26,8 @@ public class TimeSeriesService {
return ts; return ts;
} }
public Forecast getForecast(TimeSeries timeSeries, int countForecast) { public Forecast getForecast(TimeSeries timeSeries, int countPoints) throws ModelingException {
try { NoTrendNoSeason nn = new NoTrendNoSeason(0.1);
NoTrendNoSeason nn = new NoTrendNoSeason(timeSeries); return nn.getForecast(timeSeries, countPoints);
nn.setAlpa(0.1);
nn.createModel();
return nn.getForecast();
} catch (TimeSeriesValidateException ex) {
LOGGER.error("Некорректная инициализация метода моделирования", ex);
return null;
}
} }
} }

View File

@ -1,45 +1,129 @@
package ru.ulstu.tsMethods; package ru.ulstu.tsMethods;
import ru.ulstu.models.Forecast; import ru.ulstu.models.Forecast;
import ru.ulstu.models.ModelTimeSeries; import ru.ulstu.models.Model;
import ru.ulstu.models.TimeSeries; 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 ru.ulstu.models.exceptions.TimeSeriesValidateException;
import java.util.HashMap; import java.time.LocalDateTime;
import java.util.Map; import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
/**
* Наиболее общая логика моделировани и прогнозирования временных рядов
*/
public abstract class TimeSeriesMethod { public abstract class TimeSeriesMethod {
protected TimeSeries originalTimeSeries; /**
protected ModelTimeSeries model; * Возвращает модельное представление временного ряда: для тех же точек времени что и в параметре timeSeries
protected Forecast forecast; * строится модель. Количество точек может быть изменено: сокращено при сжатии ряда, увеличено при интерполяции.
protected int countForecast; * Метод является шаблонным, выполняет операции валидации исходного ряда и потом его моделирование
protected Map<Param, Double> parameters = new HashMap<>(); *
* @param timeSeries исходный временной ряд подлежащий моделированию
protected TimeSeriesMethod(TimeSeries originalTimeSeries) throws TimeSeriesValidateException { * @return модель временного ряда
validateTimeSeries(originalTimeSeries); * @throws TimeSeriesValidateException
this.originalTimeSeries = originalTimeSeries; */
model = new ModelTimeSeries(originalTimeSeries); public Model getModel(TimeSeries timeSeries) throws TimeSeriesValidateException {
forecast = new Forecast(originalTimeSeries); 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; * Выполняет построение прогноза временного ряда. Даты спрогнозированных точек будут сгенерированы по модельным точкам.
} *
* @param model модель временного ряда
public Forecast getForecast() { * @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; 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 { private void validateTimeSeries(TimeSeries timeSeries) throws TimeSeriesValidateException {
if (timeSeries == null || timeSeries.isEmpty()) { if (timeSeries == null || timeSeries.isEmpty()) {
throw new TimeSeriesValidateException("Временной ряд должен быть не пустым"); throw new TimeSeriesValidateException("Временной ряд должен быть не пустым");
} }
if (timeSeries.getLength() < 2) {
throw new TimeSeriesValidateException("Временной ряд должен содержать хотя бы 2 точки");
}
if (timeSeries.getValues().stream().anyMatch(val -> val == null || val.getValue() == null)) { if (timeSeries.getValues().stream().anyMatch(val -> val == null || val.getValue() == null)) {
throw new TimeSeriesValidateException("Временной ряд содержит пустые значения"); throw new TimeSeriesValidateException("Временной ряд содержит пустые значения");
} }

View File

@ -1,12 +1,45 @@
package ru.ulstu.tsMethods.exponential; package ru.ulstu.tsMethods.exponential;
import ru.ulstu.models.Forecast;
import ru.ulstu.models.Model;
import ru.ulstu.models.TimeSeries; import ru.ulstu.models.TimeSeries;
import ru.ulstu.models.exceptions.TimeSeriesValidateException;
import ru.ulstu.tsMethods.Param;
import ru.ulstu.tsMethods.TimeSeriesMethod; import ru.ulstu.tsMethods.TimeSeriesMethod;
public class NoTrendNoSeason extends 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); super(originalTimeSeries);
model.addValue(originalTimeSeries.getValues().get(0)); model.addValue(originalTimeSeries.getValues().get(0));
forecast.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()); // прогноз forecast.addValue(model.getModelTimeSeries().getLastValue()); // прогноз
} }
} }
*/
} }