Mathew K Analytics

Lesson 30 · Supply Chain Operations Analytics

Supplier Performance Metrics in Supply Chain Analytics

In this lesson, we will learn how to analyze and measure supplier performance using real-world supply chain data. Supplier evaluation is critical for…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Supplier Performance Metrics in Supply Chain Analytics#

  • In this lesson, we will learn how to analyze and measure supplier performance using real-world supply chain data.
  • Supplier evaluation is critical for managing procurement risk and ensuring reliable operations.
  • You will build practical, hands-on skills for extracting, interpreting, and applying supplier KPIs from public business datasets.
  • We will use Python, pandas, and real supplier datasets to assess delivery, cost, and quality metrics.
import pandas as pd
import numpy as np
import openml
import warnings
warnings.filterwarnings('ignore')

Understanding Supplier Performance Data#

  • Supplier performance analysis typically uses data about deliveries, costs, and supplier reliability.
  • Your data may include columns for order status, fulfillment times, costs, and supplier identifiers.
  • Operations datasets can be large and messy, with missing or ambiguous data.
  • Beginners often overlook issues such as date parsing, correct units, or duplicate supplier records.
  • Clean, structured data enables trusted supply chain decisions.
dataset = openml.datasets.get_dataset(42125)
df, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(df.shape)
print(df.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  

Data Structure and Key Columns#

  • Our dataset contains supplier names, salary information, hiring dates, and job titles.
  • For supplier metrics, we often select columns representing performance, time periods, and costs.
  • Beginners should always check column data types and standardize supplier names.
print(df.columns.tolist())
print(df[['full_name', 'department', '2016_gross_pay_received', 'date_first_hired']].head(5))
['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']
            full_name department  2016_gross_pay_received date_first_hired
0      Aarhus, Pam J.        POL                 71225.98       09/22/1986
1     Aaron, David J.        POL                103088.48       09/12/1988
2    Aaron, Marsha M.        HHS                107000.24       11/19/1989
3  Ababio, Godfred A.        COR                 57819.04       05/05/2014
4      Ababu, Essayas        HCA                 95815.17       03/05/2007
df = df.rename(columns={'full_name': 'supplier_name'})
df['date_first_hired'] = pd.to_datetime(df['date_first_hired'], errors='coerce')
print(df[['supplier_name', '2016_gross_pay_received', 'date_first_hired']].head(5))
        supplier_name  2016_gross_pay_received date_first_hired
0      Aarhus, Pam J.                 71225.98       1986-09-22
1     Aaron, David J.                103088.48       1988-09-12
2    Aaron, Marsha M.                107000.24       1989-11-19
3  Ababio, Godfred A.                 57819.04       2014-05-05
4      Ababu, Essayas                 95815.17       2007-03-05

Beginner Example 1: Counting Unique Suppliers#

  • A basic supplier metric is simply how many unique suppliers operate in your supply chain.
  • Use pandas to compute unique supplier counts for early insights.
unique_suppliers = df['supplier_name'].nunique()
print(f'Number of unique suppliers: {unique_suppliers}')
Number of unique suppliers: 9222

Beginner Example 2: Average Supplier Cost#

  • Knowing the average cost per supplier helps budgeting and procurement planning.
  • We will calculate the mean gross pay received as a proxy for annual supplier cost.
mean_cost = df['2016_gross_pay_received'].mean()
print(f'Average 2016 supplier cost: ${mean_cost:,.2f}')
Average 2016 supplier cost: $79,504.04

Beginner Example 3: Suppliers by Department#

  • Operations often need to view supplier support by department or location.
  • Pivot tables help you summarize suppliers by organization unit quickly.
suppliers_per_dept = df.groupby('department')['supplier_name'].nunique().sort_values(ascending=False)
print(suppliers_per_dept.head(10))
department
POL    1844
HHS    1556
FRS    1311
DOT    1226
COR     482
DLC     408
DGS     404
LIB     375
DPS     221
SHF     180
Name: supplier_name, dtype: int64

Intermediate Example 1: Supplier Tenure Analysis#

  • Assessing how long suppliers have supported your organization can reveal reliability.
  • We calculate supplier tenure by comparing hire date to end of 2016.
reference_date = pd.to_datetime('2016-12-31')
df['tenure_years'] = (reference_date - df['date_first_hired']).dt.days / 365.25
print(df[['supplier_name', 'tenure_years']].head(5))
        supplier_name  tenure_years
0      Aarhus, Pam J.     30.275154
1     Aaron, David J.     28.301164
2    Aaron, Marsha M.     27.115674
3  Ababio, Godfred A.      2.658453
4      Ababu, Essayas      9.826146

Intermediate Example 2: High-Cost Supplier Identification#

  • It is important to identify suppliers with the highest costs for negotiation or risk review.
  • We will rank the suppliers by their total gross pay received.
top_cost_suppliers = df.groupby('supplier_name')['2016_gross_pay_received'].sum().sort_values(ascending=False)
print(top_cost_suppliers.head(5))
supplier_name
Firestine, Timothy     313700.42
Stanton, Patrick       251849.97
Manger, John T.        248424.60
Sanchez, Raymond R.    244334.74
Farber, Stephen B.     243489.38
Name: 2016_gross_pay_received, dtype: float64

Intermediate Example 3: Supplier Cost per Department#

  • Comparing cost structure by department highlights which units rely most on suppliers.
  • We calculate and sort cost by department using groupby.
cost_per_dept = df.groupby('department')['2016_gross_pay_received'].sum().sort_values(ascending=False)
print(cost_per_dept.head(10))
department
POL    1.457417e+08
FRS    1.244227e+08
HHS    1.099545e+08
DOT    8.299588e+07
COR    4.268471e+07
DGS    3.380501e+07
DLC    2.189077e+07
DPS    1.976900e+07
LIB    1.961441e+07
DTS    1.625986e+07
Name: 2016_gross_pay_received, dtype: float64

Advanced Example 1: Supplier Diversity by Department#

  • Diversity of supplier base can reduce supply chain risk.
  • We calculate the proportion of a department's supplier spending with the top supplier.
dept_supplier_spend = df.groupby(['department', 'supplier_name'])['2016_gross_pay_received'].sum().reset_index()
top_supplier_fraction = dept_supplier_spend.groupby('department')['2016_gross_pay_received'].max() / dept_supplier_spend.groupby('department')['2016_gross_pay_received'].sum()
print(top_supplier_fraction.sort_values(ascending=False).head(10))
department
MPB    0.869730
BOA    0.535201
ECM    0.457899
ZAH    0.374574
IGR    0.321906
OIG    0.253607
HRC    0.211489
OAG    0.199922
OLO    0.141832
NDA    0.138193
Name: 2016_gross_pay_received, dtype: float64

Advanced Example 2: Trend in Supplier Growth#

  • Tracking supplier additions over time can reveal trends in sourcing and procurement strategy.
  • We will compute number of suppliers hired each year.
df['year_first_hired'] = pd.to_numeric(df['year_first_hired'], errors='coerce')
supplier_growth = df.groupby('year_first_hired')['supplier_name'].nunique()
print(supplier_growth.sort_index())
year_first_hired
1965      1
1967      3
1968      2
1969      1
1970      2
1971      1
1972      8
1973      3
1974      9
1975      3
1976      4
1977     16
1978     25
1979     31
1980     25
1981     21
1982     34
1983     22
1984     52
1985     91
1986     93
1987    104
1988    212
1989    185
1990    194
1991     59
1992    117
1993    124
1994    246
1995    182
1996    121
1997    204
1998    239
1999    292
2000    336
2001    379
2002    372
2003    284
2004    322
2005    371
2006    482
2007    478
2008    425
2009    134
2010    158
2011    175
2012    444
2013    586
2014    637
2015    398
2016    521
Name: supplier_name, dtype: int64

Error Handling Example: Missing or Invalid Dates#

  • Real world datasets often contain missing or malformed dates.
  • We will count records with invalid date_first_hired fields.
missing_dates = df['date_first_hired'].isna().sum()
print(f'Number of suppliers with missing or invalid hire dates: {missing_dates}')
Number of suppliers with missing or invalid hire dates: 0

Error Handling Example: Duplicate Supplier Records#

  • Duplicates in supplier names can distort metrics such as unique supplier counts.
  • We demonstrate how to check for duplicate entries.
duplicates = df.duplicated(subset=['supplier_name', 'department', 'date_first_hired'], keep=False)
print(f'Number of duplicate supplier records: {duplicates.sum()}')
Number of duplicate supplier records: 0

Error Handling Example: Inconsistent Columns or Units#

  • Supplier cost data may be unintentionally stored as strings or with currency symbols.
  • We check for non-numeric entries and convert costs to float.
df['2016_gross_pay_received'] = pd.to_numeric(df['2016_gross_pay_received'], errors='coerce')
print(f'Number of invalid supplier cost entries: {df["2016_gross_pay_received"].isna().sum()}')
Number of invalid supplier cost entries: 100

Best Practice: KPI Calculation and Reporting#

  • Reliable supply analytics start with clean, aggregated data.
  • We will create a simple cost and tenure dashboard per supplier.
kpi_df = df.groupby('supplier_name').agg({
    '2016_gross_pay_received': 'sum',
    'tenure_years': 'max'
}).rename(columns={'2016_gross_pay_received': 'total_cost', 'tenure_years': 'tenure_years'})
kpi_df = kpi_df.sort_values(by='total_cost', ascending=False)
print(kpi_df.head(10))
                     total_cost  tenure_years
supplier_name                                
Firestine, Timothy    313700.42     37.097878
Stanton, Patrick      251849.97     26.844627
Manger, John T.       248424.60     12.911704
Sanchez, Raymond R.   244334.74     30.913073
Farber, Stephen B.    243489.38     25.160849
Ahluwalia, Uma S.     236377.00      9.880903
Kennedy, David J.     235398.89     24.123203
Blake, Jason D.       233223.11     17.377139
Porter, William F.    232367.77     14.327173
Roshdieh, Al R.       231316.12     27.649555

Best Practice: Time Series Grouping#

  • Often, you will need to summarize supplier spend or activity over time.
  • We consider cost by year to spot trends and seasonal patterns.
annual_cost = df.groupby('year_first_hired')['2016_gross_pay_received'].sum().sort_index()
print(annual_cost.tail(10))
year_first_hired
2007    35938011.78
2008    29545131.75
2009     9721284.52
2010    10462165.92
2011    11128708.99
2012    28863997.12
2013    36723421.80
2014    39106322.42
2015    23071834.08
2016    11478001.56
Name: 2016_gross_pay_received, dtype: float64

End-to-End Example: Supplier Risk Heatmap#

  • Let us identify top suppliers by cost and tenure for risk-focused supply chain management.
  • We will export these results as a table.
risk_table = kpi_df.reset_index().sort_values(['total_cost', 'tenure_years'], ascending=[False, False]).head(20)
risk_table.to_csv('top_supplier_risk_table.csv', index=False)
print(risk_table)
          supplier_name  total_cost  tenure_years
0    Firestine, Timothy   313700.42     37.097878
1      Stanton, Patrick   251849.97     26.844627
2       Manger, John T.   248424.60     12.911704
3   Sanchez, Raymond R.   244334.74     30.913073
4    Farber, Stephen B.   243489.38     25.160849
5     Ahluwalia, Uma S.   236377.00      9.880903
6     Kennedy, David J.   235398.89     24.123203
7       Blake, Jason D.   233223.11     17.377139
8    Porter, William F.   232367.77     14.327173
9       Roshdieh, Al R.   231316.12     27.649555
10  Hughes, Jennifer A.   228191.07     31.750856
11    Potter, Thomas A.   226644.84     19.121150
12     Ruff, Gregory B.   224999.69     36.284736
13     Jones, Stacey R.   222712.42     26.346338
14     Segal, Harash N.   222650.51     31.211499
15     Watkins, Eric J.   222279.31     21.500342
16      Jones, Diane R.   221780.86     30.830938
17    Haynes, Terryl A.   220954.34     21.938398
18   Wenger, Melanie L.   219996.81     14.064339
19      Hansen, Marc P.   217500.21     32.459959
 

Found this useful?

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