001 · Selected work · Mid-market retail · 2024
Promotional lift,
measured. Not guessed.
Rebuilding the inventory-to-sales attribution stack at a multi-banner mid-market retailer — and proving, for the first time, which promos in a $20M shopper-marketing program actually earned their lift.
Duration: 11 weeks · Team: 3 · Programs: Diagnose · Build · Advance · Year: 2024
Brief
The ask
We were asked to fix the weekly promotional-reporting cycle at a multi-banner mid-market retailer running a $20M annual shopper-marketing program. The CRO wanted one number on Tuesday: which promos paid back, at which banner, on which SKUs. The merchandising director wanted to stop arguing about whose spreadsheet was right. The constraint we came in with was honest — eleven weeks, three people, a working dashboard the merch director would defend in a CFO review.
Starting state: nine Shopify storefronts exporting on different cadences, three ad platforms (Meta, Google, TikTok) with no shared spend taxonomy, an in-store POS feed arriving Sunday nights as a 400MB CSV with no schema contract, and a Google Sheet stitched together every Monday morning by an analyst burning three working days a week on it. BigQuery and Fivetran were already paid for. dbt and Looker were on the shelf, unused. No warehouse migration, no new BI tool license, no headcount.
Problem
The real diagnosis
The reported symptom was "Monday reporting is a nightmare." The actual problem was that nobody could agree on what "revenue" meant. We counted forty-seven distinct definitions across the merch team, the finance team, and the two agencies running paid spend — gross, net, in-store, ecom, with-tax, ex-tax, returned, kept, attributed, halo-included, halo-excluded. Each weekly report silently picked a different set of rules. Nobody could tell which.
Underneath that: nine Shopify storefronts with no shared SKU spine. SKU joins were failing on punctuation differences and banner-specific suffixes. The Monday analyst was reconciling identifiers by hand. Ad spend was attributed to promo via campaign names neither agency had ever standardized. POS landed on its own schedule, with no contract on freshness or completeness. The Tuesday meeting was running on numbers that, when we audited them, were wrong by 14–22% depending on the week.
The diagnosis: not slow reporting, and not an attribution problem in the modeling sense. A measurement-trust problem. A $20M program was being priced on instinct because lift was never instrumented at the grain anyone could defend. The quarterly status deck was a story told after the fact. The merch director had stopped believing it and was making allocation calls from the Friday store visits. Until lift could be read at SKU × banner × week against a baseline, no promo could be cut and no promo could be defended.
Approach
Five moves, in order
Move 01
Pick one definition of net revenue. Get it signed.
Week one was a written audit of all forty-seven definitions. We interviewed merch, finance, and both agencies and produced a one-page metric contract: net revenue, ex-tax, ex-shipping, returns netted in the week of return, halo store-level traffic excluded, coupons treated as cost-of-sale not as revenue reduction. The CRO signed it before we wrote a line of SQL. The hardest part of the engagement was the agreement, not the model.
Move 02
Build the SKU spine across nine storefronts.
Identity resolution, but for inventory. A canonical SKU spine that survived storefront-specific punctuation, banner-suffixed handles, and three legacy product taxonomies. Fivetran landed nine Shopify schemas, three ad platforms, and the POS SFTP drop in bronze load-as-is. Silver was where the spine got resolved — a deterministic crosswalk on UPC plus a fuzzy fallback for the long tail of POS items missing UPCs. Every row downstream of silver keys to one canonical SKU.
Move 03
Build the lift mart. SKU × banner × week, against a real baseline.
One row per acquisition cohort isn't the grain retail needs. Retail needs lift, and lift needs a baseline. We modeled an eight-week trailing baseline per SKU × banner (promo weeks excluded from the baseline window), then compared in-market promo weeks against it. The mart attributed paid and co-op spend onto the same grain via the campaign-naming convention we finally got the agencies to enforce. This is the model that ended the argument:
-- models/marts/mart_promotional_lift.sql
-- One row per (promo_id, sku_id, banner, fiscal_week).
-- Reads net revenue against an 8-week trailing baseline
-- (promo weeks excluded), attributes paid + co-op spend,
-- returns lift_ratio per the signed metric contract.
with sales as (
select
sku_id,
banner,
fiscal_week,
sum(net_revenue_ex_tax) as net_revenue,
sum(units_sold) as units_sold
from {{ ref('silver_sales_unified') }} -- POS + ecom on one SKU spine
where fiscal_week >= date_sub(current_date(), interval 18 month)
group by 1, 2, 3
),
calendar as (
select
promo_id,
promo_name,
vehicle,
banner,
sku_id,
fiscal_week,
is_in_market
from {{ ref('dim_promo_calendar') }} -- one row per promo-touch
),
spend as (
select
promo_id,
sku_id,
banner,
fiscal_week,
sum(paid_spend_usd) as paid_spend,
sum(coop_spend_usd) as coop_spend
from {{ ref('silver_promo_spend') }} -- tagged by campaign convention
group by 1, 2, 3, 4
),
baseline as (
-- 8-week trailing baseline per sku x banner,
-- promo weeks excluded so promo lift can't inflate its own baseline.
select
s.sku_id,
s.banner,
s.fiscal_week,
avg(case when c.is_in_market then null else s.net_revenue end)
over (
partition by s.sku_id, s.banner
order by s.fiscal_week
rows between 8 preceding and 1 preceding
) as baseline_revenue
from sales s
left join calendar c
on c.sku_id = s.sku_id
and c.banner = s.banner
and c.fiscal_week = s.fiscal_week
)
select
c.promo_id,
c.promo_name,
c.vehicle,
s.banner,
s.sku_id,
s.fiscal_week,
s.net_revenue,
s.units_sold,
b.baseline_revenue,
s.net_revenue - b.baseline_revenue as gross_lift,
p.paid_spend + p.coop_spend as total_spend,
safe_divide(
s.net_revenue - b.baseline_revenue,
nullif(p.paid_spend + p.coop_spend, 0)
) as lift_ratio,
safe_divide(s.net_revenue, b.baseline_revenue) as index_vs_baseline
from sales s
join calendar c
on c.sku_id = s.sku_id
and c.banner = s.banner
and c.fiscal_week = s.fiscal_week
and c.is_in_market = true
left join spend p
on p.promo_id = c.promo_id
and p.sku_id = s.sku_id
and p.banner = s.banner
and p.fiscal_week = s.fiscal_week
left join baseline b
on b.sku_id = s.sku_id
and b.banner = s.banner
and b.fiscal_week = s.fiscal_weekMove 04
Wire one Looker dashboard against the mart. Kill the quarterly deck.
Five views, all sourced from mart_promotional_lift: promo-level lift, banner roll-up, SKU outliers, week-over-week deltas, and an open-questions panel where the merch team flagged anomalies for the analyst to investigate before Tuesday. The quarterly status deck became a weekly attributed-lift review. Promotions that didn't earn their lift inside two consecutive cycles got cut by the merch director in the meeting. Six did, in the first quarter.
Move 05
Hand it to merchandising. Document the contract.
Every dbt model carries a YAML description, a freshness test, and a row-count assertion. Every metric in the dashboard names an owner. The handover packet is a forty-page Notion any new analyst can read on day one, with the metric contract pinned to the front. POS by 04:00 ET Monday, Shopify by 06:00, ad platforms by 07:00, Tuesday meeting at 10:00. We were out by week eleven. The merch team owns the model.
Outcome
3 days
to 4 hours.
The Tuesday meeting now runs on a dashboard the merch director defends in front of the CRO. In the first quarter on the new mart, six promotions failed to earn their lift two cycles running and were cut. The merch team kept the budget and redeployed it.
3 days → 4 hours
Weekly promotional-lift reporting cycle for the merchandising team.
$20M+
Promotional spend instrumented at SKU × banner × week and reconciled to actual lift.
47 → 1
Competing definitions of revenue collapsed into one governed metric, signed by the CRO.
11 weeks
Total engagement, from kickoff to a merchandising team running its own attributed-lift review.
What this enabled, beyond the headline: the merch team now defends its own number. Finance reconciled the Q4 close against the same mart and stopped running a parallel report. The two competing executive scorecards on the CRO's desk were retired. The one-page metric contract signed in week one is still the document on the wall — and it's still the one in use.
Credits
Who shipped this
- Lead Analyst
- Haris Naeem
- Data Engineering
- Contractor — anonymized
- Analytics Engineering
- Contractor — anonymized
- Client Stakeholder
- VP Marketing — anonymized
- Client Stakeholder
- Director, Merchandising — anonymized
- Client Sponsor
- Chief Revenue Officer — anonymized
Client-side names withheld for confidentiality. Named references available on request after a discovery call.
Next
Defending a promotional budget on instinct?
Send a note describing the banner mix, the spend you're answering for, and where your promo reporting and your finance close currently disagree. If it's a fit, a named reference from this engagement is available after a discovery call.