← All projects

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

Store-week panel → Counterfactual baseline → Lift + elasticity → Incremental margin → MILP promotion calendar

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 depth5%10%15%20%25%30%35%
Lift1.381.712.002.472.973.664.37
MetricHistoricalOptimized
Promotions269280 (capped at 7 per store)
Trade spend2.01B0.75B (under the same ceiling)
Incremental margin-143.6M+321.4M
Share margin-negative55.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.