Skip to content

Commit 3fb7a84

Browse files
Issue/probablistic (#202)
* add JSON column for ForecastValue ready for probablistic forecasts * add into convert function * add to forecast_value_latest * add properties when updating foreast_value_latest * add to tests * start blending forecasts * add method to just take properties from one model * normalize properties values by the blended values * add to tests * pydantic==1.10.10 * add migrations * add comments * fix convert --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent de61ac1 commit 3fb7a84

File tree

13 files changed

+234
-22
lines changed

13 files changed

+234
-22
lines changed

nowcasting_datamodel/fake.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def make_fake_forecast_value(
5454
target_time=target_time,
5555
expected_power_generation_megawatts=power,
5656
adjust_mw=0.0,
57+
properties={"10": power * 0.9, "90": power * 1.1},
5758
)
5859

5960

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Add properties column to forecast_value
2+
3+
Revision ID: a6fb75892950
4+
Revises: 08ba6879b865
5+
Create Date: 2023-07-03 11:00:31.216789
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "a6fb75892950"
13+
down_revision = "08ba6879b865"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade(): # noqa
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
21+
op.add_column("forecast_value", sa.Column("properties", sa.JSON(), nullable=True))
22+
op.add_column("forecast_value_2022_09", sa.Column("properties", sa.JSON(), nullable=True))
23+
op.add_column("forecast_value_2022_10", sa.Column("properties", sa.JSON(), nullable=True))
24+
op.add_column("forecast_value_2022_11", sa.Column("properties", sa.JSON(), nullable=True))
25+
op.add_column("forecast_value_2022_12", sa.Column("properties", sa.JSON(), nullable=True))
26+
op.add_column("forecast_value_2023_01", sa.Column("properties", sa.JSON(), nullable=True))
27+
op.add_column("forecast_value_2023_02", sa.Column("properties", sa.JSON(), nullable=True))
28+
op.add_column("forecast_value_2023_03", sa.Column("properties", sa.JSON(), nullable=True))
29+
op.add_column("forecast_value_2023_04", sa.Column("properties", sa.JSON(), nullable=True))
30+
op.add_column("forecast_value_2023_05", sa.Column("properties", sa.JSON(), nullable=True))
31+
op.add_column("forecast_value_2023_06", sa.Column("properties", sa.JSON(), nullable=True))
32+
op.add_column("forecast_value_2023_07", sa.Column("properties", sa.JSON(), nullable=True))
33+
op.add_column("forecast_value_2023_08", sa.Column("properties", sa.JSON(), nullable=True))
34+
op.add_column("forecast_value_2023_09", sa.Column("properties", sa.JSON(), nullable=True))
35+
op.add_column("forecast_value_2023_10", sa.Column("properties", sa.JSON(), nullable=True))
36+
op.add_column("forecast_value_2023_11", sa.Column("properties", sa.JSON(), nullable=True))
37+
op.add_column("forecast_value_2023_12", sa.Column("properties", sa.JSON(), nullable=True))
38+
op.add_column(
39+
"forecast_value_last_seven_days", sa.Column("properties", sa.JSON(), nullable=True)
40+
)
41+
op.add_column("forecast_value_latest", sa.Column("properties", sa.JSON(), nullable=True))
42+
op.add_column("forecast_value_old", sa.Column("properties", sa.JSON(), nullable=True))
43+
# ### end Alembic commands ###
44+
45+
46+
def downgrade(): # noqa
47+
# ### commands auto generated by Alembic - please adjust! ###
48+
op.drop_column("forecast_value_old", "properties")
49+
op.drop_column("forecast_value_latest", "properties")
50+
op.drop_column("forecast_value_last_seven_days", "properties")
51+
op.drop_column("forecast_value_2023_12", "properties")
52+
op.drop_column("forecast_value_2023_11", "properties")
53+
op.drop_column("forecast_value_2023_10", "properties")
54+
op.drop_column("forecast_value_2023_09", "properties")
55+
op.drop_column("forecast_value_2023_08", "properties")
56+
op.drop_column("forecast_value_2023_07", "properties")
57+
op.drop_column("forecast_value_2023_06", "properties")
58+
op.drop_column("forecast_value_2023_05", "properties")
59+
op.drop_column("forecast_value_2023_04", "properties")
60+
op.drop_column("forecast_value_2023_03", "properties")
61+
op.drop_column("forecast_value_2023_02", "properties")
62+
op.drop_column("forecast_value_2023_01", "properties")
63+
op.drop_column("forecast_value_2022_12", "properties")
64+
op.drop_column("forecast_value_2022_11", "properties")
65+
op.drop_column("forecast_value_2022_10", "properties")
66+
op.drop_column("forecast_value_2022_09", "properties")
67+
op.drop_column("forecast_value", "properties")
68+
69+
# ### end Alembic commands ###

nowcasting_datamodel/models/convert.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ def convert_df_to_national_forecast(
6767
:param forecast_values_df: Dataframe containing
6868
-- target_datetime_utc
6969
-- forecast_mw
70+
-- (Optional) forecast_mw_plevel_10
71+
-- (Optional) forecast_mw_plevel_90
7072
:param: session: database session
7173
:param: model_name: the name of the model
7274
:param: version: the version of the model
@@ -97,6 +99,14 @@ def convert_df_to_national_forecast(
9799
expected_power_generation_megawatts=forecast_value.forecast_mw,
98100
).to_orm()
99101
forecast_value_sql.adjust_mw = 0.0
102+
103+
forecast_value_sql.properties = {}
104+
if "forecast_mw_plevel_10" in forecast_values_df.columns:
105+
forecast_value_sql.properties["10"] = forecast_value.forecast_mw_plevel_10
106+
107+
if "forecast_mw_plevel_90" in forecast_values_df.columns:
108+
forecast_value_sql.properties["90"] = forecast_value.forecast_mw_plevel_90
109+
100110
forecast_values.append(forecast_value_sql)
101111

102112
# make forecast object

nowcasting_datamodel/models/forecast.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from pydantic import Field, validator
1414
from sqlalchemy import (
15+
JSON,
1516
Boolean,
1617
Column,
1718
DateTime,
@@ -24,6 +25,7 @@
2425
)
2526
from sqlalchemy.dialects.postgresql import UUID
2627
from sqlalchemy.ext.declarative import DeclarativeMeta, declared_attr
28+
from sqlalchemy.ext.mutable import MutableDict
2729
from sqlalchemy.orm import relationship
2830
from sqlalchemy.sql.ddl import DDL
2931

@@ -112,6 +114,9 @@ class ForecastValueSQLMixin(CreatedMixin):
112114
target_time = Column(DateTime(timezone=True), nullable=False, primary_key=True)
113115
expected_power_generation_megawatts = Column(Float(precision=6))
114116
adjust_mw = Column(Float, default=0.0)
117+
# this can be used to store any additional information about the forecast, like p_levels.
118+
# Want to keep it as json so that we can store different properties for different forecasts
119+
properties = Column(MutableDict.as_mutable(JSON), nullable=True)
115120

116121
@declared_attr
117122
def forecast_id(self):
@@ -281,6 +286,9 @@ class ForecastValueLatestSQL(Base_Forecast, CreatedMixin):
281286
model_id = Column(Integer, index=True, primary_key=True, default=-1)
282287
is_primary = Column(Boolean, default=True)
283288
adjust_mw = Column(Float, default=0.0)
289+
# this can be used to store any additional information about the forecast, like p_levels.
290+
# Want to keep it as json so that we can store different properties for different forecasts
291+
properties = Column(MutableDict.as_mutable(JSON), nullable=True)
284292

285293
forecast_id = Column(Integer, ForeignKey("forecast.id"), index=True)
286294
forecast_latest = relationship("ForecastSQL", back_populates="forecast_values_latest")
@@ -310,6 +318,14 @@ class ForecastValue(EnhancedBaseModel):
310318
"The _ at the start means it is not expose in the API",
311319
)
312320

321+
# This its better to keep this out of the current pydantic models used by the API.
322+
# A new pydantic mode can be made that includes the forecast plevels, perhaps in the API.
323+
_properties: dict = Field(
324+
None,
325+
description="Dictionary to hold properties of the forecast, like p_levels. "
326+
"The _ at the start means it is not expose in the API",
327+
)
328+
313329
_normalize_target_time = validator("target_time", allow_reuse=True)(datetime_must_have_timezone)
314330

315331
def to_orm(self) -> ForecastValueSQL:

nowcasting_datamodel/models/gsp.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ class Location(EnhancedBaseModel):
5353
None, description="The installed capacity of the GSP in MW"
5454
)
5555

56-
rm_mode = True
57-
5856
def to_orm(self) -> LocationSQL:
5957
"""Change model to LocationSQL"""
6058

nowcasting_datamodel/models/metric.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ class Metric(EnhancedBaseModel):
4141
name: str = Field(..., description="The name of the metric")
4242
description: str = Field(..., description="The description of the metric")
4343

44-
rm_mode = True
45-
4644
def to_orm(self) -> MetricSQL:
4745
"""Change model to LocationSQL"""
4846

@@ -170,8 +168,6 @@ class MetricValue(EnhancedBaseModel):
170168
)
171169
location: Location = Field(..., description="The location object for this metric value")
172170

173-
rm_mode = True
174-
175171
def to_orm(self) -> MetricValueSQL:
176172
"""Change model to MetricValueSQL"""
177173

nowcasting_datamodel/read/blend/blend.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
77
"""
88

9+
import json
910
from datetime import datetime
1011
from typing import List, Optional
1112

13+
import pandas as pd
1214
import structlog
1315
from sqlalchemy.orm.session import Session
1416

@@ -34,6 +36,7 @@ def get_blend_forecast_values_latest(
3436
model_names: Optional[List[str]] = None,
3537
weights: Optional[List[float]] = None,
3638
forecast_horizon_minutes: Optional[int] = None,
39+
properties_model: Optional[str] = None,
3740
) -> List[ForecastValue]:
3841
"""
3942
Get forecast values
@@ -44,6 +47,7 @@ def get_blend_forecast_values_latest(
4447
If None is given then all are returned.
4548
:param model_names: list of model names to use for blending
4649
:param weights: list of weights to use for blending, see structure in make_weights_df
50+
:param properties_model: the model to use for the properties
4751
4852
return: List of forecasts values blended from different models
4953
"""
@@ -59,6 +63,11 @@ def get_blend_forecast_values_latest(
5963
else:
6064
weights_df = None
6165

66+
if properties_model is not None:
67+
assert (
68+
properties_model in model_names
69+
), f"properties_model must be in model_names {model_names}"
70+
6271
# get forecast for the different models
6372
forecast_values_all_model = []
6473
for model_name in model_names:
@@ -98,7 +107,72 @@ def get_blend_forecast_values_latest(
98107
# blend together
99108
forecast_values_blended = blend_forecasts_together(forecast_values_all_model, weights_df)
100109

110+
# add properties
111+
forecast_values_df = add_properties_to_forecast_values(
112+
blended_df=forecast_values_blended,
113+
properties_model=properties_model,
114+
all_model_df=forecast_values_all_model,
115+
)
116+
101117
# convert back to list of forecast values
102-
forecast_values = convert_df_to_list_forecast_values(forecast_values_blended)
118+
forecast_values = convert_df_to_list_forecast_values(forecast_values_df)
103119

104120
return forecast_values
121+
122+
123+
def add_properties_to_forecast_values(
124+
blended_df: pd.DataFrame,
125+
all_model_df: pd.DataFrame,
126+
properties_model: Optional[str] = None,
127+
):
128+
"""
129+
Add properties to blended forecast values, we just take it from one model.
130+
131+
We normalize all properties by the "expected_power_generation_megawatts" value,
132+
and renormalize by the blended "expected_power_generation_megawatts" value.
133+
This makes sure that plevels 10 and 90 surround the blended value.
134+
135+
:param blended_df: dataframe of blended forecast values
136+
:param properties_model: which model to use for properties
137+
:param all_model_df: dataframe of all forecast values for all models
138+
:return:
139+
"""
140+
141+
logger.debug(
142+
f"Adding properties to blended forecast values for properties_model {properties_model}"
143+
)
144+
145+
if properties_model is None:
146+
blended_df["properties"] = None
147+
return blended_df
148+
149+
# get properties
150+
151+
properties_df = all_model_df[all_model_df["model_name"] == properties_model]
152+
153+
# adjust "properties" to be relative to the expected_power_generation_megawatts
154+
# this is a bit tricky becasue the "properties" column is a list of dictionaries
155+
# below we add "expected_power_generation_megawatts" value back to this.
156+
# We do this so that plevels are relative to the blended values.
157+
properties_only_df = pd.json_normalize(properties_df["properties"])
158+
for c in properties_only_df.columns:
159+
properties_only_df[c] -= properties_df["expected_power_generation_megawatts"]
160+
properties_df["properties"] = properties_only_df.apply(
161+
lambda x: json.loads(x.to_json()), axis=1
162+
)
163+
164+
# reduce columns
165+
properties_df = properties_df[["target_time", "properties"]]
166+
167+
# add properties to blended forecast values
168+
blended_df = blended_df.merge(properties_df, on=["target_time"], how="left")
169+
170+
# add "expected_power_generation_megawatts" to the properties
171+
properties_only_df = pd.json_normalize(blended_df["properties"])
172+
for c in properties_only_df.columns:
173+
properties_only_df[c] += blended_df["expected_power_generation_megawatts"]
174+
blended_df["properties"] = properties_only_df.apply(lambda x: json.loads(x.to_json()), axis=1)
175+
176+
assert "properties" in blended_df.columns
177+
178+
return blended_df

0 commit comments

Comments
 (0)