Retail sales analysis: separate price, volume and mix
In this article (5 sections)
Retail revenue can fall even when every product's unit price rises and total units stay unchanged. A shift toward cheaper products can outweigh the price increase. Price-volume-mix analysis makes that arithmetic visible, provided the effects are defined consistently and reconcile to the actual revenue change.
There is more than one way to allocate interactions between price and quantity. State the convention rather than presenting one decomposition as the uniquely correct causal explanation.
Inspect a two-product example
The original synthetic retail-period fixture contains:
| Product | Base units | Base price paise | Current units | Current price paise |
|---|---|---|---|---|
| Basic | 100 | 1,000 | 120 | 1,100 |
| Premium | 50 | 3,000 | 30 | 3,300 |
Base revenue is 250,000 paise and current revenue is 231,000. Units remain 150, while each product's price increases 10%. Revenue still falls by 19,000 paise because premium units lose share.
The average realized price falls from approximately 1,666.67 to 1,540 paise per unit. Calling that an across-the-board price reduction would be wrong: it reflects the combined effect of product mix and individual prices.
Define a bridge that adds up
Let Q0 and Q1 be total units, and let each product have base quantity q0, current quantity q1, base price p0 and current price p1.
Use these effects:
- Volume:
(Q1 − Q0) × base average price. - Mix: sum of
(q1 − Q1 × q0/Q0) × p0across products. - Price: sum of
q1 × (p1 − p0)across products.
This evaluates volume and mix at base prices and the price change at current quantities. Adding the three effects to base revenue reproduces current revenue for the matched products in this fixture.
from math import isclose
from build_and_verify import database
db = database()
base = {p:(q,v) for p,q,v in db.execute("SELECT product,units,unit_price_paise FROM retail_periods WHERE period='base'")}
current = {p:(q,v) for p,q,v in db.execute("SELECT product,units,unit_price_paise FROM retail_periods WHERE period='current'")}
db.close()
assert base.keys() == current.keys()
q0, q1 = sum(q for q,p in base.values()), sum(q for q,p in current.values())
assert q0 > 0
r0 = sum(q*p for q,p in base.values())
r1 = sum(q*p for q,p in current.values())
volume = (q1-q0)*r0/q0
mix = sum((current[p][0]-q1*base[p][0]/q0)*base[p][1] for p in base)
price = sum(current[p][0]*(current[p][1]-base[p][1]) for p in base)
assert (r0,r1) == (250000,231000)
assert isclose(volume,0) and isclose(mix,-40000) and price == 21000
assert isclose(r0+volume+mix+price,r1)
print({'base_revenue_paise':r0,'volume':volume,'mix':mix,
'price':price,'current_revenue_paise':r1})The bridge is 250,000 + 0 − 40,000 + 21,000 = 231,000 paise. The negative mix effect exceeds the positive price effect.
Keep arithmetic effects separate from business causes
The bridge identifies which measured components changed. It does not establish why customers bought fewer premium units. Availability, promotion, customer composition, assortment changes and competing products are possible explanations requiring additional evidence.
If a price increase itself changes demand, the price and quantity changes are economically connected even though the bridge displays separate arithmetic components. Do not interpret the price bar as the causal revenue gain that would have occurred with all behavior otherwise unchanged.
Handle products that are not comparable
The example asserts that both periods contain the same product set. New products have no observed base price, and discontinued products require an explicit treatment. A practical report can separate matched-product effects from entry and exit effects, with the complete bridge still reconciling.
Also standardize units, currency and net-price definitions. A pack-size change can alter the meaning of a “unit.” Discounts, returns and tax can make an invoice average differ from a list price. Record the chosen scope before comparing periods.
For multiple categories, calculate a consistent underlying bridge before aggregation. Recomputing effects independently at each hierarchy level can change the reference mix and produce apparently inconsistent totals.
Exercise: increase current total units while preserving the current product shares. Recalculate all three effects and verify the bridge. Then add a new product and explain why the matched-product assertion should fail until an entry treatment is defined.
NeuraPath's Data Analytics with Generative AI course connects business arithmetic with reproducible analysis. A useful sales bridge reconciles the numbers and makes its allocation assumptions visible.
Continue learning
This article is part of the Domain analytics and business cases sequence. Use the neighbouring tasks when you need the prerequisite or the next application.
- Continue with Inventory ageing: identify slow stock without misleading averages.
- Then apply it in Stockout analysis with incomplete availability data.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.
Explore Data Analytics with Generative AI