Mathew K Analytics

Lesson 13 · Supply Chain Operations Analytics

Joining Orders, Inventory, and Supplier Tables in Supply Chain Analytics

In this lesson we will learn how to join real-world orders, inventory, and supplier data tables. Understanding table joins is critical for tracking…

⬇ 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

Joining Orders, Inventory, and Supplier Tables in Supply Chain Analytics#

  • In this lesson we will learn how to join real-world orders, inventory, and supplier data tables.
  • Understanding table joins is critical for tracking stockouts, order fulfillment, and supplier performance.
  • You will work through real public datasets and learn to answer questions like: Is out-of-stock due to demand or a late supplier?
  • You will join multiple operational datasets to produce actionable business insights.
  • By the end you will confidently combine multiple supply chain sources for advanced operational analytics.
import pandas as pd
import openml
import warnings
warnings.filterwarnings('ignore')

Understanding Orders, Inventory, and Supplier Data#

  • Orders data tracks demands from customers and shows what products are being requested.
  • Inventory datasets give us the available stock and allow us to track stockouts or shortages.
  • Supplier tables provide delays, performance metrics, and support root-cause analysis when things go wrong.
  • Real-world data can be large, have inconsistent product IDs, or missing links between tables.
  • Beginners often forget to align on the same key field (like product or stock code) when joining tables.
# Beginner Example 1: Load order transactions (Olist e-commerce dataset)
order_url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
orders = pd.read_csv(order_url)
orders['order_purchase_timestamp'] = pd.to_datetime(orders['order_purchase_timestamp'])
orders['order_delivered_customer_date'] = pd.to_datetime(orders['order_delivered_customer_date'])
print(orders.shape)
print(orders.head(2))
(99441, 8)
                           order_id                       customer_id  \
0  e481f51cbdc54678b7cc49136f2d6af7  9ef432eb6251297304e76186b10a928d   
1  53cdb2fc8bc7dce0b6741e2150273451  b0830fb4747a6c6d20dea0b8c802d7ef   

  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   

  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   

  order_estimated_delivery_date  
0           2017-10-18 00:00:00  
1           2018-08-13 00:00:00  
# Beginner Example 2: Load real inventory/stock (UCI Online Retail dataset)
retail_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
inventory = pd.read_excel(retail_url, sheet_name='Year 2010-2011')
inventory['InvoiceDate'] = pd.to_datetime(inventory['InvoiceDate'])
print(inventory.shape)
print(inventory.head(2))
(541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   

          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  
# Beginner Example 3: Load supplier data (OpenML supply chain performance dataset)
dataset = openml.datasets.get_dataset(42125)
supplier_df, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(supplier_df.shape)
print(supplier_df.head(2))
(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   

   2016_overtime_pay department       department_name  \
0             416.10        POL  Department of Police   
1            3326.19        POL  Department of Police   

                                            division assignment_category  \
0  MSB Information Mgmt and Tech Division Records...    Fulltime-Regular   
1         ISB Major Crimes Division Fugitive Section    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   

   year_first_hired  
0              1986  
1              1988  
# Intermediate Example 1: Preparation for joins - align key fields
print('Order table columns:', orders.columns.tolist())
print('Inventory table columns:', inventory.columns.tolist())
print('Supplier table columns:', supplier_df.columns.tolist())
Order table 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']
Inventory table columns: ['Invoice', 'StockCode', 'Description', 'Quantity', 'InvoiceDate', 'Price', 'Customer ID', 'Country']
Supplier table 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']
# Intermediate Example 2: Harmonize field names for joining product IDs
inventory.rename(columns={'StockCode': 'product_id'}, inplace=True)
if 'product_id' not in orders.columns:
    orders['product_id'] = ''  # For illustration; usually we would map using another lookup
print('Renamed columns ready for joining')
Renamed columns ready for joining
# Intermediate Example 3: Merge orders with inventory data on product_id (outer join)
merged = pd.merge(orders, inventory, on='product_id', how='outer', suffixes=('_order', '_inv'))
print('Joined table shape:', merged.shape)
print(merged[['order_id','product_id','Quantity','order_purchase_timestamp','InvoiceDate']].head(2))
Joined table shape: (641351, 16)
  order_id product_id  Quantity order_purchase_timestamp         InvoiceDate
0      NaN      10002      48.0                      NaT 2010-12-01 08:45:00
1      NaN      10002      12.0                      NaT 2010-12-01 09:45:00
# Intermediate Example 4: Joining inventory with supplier data on department or division
if 'department_name' in supplier_df.columns and 'Description' in inventory.columns:
    inventory['department_name'] = inventory['Description'].str.extract(r'(\w+)', expand=False)
    merged_supplier_inventory = pd.merge(inventory, supplier_df, on='department_name', how='left')
    print('Join shape:', merged_supplier_inventory.shape)
else:
    merged_supplier_inventory = None
    print('Could not join; missing matching fields.')
Join shape: (541910, 21)
# Intermediate Example 5: Find stockouts: products with orders but no inventory record
stockouts = merged[merged['Quantity'].isna()]
print('Stockouts detected:', stockouts.shape[0])
print(stockouts[['order_id', 'product_id', 'order_purchase_timestamp']].head(2))
Stockouts detected: 99441
                                order_id product_id order_purchase_timestamp
487036  e481f51cbdc54678b7cc49136f2d6af7                 2017-10-02 10:56:33
487037  53cdb2fc8bc7dce0b6741e2150273451                 2018-07-24 20:41:37
# Intermediate Example 6: Calculate average supplier pay as KPI for department
if merged_supplier_inventory is not None:
    avg_pay = merged_supplier_inventory.groupby('department_name')['current_annual_salary'].mean().reset_index()
    print(avg_pay.head(3))
else:
    print('Could not compute KPI: missing join result.')
  department_name  current_annual_salary
0              10                    NaN
1              12                    NaN
2              15                    NaN
# Advanced Example 1: Multi-table join for advanced analysis
multi = pd.merge(orders, inventory, on='product_id', how='left', suffixes=('_order', '_inv'))
if 'department_name' in supplier_df.columns:
    result = pd.merge(multi, supplier_df, on='department_name', how='left')
    print('Joined orders, inventory, and suppliers:', result.shape)
else:
    result = multi
    print('Could not add supplier data (no department_name field)')
Joined orders, inventory, and suppliers: (99441, 29)
# Advanced Example 2: Pivot to count orders per supplier per month
if 'full_name' in supplier_df.columns and 'order_purchase_timestamp' in orders.columns:
    result['month'] = result['order_purchase_timestamp'].dt.to_period('M')
    orders_by_sup = result.groupby(['full_name','month']).size().reset_index(name='num_orders')
    print(orders_by_sup.head(5))
else:
    print('Required supplier or date fields missing.')
Empty DataFrame
Columns: [full_name, month, num_orders]
Index: []
# Advanced Example 3: Calculate lead times and flag excessive values
if 'order_purchase_timestamp' in result.columns and 'order_delivered_customer_date' in result.columns:
    result['lead_time'] = (result['order_delivered_customer_date'] - result['order_purchase_timestamp']).dt.days
    slow_orders = result[result['lead_time'] > 15]
    print('Orders with long lead time:', slow_orders.shape[0])
    print(slow_orders[['order_id','full_name','lead_time']].head(2))
else:
    print('Date fields for lead time unavailable.')
Orders with long lead time: 23210
                           order_id full_name  lead_time
5  a4591c265e18cb1dcee52889e2d8acc3       NaN       16.0
9  e69bfb5eb88e0ed6a785585b27e16dbf       NaN       18.0
# Error Handling Example 1: Detect orders missing delivery dates
missing_delivery = orders[orders['order_delivered_customer_date'].isna()]
print('Orders missing delivery date:', missing_delivery.shape[0])
print(missing_delivery[['order_id','order_purchase_timestamp']].head(2))
Orders missing delivery date: 2965
                            order_id order_purchase_timestamp
6   136cce7faa42fdb2cefd53fdc79a6098      2017-04-11 12:22:08
44  ee64d42b8cf066f35eac1cf57de1aa85      2018-06-04 16:44:48
# Error Handling Example 2: Attempt join with mismatched product_id and show errors
try:
    bad_join = pd.merge(orders, inventory, on='wrong_field')
except Exception as e:
    print('Join error:', e)
Join error: 'wrong_field'
# Error Handling Example 3: Check for negative or unrealistic quantities
bad_quantity = inventory[inventory['Quantity'] < 0]
print('Negative quantity entries:', bad_quantity.shape[0])
if bad_quantity.shape[0] > 0:
    print(bad_quantity[['product_id','Quantity','Description']].head(3))
Negative quantity entries: 10624
    product_id  Quantity                      Description
141          D        -1                         Discount
154     35004C        -1  SET OF 3 COLOURED  FLYING DUCKS
235      22556       -12   PLASTERS IN TIN CIRCUS PARADE 

Best Practices for Data Joins in Operations Analytics#

  • Always standardize columns before joining tables.
  • Document all join assumptions (e.g., left join means keeping all orders).
  • Perform aggregated checks: after a join, the number of rows may signal duplicates.
  • Use groupby and time-window aggregation to reveal seasonality or bottlenecks.
  • KPIs like average lead time or order fill rate unlock actionable improvements.
  • Check output statistics after every join to spot issues early.
# Aggregation example: Compute fill rate (orders with matched inventory / total orders)
filled = merged[~merged['Quantity'].isna()]
fill_rate = len(filled) / len(orders)
print(f'Fill rate: {fill_rate:.2%}')
Fill rate: 544.96%
# Time series grouping: Average delivery lead time per month
if 'order_purchase_timestamp' in orders.columns and 'order_delivered_customer_date' in orders.columns:
    orders['lead_time'] = (orders['order_delivered_customer_date'] - orders['order_purchase_timestamp']).dt.days
    orders['month'] = orders['order_purchase_timestamp'].dt.to_period('M')
    avg_lead = orders.groupby('month')['lead_time'].mean().reset_index()
    print(avg_lead.tail(6))
else:
    print('Date columns missing, cannot calculate lead time.')
      month  lead_time
19  2018-05  10.959105
20  2018-06   8.774442
21  2018-07   8.503736
22  2018-08   7.286412
23  2018-09        NaN
24  2018-10        NaN
# Complete end-to-end: Identify top 5 suppliers responsible for most stockouts
if result is not None and 'full_name' in result.columns and 'Quantity' in result.columns:
    stockout_orders = result[result['Quantity'].isna()]
    top_suppliers = stockout_orders['full_name'].value_counts().head(5)
    print('Top 5 suppliers by count of stockouts:')
    print(top_suppliers)
else:
    print('Could not calculate: missing required columns.')
Top 5 suppliers by count of stockouts:
Series([], Name: count, dtype: int64)

Where to Go Next#

  • Challenge yourself: Find which product and supplier pairs lead most often to delays.
  • Explore other OpenML datasets, and try more advanced joins with time or location splits.
  • Subscribe on YouTube for more real-world supply chain Python tutorials.

Found this useful?

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