Introduction¶
This notebook is a demo of the Dynamic Tariff Analysis algorithm (version 2) on EnergyID.
We try to answer the question: "Is a dynamic tariff advantageous for me?". We do this by comparing billing based on SMR3 meter readings (15 minute granularity), also known as a dynamic tariff, with billing based on (virtualised) SMR2 meter readings (eg. the monthly sum of the SMR3 readings).
By re-dividing the monthly sum of the SMR3 readings back into 15 minute intervals, using the RLP and SPP profiles, we create a "15 minute SMR2" series, that can easily be compared with the SMR3 series.
Imports¶
import pandas as pd
from openenergyid.models import TimeSeries
from openenergyid.dyntar.models import DynamicTariffAnalysisInput
from openenergyid.const import ELECTRICITY_DELIVERED, ELECTRICITY_EXPORTED, RLP, SPP, PRICE_ELECTRICITY_DELIVERED, PRICE_ELECTRICITY_EXPORTED
import seaborn as sns
from matplotlib.colors import TwoSlopeNorm
import matplotlib.pyplot as plt
Input Data¶
The input data is a JSON file, containing a TimeSeries object and a TimeZone.
The TimeSeries object needs to have following columns in 15-minute intervals:
electricity_delivered
electricity_exported
price_electricity_delivered
price_electricity_exported
RLP
SPP
DynamicTariffAnalysisInput.model_json_schema()
{'$defs': {'TimeSeries': {'description': 'Time series data.', 'properties': {'columns': {'items': {'type': 'string'}, 'title': 'Columns', 'type': 'array'}, 'index': {'items': {'format': 'date-time', 'type': 'string'}, 'title': 'Index', 'type': 'array'}, 'data': {'items': {'items': {'type': 'number'}, 'type': 'array'}, 'title': 'Data', 'type': 'array'}}, 'required': ['columns', 'index', 'data'], 'title': 'TimeSeries', 'type': 'object'}}, 'description': 'Input for dynamic tariff analysis.', 'properties': {'timeZone': {'title': 'Timezone', 'type': 'string'}, 'frame': {'$ref': '#/$defs/TimeSeries'}}, 'required': ['timeZone', 'frame'], 'title': 'DynamicTariffAnalysisInput', 'type': 'object'}
with open('data/dyntar/sample.json', 'r') as f:
data = DynamicTariffAnalysisInput.model_validate_json(f.read())
df = data.data_frame()
df
electricity_delivered | electricity_exported | price_electricity_delivered | price_electricity_exported | RLP | SPP | |
---|---|---|---|---|---|---|
2024-01-01 00:00:00+01:00 | 0.023 | 0.0 | 0.004102 | -0.003915 | 0.000041 | 0.0 |
2024-01-01 00:15:00+01:00 | 0.026 | 0.0 | 0.004102 | -0.003915 | 0.000041 | 0.0 |
2024-01-01 00:30:00+01:00 | 0.020 | 0.0 | 0.004102 | -0.003915 | 0.000040 | 0.0 |
2024-01-01 00:45:00+01:00 | 0.029 | 0.0 | 0.004102 | -0.003915 | 0.000039 | 0.0 |
2024-01-01 01:00:00+01:00 | 0.020 | 0.0 | 0.004010 | -0.003992 | 0.000038 | 0.0 |
... | ... | ... | ... | ... | ... | ... |
2024-04-18 22:00:00+02:00 | 0.115 | 0.0 | 0.095627 | 0.072355 | 0.000032 | 0.0 |
2024-04-18 22:15:00+02:00 | 0.140 | 0.0 | 0.095627 | 0.072355 | 0.000032 | 0.0 |
2024-04-18 22:30:00+02:00 | 0.116 | 0.0 | 0.095627 | 0.072355 | 0.000032 | 0.0 |
2024-04-18 22:45:00+02:00 | 0.131 | 0.0 | 0.095627 | 0.072355 | 0.000031 | 0.0 |
2024-04-18 23:00:00+02:00 | 0.131 | 0.0 | 0.083856 | 0.062547 | 0.000031 | 0.0 |
10457 rows × 6 columns
Analysis¶
def weigh_by_monthly_profile(series: pd.Series, profile: pd.Series) -> pd.Series:
df = pd.DataFrame({'series': series, 'profile': profile})
results = []
for month, frame in df.groupby(pd.Grouper(freq='MS')):
frame = frame.copy()
frame['weighted'] = frame['series'].sum() * (frame['profile'] / frame['profile'].sum())
results.append(frame)
return pd.concat(results)['weighted']
result_df = df[['electricity_delivered', 'electricity_exported']].copy()
result_df.rename(columns={'electricity_delivered': 'electricity_delivered_smr3', 'electricity_exported': 'electricity_exported_smr3'}, inplace=True)
result_df
electricity_delivered_smr3 | electricity_exported_smr3 | |
---|---|---|
2024-01-01 00:00:00+01:00 | 0.023 | 0.0 |
2024-01-01 00:15:00+01:00 | 0.026 | 0.0 |
2024-01-01 00:30:00+01:00 | 0.020 | 0.0 |
2024-01-01 00:45:00+01:00 | 0.029 | 0.0 |
2024-01-01 01:00:00+01:00 | 0.020 | 0.0 |
... | ... | ... |
2024-04-18 22:00:00+02:00 | 0.115 | 0.0 |
2024-04-18 22:15:00+02:00 | 0.140 | 0.0 |
2024-04-18 22:30:00+02:00 | 0.116 | 0.0 |
2024-04-18 22:45:00+02:00 | 0.131 | 0.0 |
2024-04-18 23:00:00+02:00 | 0.131 | 0.0 |
10457 rows × 2 columns
electricity_delivered_smr2 = weigh_by_monthly_profile(df[ELECTRICITY_DELIVERED], df[RLP])
electricity_exported_smr2 = weigh_by_monthly_profile(df[ELECTRICITY_EXPORTED], df[SPP])
result_df['electricity_delivered_smr2'] = electricity_delivered_smr2
result_df['electricity_exported_smr2'] = electricity_exported_smr2
cost_electricity_delivered_smr3 = df[ELECTRICITY_DELIVERED] * df[PRICE_ELECTRICITY_DELIVERED]
cost_electricity_exported_smr3 = df[ELECTRICITY_EXPORTED] * df[PRICE_ELECTRICITY_EXPORTED]
cost_electricity_delivered_smr2 = electricity_delivered_smr2 * df[PRICE_ELECTRICITY_DELIVERED]
cost_electricity_exported_smr2 = electricity_exported_smr2 * df[PRICE_ELECTRICITY_EXPORTED]
total_cost_electricity_smr3 = cost_electricity_delivered_smr3 - cost_electricity_exported_smr3
total_cost_electricity_smr2 = cost_electricity_delivered_smr2 - cost_electricity_exported_smr2
profit_smr3 = total_cost_electricity_smr2 - total_cost_electricity_smr3
result_df['cost_electricity_delivered_smr3'] = cost_electricity_delivered_smr3
result_df['cost_electricity_exported_smr3'] = cost_electricity_exported_smr3
result_df['cost_electricity_delivered_smr2'] = cost_electricity_delivered_smr2
result_df['cost_electricity_exported_smr2'] = cost_electricity_exported_smr2
result_df['total_cost_electricity_smr3'] = total_cost_electricity_smr3
result_df['total_cost_electricity_smr2'] = total_cost_electricity_smr2
result_df['profit_smr3'] = profit_smr3
rlp_weighted_price_delivered = (df[PRICE_ELECTRICITY_DELIVERED] * df[RLP]).resample('MS').sum() / df[RLP].resample('MS').sum()
rlp_weighted_price_delivered = rlp_weighted_price_delivered.reindex_like(df[RLP], method='ffill')
spp_weighted_price_exported = (df[PRICE_ELECTRICITY_EXPORTED] * df[SPP]).resample('MS').sum() / df[SPP].resample('MS').sum()
spp_weighted_price_exported = spp_weighted_price_exported.reindex_like(df[SPP], method='ffill')
heatmap_score_delivered = (electricity_delivered_smr2 - df[ELECTRICITY_DELIVERED]) / electricity_delivered_smr2 * (rlp_weighted_price_delivered - df[PRICE_ELECTRICITY_DELIVERED]) / rlp_weighted_price_delivered
heatmap_score_exported = (electricity_exported_smr2 - df[ELECTRICITY_EXPORTED]) / electricity_exported_smr2 * (spp_weighted_price_exported - df[PRICE_ELECTRICITY_EXPORTED]) / spp_weighted_price_exported
heatmap_score_delivered.fillna(0, inplace=True)
heatmap_score_exported.fillna(0, inplace=True)
heatmap = pd.DataFrame({'delivered': heatmap_score_delivered, 'exported': heatmap_score_exported})
heatmap_score_combined = heatmap_score_delivered - heatmap_score_exported
heatmap['combined'] = heatmap_score_combined
heatmap = heatmap.resample('H').sum()
result_ms = result_df.resample('MS').sum()
result_ms
electricity_delivered_smr3 | electricity_exported_smr3 | electricity_delivered_smr2 | electricity_exported_smr2 | cost_electricity_delivered_smr3 | cost_electricity_exported_smr3 | cost_electricity_delivered_smr2 | cost_electricity_exported_smr2 | total_cost_electricity_smr3 | total_cost_electricity_smr2 | profit_smr3 | |
---|---|---|---|---|---|---|---|---|---|---|---|
2024-01-01 00:00:00+01:00 | 382.704 | 21.953 | 382.704 | 21.953 | 36.357770 | 1.253071 | 33.097172 | 1.371023 | 35.104698 | 31.726149 | -3.378549 |
2024-02-01 00:00:00+01:00 | 318.313 | 29.987 | 318.313 | 29.987 | 23.815924 | 1.433875 | 21.769260 | 1.442623 | 22.382049 | 20.326636 | -2.055413 |
2024-03-01 00:00:00+01:00 | 258.649 | 121.385 | 258.649 | 121.385 | 20.106178 | 4.324243 | 17.706100 | 4.424964 | 15.781935 | 13.281137 | -2.500798 |
2024-04-01 00:00:00+02:00 | 59.374 | 117.995 | 59.374 | 117.995 | 3.659446 | 1.428309 | 2.753878 | 2.246670 | 2.231137 | 0.507207 | -1.723929 |
from pydantic import BaseModel, Field
class DynamicTariffAnalysisOutput(BaseModel):
result_pt15m: TimeSeries = Field(alias='ResultPT15M')
result_p1m: TimeSeries = Field(alias='ResultP1M')
heatmap: TimeSeries = Field(alias='Heatmap')
output_result = DynamicTariffAnalysisOutput.model_construct(
result_pt15m=TimeSeries.from_pandas(result_df),
result_p1m=TimeSeries.from_pandas(result_ms),
heatmap=TimeSeries.from_pandas(heatmap)
)
with open('data/dyntar/sample_output.json', 'w') as f:
f.write(output_result.model_dump_json(by_alias=True, indent=2))
Visualisation¶
Cost Comparison, monthly¶
plot = result_ms[['total_cost_electricity_smr2', 'total_cost_electricity_smr3']]
fig, ax = plt.subplots()
fig.set_size_inches(12, 8)
plot.index = plot.index.strftime('%B %Y')
plot.plot(kind='bar', ax=ax)
ax.set_ylabel('Total cost [€]')
ax.set_xlabel('Month')
# horizontal ticks
plt.xticks(rotation=0)
# Legend
# Call SMR2 "Groene Burgerstroom", SMR3 "Dynamische Burgerstroom"
plt.legend(['Groene Burgerstroom', 'Dynamische Burgerstroom'])
plt.show()
table = plot.copy()
table['difference'] = table['total_cost_electricity_smr3'] - table['total_cost_electricity_smr2']
table['difference_percentage'] = table['difference'] / table['total_cost_electricity_smr2'] * 100
# Rename columns
table.rename(columns={
'total_cost_electricity_smr2': 'Kost Groene Burgerstroom',
'total_cost_electricity_smr3': 'Kost Dynamische Burgerstroom',
'difference': 'Verschil',
'difference_percentage': 'Verschil (%)'
}, inplace=True)
table = table.round(2)
table
Kost Groene Burgerstroom | Kost Dynamische Burgerstroom | Verschil | Verschil (%) | |
---|---|---|---|---|
January 2024 | 31.73 | 35.10 | 3.38 | 10.65 |
February 2024 | 20.33 | 22.38 | 2.06 | 10.11 |
March 2024 | 13.28 | 15.78 | 2.50 | 18.83 |
April 2024 | 0.51 | 2.23 | 1.72 | 339.89 |
Breakdown between electricity delivered and electricity exported¶
These bars should be directly below each other and in a similar colour but you get the idea.
# Now, let's do the same graph, but we split each bar into two bars, one for the cost of electricity delivered and one for the cost of electricity exported
# The electricity exported should be shown as negative values
plot = result_ms[['cost_electricity_delivered_smr3', 'cost_electricity_exported_smr3', 'cost_electricity_delivered_smr2', 'cost_electricity_exported_smr2']].copy()
plot['cost_electricity_exported_smr3'] = -plot['cost_electricity_exported_smr3']
plot['cost_electricity_exported_smr2'] = -plot['cost_electricity_exported_smr2']
fig, ax = plt.subplots()
fig.set_size_inches(12, 8)
plot.index = plot.index.strftime('%B %Y')
# Show the bars
# Plot the two bars for SMR2 on top of each other
# Offset the first stacked bar to the left and make it slimmer so the second bar is visible
# Give them a shade of blue, the first bar a little bit darker than the second
plot[['cost_electricity_delivered_smr2', 'cost_electricity_exported_smr2']].plot(kind='bar', ax=ax, stacked=True, position=1, width=0.4, color=['#1f77b4', '#aec7e8'])
# Plot the two bars for SMR3 on top of each other
# Offset the first stacked bar a little bit to the right and make it slimmer so the second bar is visible
# Give them a shade of orange, the first bar a little bit darker than the second
plot[['cost_electricity_delivered_smr3', 'cost_electricity_exported_smr3']].plot(kind='bar', ax=ax, stacked=True, position=0, width=0.4, color=['#ff7f0e', '#ffbb78'])
plt.tight_layout()
xmin, xmax = ax.get_xlim()
ax.set_xlim(xmin - 0.5, xmax)
# Set the labels
ax.set_ylabel('Kost [€]')
ax.set_xlabel('Maand')
# horizontal ticks
plt.xticks(rotation=0)
# Legend
# Call SMR2 "Groene Burgerstroom", SMR3 "Dynamische Burgerstroom"
plt.legend(['Kost Groene Burgerstroom - Geleverd', 'Kost Groene Burgerstroom - Teruggeleverd', 'Kost Dynamische Burgerstroom - Geleverd', 'Kost Dynamische Burgerstroom - Teruggeleverd'])
plt.show()
table = plot.copy()
table['difference_delivered'] = table['cost_electricity_delivered_smr3'] - table['cost_electricity_delivered_smr2']
table['difference_exported'] = table['cost_electricity_exported_smr3'] - table['cost_electricity_exported_smr2']
table['difference_delivered_percentage'] = table['difference_delivered'] / table['cost_electricity_delivered_smr2'] * 100
table['difference_exported_percentage'] = table['difference_exported'] / table['cost_electricity_exported_smr2'] * 100
# Rename columns
table.rename(columns={
'cost_electricity_delivered_smr2': 'Kost Groene Burgerstroom - Geleverd',
'cost_electricity_exported_smr2': 'Kost Groene Burgerstroom - Teruggeleverd',
'cost_electricity_delivered_smr3': 'Kost Dynamische Burgerstroom - Geleverd',
'cost_electricity_exported_smr3': 'Kost Dynamische Burgerstroom - Teruggeleverd',
'difference_delivered': 'Verschil Geleverd',
'difference_exported': 'Verschil Teruggeleverd',
'difference_delivered_percentage': 'Verschil Geleverd (%)',
'difference_exported_percentage': 'Verschil Teruggeleverd (%)'
}, inplace=True)
table
Kost Dynamische Burgerstroom - Geleverd | Kost Dynamische Burgerstroom - Teruggeleverd | Kost Groene Burgerstroom - Geleverd | Kost Groene Burgerstroom - Teruggeleverd | Verschil Geleverd | Verschil Teruggeleverd | Verschil Geleverd (%) | Verschil Teruggeleverd (%) | |
---|---|---|---|---|---|---|---|---|
January 2024 | 36.357770 | -1.253071 | 33.097172 | -1.371023 | 3.260598 | 0.117952 | 9.851590 | -8.603193 |
February 2024 | 23.815924 | -1.433875 | 21.769260 | -1.442623 | 2.046665 | 0.008748 | 9.401627 | -0.606407 |
March 2024 | 20.106178 | -4.324243 | 17.706100 | -4.424964 | 2.400077 | 0.100721 | 13.555087 | -2.276200 |
April 2024 | 3.659446 | -1.428309 | 2.753878 | -2.246670 | 0.905569 | 0.818361 | 32.883400 | -36.425494 |
Detail Graphs for one month¶
To show the difference between the SMR2 and SMR3 profiles, we show the following graphs
result_slice = result_df.truncate(before=pd.Timestamp('2024-03-01', tz='Europe/Brussels'), after=pd.Timestamp('2024-03-31T23:45:00', tz='Europe/Brussels'))
# Show line graphs of electricty delivered for SMR2 and SMR3
fig, ax = plt.subplots()
# Set the size of the figure
fig.set_size_inches(12, 8)
# Give both lines a bit of transparency
result_slice[['electricity_delivered_smr2', 'electricity_delivered_smr3']].plot(ax=ax, alpha=0.7)
# Set the labels
ax.set_ylabel('Elektriciteit [kWh/15 min]')
ax.set_xlabel('Datum')
# horizontal ticks
plt.xticks(rotation=45)
# Legend
# Call SMR2 "Groene Burgerstroom", SMR3 "Dynamische Burgerstroom"
plt.legend(['Groene Burgerstroom', 'Dynamische Burgerstroom'])
# Title of the graph: "Afname van elektriciteit in maart 2024"
plt.title('Afname van elektriciteit in maart 2024')
plt.show()
# Show line graphs of electricty exported for SMR2 and SMR3
fig, ax = plt.subplots()
# Set the size of the figure
fig.set_size_inches(12, 8)
# Give both lines a bit of transparency
result_slice[['electricity_exported_smr2', 'electricity_exported_smr3']].plot(ax=ax, alpha=0.7)
# Set the labels
ax.set_ylabel('Elektriciteit [kWh/15 min]')
ax.set_xlabel('Datum')
# horizontal ticks
plt.xticks(rotation=45)
# Legend
# Call SMR2 "Groene Burgerstroom", SMR3 "Dynamische Burgerstroom"
plt.legend(['Groene Burgerstroom', 'Dynamische Burgerstroom'])
# Title of the graph: "Teruglevering van elektriciteit in maart 2024"
plt.title('Teruglevering van elektriciteit in maart 2024')
plt.show()
heatmap_plot = result_slice['heatmap_score_combined'].resample('H').sum().to_frame()
heatmap_plot['date'] = heatmap_plot.index.date
heatmap_plot['time'] = heatmap_plot.index.time
heatmap_plot = heatmap_plot.pivot(index='date', columns='time', values='heatmap_score_combined')
min_ = heatmap_plot.min().min()
max = heatmap_plot.max().max()
abs_max = max if abs(max) > abs(min_) else abs(min_)
# Plot the carpetplot
fig, ax = plt.subplots()
# Set figsize
fig.set_size_inches(20, 10)
# Choose a colormap that goes from red to green, with 0 in the middle
rdgn = sns.diverging_palette(h_neg=130, h_pos=10, s=99, l=55, sep=3, as_cmap=True)
divnorm = TwoSlopeNorm(vmin=abs_max*-1, vcenter=0, vmax=abs_max)
sns.heatmap(heatmap_plot, cmap=rdgn, norm=divnorm,
linewidths=.5, linecolor='black', cbar=True, ax=ax)
plt.title('Heatmap score combined')
plt.ylabel(None)
plt.xlabel(None)
# x-axis label rotated
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()