Lesson 24 · Supply Chain Operations Analytics
Forecast Accuracy Evaluation and Monitoring in Supply Chain Operations
In this lesson, we will tackle how to measure and monitor the accuracy of demand forecasts using real-world operational datasets. Accurate forecasting is…
- CourseSupply Chain Operations Analytics
- Lesson24 of 27
- Video26 min
- FormatJupyter notebook · 19 code cells
What you'll learn
- What Data Will We Use and Why?
- Understanding Forecasts and Actuals
- Beginner Example 1: Calculating Forecast Errors
- Beginner Example 2: Visualizing Forecast Errors Over Time
- Beginner Example 3: Mean Absolute Error (MAE)
- Intermediate Example 1: Calculating MAPE (Mean Absolute Percentage Error)
- Intermediate Example 2: Bias and Forecast Monitoring
- Intermediate Example 3: Tracking Accuracy Over Time for Continuous Monitoring
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbForecast Accuracy Evaluation and Monitoring in Supply Chain Operations#
- In this lesson, we will tackle how to measure and monitor the accuracy of demand forecasts using real-world operational datasets.
- Accurate forecasting is crucial for inventory planning, minimizing costs, and ensuring product availability in supply chain management.
- You will learn to use Python to calculate forecast accuracy metrics, visualize errors, detect bias, and set up ongoing monitoring.
- By the end, you will be able to identify common forecasting pitfalls and calculate meaningful metrics for real business data.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
What Data Will We Use and Why?#
- We will use real retail demand data from the UCI Online Retail II dataset.
- This dataset includes product transactions with timestamps and quantities sold.
- Each record represents an item sold at a point in time, ideal for time series forecasting.
- Beginners often confuse sales transactions with aggregated demand over time.
- Always check date columns and missing values when working with demand data.
# Load data: UCI Online Retail II dataset
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df = pd.read_excel(url, sheet_name='Year 2010-2011')
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
print(df.shape)
print(df.head(3))
Understanding Forecasts and Actuals#
- In real operations, the team creates forecasts for demand before actual sales happen.
- Actuals are the recorded sales after the period ends.
- Comparing forecasts and actuals helps spot over- or under-estimating demand.
- Common mistake: forgetting to align forecast and actual dates correctly.
# Aggregate by week and item for forecasting example
df['week'] = df['InvoiceDate'].dt.to_period('W').apply(lambda r: r.start_time)
weekly = df.groupby(['week', 'StockCode'])['Quantity'].sum().reset_index()
weekly.rename(columns={'Quantity': 'actual_demand'}, inplace=True)
print(weekly.head())
# Simulate a naive forecast: previous week's sales-> forecast for next week
weekly['forecast'] = weekly.groupby('StockCode')['actual_demand'].shift(1)
print(weekly.head(10))
Beginner Example 1: Calculating Forecast Errors#
- The simplest way to check accuracy is to compute the forecast error: actual - forecast.
- Large errors can lead to excess stock or missed sales in operations.
# Compute forecast error
weekly['error'] = weekly['actual_demand'] - weekly['forecast']
print(weekly[['week','StockCode','actual_demand','forecast','error']].head(10))
Beginner Example 2: Visualizing Forecast Errors Over Time#
- Visualizations help spot trends like continual over- or under-forecasting.
- Sharp spikes may signal operational problems or special events.
# Plot errors for an example item
item = weekly['StockCode'].unique()[0]
item_data = weekly[weekly['StockCode']==item]
plt.figure(figsize=(10,4))
plt.plot(item_data['week'], item_data['error'], marker='o')
plt.title(f'Forecast Error for Item {item}')
plt.xlabel('Week')
plt.ylabel('Error (Actual - Forecast)')
plt.axhline(0, color='gray', linestyle='--')
plt.tight_layout()
plt.show()
Beginner Example 3: Mean Absolute Error (MAE)#
- MAE is a common metric to summarize how far off forecasts are on average.
- It is preferred in operations because it is simple to explain.
# Calculate MAE for each item
mae = weekly.dropna().groupby('StockCode').apply(lambda g: np.mean(np.abs(g['error']))).reset_index(name='MAE')
print(mae.head())
Intermediate Example 1: Calculating MAPE (Mean Absolute Percentage Error)#
- MAPE expresses error as a percentage of actual demand.
- This makes it easy to compare accuracy across items with different sales volumes.
# Calculate MAPE for each item, avoiding division by zero
def mape(y_true, y_pred):
non_zero = y_true != 0
return np.mean(np.abs((y_true[non_zero] - y_pred[non_zero]) / y_true[non_zero])) * 100
mapes = weekly.dropna().groupby('StockCode').apply(lambda g: mape(g['actual_demand'].values, g['forecast'].values)).reset_index(name='MAPE')
print(mapes.head())
Intermediate Example 2: Bias and Forecast Monitoring#
- Forecast bias happens when the model systematically over- or underestimates demand.
- The mean error shows whether bias exists.
- Businesses monitor bias to avoid running out of stock (negative bias) or overstocking (positive bias).
# Calculate mean forecast bias for each item
bias = weekly.dropna().groupby('StockCode').apply(lambda g: np.mean(g['error'])).reset_index(name='Bias')
print(bias.head())
Intermediate Example 3: Tracking Accuracy Over Time for Continuous Monitoring#
- In operations, teams track rolling forecast accuracy to react quickly to changes.
- Setting up time-based KPIs helps spot drift and emerging issues.
# Compute rolling 6-week MAE for a specific item
item_eg = weekly['StockCode'].unique()[0]
item_data = weekly[weekly['StockCode']==item_eg].dropna()
item_data = item_data.sort_values('week')
item_data['MAE_6w'] = item_data['error'].abs().rolling(window=6).mean()
plt.figure(figsize=(10,4))
plt.plot(item_data['week'], item_data['MAE_6w'], label='6-week MAE')
plt.title(f'Rolling 6-week MAE for Item {item_eg}')
plt.xlabel('Week')
plt.ylabel('MAE')
plt.legend()
plt.tight_layout()
plt.show()
Advanced Example 1: Monitoring Forecasts for Multiple SKUs#
- Real operations must monitor many products at once.
- Dashboards plot accuracy metrics for top products or categories.
- Monitoring helps prioritize analyst attention where errors are growing.
# Plot rolling MAE for 5 random SKUs
np.random.seed(42)
sample_items = np.random.choice(weekly['StockCode'].unique(), 5, replace=False)
plt.figure(figsize=(12,6))
for code in sample_items:
itemd = weekly[weekly['StockCode']==code].dropna().sort_values('week')
itemd['MAE_6w'] = itemd['error'].abs().rolling(window=6).mean()
plt.plot(itemd['week'], itemd['MAE_6w'], label=f'SKU {code}')
plt.title('Rolling 6-week MAE for 5 Sample SKUs')
plt.xlabel('Week')
plt.ylabel('6-week MAE')
plt.legend()
plt.tight_layout()
plt.show()
Advanced Example 2: Detecting Outlier Forecast Errors#
- Outlier errors may indicate special causes like stockouts or promotions.
- Operational teams flag large errors for investigation.
# Identify potential error outliers
weekly['abs_error'] = weekly['error'].abs()
threshold = weekly['abs_error'].mean() + 3*weekly['abs_error'].std()
outliers = weekly[weekly['abs_error'] > threshold]
print(f'Total outlier weeks: {outliers.shape[0]}')
print(outliers[['week','StockCode','actual_demand','forecast','error']].head())
Error Handling Example: Missing Weeks in Demand Data#
- When data is missing for certain weeks, forecasts and accuracy metrics can be misleading.
- Always check for missing weeks and fill gaps before evaluating performance.
# Ensure each item has all weeks (fill missing periods with 0 actuals)
all_weeks = pd.date_range(weekly['week'].min(), weekly['week'].max(), freq='W')
all_items = weekly['StockCode'].unique()
index = pd.MultiIndex.from_product([all_weeks, all_items], names=['week','StockCode'])
full = weekly.set_index(['week','StockCode']).reindex(index, fill_value=0).reset_index()
print(f'Filled data shape: {full.shape}')
print(full.head())
Error Handling Example: Incorrect Forecast Alignment#
- Aligning forecast and actual data incorrectly changes the meaning of errors.
- Always check that the forecast for week X matches the actual for week X (not X+1 by mistake).
# Example of misaligned forecasts: shifting in the wrong direction
full['misaligned_forecast'] = full.groupby('StockCode')['actual_demand'].shift(-1)
full['misaligned_error'] = full['actual_demand'] - full['misaligned_forecast']
print(full[['week','StockCode','actual_demand','misaligned_forecast','misaligned_error']].head(10))
Error Handling Example: Zero Actuals in Percentage Error Calculations#
- MAPE and similar metrics break when actual demand is zero.
- Always check for zero actuals before dividing.
# Show how dividing by zero gives problems in MAPE
zeros = full[full['actual_demand']==0]
print(f'Sample weeks with zero demand: {len(zeros)}')
if not zeros.empty:
print(zeros[['week','StockCode','actual_demand','forecast']].head(3))
Best Practices: Aggregation and KPI Generation#
- Aggregate demand before calculating forecasting metricsitem and time period must match.
- Visualize errors by business segment or geography to direct improvement.
- Use rolling averages to monitor ongoing accuracy and make operational dashboards.
# Example: Aggregate MAE by country (if available)
if 'Country' in df.columns:
merged = (weekly.merge(df[['StockCode','Country']].drop_duplicates(), on='StockCode', how='left'))
country_mae = merged.dropna().groupby('Country')['error'].apply(lambda x: np.mean(np.abs(x))).reset_index(name='MAE')
print(country_mae.sort_values('MAE', ascending=False).head())
Operational Analytics Pattern: Time Series Grouping#
- Always group and aggregate demand and forecast by the same time grain (week, month, etc).
- Choosing the wrong time interval leads to misleading accuracy scores.
# Example: Group at monthly instead of weekly level
weekly['month'] = pd.to_datetime(weekly['week']).dt.to_period('M').astype(str)
monthly = weekly.groupby(['month','StockCode'])[['actual_demand','forecast']].sum().reset_index()
monthly['error'] = monthly['actual_demand'] - monthly['forecast']
print(monthly.head())
Tiny End-to-End Case: Identifying Items with Declining Forecast Accuracy#
- Goal: Spot SKUs that need urgent forecast improvements so the team can intervene.
- Steps:
- Aggregate data weekly per item.
- Calculate rolling 8-week MAE for each.
- Identify SKUs where MAE has grown by 50 percent or more over the last 10 weeks.
# End-to-end: Flagging SKUs with rising MAE
result = []
for code in weekly['StockCode'].unique()[:100]: # limit to 100 for speed
sku = weekly[weekly['StockCode']==code].dropna().sort_values('week')
if len(sku) < 18: continue
sku['MAE_8w'] = sku['error'].abs().rolling(window=8).mean()
last10 = sku['MAE_8w'].iloc[-10:]
if len(last10)==10 and last10.iloc[-1] > 1.5*last10.iloc[0]:
result.append({'StockCode': code, 'Start_MAE_8w': last10.iloc[0], 'End_MAE_8w': last10.iloc[-1]})
df_flagged = pd.DataFrame(result)
print(df_flagged.head())
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



