Mathew K Analytics

Lesson 35 · Python for Retail E-commerce Analytics

Product Bundle Insights and Cross-Selling Strategies with Python for Retail Analytics

In this lesson, we explore how to analyze retail transaction data to identify product bundles and cross-selling opportunities. Cross-selling can…

⬇ 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 Bundle Insights and Cross-Selling#

  • In this lesson, we explore how to analyze retail transaction data to identify product bundles and cross-selling opportunities.
  • Cross-selling can significantly boost sales and customer value by recommending products that are often purchased together.
  • Learners will produce actionable insights such as which products drive bundle purchases, key product pairings, and how to prioritize bundle promotions.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Core Retail Analytics Concepts#

  • Retail datasets typically represent transactions: each row may be a product purchased within an invoice or a shopping basket.
  • Sales metrics like quantity, price, and revenue are often tracked at the line or order level.
  • Beginners often confuse transactions with customers, undercount quantities, or incorrectly aggregate product bundles.
  • Accurate analysis requires grouping by transaction, customer, or product as needed.
# Beginner Example 1: Load market basket data for bundle analysis
np.random.seed(42)
products = ['Bread','Milk','Butter','Eggs','Apples','Chicken','Rice','Cheese']
transaction_ids = np.repeat(np.arange(1,301),3)
product_choices = np.random.choice(products,len(transaction_ids))
basket_df = pd.DataFrame({'TransactionID':transaction_ids,'Product':product_choices})
print(basket_df.shape)
print(basket_df.head(5))
(900, 2)
   TransactionID Product
0              1    Rice
1              1    Eggs
2              1  Apples
3              2    Rice
4              2  Butter
# Beginner Example 2: Count unique product combinations per transaction
combinations_per_basket = basket_df.groupby('TransactionID')['Product'].apply(set)
print(combinations_per_basket.head(5))
TransactionID
1      {Eggs, Rice, Apples}
2    {Cheese, Rice, Butter}
3            {Rice, Apples}
4      {Rice, Butter, Milk}
5          {Cheese, Butter}
Name: Product, dtype: object
# Beginner Example 3: Find transaction counts for each product
product_transaction_counts = basket_df.groupby('Product')['TransactionID'].nunique()
print(product_transaction_counts.sort_values(ascending=False))
Product
Eggs       113
Cheese     108
Bread      105
Apples     100
Rice       100
Milk        92
Butter      91
Chicken     89
Name: TransactionID, dtype: int64
# Beginner Example 4: Create a product co-occurrence matrix
from collections import Counter

pairs_counter = Counter()
for items in combinations_per_basket:
    for prod1 in items:
        for prod2 in items:
            if prod1 < prod2:
                pairs_counter[(prod1, prod2)] += 1
sorted_pairs = sorted(pairs_counter.items(), key=lambda x: -x[1])
print('Top 5 most common product pairs:')
for pair, count in sorted_pairs[:5]:
    print(f'{pair}: {count} baskets')
Top 5 most common product pairs:
('Apples', 'Eggs'): 36 baskets
('Bread', 'Eggs'): 35 baskets
('Cheese', 'Rice'): 32 baskets
('Butter', 'Cheese'): 30 baskets
('Eggs', 'Milk'): 30 baskets
# Beginner Example 5: Visualizing simple bundle frequency
import matplotlib.pyplot as plt

top_pairs = sorted_pairs[:5]
labels = [f'{a} & {b}' for (a,b),_ in top_pairs]
counts = [count for _, count in top_pairs]
plt.bar(labels, counts, color='skyblue')
plt.ylabel('Count of Baskets')
plt.title('Most Frequent Product Bundles')
plt.show()
No description has been provided for this image
# Intermediate Example 1: Use the real Online Retail Transactions Dataset
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
retail_df = pd.read_excel(url, sheet_name='Year 2010-2011')
retail_df['InvoiceDate'] = pd.to_datetime(retail_df['InvoiceDate'])
print(retail_df.shape)
print(retail_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  
# Intermediate Example 2: Sample data for faster analysis
sample_df = retail_df.sample(n=10000, random_state=42)
print(sample_df.head(3))
       Invoice StockCode                          Description  Quantity  \
209270  555199    85123A   WHITE HANGING HEART T-LIGHT HOLDER         1   
207110  554974     21794  CLASSIC FRENCH STYLE BASKET NATURAL        30   
507583  579187     22356          CHARLOTTE BAG PINK POLKADOT        13   

               InvoiceDate  Price  Customer ID         Country  
209270 2011-06-01 12:05:00   2.95      14606.0  United Kingdom  
207110 2011-05-27 17:14:00   3.95      14031.0  United Kingdom  
507583 2011-11-28 15:31:00   1.63          NaN  United Kingdom  
# Intermediate Example 3: Find top 10 products by number of transactions
top_products = sample_df.groupby('Description')['Invoice'].nunique().sort_values(ascending=False).head(10)
print(top_products)
Description
WHITE HANGING HEART T-LIGHT HOLDER    46
JUMBO BAG RED RETROSPOT               42
REGENCY CAKESTAND 3 TIER              34
LUNCH BAG RED RETROSPOT               32
WOODEN PICTURE FRAME WHITE FINISH     31
PACK OF 72 RETROSPOT CAKE CASES       29
WOODEN FRAME ANTIQUE WHITE            29
PINK REGENCY TEACUP AND SAUCER        28
PARTY BUNTING                         28
GREEN REGENCY TEACUP AND SAUCER       27
Name: Invoice, dtype: int64
# Intermediate Example 4: Calculate revenue by product pair
# First, aggregate all products per invoice
invoice_products = sample_df.groupby('Invoice')['Description'].apply(set)
pair_revenue = Counter()
for idx, group in sample_df.groupby('Invoice'):
    prods = set(group['Description'])
    total_revenue = (group['Quantity'] * group['Price']).sum()
    for prod1 in prods:
        for prod2 in prods:
            if prod1 < prod2:
                pair_revenue[(prod1, prod2)] += total_revenue
top_revenue_pairs = sorted(pair_revenue.items(), key=lambda x: -x[1])[:5]
print('Top 5 product pairs by combined invoice revenue:')
for pair, revenue in top_revenue_pairs:
    print(pair, '->', round(revenue,2))
Top 5 product pairs by combined invoice revenue:
('DOTCOM POSTAGE', "PAPER CHAIN KIT 50'S CHRISTMAS ") -> 3086.47
('HEART OF WICKER SMALL', "JUMBO BAG 50'S CHRISTMAS ") -> 2912.15
('HEART OF WICKER SMALL', "PACK OF 12 50'S CHRISTMAS TISSUES") -> 2912.15
("JUMBO BAG 50'S CHRISTMAS ", "PACK OF 12 50'S CHRISTMAS TISSUES") -> 2912.15
('TOOTHPASTE TUBE PEN', 'TROPICAL  HONEYCOMB PAPER GARLAND ') -> 2224.48
# Intermediate Example 5: Cross-sell ratio for a lead product ('WHITE HANGING HEART T-LIGHT HOLDER')
lead = 'WHITE HANGING HEART T-LIGHT HOLDER'
with_lead = invoice_products[invoice_products.apply(lambda x: lead in x)]
cross_counts = Counter()
for prods in with_lead:
    for prod in prods:
        if prod != lead:
            cross_counts[prod] += 1
cross_sells = cross_counts.most_common(5)
print(f'Top 5 products purchased with {lead}:')
for prod, count in cross_sells:
    print(f'{prod}: {count} baskets')
Top 5 products purchased with WHITE HANGING HEART T-LIGHT HOLDER:
WOODEN FRAME ANTIQUE WHITE : 2 baskets
RED HANGING HEART T-LIGHT HOLDER: 2 baskets
RABBIT NIGHT LIGHT: 2 baskets
JUMBO BAG 50'S CHRISTMAS : 2 baskets
REGENCY TEA PLATE ROSES : 2 baskets
# Intermediate Example 6: Visualize cross-sell counts for lead product
labels = [prod for (prod,_) in cross_sells]
counts = [count for (_,count) in cross_sells]
plt.bar(labels, counts, color='orange')
plt.title('Top Cross-Sell Items with Lead Product')
plt.ylabel('Basket Count')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
No description has been provided for this image
# Advanced Example 1: Create a pivot table of top cross-sold products per main item
pivot_counts = {}
for lead in top_products.index[:5]:
    cross_counts = Counter()
    with_lead = invoice_products[invoice_products.apply(lambda x: lead in x)]
    for prods in with_lead:
        for prod in prods:
            if prod != lead:
                cross_counts[prod] += 1
    pivot_counts[lead] = cross_counts.most_common(3)
for lead, pairs in pivot_counts.items():
    print(f'{lead}:')
    for prod, count in pairs:
        print(f'   {prod}: {count}')
WHITE HANGING HEART T-LIGHT HOLDER:
   WOODEN FRAME ANTIQUE WHITE : 2
   RED HANGING HEART T-LIGHT HOLDER: 2
   RABBIT NIGHT LIGHT: 2
JUMBO BAG RED RETROSPOT:
   CARD DOLLY GIRL : 2
   BULL DOG BOTTLE OPENER: 2
   BIRD HOUSE HOT WATER BOTTLE: 1
REGENCY CAKESTAND 3 TIER:
   SET OF 3 REGENCY CAKE TINS: 2
   PAPER CHAIN KIT VINTAGE CHRISTMAS: 1
   PINK POT PLANT CANDLE: 1
LUNCH BAG RED RETROSPOT:
   PACK OF 72 RETROSPOT CAKE CASES: 2
   FUNKY DIVA PEN: 1
   SET 12 KIDS COLOUR  CHALK STICKS: 1
WOODEN PICTURE FRAME WHITE FINISH:
   COSY SLIPPER SHOES LARGE GREEN: 1
   CUPID DESIGN SCENTED CANDLES: 1
   ALARM CLOCK BAKELIKE ORANGE: 1
# Advanced Example 2: Compute lift metric for product pairs
def compute_lift(pair, basket_sets, prods_list):
    prod_a, prod_b = pair
    num_baskets = len(basket_sets)
    a_count = sum([prod_a in items for items in basket_sets])
    b_count = sum([prod_b in items for items in basket_sets])
    ab_count = sum([(prod_a in items) and (prod_b in items) for items in basket_sets])
    if a_count==0 or b_count==0:
        return 0
    return (ab_count / num_baskets) / ((a_count / num_baskets) * (b_count / num_baskets))
all_pairs = [pair for (pair,_) in sorted_pairs[:10]]
lifts = [(pair, compute_lift(pair, combinations_per_basket, products)) for pair in all_pairs]
lifts.sort(key=lambda x: -x[1])
print('Top product bundles by lift:')
for pair, lift in lifts[:5]:
    print(f'{pair}: lift={round(lift,2)}')
Top product bundles by lift:
('Apples', 'Eggs'): lift=0.96
('Butter', 'Cheese'): lift=0.92
('Cheese', 'Rice'): lift=0.89
('Bread', 'Eggs'): lift=0.88
('Eggs', 'Milk'): lift=0.87
# Advanced Example 3: Association rule mining using mlxtend (if installed)
try:
    from mlxtend.frequent_patterns import apriori, association_rules
    # Prepare transaction-basket format
    basket_sets = basket_df.groupby('TransactionID')['Product'].apply(list)
    from mlxtend.preprocessing import TransactionEncoder
    te = TransactionEncoder()
    te_ary = te.fit(basket_sets.tolist()).transform(basket_sets.tolist())
    basket_ready = pd.DataFrame(te_ary, columns=te.columns_)
    frequent_itemsets = apriori(basket_ready, min_support=0.05, use_colnames=True)
    rules = association_rules(frequent_itemsets, metric='lift', min_threshold=1)
    print(rules[['antecedents','consequents','support','confidence','lift']].head())
except Exception as e:
    print('mlxtend not installed. Please install with: pip install mlxtend')
Empty DataFrame
Columns: [antecedents, consequents, support, confidence, lift]
Index: []
# Error Handling Example 1: Check for missing products in baskets
missing_products = basket_df['Product'].isnull().sum()
print(f'Missing product entries: {missing_products}')
Missing product entries: 0
# Error Handling Example 2: Detect empty baskets after filtering
baskets_with_no_products = combinations_per_basket.apply(lambda s: len(s)==0).sum()
print(f'Number of transactions with no products: {baskets_with_no_products}')
Number of transactions with no products: 0
# Error Handling Example 3: Incorrect product grouping bug example
wrong_group = basket_df.groupby('Product')['TransactionID'].count()
correct_group = basket_df.groupby('Product')['TransactionID'].nunique()
print('Count:', wrong_group.max(), 'Unique:', correct_group.max())
Count: 127 Unique: 113

Best Practices and Analytics Patterns#

  • Segment customers based on purchase history to tailor cross-sell strategies.
  • Analyze individual product performance to identify lead items for bundling.
  • Use market basket analysis for discovering actionable product associations.
  • Test different cross-sell offers and track seasonal or promotional uplift.
  • Forecast bundle demand to prepare inventory and logistics.
# End-to-end Example: Find and recommend a high-revenue cross-sell bundle
lead_item = top_products.index[0]
with_lead = invoice_products[invoice_products.apply(lambda x: lead_item in x)]
cross_counter = Counter()
for prods in with_lead:
    for prod in prods:
        if prod != lead_item:
            cross_counter[prod] += 1
top_partner, max_count = cross_counter.most_common(1)[0]
recommend_pair = (lead_item, top_partner)
bundled_total = 0.0
for idx, group in sample_df.groupby('Invoice'):
    items = set(group['Description'])
    if recommend_pair[0] in items and recommend_pair[1] in items:
        bundled_total += (group['Quantity'] * group['Price']).sum()
print(f'Recommend bundle: {lead_item} + {top_partner}')
print(f'Estimated bundle revenue in sample: {bundled_total:.2f}')
Recommend bundle: WHITE HANGING HEART T-LIGHT HOLDER + WOODEN FRAME ANTIQUE WHITE 
Estimated bundle revenue in sample: 62.70

Practice: Run your own bundle analysis#

  • Try changing the sample size, product list, or cross-sell metrics above.
  • Use these patterns to explore any retail, e-commerce, or transaction-level dataset.
  • Share your top bundle findings with a friend or classmatewhat surprises you?

Found this useful?

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