From a5b590b24c6e78c9f1c07355282a6d88db5854e7 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Sun, 31 Jan 2021 18:29:53 +0100 Subject: [PATCH] Add `Forecast.actual` column --- ...2af85bada01_store_actuals_with_forecast.py | 41 +++++++++++++++++++ src/urban_meal_delivery/db/forecasts.py | 5 +++ tests/db/test_forecasts.py | 12 ++++++ 3 files changed, 58 insertions(+) create mode 100644 migrations/versions/rev_20210129_11_c2af85bada01_store_actuals_with_forecast.py diff --git a/migrations/versions/rev_20210129_11_c2af85bada01_store_actuals_with_forecast.py b/migrations/versions/rev_20210129_11_c2af85bada01_store_actuals_with_forecast.py new file mode 100644 index 0000000..810fbb5 --- /dev/null +++ b/migrations/versions/rev_20210129_11_c2af85bada01_store_actuals_with_forecast.py @@ -0,0 +1,41 @@ +"""Store actuals with forecast. + +Revision: #c2af85bada01 at 2021-01-29 11:13:15 +Revises: #e86290e7305e +""" + +import os + +import sqlalchemy as sa +from alembic import op + +from urban_meal_delivery import configuration + + +revision = 'c2af85bada01' +down_revision = 'e86290e7305e' +branch_labels = None +depends_on = None + + +config = configuration.make_config('testing' if os.getenv('TESTING') else 'production') + + +def upgrade(): + """Upgrade to revision c2af85bada01.""" + op.add_column( + 'forecasts', + sa.Column('actual', sa.SmallInteger(), nullable=False), + schema=config.CLEAN_SCHEMA, + ) + op.create_check_constraint( + op.f('ck_forecasts_on_actuals_must_be_non_negative'), + 'forecasts', + 'actual >= 0', + schema=config.CLEAN_SCHEMA, + ) + + +def downgrade(): + """Downgrade to revision e86290e7305e.""" + op.drop_column('forecasts', 'actual', schema=config.CLEAN_SCHEMA) diff --git a/src/urban_meal_delivery/db/forecasts.py b/src/urban_meal_delivery/db/forecasts.py index 2edb695..f9ebc44 100644 --- a/src/urban_meal_delivery/db/forecasts.py +++ b/src/urban_meal_delivery/db/forecasts.py @@ -22,6 +22,10 @@ class Forecast(meta.Base): time_step = sa.Column(sa.SmallInteger, nullable=False) training_horizon = sa.Column(sa.SmallInteger, nullable=False) model = sa.Column(sa.Unicode(length=20), nullable=False) + # We also store the actual order counts for convenient retrieval. + # A `UniqueConstraint` below ensures that redundant values that + # are to be expected are consistend across rows. + actual = sa.Column(sa.SmallInteger, nullable=False) # Raw `.prediction`s are stored as `float`s (possibly negative). # The rounding is then done on the fly if required. prediction = sa.Column(postgresql.DOUBLE_PRECISION, nullable=False) @@ -62,6 +66,7 @@ class Forecast(meta.Base): sa.CheckConstraint( 'training_horizon > 0', name='training_horizon_must_be_positive', ), + sa.CheckConstraint('actual >= 0', name='actuals_must_be_non_negative'), sa.CheckConstraint( """ NOT ( diff --git a/tests/db/test_forecasts.py b/tests/db/test_forecasts.py index 426de7b..d6780df 100644 --- a/tests/db/test_forecasts.py +++ b/tests/db/test_forecasts.py @@ -18,6 +18,7 @@ def forecast(pixel): time_step=60, training_horizon=8, model='hets', + actual=12, prediction=12.3, low80=1.23, high80=123.4, @@ -131,6 +132,16 @@ class TestConstraints: ): db_session.commit() + def test_non_negative_actuals(self, db_session, forecast): + """Insert an instance with invalid data.""" + forecast.actual = -1 + db_session.add(forecast) + + with pytest.raises( + sa_exc.IntegrityError, match='actuals_must_be_non_negative', + ): + db_session.commit() + def test_set_prediction_without_ci(self, db_session, forecast): """Sanity check to see that the check constraint ... @@ -388,6 +399,7 @@ class TestConstraints: time_step=forecast.time_step, training_horizon=forecast.training_horizon, model=forecast.model, + actual=forecast.actual, prediction=2, low80=1, high80=3,