Mathew K Analytics

Lesson 6 · Supply Chain Operations Analytics

Loading and Exploring Supply Chain Datasets

In this lesson, we will learn how to load, clean, and explore real-world supply chain datasets. Loading and understanding your operational data is critical…

⬇ 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

Loading and Exploring Supply Chain Datasets#

  • In this lesson, we will learn how to load, clean, and explore real-world supply chain datasets.
  • Loading and understanding your operational data is critical for supply chain and operations analytics.
  • You will learn how to access data from real retail, manufacturing, and logistics sources.
  • We will discover common pitfalls and best practices for handling supply chain data.
  • By the end, you will be ready to start analyzing and drawing insights from raw data.
import pandas as pd
import openml
import warnings
warnings.filterwarnings('ignore')

Understanding supply chain, inventory, and operations data#

  • In supply chain analytics, we typically work with datasets such as transactions, orders, inventory levels, and delivery records.
  • The data is often tabular, with each row representing an operational event (for example, an order or shipment).
  • Key columns usually include IDs, dates, product codes, quantities, and partner information.
  • Common mistakes are missing date conversions, misunderstanding categorical codes, and misaligned joins between tables.
# Example 1: Loading a retail demand dataset (UCI Online Retail II)
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df_retail = pd.read_excel(url, sheet_name='Year 2010-2011')
df_retail['InvoiceDate'] = pd.to_datetime(df_retail['InvoiceDate'])
print(df_retail.shape)
print(df_retail.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: Loading an order fulfillment dataset (Olist e-commerce orders)
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  
# Example 3: Loading a supplier performance dataset (OpenML Supply Chain)
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 4: Loading inventory and operational records (OpenML Inventory Operations)
dataset = openml.datasets.get_dataset(43900)
X, y, _, _ = dataset.get_data(dataset_format='dataframe')
df_inventory_ops = X.copy() if y is None else pd.concat([X, y], axis=1)
print(df_inventory_ops.shape)
print(df_inventory_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  

Previewing and summarizing datasets#

  • Exploring head() and tail() gives you a quick look at data.
  • Use shape and info() to understand dimensions and types.
  • Be wary of nulls and inconsistent data types in operational systems.
print('Retail columns:', df_retail.columns.tolist())
print('Order columns:', df_orders.columns.tolist())
print('Supplier columns:', df_supplier.columns.tolist())
Retail columns: ['Invoice', 'StockCode', 'Description', 'Quantity', 'InvoiceDate', 'Price', 'Customer ID', 'Country']
Order columns: ['order_id', 'customer_id', 'order_status', 'order_purchase_timestamp', 'order_approved_at', 'order_delivered_carrier_date', 'order_delivered_customer_date', 'order_estimated_delivery_date']
Supplier columns: ['full_name', 'gender', 'current_annual_salary', '2016_gross_pay_received', '2016_overtime_pay', 'department', 'department_name', 'division', 'assignment_category', 'employee_position_title', 'underfilled_job_title', 'date_first_hired', 'year_first_hired']
print('Retail Info:')
df_retail.info()
print('\nOrder Info:')
df_orders.info()
Retail Info:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541910 entries, 0 to 541909
Data columns (total 8 columns):
 #   Column       Non-Null Count   Dtype         
---  ------       --------------   -----         
 0   Invoice      541910 non-null  object        
 1   StockCode    541910 non-null  object        
 2   Description  540456 non-null  object        
 3   Quantity     541910 non-null  int64         
 4   InvoiceDate  541910 non-null  datetime64[ns]
 5   Price        541910 non-null  float64       
 6   Customer ID  406830 non-null  float64       
 7   Country      541910 non-null  object        
dtypes: datetime64[ns](1), float64(2), int64(1), object(4)
memory usage: 33.1+ MB

Order Info:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 99441 entries, 0 to 99440
Data columns (total 8 columns):
 #   Column                         Non-Null Count  Dtype         
---  ------                         --------------  -----         
 0   order_id                       99441 non-null  object        
 1   customer_id                    99441 non-null  object        
 2   order_status                   99441 non-null  object        
 3   order_purchase_timestamp       99441 non-null  datetime64[ns]
 4   order_approved_at              99281 non-null  object        
 5   order_delivered_carrier_date   97658 non-null  object        
 6   order_delivered_customer_date  96476 non-null  datetime64[ns]
 7   order_estimated_delivery_date  99441 non-null  object        
dtypes: datetime64[ns](2), object(6)
memory usage: 6.1+ MB
print('Retail null values:')
print(df_retail.isnull().sum().sort_values(ascending=False).head(7))
Retail null values:
Customer ID    135080
Description      1454
StockCode           0
Invoice             0
Quantity            0
InvoiceDate         0
Price               0
dtype: int64
print('How many unique customers?')
print(df_retail['Customer ID'].nunique())
print('How many unique StockCodes?')
print(df_retail['StockCode'].nunique())
How many unique customers?
4372
How many unique StockCodes?
4070
# Basic aggregation: total sales per country
df_retail['TotalValue'] = df_retail['Quantity'] * df_retail['Price']
country_sales = df_retail.groupby('Country')['TotalValue'].sum().sort_values(ascending=False)
print(country_sales.head(6))
Country
United Kingdom    8187806.364
Netherlands        284661.540
EIRE               263276.820
Germany            221698.210
France             197421.900
Australia          137077.270
Name: TotalValue, dtype: float64
# Group by time: monthly sales trends
df_retail['Month'] = df_retail['InvoiceDate'].dt.to_period('M')
monthly_demand = df_retail.groupby('Month')['TotalValue'].sum()
print(monthly_demand.tail(6))
Month
2011-07     681300.111
2011-08     682680.510
2011-09    1019687.622
2011-10    1070704.670
2011-11    1461756.250
2011-12     433686.010
Freq: M, Name: TotalValue, dtype: float64
# Filtering logistics records: delivered orders only
delivered = df_orders[df_orders['order_delivered_customer_date'].notna()]
print('Delivered orders:', delivered.shape[0])
Delivered orders: 96476
# Example: Finding orders with missing purchase dates
missing_purchase = df_orders[df_orders['order_purchase_timestamp'].isna()]
print('Orders with missing purchase date:', missing_purchase.shape[0])
Orders with missing purchase date: 0
# Example: Accidentally joining on the wrong column
try:
    merged_wrong = pd.merge(df_orders, df_supplier, left_on='order_id', right_on='Supplier ID')
    print('Join rows:', merged_wrong.shape[0])
except Exception as e:
    print('Error from bad merge:', e)
Error from bad merge: 'Supplier ID'
# Example: Misinterpreting lead time columns
if 'lead_time' in df_supplier.columns:
    print(df_supplier['lead_time'].describe())
else:
    print('The supplier data does not have a lead_time column.')
The supplier data does not have a lead_time column.

Operational analytics best practices#

  • Always validate types and ranges for dates, quantities, and currency columns.
  • Use groupby and aggregation for all KPI calculations.
  • Plot time trends to spot seasonality and outliers.
  • Sample your data to spot irregularities early.
# Calculate average delivery lead time (orders)
delivered = df_orders[df_orders['order_delivered_customer_date'].notna()].copy()
delivered['lead_time_days'] = (delivered['order_delivered_customer_date'] - delivered['order_purchase_timestamp']).dt.days
print('Average lead time:', delivered['lead_time_days'].mean())
Average lead time: 12.094085575687217
# Group by product: sales by SKU
sku_sales = df_retail.groupby('StockCode')['TotalValue'].sum().sort_values(ascending=False)
print(sku_sales.head(5))
StockCode
DOT       206245.48
22423     164762.19
47566      98302.98
85123A     97894.50
85099B     92356.03
Name: TotalValue, dtype: float64
# Example: Advancedmonthly sales pivot table by country
pivot = df_retail.pivot_table(index='Month', columns='Country', values='TotalValue', aggfunc='sum')
print(pivot.tail(3))
Country  Australia  Austria  Bahrain  Belgium  Brazil  Canada  \
Month                                                           
2011-10   17150.53  1043.78      NaN  5651.38     NaN     NaN   
2011-11    6805.99  1329.78      NaN  6229.41     NaN     NaN   
2011-12        NaN   683.20      NaN  1409.43     NaN     NaN   

Country  Channel Islands   Cyprus  Czech Republic  Denmark  ...      RSA  \
Month                                                       ...            
2011-10          2623.32  4216.52          277.48  1438.11  ...  1002.31   
2011-11          1495.17   460.89          -61.51  2699.57  ...      NaN   
2011-12           194.15   -91.25             NaN   168.90  ...      NaN   

Country  Saudi Arabia  Singapore    Spain   Sweden  Switzerland     USA  \
Month                                                                     
2011-10           NaN     999.26  5078.65  5766.16      8033.98  731.69   
2011-11           NaN        NaN  8533.87  2612.71      8116.96     NaN   
2011-12           NaN        NaN   271.43     0.00          NaN  615.28   

Country  United Arab Emirates  United Kingdom  Unspecified  
Month                                                       
2011-10                   NaN       877438.19          NaN  
2011-11                   NaN      1282805.78       965.75  
2011-12                   NaN       388735.43          NaN  

[3 rows x 38 columns]
# Example: Correlation analysis for operational drivers
numeric_cols = df_retail.select_dtypes(include='number')
cor_matrix = numeric_cols.corr()
print(cor_matrix[['TotalValue']])
             TotalValue
Quantity       0.886681
Price         -0.162029
Customer ID   -0.002274
TotalValue     1.000000

Mini-project: Analyzing delayed deliveries#

  • Our goal is to identify the month with the highest number of late deliveries in the Olist order data.
  • We will use our datetime columns and groupby skills.
  • Real companies use such dashboards to detect bottlenecks and risk.
# Find late deliveries (delivered after estimated date)
df_orders['order_estimated_delivery_date'] = pd.to_datetime(df_orders['order_estimated_delivery_date'])
late = df_orders[(
    (df_orders['order_delivered_customer_date'].notna()) &
    (df_orders['order_delivered_customer_date'] > df_orders['order_estimated_delivery_date'])
)]
late['late_month'] = late['order_delivered_customer_date'].dt.to_period('M')
late_month_counts = late.groupby('late_month').size().sort_values(ascending=False)
print('Month with most late deliveries:')
print(late_month_counts.head(1))
Month with most late deliveries:
late_month
2018-04    1462
Freq: M, dtype: int64

Great work! Reflections & More Practice#

  • You have learned to load, clean, and analyze real supply chain datasets from retail, logistics, and manufacturing sources.
  • Practice: What would you look to automate after these first analyses?
  • To see more supply chain analytics with real data, search for 'supply chain analytics in python' on YouTube.

Found this useful?

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