File size: 4,405 Bytes
5d7f266 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | """Leak-free fuel features for the (zone, target hour, horizon h) rows of the EU model, from the daily cache of fetch_fuel.py.
Leakage rule. A forecast of target day T at horizon h is issued on day T - h (h = 2: the day before the day-after-tomorrow's
price auction results are known; the last known PRICE day is T - h + 1). The fuel value used is the one of the CLOSE of the
day BEFORE issue, f = T - h - 1: a full day more cautious than necessary (the MIBGAS index of the issue day and the 11:00
EUA auction of the issue day are public before the day-ahead auction results at 13:00), so no feature can contain a number
that was not settled when the forecast was made. Weekends and holidays carry the last published value forward (the EUA is
auctioned on weekdays only).
Features (per row; the same value for every zone - the fuel is a EUROPE-wide level, the zone decides what it does with it):
gas, gas_d7, gas_d28 gas day-ahead level (EUR/MWh) and its 7-/28-day change
gas_m1_spread month-ahead minus day-ahead gas: where the market expects the level to go
eua, eua_d7, eua_d28 carbon level (EUR/tCO2) and its changes
ccgt_mc, ccgt_mc_d7, _d28 marginal cost of a combined-cycle gas plant, gas / 0.55 + 0.202 / 0.55 * eua (EUR/MWh_e):
the number a gas-priced hour clears near, with the two inputs already combined
full, full_d28 storage fill (per cent) and its 28-day change, only when the cache has the AGSI+ column
fuel_age days between f and the newest value actually published at or before f (a stale feed is visible)
"""
import numpy as np
import pandas as pd
CCGT_EFF = 0.55
EF_T_PER_MWH_TH = 0.202 # tCO2 per MWh of gas burnt
BASE = ["gas", "gas_d7", "gas_d28", "gas_m1_spread", "eua", "eua_d7", "eua_d28", "ccgt_mc", "ccgt_mc_d7", "ccgt_mc_d28", "fuel_age"]
STORAGE = ["full", "full_d28"]
def calendar_frame(fuel):
"""The cache on a gapless daily index, filled forward, plus `age` = days since the value was actually published."""
idx = pd.date_range(fuel.index.min().normalize(), fuel.index.max().normalize(), freq="D")
f = fuel.reindex(idx)
published = pd.Series(np.where(f["gas_da"].notna(), idx.to_numpy(), np.datetime64("NaT")), index=idx)
age = (pd.Series(idx, index=idx) - published.ffill()).dt.days
out = f.ffill()
out["age"] = age
return out
def daily_table(fuel):
"""Per fuel day f: the level and change columns, as a frame indexed by f."""
c = calendar_frame(fuel)
t = pd.DataFrame(index=c.index)
t["gas"] = c["gas_da"]
t["gas_d7"] = c["gas_da"] - c["gas_da"].shift(7)
t["gas_d28"] = c["gas_da"] - c["gas_da"].shift(28)
t["gas_m1_spread"] = c["gas_m1"] - c["gas_da"]
t["eua"] = c["eua"]
t["eua_d7"] = c["eua"] - c["eua"].shift(7)
t["eua_d28"] = c["eua"] - c["eua"].shift(28)
mc = c["gas_da"] / CCGT_EFF + EF_T_PER_MWH_TH / CCGT_EFF * c["eua"]
t["ccgt_mc"] = mc
t["ccgt_mc_d7"] = mc - mc.shift(7)
t["ccgt_mc_d28"] = mc - mc.shift(28)
if "full" in c and c["full"].notna().any():
t["full"] = c["full"]
t["full_d28"] = c["full"] - c["full"].shift(28)
t["fuel_age"] = c["age"]
return t
def for_rows(table, target_dates, h):
"""Fuel features for target DAYS (any date-like array) at horizon h: the table row of f = T - h - 1.
Days before the table starts (or after it ends: the newest value is carried) give NaN / the last row."""
f = pd.DatetimeIndex(target_dates).normalize() - pd.Timedelta(days=int(h) + 1)
rows = table.reindex(f)
last = table.index.max()
late = f > last # a feed that stopped: carry the newest row, its age grows (visible in fuel_age)
if late.any():
rows = rows.copy()
rows.loc[late, :] = table.iloc[-1].to_numpy()
rows.loc[late, "fuel_age"] = table.iloc[-1]["fuel_age"] + (f[late] - last).days
return rows.reset_index(drop=True)
def add_to_frame(df, table, date_col="date", h_col="h"):
"""Append the fuel columns to a rows frame that has a date and an h column, one call per distinct h."""
parts = []
for h, g in df.groupby(h_col, sort=False):
feats = for_rows(table, g[date_col].to_numpy(), h)
feats.index = g.index
parts.append(feats)
out = pd.concat(parts).reindex(df.index)
return pd.concat([df, out], axis=1)
|