Mathew K Analytics

Lesson 4 · Supply Chain Operations Analytics

Functions for Supply Chain Calculations

In this lesson, we learn to create Python functions for real supply chain and operations analytics tasks. We focus on real-world business data: retail…

⬇ 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

Functions for Supply Chain Calculations#

  • In this lesson, we learn to create Python functions for real supply chain and operations analytics tasks.
  • We focus on real-world business data: retail demand, supplier performance, and order fulfillment.
  • You will see how functions simplify calculations for demand forecasting, inventory turnover, and KPIs.
  • By the end, you will know how to build repeatable, accurate analytics routines for actual operational datasets.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Understanding key supply chain datasets#

  • Supply chain analytics uses data like demand (sales), inventory, supplier metrics, and order fulfillment times.
  • Data is usually organized as tables: each row is an event like a purchase or delivery.
  • Real datasets can be large and messy: dates might be missing, quantities can be negative or zero.
  • Beginners often confuse unique identifiers, groupings, or misunderstood column names.
  • Always check what each column actually means before you analyze!
# Example 1: Load retail demand data
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  
# Example 2: Load supplier performance data
import openml
dataset = openml.datasets.get_dataset(42125)
df_supplier, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(df_supplier.shape)
print(df_supplier.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  
# Example 3: Load order fulfillment data
olist_url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
df_orders = pd.read_csv(olist_url)
df_orders['order_purchase_timestamp'] = pd.to_datetime(df_orders['order_purchase_timestamp'])
df_orders['order_delivered_customer_date'] = pd.to_datetime(df_orders['order_delivered_customer_date'])
print(df_orders.shape)
print(df_orders.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  
# Beginner Example 1: Write a function to calculate total demand for an item
def total_demand(df, stockcode):
    total = df[df['StockCode'] == stockcode]['Quantity'].sum()
    return total
 
sample_item = df['StockCode'].iloc[0]
print('Total demand for item', sample_item, ':', total_demand(df, sample_item))
Total demand for item 85123A : 38830
# Beginner Example 2: Function for average daily demand
def average_daily_demand(df, stockcode):
    sub = df[df['StockCode'] == stockcode]
    daily = sub.groupby(sub['InvoiceDate'].dt.date)['Quantity'].sum()
    return daily.mean()
 
print('Average daily demand for item', sample_item, ':', average_daily_demand(df, sample_item))
Average daily demand for item 85123A : 127.31147540983606
# Beginner Example 3: Find number of unique customers for a product
def unique_customers(df, stockcode):
    sub = df[df['StockCode'] == stockcode]
    return sub['Customer ID'].nunique()
 
print('Unique customers for item', sample_item, ':', unique_customers(df, sample_item))
Unique customers for item 85123A : 858
# Intermediate Example 1: Function to calculate inventory turnover rate
def inventory_turnover(df, stockcode, inventory_level):
    sales = total_demand(df, stockcode)
    if inventory_level == 0:
        return np.nan
    return sales / inventory_level
 
# Assume starting stock for this item is 100
print('Inventory turnover rate:', inventory_turnover(df, sample_item, 100))
Inventory turnover rate: 388.3
# Intermediate Example 2: Function for supplier on-time delivery rate
def on_time_delivery_rate(df):
    if 'order_purchase_timestamp' not in df.columns or 'order_delivered_customer_date' not in df.columns:
        return np.nan
    delivered = df.dropna(subset=['order_delivered_customer_date'])
    estimated = df.dropna(subset=['order_estimated_delivery_date'])
    if delivered.empty or estimated.empty:
        return np.nan
    on_time = (delivered['order_delivered_customer_date'] <= delivered['order_estimated_delivery_date']).mean()
    return on_time
 
rate = on_time_delivery_rate(df_orders)
print(f'On-time delivery rate: {rate:.2%}')
On-time delivery rate: 91.89%
# Intermediate Example 3: Calculate supplier fill rate using order and delivery quantity
def fill_rate(df, ordered_col, delivered_col):
    if df[ordered_col].sum() == 0:
        return np.nan
    return df[delivered_col].sum() / df[ordered_col].sum()
 
# This is a demo with retail demand as delivered and using a hypothetical ordered column
df['OrderedQty'] = df['Quantity'].abs() + np.random.randint(0, 5, len(df))
print('Fill rate:', fill_rate(df, 'OrderedQty', 'Quantity'))
Fill rate: 0.7160935189775917
# Advanced Example 1: Create a function for moving average demand (time series)
def moving_average_demand(df, stockcode, window=7):
    sub = df[df['StockCode'] == stockcode].copy()
    sub['Date'] = sub['InvoiceDate'].dt.date
    daily = sub.groupby('Date')['Quantity'].sum().sort_index()
    ma = daily.rolling(window=window, min_periods=1).mean()
    return ma
 
ma = moving_average_demand(df, sample_item, window=7)
print(ma.tail(7))
Date
2011-12-02    135.285714
2011-12-04    104.714286
2011-12-05    146.142857
2011-12-06    132.571429
2011-12-07    144.571429
2011-12-08    115.714286
2011-12-09    110.428571
Name: Quantity, dtype: float64
# Advanced Example 2: Calculate lead time for orders using order purchase and delivery date
def lead_time_days(df):
    df = df.dropna(subset=['order_purchase_timestamp', 'order_delivered_customer_date'])
    if df.empty:
        return np.nan
    return (df['order_delivered_customer_date'] - df['order_purchase_timestamp']).dt.days.mean()
 
avg_lead = lead_time_days(df_orders)
print(f'Average lead time (days): {avg_lead:.2f}')
Average lead time (days): 12.09
# Advanced Example 3: Write a multi-metric order analytics function
def order_kpis(df):
    valid = df.dropna(subset=['order_purchase_timestamp', 'order_delivered_customer_date'])
    total_orders = len(valid)
    if total_orders == 0:
        return {}
    on_time = (valid['order_delivered_customer_date'] <= valid['order_estimated_delivery_date']).sum()
    mean_lead_time = (valid['order_delivered_customer_date'] - valid['order_purchase_timestamp']).dt.days.mean()
    return {
        'total_orders': total_orders,
        'on_time_rate': on_time / total_orders,
        'average_lead_time': mean_lead_time
    }
 
kpis = order_kpis(df_orders)
print('Order KPIs:', kpis)
Order KPIs: {'total_orders': 96476, 'on_time_rate': np.float64(0.9188710145528421), 'average_lead_time': np.float64(12.094085575687217)}
# Error Example 1: Handling missing invoice dates in demand calculations
df_missing_date = df.copy()
df_missing_date.loc[0:4, 'InvoiceDate'] = pd.NaT
try:
    result = average_daily_demand(df_missing_date, sample_item)
except Exception as e:
    print('Error:', str(e))
# Error Example 2: Common pitfall - misinterpreting order join keys
wrong_merge = pd.merge(df, df_orders, left_on='Invoice', right_on='order_id', how='inner')
print('Merged shape:', wrong_merge.shape)
Merged shape: (0, 17)
# Error Example 3: Wrong interpretation of negative or zero quantities
df_neg = df.copy()
df_neg.loc[0:2, 'Quantity'] = 0
print('Total demand for item with 0 quantities:', total_demand(df_neg, sample_item))
Total demand for item with 0 quantities: 38824

Common supply chain calculation patterns#

  • Grouping by time periods (days, weeks, months) helps track seasonality and trends.
  • Aggregating quantities by product or supplier gives easy comparisons.
  • Creating functions for KPIs lets you reuse logic anywhere in an analysis.
  • Visualizing results with plots (practice outside Jupyter for more insight).
  • Always document your function inputs and assumptions.
# KPI Example: Function for group-by month item sales
def monthly_item_sales(df, stockcode):
    mask = df['StockCode'] == stockcode
    by_month = df[mask].groupby(df['InvoiceDate'].dt.to_period('M'))['Quantity'].sum()
    return by_month
 
print(monthly_item_sales(df, sample_item).head(6))
InvoiceDate
2010-12    3225
2011-01    5522
2011-02    1874
2011-03    1982
2011-04    1907
2011-05    3991
Freq: M, Name: Quantity, dtype: int64
# KPI Example: Top-N best-selling items function
def top_n_products(df, n=5):
    grouped = df.groupby('StockCode')['Quantity'].sum()
    return grouped.sort_values(ascending=False).head(n)
 
print('Top 5 best-selling items:')
print(top_n_products(df, n=5))
Top 5 best-selling items:
StockCode
22197     56450
84077     53847
85099B    47363
85123A    38830
84879     36221
Name: Quantity, dtype: int64
# End-to-end: Track monthly demand trend and identify reorder points for a key item
def reorder_signal(daily_qty, reorder_level):
    below = daily_qty[daily_qty < reorder_level]
    return below.index.astype(str).tolist()
 
item_monthly = monthly_item_sales(df, sample_item)
reorder_level = 10
reorder_dates = reorder_signal(item_monthly, reorder_level)
print('Reorder signals for item', sample_item, 'on months:', reorder_dates)
Reorder signals for item 85123A on months: []
 

Found this useful?

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