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¶

In [ ]:
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
In [ ]:
DynamicTariffAnalysisInput.model_json_schema()
Out[ ]:
{'$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'}
In [ ]:
with open('data/dyntar/sample.json', 'r') as f:
    data = DynamicTariffAnalysisInput.model_validate_json(f.read())

df = data.data_frame()

df
Out[ ]:
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¶

In [ ]:
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']
In [ ]:
result_df = df[['electricity_delivered', 'electricity_exported']].copy()
result_df.rename(columns={'electricity_delivered': 'electricity_delivered_smr3', 'electricity_exported': 'electricity_exported_smr3'}, inplace=True)
In [ ]:
result_df
Out[ ]:
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

In [ ]:
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
In [ ]:
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
In [ ]:
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')
In [ ]:
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})
In [ ]:
heatmap_score_combined = heatmap_score_delivered - heatmap_score_exported
heatmap['combined'] = heatmap_score_combined
In [ ]:
heatmap = heatmap.resample('H').sum()
In [ ]:
result_ms = result_df.resample('MS').sum()
In [ ]:
result_ms
Out[ ]:
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
In [ ]:
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')
In [ ]:
output_result = DynamicTariffAnalysisOutput.model_construct(
    result_pt15m=TimeSeries.from_pandas(result_df),
    result_p1m=TimeSeries.from_pandas(result_ms),
    heatmap=TimeSeries.from_pandas(heatmap)
)
In [ ]:
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¶

In [ ]:
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
No description has been provided for this image
Out[ ]:
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.

In [ ]:
# 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
No description has been provided for this image
Out[ ]:
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

In [ ]:
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'))
In [ ]:
# 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()
No description has been provided for this image
In [ ]:
# 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()
No description has been provided for this image
In [ ]:
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()
No description has been provided for this image
In [ ]: