Add OrderHistory.avg_daily_demand()
- the method calculates the number of daily `Order`s in a `Pixel` withing the `train_horizon` preceding the `predict_day`
This commit is contained in:
parent
67cd58cf16
commit
cb7611d587
4 changed files with 83 additions and 11 deletions
|
@ -467,3 +467,38 @@ class OrderHistory:
|
||||||
raise LookupError('`predict_at` is not in the order history')
|
raise LookupError('`predict_at` is not in the order history')
|
||||||
|
|
||||||
return training_ts, frequency, actuals_ts
|
return training_ts, frequency, actuals_ts
|
||||||
|
|
||||||
|
def avg_daily_demand(
|
||||||
|
self, pixel_id: int, predict_day: dt.date, train_horizon: int,
|
||||||
|
) -> float:
|
||||||
|
"""Calculate the average daily demand (ADD) for a `Pixel`.
|
||||||
|
|
||||||
|
The ADD is defined as the average number of daily `Order`s in a
|
||||||
|
`Pixel` within the training horizon preceding the `predict_day`.
|
||||||
|
|
||||||
|
The ADD is primarily used for the rule-based heuristic to determine
|
||||||
|
the best forecasting model for a `Pixel` on the `predict_day`.
|
||||||
|
|
||||||
|
Implementation note: To calculate the ADD, the order counts are
|
||||||
|
generated as a vertical time series. That must be so as we need to
|
||||||
|
include all time steps of the days before the `predict_day` and
|
||||||
|
no time step of the latter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pixel_id: pixel for which the ADD is calculated
|
||||||
|
predict_day: following the `train_horizon` on which the ADD is calculated
|
||||||
|
train_horizon: time horizon over which the ADD is calculated
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
average number of orders per day
|
||||||
|
"""
|
||||||
|
training_ts, _, _ = self.make_vertical_ts( # noqa:WPS434
|
||||||
|
pixel_id=pixel_id, predict_day=predict_day, train_horizon=train_horizon,
|
||||||
|
)
|
||||||
|
|
||||||
|
first_day = training_ts.index.min().date()
|
||||||
|
last_day = training_ts.index.max().date()
|
||||||
|
# `+1` as both `first_day` and `last_day` are included.
|
||||||
|
n_days = (last_day - first_day).days + 1
|
||||||
|
|
||||||
|
return round(training_ts.sum() / n_days, 1)
|
||||||
|
|
|
@ -82,6 +82,17 @@ def good_pixel_id(pixel):
|
||||||
return pixel.id # `== 1`
|
return pixel.id # `== 1`
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def predict_at() -> dt.datetime:
|
||||||
|
"""`NOON` on the day to be predicted."""
|
||||||
|
return dt.datetime(
|
||||||
|
test_config.END.year,
|
||||||
|
test_config.END.month,
|
||||||
|
test_config.END.day,
|
||||||
|
test_config.NOON,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def order_totals(good_pixel_id):
|
def order_totals(good_pixel_id):
|
||||||
"""A mock for `OrderHistory.totals`.
|
"""A mock for `OrderHistory.totals`.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""Tests for the `urban_meal_delivery.forecasts.models` sub-package."""
|
"""Tests for the `urban_meal_delivery.forecasts.models` sub-package."""
|
||||||
|
|
||||||
import datetime as dt
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -59,16 +58,6 @@ class TestGenericForecastingModelProperties:
|
||||||
|
|
||||||
self.unique_model_names.add(model.name)
|
self.unique_model_names.add(model.name)
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def predict_at(self) -> dt.datetime:
|
|
||||||
"""`NOON` on the day to be predicted."""
|
|
||||||
return dt.datetime(
|
|
||||||
test_config.END.year,
|
|
||||||
test_config.END.month,
|
|
||||||
test_config.END.day,
|
|
||||||
test_config.NOON,
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.r
|
@pytest.mark.r
|
||||||
def test_make_prediction_structure(
|
def test_make_prediction_structure(
|
||||||
self, model_cls, order_history, pixel, predict_at,
|
self, model_cls, order_history, pixel, predict_at,
|
||||||
|
|
37
tests/forecasts/timify/test_avg_daily_demand.py
Normal file
37
tests/forecasts/timify/test_avg_daily_demand.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""Tests for the `OrderHistory.avg_daily_demand()` method."""
|
||||||
|
|
||||||
|
from tests import config as test_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_avg_daily_demand_with_constant_demand(
|
||||||
|
order_history, good_pixel_id, predict_at,
|
||||||
|
):
|
||||||
|
"""The average daily demand must be the number of time steps ...
|
||||||
|
|
||||||
|
... if the demand is `1` at each time step.
|
||||||
|
|
||||||
|
Note: The `order_history` fixture assumes `12` time steps per day as it
|
||||||
|
uses `LONG_TIME_STEP=60` as the length of a time step.
|
||||||
|
"""
|
||||||
|
result = order_history.avg_daily_demand(
|
||||||
|
pixel_id=good_pixel_id,
|
||||||
|
predict_day=predict_at.date(),
|
||||||
|
train_horizon=test_config.LONG_TRAIN_HORIZON,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == 12.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_avg_daily_demand_with_no_demand(
|
||||||
|
order_history, good_pixel_id, predict_at,
|
||||||
|
):
|
||||||
|
"""Without demand, the average daily demand must be `0.0`."""
|
||||||
|
order_history._data.loc[:, 'n_orders'] = 0
|
||||||
|
|
||||||
|
result = order_history.avg_daily_demand(
|
||||||
|
pixel_id=good_pixel_id,
|
||||||
|
predict_day=predict_at.date(),
|
||||||
|
train_horizon=test_config.LONG_TRAIN_HORIZON,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == 0.0
|
Loading…
Reference in a new issue