Mathew K Analytics

Lesson 14 · Supply Chain Operations Analytics

SQL vs Pandas for Operational Reporting: Key Differences & Best Practices

In this lesson, we will explore how to answer key supply chain operations questions using both SQL and Python pandas. We will work with real-world datasets…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

SQL vs Pandas for Operational Reporting in Supply Chains#

  • In this lesson, we will explore how to answer key supply chain operations questions using both SQL and Python pandas.
  • We will work with real-world datasets such as retail demand, fulfillment, and supplier performance.
  • You will learn how to translate classic operational reporting tasks from SQL to pandas.
  • The lesson covers beginner tasks, intermediate analyses, and advanced operational scenarios.
  • You will learn best practices, debugging techniques, and common pitfalls for supply chain analytics reporting.
import pandas as pd
import openml
import warnings
warnings.filterwarnings('ignore')

Understanding the Data in Supply Chain Operations#

  • Operational supply chain datasets typically include demand, orders, fulfillment, inventory, and supplier information.
  • Data is often organized in transaction records, with time-stamped events and quantities.
  • It is common to make mistakes such as misinterpreting column meanings, missing date formats, or misaligning joins.
  • Accurate reporting depends on correctly understanding each field and how it connects to business operations.
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  
openml_id = 42125
dataset = openml.datasets.get_dataset(openml_id)
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  
url_orders = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
df_orders = pd.read_csv(url_orders)
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  

Beginner Task 1: Counting Orders by Country with SQL (as a reference)#

  • In SQL, grouping and counting is common for demand or sales reports.
  • SQL example: SELECT Country, COUNT(*) FROM sales GROUP BY Country;
  • In pandas, we use groupby and size for the same result.
country_counts = df_retail.groupby('Country').size().sort_values(ascending=False)
print(country_counts.head(10))
Country
United Kingdom    495478
Germany             9495
France              8558
EIRE                8196
Spain               2533
Netherlands         2371
Belgium             2069
Switzerland         2002
Portugal            1519
Australia           1259
dtype: int64

Beginner Task 2: Filtering Records (WHERE Clause Equivalent)#

  • SQL uses WHERE for conditional filters, such as WHERE Country = 'United Kingdom'.
  • In pandas, logical masks or .query are used.
uk_sales = df_retail[df_retail['Country'] == 'United Kingdom']
print(uk_sales.shape)
print(uk_sales.head(2))
(495478, 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 Task 3: Calculating Total Revenue per Transaction#

  • SQL uses expressions in SELECT, e.g., Quantity * Price as Revenue.
  • In pandas, create a new column as a computed field.
df_retail['Revenue'] = df_retail['Quantity'] * df_retail['Price']
print(df_retail[['Quantity', 'Price', 'Revenue']].head(3))
   Quantity  Price  Revenue
0         6   2.55    15.30
1         6   3.39    20.34
2         8   2.75    22.00

Intermediate Task 1: Aggregating Revenue by Month and Country#

  • SQL often uses GROUP BY EXTRACT(MONTH FROM date), Country.
  • In pandas, use groupby with .dt accessor for datetime fields.
df_retail['Month'] = df_retail['InvoiceDate'].dt.month
monthly_country = df_retail.groupby(['Month', 'Country'])['Revenue'].sum().unstack().fillna(0)
print(monthly_country.iloc[:3, :5])
Country  Australia  Austria  Bahrain  Belgium  Brazil
Month                                                
1          9017.71     0.00  -205.74  1154.05     0.0
2         14627.47   518.36     0.00  2161.32     0.0
3         17055.29  1708.12     0.00  3333.58     0.0
monthly_revenue = df_retail.groupby('Month')['Revenue'].sum()
print(monthly_revenue)
Month
1      560000.260
2      498062.650
3      683267.080
4      493207.121
5      723333.510
6      691123.120
7      681300.111
8      682680.510
9     1019687.622
10    1070704.670
11    1461756.250
12    1182643.030
Name: Revenue, dtype: float64

Intermediate Task 2: Calculating Average Lead Time using Order Data#

  • SQL typically uses DATEDIFF or TIMESTAMPDIFF on date columns.
  • In pandas, subtract datetime columns and calculate mean.
lead_time = (df_orders['order_delivered_customer_date'] - df_orders['order_purchase_timestamp']).dt.days
mean_lead_time = lead_time.mean()
print(f'Average lead time in days: {mean_lead_time:.2f}')
Average lead time in days: 12.09
lead_time_valid = lead_time.dropna()
print('Percentage of orders with valid lead times:', len(lead_time_valid) / len(lead_time) * 100)
Percentage of orders with valid lead times: 97.01833247855512

Intermediate Task 3: Combining Supplier and Order Insights#

  • SQL would use JOIN statements to combine purchase data and supplier details.
  • In pandas, this is achieved using merge().
# Note: For demonstration, let us assume both DataFrames have a 'customer_id' column for joining
common_customers = set(df_orders['customer_id']).intersection(set(df_supplier['full_name']))
print('Common customer count (as fake join key):', len(common_customers))
Common customer count (as fake join key): 0

Advanced Task 1: Detecting Negative Quantities and Returns#

  • In retail operations, negative quantities often indicate product returns.
  • SQL: SELECT * FROM sales WHERE Quantity < 0;
  • In pandas: sales[sales.Quantity < 0]
returns = df_retail[df_retail['Quantity'] < 0]
print('Number of return transactions:', len(returns))
print(returns[['Invoice', 'Quantity', 'Revenue']].head(3))
Number of return transactions: 10624
     Invoice  Quantity  Revenue
141  C536379        -1   -27.50
154  C536383        -1    -4.65
235  C536391       -12   -19.80

Advanced Task 2: Calculating Fill Rate Over Time#

  • Fill Rate is a key operational KPI for fulfillment performance.
  • Fill Rate = (delivered orders) / (total orders) for a period.
  • SQL would GROUP BY time period and SUM CASE WHEN delivered.
  • In pandas, use groupby and logical conditions.
df_orders['month'] = df_orders['order_purchase_timestamp'].dt.to_period('M')
delivered = df_orders['order_status'] == 'delivered'
monthly_fill_rate = delivered.groupby(df_orders['month']).mean()
print(monthly_fill_rate.head())
month
2016-09    0.250000
2016-10    0.817901
2016-12    1.000000
2017-01    0.937500
2017-02    0.928652
Freq: M, Name: order_status, dtype: float64

Advanced Task 3: Supplier Cost Efficiency Insight#

  • Typically, SQL queries rank suppliers by cost or efficiency.
  • In pandas, sort supplier metrics and filter top performers.
if 'current_annual_salary' in df_supplier.columns:
    top_cost_suppliers = df_supplier.sort_values('current_annual_salary').head(5)
    print(top_cost_suppliers[['full_name', 'current_annual_salary']])
                 full_name  current_annual_salary
1459     Cherry, Levora D.                9196.00
5707        Meyer, Paul D.               11147.24
7566  Sekhsaria, Vineet K.               13244.50
165        Allen, Tyler H.               14976.00
7576        Seo, Steven H.               15577.66

Error Handling 1: Handling Missing Dates for Lead Time Calculation#

  • Missing date fields break TIMESTAMPDIFF or subtraction in SQL and pandas.
  • Pandas throws errors or NaN values when dates are missing.
missing_delivery = df_orders['order_delivered_customer_date'].isnull().sum()
missing_purchase = df_orders['order_purchase_timestamp'].isnull().sum()
print(f'Missing delivered dates: {missing_delivery}')
print(f'Missing purchase dates: {missing_purchase}')
Missing delivered dates: 2965
Missing purchase dates: 0

Error Handling 2: Join Errors and Data Misalignment#

  • SQL will fail or produce empty joins when keys are inconsistent.
  • In pandas, merge() can create many NaNs and miss expected rows.
  • Always check the key uniqueness and types before joining.
unique_order_ids = df_orders['order_id'].nunique()
total_order_rows = len(df_orders)
print(f'Orders: {unique_order_ids} unique order_ids vs {total_order_rows} rows.')
Orders: 99441 unique order_ids vs 99441 rows.

Error Handling 3: Quantity and Lead Time Misinterpretation#

  • Negative quantities often mean returns, not demand.
  • SQL and pandas both include these in aggregates unless filtered.
  • Always clarify business definitions before calculation.
total_quantity = df_retail['Quantity'].sum()
demand_quantity = df_retail[df_retail['Quantity'] > 0]['Quantity'].sum()
returns_quantity = df_retail[df_retail['Quantity'] < 0]['Quantity'].sum()
print(f'Total quantity (including returns): {total_quantity}')
print(f'Demand (positive only): {demand_quantity}')
print(f'Returns (negative only): {returns_quantity}')
Total quantity (including returns): 5176451
Demand (positive only): 5660982
Returns (negative only): -484531

Best Practices: Aggregation and KPI Calculation Patterns#

  • Always use explicit grouping, clean column names, and conversions for reporting.
  • Validate aggregations by checking shapes and sample outputs.
  • Time-series operations should use datetime columns with proper frequency.
  • KPIs must be defined carefully and checked for degeneracies (NaN, zeros, missing).
kpi = monthly_revenue.max() / monthly_revenue.mean()
print(f'Maximum-to-average monthly revenue ratio: {kpi:.2f}')
Maximum-to-average monthly revenue ratio: 1.80

Best Practices: Time Series Grouping#

  • For operational analytics, group by period using .to_period or .dt accessor.
  • Always ensure you group on business-relevant time boundaries (month, week, day).
  • Double-check if you are grouping on the correct time column.
daily_orders = df_orders.groupby(df_orders['order_purchase_timestamp'].dt.date).size()
print(daily_orders.head())
order_purchase_timestamp
2016-09-04    1
2016-09-05    1
2016-09-13    1
2016-09-15    1
2016-10-02    1
dtype: int64

Example: End-to-End Operational Metric Lead Time Variability#

  • We will use order data to calculate the standard deviation of delivery lead times each month.
  • This metric allows an operations manager to detect unstable fulfillment processes.
lead_time = (df_orders['order_delivered_customer_date'] - df_orders['order_purchase_timestamp']).dt.days
df_orders['lead_time'] = lead_time
monthly_lead_time_std = df_orders.groupby('month')['lead_time'].std()
print(monthly_lead_time_std.head())
month
2016-09          NaN
2016-10    12.069604
2016-12          NaN
2017-01     9.973038
2017-02    10.883619
Freq: M, Name: lead_time, dtype: float64

Summary: SQL vs Pandas in Supply Chain Operational Analytics#

  • Both SQL and pandas offer powerful tools for operational reporting.
  • SQL excels at batch queries and data warehousing. Pandas enables flexible data cleaning and quick analytics.
  • Knowing both approaches lets you choose the right tool for each operational analytics task.
  • Want more? Watch our YouTube playlist for live demonstrations and expert tips!

Found this useful?

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