Mathew K Analytics

Lesson 7 · Supply Chain Operations Analytics

Order-Level and Demand Analysis in Supply Chain Operations Analytics | Full Training

In this lesson, we will solve real-world order-level and demand analysis problems using Python. These types of problems help businesses manage inventory,…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Order-Level and Demand Analysis in Supply Chain Operations#

  • In this lesson, we will solve real-world order-level and demand analysis problems using Python.
  • These types of problems help businesses manage inventory, fulfill customer needs, and improve supply chain efficiency.
  • Understanding demand patterns and order behaviors prevents stockouts and helps plan future operations.
  • By the end, you will be able to analyze real datasets, spot demand trends, and extract essential supply chain metrics.
import pandas as pd
import numpy as np
import openml
import warnings
warnings.filterwarnings('ignore')

Core Data Concepts in Order and Demand Analysis#

  • Supply chain datasets often record every order or line item, including date, product, quantity, and customer.
  • Retail demand data tracks what was sold, when, and to whom.
  • Operations datasets may include lead times, supplier info, or delivery performance.
  • Beginners sometimes forget to check date consistency or misinterpret demand quantities.
  • Aggregating at the wrong level (like mixing daily and monthly orders) is a common mistake.
  • Clean, consistent timestamps and correct units are critical for good analysis.

Beginner Example 1: Loading Retail Demand Data#

  • Let us explore a real-world online retail transactions dataset.
  • We will look at the first few order records to see how demand data is organized.
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))
(541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  

Beginner Example 2: Loading Manufacturing Operations Data#

  • Not all supply chain analysis is retail.
  • Here is an operations dataset from OpenML showing process-level order records.
dataset = openml.datasets.get_dataset(43900)
X, y, _, _ = dataset.get_data(dataset_format='dataframe')
df_ops = X.copy() if y is None else pd.concat([X, y], axis=1)
print(df_ops.shape)
print(df_ops.head(3))
(32769, 10)
  ACTION RESOURCE MGR_ID ROLE_ROLLUP_1 ROLE_ROLLUP_2 ROLE_DEPTNAME ROLE_TITLE  \
0      1    39353  85475        117961        118300        123472     117905   
1      1    17183   1540        117961        118343        123125     118536   
2      1    36724  14457        118219        118220        117884     117879   

  ROLE_FAMILY_DESC ROLE_FAMILY ROLE_CODE  
0           117906      290919    117908  
1           118536      308574    118539  
2           267952       19721    117880  

Beginner Example 3: Counting Unique Orders#

  • A basic analysis step is counting distinct orders over a period.
  • Let us do this with the retail demand dataset.
unique_orders = df['Invoice'].nunique()
print('Number of unique orders:', unique_orders)
Number of unique orders: 25900

Intermediate Example 1: Daily Demand Time Series#

  • Real supply chains monitor demand per day.
  • We can group sales transactions by date to generate a daily demand curve.
daily_demand = df.groupby(df['InvoiceDate'].dt.date)['Quantity'].sum()
print(daily_demand.head())
InvoiceDate
2010-12-01    26814
2010-12-02    21023
2010-12-03    14830
2010-12-05    16395
2010-12-06    21419
Name: Quantity, dtype: int64

Intermediate Example 2: Most Demanded Products#

  • Knowing top sellers helps manage inventory and promotions.
  • Let us find the most demanded product in our retail dataset.
top_product = df.groupby('Description')['Quantity'].sum().sort_values(ascending=False).head(1)
print('Most demanded product:')
print(top_product)
Most demanded product:
Description
WORLD WAR 2 GLIDERS ASSTD DESIGNS    53847
Name: Quantity, dtype: int64

Intermediate Example 3: Analyzing Order Frequency by Country#

  • Global distributors want to see which markets order most frequently.
  • We will count orders by country in our retail dataset.
orders_country = df.groupby('Country')['Invoice'].nunique().sort_values(ascending=False)
print(orders_country.head())
Country
United Kingdom    23494
Germany             603
France              461
EIRE                360
Belgium             119
Name: Invoice, dtype: int64

Advanced Example 1: Order Lead Time Analysis (Delivery Dataset)#

  • Meeting promised delivery dates is critical for customer satisfaction.
  • We will analyze order lead times using real e-commerce order and delivery data.
olist_url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
olist_df = pd.read_csv(olist_url)
olist_df['order_purchase_timestamp'] = pd.to_datetime(olist_df['order_purchase_timestamp'])
olist_df['order_delivered_customer_date'] = pd.to_datetime(olist_df['order_delivered_customer_date'])
olist_df['lead_time_days'] = (olist_df['order_delivered_customer_date'] - olist_df['order_purchase_timestamp']).dt.days
mean_lead_time = olist_df['lead_time_days'].mean()
print('Average lead time (days):', mean_lead_time)
Average lead time (days): 12.094085575687217

Advanced Example 2: Detecting Demand Outliers#

  • Sudden demand spikes could signal errors or special events.
  • Let us flag unusually high daily demand days in the retail data.
Q1 = daily_demand.quantile(0.25)
Q3 = daily_demand.quantile(0.75)
IQR = Q3 - Q1
outlier_days = daily_demand[daily_demand > Q3 + 1.5 * IQR]
print('Unusually high demand days:')
print(outlier_days)
Unusually high demand days:
InvoiceDate
2011-08-04    37992
2011-08-11    40191
2011-09-20    43702
2011-10-05    46161
2011-10-20    40802
2011-11-10    38077
2011-11-14    45959
2011-11-23    37350
2011-12-05    44119
2011-12-07    39612
Name: Quantity, dtype: int64

Advanced Example 3: Analyzing Demand Variability by Product#

  • Highly variable demand skews safety stock calculations.
  • Let us compute the standard deviation of demand by product.
prod_daily = df.groupby(['Description', df['InvoiceDate'].dt.date])['Quantity'].sum().reset_index()
var_by_prod = prod_daily.groupby('Description')['Quantity'].std().sort_values(ascending=False)
print('Top 5 most variable products by daily demand:')
print(var_by_prod.head())
Top 5 most variable products by daily demand:
Description
ASSTD DESIGN 3D PAPER STICKERS    2499.345681
Damaged                           2006.300006
wrongly coded 20713               1272.792206
thrown away                       1155.222489
?                                  883.796867
Name: Quantity, dtype: float64

Error Handling Example 1: Finding Missing Dates#

  • Missing sales dates can impact trends and forecasts.
  • Let us check for missing days in the daily demand time series.
all_days = pd.date_range(start=daily_demand.index.min(), end=daily_demand.index.max())
missing_days = all_days.difference(pd.to_datetime(daily_demand.index))
print('Number of missing sales days:', len(missing_days))
print('Missing dates:', missing_days[:5].strftime('%Y-%m-%d').tolist())
Number of missing sales days: 69
Missing dates: ['2010-12-04', '2010-12-11', '2010-12-18', '2010-12-24', '2010-12-25']

Error Handling Example 2: Preventing Incorrect Data Joins#

  • Joining datasets on the wrong field can break analysis.
  • Here is how to spot a mismatch in key fields.
# Example: matching orders from two sources by invoice - simulate mismatch
order_keys = set(df['Invoice'].astype(str).unique())
olist_keys = set(olist_df['order_id'].astype(str).unique())
join_overlap = order_keys & olist_keys
print('Number of matching order keys between datasets:', len(join_overlap))
Number of matching order keys between datasets: 0

Error Handling Example 3: Interpreting Negative or Null Values#

  • Sometimes data has negative quantities or lead times.
  • Let us scan for apparent errors in our order-level demand and delivery data.
neg_qty = df[df['Quantity'] < 0]
neg_lead = olist_df[olist_df['lead_time_days'] < 0]
print('Negative quantity rows:', neg_qty.shape[0])
print('Negative lead time rows:', neg_lead.shape[0])
Negative quantity rows: 10624
Negative lead time rows: 0

Best Practices: Aggregate Demand by Time Period#

  • Consistently aggregating by day, week, or month reduces errors.
  • Here is how to safely sum demand by month in the retail dataset.
df['month'] = df['InvoiceDate'].dt.to_period('M')
monthly_demand = df.groupby('month')['Quantity'].sum()
print(monthly_demand.head())
month
2010-12    342228
2011-01    308966
2011-02    277989
2011-03    351872
2011-04    289098
Freq: M, Name: Quantity, dtype: int64

Best Practices: Calculating Key Performance Indicators (KPIs)#

  • KPIs such as fill rate, lead time, and order cycle time drive better decisions.
  • Let us compute the percent of orders delivered within seven days.
on_time_pct = (olist_df['lead_time_days'] <= 7).mean() * 100
print(f'Percent of orders delivered within 7 days: {on_time_pct:.2f}%')
Percent of orders delivered within 7 days: 33.89%

Mini End-to-End Problem: Demand Spike Alert#

  • Let us solve a real scenario.
  • Detect any week where retail demand is 50% higher than the prior week and alert operations managers.
df['week'] = df['InvoiceDate'].dt.to_period('W')
weekly_demand = df.groupby('week')['Quantity'].sum()
weekly_growth = weekly_demand.pct_change()
spike_weeks = weekly_growth[weekly_growth > 0.5].index.astype(str).tolist()
print('Weeks with demand spike alert:', spike_weeks)
Weeks with demand spike alert: ['2011-01-03/2011-01-09', '2011-02-14/2011-02-20', '2011-03-14/2011-03-20', '2011-06-06/2011-06-12', '2011-09-05/2011-09-11']
 

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.