GB Electricity Generation Data (Part 2)

Guy Lipman
4 min readApr 16, 2019

--

In my previous post I discussed how Great Britain’s electricity is generated, and how we might get data on how much electricity was generated by each generating unit each hour.

I mentioned that the electricity generated in a given half hour is partly a function of each generator’s Final Physical Notification, and partly a function of bids and offers to decrease or increase generation. In this post I will examine the bid/offer data, to understand which generators are offering flexibility, and under what terms.

You can access the bids/offers for a particular half hour on the Elexon website, however you can retrieve them from the api using the following python code:

def load_stackdata(date, period):
import requests
from io import StringIO
import pandas as pd
l1 = 'https://api.bmreports.com/BMRS/'
l2 = 'DETSYSPRICES'
l3 = '/V1?APIKey=' + APIKEY + '&'
l7 = 'ServiceType=csv'
d = date.strftime('%Y-%m-%d')
l4 = "SettlementDate=" + d
l4 += '&SettlementPeriod=' + str(period) + '&'
r = requests.get(l1+l2+l3+l4+l7 )
data = pd.read_csv(StringIO(r.text),
header=None,
skiprows=1)
data = data.iloc[:-1]
colnames = {4: 'bmunitid', 13: 'volume_orig', 12: 'price',
15: 'volume_arb_adj', 16: 'volume_accepted',
8: 'System'}
data = data[[4, 13, 12, 15, 16, 8]].rename(columns=colnames)
return data
import pandas as pd
data = load_stackdata(pd.datetime(2019,3,3), 18)

This returns a table for period 18 on the 3rd of March:

All rows with positive volume_orig indicate a generator that was willing to generate more (hence tending to be at higher prices), while negative volume_orig indicate a generator that was willing to buy back volume, ie reduce generation. If there are any generators willing to sell for a lower price than others are prepared to buy back, this is considered an arbitrage and automatically transacted, leading to arbitrage adjusted volumes. Also, some trades are tagged as System balancing transactions — these may occur to address regional imbalances.

Finally, if (as occurs in this case) grid supply exceeds demand, National Grid will sell the extra electricity in descending price order. Any generators that are at the clearing price (in this case -65.43 — that is, the generators had to be paid £65.43/MWh) are partially fulfilled, in this case each receiving 84% of their bid volume.

On the other hand, if grid demand exceeds supply, National Grid will buy the extra electricity in ascending price order. Again, any generators that are at the clearing price are partially fulfilled.

You can tell which generators are offering to sell or buyback the extra electricity from the bmunitid (see my previous post to understand how to convert these to full name and fuel type). I’m not actually sure what the rows with bmunitid = 4, 5 and 7 in the above table (maybe a reader will inform me).

While it was interesting to see how the actual levels of generation arose, what I thought might be more interesting was how much scope there was to move generation away from the actual level, using what technology and at what cost.

To do this I made a couple of assumptions:

  1. Any bids or offers that generators offered that weren’t accepted would still be available at the same price.
  2. Any bids or offers that generators had transacted (including arbitrage transactions) could be reversed at that same transaction cost, unless they are flagged as system transactions.
data['volume_untransacted'] = (data.volume_arb_adj 
- data.volume_accepted)
data['volume_transacted'] = (data.volume_untransacted
- data.volume_orig)
data['volume_transacted'] *= np.where(data.System=='T',0.0,1.0)
data['volume_to_sell'] = (np.where(data.volume_untransacted>0,
data.volume_untransacted, 0)
+ np.where(data.volume_transacted>0,
data.volume_transacted, 0))
data['volume_to_buy'] = (np.where(data.volume_untransacted<0,
data.volume_untransacted, 0)
+ np.where(data.volume_transacted<0,
data.volume_transacted, 0))*-1
sells = data.rename({'volume_to_sell': 'volume'})
sells = sells[sells.volume>=0.99]
sells = sells[['bmunitid','price','volume']]
sells.sort_values('price', ascending=True, inplace=True)
sells['total_volume'] = sells.volume.cumsum()
sells = sells[sells.total_volume<250]
buys = data.rename({'volume_to_buy': 'volume'})
buys = buys[buys.volume>=0.99]
buys = buys[['bmunitid','price','volume']]
buys.sort_values('price', ascending=False, inplace=True)
buys['total_volume'] = buys.volume.cumsum()
buys = buys[buys.total_volume<250]

This code gives me two sorted tables, the first showing the first 250MWh of volume that generators were willing to buyback, and the second showing the first 250MWh of volume that generators were willing to sell.

To make the tables slightly more informative, I also mapped the units to their fueltype (using the table genunits created in the previous post, noting that some of the mapping is unreliable):

genunitmap = pd.Series(genunits.fueltype.values, 
index=genunits.bmunitid)
sells['fueltype'] = sells.bmunitid.map(genunitmap)
buys['fueltype'] = buys.bmunitid.map(genunitmap)
willingness to buy (back)
willingness to sell extra

This suggests that if we were to increase energy demand, it would be met by increasing gas generation (CCGT), but if we were to reduce energy demand, it would be met by reducing wind energy supply. I find this interesting — I would have expected gas to be the marginal supplier in both directions. In fact, it appears that all the bids by gas power stations to buyback volumes were already accepted, and if we want to reduce supply we have to resort to paying wind resources a lot.

The obvious next step would be to see how this situation varies over time, but that will have to wait until I have a bit more time for analysis.

--

--

Guy Lipman

Fascinated by what makes societies and markets work, especially in sustainable energy. http://guylipman.com. Views not necessarily reflect those of my employer.