Mathew K Analytics

Lesson 31 · Supply Chain Operations Analytics

Lead Time Variability Analysis in Supply Chains

We will learn how to analyze the variation in delivery lead times using real order data. Lead time variability affects inventory costs, stockouts, and…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Lead Time Variability Analysis in Supply Chains#

  • We will learn how to analyze the variation in delivery lead times using real order data.
  • Lead time variability affects inventory costs, stockouts, and customer satisfaction.
  • Understanding this helps operations managers improve planning and reduce risks.
  • You will build hands-on skills to measure and visualize lead time variability in real supply chain data.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

Understanding Lead Time Data in Supply Chains#

  • In supply chains, lead time is the interval from order placement to order delivery.
  • Lead time variability is the fluctuation in how long deliveries actually take.
  • Real-world data typically includes timestamps for order placement, shipment, and final delivery.
  • Columns may have missing or poorly formatted dates that can create major analysis mistakes.
  • A common error is to compute negative or zero lead times due to missing data or time zone issues.
url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
df = pd.read_csv(url)
print(df.shape)
print(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  
df['order_purchase_timestamp'] = pd.to_datetime(df['order_purchase_timestamp'])
df['order_delivered_customer_date'] = pd.to_datetime(df['order_delivered_customer_date'])
print(df[['order_purchase_timestamp', 'order_delivered_customer_date']].head())
  order_purchase_timestamp order_delivered_customer_date
0      2017-10-02 10:56:33           2017-10-10 21:25:13
1      2018-07-24 20:41:37           2018-08-07 15:27:45
2      2018-08-08 08:38:49           2018-08-17 18:06:29
3      2017-11-18 19:28:06           2017-12-02 00:28:42
4      2018-02-13 21:18:39           2018-02-16 18:17:02

Key Lead Time Fields#

  • The main date columns are:
  • order_purchase_timestamp: when the customer placed the order.
  • order_delivered_customer_date: when the customer received the order.
  • Some orders may have missing delivery dates (for example, canceled or pending orders).
  • Always check for missing or inconsistent fields before analysis.
df['lead_time_days'] = (df['order_delivered_customer_date'] - df['order_purchase_timestamp']).dt.days
print(df[['order_purchase_timestamp', 'order_delivered_customer_date', 'lead_time_days']].head())
  order_purchase_timestamp order_delivered_customer_date  lead_time_days
0      2017-10-02 10:56:33           2017-10-10 21:25:13             8.0
1      2018-07-24 20:41:37           2018-08-07 15:27:45            13.0
2      2018-08-08 08:38:49           2018-08-17 18:06:29             9.0
3      2017-11-18 19:28:06           2017-12-02 00:28:42            13.0
4      2018-02-13 21:18:39           2018-02-16 18:17:02             2.0
lead_times = df['lead_time_days'].dropna()
print('Number of orders with valid lead times:', lead_times.shape[0])
print('Typical lead time (days):', lead_times.median())
Number of orders with valid lead times: 96476
Typical lead time (days): 10.0
plt.figure(figsize=(8,4))
plt.hist(lead_times, bins=30, color='skyblue', edgecolor='black')
plt.title('Distribution of Delivery Lead Times (days)')
plt.xlabel('Lead Time (days)')
plt.ylabel('Number of Orders')
plt.show()
No description has been provided for this image

Beginner Example 1: Basic Lead Time Calculation#

  • Compute the average lead time for all non-canceled orders.
  • This metric gives operations managers an overview of typical delivery speed.
  • Average values can be influenced by outliers, so always check distribution.
valid = df[df['lead_time_days'].notnull() & (df['lead_time_days'] >= 0)]
average_lead_time = valid['lead_time_days'].mean()
print('Average delivery lead time (days):', round(average_lead_time, 2))
Average delivery lead time (days): 12.09

Beginner Example 2: Flagging Outlier Lead Times#

  • Outliers can distort your metrics and operational decisions.
  • Learn how to flag unusually short or long deliveries.
q_low = valid['lead_time_days'].quantile(0.01)
q_high = valid['lead_time_days'].quantile(0.99)
outliers = valid[(valid['lead_time_days'] < q_low) | (valid['lead_time_days'] > q_high)]
print('Orders with lead times outside the 1st and 99th percentiles:', outliers.shape[0])
Orders with lead times outside the 1st and 99th percentiles: 893

Beginner Example 3: Lead Time by Status#

  • Orders can have different delivery statuses (delivered, shipped, canceled, etc).
  • Group your analysis by order status to separate operational realities.
status_group = valid.groupby('order_status')['lead_time_days'].mean()
print(status_group)
order_status
canceled     19.833333
delivered    12.093604
Name: lead_time_days, dtype: float64
status_counts = valid['order_status'].value_counts()
plt.figure(figsize=(8,3))
plt.bar(status_counts.index, status_counts.values, color='orange')
plt.title('Count of Completed Orders by Status')
plt.xlabel('Order Status')
plt.ylabel('Number of Orders')
plt.show()
No description has been provided for this image

Intermediate Example 1: Lead Time Variability by State#

  • Sometimes, delivery times vary greatly by region or state.
  • Group lead time analysis by state to find regional issues.
customers_url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_customers_dataset.csv'
customers = pd.read_csv(customers_url)
df_state = pd.merge(valid, customers[['customer_id','customer_state']], on='customer_id', how='left')
state_stats = df_state.groupby('customer_state')['lead_time_days'].agg(['mean','std','count']).sort_values(by='mean')
print(state_stats.head())
                     mean       std  count
customer_state                            
SP               8.298061  6.759729  40495
PR              11.526711  6.985404   4923
MG              11.543813  7.205891  11355
DF              12.509135  7.060578   2080
SC              14.479560  8.584494   3547
plt.figure(figsize=(12,5))
plt.bar(state_stats.index, state_stats['mean'], yerr=state_stats['std'], color='seagreen')
plt.title('Mean Lead Time by State with Variability')
plt.xlabel('State')
plt.ylabel('Mean Lead Time (days)')
plt.xticks(rotation=90)
plt.show()
No description has been provided for this image

Intermediate Example 2: Month-over-Month Lead Time Trends#

  • Delivery performance may change seasonally or monthly.
  • Plot and measure lead time variability across different months.
valid['order_month'] = valid['order_purchase_timestamp'].dt.to_period('M')
month_stats = valid.groupby('order_month')['lead_time_days'].agg(['mean','std','count'])
print(month_stats.tail(12))
                  mean        std  count
order_month                             
2017-09      11.400723   7.549636   4150
2017-10      11.416927   7.503920   4478
2017-11      14.699506  10.747319   7288
2017-12      14.936695  10.515305   5513
2018-01      13.637290  10.253245   7069
2018-02      16.510677  11.973848   6556
2018-03      15.867485  11.450345   7003
2018-04      11.048544   8.533847   6798
2018-05      10.959105   8.016439   6749
2018-06       8.774442   6.610068   6096
2018-07       8.503736   6.205091   6156
2018-08       7.286412   4.344279   6351
plt.figure(figsize=(10,4))
plt.errorbar(month_stats.index.astype(str), month_stats['mean'], yerr=month_stats['std'], fmt='-o', color='navy')
plt.title('Monthly Mean Lead Time with Variability')
plt.xlabel('Month')
plt.ylabel('Mean Lead Time (days)')
plt.xticks(rotation=45)
plt.show()
No description has been provided for this image

Intermediate Example 3: Lead Time Variability by Seller#

  • In some supply chain networks, each seller or supplier may contribute their own delays.
  • Identify which sellers have the highest lead time variability.
sellers_url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_order_items_dataset.csv'
items = pd.read_csv(sellers_url)
seller_lead = pd.merge(valid, items[['order_id','seller_id']], on='order_id', how='left')
seller_stats = seller_lead.groupby('seller_id')['lead_time_days'].agg(['mean','std','count'])
top_sellers = seller_stats.sort_values('count', ascending=False).head(5)
print(top_sellers)
                                       mean       std  count
seller_id                                                   
6560211a19b47992c3666cc44a7e94c0   9.058617  7.502385   1996
4a3ca9315b744ce9f8e9374361493884  13.939456  9.331694   1949
1f50f920176fa81dab994f9023523100  15.106957  9.229708   1926
cc419e0650a3c5ba77189a1882b7556a  11.063409  8.122179   1719
da8622b14eb17ae2831f4ac5b9dab84a  10.698320  9.431141   1548

Advanced Example 1: Identifying Orders with Missing or Negative Lead Time#

  • Real datasets often include errors: missing delivery dates or negative intervals.
  • These orders can cause misleading analysis or failed reports.
missing_lt = df['lead_time_days'].isnull().sum()
negative_lt = (df['lead_time_days'] < 0).sum()
print(f'Orders missing lead time: {missing_lt}')
print(f'Orders with negative lead time: {negative_lt}')
Orders missing lead time: 2965
Orders with negative lead time: 0
problem_orders = df[(df['lead_time_days'].isnull()) | (df['lead_time_days'] < 0)]
print(problem_orders[['order_id', 'order_status', 'lead_time_days']].head())
                             order_id order_status  lead_time_days
6    136cce7faa42fdb2cefd53fdc79a6098     invoiced             NaN
44   ee64d42b8cf066f35eac1cf57de1aa85      shipped             NaN
103  0760a852e4e9d89eb77bf631eaaf1c84     invoiced             NaN
128  15bed8e2fec7fdbadb186b57c46c92f2   processing             NaN
154  6942b8da583c2f9957e990d028607019      shipped             NaN

Advanced Example 2: Quantifying Lead Time Variability using Coefficient of Variation#

  • The coefficient of variation (CV) measures variability relative to mean lead time.
  • High CV means unpredictable delivery and higher safety stock needs.
mean_lt = lead_times.mean()
std_lt = lead_times.std()
cv_lt = std_lt / mean_lt if mean_lt != 0 else np.nan
print(f'Coefficient of Variation (lead time): {cv_lt:.2f}')
Coefficient of Variation (lead time): 0.79

Advanced Example 3: Simulating Safety Stock Needs#

  • Lead time variability drives up inventory and working capital requirements.
  • Managers need to estimate extra stock to buffer against delays.
  • Use lead time CV to simulate safety stock for a typical SKU.
avg_daily_sales = 50
z = 1.65  # for about 95 percent service level
safety_stock = z * avg_daily_sales * std_lt ** 0.5
print(f'Estimated safety stock units (given observed lead time variability): {int(safety_stock)}')
Estimated safety stock units (given observed lead time variability): 254

Error Handling: Dealing with Missing Dates#

  • Before calculating any lead time metric, always check for missing purchase or delivery dates.
  • Fill, drop, or flag missing rows to prevent misleading KPI calculations.
  • Decide on a standard method for handling missing or duplicate timestamps.
null_rows = df[df['order_purchase_timestamp'].isnull() | df['order_delivered_customer_date'].isnull()]
print(f'Orders with missing critical dates: {null_rows.shape[0]}')
Orders with missing critical dates: 2965

Error Handling: Avoiding Incorrect Joins#

  • When merging data frames (for example, with seller or customer info), always check row counts before and after.
  • Incorrect joins may duplicate, drop, or disorder records, distorting average lead time results.
# Example: Bad join scenario
pre_join = valid.shape[0]
joined = pd.merge(valid, customers, on='customer_id', how='left')
post_join = joined.shape[0]
print(f'Rows before join: {pre_join}, after join: {post_join}')
Rows before join: 96476, after join: 96476

KPI Calculation Patterns: Lead Time Service Level#

  • Many companies track the fraction of orders delivered within an SLA or promised time.
  • Calculate and visualize service level KPIs directly from lead time data.
  • Group KPIs by month, region, or seller to influence operational strategy.
sla_days = 7
service_level = (valid['lead_time_days'] <= sla_days).mean()
print(f'Percent of orders delivered within {sla_days} days: {service_level:.2%}')
Percent of orders delivered within 7 days: 34.93%
monthly_sla = valid.groupby('order_month').apply(lambda g: (g['lead_time_days'] <= sla_days).mean())
monthly_sla.plot(kind='bar', figsize=(10,4), color='teal')
plt.title('Monthly SLA Service Level')
plt.xlabel('Month')
plt.ylabel('Percent Orders < 7 days')
plt.show()
No description has been provided for this image

Tiny End-to-End Example: Diagnosing a Sudden Lead Time Spike#

  • Suppose management sees a sudden spike in delivery lead time in one month.
  • Use all the analysis skills above to identify cause and generate an action plan.
  • Steps: spot the spike, find affected orders, segment by seller, and list potential operational reasons.
worst_month = month_stats['mean'].idxmax()
print('Month with highest mean delivery lead time:', worst_month)
Month with highest mean delivery lead time: 2016-09
bad_month_orders = valid[valid['order_month'] == worst_month]
late_sellers = pd.merge(bad_month_orders, items[['order_id','seller_id']], on='order_id', how='left')
seller_risk = late_sellers.groupby('seller_id')['lead_time_days'].mean().sort_values(ascending=False).head(3)
print('Sellers with highest mean lead time during worst month:')
print(seller_risk)
Sellers with highest mean lead time during worst month:
seller_id
ecccfa2bb93b34a3bf033cc5d1dcdc69    54.0
Name: lead_time_days, dtype: float64

Congratulations, You Now Have Lead Time Variability Analytics Skills!#

  • Try these patterns on your own firm's supply chain data.
  • Want more deep dives? Subscribe to our YouTube channel for operational analytics walkthroughs.

Found this useful?

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