Mathew K Analytics

Lesson 31 · Python for Retail E-commerce Analytics

Product Performance Analysis with Python for Retail E-commerce Analytics

In this lesson, we will learn to analyze which products sell best and why. Product performance analysis helps drive better marketing, sales strategies, and…

⬇ 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

Product Performance Analysis in Retail and E-Commerce#

  • In this lesson, we will learn to analyze which products sell best and why.
  • Product performance analysis helps drive better marketing, sales strategies, and inventory decisions.
  • You will explore real transaction data to find top products, revenue contributions, seasonal patterns, and actionable insights.
  • By the end, you will be able to identify high and low performing products and make practical business recommendations.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Key Retail Data Concepts for Product Analysis#

  • Retail datasets often represent transactions (sales), products (catalog), and customers.
  • Sales metrics like revenue, quantity sold, and unit price are the foundation of product performance analysis.
  • A common mistake is to sum prices instead of multiplying quantity by price for true revenue.
  • Always check for missing or inconsistent values before running aggregations.
# Loading Retail Transactions: Online Retail Dataset (UCI)
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  
# Create a product catalog from the transaction data
products = df[['StockCode','Description','Price']].drop_duplicates()
products = products.rename(columns={'StockCode':'ProductID'})
print(products.shape)
print(products.head(3))
(18053, 3)
  ProductID                         Description  Price
0    85123A  WHITE HANGING HEART T-LIGHT HOLDER   2.55
1     71053                 WHITE METAL LANTERN   3.39
2    84406B      CREAM CUPID HEARTS COAT HANGER   2.75
# Add a Revenue column to the transactions data
df['Revenue'] = df['Quantity'] * df['Price']
print(df[['StockCode','Quantity','Price','Revenue']].head(3))
  StockCode  Quantity  Price  Revenue
0    85123A         6   2.55    15.30
1     71053         6   3.39    20.34
2    84406B         8   2.75    22.00
# Beginner: Total revenue for the entire store
total_revenue = df['Revenue'].sum()
print('Total revenue:', total_revenue)
Total revenue: 9747765.934
# Beginner: Find and display total product sales volume
product_sales = df.groupby('StockCode')['Quantity'].sum().reset_index()
print(product_sales.head(3))
  StockCode  Quantity
0     10002      1037
1     10080       495
2     10120       193
# Beginner: Top 5 revenue-generating products
top_products = df.groupby(['StockCode', 'Description'])['Revenue'].sum().sort_values(ascending=False).head(5)
print(top_products)
StockCode  Description                       
DOT        DOTCOM POSTAGE                        206245.48
22423      REGENCY CAKESTAND 3 TIER              164762.19
47566      PARTY BUNTING                          98302.98
85123A     WHITE HANGING HEART T-LIGHT HOLDER     97715.99
85099B     JUMBO BAG RED RETROSPOT                92356.03
Name: Revenue, dtype: float64
# Beginner: Find number of unique products sold
num_products_sold = df['StockCode'].nunique()
print('Unique products sold:', num_products_sold)
Unique products sold: 4070
# Intermediate: Top 5 products by average price (of products sold)
product_avg_prices = df.groupby(['StockCode','Description'])['Price'].mean().sort_values(ascending=False).head(5)
print(product_avg_prices)
StockCode  Description                   
AMAZONFEE  AMAZON FEE                        7324.784706
22502      PICNIC BASKET WICKER 60 PIECES     649.500000
CRUK       CRUK Commission                    495.839375
M          Manual                             375.566392
DOT        DOTCOM POSTAGE                     290.905585
Name: Price, dtype: float64
# Intermediate: Monthly product sales trend for a top seller
top_stock = top_products.index[0][0]
df['Month'] = df['InvoiceDate'].dt.to_period('M')
monthly_trend = df[df['StockCode'] == top_stock].groupby('Month')['Revenue'].sum()
print(monthly_trend)
Month
2010-12    24671.19
2011-01    13918.53
2011-02    10060.57
2011-03    11829.71
2011-04     7535.38
2011-05    10229.30
2011-06    11848.66
2011-07    12841.00
2011-08    13400.52
2011-09    15177.40
2011-10    17955.13
2011-11    36905.40
2011-12    19872.69
Freq: M, Name: Revenue, dtype: float64
# Intermediate: Aggregate revenue by country (market segmentation)
country_revenue = df.groupby('Country')['Revenue'].sum().sort_values(ascending=False)
print(country_revenue.head(5))
Country
United Kingdom    8187806.364
Netherlands        284661.540
EIRE               263276.820
Germany            221698.210
France             197421.900
Name: Revenue, dtype: float64
# Intermediate: Average order value (AOV) calculation
invoice_totals = df.groupby('Invoice')['Revenue'].sum()
aov = invoice_totals.mean()
print('Average Order Value (AOV):', round(aov,2))
Average Order Value (AOV): 376.36
# Intermediate: Top 5 products by number of orders
orders_per_product = df.groupby('StockCode')['Invoice'].nunique().sort_values(ascending=False).head(5)
print(orders_per_product)
StockCode
85123A    2246
22423     2172
85099B    2135
47566     1706
20725     1608
Name: Invoice, dtype: int64
# Advanced: Product revenue share across portfolio
total_revenue = df['Revenue'].sum()
product_revenue_share = df.groupby(['StockCode','Description'])['Revenue'].sum() / total_revenue
product_revenue_share = product_revenue_share.sort_values(ascending=False).head(5)
print(product_revenue_share)
StockCode  Description                       
DOT        DOTCOM POSTAGE                        0.021158
22423      REGENCY CAKESTAND 3 TIER              0.016903
47566      PARTY BUNTING                         0.010085
85123A     WHITE HANGING HEART T-LIGHT HOLDER    0.010024
85099B     JUMBO BAG RED RETROSPOT               0.009475
Name: Revenue, dtype: float64
# Advanced: Top 3 high revenue products by country
country_top3 = (df.groupby(['Country','StockCode','Description'])['Revenue'].sum()
                  .reset_index()
                  .sort_values(['Country','Revenue'], ascending=[True,False]))
country_top3 = country_top3.groupby('Country').head(3)
print(country_top3.head(9))
       Country StockCode                         Description  Revenue
396  Australia     23084                  RABBIT NIGHT LIGHT  3375.84
296  Australia     22722   SET OF 6 SPICE TINS PANTRY DESIGN  2082.00
87   Australia     21731       RED TOADSTOOL LED NIGHT LIGHT  1987.20
916    Austria      POST                             POSTAGE  1456.00
755    Austria     22582        PACK OF 6 SWEETIE GIFT BOXES   302.40
757    Austria     22584      PACK OF 6 PANNETONE GIFT BOXES   302.40
924    Bahrain     23076          ICE CREAM SUNDAE LIP GLOSS   120.00
925    Bahrain     23077                 DOUGHNUT LIP GLOSS     75.00
923    Bahrain     22890  NOVELTY BISCUITS CAKE STAND 3 TIER    59.70
# Advanced: Cumulative revenue percent (Pareto/80-20 analysis)
product_rev = df.groupby('StockCode')['Revenue'].sum().sort_values(ascending=False)
cum_rev_pct = product_rev.cumsum() / product_rev.sum()
top20_pct = (cum_rev_pct <= 0.8).sum() / len(product_rev)
print('Share of products responsible for 80% of revenue:', round(top20_pct*100,2), '%')
Share of products responsible for 80% of revenue: 18.08 %
# Advanced: Detect products with no sales (inactive inventory)
all_products = products['ProductID'].unique()
sold_products = df['StockCode'].unique()
unsold = set(all_products) - set(sold_products)
print('Number of products never sold:', len(unsold))
Number of products never sold: 0
# Error handling: Check for missing values in key columns
missing = df[['StockCode','Quantity','Price','Revenue']].isnull().sum()
print('Missing values per column:')
print(missing)
Missing values per column:
StockCode    0
Quantity     0
Price        0
Revenue      0
dtype: int64
# Error correction: Remove transactions with negative quantities (usually returns or data errors)
df_clean = df[df['Quantity'] > 0]
print('Original rows:', len(df), ' | Cleaned rows:', len(df_clean))
Original rows: 541910  | Cleaned rows: 531286
# Error example: Incorrect aggregation (summing prices instead of revenue)
wrong_total = df['Price'].sum()
real_total = df['Revenue'].sum()
print('Incorrect total (price sum):', wrong_total)
print('Correct total (revenue sum):', real_total)
Incorrect total (price sum): 2498821.9739999995
Correct total (revenue sum): 9747765.934
# Pattern: Product performance ranking for executive reports
product_ranking = df.groupby(['StockCode','Description'])['Revenue'].sum().sort_values(ascending=False).reset_index()
product_ranking['Rank'] = range(1, len(product_ranking)+1)
print(product_ranking[['StockCode','Description','Revenue','Rank']].head(10))
  StockCode                         Description    Revenue  Rank
0       DOT                      DOTCOM POSTAGE  206245.48     1
1     22423            REGENCY CAKESTAND 3 TIER  164762.19     2
2     47566                       PARTY BUNTING   98302.98     3
3    85123A  WHITE HANGING HEART T-LIGHT HOLDER   97715.99     4
4    85099B             JUMBO BAG RED RETROSPOT   92356.03     5
5     23084                  RABBIT NIGHT LIGHT   66756.59     6
6      POST                             POSTAGE   66248.64     7
7     22086     PAPER CHAIN KIT 50'S CHRISTMAS    63791.94     8
8     84879       ASSORTED COLOUR BIRD ORNAMENT   58959.73     9
9     79321                       CHILLI LIGHTS   53768.06    10
# Pattern: Compare top versus bottom performing products
top = product_ranking.head(3)
bottom = product_ranking.tail(3)
print('Top performers:')
print(top[['StockCode','Description','Revenue']])
print(' ')
print('Low performers:')
print(bottom[['StockCode','Description','Revenue']])
Top performers:
  StockCode               Description    Revenue
0       DOT            DOTCOM POSTAGE  206245.48
1     22423  REGENCY CAKESTAND 3 TIER  164762.19
2     47566             PARTY BUNTING   98302.98
 
Low performers:
      StockCode      Description    Revenue
4789          B  Adjust bad debt  -11062.06
4790          M           Manual  -68674.19
4791  AMAZONFEE       AMAZON FEE -221520.50
# End-to-end: Identify top 2 bestsellers and recommend next steps
best_two = product_ranking.head(2)
product_names = ', '.join(best_two['Description'].tolist())
print('Top 2 Bestselling Products: ', product_names)
print('\nRecommendation: Focus marketing, stock, and promotions on these items to maximize revenue impact.')
Top 2 Bestselling Products:  DOTCOM POSTAGE, REGENCY CAKESTAND 3 TIER

Recommendation: Focus marketing, stock, and promotions on these items to maximize revenue impact.
 

Found this useful?

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