Mathew K Analytics

Lesson 38 · Supply Chain Operations Analytics

End-to-End Supply Chain Analytics Project

In this lesson, we solve a real-world supply chain analytics problem using real data. Supply chain analytics helps businesses optimize inventory, demand,…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

End-to-End Supply Chain Analytics Project#

  • In this lesson, we solve a real-world supply chain analytics problem using real data.
  • Supply chain analytics helps businesses optimize inventory, demand, supplier performance, and order fulfillment.
  • Understanding these analytics is essential for reducing costs and improving service levels.
  • You will learn to load, explore, join, and analyze supply chain datasets from procurement to delivery.
  • You will gain skills to spot mistakes, build metrics, and communicate operational insights.
import pandas as pd
import openml
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

Understanding supply chain and operations data#

  • Supply chain data often includes orders, inventory, demand, supplier performance, and deliveries.
  • Data may be structured as transactions, logs, or status snapshots over time.
  • Common mistakes include misinterpreting dates, confusing product IDs, or ignoring missing data.
  • Consistent formatting and date parsing are especially important.
# Beginner Example 1: Load retail demand dataset
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
retail_df = pd.read_excel(url, sheet_name='Year 2010-2011')
retail_df['InvoiceDate'] = pd.to_datetime(retail_df['InvoiceDate'])
print(retail_df.shape)
print(retail_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: Load supplier performance dataset
supplier_dataset = openml.datasets.get_dataset(42125)
supplier_df, _, _, _ = supplier_dataset.get_data(dataset_format='dataframe')
print(supplier_df.shape)
print(supplier_df.head(3))
(9228, 13)
          full_name gender  current_annual_salary  2016_gross_pay_received  \
0    Aarhus, Pam J.      F               69222.18                 71225.98   
1   Aaron, David J.      M               97392.47                103088.48   
2  Aaron, Marsha M.      F              104717.28                107000.24   

   2016_overtime_pay department                          department_name  \
0             416.10        POL                     Department of Police   
1            3326.19        POL                     Department of Police   
2            1353.32        HHS  Department of Health and Human Services   

                                            division assignment_category  \
0  MSB Information Mgmt and Tech Division Records...    Fulltime-Regular   
1         ISB Major Crimes Division Fugitive Section    Fulltime-Regular   
2      Adult Protective and Case Management Services    Fulltime-Regular   

       employee_position_title underfilled_job_title date_first_hired  \
0  Office Services Coordinator                  None       09/22/1986   
1        Master Police Officer                  None       09/12/1988   
2             Social Worker IV                  None       11/19/1989   

   year_first_hired  
0              1986  
1              1988  
2              1989  
# Beginner Example 3: Load order fulfillment data
order_url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
orders_df = pd.read_csv(order_url)
orders_df['order_purchase_timestamp'] = pd.to_datetime(orders_df['order_purchase_timestamp'])
orders_df['order_delivered_customer_date'] = pd.to_datetime(orders_df['order_delivered_customer_date'])
print(orders_df.shape)
print(orders_df.head(3))
(99441, 8)
                           order_id                       customer_id  \
0  e481f51cbdc54678b7cc49136f2d6af7  9ef432eb6251297304e76186b10a928d   
1  53cdb2fc8bc7dce0b6741e2150273451  b0830fb4747a6c6d20dea0b8c802d7ef   
2  47770eb9100c2d0c44946d9cf07ec65d  41ce2a54c0b03bf3443c3d931a367089   

  order_status order_purchase_timestamp    order_approved_at  \
0    delivered      2017-10-02 10:56:33  2017-10-02 11:07:15   
1    delivered      2018-07-24 20:41:37  2018-07-26 03:24:27   
2    delivered      2018-08-08 08:38:49  2018-08-08 08:55:23   

  order_delivered_carrier_date order_delivered_customer_date  \
0          2017-10-04 19:55:00           2017-10-10 21:25:13   
1          2018-07-26 14:31:00           2018-08-07 15:27:45   
2          2018-08-08 13:50:00           2018-08-17 18:06:29   

  order_estimated_delivery_date  
0           2017-10-18 00:00:00  
1           2018-08-13 00:00:00  
2           2018-09-04 00:00:00  
# Intermediate Example 1: Detect missing deliveries
missing_deliveries = orders_df['order_delivered_customer_date'].isnull().sum()
print(f'Missing delivered customer dates: {missing_deliveries}')
Missing delivered customer dates: 2965
# Intermediate Example 2: Summarize supplier on-time delivery
if 'on_time_delivery' in supplier_df.columns:
    performance = supplier_df['on_time_delivery'].value_counts(normalize=True)
    print(performance)
else:
    print('on_time_delivery column not present in supplier data.')
on_time_delivery column not present in supplier data.
# Intermediate Example 3: Aggregate retail sales by day
retail_daily = retail_df.groupby(retail_df['InvoiceDate'].dt.date)['Quantity'].sum()
print(retail_daily.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 4: Plot order lead time distribution
orders_df['order_lead_time'] = (orders_df['order_delivered_customer_date'] - orders_df['order_purchase_timestamp']).dt.days
plt.figure(figsize=(8,4))
sns.histplot(orders_df['order_lead_time'].dropna(), bins=30, kde=True)
plt.title('Distribution of Order Lead Times (Days)')
plt.xlabel('Lead Time (days)')
plt.ylabel('Order Count')
plt.show()
No description has been provided for this image
# Intermediate Example 5: Simulate join between orders and suppliers
if 'SupplierID' in orders_df.columns and 'SupplierID' in supplier_df.columns:
    merged_df = orders_df.merge(supplier_df, on='SupplierID', how='left')
    print(merged_df[['order_id', 'SupplierID']].head())
else:
    print('SupplierID not present in both dataframes - join cannot be performed.')
SupplierID not present in both dataframes - join cannot be performed.
# Advanced Example 1: Calculate moving average demand (7-day window)
retail_daily = retail_daily.sort_index()
ma_7d = retail_daily.rolling(window=7, min_periods=1).mean()
plt.figure(figsize=(10,4))
plt.plot(retail_daily, label='Daily Demand')
plt.plot(ma_7d, label='7-Day Moving Average', linewidth=2)
plt.legend()
plt.xlabel('Date')
plt.ylabel('Units Sold')
plt.title('Retail Demand and 7-Day Moving Average')
plt.show()
No description has been provided for this image
# Advanced Example 2: Supplier performance by region
if 'Region' in supplier_df.columns and 'on_time_delivery' in supplier_df.columns:
    regional = supplier_df.groupby('Region')['on_time_delivery'].mean()
    print(regional)
else:
    print('Required columns not available for region-based supplier performance.')
Required columns not available for region-based supplier performance.
# Advanced Example 3: Identify possible outliers in lead time
lead_time_mean = orders_df['order_lead_time'].mean()
lead_time_std = orders_df['order_lead_time'].std()
outlier_mask = (orders_df['order_lead_time'] > lead_time_mean + 3 * lead_time_std)
outliers = orders_df[outlier_mask]
print(f'Number of lead time outliers: {outliers.shape[0]}')
print(outliers[['order_id', 'order_lead_time']].head())
Number of lead time outliers: 1614
                             order_id  order_lead_time
110  9d531c565e28c3e0d756192f84d8731f             56.0
115  8fc207e94fa91a7649c5a5dab690272a             54.0
252  f31535f21d145b2345e2bf7f09d62322             81.0
445  690199d6a2c51ff57c6b392d7680cbfd             59.0
453  f30607daae66843552e00843c6de88ef             49.0
# Error Handling: Check for missing InvoiceDate in retail data
missing_dates = retail_df['InvoiceDate'].isnull().sum()
print(f'Missing InvoiceDate entries: {missing_dates}')
Missing InvoiceDate entries: 0
# Error Handling: Test for duplicate Order IDs before join
duplicate_orders = orders_df['order_id'].duplicated().sum()
print(f'Duplicate order IDs in orders_df: {duplicate_orders}')
Duplicate order IDs in orders_df: 0
# Error Handling: Negative quantities in retail data
neg_qty = retail_df[retail_df['Quantity'] < 0]
print(f'Negative quantity records: {neg_qty.shape[0]}')
Negative quantity records: 10624
# KPI: Calculate average order lead time
avg_lead_time = orders_df['order_lead_time'].mean()
print(f'Average order lead time (days): {avg_lead_time:.2f}')
Average order lead time (days): 12.09
# KPI: Rolling weekly order count
orders_daily_count = orders_df.groupby(orders_df['order_purchase_timestamp'].dt.date)['order_id'].count()
orders_rolling = orders_daily_count.rolling(window=7, min_periods=1).sum()
plt.figure(figsize=(10, 4))
plt.plot(orders_daily_count, label='Daily Orders')
plt.plot(orders_rolling, label='7-Day Rolling Orders', linewidth=2)
plt.title('Order Volume (Daily and 7-Day Rolling)')
plt.xlabel('Date')
plt.ylabel('Order Count')
plt.legend()
plt.show()
No description has been provided for this image
# Best Practice: Supplier groups by delivery performance
if 'on_time_delivery' in supplier_df.columns:
    counts = supplier_df['on_time_delivery'].value_counts()
    print(counts)
else:
    print('on_time_delivery field not present in supplier_df.')
on_time_delivery field not present in supplier_df.
# Time series pattern: Weekday vs weekend sales
retail_df['weekday'] = retail_df['InvoiceDate'].dt.dayofweek
weekday_sales = retail_df.groupby('weekday')['Quantity'].sum()
print(weekday_sales)
weekday
0     815354
1     961543
2     969558
3    1167823
4     794441
6     467732
Name: Quantity, dtype: int64
# End-to-end: Aggregate daily retail demand, join with supplier, calculate KPI (if possible)
proj_results = None
if 'SupplierID' in retail_df.columns and 'SupplierID' in supplier_df.columns:
    merged_proj = retail_df.merge(supplier_df, on='SupplierID', how='left')
    merged_proj['InvoiceDate'] = pd.to_datetime(merged_proj['InvoiceDate'])
    daily_kpi = merged_proj.groupby(merged_proj['InvoiceDate'].dt.date).agg({'Quantity': 'sum', 'on_time_delivery': 'mean'})
    proj_results = daily_kpi
    print(daily_kpi.head(7))
else:
    print('No SupplierID key for join in retail or supplier dataset - cannot calculate combined KPI.')
No SupplierID key for join in retail or supplier dataset - cannot calculate combined KPI.
 

Found this useful?

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