Mathew K Analytics

Lesson 37 · Python for Retail E-commerce Analytics

Master Average Order Value & Conversion Metrics with Python for Retail E-commerce

In this lesson, we will solve real-world business problems related to order value and conversion rates. Understanding Average Order Value (AOV) helps…

⬇ 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

Average Order Value and Conversion Metrics in Retail and E-Commerce#

  • In this lesson, we will solve real-world business problems related to order value and conversion rates.
  • Understanding Average Order Value (AOV) helps businesses optimize pricing strategies and promotions.
  • Conversion metrics show how effectively a store turns visitors or baskets into completed sales.
  • You will learn how to compute AOV and conversion rates, diagnose calculation mistakes, and apply best practices.
  • By the end, you will generate actionable insights for sales and marketing decision-making.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Core retail analytics concepts for AOV and conversion metrics#

  • Retail transaction datasets record every product purchased within an order or basket.
  • Key columns include Invoice, Product, Price, Quantity, Order Timestamp, and Customer.
  • Sales metrics like revenue are calculated as Quantity times Price per product line.
  • Beginners often confuse order-level and product-level aggregations.
  • Mistakes may happen if you double-count transactions or mix product-level with order-level metrics.
  • Understanding groupings is critical for correct AOV and conversion analysis.
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df = pd.read_excel(url, sheet_name='Year 2010-2011')
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
print(df.shape)
print(df.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  
df['Revenue'] = df['Quantity'] * df['Price']
print(df[['Invoice', 'StockCode', 'Quantity', 'Price', 'Revenue']].head(3))
  Invoice StockCode  Quantity  Price  Revenue
0  536365    85123A         6   2.55    15.30
1  536365     71053         6   3.39    20.34
2  536365    84406B         8   2.75    22.00
num_invoices = df['Invoice'].nunique()
total_revenue = df['Revenue'].sum()
print(f'Number of unique orders (invoices): {num_invoices}')
print(f'Total revenue: {total_revenue:.2f}')
Number of unique orders (invoices): 25900
Total revenue: 9747765.93
invoice_totals = df.groupby('Invoice')['Revenue'].sum()
aov = invoice_totals.mean()
print(f'Average Order Value (AOV): {aov:.2f}')
Average Order Value (AOV): 376.36
country_invoice_totals = df.groupby(['Country', 'Invoice'])['Revenue'].sum().reset_index()
country_aov = country_invoice_totals.groupby('Country')['Revenue'].mean().sort_values(ascending=False)
print(country_aov.head(5))
Country
Netherlands    2818.431089
Australia      1986.627101
Lebanon        1693.880000
Japan          1262.165000
Brazil         1143.600000
Name: Revenue, dtype: float64
high_value_orders = invoice_totals[invoice_totals > aov * 2]
print(f'Number of orders with high value: {high_value_orders.count()}')
Number of orders with high value: 2781
np.random.seed(42)
n_orders = 1000
order_ids = list(range(1, n_orders+1))
customer_ids = np.random.randint(1000,1500, n_orders)
product_ids = np.random.randint(1001,1100, n_orders)
quantities = np.random.randint(1, 5, n_orders)
order_dates = pd.date_range('2023-01-01', periods=n_orders, freq='h')
df_orders = pd.DataFrame({'OrderID':order_ids,'CustomerID':customer_ids,'ProductID':product_ids,'Quantity':quantities,'OrderDate':order_dates})
product_prices = np.round(np.random.uniform(5,500,100),2)
product_catalog = pd.DataFrame({'ProductID':range(1001,1101),'Price':product_prices})
df_orders = df_orders.merge(product_catalog, on='ProductID', how='left')
df_orders['Revenue'] = df_orders['Quantity'] * df_orders['Price']
print(df_orders.head(3))
   OrderID  CustomerID  ProductID  Quantity           OrderDate   Price  \
0        1        1102       1049         3 2023-01-01 00:00:00  222.56   
1        2        1435       1011         1 2023-01-01 01:00:00  108.21   
2        3        1348       1085         3 2023-01-01 02:00:00  414.63   

   Revenue  
0   667.68  
1   108.21  
2  1243.89  
synthetic_invoice_vals = df_orders.groupby('OrderID')['Revenue'].sum()
aov_syn = synthetic_invoice_vals.mean()
print(f'Synthetic dataset AOV: {aov_syn:.2f}')
Synthetic dataset AOV: 605.40
orders_per_customer = df_orders.groupby('CustomerID')['OrderID'].nunique()
print(orders_per_customer.describe())
count    423.000000
mean       2.364066
std        1.369898
min        1.000000
25%        1.000000
50%        2.000000
75%        3.000000
max        8.000000
Name: OrderID, dtype: float64
np.random.seed(42)
website_visits = 5000
orders = len(df_orders['OrderID'].unique())
conversion_rate = orders / website_visits
print(f'Estimated conversion rate: {conversion_rate:.2%}')
Estimated conversion rate: 20.00%
df_orders['Hour'] = df_orders['OrderDate'].dt.hour
hourly_aov = df_orders.groupby('Hour').apply(lambda x: x.groupby('OrderID')['Revenue'].sum().mean())
print(hourly_aov.head())
Hour
0    638.330476
1    493.646905
2    707.363095
3    549.311905
4    591.506905
dtype: float64
product_catalog['Category'] = np.random.choice(['Electronics','Clothing','Home','Sports','Beauty'], size=len(product_catalog), replace=True)
df_orders = df_orders.merge(product_catalog[['ProductID','Category']], on='ProductID', how='left')
cat_aov = df_orders.groupby('Category').apply(lambda x: x.groupby('OrderID')['Revenue'].sum().mean())
print(cat_aov.sort_values(ascending=False))
Category
Home           708.530000
Beauty         657.139585
Clothing       592.432139
Sports         567.453801
Electronics    528.746705
dtype: float64
hours = df_orders['Hour'].unique()
np.random.seed(42)
visits_per_hour = dict(zip(hours, np.random.randint(150, 350, len(hours))))
orders_per_hour = df_orders.groupby('Hour')['OrderID'].nunique().to_dict()
hourly_conversion = {h: orders_per_hour.get(h,0)/visits_per_hour[h] for h in hours}
sorted_hourly = dict(sorted(hourly_conversion.items(), key=lambda x:x[1], reverse=True))
print('Hourly conversion rates:')
for h, rate in list(sorted_hourly.items())[:5]: print(f'Hour {h}: {rate:.2%}')
Hourly conversion rates:
Hour 19: 27.15%
Hour 3: 25.61%
Hour 7: 24.71%
Hour 22: 21.93%
Hour 18: 20.30%
customer_totals = df_orders.groupby('CustomerID').agg({'OrderID':'nunique', 'Revenue':'sum'})
customer_totals['AOV'] = customer_totals['Revenue'] / customer_totals['OrderID']
segments = pd.qcut(customer_totals['AOV'], 4, labels=['Low','Medium','High','Very High'])
customer_totals['AOV_Segment'] = segments
print(customer_totals['AOV_Segment'].value_counts())
AOV_Segment
Low          108
Very High    106
High         105
Medium       104
Name: count, dtype: int64
outliers = customer_totals[customer_totals['AOV'] > customer_totals['AOV'].mean() + 2 * customer_totals['AOV'].std()]
print(f'Number of AOV outlier customers: {outliers.shape[0]}')
print(outliers[['AOV']].head())
Number of AOV outlier customers: 21
                 AOV
CustomerID          
1005        1565.920
1017        1840.400
1059        1572.235
1120        1488.360
1148        1488.360
neg_orders = df[df['Revenue'] < 0]
print(f'Orders with negative revenue: {len(neg_orders)}')
df_clean = df[df['Revenue'] >= 0]
Orders with negative revenue: 9290
# Incorrect: Calculating mean revenue at product-row level instead of invoice
mean_row_val = df['Revenue'].mean()
print(f'Incorrect mean at product-row level: {mean_row_val:.2f}')
# Correct: Use invoice-level sum
correct_aov = df.groupby('Invoice')['Revenue'].sum().mean()
print(f'Correct AOV at order level: {correct_aov:.2f}')
Incorrect mean at product-row level: 17.99
Correct AOV at order level: 376.36
df['Category'] = df['Description'].str.extract('(\w+)', expand=False).fillna('Unknown')
category_invoice_totals = df.groupby(['Category', 'Invoice'])['Revenue'].sum().reset_index()
category_aov = category_invoice_totals.groupby('Category')['Revenue'].mean().sort_values(ascending=False)
print(category_aov.head(5))
Category
WEEKEND     527.850000
HALL        520.706000
UTILTY      435.048333
DOTCOM      290.896305
MISELTOE    268.883333
Name: Revenue, dtype: float64
df['Day'] = df['InvoiceDate'].dt.date
daily_orders = df.groupby('Day')['Invoice'].nunique()
daily_revenue = df.groupby('Day')['Revenue'].sum()
daily_aov = daily_revenue / daily_orders
print(daily_aov.head())
Day
2010-12-01    410.038881
2010-12-02    276.690299
2010-12-03    422.411667
2010-12-05    330.357368
2010-12-06    404.963759
dtype: float64
customer_invoice_totals = df.groupby(['Customer ID', 'Invoice'])['Revenue'].sum().reset_index()
aov_per_customer = customer_invoice_totals.groupby('Customer ID')['Revenue'].mean().sort_values(ascending=False)
print('Top 5 customers by AOV:')
print(aov_per_customer.head(5))
Top 5 customers by AOV:
Customer ID
12357.0    6207.670000
15749.0    5383.975000
12688.0    4873.810000
12415.0    4758.671154
12752.0    4366.780000
Name: Revenue, dtype: float64
top_orders = invoice_totals.sort_values(ascending=False).head(20).index
df_top_orders = df[df['Invoice'].isin(top_orders)]
product_counts = df_top_orders['Description'].value_counts().head(5)
print('Most common products in top 20 high-value orders:')
print(product_counts)
Most common products in top 20 high-value orders:
Description
PLASTERS IN TIN SPACEBOY        10
SPACEBOY LUNCH BOX               9
DOLLY GIRL LUNCH BOX             9
RED  HARMONICA IN BOX            8
SET OF 4 PANTRY JELLY MOULDS     8
Name: count, dtype: int64
  • For more data science case studies, check out our YouTube channel!

Found this useful?

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