Forecast Monthly Revenue with ARIMA
Build an ARIMA model to forecast 3 months of revenue from 541K transactions.
Introduction
30 Second Summary
Every retail business wants to know how much revenue is coming in next month. Thousands of transactions flow through the system daily, but turning that raw data into a reliable prediction is what separates an analyst from a decision-maker.
In this project, you will download 541,909 real transactions from a UK online retailer, clean the messy data, uncover seasonal revenue patterns, and build a forecasting model that predicts the next 3 months of revenue with confidence intervals.
What You'll Build
Picture running the final cell of your Jupyter Notebook and seeing a chart where historical revenue traces a solid line, a dashed forecast extends three months into the future, and a shaded band shows how confident the model is in each prediction.
By the end of this project, you'll have:
- A cleaned monthly revenue chart built from 541,909 real transactions downloaded from the UCI Machine Learning Repository, showing exactly how a UK retailer's income changed month by month.
- A decomposition plot that breaks revenue into trend, seasonal, and residual components, revealing hidden patterns invisible in the raw numbers.
- A 3-month ARIMA forecast with shaded confidence intervals and a printed accuracy score, ready to present as a data-backed business recommendation.
- Secret Mission: A top-product revenue breakdown identifying which products drive the most revenue.
Are there any prerequisites?
This project assumes intermediate Python knowledge. You should have Anaconda installed with Jupyter Notebook, pandas, matplotlib, and statsmodels already available.
Before We Start
Before you write a single line of code, take a moment to lock in what you're about to build and why it matters.
Set Up Your Environment and Load the Data
You have committed to building a revenue forecasting notebook. Before you can analyze any trends or build any models, you need to get the raw data into your notebook and confirm you can work with it.
In this step, you will open a fresh Jupyter Notebook, install a package that downloads datasets from the UCI Machine Learning Repository, and load 541,909 real retail transactions into a DataFrame you can explore.
In this step, get ready to:
- Open Jupyter Notebook and create a new project notebook.
- Install the ucimlrepo package and load the Online Retail dataset.
- Inspect the raw data to understand its shape, columns, and contents.
Open Jupyter Notebook and install the data package
The ucimlrepo package lets you download datasets from the UCI Machine Learning Repository with a single Python command. You need to install it before you can fetch the retail data.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Anaconda Navigator and press Enter to open it.
- Find the Jupyter Notebook tile and click Launch.
Jupyter Notebook opens in your browser and shows a file browser.
- Click New in the top right corner, then select Python 3.
- Click the title Untitled at the top of the page to open the rename dialog.
- Type revenue_forecast and click Rename.
You now have an empty notebook called revenue_forecast.ipynb with one empty cell.
- Install the ucimlrepo package by typing this command into the first cell and pressing Shift+Enter to run it:
!pip install ucimlrepo
What does this command do?
The ! at the start tells Jupyter to run this as a terminal command instead of Python code. pip install ucimlrepo downloads the ucimlrepo package, which gives you a one-line function to fetch any dataset from the UCI Machine Learning Repository.
- Wait for the output to show Successfully installed ucimlrepo (or Requirement already satisfied if it was already present).
- Click Kernel in the top menu bar, then click Restart.
- When the dialog asks you to confirm, click Restart. This lets Python recognize the newly installed package.
- Go back to Cell 1 and comment out the install command by adding a # before the ! so the cell reads:
# Cell 1: Install ucimlrepo (run once, then restart kernel)
# !pip install ucimlrepo
Why comment it out?
You only need to install a package once. Commenting the line out prevents it from running again every time you re-run your notebook, while still leaving a note for anyone reading your code later.
Seeing an install error?
- Make sure you typed the ! before pip. Without it, Jupyter tries to run pip install as Python code and throws a syntax error.
- If you see a network or timeout error, check your internet connection and try running the cell again.
- help me fix my ucimlrepo install error in Jupyter Notebook
Import libraries and load the dataset
With the package installed, you can now pull all 541,909 transactions from the UCI Machine Learning Repository directly into your notebook. The ucimlrepo package splits the data across separate DataFrames, so you will also need to reconstruct them into a single table.
- Click into the empty cell below Cell 1 (Jupyter creates one automatically after a kernel restart).
- Import your libraries and fetch the dataset by pasting the following code into the cell:
# Cell 2: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from ucimlrepo import fetch_ucirepo
# Fetch the Online Retail dataset
online_retail = fetch_ucirepo(id=352)
# Reconstruct the full DataFrame
df = pd.concat([online_retail.data.ids, online_retail.data.features], axis=1)
What does this code do?
- pandas (imported as pd) is the core library for working with tabular data in Python.
- numpy (imported as np) provides numerical operations you will use later for error calculations.
- matplotlib.pyplot (imported as plt) is the charting library you will use for all your plots.
- fetch_ucirepo(id=352) downloads the Online Retail dataset from UCI. The returned object stores columns across separate DataFrames (.data.ids and .data.features), so pd.concat stitches them back into one complete table called df.
- Press Shift+Enter to run the cell. The dataset is about 22.6 MB, so the download may take a few seconds.
When the cell finishes, you see a new empty cell appear below it. No output is printed yet because you have not asked Python to display anything.
Seeing a ModuleNotFoundError?
- This means the kernel does not recognize the ucimlrepo package. Go back to Cell 1, uncomment the install line (remove the # before !), run it again, and restart the kernel before re-running Cell 2.
- If the download times out, check your internet connection and re-run the cell.
- help me fix ModuleNotFoundError for ucimlrepo in Jupyter Notebook
Inspect the data
The data is loaded, but you do not yet know what it looks like. Inspecting the shape, first few rows, column types, and summary statistics tells you what you are working with before you start cleaning.
Before you run the next cell, take a guess. How many rows do you think this dataset has, and what columns would a retail transaction dataset need?
- Add the following inspection code to the bottom of Cell 2, below your existing code:
# Inspect the data
print(df.shape)
print(df.head())
print(df.info())
print(df.describe())
What do these commands show?
- df.shape prints the number of rows and columns as a tuple.
- df.head() shows the first 5 rows so you can see what a single transaction looks like.
- df.info() lists every column with its data type and how many non-null values it has. Missing values show up here.
- df.describe() prints summary statistics (mean, min, max) for the numeric columns.
- Press Shift+Enter to re-run Cell 2.
You should see (541909, 8) printed first. That means 541,909 rows and 8 columns: InvoiceNo, StockCode, Description, Quantity, InvoiceDate, UnitPrice, CustomerID, and Country.
What should I see?
The head() output shows individual transactions from a UK-based online retailer, with columns for invoice number, product code, description, quantity ordered, date, unit price, customer ID, and country.
The info() output reveals that some columns have missing values. You will address those missing values in the next step when you clean the data.
Not seeing 541,909 rows?
- Make sure the pd.concat line includes both online_retail.data.ids and online_retail.data.features. Using only one of them gives you a partial table.
- Check that the argument in fetch_ucirepo is id=352. A different ID loads a different dataset.
- help me troubleshoot my UCI dataset loading issue in Jupyter Notebook
✔️ Awesome, I've got everything!
Great. Make sure you have saved your notebook by pressing Cmd+S (macOS) or Ctrl+S (Windows).
ⓧ I'd like to double check the full code
Here is the full code for revenue_forecast.ipynb so far. Cell 1 is the commented-out install command. Cell 2 contains your imports, data loading, and inspection.
# Cell 1: Install ucimlrepo (run once, then restart kernel)
# !pip install ucimlrepo
# Cell 2: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from ucimlrepo import fetch_ucirepo
# Fetch the Online Retail dataset
online_retail = fetch_ucirepo(id=352)
# Reconstruct the full DataFrame
df = pd.concat([online_retail.data.ids, online_retail.data.features], axis=1)
# Inspect the data
print(df.shape)
print(df.head())
print(df.info())
print(df.describe())
Your data is loaded and you have had a first look at half a million real transactions. Next up, you will clean the messy parts of this data and build a monthly revenue time series you can actually analyze.
Clean the Data and Build a Monthly Revenue Series
You have 541,909 rows of real retail transactions loaded and ready to explore. Now it is time to turn that raw data into something you can actually analyze.
The dataset contains cancellations, negative quantities, and missing customer IDs. If you skip cleaning, cancelled orders will inflate your revenue totals and produce numbers you cannot trust.
In this step, get ready to:
- Clean the raw data by removing cancellations, negative quantities, and missing customer IDs.
- Create a Revenue column from Quantity and UnitPrice.
- Aggregate into a monthly revenue time series and plot the trend.
Clean the raw transaction data
The UCI dataset description notes that any InvoiceNo starting with the letter "C" indicates a cancellation. You also need to handle rows with zero or negative quantities and missing CustomerID values.
- Create a new cell in your notebook and add the following code to clean the dataset and create a Revenue column:
# Cell 3: Clean the data
# Convert InvoiceDate to datetime
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
# Remove cancellations (InvoiceNo starting with 'C')
df = df[~df['InvoiceNo'].astype(str).str.startswith('C')]
# Remove rows with Quantity <= 0
df = df[df['Quantity'] > 0]
# Remove rows with missing CustomerID
df = df.dropna(subset=['CustomerID'])
# Create Revenue column
df['Revenue'] = df['Quantity'] * df['UnitPrice']
print(f"Cleaned data shape: {df.shape}")
print(df.head())
What does this code do?
- pd.to_datetime() converts the InvoiceDate column from raw strings into proper datetime objects so pandas can work with dates.
- The ~df['InvoiceNo'].astype(str).str.startswith('C') filter removes every cancelled order. The ~ symbol means "not", so you keep only rows that do NOT start with "C".
- Filtering Quantity > 0 removes any remaining rows with zero or negative quantities that would distort your totals.
- dropna(subset=['CustomerID']) drops rows where the customer is unknown, since you cannot attribute that revenue to a real transaction.
- The new Revenue column multiplies Quantity by UnitPrice to get the total revenue for each line item.
- Run the cell by pressing Shift+Enter.
You should see a printed shape showing a significantly smaller DataFrame (the exact row count depends on how many cancellations and invalid entries were removed). The head() output now includes your new Revenue column on the far right.
Seeing a KeyError or TypeError?
- Make sure you ran the Cell 2 code from Step 1 first. This cell depends on df already existing in memory.
- If you restarted the kernel since Step 1, re-run Cell 2 before running this cell.
- Check that column names match exactly. The dataset uses InvoiceNo (no space), not Invoice No.
Still stuck? Ask for help.
Aggregate into a monthly revenue series
Your cleaned DataFrame has one row per transaction, but you need one row per month. Resampling groups all transactions within each calendar month and sums their revenue into a single value.
- Create a new cell below and add the following code to build the monthly time series and plot it:
# Cell 4: Build monthly revenue series and plot
monthly_revenue = df.resample('MS', on='InvoiceDate')['Revenue'].sum()
plt.figure(figsize=(12, 5))
plt.plot(monthly_revenue, marker='o', linewidth=2)
plt.title('Monthly Revenue - UK Online Retailer')
plt.xlabel('Month')
plt.ylabel('Revenue (GBP)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(monthly_revenue)
What does this code do?
- resample('MS', on='InvoiceDate') groups transactions by calendar month. 'MS' stands for "month start", which gives each month a clean first-of-the-month date on the index.
- ['Revenue'].sum() adds up all the individual transaction revenues within each month to produce one total per month.
- The matplotlib plot draws a line chart with circular markers at each month. The grid and tight layout keep the chart clean and readable.
Before you run this cell, think about what you expect the chart to show. Do you think revenue will be flat, steadily rising, or have seasonal spikes?
- Run the cell by pressing Shift+Enter.
You should see a line chart with months on the x-axis and revenue (in GBP) on the y-axis. The chart covers roughly 12 months from December 2010 to December 2011, with a clear upward spike toward the end of the year.
Why the spike at the end?
This is a UK-based retailer. The revenue spike in late 2011 aligns with the holiday shopping season. Spotting patterns like this is exactly why you clean and visualize data before building a forecasting model.
Not seeing a chart?
- Make sure you ran the Cell 3 cleaning code first. This cell depends on the Revenue column existing in df.
- If you see an empty plot, double check that InvoiceDate was converted to datetime in Cell 3. The resample() method requires a datetime column to group by month.
- If the chart appears in a separate window instead of inline, add %matplotlib inline in a cell at the top of your notebook and re-run.
Need more help debugging?
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook before moving on.
ⓧ I'd like to double check the full code
The full notebook at this point exceeds what fits in a single code block. Scroll up and confirm you have the following cells in order:
- Cell 1: The commented-out !pip install ucimlrepo line from Step 1.
- Cell 2: Imports (pandas, numpy, matplotlib, fetch_ucirepo), dataset fetch, DataFrame reconstruction, and inspection prints.
- Cell 3: Data cleaning (datetime conversion, cancellation removal, quantity filter, missing CustomerID drop, Revenue column creation).
- Cell 4: Monthly revenue resample with 'MS' frequency, line chart plot, and print(monthly_revenue).
You now have a clean monthly revenue series with a visible seasonal pattern. Next up, you will decompose that pattern into its individual components and test whether the data is ready for forecasting.
Decompose the Time Series and Test for Stationarity
Your cleaned data and monthly revenue chart from the last step reveal a clear pattern: revenue rises and falls throughout the year, with a dramatic spike near the end. But you cannot tell yet whether those ups and downs are true seasonality or just random noise.
In this step, you will decompose the revenue series into its individual components to separate the real signal from the noise. You will also run a statistical test to check whether this data is ready for forecasting.
In this step, get ready to:
- Decompose the monthly revenue series into trend, seasonal, and residual components.
- Test for stationarity using the Augmented Dickey-Fuller test.
Decompose monthly revenue into trend, seasonal, and residual components
A time series like your monthly revenue is made up of layered signals. Seasonal decomposition separates those signals into three parts: the overall trend (is revenue going up or down over time?), the seasonal pattern (does the same month tend to spike or dip every year?), and the residual (the leftover noise that does not fit either pattern).
- Create a new cell in your notebook and add the following code to decompose your monthly_revenue series:
# Cell 5: Decompose the time series
from statsmodels.tsa.seasonal import seasonal_decompose
decomposition = seasonal_decompose(monthly_revenue, model='additive', period=12)
fig = decomposition.plot()
fig.set_size_inches(12, 8)
plt.tight_layout()
plt.show()
What does this code do?
- seasonal_decompose splits your revenue series into three additive components: trend + seasonal + residual.
- model='additive' means the components add together to produce the observed values. This is the right choice when the seasonal swings stay roughly the same size over time.
- period=12 tells the function to look for patterns that repeat every 12 months (annual seasonality).
- decomposition.plot() generates a 4-panel figure showing the observed data, trend, seasonal pattern, and residual noise.
- Run the cell by pressing Shift+Enter.
You should see a 4-panel plot. The top panel shows your observed monthly revenue. Below it, the trend panel reveals the overall direction. The seasonal panel shows the repeating annual cycle. The residual panel captures everything that does not fit the first two.
Why does the trend panel have gaps at the edges?
The decomposition uses a moving average to extract the trend. At the very start and end of your data, there are not enough surrounding months to compute the average. Those missing values show as gaps (NaN) in the trend and residual panels.
Seeing a ValueError about period?
- Make sure monthly_revenue exists in your notebook. It was created in Cell 4 from the previous step. If you restarted the kernel, re-run all cells above this one first.
- Check that period=12 is spelled correctly and passed as a keyword argument to seasonal_decompose.
Help me debug my seasonal decomposition
Test for stationarity with the Augmented Dickey-Fuller test
The decomposition shows you the components visually. Now you need a statistical answer to a specific question: is this data stationary?
What is stationarity?
A stationary time series has a constant mean and variance over time. It does not trend upward or downward, and its statistical properties stay stable.
Most forecasting models, including ARIMA, require stationary data to produce reliable predictions. If your data is not stationary, you need to transform it first. The Augmented Dickey-Fuller (ADF) test checks for a unit root, which is a statistical indicator of non-stationarity.
Before you run the test, take a moment to predict the result. Look at your trend panel from the decomposition plot. Do you think the monthly revenue data is stationary, or does it have a trend that would make it non-stationary?
- Create a new cell and add the following code to run the ADF test on your monthly_revenue series:
# Cell 6: Augmented Dickey-Fuller test for stationarity
from statsmodels.tsa.stattools import adfuller
result = adfuller(monthly_revenue, autolag='AIC')
print('Augmented Dickey-Fuller Test Results:')
print(f'Test Statistic: {result[0]:.4f}')
print(f'p-value: {result[1]:.4f}')
print(f'Lags Used: {result[2]}')
print(f'Number of Observations: {result[3]}')
print('Critical Values:')
for key, value in result[4].items():
print(f' {key}: {value:.4f}')
if result[1] > 0.05:
print('\np-value > 0.05: The series is likely non-stationary. Differencing (d=1) is needed for ARIMA.')
else:
print('\np-value <= 0.05: The series is likely stationary.')
What does this code do?
- adfuller runs the Augmented Dickey-Fuller test. The null hypothesis is that the series has a unit root (is non-stationary).
- autolag='AIC' automatically selects the best number of lags to include using the Akaike Information Criterion.
- The p-value is the key output. If it is above 0.05, you cannot reject the null hypothesis, meaning the data is likely non-stationary.
- The if/else block at the end prints a plain-English interpretation so you do not have to decode the numbers yourself.
- Run the cell by pressing Shift+Enter.
You should see the test statistic, p-value, lags used, number of observations, and critical values printed below the cell. The p-value is likely above 0.05.
That result means the series is not stationary. If you fitted an ARIMA model directly on this data without accounting for the non-stationarity, the forecasts would be unreliable. This is exactly the problem that the next step solves.
How does ARIMA handle non-stationarity?
The "I" in ARIMA stands for "Integrated," which means differencing. When you set d=1 in the ARIMA order, the model automatically subtracts each month's revenue from the previous month's before fitting. This removes the trend and makes the data stationary behind the scenes.
Seeing an error when running the ADF test?
- Confirm that monthly_revenue exists by running print(monthly_revenue) in a separate cell. If it is not defined, re-run Cells 3 and 4 from the previous step.
- Check the import line matches exactly: from statsmodels.tsa.stattools import adfuller.
Help me debug my ADF stationarity test
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook before moving on.
ⓧ I'd like to double check the full code
You added two new cells in this step. Scroll up and confirm each one is present, in order:
- Cell 5: Imports seasonal_decompose, creates the decomposition with model='additive' and period=12, and plots the 4-panel figure.
- Cell 6: Imports adfuller, runs the ADF test with autolag='AIC', prints the test statistic, p-value, lags, observations, and critical values, and interprets the result.
You now know exactly what your revenue data looks like under the hood. The decomposition revealed the seasonal rhythm, and the ADF test confirmed the series is not yet ready for modeling. Next up, you will build an ARIMA model that handles this non-stationarity and forecasts the next 3 months of revenue.
Build and Evaluate the ARIMA Forecast
In the previous step, the Augmented Dickey-Fuller test confirmed what the decomposition plot hinted at: your monthly revenue series is not stationary. The trend keeps shifting, which means a model fitted directly to the raw data would produce unreliable forecasts.
ARIMA is built for exactly this problem. The "I" stands for "Integrated", which means the model applies differencing automatically to remove the trend before fitting. In this step, you will split your data for honest evaluation, train an ARIMA model, and generate a 3-month revenue forecast with confidence intervals.
In this step, get ready to:
- Split your data into training and test sets for honest evaluation.
- Fit an ARIMA(1,1,1) model that handles non-stationarity with differencing.
- Generate a 3-month forecast and measure accuracy with Mean Absolute Error.
Split the data into training and test sets
To evaluate a forecast honestly, you need data the model has never seen. You will hold out the last 3 months of your monthly_revenue series as a test set and train on everything before it.
Why hold out the last 3 months?
This simulates a real forecasting scenario. You pretend those final months have not happened yet, build a model on the earlier data, and then compare your predictions against what actually occurred. If you trained on all the data, you would have no way to measure how accurate your forecast really is.
- Create a new cell in your notebook and paste the following code:
# Cell 7: Train-test split
train = monthly_revenue[:-3]
test = monthly_revenue[-3:]
print(f"Training set: {train.index[0].strftime('%Y-%m')} to {train.index[-1].strftime('%Y-%m')} ({len(train)} months)")
print(f"Test set: {test.index[0].strftime('%Y-%m')} to {test.index[-1].strftime('%Y-%m')} ({len(test)} months)")
What does this code do?
- monthly_revenue[:-3] takes all months except the last 3 and stores them in train. This is what the ARIMA model will learn from.
- monthly_revenue[-3:] captures the final 3 months as test. You will compare these actual values against the model's predictions.
- The print statements confirm the date ranges so you can verify the split looks right.
- Run the cell by pressing Shift+Enter.
- You should see two lines printed: the training set spanning roughly 10 months and the test set covering the final 3 months.
Seeing a NameError for monthly_revenue?
- Make sure you ran Cell 4 (where monthly_revenue was created) earlier in this session. If you restarted your kernel, re-run all cells from Cell 2 through Cell 6 first.
- help me debug a NameError for monthly_revenue in my train-test split cell
Fit an ARIMA model on the training data
ARIMA models have three parameters written as (p, d, q). You will use order=(1, 1, 1), where the d=1 is the key choice. It tells ARIMA to apply one round of differencing, which is exactly what the ADF test in Step 3 said the data needs.
- Create a new cell and paste this code to import the ARIMA class and fit the model:
# Cell 8: Fit ARIMA model
from statsmodels.tsa.arima.model import ARIMA
model = ARIMA(train, order=(1, 1, 1))
results = model.fit()
print(results.summary())
What do the ARIMA parameters mean?
- p=1 (AutoRegressive): the model uses the previous month's value to predict the next one.
- d=1 (Integrated): the model differences the data once to remove the non-stationarity you found in Step 3.
- q=1 (Moving Average): the model also learns from the previous month's forecast error.
These are conservative starting values that work well on short series. The model summary includes an AIC score you can use to compare different parameter combinations if you experiment later.
- Run the cell by pressing Shift+Enter.
- You should see a detailed model summary table printed below the cell, including the fitted coefficients, standard errors, and an AIC score.
Seeing a convergence warning?
- A convergence warning can appear because the series is short (roughly 10 training months). The model still produces usable forecasts. If you see this warning, you can continue safely.
- Check that you are passing train (not monthly_revenue) to the ARIMA constructor. Fitting on the full series instead of the training set will change your results.
- help me understand a convergence warning in my ARIMA model
Generate the forecast and evaluate accuracy
Your model is trained. Now you will ask it to predict the next 3 months of revenue and see how those predictions compare to the actual values you held out.
- Create a new cell and paste this code to generate the forecast and plot it alongside the real data:
# Cell 9: Generate forecast and plot
forecast_result = results.get_forecast(steps=3)
forecast_mean = forecast_result.predicted_mean
forecast_ci = forecast_result.conf_int(alpha=0.05)
plt.figure(figsize=(12, 6))
plt.plot(train, label='Training Data', linewidth=2)
plt.plot(test, label='Actual (Test)', linewidth=2, color='green', marker='o')
plt.plot(forecast_mean, label='Forecast', linewidth=2, color='red', linestyle='--', marker='s')
plt.fill_between(forecast_ci.index,
forecast_ci.iloc[:, 0],
forecast_ci.iloc[:, 1],
color='red', alpha=0.15, label='95% Confidence Interval')
plt.title('Monthly Revenue Forecast - ARIMA(1,1,1)')
plt.xlabel('Month')
plt.ylabel('Revenue (GBP)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
What does this code do?
- get_forecast(steps=3) asks the fitted model to predict the next 3 time periods beyond the training data.
- predicted_mean extracts the point forecast (the model's best guess for each month).
- conf_int(alpha=0.05) extracts the 95% confidence interval. There is a 95% chance the true revenue falls within this band.
- fill_between shades the area between the upper and lower confidence bounds, making the uncertainty visible at a glance.
Before you run this cell, think about what you expect. Will the forecast line land close to the actual test values, or will it miss? Will the confidence band be narrow or wide on such a short training series?
- Run the cell by pressing Shift+Enter.
You should see a chart with the training data as a solid blue line, the actual test values as green circles, the forecast as a red dashed line with square markers, and a shaded pink band showing the 95% confidence interval.
Plot not showing or looks wrong?
- If the plot is blank, make sure you ran Cell 8 first so that results exists. The forecast depends on the fitted model.
- If you see a KeyError or index mismatch, confirm that train and test were created from Cell 7. Re-run Cell 7 if needed.
- help me fix my ARIMA forecast plot
The plot tells a visual story, but you also need a number to measure how far off the forecast was. Mean Absolute Error (MAE) calculates the average gap between your predicted values and the actual test values, in GBP.
- Create a new cell and paste this code to calculate the MAE and print a month-by-month comparison:
# Cell 10: Calculate Mean Absolute Error
mae = np.mean(np.abs(test.values - forecast_mean.values))
print(f"Mean Absolute Error: {mae:,.2f} GBP")
print(f"\nForecast vs Actual:")
for date, actual, predicted in zip(test.index, test.values, forecast_mean.values):
print(f" {date.strftime('%Y-%m')}: Actual = {actual:,.2f} | Predicted = {predicted:,.2f}")
What does this code do?
- np.abs(test.values - forecast_mean.values) calculates the absolute difference between each actual and predicted value. Taking the absolute value means both over-predictions and under-predictions count equally.
- np.mean(...) averages those differences into a single number. A lower MAE means a more accurate forecast.
- The loop at the end prints each test month side-by-side with its predicted value so you can spot which months the model handled well and which it missed.
- Run the cell by pressing Shift+Enter.
- You should see the MAE printed in GBP, followed by a line-by-line comparison of the 3 test months showing Actual vs Predicted values.
Seeing a ValueError about array lengths?
- This means test and forecast_mean have different numbers of elements. Confirm that steps=3 in Cell 9 matches the [-3:] slice in Cell 7.
- help me fix a ValueError when calculating MAE for my ARIMA forecast
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook by pressing Cmd+S (macOS) or Ctrl+S (Windows).
ⓧ I'd like to double check the full code
Your notebook should now contain 10 cells. Scroll through from top to bottom and confirm each one is present in this order:
- Cell 1: Install ucimlrepo (commented out).
- Cell 2: Import libraries and load data with fetch_ucirepo(id=352).
- Cell 3: Clean the data (remove cancellations, filter quantities, drop missing CustomerID, create Revenue).
- Cell 4: Build monthly_revenue series and plot.
- Cell 5: Seasonal decomposition with seasonal_decompose.
- Cell 6: Augmented Dickey-Fuller test with adfuller.
- Cell 7: Train-test split (train and test).
- Cell 8: Fit ARIMA model with order=(1, 1, 1).
- Cell 9: Generate forecast and plot with confidence intervals.
- Cell 10: Calculate Mean Absolute Error and print forecast vs actual comparison.
Secret mission
Add a Top-Product Revenue Breakdown
Your ARIMA forecast predicts how much revenue to expect. But which products are actually driving that revenue? Add a top-product breakdown to answer the natural follow-up question every stakeholder will ask.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using free, open-source tools and a free dataset. There are no ongoing costs or cloud resources to worry about.
Decide whether to keep your resources as they are, pause them to free up memory, or delete them entirely.
Resources you used:
- Your revenue_forecast.ipynb Jupyter Notebook file.
- The ucimlrepo Python package (installed via pip).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your forecast or extending the notebook.
- Your notebook and all its outputs are saved in place.
- The ucimlrepo package stays installed in your Anaconda environment for future use.
Pause - I'll come back to this later
Shut down the Jupyter Notebook server to free up memory. Your notebook file stays on disk so you can reopen it anytime.
- In the Jupyter Notebook browser tab, click File, then Close and Halt to stop the notebook kernel.
- In Anaconda Navigator, close the Jupyter Notebook application to stop the server.
- When you are ready to resume, reopen Jupyter Notebook from Anaconda Navigator and navigate to your revenue_forecast.ipynb file.
Delete - I don't want to use this again
Remove all project files and the installed package to start completely fresh.
- In the Jupyter Notebook file browser, select the checkbox next to revenue_forecast.ipynb and click the trash can icon to delete it. Confirm when prompted.
- If you also want to remove any .ipynb_checkpoints folder that Jupyter created, delete that folder from the same file browser.
- Uninstall the ucimlrepo package by running this command in a terminal or a new Jupyter cell:
pip uninstall ucimlrepo -y
- Close and halt the notebook kernel via File > Close and Halt, then shut down the Jupyter server from Anaconda Navigator.
Nice Work!
Nice Work!
You just built a complete revenue forecasting notebook from scratch using real business data. Starting from 541,909 messy retail transactions, you cleaned, aggregated, analyzed, and predicted future revenue with a professional-grade ARIMA model.
You've learned how to:
- Downloaded and cleaned 541,909 real retail transactions from the UCI Machine Learning Repository, removing cancellations, negative quantities, and missing values to engineer a monthly revenue time series.
- Decomposed the time series into trend, seasonal, and residual components, then tested for stationarity using the Augmented Dickey-Fuller test to confirm the data needed differencing before modeling.
- Built an ARIMA(1,1,1) forecasting model, generated a 3-month revenue forecast with confidence intervals, and evaluated accuracy with Mean Absolute Error.
- Secret Mission: Created a top-product revenue breakdown showing which products drive the most revenue.
Ready to quiz yourself?