Contribution Margin by Product Category
Three numbers that all sound like "which products actually make money", and the allocation decisions that sit underneath the one worth trusting. Including the ones your reporting tool made for you without saying so.
Three numbers get used interchangeably in store meetings: gross margin, contribution margin, and "profit by product". They are different calculations, they can point in opposite directions on the same category, and the disagreement is rarely about arithmetic. It is about what got subtracted and how order-level money was pushed down to a product line.
This is the calculation, the four decisions underneath it, and what happens to each of them if you do not make the decision yourself.
The ladder, stated once
Gross sales sum of line totals
Net sales gross sales − discounts − refunds
Gross profit gross sales − discounts − cost of goods
Contribution margin gross sales − discounts − cost of goods
− refunds − attributed ad spend
Each rung answers a different question. Net sales is what the customer actually paid you and kept. Gross profit is what was left after the goods. Contribution margin is what was left after the goods and after the cost of persuading someone to buy them.
The last one is the only rung on which a category can be negative while looking healthy on every rung above it, which is why it is worth the trouble.
Why gross margin misleads on paid traffic
A worked pair, from one month of a store selling both:
Accessories Outerwear
Gross sales 84,000 210,000
Discounts (6,300) (9,800)
Cost of goods (28,500) (121,000)
-------- --------
Gross profit 49,200 79,200
Gross margin 58.6% 37.7%
Refunds (1,900) (14,600)
Ad spend (52,400) (31,000)
-------- --------
Contribution margin (5,100) 33,600
On gross margin, accessories look like the better business by twenty-one points. On contribution margin they lose money. Nothing is wrong with the product: the category simply does not sell without paid traffic, and the traffic costs more than the goods make.
This is the whole reason the metric exists. A store that ranks categories by gross margin will keep buying traffic for the category that is quietly funding its own losses, and the decision looks well-informed the entire time.
The four choices, and what each one costs
Every figure above required a decision that the formula does not contain. Four of them, in the order they bite.
1. Which date does each component belong to
An order placed on 28 January and refunded on 14 February. A campaign that ran all of January and drove an order that landed on 2 February. Sales bucket by order date; refunds could bucket by refund date or by the date of the order they undo; ad spend buckets by the day the money was spent or by the date of the order it is credited with.
None of these is wrong. They give different monthly series, and a store that has not chosen will get different series from two reports built a fortnight apart. We bucket sales and refunds by the order date, so a refund reduces the month that recorded the sale, and ad spend by the spend date, because that is the month the money actually left.
2. How order-level money gets down to a line
Discounts and refunds are recorded against the order. Product category is a property of the line. Something has to bridge them, and the honest bridge is proportional to line revenue within that order:
Order #1041 jacket 60.00 + shirt 40.00 = 100.00, discount 20.00
jacket share 60/100 → 12.00
shirt share 40/100 → 8.00
The trap is the version that allocates by each line share of the month instead of the order. It reconciles perfectly — the grand total is identical — and it is wrong per category, because it takes discount from orders that had none and gives it to orders that did. A category-level check will not catch it. Only recomputing one order by hand will.
3. What happens to ad spend that matches no order
Some spend never resolves. A campaign with no conversions, a UTM that was not recorded, a channel with no link to orders at all. Three options, in rising order of honesty:
- Drop it. The categories now sum to less than the total and nothing says so.
- Spread it across everything. Every category gets charged for spend that demonstrably reached none of them.
- Report it as its own row. An
Unallocatedline that shows exactly how much spend the attribution could not place.
The third is the one we use, and the row is frequently the most informative thing in the table. If a fifth of your ad spend cannot be attributed to any order, the category breakdown is a smaller finding than the attribution gap.
4. What counts as an order at all
Pending payments, unfulfilled orders, test orders, drafts, cancellations. Include them and revenue is overstated by things that may never happen; exclude them and a fulfilment backlog shows up as a bad sales month. Whichever you choose, it has to be the same choice in every report, or two people comparing figures are comparing populations.
The four decisions above are always made. The only variable is whether they are made deliberately by you or incidentally by whoever wrote the formula, and an incidental choice is invisible precisely because nobody remembers making it.
The cheapest defence I know is to put the choice in the label. “Contribution margin (ad spend by spend date, unmatched shown separately)” is a long column header and it has ended more meetings early than any dashboard I have built. A definitions page gets skipped. A column header cannot be.
Doing it in Excel
With an order-line export in a table named Lines, orders in Orders and a variant cost list in Costs, the allocation is the only fiddly part. Add two helper columns to Lines:
Order revenue =SUMIFS(Lines[Line total],Lines[Order],[@Order]) Line share =[@[Line total]]/[@[Order revenue]] Alloc discount =[@[Line share]]*SUMIFS(Orders[Discount],Orders[Order],[@Order]) Line cost =[@Qty]*IFERROR(SUMIFS(Costs[Unit cost],Costs[SKU],[@SKU]),0)
Then the category roll-up is four SUMIFS against the same criteria:
Gross sales =SUMIFS(Lines[Line total],Lines[Category],$A2) Discounts =SUMIFS(Lines[Alloc discount],Lines[Category],$A2) Cost of goods =SUMIFS(Lines[Line cost],Lines[Category],$A2) Contribution =B2-C2-D2-refunds-ad spend
Note the IFERROR around the cost lookup. A SKU with no cost row silently returns zero cost, which reports that category as pure profit — the single most common way a margin report flatters a product line. Better to return an error deliberately and count the gaps before you trust anything. If SUMIFS with several conditions is unfamiliar, the multiple-criteria walkthrough covers it properly.
Refunds and ad spend need the same allocation treatment against their own exports, which is where a workbook usually stops being maintainable. Two exports is a Saturday. Four exports, monthly, is a job.
The check that catches most of it
Before showing anyone a category breakdown, sum it and compare it against the same metric with no grouping at all. They should be equal to the cent.
If the breakdown is smaller, something is falling out: an allocation that does not sum back, spend that matched nothing, or product lines whose category is blank. That last one is worth checking by hand — a blank category does not appear as a row called blank. It simply is not there, and the table looks complete.
Doing it without rebuilding the workbook every month
All of the above works in a spreadsheet, and a working spreadsheet needs no replacing. The cost is not the arithmetic; it is that four exports have to be re-joined every month and the four decisions live in cell references only one person can read.
That is the shape Quiriz for e-commerce takes. The store, refund and ad exports are uploaded, contribution margin is defined once in the product rather than in a formula bar, and contribution margin by product category returns the same figure whoever asks. In Excel that is =QUIRIZ.ASK("contribution margin by product category in 2025", "table"); in the app it is the same sentence in a box.
The allocation choices above are the product defaults, and it says which it used alongside the figure rather than leaving you to infer it. Unattributable ad spend comes back as an Unallocated row instead of disappearing.
The honest boundary: contribution margin is not net profit, and we will not present it as though it were. Overhead, salaries, software and processing fees are not in your store export and we do not invent them. If the question you actually have is whether the business made money last month, that is an accounting question and this is not the tool for it.
Ask your store exports which categories actually pay for themselves
Upload your Shopify order, refund and ad exports and ask for contribution margin by product category in plain English. Free to start.
Try Quiriz free →