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_week

Move 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.

© 2026 Coded Level — Toronto analytics studio.Toronto, ON · Canada