Mathew K Analytics

Lesson 10 · Real-World Data Analytics

Python Data Analytics #10: Retail Analytics Capstone — End-to-End Report in Python

Video ten of the hundred-video real-world data analytics series, and the real capstone for Domain 1. Pulling every technique from this real retail series…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Data Analytics 100, Video 10: Capstone, End-to-End Retail Analytics Report#

  • Video ten of the hundred-video real-world data analytics series, and the real capstone for Domain 1.
  • Pulling every technique from this real retail series into one real consolidated end-to-end report.
  • Let's get into it.

Part 1: One Real Dataset, Nine Real Techniques#

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
clean = pd.read_csv('online_retail_clean.csv', parse_dates=['InvoiceDate'])
clean['CustomerID'] = clean['CustomerID'].astype(int)
clean.shape
(391150, 9)

Part 2: Real Data Quality Recap#

n_customers = clean['CustomerID'].nunique()
n_invoices = clean['InvoiceNo'].nunique()
total_revenue = clean['Revenue'].sum()
print(f'{n_customers} real customers, {n_invoices} real invoices, {total_revenue:,.0f} GBP total real revenue.')
4334 real customers, 18402 real invoices, 8,737,228 GBP total real revenue.

Part 3: Real RFM Segmentation Recap#

snapshot_date = clean['InvoiceDate'].max() + pd.Timedelta(days=1)
rfm = clean.groupby('CustomerID').agg(Recency=('InvoiceDate', lambda s: (snapshot_date - s.max()).days), Frequency=('InvoiceNo', 'nunique'), Monetary=('Revenue', 'sum'))
rfm['R_score'] = pd.qcut(rfm['Recency'], 4, labels=[4, 3, 2, 1]).astype(int)
rfm['F_score'] = pd.qcut(rfm['Frequency'].rank(method='first'), 4, labels=[1, 2, 3, 4]).astype(int)
champions = rfm[(rfm['R_score'] >= 3) & (rfm['F_score'] >= 3)]
print(f'{len(champions)} real Champion customers, worth {champions["Monetary"].sum():,.0f} GBP in real total historical revenue.')
1524 real Champion customers, worth 6,469,790 GBP in real total historical revenue.

Part 4: Real Market Basket Recap#

uk = clean[clean['Country'] == 'United Kingdom']
top_products = uk.groupby('Description')['InvoiceNo'].nunique().sort_values(ascending=False).head(30)
baskets = uk[uk['Description'].isin(top_products.index)].groupby('InvoiceNo')['Description'].apply(set)
baskets = baskets[baskets.apply(len) >= 2]
print(f'{len(baskets)} real multi-item baskets found among the real top thirty products.')
6559 real multi-item baskets found among the real top thirty products.

Part 5: Real Cohort Retention Recap#

clean['InvoiceMonth'] = clean['InvoiceDate'].values.astype('datetime64[M]')
first_purchase = clean.groupby('CustomerID')['InvoiceMonth'].min()
merged = clean.merge(first_purchase.rename('CohortMonth'), on='CustomerID')
merged['CohortIndex'] = (merged['InvoiceMonth'].dt.year - merged['CohortMonth'].dt.year) * 12 + (merged['InvoiceMonth'].dt.month - merged['CohortMonth'].dt.month)
cohort_pivot = merged.groupby(['CohortMonth', 'CohortIndex'])['CustomerID'].nunique().unstack()
retention = cohort_pivot.divide(cohort_pivot[0], axis=0)
avg_month1_retention = retention[1].mean()
print(f'Average real month-one retention across all cohorts: {round(avg_month1_retention * 100, 1)}%.')
Average real month-one retention across all cohorts: 20.4%.

Part 6: Real Sales Forecast Recap#

from statsmodels.tsa.holtwinters import ExponentialSmoothing
daily = clean.groupby(clean['InvoiceDate'].dt.date)['Revenue'].sum()
daily.index = pd.to_datetime(daily.index)
weekly = daily.resample('W').sum().iloc[1:-1]
forecast_model = ExponentialSmoothing(weekly, trend='add', seasonal=None).fit()
next_week_forecast = forecast_model.forecast(1).iloc[0]
print(f'Next week real forecasted revenue: {next_week_forecast:,.0f} GBP.')
Next week real forecasted revenue: 270,317 GBP.

Part 7: Real Price Elasticity Recap#

qty_by_sc = clean.groupby('StockCode')['Quantity'].sum()
price_var_by_sc = clean.groupby('StockCode')['UnitPrice'].nunique()
candidates = price_var_by_sc[(price_var_by_sc >= 4) & (qty_by_sc > 500)].index
elastic_count = 0
for sc in candidates:
    bp = clean[clean['StockCode'] == sc].groupby('UnitPrice')['Quantity'].sum()
    bp = bp[bp > 0]
    if len(bp) < 4 or bp.index.to_series().std() == 0:
        continue
    e, _ = np.polyfit(np.log(bp.index.values.astype(float)), np.log(bp.values.astype(float)), 1)
    elastic_count += 1 if e < -1 else 0
print(f'{elastic_count} of {len(candidates)} real qualifying products are genuinely elastic, good discounting candidates.')
303 of 373 real qualifying products are genuinely elastic, good discounting candidates.

Part 8: Real Customer Lifetime Value Recap#

cust_orders = clean.groupby('CustomerID').agg(TotalRevenue=('Revenue', 'sum'), Orders=('InvoiceNo', 'nunique'))
cust_orders['AvgOrderValue'] = cust_orders['TotalRevenue'] / cust_orders['Orders']
total_days = (clean['InvoiceDate'].max() - clean['InvoiceDate'].min()).days
cust_orders['FreqPerMonth'] = cust_orders['Orders'] / (total_days / 30)
cust_orders['PredictedCLV'] = cust_orders['AvgOrderValue'] * cust_orders['FreqPerMonth'] * 12
total_predicted_clv = cust_orders['PredictedCLV'].sum()
print(f'Total real predicted twelve-month CLV across the customer base: {total_predicted_clv:,.0f} GBP.')
Total real predicted twelve-month CLV across the customer base: 8,432,713 GBP.

Part 9: Real ABC Analysis Recap#

product_revenue = clean.groupby('StockCode')['Revenue'].sum().sort_values(ascending=False)
cumulative_pct = product_revenue.cumsum() / product_revenue.sum() * 100
n_tier_a = (cumulative_pct <= 80).sum()
pct_tier_a = n_tier_a / len(product_revenue) * 100
print(f'{round(pct_tier_a, 1)}% of products drive 80% of total real revenue.')
21.4% of products drive 80% of total real revenue.

Part 10: Building the Real Consolidated Dashboard#

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes[0, 0].plot(weekly.index, weekly.values, color='navy')
axes[0, 0].set_title('Real Weekly Revenue History')
axes[0, 1].plot(retention.columns, retention.mean(), marker='o', color='seagreen')
axes[0, 1].set_title('Real Average Retention Curve')
rank = np.arange(1, len(cumulative_pct) + 1)
axes[1, 0].plot(rank, cumulative_pct.values, color='crimson')
axes[1, 0].axhline(80, color='black', linestyle='--')
axes[1, 0].set_title('Real Revenue Pareto Curve')
segment_sizes = pd.Series({'Champions': len(champions), 'Everyone Else': n_customers - len(champions)})
axes[1, 1].bar(segment_sizes.index, segment_sizes.values, color=['gold', 'gray'])
axes[1, 1].set_title('Real Champions vs Real Everyone Else')
plt.tight_layout()
plt.savefig('capstone_dashboard.png', dpi=120)
plt.close()

Part 11: Writing the Real Executive Summary#

summary_lines = []
summary_lines.append('RETAIL ANALYTICS CAPSTONE, EXECUTIVE SUMMARY')
summary_lines.append(f'Dataset: {n_customers} real customers, {n_invoices} real invoices, {total_revenue:,.0f} GBP total revenue')
summary_lines.append(f'Champions segment: {len(champions)} customers, {champions["Monetary"].sum():,.0f} GBP historical revenue')
summary_lines.append(f'Average month-1 retention across cohorts: {round(avg_month1_retention * 100, 1)}%')
summary_lines.append(f'Next week forecasted revenue: {next_week_forecast:,.0f} GBP')
summary_lines.append(f'{elastic_count} of {len(candidates)} qualifying products are price-elastic')
summary_lines.append(f'Total predicted 12-month customer lifetime value: {total_predicted_clv:,.0f} GBP')
summary_lines.append(f'{round(pct_tier_a, 1)}% of products (tier A) drive 80% of total revenue')

Part 12: Saving the Real Executive Summary#

with open('capstone_executive_summary.txt', 'w') as f:
    for line in summary_lines:
        f.write(line + chr(10))
print('Real executive summary saved.')
Real executive summary saved.

Part 13: Real Reload Verification#

with open('capstone_executive_summary.txt') as f:
    reloaded_lines = f.read().splitlines()
len(reloaded_lines) == len(summary_lines)
True

Part 14: Real Consolidated Metrics Table#

metrics = pd.DataFrame({
    'Metric': ['Customers', 'Total Revenue', 'Champions', 'Avg Month1 Retention %', 'Next Week Forecast', 'Elastic Products', 'Total Predicted CLV', 'Tier A Product %'],
    'Value': [n_customers, round(total_revenue, 0), len(champions), round(avg_month1_retention * 100, 1), round(next_week_forecast, 0), elastic_count, round(total_predicted_clv, 0), round(pct_tier_a, 1)]
})
metrics
Metric Value
0 Customers 4334.0
1 Total Revenue 8737228.0
2 Champions 1524.0
3 Avg Month1 Retention % 20.4
4 Next Week Forecast 270317.0
5 Elastic Products 303.0
6 Total Predicted CLV 8432713.0
7 Tier A Product % 21.4

Part 15: Saving the Real Metrics Table#

metrics.to_csv('capstone_metrics_table.csv', index=False)
reloaded_metrics = pd.read_csv('capstone_metrics_table.csv')
reloaded_metrics.shape[0] == metrics.shape[0]
True

Part 16: Real Top-Line Business Recommendation#

recommendation = f'Focus retention spend on the {len(champions)} real Champions worth {champions["Monetary"].sum():,.0f} GBP, while using the {elastic_count} genuinely elastic products identified as safe discount levers to win back the segments below them.'
print(recommendation)
Focus retention spend on the 1524 real Champions worth 6,469,790 GBP, while using the 303 genuinely elastic products identified as safe discount levers to win back the segments below them.

Part 17: Real Domain Recap#

print('Domain 1, Retail and E-Commerce Analytics: 10 real lessons complete.')
Domain 1, Retail and E-Commerce Analytics: 10 real lessons complete.

Part 4b: Real Top Association Rule#

from itertools import combinations
from collections import Counter
pair_counts = Counter()
for basket in baskets:
    for a, b in combinations(sorted(basket), 2):
        pair_counts[(a, b)] = pair_counts.get((a, b), 0) + 1
top_pair = max(pair_counts, key=pair_counts.get)
print(f'Most frequent real pairing: {top_pair[0][:20]} + {top_pair[1][:20]}, seen together in {pair_counts[top_pair]} real baskets.')
Most frequent real pairing: JUMBO BAG PINK POLKA + JUMBO BAG RED RETROS, seen together in 506 real baskets.

Part 6b: Real Forecast Residual Sanity Check#

residuals = weekly - forecast_model.fittedvalues
round(residuals.mean(), 1)
np.float64(7317.9)

Part 9b: Real Tier B and Tier C Breakdown#

n_tier_b = ((cumulative_pct > 80) & (cumulative_pct <= 95)).sum()
n_tier_c = (cumulative_pct > 95).sum()
print(f'Tier A: {n_tier_a} products, Tier B: {n_tier_b} products, Tier C: {n_tier_c} products.')
Tier A: 783 products, Tier B: 920 products, Tier C: 1956 products.

Part 9c: Real Country Revenue Breakdown#

clean.groupby('Country')['Revenue'].sum().sort_values(ascending=False).head(3).round(0)
Country
United Kingdom    7242855.0
Netherlands        283889.0
EIRE               257013.0
Name: Revenue, dtype: float64

Part 9d: Real Overall Average Order Value#

overall_aov = clean.groupby('InvoiceNo')['Revenue'].sum().mean()
round(overall_aov, 2)
np.float64(474.8)

Part 9e: Real Busiest Month by Revenue#

monthly_revenue = clean.groupby(clean['InvoiceDate'].dt.to_period('M'))['Revenue'].sum()
busiest_month = monthly_revenue.idxmax()
print(f'Busiest real month: {busiest_month}, with {monthly_revenue[busiest_month]:,.0f} GBP in real revenue.')
Busiest real month: 2011-11, with 1,136,534 GBP in real revenue.

Part 9f: Real Reconciliation Check#

abs(rfm['Monetary'].sum() - total_revenue) < 1
np.True_

Part 9g: Real Top 5 Customers by Predicted CLV#

cust_orders.sort_values('PredictedCLV', ascending=False).head(5)[['AvgOrderValue', 'FreqPerMonth', 'PredictedCLV']].round(2)
AvgOrderValue FreqPerMonth PredictedCLV
CustomerID
14646 3876.92 5.79 269409.35
18102 4327.62 4.83 250607.58
17450 4225.89 3.70 187615.78
16446 84236.25 0.16 162600.80
14911 687.69 15.92 131416.24

Part 9h: Real One-Time vs Repeat Customer Split#

repeat_rate = (cust_orders['Orders'] > 1).mean()
round(repeat_rate * 100, 1)
np.float64(65.3)

Part 9i: Real Total Products in Catalog#

clean['StockCode'].nunique()
3659

Part 9j: Real Cancellation Rate Sanity Check#

clean['InvoiceNo'].astype(str).str.startswith('C').sum() == 0
np.True_

Part 9k: Real Median Customer Revenue#

cust_orders['TotalRevenue'].median().round(2)
np.float64(662.56)

Part 9l: Real Domain 1 Lesson Count Confirmation#

domain_1_lessons = ['Acquiring & Auditing', 'RFM Segmentation', 'Market Basket', 'Cohort Retention', 'Sales Forecasting', 'Price Elasticity', 'Customer Lifetime Value', 'ABC Analysis', 'Scraping Prices', 'Capstone']
len(domain_1_lessons)
10

Wrap-Up: What You Learned#

  • A real capstone genuinely recomputes every prior headline number fresh, straight from the real same source data, rather than trusting cached results blindly.
  • RFM segmentation, market basket analysis, cohort retention, forecasting, price elasticity, CLV, and ABC analysis all draw on the exact real same cleaned dataset.
  • A real consolidated dashboard, four charts in one figure, is exactly the kind of artifact a real stakeholder actually wants to see.
  • A real written executive summary translates every real computed number into a real plain-language business narrative.
  • The real most valuable output of any analysis is a real specific, actionable recommendation, not just a pile of real numbers.
  • Next up: Domain 2, Finance and Stock Market Analytics, ten more real lessons starting with acquiring real historical stock price data.

Found this useful?

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