Energy Load Forecast with Visualization in Interactive Tool#
In this notebook, we will show how to use NeuralProphet to forecast energy load and visualize the results in an interactive tool. The forecast is based on the RE-Europe data set, which models the continental European electricity system, including demand and renewable energy inflows for the period 2012-2014. The total data set comprises 1494 buses, which we will reduce to 5 buses for the purpose of this example.
[1]:
import pandas as pd
import numpy as np
from neuralprophet import NeuralProphet, set_log_level
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import ipywidgets as widgets
from ipywidgets import interact_manual
# Set plotting backend to plotly for interactive plots and plotly-static for static plots
plotting_backend = "plotly-static"
The dataset includes for every bus and every hour of the year the following information: * load (MW) * actual solar production (MW) * forecasted solar production (MW)
The forecasts are at hour 00 of every day for the next 24 hours.
[2]:
# load data
df = pd.read_csv(
"https://raw.githubusercontent.com/ourownstory/neuralprophet-data/main/datasets/multivariate/ER_Europe_subset_10nodes.csv"
)
df["ID"] = df["ID"].astype(str)
IDs = df["ID"].unique()
df["ds"] = pd.to_datetime(df["ds"])
# use one year for faster training
df = df[df["ds"] > "2014-01-01"]
df.head()
[2]:
ds | ID | y | solar | solar_fcs | |
---|---|---|---|---|---|
17545 | 2014-01-01 01:00:00 | 1 | 72.3142 | 0.0 | 0.0 |
17546 | 2014-01-01 02:00:00 | 1 | 67.5617 | 0.0 | 0.0 |
17547 | 2014-01-01 03:00:00 | 1 | 63.0677 | 0.0 | 0.0 |
17548 | 2014-01-01 04:00:00 | 1 | 59.9401 | 0.0 | 0.0 |
17549 | 2014-01-01 05:00:00 | 1 | 58.9296 | 0.0 | 0.0 |
[16]:
# plot first month of data
fig = px.line(df[df["ds"] < "2014-02-01"], x="ds", y="y", color="ID")
fig.update_layout(
xaxis_title="Date",
yaxis_title="Energy consumption",
legend_title="ID",
height=600,
width=1000,
)
if plotting_backend == "plotly-static":
fig.show("svg")
1. Data Preparation#
To better capture different seasons of the year and days of the week, we add conditional seasonaility. For every season of the year (summer, winter, fall and spring) and weekdays/weekend a separate seasonality is modelled.
[4]:
# Conditional Seasonality: 4 Seasons for yearly and weekly seasonality
df["summer"] = 0
df.loc[df["ds"].dt.month.isin([6, 7, 8]), "summer"] = 1
df["winter"] = 0
df.loc[df["ds"].dt.month.isin([12, 1, 2]), "winter"] = 1
df["fall"] = 0
df.loc[df["ds"].dt.month.isin([9, 10, 11]), "fall"] = 1
df["spring"] = 0
df.loc[df["ds"].dt.month.isin([3, 4, 5]), "spring"] = 1
# Conditional Seasonality: 4 Seasons, Weekday/Weekend distinction for each season, for daily seasonality
df["summer_weekday"] = 0
df.loc[(df["ds"].dt.month.isin([6, 7, 8])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])), "summer_weekday"] = 1
df["summer_weekend"] = 0
df.loc[(df["ds"].dt.month.isin([6, 7, 8])) & (df["ds"].dt.dayofweek.isin([5, 6])), "summer_weekend"] = 1
df["winter_weekday"] = 0
df.loc[(df["ds"].dt.month.isin([12, 1, 2])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])), "winter_weekday"] = 1
df["winter_weekend"] = 0
df.loc[(df["ds"].dt.month.isin([12, 1, 2])) & (df["ds"].dt.dayofweek.isin([5, 6])), "winter_weekend"] = 1
df["spring_weekday"] = 0
df.loc[(df["ds"].dt.month.isin([3, 4, 5])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])), "spring_weekday"] = 1
df["spring_weekend"] = 0
df.loc[(df["ds"].dt.month.isin([3, 4, 5])) & (df["ds"].dt.dayofweek.isin([5, 6])), "spring_weekend"] = 1
df["fall_weekday"] = 0
df.loc[(df["ds"].dt.month.isin([9, 10, 11])) & (df["ds"].dt.dayofweek.isin([0, 1, 2, 3, 4])), "fall_weekday"] = 1
df["fall_weekend"] = 0
df.loc[(df["ds"].dt.month.isin([9, 10, 11])) & (df["ds"].dt.dayofweek.isin([5, 6])), "fall_weekend"] = 1
df.head()
[4]:
ds | ID | y | solar | solar_fcs | summer | winter | fall | spring | summer_weekday | summer_weekend | winter_weekday | winter_weekend | spring_weekday | spring_weekend | fall_weekday | fall_weekend | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
17545 | 2014-01-01 01:00:00 | 1 | 72.3142 | 0.0 | 0.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
17546 | 2014-01-01 02:00:00 | 1 | 67.5617 | 0.0 | 0.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
17547 | 2014-01-01 03:00:00 | 1 | 63.0677 | 0.0 | 0.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
17548 | 2014-01-01 04:00:00 | 1 | 59.9401 | 0.0 | 0.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
17549 | 2014-01-01 05:00:00 | 1 | 58.9296 | 0.0 | 0.0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
2. Model definition#
The best parameters for the model can be obtained with hyperparameter tuning. By setting the quantiles, a prediction interval is modelled instead of a point forecast.
[5]:
# Model and prediction
quantiles = [0.015, 0.985]
params = {
"n_lags": 7 * 24,
"n_forecasts": 24,
"n_changepoints": 20,
"learning_rate": 0.01,
"ar_layers": [32, 16, 16, 32],
"yearly_seasonality": 10,
"weekly_seasonality": False,
"daily_seasonality": False,
"epochs": 40,
"batch_size": 1024,
"quantiles": quantiles,
}
m = NeuralProphet(**params)
m.set_plotting_backend(plotting_backend)
set_log_level("ERROR")
2.1 Lagged and future regressors#
With additional information, our model can get better. We use the last 10 days of the actual solar production as lagged regressor and the solar forecast as the future regressor.
[6]:
m.add_lagged_regressor(names="solar", n_lags=10 * 24)
m.add_future_regressor(name="solar_fcs")
[6]:
<neuralprophet.forecaster.NeuralProphet at 0x12cc57b20>
2.2 Conditional seasonalities#
[7]:
m.add_seasonality(name="summer_weekly", period=7, fourier_order=14, condition_name="summer")
m.add_seasonality(name="winter_weekly", period=7, fourier_order=14, condition_name="winter")
m.add_seasonality(name="spring_weekly", period=7, fourier_order=14, condition_name="spring")
m.add_seasonality(name="fall_weekly", period=7, fourier_order=14, condition_name="fall")
m.add_seasonality(name="summer_weekday", period=1, fourier_order=6, condition_name="summer_weekday")
m.add_seasonality(name="winter_weekday", period=1, fourier_order=6, condition_name="winter_weekday")
m.add_seasonality(name="spring_weekday", period=1, fourier_order=6, condition_name="spring_weekday")
m.add_seasonality(name="fall_weekday", period=1, fourier_order=6, condition_name="fall_weekday")
m.add_seasonality(name="summer_weekend", period=1, fourier_order=6, condition_name="summer_weekend")
m.add_seasonality(name="winter_weekend", period=1, fourier_order=6, condition_name="winter_weekend")
m.add_seasonality(name="spring_weekend", period=1, fourier_order=6, condition_name="spring_weekend")
m.add_seasonality(name="fall_weekend", period=1, fourier_order=6, condition_name="fall_weekend")
[7]:
<neuralprophet.forecaster.NeuralProphet at 0x12cc57b20>
3. Model training#
To test our model later, data is split into training and test data. The test data is the last 5% of the data. The model is trained on the training data.
[8]:
# Split Train and Test Data
df_train, df_test = m.split_df(df, valid_p=0.05, local_split=True)
print(f"Train shape: {df_train.shape}")
print(f"Test shape: {df_test.shape}")
Train shape: (41545, 17)
Test shape: (3090, 17)
[9]:
# Fit model
metrics_fit = m.fit(df_train, freq="H", metrics=True)
[ ]:
# Predict
forecast = m.predict(df)
[11]:
# extract season columns and convert to numeric
season_cols = [col for col in forecast.columns if "season" in col or "trend" in col]
forecast[season_cols] = forecast[season_cols].apply(pd.to_numeric)
4. Visualization#
4.1 Model plots#
NeuralProphet includes plot functions to explore the forecasted data. For an overview over the plot functions see the Visualizing tutorial.
[12]:
m.highlight_nth_step_ahead_of_each_forecast(1).plot(forecast.iloc[-10 * 24 :])
[13]:
m.plot_parameters(components=["trend", "trend_rate_change", "lagged_regressors"])
4.2 Interactive tool#
Let’s explore the data more interactively. By choosing a date, the forecast for the next 24 hours is shown, which represents industry standards.
[14]:
def get_day_ahead_format(date, ID, column_name):
values = pd.DataFrame(columns=["ds", column_name])
forecast_ID = forecast[forecast["ID"] == ID]
for i in range(0, 24):
if "%" in column_name:
step_name = f"yhat{i+1} {column_name}" # eg yhat10 1.5%
else:
step_name = f"{column_name}{i+1}"
ds = date + pd.Timedelta(hours=i + 1)
# throw an error if the date is not in the forecast
if ds not in forecast_ID["ds"].values:
raise ValueError(
f'The date {ds} is not in the forecast. Please choose a date between {forecast_ID["ds"].min()} and {forecast_ID["ds"].max()}'
)
step_value = forecast_ID[forecast_ID["ds"] == ds][step_name].values[0]
values = pd.concat([values, pd.DataFrame({"ds": ds, column_name: step_value}, index=[0])], ignore_index=True)
return values
[15]:
@interact_manual # with this line of code the forecast becomes interactive by letting you choose the ID and date
def show_forecast_uncertainty(ID=IDs, date=widgets.DatePicker()):
start = pd.to_datetime(date)
end = start + pd.DateOffset(days=1)
# get forecast
np_yhats = get_day_ahead_format(start, ID, "yhat")
np_yhats_lower = get_day_ahead_format(start, ID, "1.5%")
np_yhats_upper = get_day_ahead_format(start, ID, "98.5%")
np_ar = get_day_ahead_format(start, ID, "ar")
# get actual, temp and temp forecast
forecast_window = forecast[forecast["ID"] == ID]
forecast_window = forecast_window[(forecast_window["ds"] >= start) & (forecast_window["ds"] < end)]
df_window = df[df["ID"] == ID]
df_window = df_window[(df_window["ds"] >= start) & (df_window["ds"] < end)]
# get components
seasonalities_to_plot = []
for season in season_cols:
if np.abs(forecast_window[season].sum()) > 0:
seasonalities_to_plot.append(season)
fig = make_subplots(
rows=3,
cols=1,
shared_xaxes=True,
subplot_titles=("Actuals and Forecast", "Solar production", "Forecast Components"),
)
# plot actuals
fig.add_trace(
go.Scatter(
x=forecast_window["ds"], y=forecast_window["y"], name="Actuals", mode="markers", marker=dict(symbol="x")
),
row=1,
col=1,
)
# plot forecast and uncertainty
fig.add_trace(
go.Scatter(
x=np_yhats_lower["ds"],
y=np_yhats_lower["1.5%"],
fill=None,
mode="lines",
line_color="#A6B168",
name="1.5% quantile",
),
row=1,
col=1,
)
fig.add_trace(
go.Scatter(
x=np_yhats_upper["ds"],
y=np_yhats_upper["98.5%"],
fill="tonexty",
mode="lines",
line_color="#A6B168",
name="98.5% quantile",
),
row=1,
col=1,
)
fig.add_trace(go.Scatter(x=np_yhats["ds"], y=np_yhats["yhat"], name="Forecast", line_color="#7A863B"), row=1, col=1)
# plot solar
fig.add_trace(
go.Scatter(x=df_window["ds"], y=df_window["solar"], mode="lines", name="Solar actual production"), row=2, col=1
)
fig.add_trace(
go.Scatter(x=df_window["ds"], y=df_window["solar_fcs"], mode="lines", name="Solar forecast production"),
row=2,
col=1,
)
# plot different seasons of seasons_to_plot
for season in seasonalities_to_plot:
fig.add_trace(go.Scatter(x=forecast_window["ds"], y=forecast_window[season], name=season), row=3, col=1)
fig.add_trace(go.Scatter(x=np_ar["ds"], y=np_ar["ar"], name="Autoregression"), row=3, col=1)
fig.update_layout(
title=f"Forecast for ID {ID} on {date}",
yaxis=dict(title="Load (MW)"),
width=1200,
height=800,
)
if plotting_backend == "plotly-static":
fig.show("svg")
else:
fig.show()
[ ]: