Skip to content
Forecasting the local electricity market
  • AI Chat
  • Code
  • Report
  • Understanding the local electricity market

    📖 Background

    You work for an energy company in Australia. Your company builds solar panel arrays and then sells the energy they produce to industrial customers. The company wants to expand to the city of Melbourne in the state of Victoria.

    Prices and demand for electricity change every day. Customers pay for the energy received using a formula based on the local energy market's daily price.

    Your company's pricing committee wants your team to estimate energy prices for the next 12-18 months to use those prices as the basis for contract negotiations.

    In addition, the VP of strategy is researching investing in storage capacity (i.e., batteries) as a new source of revenue. The plan is to store some of the energy produced by the solar panels when pricing conditions are unfavorable and sell it by the next day on the open market if the prices are higher.

    💾 The data

    You have access to over five years of energy price and demand data (source):

    • "date" - from January 1, 2015, to October 6, 2020.
    • "demand" - daily electricity demand in MWh.
    • "price" - recommended retail price in AUD/MWh.
    • "demand_pos_price" - total daily demand at a positive price in MWh.
    • "price_positive" - average positive price, weighted by the corresponding intraday demand in AUD/MWh.
    • "demand_neg_price" - total daily demand at a negative price in MWh.
    • "price_negative" - average negative price, weighted by the corresponding intraday demand in AUD/MWh.
    • "frac_neg_price" - the fraction of the day when the demand traded at a negative price.
    • "min_temperature" - minimum temperature during the day in Celsius.
    • "max_temperature" - maximum temperature during the day in Celsius.
    • "solar_exposure" - total daily sunlight energy in MJ/m^2.
    • "rainfall" - daily rainfall in mm.
    • "school_day" - "Y" if that day was a school day, "N" otherwise.
    • "holiday" - "Y" if the day was a state or national holiday, "N" otherwise.

    Note: The price was negative during some intraday intervals, so energy producers were paying buyers rather than vice-versa.

    💪 Competition challenge

    Create a report that covers the following:

    1. How do energy prices change throughout the year? Are there any patterns by season or month of the year?
    2. Build a forecast of daily energy prices the company can use as the basis of its financial planning.
    3. Provide guidance on how much revenue the energy storage venture could generate per year using retail prices and a 70MWh storage system.
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    from statsmodels.tsa.seasonal import seasonal_decompose
    from statsmodels.tsa.holtwinters import ExponentialSmoothing
    
    sns.set_style('whitegrid')
    data = pd.read_csv('./data/energy_demand.csv', parse_dates=['date'])
    data.head()

    Q1. How do energy prices change throughout the year? Are there any patterns by season or month of the year?

    Pivot display of energy prices by month of year

    # Convert the 'date' column to datetime format
    data['date'] = pd.to_datetime(data['date'])
    
    # Extract the year and month for grouping
    data['year'] = data['date'].dt.year
    data['month'] = data['date'].dt.month
    
    # Group by year and month to calculate the mean price for each month
    monthly_avg_price = data.groupby(['year', 'month'])['price'].mean().reset_index()
    
    # Plot the data
    plt.figure(figsize=(20, 10))
    
    plt.subplot(3, 1, 1)
    
    # Pivot the data for better visualization
    monthly_avg_price_pivot = monthly_avg_price.pivot(index='month', columns='year', values='price')
    
    for year in monthly_avg_price_pivot.columns:
        plt.plot(monthly_avg_price_pivot.index, monthly_avg_price_pivot[year], marker='o', label=year)
    
    plt.title('Monthly Average Energy Prices (2015-2020)')
    plt.xlabel('Month')
    plt.ylabel('Average Price (AUD/MWh)')
    plt.xticks(range(1, 13))
    plt.legend(title='Year')
    plt.grid(True)
    
    plt.subplot(3, 1, 2)
    # Insights on yearly trends
    yearly_avg_price = data.groupby('year')['price'].mean()
    yearly_avg_price.plot(kind='bar', figsize=(10, 6), color='skyblue')
    plt.title('Yearly Average Energy Prices (2015-2020)')
    plt.xlabel('Year')
    plt.ylabel('Average Price (AUD/MWh)')
    plt.grid(True)
    
    
    plt.subplot(3, 1, 3)
    # Insights on monthly trends
    yearly_avg_price = data.groupby('month')['price'].mean()
    yearly_avg_price.plot(kind='bar', figsize=(10, 6), color='skyblue')
    plt.title('Monthly Average Energy Prices (2015-2020)')
    plt.xlabel('Month')
    plt.ylabel('Average Price (AUD/MWh)')
    plt.grid(True)
    
    plt.tight_layout()
    # Avg price by year and month
    monthly_avg_price_pivot
    df1 = data.copy()
    mask1 = (df1['price'] < 500)
    mask2 = (df1['date'] >= '2019-07-01')
    mask3 = (df1['date'] <= '2019-09-30')
    
    plt.figure(figsize = (16, 6))
    df1 = df1[mask1 & mask2 & mask3].reset_index(drop = True)
    sns.scatterplot(data = df1, y = 'demand', x = 'price', alpha = 0.5)
    plt.title("2019 Q3 Daily avg price vs Daily demand (MWh)")
    plt.ylim(0, 200000)
    plt.tight_layout()

    Time Series Decomposition

    # Ensure the data is sorted by date
    df1 = data.sort_values(by='date')
    
    # Set the date as the index
    df1.set_index('date', inplace=True)
    
    # Use additive model instead due to negative and zero values in the price data
    decomposition = seasonal_decompose(df1['price'], model='additive', period=365)
    
    # Plot the decomposed components
    plt.figure(figsize=(12, 8))
    
    # Observed
    plt.subplot(411)
    plt.plot(decomposition.observed, label='Observed')
    plt.legend(loc='upper left')
    
    # Trend
    plt.subplot(412)
    plt.plot(decomposition.trend, label='Trend', color='orange')
    plt.legend(loc='upper left')
    
    # Seasonal
    plt.subplot(413)
    plt.plot(decomposition.seasonal, label='Seasonal', color='green')
    plt.legend(loc='upper left')
    
    # Residual
    plt.subplot(414)
    plt.plot(decomposition.resid, label='Residual', color='red')
    plt.legend(loc='upper left')
    
    plt.tight_layout()
    plt.show()
    

    Q2. Build a forecast of daily energy prices the company can use as the basis of its financial planning.

    # Select the price column and drop any missing values
    df1 = data.set_index('date')
    
    price_series = df1['price'].dropna()
    
    # Split the data into training and test sets
    train_size = int(len(price_series) * 0.8)
    train, test = price_series[:train_size], price_series[train_size:]
    
    # Build the Holt-Winters model
    model = ExponentialSmoothing(train, seasonal='add', seasonal_periods=365).fit()
    
    # Forecast the next 12-18 months (let's choose 540 days, approximately 18 months)
    forecast_steps = 540
    forecast = model.forecast(steps=forecast_steps)
    
    # Plot the actual data and the forecast
    plt.figure(figsize=(14, 6))
    plt.plot(price_series, label='Actual Price')
    plt.plot(forecast, label='Forecasted Price', linestyle='--')
    plt.title('Energy Price Forecast')
    plt.xlabel('Date')
    plt.ylabel('Price (AUD/MWh)')
    plt.legend()
    plt.grid(True)
    plt.show()
    
    forecast_df = forecast.to_frame(name='forecasted_price')
    forecast = forecast_df.loc['2020-10-07':]
    plt.figure(figsize = (16, 5))
    sns.lineplot(data = forecast, x = forecast.index, y = 'forecasted_price')
    plt.xticks(rotation = 90)
    plt.title("Price forecast from October 7 through February 1")
    plt.xlabel("Date")
    plt.tight_layout()