Add Forecast model to ORM layer
- the model handles the caching of demand forecasting results - include the database migration script
This commit is contained in:
parent
54ff377579
commit
e8c97dd7da
5 changed files with 311 additions and 0 deletions
|
|
@ -8,6 +8,7 @@ from urban_meal_delivery.db.connection import engine
|
|||
from urban_meal_delivery.db.connection import session
|
||||
from urban_meal_delivery.db.couriers import Courier
|
||||
from urban_meal_delivery.db.customers import Customer
|
||||
from urban_meal_delivery.db.forecasts import Forecast
|
||||
from urban_meal_delivery.db.grids import Grid
|
||||
from urban_meal_delivery.db.meta import Base
|
||||
from urban_meal_delivery.db.orders import Order
|
||||
|
|
|
|||
66
src/urban_meal_delivery/db/forecasts.py
Normal file
66
src/urban_meal_delivery/db/forecasts.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Provide the ORM's `Forecast` model."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from urban_meal_delivery.db import meta
|
||||
|
||||
|
||||
class Forecast(meta.Base):
|
||||
"""A demand forecast for a `.pixel` and `.time_step` pair.
|
||||
|
||||
This table is denormalized on purpose to keep things simple.
|
||||
"""
|
||||
|
||||
__tablename__ = 'forecasts'
|
||||
|
||||
# Columns
|
||||
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # noqa:WPS125
|
||||
pixel_id = sa.Column(sa.Integer, nullable=False, index=True)
|
||||
start_at = sa.Column(sa.DateTime, nullable=False)
|
||||
time_step = sa.Column(sa.SmallInteger, nullable=False)
|
||||
training_horizon = sa.Column(sa.SmallInteger, nullable=False)
|
||||
method = sa.Column(sa.Unicode(length=20), nullable=False) # noqa:WPS432
|
||||
# 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)
|
||||
|
||||
# Constraints
|
||||
__table_args__ = (
|
||||
sa.ForeignKeyConstraint(
|
||||
['pixel_id'], ['pixels.id'], onupdate='RESTRICT', ondelete='RESTRICT',
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"""
|
||||
NOT (
|
||||
EXTRACT(HOUR FROM start_at) < 11
|
||||
OR
|
||||
EXTRACT(HOUR FROM start_at) > 22
|
||||
)
|
||||
""",
|
||||
name='start_at_must_be_within_operating_hours',
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
'CAST(EXTRACT(MINUTES FROM start_at) AS INTEGER) % 15 = 0',
|
||||
name='start_at_minutes_must_be_quarters_of_the_hour',
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
'EXTRACT(SECONDS FROM start_at) = 0', name='start_at_allows_no_seconds',
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
'CAST(EXTRACT(MICROSECONDS FROM start_at) AS INTEGER) % 1000000 = 0',
|
||||
name='start_at_allows_no_microseconds',
|
||||
),
|
||||
sa.CheckConstraint('time_step > 0', name='time_step_must_be_positive'),
|
||||
sa.CheckConstraint(
|
||||
'training_horizon > 0', name='training_horizon_must_be_positive',
|
||||
),
|
||||
# There can be only one prediction per forecasting setting.
|
||||
sa.UniqueConstraint(
|
||||
'pixel_id', 'start_at', 'time_step', 'training_horizon', 'method',
|
||||
),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
pixel = orm.relationship('Pixel', back_populates='forecasts')
|
||||
|
|
@ -39,6 +39,7 @@ class Pixel(meta.Base):
|
|||
# Relationships
|
||||
grid = orm.relationship('Grid', back_populates='pixels')
|
||||
addresses = orm.relationship('AddressPixelAssociation', back_populates='pixel')
|
||||
forecasts = orm.relationship('Forecast', back_populates='pixel')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Non-literal text representation."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue