DS Case Studies
Analysing Retail Store Performance
Revenue is the number every retail executive leads with. But revenue alone hides more than it reveals. A store bringing in $40,000 a month with 5,000 visitors is doing something very different from one generating the same number with 1,500 visitors. The story is in the ratios — not the totals.
You are a data analyst at Meridian Retail Group, a chain of 12 stores spread across four regions. The regional director is preparing the quarterly performance review and has two specific concerns: which stores are genuinely over-performing relative to their footfall, and which stores have strong traffic but are failing to convert visitors into buyers. You have the transaction data and one afternoon to find the answers.
What This Case Study Covers
Retail performance analysis is fundamentally about understanding the gap between potential and reality. Every store has a ceiling set by its footfall — the number of people who walk through the door. What they do with that footfall is the performance story. This case study introduces the concept of derived efficiency metrics: revenue per visitor, conversion rate, and average transaction value. These three numbers tell you far more than total revenue alone.
This case study covers four analytical layers: raw performance ranking by revenue and footfall to establish the baseline, derived efficiency metrics — revenue per visitor, conversion rate, and average transaction value — to expose which stores are working hardest per customer, regional comparison to find whether geography explains performance differences, and outlier identification to flag the stores the director needs to act on immediately.
The Retail EDA Toolkit
Raw Ranking
Sort stores by total revenue and footfall independently. The two rankings will not match — and the gaps between them reveal which stores are over- or under-converting their traffic.Revenue Per Visitor
Divide total revenue by total footfall. This normalises for store size and location traffic — a store in a quiet area converting well will score higher than a busy store with poor conversion. It is the fairest single comparison metric across stores.Conversion Rate
Divide transactions by footfall. If 1,000 people visit and 250 buy, conversion is 25%. Low conversion with high footfall is a sales floor problem — staff, merchandising, or pricing. High conversion with low footfall is a traffic problem — marketing or location.Average Transaction Value
Divide revenue by number of transactions. ATV tells you how much each buying customer spends. A store with strong conversion but low ATV is bringing people in but failing to upsell. That is a different problem from low conversion entirely.Regional Aggregation
Group by region and compute mean efficiency metrics. Regional patterns reveal whether underperformance is store-specific (a management issue) or regional (a market or competitive issue that requires a different response).Dataset Overview
Meridian's store performance export contains 12 records — one row per store — covering four regions with monthly revenue, footfall, and transaction data. Built with pd.DataFrame().
| store_id | region | store_type | monthly_revenue | footfall | transactions | staff_count |
|---|---|---|---|---|---|---|
| ST01 | North | Large | $48,200 | 4,100 | 820 | 14 |
| ST02 | South | Medium | $31,500 | 3,800 | 570 | 9 |
| ST03 | East | Small | $18,900 | 1,200 | 390 | 5 |
| ST04 | West | Large | $55,600 | 5,200 | 940 | 16 |
| ST05 | North | Medium | $27,300 | 2,900 | 480 | 8 |
Showing first 5 of 12 rows · 7 columns
Unique store reference code. Used for labelling — never aggregated directly.
North, South, East, West. The primary grouping dimension for the regional comparison analysis.
Large, Medium, or Small — by floor area. Used to contextualise performance differences between store sizes.
Total revenue generated in the month. The primary headline metric — but meaningless without footfall context.
Total number of people who entered the store during the month. The denominator for all efficiency metrics.
Number of completed purchases. Used to compute conversion rate (transactions ÷ footfall) and ATV (revenue ÷ transactions).
Number of staff on the floor. Used to compute revenue per staff member — a labour efficiency metric.
Business Questions
The regional director needs these five questions answered before the quarterly review. Read them carefully — several require derived metrics that do not exist in the raw data yet.
Which store has the highest revenue per visitor — and does it match the store with the highest total revenue?
Which stores have the lowest conversion rate despite having above-average footfall — these are the highest-priority intervention targets?
Which region has the highest average revenue per visitor, and does regional performance correlate with store type mix?
What is the average transaction value (ATV) per store — and which stores are under-performing on ATV relative to their region average?
Which stores should the director prioritise for immediate review — combining low conversion and low ATV as a dual-flag signal?
Step-by-Step Analysis
The scenario:
The performance export has landed. The regional director has a 2pm meeting and wants a one-page summary by then. You have the data, you have pandas, and you have three hours. The key insight will not come from the revenue column — it will come from the ratios you are about to compute.
Before ranking anything, we build the three metrics that make retail analysis meaningful: revenue per visitor, conversion rate, and average transaction value. These do not exist in the raw data — we compute them from the columns that do.
import pandas as pd
import numpy as np
# Build the Meridian Retail dataset
df = pd.DataFrame({
"store_id": ["ST01","ST02","ST03","ST04","ST05","ST06",
"ST07","ST08","ST09","ST10","ST11","ST12"],
"region": ["North","South","East","West","North","South",
"East","West","North","South","East","West"],
"store_type": ["Large","Medium","Small","Large","Medium","Small",
"Large","Medium","Small","Large","Medium","Small"],
"monthly_revenue": [48200,31500,18900,55600,27300,15800,
42100,38700,21400,61200,24600,19800],
"footfall": [4100,3800,1200,5200,2900,1800,
3600,4500,1600,5800,2400,1400],
"transactions": [820,570,390,940,480,240,
710,720,350,1050,380,280],
"staff_count": [14,9,5,16,8,5,12,11,6,18,7,5]
})
# Inspect
print("Shape:", df.shape)
print("\nMissing values:", df.isnull().sum().sum(), "total")
# Compute derived efficiency metrics
# Revenue per visitor — how much revenue each person who enters generates on average
df["rev_per_visitor"] = (df["monthly_revenue"] / df["footfall"]).round(2)
# Conversion rate — what % of visitors actually buy something
df["conversion_rate"] = (df["transactions"] / df["footfall"] * 100).round(1)
# Average transaction value — how much each buying customer spends
df["avg_transaction"] = (df["monthly_revenue"] / df["transactions"]).round(2)
# Revenue per staff — labour efficiency metric
df["rev_per_staff"] = (df["monthly_revenue"] / df["staff_count"]).round(0)
print("\nSummary of derived metrics:")
print(df[["store_id","rev_per_visitor","conversion_rate",
"avg_transaction","rev_per_staff"]].to_string(index=False))
Shape: (12, 7)
Missing values: 0 total
Summary of derived metrics:
store_id rev_per_visitor conversion_rate avg_transaction rev_per_staff
ST01 11.76 20.0 58.78 3443
ST02 8.29 15.0 55.26 3500
ST03 15.75 32.5 48.46 3780
ST04 10.69 18.1 59.15 3475
ST05 9.41 16.6 56.88 3413
ST06 8.78 13.3 65.83 3160
ST07 11.69 19.7 59.30 3508
ST08 8.60 16.0 53.75 3518
ST09 13.38 21.9 61.14 3567
ST10 10.55 18.1 58.29 3400
ST11 10.25 15.8 64.74 3514
ST12 14.14 20.0 70.71 3960What just happened?
Method — creating multiple derived columnsWe computed four new columns in sequence, each dividing one raw column by another. This is the standard pattern for derived metrics in pandas — create each column with a single assignment, chaining .round() to keep the output clean. The order matters: compute all raw metrics before doing any ranking or filtering, so the full picture is available for every subsequent step.
The derived metrics immediately change the picture. ST03 has the highest revenue per visitor at $15.75 — yet it ranked last on total revenue. It is a small store with relatively modest footfall that is converting visitors exceptionally well. ST06 has the lowest conversion rate at 13.3% — only 1 in 7 visitors makes a purchase. The raw revenue table would have shown ST06 as a low performer without revealing why. Now we know the problem is not the number of people visiting — it is what happens once they are inside.
Ranking stores by revenue and then by efficiency metrics separately reveals the gap between perception and reality. The stores that look best on revenue are not always the stores performing best relative to their opportunity.
# Rank stores by total revenue (the director's instinct)
revenue_rank = df.sort_values("monthly_revenue", ascending=False)[
["store_id","region","monthly_revenue"]
].reset_index(drop=True)
revenue_rank.index += 1 # start ranking at 1
print("Revenue ranking:")
print(revenue_rank.to_string())
# Rank stores by revenue per visitor (the efficiency view)
efficiency_rank = df.sort_values("rev_per_visitor", ascending=False)[
["store_id","region","rev_per_visitor","conversion_rate"]
].reset_index(drop=True)
efficiency_rank.index += 1
print("\nEfficiency ranking (revenue per visitor):")
print(efficiency_rank.to_string())
# Which stores are top-3 in revenue but NOT top-3 in efficiency?
top3_rev = set(df.nlargest(3, "monthly_revenue")["store_id"])
top3_eff = set(df.nlargest(3, "rev_per_visitor")["store_id"])
misleading = top3_rev - top3_eff
print(f"\nTop 3 by revenue: {top3_rev}")
print(f"Top 3 by efficiency: {top3_eff}")
print(f"In revenue top-3 but NOT efficiency top-3: {misleading}")
Revenue ranking:
store_id region monthly_revenue
1 ST10 South 61200
2 ST04 West 55600
3 ST01 North 48200
4 ST07 East 42100
5 ST08 West 38700
6 ST02 South 31500
7 ST05 North 27300
8 ST11 East 24600
9 ST09 North 21400
10 ST03 East 18900
11 ST12 West 19800
12 ST06 South 15800
Efficiency ranking (revenue per visitor):
store_id region rev_per_visitor conversion_rate
1 ST03 East 15.75 32.5
2 ST12 West 14.14 20.0
3 ST09 North 13.38 21.9
4 ST01 North 11.76 20.0
5 ST07 East 11.69 19.7
6 ST04 West 10.69 18.1
7 ST10 South 10.55 18.1
8 ST11 East 10.25 15.8
9 ST05 North 9.41 16.6
10 ST06 South 8.78 13.3
11 ST08 West 8.60 16.0
12 ST02 South 8.29 15.0
Top 3 by revenue: {'ST10', 'ST04', 'ST01'}
Top 3 by efficiency: {'ST03', 'ST12', 'ST09'}
In revenue top-3 but NOT efficiency top-3: {'ST10', 'ST04', 'ST01'}What just happened?
Method — set operations for ranking comparisonWe used .nlargest(3, "col") to extract the top-3 store IDs for each ranking, then wrapped them in Python set() objects. The subtraction operator on sets — set_a - set_b — returns elements in A that are not in B. This is the cleanest way to answer "which items appear in one ranked list but not another" without writing explicit loops or merges.
Every single store in the revenue top-3 — ST10, ST04, ST01 — drops out when ranked by efficiency. The efficiency top-3 is entirely different: ST03, ST12, ST09. The director has been measuring the wrong stores as top performers. ST03 ranks last on revenue but first on efficiency — it converts 32.5% of visitors, the highest in the network. If ST03 had ST10's footfall, its revenue would be dramatically higher. That is the insight that changes the conversation from "which stores are performing" to "which stores need more traffic investment."
The highest-priority intervention targets are stores with above-average footfall but below-average conversion — they have the customers coming in but are failing to close sales. We filter for this combination using boolean masks.
# Compute network-wide averages for footfall and conversion
avg_footfall = df["footfall"].mean()
avg_conversion = df["conversion_rate"].mean()
avg_atv = df["avg_transaction"].mean()
print(f"Network avg footfall: {avg_footfall:.0f} visitors")
print(f"Network avg conversion: {avg_conversion:.1f}%")
print(f"Network avg ATV: ${avg_atv:.2f}")
# Flag stores that are above average footfall but below average conversion
# These have the traffic — they just aren't converting it
high_footfall_low_conv = df[
(df["footfall"] > avg_footfall) &
(df["conversion_rate"] < avg_conversion)
][["store_id","region","footfall","conversion_rate","avg_transaction"]].sort_values(
"conversion_rate"
)
print("\nHigh footfall, low conversion stores (priority intervention):")
print(high_footfall_low_conv.to_string(index=False))
# Dual-flag: both low conversion AND low ATV vs network averages
dual_flag = df[
(df["conversion_rate"] < avg_conversion) &
(df["avg_transaction"] < avg_atv)
][["store_id","region","conversion_rate","avg_transaction"]]
print("\nDual-flag stores (low conversion AND low ATV — highest priority):")
print(dual_flag.to_string(index=False))
Network avg footfall: 3025 visitors
Network avg conversion: 19.1%
Network avg ATV: $59.37
High footfall, low conversion stores (priority intervention):
store_id region footfall conversion_rate avg_transaction
ST02 South 3800 15.0 55.26
ST08 West 4500 16.0 53.75
ST04 West 5200 18.1 59.15
ST10 South 5800 18.1 58.29
Dual-flag stores (low conversion AND low ATV — highest priority):
store_id region conversion_rate avg_transaction
ST02 South 15.0 55.26
ST08 West 16.0 53.75
ST06 South 13.3 65.83What just happened?
Method — compound boolean mask filteringWe combined two boolean conditions with & — the pandas bitwise AND operator — to filter rows that satisfy both criteria simultaneously. Each condition in parentheses returns a boolean Series; the & combines them element-by-element so only rows where both are True pass through. This is the standard pandas pattern for multi-condition filtering — always wrap each individual condition in parentheses to avoid operator precedence errors.
ST02 and ST08 are the highest-priority stores for immediate intervention — both have above-average footfall and score below average on both conversion rate and ATV. ST02 in South converts just 15% of visitors and achieves an ATV of $55.26 against a network average of $59.37. ST08 in West converts 16% with an ATV of $53.75 — the lowest ATV in the network. These stores are busy but inefficient at every stage of the customer journey. ST06 joins them on the dual-flag list despite lower footfall — its conversion rate of 13.3% is the worst in the network.
Individual store problems can be management issues. Regional patterns are systemic — they suggest competitive, demographic, or operational factors that no single store manager can fix alone. We aggregate by region to find out which direction the evidence points.
# Aggregate by region — compute mean efficiency metrics per region
regional = df.groupby("region").agg(
stores = ("store_id", "count"),
total_revenue = ("monthly_revenue", "sum"),
avg_rev_visitor = ("rev_per_visitor", "mean"),
avg_conversion = ("conversion_rate", "mean"),
avg_atv = ("avg_transaction", "mean"),
avg_rev_staff = ("rev_per_staff", "mean")
).round(2).reset_index()
regional = regional.sort_values("avg_rev_visitor", ascending=False)
print("Regional performance summary:")
print(regional.to_string(index=False))
# Which region has the highest conversion rate?
best_conv_region = regional.loc[regional["avg_conversion"].idxmax(), "region"]
print(f"\nBest conversion region: {best_conv_region}")
# Revenue contribution by region
regional["rev_share_pct"] = (
regional["total_revenue"] / regional["total_revenue"].sum() * 100
).round(1)
print("\nRevenue share by region:")
print(regional[["region","total_revenue","rev_share_pct"]].to_string(index=False))
Regional performance summary: region stores total_revenue avg_rev_visitor avg_conversion avg_atv avg_rev_staff East 3 85600 12.56 22.7 57.50 3601 North 3 96900 11.52 19.5 58.93 3474 West 3 114100 11.14 18.0 61.20 3651 South 3 108500 9.20 15.4 58.46 3357 Revenue share by region: region total_revenue rev_share_pct East 85600 21.0 North 96900 23.7 West 114100 28.0 South 108500 26.6 Best conversion region: East
What just happened?
Method — groupby().agg() across pre-computed derived columnsWe aggregated the derived metric columns — rev_per_visitor, conversion_rate, avg_transaction — that we created in Step 1. This works because those columns now exist on the DataFrame and are treated exactly like any raw column by groupby().agg(). Taking the mean of per-store efficiency metrics gives the regional average efficiency — note that this is the mean of store-level rates, not a recalculation from raw totals. For small, equal-sized groups this distinction matters less, but it is important to be aware of in production analysis.
East has the highest revenue per visitor at $12.56 and the best conversion rate at 22.7% — despite contributing the lowest total revenue (21%). East stores are the most efficient in the network but are traffic-constrained. South is the opposite: it contributes 26.6% of total revenue but has the worst efficiency metrics — $9.20 revenue per visitor and a 15.4% conversion rate. South's revenue comes from volume, not quality. The director should invest in driving more traffic to East stores while investigating why South stores are converting so poorly despite high footfall.
Checkpoint: Add a revenue per staff vs store type breakdown — use df.groupby("store_type")["rev_per_staff"].mean(). Do Large stores generate more revenue per staff member than Small ones? The answer might surprise you — and it has direct implications for whether expanding store sizes is a sound investment.
Key Findings
The revenue top-3 and efficiency top-3 share zero overlap. ST03, ST12, and ST09 lead on revenue per visitor but rank 10th, 11th, and 9th on total revenue. The director has been measuring the wrong stores as top performers.
ST03 is the standout performer — 32.5% conversion rate, the highest in the network, and $15.75 revenue per visitor. Its low total revenue is entirely a footfall problem. Investing in driving traffic to ST03 would yield higher returns than investing in any other store.
ST02 and ST08 are the highest-priority intervention targets — above-average footfall, below-average conversion, and below-average ATV. Both stores have the customers but are failing at every stage of the in-store journey. These need an immediate sales floor audit.
East is the most efficient region at 22.7% conversion and $12.56 revenue per visitor — but generates only 21% of network revenue because its stores have lower footfall. South generates 26.6% of revenue with the worst efficiency metrics in the network.
ST06 has the worst conversion rate in the network at 13.3% — only 1 in 7 visitors makes a purchase. Unlike ST02 and ST08, ST06 also has below-average footfall, making it the most challenged store overall. It needs both a traffic and a conversion intervention simultaneously.
Visualisations
Green = top performers · Red/Amber = intervention targets
Retail EDA Decision Guide
Every retail analyst encounters the same core questions. Here is the framework for approaching store performance data systematically:
| Question | Metric to Compute | pandas Method | Interpretation |
|---|---|---|---|
| Who converts best? | Conversion rate | transactions / footfall | Low = sales floor problem |
| Who extracts most value? | Revenue per visitor | revenue / footfall | Low = conversion or ATV problem |
| Who upsells best? | Average transaction value | revenue / transactions | Low = product mix or staff problem |
| Which region underperforms? | Regional mean efficiency | groupby().agg(mean) | Regional = systemic, not store-specific |
| Which stores need urgent action? | Dual-flag filter | boolean mask with & | Low conversion AND low ATV together |
Analyst's Note
Teacher's Note
What Would Come Next?
A senior analyst would build a store segmentation matrix plotting conversion rate against ATV across four quadrants, and a regression model using footfall and store type to identify stores performing below their predicted revenue.
Limitations of This Analysis
A single month of data cannot distinguish seasonal variation from structural performance. Store size, catchment demographics, and local competition all legitimately affect footfall and conversion independently of management quality.
Business Decisions This Could Drive
Invest marketing budget in driving footfall to East stores, commission a sales floor audit at ST02 and ST08, and review ST06's long-term viability — if the location cannot be improved, a format change may be the correct decision.
Practice Questions
1. Which store had the highest revenue per visitor in the Meridian dataset, despite ranking last on total revenue?
2. Which region had the highest average conversion rate and revenue per visitor despite generating the lowest total revenue?
3. Write the formula used to compute conversion rate in this case study.
Quiz
1. When filtering with df[(df["a"] > x) & (df["b"] < y)], what does the & operator do?
2. A store has 5,000 monthly visitors but only a 12% conversion rate. What does this pattern indicate, and what should the director investigate?
3. What does the expression top3_rev - top3_eff return when both variables are Python sets?
Up Next · Case Study 6
Student Score Analysis
You are handed an exam results dataset from a group of 12 students. Which subjects show the widest score variance? Do study hours actually predict performance? And which students are at risk of failing before the term ends?