// Copyright © 2015 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.core.domain.prediction; import fi.hsl.parkandride.back.ListUtil; import fi.hsl.parkandride.core.back.PredictionRepository; import fi.hsl.parkandride.core.domain.Utilization; import org.joda.time.DateTime; import org.joda.time.Minutes; import org.joda.time.ReadablePeriod; import org.joda.time.Weeks; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.stream.Collectors; public class RelativizedAverageOfPreviousWeeksPredictor implements Predictor { private static final Logger log = LoggerFactory.getLogger(RelativizedAverageOfPreviousWeeksPredictor.class); public static final List<ReadablePeriod> LOOKBACK_PERIODS = Arrays.asList(Weeks.weeks(1), Weeks.weeks(2), Weeks.weeks(3)); public static final Minutes LOOKBACK_MINUTES = Minutes.minutes(120); public static final String TYPE = "relative-average-of-previous-weeks"; @Override public String getType() { return TYPE; } @Override public List<Prediction> predict(PredictorState state, UtilizationHistory history, int maxCapacity) { Optional<Utilization> latest = history.getLatest(); if (!latest.isPresent()) { return Collections.emptyList(); } DateTime now = state.latestUtilization = latest.get().timestamp; final UtilizationHistory inMemoryHistory = new UtilizationHistoryList(history.getRange(now.minusWeeks(3).minus(LOOKBACK_MINUTES), now)); List<List<Prediction>> groupedByWeek = LOOKBACK_PERIODS.stream() .map(offset -> { DateTime start = now.minus(offset); DateTime end = start.plus(PredictionRepository.PREDICTION_WINDOW); Optional<Utilization> utilizationAtReferenceTime = inMemoryHistory.getAt(start); if (!utilizationAtReferenceTime.isPresent()) { return null; } Integer spacesAvailableAtReferenceTime = utilizationAtReferenceTime.get().spacesAvailable; List<Utilization> utilizations = inMemoryHistory.getRange(start, end); return utilizations.stream() .map(u -> new Prediction(u.timestamp.plus(offset), u.spacesAvailable - spacesAvailableAtReferenceTime)) .collect(Collectors.toList()); }) .filter(Objects::nonNull) .collect(Collectors.toList()); List<List<Prediction>> groupedByTimeOfDay = ListUtil.transpose(groupedByWeek); return groupedByTimeOfDay.stream() .map(predictions -> reduce(predictions, latest.get().spacesAvailable, getUtilizationMultiplier(now, inMemoryHistory), maxCapacity)) .collect(Collectors.toList()); } private Double getUtilizationMultiplier(DateTime now, UtilizationHistory inMemoryHistory) { final List<Utilization> recentUtilizations = inMemoryHistory.getRange(now.minus(LOOKBACK_MINUTES), now); Double recentUtilizationArea = Math.max(1, calculateAreaAverageByDataPoints(recentUtilizations)); Double referenceUtilizationAreaAverage = Math.max(1, LOOKBACK_PERIODS.stream() .map(offset -> now.minus(offset)) .map(referenceTime -> inMemoryHistory.getRange(referenceTime.minus(LOOKBACK_MINUTES), referenceTime)) .filter(utilizationList -> !utilizationList.isEmpty()) .mapToDouble(utilizationList -> calculateAreaAverageByDataPoints(utilizationList)) .average() .orElseGet(() -> recentUtilizationArea)); return Math.max(1, recentUtilizationArea / referenceUtilizationAreaAverage); } private double calculateAreaAverageByDataPoints(List<Utilization> utilizationList) { int referenceSpaces = utilizationList.get(utilizationList.size() - 1).spacesAvailable; return utilizationList.stream() .mapToDouble(u -> Math.abs(u.spacesAvailable - referenceSpaces)).average().getAsDouble(); } private Prediction reduce(List<Prediction> predictions, int spacesAvailableCorrection, double utilizationMultiplier, int maxCapacity) { DateTime timestamp = predictions.get(0).timestamp; if (!predictions.stream() .map(p -> p.timestamp) .allMatch(timestamp::equals)) { log.warn("Something went wrong. Not all predictions have the same timestamp: {}", predictions); } int spacesAvailable = (int) Math.round(utilizationMultiplier * predictions.stream() .mapToDouble(u -> u.spacesAvailable) .average() .getAsDouble()); final int predictedSpacesAvailable = Math.min(maxCapacity, Math.max(0, spacesAvailable + spacesAvailableCorrection)); return new Prediction(timestamp, predictedSpacesAvailable); } }