Energy Consumption Forecasting with Prophet

Analyze hourly energy consumption data, explore daily and weekly patterns, and build a forecasting model using Prophet.

This page shows an AI data analysis example. A workflow is a list of prompts (instructions) used to complete one analysis task step by step. We send the same prompt list to multiple AI models, then compare their notebook outputs and scores.

What this AI workflow does

This AI Data Analyst workflow loads the AEP hourly energy consumption CSV and plots the full time series to inspect the overall trend. It computes and visualizes average consumption patterns by hour of day and by day of week to highlight recurring seasonality. It fits a Prophet model to the historical data, forecasts the next 30 days, and displays forecast components including trend and seasonal effects.

Who this example is for

This is for analysts and data scientists who need a quick, reproducible baseline forecast for hourly electricity demand. It is also useful for practitioners learning how to translate exploratory time-series patterns into a Prophet forecasting workflow.

Expected analysis outcomes

These are the results the AI workflow is expected to generate.

  • Line chart of the full hourly consumption time series
  • Bar chart of average consumption by hour of day
  • Bar chart of average consumption by day of week
  • 30-day Prophet forecast plot with uncertainty intervals
  • Prophet components plot showing trend and seasonalities

Tools and libraries used

Main Python packages and tooling used to run this AI data analysis task.

  • pandas
  • numpy
  • matplotlib
  • seaborn
  • prophet

Prompt sequence

This is the exact list of prompts used in this workflow. The same prompt sequence is sent to each model so outputs and scores can be compared fairly.

  1. 1load AEP hourly energy data from https://raw.githubusercontent.com/pplonski/datasets-for-start/refs/heads/master/aep-hourly-energy-consumption/AEP_hourly.csv and plot overall trend
  2. 2show average consumption by hour of day and by day of week
  3. 3fit a hourly Prophet model and forecast for the next 30 days
  4. 4show the forecast components: trend, weekly, yearly seasonality

Model Comparison

We compared several LLM models on the same analysis task. The table below shows their scores side by side. You can also open each model run to check the full conversation and notebook results.

Model SourceScoreTaskExecutionOutputReasoningReliabilityRun
gemma4:31b10/102/22/23/32/21/1Open gemma4:31b conversation
glm-5.110/102/22/23/32/21/1Open glm-5.1 conversation
gpt-5.410/102/22/23/32/21/1Open gpt-5.4 conversation
gpt-oss:120b10/102/22/23/32/21/1Open gpt-oss:120b conversation
qwen3.5:397b10/102/22/23/32/21/1Open qwen3.5:397b conversation
qwen3-coder-next3/101/20/21/31/20/1Open qwen3-coder-next conversation
How scoring works

Each run is graded on five dimensions:

  • Task Completion (0-2): how fully the workflow answered the requested analysis task and completed required steps.
  • Execution Correctness (0-2): whether generated code is correct, consistent, and likely runnable without major fixes.
  • Output Quality (0-3): how well tables, charts, and results match the expected analytical outcomes.
  • Reasoning Quality (0-2): clarity and correctness of interpretation, explanation, and conclusions.
  • Reliability (0-1): robustness of the run, including low hallucination risk and stable behavior.

The final score is the sum of these dimensions, so total score = 0-10. Higher values indicate more complete, correct, and reliable notebook results for this workflow.

AI Analysis with gpt-5.4

This run uses AI with a step-by-step list of prompts to perform the analysis task Energy Consumption Forecasting with Prophet. Below, you can review the full conversation generated with gpt-5.4. After the conversation, we show detailed score cards for this run.

load AEP hourly energy data from https://raw.githubusercontent.com/pplonski/datasets-for-start/refs/heads/master/aep-hourly-energy-consumption/AEP_hourly.csv and plot overall trend

Let’s start by loading the data and checking the basic structure first. After that, we can plot the overall trend cleanly.

Load and inspect dataset
import pandas as pd
from IPython.display import display

url = "https://raw.githubusercontent.com/pplonski/datasets-for-start/refs/heads/master/aep-hourly-energy-consumption/AEP_hourly.csv"

df = pd.read_csv(url, parse_dates=["Datetime"])

display(pd.DataFrame({"rows": [df.shape[0]], "columns": [df.shape[1]]}))
display(pd.DataFrame({"column_name": df.columns}))
display(df.head())
rows columns
0 121273 2
column_name
0 Datetime
1 AEP_MW
Datetime AEP_MW
0 2004-12-31 01:00:00 13478.0
1 2004-12-31 02:00:00 12865.0
2 2004-12-31 03:00:00 12577.0
3 2004-12-31 04:00:00 12517.0
4 2004-12-31 05:00:00 12670.0

The data looks ready for a time-series plot: it has a datetime column and one hourly energy column, with 121,273 rows, so you have a long hourly history to visualize. The sample also shows the readings are numeric megawatt values over time, which matches the trend-plot goal.

Plot energy consumption over time
import seaborn as sns
import matplotlib.pyplot as plt

df = df.sort_values("Datetime")

plt.figure(figsize=(14, 5))
sns.lineplot(data=df, x="Datetime", y="AEP_MW", linewidth=0.6)
plt.title("AEP Hourly Energy Consumption Over Time")
plt.xlabel("Datetime")
plt.ylabel("AEP_MW")
plt.tight_layout()
Thinking

Creating yearly summaries

I think we shouldn't replot everything but rather create a compact summary of yearly values. I want to end the code with just one simple expression. Maybe displaying the summary in a table format would work well, capturing yearly averages, min, max, and counts since the goal is to visualize trends. I’ll use a code snippet to aggregate the data by year and will keep it minimal without headings for easy insight.

The data inspection already shows a clean 2-column time series with hourly load values, and the plot call produced a valid figure. To support the overall-trend view with explicit values, a compact yearly summary is the missing piece.

Compute yearly average AEP MW
yearly_trend = (
    df.set_index("Datetime")["AEP_MW"]
    .resample("YE")
    .mean()
    .rename("avg_AEP_MW")
    .reset_index()
)

yearly_trend["year"] = yearly_trend["Datetime"].dt.year
yearly_trend[["year", "avg_AEP_MW"]]
year avg_AEP_MW
0 2004 15176.724388
1 2005 15842.990865
2 2006 15737.224252
3 2007 16645.519297
4 2008 16536.655659
5 2009 15254.111669
6 2010 16008.619504
7 2011 15815.389472
8 2012 15352.940667
9 2013 15198.211578
10 2014 15169.078890
11 2015 14868.922603
12 2016 14784.225979
13 2017 14483.738356
14 2018 15290.612734
Thinking

Deciding on data presentation

I need to provide an answer based on my notebook state, choosing between Path A or B. The objective is to load and plot the overall trend using the chart and numeric yearly trend data. The data I've gathered shows that the yearly average AEP_MW values fluctuated from 2004 to 2018. The peak was in 2007 at 16,645.5, followed by a decline by 2017 to 14,483.7, and then a rebound in 2018 to 15,290.6. I should avoid restating visible outputs and instead focus on insights.

The overall trend is now supported by both the time-series plot and the yearly summary values.

The plot shows a long hourly load series with strong short-term volatility and clear seasonality, which is typical for electricity demand. The yearly averages add the readable trend underneath that noise: demand rises from the mid-2000s into a peak around 2007, then generally softens over the following years, reaching its lowest average around 2017, with a modest rebound in 2018.

So, for the original request, the data has been loaded and the overall trend is visible: AEP hourly energy consumption does not follow a steady upward or downward line over time; instead, it peaks in the late 2000s and trends lower afterward, while still fluctuating substantially hour to hour.

show average consumption by hour of day and by day of week
Thinking

Crafting code for data visualization

I’m putting together code to create an "hourly_avg" dataframe and then a plot, probably either a bar plot or a line plot. I want to avoid mutating my original dataframe too much. I’ll use assign to create an "hour" column without altering the original df. At the end, I’ll call plt.show() without needing a final bare expression, since that doesn’t count as returning a table or displaying extra data. I’ll keep it concise!

Let’s start with average consumption by hour of day. After that, we can do day of week in the next step.

Plot average consumption by hour
import seaborn as sns
import matplotlib.pyplot as plt

hourly_avg = (
    df.assign(hour=df["Datetime"].dt.hour)
    .groupby("hour", as_index=False)["AEP_MW"]
    .mean()
    .rename(columns={"AEP_MW": "avg_AEP_MW"})
)

plt.figure(figsize=(10, 4))
sns.lineplot(data=hourly_avg, x="hour", y="avg_AEP_MW", marker="o")
plt.title("Average AEP Consumption by Hour of Day")
plt.xlabel("Hour of Day")
plt.ylabel("Average AEP_MW")
plt.xticks(range(24))
plt.tight_layout()
plt.show()
Thinking

Deciding on Data Outputs

I need to decide between option A or B for showing average consumption by hour and day of the week. Right now, I only have a chart that shows hourly data, but it doesn't provide numeric values or daily insights. Path B seems necessary since we need those metrics. The latest outputs highlight intraday variations, but without values, they don't fulfill the goal. I need to include a minimal runnable code block to gather missing evidence and display both hourly and day-of-week averages clearly.Formatting Output Display

I’m considering using the display function without commas to show the outputs sequentially. I can import the display function from IPython and call it to display hourly_avg followed by dow_avg. It looks like I don't need a bare expression at the end, so that’s acceptable for my code block. I want to make sure that everything is concise and clean, as I think that will keep it clear for anyone checking the results.

The hourly chart already shows a clear intraday pattern, so average consumption does vary by hour. But the current notebook state still lacks the actual hourly values and any day-of-week summary, so one compact table step will complete the original request.

Compute weekday averages
from IPython.display import display

day_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

dow_avg = (
    df.assign(day_of_week=df["Datetime"].dt.day_name())
    .groupby("day_of_week", as_index=False)["AEP_MW"]
    .mean()
    .rename(columns={"AEP_MW": "avg_AEP_MW"})
)

dow_avg["day_of_week"] = pd.Categorical(dow_avg["day_of_week"], categories=day_order, ordered=True)
dow_avg = dow_avg.sort_values("day_of_week").reset_index(drop=True)

display(hourly_avg)
display(dow_avg)
hour avg_AEP_MW
0 0 14651.191569
1 1 13891.478433
2 2 13432.062995
3 3 13184.049008
4 4 13095.193350
5 5 13240.535813
6 6 13802.401464
7 7 14781.668381
8 8 15478.830233
9 9 15822.653740
10 10 16084.283934
11 11 16306.315592
12 12 16398.177087
13 13 16477.793035
14 14 16534.660202
15 15 16492.810645
16 16 16444.506134
17 17 16542.038781
18 18 16762.940047
19 19 16868.728334
20 20 16821.335180
21 21 16763.806292
22 22 16468.967550
23 23 15632.594183
day_of_week avg_AEP_MW
0 Monday 15810.973684
1 Tuesday 16057.615571
2 Wednesday 16013.589739
3 Thursday 16028.138281
4 Friday 15773.123911
5 Saturday 14610.979628
6 Sunday 14200.754680

Average consumption shows a strong daily cycle and a clear weekday/weekend split.

By hour of day:

  • Lowest average use is in the early morning, with the minimum around 4:00 at about 13,095 MW.
  • Consumption rises steadily after that through the morning and afternoon.
  • The highest average use is in the evening, peaking around 19:00 at about 16,869 MW.
  • Demand stays elevated from roughly 18:00 to 21:00, then declines late at night.

By day of week:

  • Tuesday has the highest average consumption at about 16,058 MW.
  • Wednesday and Thursday are very close behind, and Monday and Friday are slightly lower but still similar.
  • Saturday drops noticeably to about 14,611 MW.
  • Sunday is the lowest at about 14,201 MW.

So the pattern is:

  • Within a day: lowest before dawn, highest in the evening.
  • Within a week: weekdays are consistently higher than weekends, with Sunday being the lowest-consumption day.
fit a hourly Prophet model and forecast for the next 30 days
Thinking

Crafting summary table

I'm working on a code block to create a summary from my dataframe. I want to generate a prophet_df variable from df, sort it, and then check for duplicates and any missing hours using a full date range. I think displaying one summary table makes the most sense—maybe just using display(summary) for clean presentation. The rule suggests only one chart or table per step, so I’ll keep it streamlined. Let's proceed with crafting this!

We already have the hourly series loaded and sorted, so the next useful step is to make it Prophet-ready and quickly check whether the hourly timeline is clean enough for modeling.

Summarize hourly time series
from IPython.display import display

prophet_df = (
    df[["Datetime", "AEP_MW"]]
    .rename(columns={"Datetime": "ds", "AEP_MW": "y"})
    .sort_values("ds")
    .reset_index(drop=True)
)

full_hours = pd.date_range(prophet_df["ds"].min(), prophet_df["ds"].max(), freq="h")

summary = pd.DataFrame({
    "start": [prophet_df["ds"].min()],
    "end": [prophet_df["ds"].max()],
    "rows": [len(prophet_df)],
    "duplicate_timestamps": [prophet_df["ds"].duplicated().sum()],
    "expected_hourly_rows": [len(full_hours)],
    "missing_hours": [len(full_hours) - prophet_df["ds"].nunique()]
})

display(summary)
start end rows duplicate_timestamps expected_hourly_rows missing_hours
0 2004-10-01 01:00:00 2018-08-03 121273 4 121296 27

If that looks clean, the next step will be fitting an hourly Prophet model and generating a 30-day forecast.

The latest check shows the series is almost hourly-complete over a long history, but it has 4 duplicate timestamps and 27 missing hours. That matters for Prophet because duplicates should be resolved before fitting. The hourly and day-of-week summaries also show strong daily and weekly seasonality, which is a good fit for an hourly Prophet model.

Forecast next 30 days
try:
    from prophet import Prophet
except ImportError:
    import sys
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "prophet"])
    from prophet import Prophet

from IPython.display import display

train_df = (
    prophet_df.groupby("ds", as_index=False)["y"]
    .mean()
    .sort_values("ds")
    .reset_index(drop=True)
)

model = Prophet(
    daily_seasonality=True,
    weekly_seasonality=True,
    yearly_seasonality=True
)
model.fit(train_df)

future = model.make_future_dataframe(periods=30 * 24, freq="h")
forecast = model.predict(future)

forecast_30d = forecast.loc[forecast["ds"] > train_df["ds"].max(), ["ds", "yhat", "yhat_lower", "yhat_upper"]].reset_index(drop=True)

display(forecast_30d.head())
display(forecast_30d.tail())
forecast_30d
Importing plotly failed. Interactive plots will not work.
12:42:37 - cmdstanpy - INFO - Chain [1] start processing
12:45:43 - cmdstanpy - INFO - Chain [1] done processing
ds yhat yhat_lower yhat_upper
0 2018-08-03 01:00:00 15322.018171 13240.058394 17333.212748
1 2018-08-03 02:00:00 14737.911083 12782.909908 16836.807003
2 2018-08-03 03:00:00 14398.084738 12390.954742 16309.886219
3 2018-08-03 04:00:00 14350.303623 12179.569822 16221.600342
4 2018-08-03 05:00:00 14626.891119 12719.905609 16762.437900
ds yhat yhat_lower yhat_upper
715 2018-09-01 20:00:00 15393.469878 13422.430695 17284.973028
716 2018-09-01 21:00:00 15198.546230 13164.900906 17235.001366
717 2018-09-01 22:00:00 14672.406874 12706.757333 16692.664236
718 2018-09-01 23:00:00 13891.845784 12050.447761 16009.778234
719 2018-09-02 00:00:00 13027.480827 11007.020618 15095.652725
ds yhat yhat_lower yhat_upper
0 2018-08-03 01:00:00 15322.018171 13240.058394 17333.212748
1 2018-08-03 02:00:00 14737.911083 12782.909908 16836.807003
2 2018-08-03 03:00:00 14398.084738 12390.954742 16309.886219
3 2018-08-03 04:00:00 14350.303623 12179.569822 16221.600342
4 2018-08-03 05:00:00 14626.891119 12719.905609 16762.437900
... ... ... ... ...
715 2018-09-01 20:00:00 15393.469878 13422.430695 17284.973028
716 2018-09-01 21:00:00 15198.546230 13164.900906 17235.001366
717 2018-09-01 22:00:00 14672.406874 12706.757333 16692.664236
718 2018-09-01 23:00:00 13891.845784 12050.447761 16009.778234
719 2018-09-02 00:00:00 13027.480827 11007.020618 15095.652725

720 rows × 4 columns

You now have an hourly Prophet model fitted and a 30-day forecast produced.

From the notebook state, the historical series spans 2004-10-01 01:00:00 to 2018-08-03 00:00:00. Before fitting, there were 4 duplicate timestamps and 27 missing hours. The duplicates were handled by averaging values at the same timestamp, which is a reasonable fix for Prophet. The small number of missing hours is not a blocker because Prophet can train with gaps in time.

The fitted model generated exactly 720 hourly forecasts, which matches 30 days of hourly predictions. The forecast starts at 2018-08-03 01:00:00 and ends at 2018-09-02 00:00:00. The point forecasts and intervals look plausible relative to the seasonality seen earlier in the notebook: demand is lower overnight, rises through the day, and weekends are softer than weekdays. For example, the forecast begins around 15,322 MW at 01:00, dips into the early morning hours, and later forecasted evening values remain higher than late-night values. The uncertainty bands are also present for each hour, so you have both central predictions (yhat) and lower/upper bounds (yhat_lower, yhat_upper).

So, against the original goal, the notebook has already produced the requested hourly Prophet forecast for the next 30 days.

show the forecast components: trend, weekly, yearly seasonality
Thinking

Planning forecast visualization

I need to provide the forecast components in an iterative way. The user asks for a plot of components like trend, weekly, and yearly seasonality. Since I have the model and forecast ready, I can use Prophet's built-in plot_components function, which also includes daily despite the user only wanting specific components. However, it might be better to manually plot trend, weekly, and yearly as three subplots using Matplotlib. I just need to mention that the model fit succeeded and I have a 720-hour forecast before showing the code.Creating forecast components

I see that Prophet components are in the forecast columns, so I might create a DataFrame for the entire future horizon. However, showing seasonality components for the full forecast could make interpretation tricky due to repeated patterns. I could use the model.plot_components(forecast) utility, which gives a figure with trend and seasonality. But let's stick to the user's request and make a manual figure with three subplots: trend for the entire period, and a representative week for the weekly component. I need to proceed carefully!Generating component curves

I could generate standardized component curves for the forecast. For the trend, I can use the full forecast. The weekly seasonality might be tricky with plot_weekly, and same goes for using plot_yearly. A simple solution is to use model.plot_components(forecast), so the user can see trend, weekly, yearly, and daily components in one go. Though the request focuses on trend, weekly, and yearly, I’ll mention that the daily component will appear as well since it’s included in the model. Let's create a clear response.

The model fit completed cleanly, and the forecast already contains the component pieces. Since you enabled daily, weekly, and yearly seasonality, Prophet’s component plot will show all of them, including trend.

Plot forecast components
fig = model.plot_components(forecast)
Thinking

Displaying forecast data

I'm thinking about how to display a sample of forecast data over a week, but I want it to be minimal yet informative. Maybe a compact table would work well, showing components like dates, trends, and weekly and yearly forecasts — all grouped in one place. My goal is to highlight the underlying values since a chart alone wouldn’t suffice. I need to ensure I follow the notebook rule about displaying tables sequentially, but I think it could be okay to show two tables at once.Considering forecast components

I’m thinking about how to display the yearly components by sampling one day each week. However, since the forecast horizon is only 30 days, yearly changes happen gradually. It might be sufficient to create a compact table showcasing the first and last forecast timestamps, including relevant components. My original goal is to highlight these components rather than examining every numerical detail, so I’ll focus on presenting the most important information effectively.Providing insights and code

I think a single sampled table will work well, and I don't need to ask the user for additional information. I’ll provide insights alongside the code. Perhaps I can say something like, "The components plot rendered shows the model has separable trend, weekly, and yearly effects." Since the latest output is just a figure, I'll include a compact table of the underlying Prophet component values across the 30-day horizon along with the code. Crafting this concisely sounds good!

The components plot rendered successfully, so the model is decomposing the forecast into separate effects. From the current notebook state, those component values are already inside forecast; the only missing piece is a numeric view of trend, weekly, and yearly to support the chart.

Sample forecast components
import numpy as np

components_30d = forecast.loc[
    forecast["ds"] > train_df["ds"].max(),
    ["ds", "trend", "weekly", "yearly"]
].reset_index(drop=True)

sample_idx = np.linspace(0, len(components_30d) - 1, 12, dtype=int)
components_30d.iloc[sample_idx]
ds trend weekly yearly
0 2018-08-03 01:00:00 15051.945856 480.626164 1308.080792
65 2018-08-05 18:00:00 15053.894673 -994.828059 1224.220435
130 2018-08-08 11:00:00 15055.843490 494.399494 1152.024633
196 2018-08-11 05:00:00 15057.822289 -466.699982 1094.426314
261 2018-08-13 22:00:00 15059.771107 631.211548 1049.266630
326 2018-08-16 15:00:00 15061.719924 549.488077 1005.529413
392 2018-08-19 09:00:00 15063.698723 -1436.928087 948.777633
457 2018-08-22 02:00:00 15065.647540 470.594262 867.203381
522 2018-08-24 19:00:00 15067.596357 64.447977 750.149548
588 2018-08-27 13:00:00 15069.575156 334.114099 590.882494
653 2018-08-30 06:00:00 15071.523973 575.227827 397.302974
719 2018-09-02 00:00:00 15073.502773 -1458.776712 173.354052

The forecast components are now visible and supported by the numeric output.

From the observed component values:

  • Trend is nearly flat but slightly increasing over the 30-day forecast horizon, rising from about 15051.95 to 15073.50. That suggests the baseline energy demand is stable with a mild upward drift.
  • Weekly seasonality has a meaningful swing. In the sampled future points it ranges from roughly -1458.78 to 631.21, so day-of-week effects are clearly influencing the forecast. Negative values pull the forecast below the trend, positive values push it above the trend.
  • Yearly seasonality is strongly positive at the start of the forecast and then fades over the month, dropping from about 1308.08 to 173.35. That means the annual seasonal effect is still boosting demand in early August, but its contribution weakens as the forecast moves toward early September.

These outputs align with the components plot:

  • Trend = long-run baseline level
  • Weekly = repeating within-week pattern
  • Yearly = broader seasonal pattern across the year

So the forecast is being driven mostly by a stable baseline around 15k MW, plus noticeable weekly variation and a declining late-summer yearly seasonal boost.

Score cards for gpt-5.4 for Energy Consumption Forecasting with Prophet

Total score: 10/10

  • Task Completion: 2/2

    Excellent

    The notebook covers all major requested steps: loading the data, plotting the full series, summarizing hourly and weekly patterns, fitting Prophet, forecasting 30 days, and showing components.

  • Execution Correctness: 2/2

    Excellent

    The provided code appears runnable and produced outputs for each major step, including the Prophet fit and forecast. No major syntax or execution errors are evident in the notebook content.

  • Output Quality: 3/3

    Excellent

    Outputs match the expected outcomes semantically: a full time-series plot, hourly and weekly summaries, a 30-day forecast with uncertainty intervals, and a components plot. The hourly pattern peaks in the evening as expected.

  • Reasoning Quality: 2/2

    Excellent

    The explanations are generally correct and tied to the outputs, including the observed daily and weekly seasonality and the handling of duplicate timestamps before Prophet fitting. The reasoning is clear and relevant.

  • Reliability: 1/1

    Excellent

    The workflow is reasonably robust: it checks for duplicates and missing hours, aggregates duplicates before modeling, and handles Prophet installation if needed.

Try MLJAR Studio

Run the same type of AI-powered data analysis on your own datasets with conversational notebooks in MLJAR Studio.

Explore More AI Analysis Examples

Discover additional workflows across categories. Each example includes prompts, conversation outputs, and model-level scoring so you can compare approaches and results.