DS Case Study 5 – Retail Store Performance | Dataplexa
Beginner Case Study · CS 5

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.

IndustryRetail
TechniqueEDA · Derived Metrics
Librariespandas · numpy
DifficultyBeginner
Est. Time40–50 min
Overview

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

1

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.
2

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.
3

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.
4

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.
5

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).
01

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_idregionstore_typemonthly_revenuefootfalltransactionsstaff_count
ST01NorthLarge$48,2004,10082014
ST02SouthMedium$31,5003,8005709
ST03EastSmall$18,9001,2003905
ST04WestLarge$55,6005,20094016
ST05NorthMedium$27,3002,9004808

Showing first 5 of 12 rows · 7 columns

store_idstring · unique identifier

Unique store reference code. Used for labelling — never aggregated directly.

regionobject · 4 categories

North, South, East, West. The primary grouping dimension for the regional comparison analysis.

store_typeobject · 3 categories

Large, Medium, or Small — by floor area. Used to contextualise performance differences between store sizes.

monthly_revenueint64 · USD

Total revenue generated in the month. The primary headline metric — but meaningless without footfall context.

footfallint64 · visitor count

Total number of people who entered the store during the month. The denominator for all efficiency metrics.

transactionsint64 · count

Number of completed purchases. Used to compute conversion rate (transactions ÷ footfall) and ATV (revenue ÷ transactions).

staff_countint64 · headcount

Number of staff on the floor. Used to compute revenue per staff member — a labour efficiency metric.

02

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.

1

Which store has the highest revenue per visitor — and does it match the store with the highest total revenue?

2

Which stores have the lowest conversion rate despite having above-average footfall — these are the highest-priority intervention targets?

3

Which region has the highest average revenue per visitor, and does regional performance correlate with store type mix?

4

What is the average transaction value (ATV) per store — and which stores are under-performing on ATV relative to their region average?

5

Which stores should the director prioritise for immediate review — combining low conversion and low ATV as a dual-flag signal?

03

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.

Step 1Load, Inspect, and Compute Derived Efficiency Metrics

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           3960

What just happened?

Method — creating multiple derived columns

We 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.

Business Insight

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.

Step 2Store Performance Ranking — Raw vs Efficiency

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 comparison

We 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.

Business Insight

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."

Step 3Identify Low-Conversion, High-Footfall Stores

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.83

What just happened?

Method — compound boolean mask filtering

We 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.

Business Insight

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.

Step 4Regional Performance Comparison

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 columns

We 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.

Business Insight

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.

04

Key Findings

01

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.

02

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.

03

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.

04

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.

05

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.

05

Visualisations

Revenue Per Visitor by Store
Efficiency ranking — normalised for footfall differences
ST03 · East
$15.75
$15.75
ST12 · West
$14.14
$14.14
ST09 · North
$13.38
$13.38
ST01 · North
$11.76
$11.76
ST08 · West
$8.60
$8.60
ST02 · South
$8.29
$8.29

Green = top performers · Red/Amber = intervention targets

Conversion Rate by Store
% of visitors who make a purchase · avg = 19.1%
ST03 · East
32.5%
32.5%
ST09 · North
21.9%
21.9%
ST01 · North
20.0%
20.0%
ST08 · West
16.0%
16.0%
ST02 · South
15.0%
15.0%
ST06 · South
13.3%
13.3%
Regional Efficiency Comparison
Revenue per visitor by region — East leads despite lowest total revenue
East
$12.56
$12.56
North
$11.52
$11.52
West
$11.14
$11.14
South
$9.20
$9.20
06

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 ratetransactions / footfallLow = sales floor problem
Who extracts most value?Revenue per visitorrevenue / footfallLow = conversion or ATV problem
Who upsells best?Average transaction valuerevenue / transactionsLow = product mix or staff problem
Which region underperforms?Regional mean efficiencygroupby().agg(mean)Regional = systemic, not store-specific
Which stores need urgent action?Dual-flag filterboolean mask with &Low conversion AND low ATV together
07

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?