Solar and Wind Curtailment#

Renewables like wind and solar regularly produce energy in excess of demand. In order to keep supply and demand balanced on the grid, the result is “curtailment”, or purposefully reducing output.

In this notebook, we’ll walk through accessing the curtailment data for CAISO

import gridstatus
import pandas as pd
import plotly.express as px
import numpy as np
from plotly.subplots import make_subplots
import plotly.graph_objects as go
iso = gridstatus.CAISO()

Get Curtailment Data#

First, we will query for curtailment data. CAISO publishes curtailment data starting on June 30, 2016. We use the save_to parameter to save the data locally, so it is easier to reload later.

df = iso.get_curtailment(start="Jan 1, 2020", end="Nov 30, 2022", save_to="curtailment/")

we can easily reload the data in the curtailment folder like this. By default it loads with UTC timezone unless we specify otherwise.

df = gridstatus.load_folder("curtailment", time_zone=gridstatus.CAISO.default_timezone)
df
100%|██████████| 1052/1052 [00:00<00:00, 1347.85it/s]
Time Curtailment Type Curtailment Reason Fuel Type Curtailment (MWh) Curtailment (MW)
0 2020-01-01 08:00:00-08:00 Economic Local Solar 0.0 NaN
1 2020-01-01 08:00:00-08:00 Economic System Solar 27.0 153.0
2 2020-01-01 08:00:00-08:00 Economic System Wind 6.0 40.0
3 2020-01-01 09:00:00-08:00 Economic Local Solar 78.0 524.0
4 2020-01-01 09:00:00-08:00 Economic Local Wind 7.0 46.0
... ... ... ... ... ... ...
28818 2022-11-17 13:00:00-08:00 Economic Local Solar 143.0 273.0
28819 2022-11-17 13:00:00-08:00 Economic System Solar 0.0 NaN
28820 2022-11-17 14:00:00-08:00 Economic Local Solar 18.0 41.0
28821 2022-11-17 15:00:00-08:00 Economic System Solar 1.0 9.0
28822 2022-11-17 16:00:00-08:00 Economic System Solar 1.0 3.0

28823 rows × 6 columns

next, let’s reformat the data to make it easier to work with

df["Type"] = (
        df["Curtailment Reason"].str.lower().str.capitalize()
        + " "
        + df["Fuel Type"]
        + " Curtailment (MWh)"
    )
curtailment = df.pivot_table(
        values="Curtailment (MWh)", index="Time", columns="Type"
    ).fillna(
        0
    )

curtailment["Total Solar Curtailment (MWh)"] = curtailment["Local Solar Curtailment (MWh)"] + curtailment["System Solar Curtailment (MWh)"] 
curtailment["Total Wind Curtailment (MWh)"] = curtailment["Local Wind Curtailment (MWh)"] + curtailment["System Wind Curtailment (MWh)"] 
curtailment["Total Curtailment (MWh)"] = curtailment["Total Solar Curtailment (MWh)"] + curtailment["Total Wind Curtailment (MWh)"]
curtailment.columns.name = None
curtailment = curtailment.resample("1H").sum()
curtailment
Local Solar Curtailment (MWh) Local Wind Curtailment (MWh) System Solar Curtailment (MWh) System Wind Curtailment (MWh) Total Solar Curtailment (MWh) Total Wind Curtailment (MWh) Total Curtailment (MWh)
Time
2020-01-01 08:00:00-08:00 0.0 0.0 27.0 6.0 27.0 6.0 33.0
2020-01-01 09:00:00-08:00 78.0 7.0 138.0 13.0 216.0 20.0 236.0
2020-01-01 10:00:00-08:00 12.0 0.0 917.0 43.0 929.0 43.0 972.0
2020-01-01 11:00:00-08:00 19.0 0.0 1229.0 44.0 1248.0 44.0 1292.0
2020-01-01 12:00:00-08:00 216.0 25.0 1194.0 131.0 1410.0 156.0 1566.0
... ... ... ... ... ... ... ...
2022-11-17 12:00:00-08:00 95.0 0.0 0.0 0.0 95.0 0.0 95.0
2022-11-17 13:00:00-08:00 143.0 0.0 0.0 0.0 143.0 0.0 143.0
2022-11-17 14:00:00-08:00 18.0 0.0 0.0 0.0 18.0 0.0 18.0
2022-11-17 15:00:00-08:00 0.0 0.0 1.0 0.0 1.0 0.0 1.0
2022-11-17 16:00:00-08:00 0.0 0.0 1.0 0.0 1.0 0.0 1.0

25233 rows × 7 columns

Visualizing Curtailment#

Monthly Curtailment#

monthly = curtailment.resample("M").sum()
monthly["Month"] = monthly.index.month
monthly["Year"] = monthly.index.year

fig = px.bar(monthly, 
             x=monthly.index, 
             y=["Total Solar Curtailment (MWh)", "Total Wind Curtailment (MWh)"],
             title="Monthly Solar and Wind Curtailment in CAISO (MWh)")
             
# legend upper left corner
fig.update_layout(legend=dict(
    orientation="h",
    yanchor="bottom",
    y=1.02,
    xanchor="left",
    x=0,
    title_text=None
))
fig.update_yaxes(title_text="Curtailment (MWh)")
fig.show("svg", width=1200, height=600)
../../_images/ce961028c57da1043907e7537fe9d134b9280d22f35290e2b4c6e599d6be0a00.svg

Average Hourly Curtailment#

avg_hourly = curtailment.groupby(curtailment.index.hour).mean()
fig = px.line(avg_hourly,
              x=avg_hourly.index,
              y=["Total Solar Curtailment (MWh)", "Total Wind Curtailment (MWh)"],
              title="Average Hourly Solar and Wind Curtailment in CAISO (MWh)")
fig.update_yaxes(title_text="Curtailment (MWh)")              
fig.show("svg", width=1200, height=600)
../../_images/f7cd6ff96d29b7e6ece6d3f87d3def95a769680c5c44f5f0e4f3c9640370adb9.svg
curtailment["Year"] = curtailment.index.year
yearly_sum = curtailment.groupby("Year").sum()
index = yearly_sum.index.astype(str).tolist()
index[-1] = "2022 YTD"
yearly_sum.index = index

fig = px.bar(yearly_sum,
    x=yearly_sum.index, y=["Total Solar Curtailment (MWh)", "Total Wind Curtailment (MWh)"],
    title="Total Solar and Wind Curtailment in CAISO (MWh)",)
fig.update_layout(legend=dict(
    yanchor="bottom",
    y=.9,
    xanchor="left",
    x=0,
    title_text=None
))
fig.update_yaxes(title_text="Curtailment (MWh)")
fig.update_xaxes(title_text="Year")
fig.show("svg", width=1200, height=600)
../../_images/6d22e12891df3b03a2f91cd70041568bd5a95d4dac1f588e60a391f5610d7c2f.svg