What this is

On this chain's own data, almost no markdown pays for itself

A fresh-grocery chain discounts on 51.4% of all store-days, and its own demand response says almost none of those markdowns should happen. Across 2,457,493 fully-stocked store-days from 898 stores and 865 perishable SKUs (FreshRetailNet-50K, CC BY 4.0), the discount elasticity of demand is -1.49 (95% CI [-1.51, -1.46]) once store-product and day fixed effects and the chain's own marketing flag are held constant. For constant-elasticity demand the profit-maximising gross margin is one over that, so a markdown only improves profit on a product already carrying more than a 67.3% full-price margin — and fresh grocery does not run 67% margins. Of 23 categories with enough data to estimate separately, 1 clears the bar at a 30% margin. Separately, 11% of the apparent price response turns out to be the campaign rather than the price: a retailer does not cut a price silently, and an elasticity estimated without separating the two credits the promotion's traffic to the discount.

This is a real analysis on a real dataset, not an illustration. The code is published, it runs in under two minutes, and a verification script re-runs it from scratch and fails if any headline number moves.

Every figure below is read from results.json, which is written by the analysis itself. Where the result is unflattering it is published unchanged — the findings and the limitations both come straight out of the run.

Source figures are in the currency of the data: sterling for the UK retail and wholesale sets, euro for the Portuguese bank set. Dollar amounts are converted at 1.55 USD/GBP and 1.40 USD/EUR, roughly the averages for the periods the data covers — stated assumptions, not live rates.

How this is checked

verify.py --only 24 deletes this project's results, re-runs it and diffs every published number before the page ships.

The pipeline
Findings

What the analysis found, including the results that went against us

Each of these came out of the run. None has been softened.

THE ELASTICITY IS -1.49 AND THAT IS NOT ELASTIC ENOUGH. Across 2,457,493 fully-stocked store-days, with store-product and day fixed effects and the chain's own marketing flag held constant, a one per cent price cut raises units sold by 1.49 per cent (95% CI [-1.51, -1.46]). For constant-elasticity demand the profit-maximising gross margin is one over that, so a markdown only improves profit on a product already carrying more than a 67.3% full-price margin.

How deep the markdowns go

Store-days by markdown depth. The chain's discounting is concentrated in shallow cuts, which is where the profit arithmetic is least forgiving.

run.py section 7: distribution of applied discount depth.

FRESH GROCERY DOES NOT RUN 67% MARGINS, AND THE CHAIN DISCOUNTS ANYWAY. 51.4% of store-days carry a discount, at a median depth of 14.4%. The threshold the chain's own demand response sets is above any plausible fresh-grocery margin, so on this evidence the markdown programme is not a profit-optimising exercise. It is doing something else — clearing stock, defending footfall, matching a competitor — and those are legitimate aims that the elasticity framework sold as 'markdown optimisation' does not price.

Elasticity by category

Discount elasticity of demand per product category, each with store-product and day fixed effects. None is elastic enough to justify markdowns at fresh-grocery margins.

run.py section 6: per-category two-way fixed-effects estimates.

11% OF THE APPARENT PRICE RESPONSE WAS THE CAMPAIGN, NOT THE PRICE. With fixed effects but no marketing control the elasticity is -1.66; adding the chain's own activity flag moves it to -1.49. A retailer does not cut a price silently — it runs a promotion — and an elasticity estimated without separating the two credits the campaign's traffic to the discount, then recommends deeper discounts on the strength of it.

What the controls remove

The estimated price response shrinks as confounders are removed. The last step separates the campaign from the price cut.

run.py sections 3 and 4.

NO CATEGORY CLEARS THE BAR AT A REALISTIC MARGIN. Of 23 product categories with enough data to estimate separately, 0 clear the threshold at a 25% gross margin and 1 at 30%. Even at an implausible 50% margin only 8 do. The most elastic category sits at -3.85 and the least at -0.50, so this is not an average concealing a profitable subset.

THE UNCONTROLLED NUMBER IS 1.03 AND IT IS THE ONE A SPREADSHEET PRODUCES. Regressing log units on log price with nothing held constant gives -1.03. Every difference between stores, products and seasons lands in that coefficient. It is published first because it is what the tooling gives you by default, and the distance between it and -1.49 is the cost of not asking what else was moving.

MOST STORE-DAYS COULD NOT MEASURE DEMAND AT ALL. 44.1% of store-days carry at least one hour with an empty shelf, averaging 3.2 hours out of the sixteen-hour trading day. Sales on those days measure supply, not demand, and a markdown study that includes them understates the response. Only 2,504,437 store-days were fully stocked, and the estimate uses those; this file is unusual in making that distinction possible at all.

A SENTINEL VALUE WOULD HAVE DOMINATED THE ESTIMATE. 16,039 rows carry a discount of exactly zero — free — and they average 14.5 units against roughly one for the file as a whole. They are not hundred-per-cent markdowns, they are a placeholder, and because log(0) is undefined the only way to keep them is a transformation that would have let sixteen-times-typical volume anchor the price response.

The detail

Full results tables

What each specification gives

What each specification gives
SpecificationElasticityStd. errorImplied margin threshold
No controls-1.0330.015496.8%
Store-product + day fixed effects-1.66160.2%
+ marketing activity and holiday flags-1.4850.013467.3%

Would a markdown pay for itself at this gross margin?

Would a markdown pay for itself at this gross margin?
Full-price gross marginMarkdown improves profit
20%no
25%no
30%no
35%no
40%no
50%no
60%no
70%yes

Where the store-days go

Where the store-days go
StageCountShare
Store-days in the file4,500,000100.0%
After removing the zero-discount sentinel and price rises4,483,92799.6%
Fully in stock all day2,504,43755.7%
…and with positive sales, used for estimation2,457,49354.6%

Limitations

Stated by the analysis, not added afterwards. A project without these is not finished.

  • The discount is chosen by the retailer, not assigned at random, and the timing is the problem. Markdowns are plausibly triggered by weak demand, excess stock or approaching expiry — exactly the variables the estimate wants to hold constant and cannot observe. Store-product and day fixed effects absorb persistent differences and chain-wide shocks, not a decision made about this product on this morning. The elasticity should be read as a conditional association, and if markdowns are timed to expiry it is biased toward zero.
  • The profit threshold assumes constant-elasticity demand and a constant unit cost, which is what the Lerner condition requires. Real markdown decisions on perishables also involve salvage value, waste-disposal cost and the option value of holding stock another day, none of which are in this file. The threshold is the bar under the retailer's own demand response, not a full profit model.
  • No cost data exists. Gross margin is swept across a plausible range rather than measured, because no public retail dataset carries a genuine unit cost alongside price and volume — the one that does, Dominick's Finer Foods, is licensed for academic use only. Every statement about profitability is conditional on the margin assumed, and the sweep is published so the reader can pick their own.
  • This is one chain, one country, 90 days (2024-03-28 to 2024-06-25), and perishables only. Fresh grocery has short shelf lives and high waste, so the markdown calculus differs from apparel or electronics where clearance is seasonal rather than daily. Nothing here transfers to those categories without re-estimation.
  • Estimation drops 46,944 fully-stocked store-days with zero sales because log(0) is undefined. Those are genuine zero-demand days rather than missing data, and excluding them conditions the estimate on the product having sold at all, which will understate how much a discount moves a product that would otherwise not sell.
  • The source is a HuggingFace dataset repository whose default branch moves — it was last modified 2026-01-09. Every figure here comes from revision 08c1fab7f925, asserted by digest at load, so a later run reproduces these numbers rather than silently reporting whatever the branch points at now.

Data: FreshRetailNet-50K (Dingdong Inc.), CC BY 4.0 — the dataset card states it is ready for commercial use. 4,500,000 rows, 2024-03-28 to 2024-06-25.

Libraries and methods this analysis used

Read from this project's own run.py when the page was built — 496 lines of it. Not a list of everything we know; a list of what this analysis imports and calls.

10 names from NumPy appear in this analysis.

  • bincount
  • clip
  • column_stack
  • diag
  • errstate
  • isfinite
  • linalg
  • log
  • ones
  • sqrt
Verification

How every number on this page is checked

A script deletes each result file, re-runs the project and diffs the output. If a headline figure moves, the check fails, and the page does not ship. That is the only reason to believe anything on this page.

Running this method on your own data

The method above transfers; the result will not. Send us a extract and we will tell you what is forecastable in it and what is not, before anyone signs anything.