Mathew K Analytics

Lesson 15 · Supply Chain Operations Analytics

Mastering Core Supply Chain Views for Enhanced Operations Analytics

In this lesson, we explore how to build fundamental supply chain and operations analytics views using real-world data. Building these views helps…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Building Core Supply Chain Views#

  • In this lesson, we explore how to build fundamental supply chain and operations analytics views using real-world data.
  • Building these views helps organizations monitor inventory, track sales, measure supplier performance, and optimize fulfillment.
  • You will learn to organize operational data, clean and analyze it, and build key dashboards or analytical tables.
  • By the end, you will know how to transform raw supply chain data into actionable business insights.
import pandas as pd
import numpy as np
import openml
import warnings
warnings.filterwarnings('ignore')

Understanding Core Supply Chain Data#

  • Supply chain data includes sales, inventory, supplier, and operational performance records.
  • Each dataset usually contains time, location, product/part, and quantity information.
  • Beginners often confuse transactional records (orders) with static records (inventory balance).
  • It is important to recognize what your data describes and its time granularity.

Example 1: Loading Retail Sales Data#

  • Our first example uses historical sales data from a real online retail store.
  • We will preview the data and discuss what each column means.
# Load retail sales dataset from UCI repository
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df_sales = pd.read_excel(url, sheet_name='Year 2010-2011')
df_sales['InvoiceDate'] = pd.to_datetime(df_sales['InvoiceDate'])
print(df_sales.shape)
print(df_sales.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: Inspecting an Inventory and Operations Log#

  • Next, we examine manufacturing operations data with inventory actions.
  • Operations logs help us trace resource usage and bottlenecks.
dataset = openml.datasets.get_dataset(43900)
X_ops, y_ops, _, _ = dataset.get_data(dataset_format='dataframe')
df_ops = X_ops.copy() if y_ops is None else pd.concat([X_ops, y_ops], axis=1)
print(df_ops.shape)
print(df_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  

Example 3: Loading a Supplier Performance Dataset#

  • Supplier performance metrics are critical to procurement and logistics.
  • Let us load and review a real supplier-focused operations dataset.
dataset = openml.datasets.get_dataset(42125)
df_suppliers, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(df_suppliers.shape)
print(df_suppliers.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: Viewing Order Fulfillment Data#

  • Order fulfillment data tracks each stage of the customer order lifecycle.
  • These datasets allow us to study lead times and delivery performance.
url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
df_orders = pd.read_csv(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 5: Exploring Sales Forecasting Data#

  • Sales forecasting datasets help predict future demand based on historic trends.
  • Using real-world data, we can practice time-series aggregations.
dataset = openml.datasets.get_dataset(4549)
X_sales, y_sales, _, _ = dataset.get_data(dataset_format='dataframe')
df_forecast = pd.concat([X_sales, y_sales], axis=1)
print(df_forecast.shape)
print(df_forecast.head(3))
(583250, 78)
   NCD_0  NCD_1  NCD_2  NCD_3  NCD_4  NCD_5  NCD_6  AI_0  AI_1  AI_2  ...  \
0    0.0    2.0    0.0    0.0    1.0    1.0    1.0   0.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  ...   
2    1.0    0.0    0.0    0.0    0.0    4.0    1.0   1.0   0.0   0.0  ...   

   ADL_5  ADL_6  NAD_0  NAD_1  NAD_2  NAD_3  NAD_4  NAD_5  NAD_6  Annotation  
0    1.0    1.0    0.0    2.0    0.0    0.0    1.0    1.0    1.0         0.0  
1    0.0    1.0    2.0    1.0    0.0    0.0    0.0    0.0    4.0         0.5  
2    1.0    1.0    1.0    0.0    0.0    0.0    0.0    4.0    1.0         0.0  

[3 rows x 78 columns]

Intermediate 1: Counting Stock Keeping Units (SKUs)#

  • Understanding how many unique products you stock is a basic but crucial supply chain view.
  • Let us calculate the number of unique SKUs in the sales data.
sku_count = df_sales['StockCode'].nunique()
print(f'Total unique SKUs in sales data: {sku_count}')
Total unique SKUs in sales data: 4070

Intermediate 2: Calculating Daily Total Demand#

  • Summing up sales quantities by day gives a core view of demand trends.
  • Let us aggregate the retail data by invoice date.
df_sales['date'] = df_sales['InvoiceDate'].dt.date
daily_demand = df_sales.groupby('date')['Quantity'].sum()
print(daily_demand.head())
date
2010-12-01    26814
2010-12-02    21023
2010-12-03    14830
2010-12-05    16395
2010-12-06    21419
Name: Quantity, dtype: int64

Intermediate 3: Measuring Average Lead Time for Fulfillment#

  • Lead time is the delay between receiving an order and fulfilling it.
  • We will measure average delivery lead time using our order fulfillment data.
mask = (~df_orders['order_purchase_timestamp'].isnull()) & (~df_orders['order_delivered_customer_date'].isnull())
df_orders['lead_time'] = (df_orders['order_delivered_customer_date'] - df_orders['order_purchase_timestamp']).dt.days
avg_lead_time = df_orders.loc[mask, 'lead_time'].mean()
print(f'Average delivery lead time: {avg_lead_time:.2f} days')
Average delivery lead time: 12.09 days

Advanced 1: Building a Pivot Table Across Key Supply Chain Dimensions#

  • A pivot table helps summarize by multiple dimensions, such as country and product.
  • Let us create a summary of total quantities sold by country and product.
pivot = df_sales.pivot_table(index='Country', columns='StockCode', values='Quantity', aggfunc='sum', fill_value=0)
print(pivot.iloc[:5, :5])
StockCode  10002  10080  10120  10125  10133
Country                                     
Australia      0      0      0      0      0
Austria        0      0      0      0      0
Bahrain        0      0      0      0      0
Belgium        0      0      0      0      0
Brazil         0      0      0      0      0

Advanced 2: Generating a Supplier Performance Ranking#

  • Ranking suppliers by performance metrics lets you target improvements.
  • Here, let us rank suppliers by gross pay received.
if '2016_gross_pay_received' in df_suppliers.columns:
    ranking = df_suppliers[['full_name', '2016_gross_pay_received']].sort_values(by='2016_gross_pay_received', ascending=False)
    print(ranking.head(5))
else:
    print('Metric not available in this dataset.')
                full_name  2016_gross_pay_received
2665   Firestine, Timothy                313700.42
7948     Stanton, Patrick                251849.97
5244      Manger, John T.                248424.60
7384  Sanchez, Raymond R.                244334.74
2581   Farber, Stephen B.                243489.38

Advanced 3: Visualizing Seasonality in Sales (with pandas plotting)#

  • Visualizing time trends helps identify seasonality in sales demand.
  • Let us plot monthly retail demand using a built-in pandas plot.
import matplotlib.pyplot as plt
df_sales['month'] = df_sales['InvoiceDate'].dt.to_period('M')
monthly_demand = df_sales.groupby('month')['Quantity'].sum()
monthly_demand.plot(kind='line', marker='o', title='Monthly Retail Demand')
plt.xlabel('Month')
plt.ylabel('Total Quantity Sold')
plt.tight_layout()
plt.show()
No description has been provided for this image

Error Handling: Missing Dates in Order Data#

  • Sometimes, delivery dates or purchase timestamps are missing in real order records.
  • We should always check for missing or invalid date fields before analysis.
missing_dates = df_orders['order_delivered_customer_date'].isnull().sum()
print(f'Number of orders missing delivered date: {missing_dates}')
Number of orders missing delivered date: 2965

Error Handling: Incorrect Joins Produce NaN Values#

  • Common joining mistakes in supply chain views lead to missing connections (NaN).
  • We will illustrate a join between sales and supplier data on an incorrect key.
# This join is purposely incorrect for illustration
df_bad_join = pd.merge(df_sales, df_suppliers, left_on='StockCode', right_on='full_name', how='left')
nan_count = df_bad_join['full_name'].isnull().sum()
print(f'Rows with missing supplier after join: {nan_count}')
Rows with missing supplier after join: 541910

Error Handling: Misinterpreting Lead Times or Quantities#

  • Sometimes, negative lead times or quantities signal data entry errors.
  • Let us check for negative or zero values in these fields.
num_negative_qty = (df_sales['Quantity'] <= 0).sum()
num_negative_lt = (df_orders.get('lead_time', pd.Series()) < 0).sum()
print(f'Negative or zero sales quantities: {num_negative_qty}')
print(f'Negative lead times detected: {num_negative_lt}')
Negative or zero sales quantities: 10624
Negative lead times detected: 0

Best Practices: Using Group-By for Supply Chain Dashboards#

  • Group-by is a workhorse operation for summarizing all supply chain data.
  • Let us group demand by country and month for higher-level insight.
agg = df_sales.groupby(['Country', 'month'])['Quantity'].sum().reset_index()
print(agg.head())
     Country    month  Quantity
0  Australia  2010-12       454
1  Australia  2011-01      5644
2  Australia  2011-02      8659
3  Australia  2011-03     10329
4  Australia  2011-04       117

Best Practices: Calculating Fill Rate as an Operations KPI#

  • Fill rate measures the percentage of demand satisfied by on-time supply.
  • Let us demonstrate a simplified fill rate calculation with sales data.
# For demo, define delivered as Quantity > 0; calculate percent of positive quantity lines
filled_orders = (df_sales['Quantity'] > 0).sum()
total_orders = df_sales.shape[0]
fill_rate = 100 * filled_orders / total_orders
print(f'Fill rate (proxy): {fill_rate:.2f}%')
Fill rate (proxy): 98.04%

End-to-End Problem: From Raw Sales Data to a 3-Level Summary Table#

  • Let us solve a full supply chain analytics case: summarize sales by country, month, and product.
  • Our final table will help understand demand patterns at several levels.
summary = df_sales.groupby(['Country', 'month', 'StockCode'])['Quantity'].sum().reset_index()
summary = summary.sort_values(['Country', 'month', 'Quantity'], ascending=[True, True, False])
print(summary.head(10))
      Country    month StockCode  Quantity
21  Australia  2010-12     22915       120
26  Australia  2010-12     79067        50
11  Australia  2010-12     22196        48
10  Australia  2010-12     22195        36
17  Australia  2010-12     22567        24
24  Australia  2010-12     22953        24
25  Australia  2010-12     48138        20
4   Australia  2010-12     21791        12
12  Australia  2010-12     22219        12
15  Australia  2010-12     22555        12

Recap: Building Core Views in Supply Chain & Operations Analytics#

  • We used real datasets to learn how to load, clean, aggregate, and summarize supply chain data.
  • You now know how to build operational reports, debug common errors, and answer core business questions.
  • Practice these skills on your own datasetsand subscribe to our YouTube channel for more advanced tutorials!

Found this useful?

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