Case study · Data science & causal inference
Promotion Lift and Trade Optimization
Whether a promotion created profit or merely moved volume is a causal question, and most trade reporting cannot answer it because it has no counterfactual. This system builds one: a no-promo baseline, a controlled price elasticity, a promotion lift curve, incremental margin net of the discount, and a mixed-integer program that plans the next calendar under a fixed budget.
At a glance
- The question
- Did a promotion create incremental profit, or move volume that would have sold anyway
- Counterfactual
- A no-promo demand baseline trained only on non-promoted store-weeks
- Elasticity
- Log-log price response, naive -0.25 versus controlled -1.45, against a true -1.5
- Lift curve
- Observed over counterfactual demand by discount depth: 1.38 at 5% rising to 4.37 at 35%
- Incremental margin
- Promoted margin minus no-promo margin, net of the discount on every promoted unit
- Optimization
- Mixed-integer program: one depth per store-week under a fixed budget and a per-store frequency cap
- Finding
- 55% of promotions were margin-negative; the program was net-negative in aggregate
- Result
- Same budget, margin from -143.6M to +321.4M (+465.0M), Python, scipy, Dagster, 8 tests
Pipeline
What I built
- Counterfactual demand. A gradient-boosted no-promo baseline trained on non-promoted store-weeks, so every promoted week can be compared to what would plausibly have happened without the promotion.
- Price elasticity, done honestly. A log-log regression of units on price, shown both naively and with controls for discount depth, display, season, and store fixed effects. The naive estimate is confounded because price moves mainly during promotions.
- Lift curve and incremental margin. The observed-to-counterfactual ratio by discount depth, and incremental profit computed as promoted margin minus no-promo margin, charging the discount on all promoted units rather than only the incremental ones.
- Budget-constrained planner. A mixed-integer program (scipy.optimize.milp) selects at most one discount depth per store-week to maximize incremental margin under the trade budget, with a per-store frequency cap so the solver cannot simply promote everything.
- Orchestration and tests. A Dagster job wires data, lift metrics, the optimized plan, and reporting; 8 tests cover elasticity recovery, the confounding contrast, lift positivity, and the optimizer's budget and frequency constraints.
Results
Synthetic panel, 40 stores, 104 weeks, 26-week evaluation window.
| Elasticity estimate (true -1.5) | Value |
|---|---|
| Naive (log price only) | -0.25 |
| Controlled (discount, display, season, store) | -1.45 |
| Discount depth | 5% | 10% | 15% | 20% | 25% | 30% | 35% |
|---|---|---|---|---|---|---|---|
| Lift | 1.38 | 1.71 | 2.00 | 2.47 | 2.97 | 3.66 | 4.37 |
| Metric | Historical | Optimized |
|---|---|---|
| Promotions | 269 | 280 (capped at 7 per store) |
| Trade spend | 2.01B | 0.75B (under the same ceiling) |
| Incremental margin | -143.6M | +321.4M |
| Share margin-negative | 55.0% | - |
Decisions worth noting
- Why a counterfactual, not promoted sales. Promoted sales count units that would have sold anyway, and the discount is given on all of them. Without a no-promo baseline, promotion ROI is unknowable.
- Why the naive elasticity is shown. Price is near-unidentifiable when it only moves during promotions, so the naive estimate is biased. Showing both makes the confounding visible instead of hiding it.
- Why regular price variation was added to the panel. To identify elasticity honestly, price must move for reasons other than the promotion. That is a data-design point, not a modelling trick.
- Why a frequency cap in the optimizer. Left unconstrained, the solver promotes every store every week because shallow discounts are individually profitable. Real calendars are constrained, so the model is too.
Honest notes
All figures are synthetic. The panel embeds a known elasticity and lift, which is what makes the recovery checkable. These are not a business result.
This is a portfolio project. No retailer's promotion calendar was used, and the planner is not wired into a trade-spend system.
Real promotion response is richer. Cannibalization across SKUs, display compliance, and competitor response are all absent here; the structure leaves room for them as additional data and constraints.