Mathew K Analytics

Lesson 1 · Supply Chain Operations Analytics

Python Basics for Supply Chain Analytics

Welcome to supply chain and operations analytics with Python. We focus on using simple code to answer real-world business problems. You will preview and…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Python Basics for Supply Chain Analytics#

  • Welcome to supply chain and operations analytics with Python.

  • We focus on using simple code to answer real-world business problems.

  • You will preview and analyze real supply chain data.

  • By the end, you will extract and interpret important operational insights.

  • The goal is not to be a programming expert, but to be a data-driven operations professional.

import pandas as pd
import numpy as np
import openml
import warnings
warnings.filterwarnings('ignore')

Working with Supply Chain Data: Core Concepts#

  • Operational datasets capture real-world transactions and flows.
  • These datasets typically track events like orders, shipments, and demand.
  • Data is usually structured as tables: rows = records, columns = details.
  • Common mistakes: forgetting to parse date columns, missing units, or ignoring data timeframes.
  • Cleaning, summarizing, and understanding structure is critical in supply chain analytics.
# Example 1: Loading a retail transaction dataset
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')
print(f"Retail dataset shape: {df_retail.shape}")
df_retail.head(3)
Retail dataset shape: (541910, 8)
Invoice StockCode Description Quantity InvoiceDate Price Customer ID Country
0 536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 2010-12-01 08:26:00 2.55 17850.0 United Kingdom
1 536365 71053 WHITE METAL LANTERN 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom
2 536365 84406B CREAM CUPID HEARTS COAT HANGER 8 2010-12-01 08:26:00 2.75 17850.0 United Kingdom
# Example 2: Parse dates for time-based analysis
df_retail['InvoiceDate'] = pd.to_datetime(df_retail['InvoiceDate'])
print(df_retail['InvoiceDate'].dtype)
print(df_retail['InvoiceDate'].min(), '-', df_retail['InvoiceDate'].max())
datetime64[ns]
2010-12-01 08:26:00 - 2011-12-09 12:50:00
# Example 3: Counting unique products
unique_products = df_retail['StockCode'].nunique()
print(f"Number of unique products: {unique_products}")
Number of unique products: 4070
# Example 4: Loading a supplier performance dataset
dataset = openml.datasets.get_dataset(42125)
df_supplier, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(f"Supplier performance dataset: {df_supplier.shape}")
df_supplier.head(3)
Supplier performance dataset: (9228, 13)
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
0 Aarhus, Pam J. F 69222.18 71225.98 416.10 POL Department of Police MSB Information Mgmt and Tech Division Records... Fulltime-Regular Office Services Coordinator None 09/22/1986 1986
1 Aaron, David J. M 97392.47 103088.48 3326.19 POL Department of Police ISB Major Crimes Division Fugitive Section Fulltime-Regular Master Police Officer None 09/12/1988 1988
2 Aaron, Marsha M. F 104717.28 107000.24 1353.32 HHS Department of Health and Human Services Adult Protective and Case Management Services Fulltime-Regular Social Worker IV None 11/19/1989 1989
# Example 5: Checking for missing values (critical in operations)
missing_columns = df_supplier.columns[df_supplier.isnull().any()]
print(f"Columns with missing values: {list(missing_columns)}")
Columns with missing values: ['gender', '2016_gross_pay_received', '2016_overtime_pay', 'underfilled_job_title']
# Example 6: Load order fulfillment data (E-commerce delivery)
url_order = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
df_order = pd.read_csv(url_order)
print(f"Order dataset shape: {df_order.shape}")
df_order.head(3)
Order dataset shape: (99441, 8)
order_id customer_id order_status order_purchase_timestamp order_approved_at order_delivered_carrier_date order_delivered_customer_date order_estimated_delivery_date
0 e481f51cbdc54678b7cc49136f2d6af7 9ef432eb6251297304e76186b10a928d delivered 2017-10-02 10:56:33 2017-10-02 11:07:15 2017-10-04 19:55:00 2017-10-10 21:25:13 2017-10-18 00:00:00
1 53cdb2fc8bc7dce0b6741e2150273451 b0830fb4747a6c6d20dea0b8c802d7ef delivered 2018-07-24 20:41:37 2018-07-26 03:24:27 2018-07-26 14:31:00 2018-08-07 15:27:45 2018-08-13 00:00:00
2 47770eb9100c2d0c44946d9cf07ec65d 41ce2a54c0b03bf3443c3d931a367089 delivered 2018-08-08 08:38:49 2018-08-08 08:55:23 2018-08-08 13:50:00 2018-08-17 18:06:29 2018-09-04 00:00:00
# Example 7: Calculating late deliveries
df_order['order_purchase_timestamp'] = pd.to_datetime(df_order['order_purchase_timestamp'])
df_order['order_delivered_customer_date'] = pd.to_datetime(df_order['order_delivered_customer_date'])
df_order['delivery_delay'] = (df_order['order_delivered_customer_date'] - df_order['order_purchase_timestamp']).dt.days
late_orders = df_order[df_order['delivery_delay'] > 7]
print(f"Orders delivered after more than one week: {late_orders.shape[0]}")
Orders delivered after more than one week: 62778

Intermediate Example: OpenML Sales Forecasting Data#

  • Now we move beyond transactions, toward predicting future sales.
  • Forecasting is vital for inventory and planning.
  • OpenML datasets can be used for machine learning and forecasting in supply chain analytics.
# Example 8: Load a sales forecasting dataset
dataset = openml.datasets.get_dataset(4549)
X, y, _, _ = dataset.get_data(dataset_format='dataframe')
df_sales = pd.concat([X, y], axis=1)
print(f"Sales forecasting data: {df_sales.shape}")
df_sales.head(3)
Sales forecasting data: (583250, 78)
NCD_0 NCD_1 NCD_2 NCD_3 NCD_4 NCD_5 NCD_6 AI_0 AI_1 AI_2 ... ADL_5 ADL_6 NAD_0 NAD_1 NAD_2 NAD_3 NAD_4 NAD_5 NAD_6 Annotation
0 0.0 2.0 0.0 0.0 1.0 1.0 1.0 0.0 1.0 0.0 ... 1.0 1.0 0.0 2.0 0.0 0.0 1.0 1.0 1.0 0.0
1 2.0 1.0 0.0 0.0 0.0 0.0 4.0 2.0 1.0 0.0 ... 0.0 1.0 2.0 1.0 0.0 0.0 0.0 0.0 4.0 0.5
2 1.0 0.0 0.0 0.0 0.0 4.0 1.0 1.0 0.0 0.0 ... 1.0 1.0 1.0 0.0 0.0 0.0 0.0 4.0 1.0 0.0

3 rows × 78 columns

# Example 9: Aggregate daily sales totals
if 'date' in df_sales.columns:
    df_sales['date'] = pd.to_datetime(df_sales['date'])
    df_daily = df_sales.groupby('date')['target'].sum().reset_index()
    print(df_daily.head())
else:
    print('Dataset has no date field.')
Dataset has no date field.
# Example 10: Investigate outliers in sales data
if 'target' in df_sales.columns:
    sales_q1 = df_sales['target'].quantile(0.25)
    sales_q3 = df_sales['target'].quantile(0.75)
    iqr = sales_q3 - sales_q1
    outliers = df_sales[(df_sales['target'] < sales_q1 - 1.5 * iqr) | (df_sales['target'] > sales_q3 + 1.5 * iqr)]
    print(f"Number of sales outliers: {outliers.shape[0]}")
else:
    print('No target column found in sales dataset.')
No target column found in sales dataset.

Intermediate Example: Manufacturing Operations Data#

  • Manufacturing sites use data tables to spot bottlenecks and improve throughput.
  • The OpenML manufacturing operations dataset is a good testbed for typical plant analytics.
  • Data includes order flows, process timings, and outcomes.
# Example 11: Load manufacturing operations dataset
dataset = openml.datasets.get_dataset(43900)
X_mfg, y_mfg, _, _ = dataset.get_data(dataset_format='dataframe')
df_mfg = X_mfg.copy() if y_mfg is None else pd.concat([X_mfg, y_mfg], axis=1)
print(f"Manufacturing operation data shape: {df_mfg.shape}")
df_mfg.head(3)
Manufacturing operation data shape: (32769, 10)
ACTION RESOURCE MGR_ID ROLE_ROLLUP_1 ROLE_ROLLUP_2 ROLE_DEPTNAME ROLE_TITLE ROLE_FAMILY_DESC ROLE_FAMILY ROLE_CODE
0 1 39353 85475 117961 118300 123472 117905 117906 290919 117908
1 1 17183 1540 117961 118343 123125 118536 118536 308574 118539
2 1 36724 14457 118219 118220 117884 117879 267952 19721 117880
# Example 12: Compute average cycle time for a process step
process_cols = [col for col in df_mfg.columns if 'cycle' in col.lower() or 'time' in col.lower()]
if process_cols:
    mean_cycle = df_mfg[process_cols[0]].mean()
    print(f"Average cycle time: {mean_cycle:.2f}")
else:
    print('No cycle time columns found.')
No cycle time columns found.
# Example 13: Checking for duplicate order records
order_id_col = [col for col in df_mfg.columns if 'order' in col.lower()]
if order_id_col:
    dupes = df_mfg.duplicated(order_id_col[0]).sum()
    print(f"Number of duplicate order IDs: {dupes}")
else:
    print('No order id column found.')
No order id column found.

Advanced Example: SECOM Quality Anomaly Dataset#

  • Quality variation and defects can halt supply chain performance.
  • The SECOM dataset is designed for anomaly detection in manufacturing.
  • Focus is on mixing process measurements with predicted quality outcome.
# Example 14: Load SECOM manufacturing quality dataset
dataset = openml.datasets.get_dataset(1500)
X_qual, y_qual, _, _ = dataset.get_data(dataset_format='dataframe')
df_secom = pd.concat([X_qual, y_qual], axis=1)
print(f"Quality records: {df_secom.shape}")
df_secom.head(3)
Quality records: (210, 8)
V1 V2 V3 V4 V5 V6 V7 Class
0 15.26 14.84 0.8710 5.763 3.312 2.221 5.220 1
1 14.88 14.57 0.8811 5.554 3.333 1.018 4.956 1
2 14.29 14.09 0.9050 5.291 3.337 2.699 4.825 1
# Example 15: Detect missing sensor values
missing_sensor = df_secom.isnull().any(axis=1).sum()
print(f"Production records with missing sensor data: {missing_sensor}")
Production records with missing sensor data: 0
# Example 16: Identify defective unit share
if y_qual is not None and y_qual.name in df_secom.columns:
    defect_share = (df_secom[y_qual.name] != 1).mean()
    print(f"Fraction of defective units: {defect_share:.2%}")
else:
    print('Defect label not present.')
Defect label not present.
# Advanced Example 2: Calculate supplier on-time rate
if 'OnTimeDelivery' in df_supplier.columns:
    on_time_rate = (df_supplier['OnTimeDelivery'] == 1).mean()
    print(f"Supplier on-time delivery rate: {on_time_rate:.2%}")
else:
    print('No on-time delivery field in supplier data.')
No on-time delivery field in supplier data.

Error Handling and Debugging in Supply Chain Data#

  • Real-world datasets often contain missing, misformatted, or duplicated entries.
  • Python helps you surface common business data mistakes before they become operational disasters.
  • Always check for nulls, duplicates, and strange outliers as a first step.
  • Robust analytics relies on robust raw data handling.
# Example 17: Handling missing delivery dates
missing_date = df_order['order_delivered_customer_date'].isnull().sum()
print(f"Orders with missing delivery date: {missing_date}")
Orders with missing delivery date: 2965
# Example 18: Joining with mismatched keys
fake_left = pd.DataFrame({'OrderID': [1, 2, 3], 'Location': ['A', 'B', 'C']})
fake_right = pd.DataFrame({'OrderRef': [2, 3, 4], 'Value': [50, 60, 70]})
merged = pd.merge(fake_left, fake_right, left_on='OrderID', right_on='OrderRef', how='left')
print(merged)
   OrderID Location  OrderRef  Value
0        1        A       NaN    NaN
1        2        B       2.0   50.0
2        3        C       3.0   60.0
# Example 19: Misinterpreting inventory quantities
data_qty = pd.DataFrame({'Product': ['X', 'Y'], 'Stock': ['12', 'eight']})
data_qty['Stock'] = pd.to_numeric(data_qty['Stock'], errors='coerce')
print(data_qty)
  Product  Stock
0       X   12.0
1       Y    NaN

Patterns & Best Practices: Operational Analytics#

  • Always aggregate transaction data before making broad supply chain claims.
  • Use groupby for aggregated KPIs: total orders, mean lead time, or on-time rate.
  • Use time-based grouping to spot seasonality or bottleneck periods.
  • Calculate KPI metrics directly to answer strategic supply chain questions.
# Example 20: Aggregate orders per customer
if 'customer_id' in df_order.columns:
    customer_orders = df_order.groupby('customer_id').size().reset_index(name='num_orders')
    print(customer_orders.head())
else:
    print('Customer id not found in data.')
                        customer_id  num_orders
0  00012a2ce6f8dcda20d059ce98491703           1
1  000161a058600d5901f007fab4c27140           1
2  0001fd6190edaaf884bcaf3d49edf079           1
3  0002414f95344307404f0ace7a26f1d5           1
4  000379cdec625522490c315e70c7a9fb           1
# Example 21: Group sales by product for top-sellers
if 'StockCode' in df_retail.columns and 'Quantity' in df_retail.columns:
    product_sales = df_retail.groupby('StockCode')['Quantity'].sum().reset_index()
    product_sales = product_sales.sort_values('Quantity', ascending=False).head(5)
    print(product_sales)
else:
    print('Sales columns not present in retail data.')
     StockCode  Quantity
1070     22197     56450
2622     84077     53847
3659    85099B     47363
3670    85123A     38830
2735     84879     36221
# Example 22: Calculate on-time order fulfillment rate
if 'delivery_delay' in df_order.columns:
    on_time_fulfillment = (df_order['delivery_delay'] <= 7).mean()
    print(f"On-time fulfillment rate: {on_time_fulfillment:.2%}")
else:
    print('Delivery delay field missing in order data.')
On-time fulfillment rate: 33.89%

Mini End-to-End Example: Identify a Supply Chain Bottleneck#

  • Goal: Find the supplier with the worst late delivery record.
  • Load data, clean it, and extract a high-impact metric in a few minutes.
  • This pattern applies directly in real operational dashboards.
# Step 1: Load supplier performance data (repeat from earlier)
dataset = openml.datasets.get_dataset(42125)
df_supplier, _, _, _ = dataset.get_data(dataset_format='dataframe')
print('Supplier data loaded:', df_supplier.shape)
Supplier data loaded: (9228, 13)
# Step 2: Clean late delivery values (handling missing and outliers)
if 'LateDeliveries' in df_supplier.columns:
    late_counts = df_supplier['LateDeliveries'].copy()
    late_counts = pd.to_numeric(late_counts, errors='coerce').fillna(0)
    df_supplier['LateDeliveriesClean'] = late_counts
    print('Missing values filled. Outliers and non-numbers handled.')
else:
    print('LateDeliveries field not present.')
LateDeliveries field not present.
# Step 3: Extract the supplier with the most late deliveries
if 'SupplierName' in df_supplier.columns and 'LateDeliveriesClean' in df_supplier.columns:
    top_late = df_supplier.groupby('SupplierName')['LateDeliveriesClean'].sum().reset_index()
    worst = top_late.sort_values('LateDeliveriesClean', ascending=False).head(1)
    print('Supplier with the most late deliveries:')
    print(worst)
else:
    print('Fields needed for this KPi are missing.')
Fields needed for this KPi are missing.
 

Found this useful?

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