Jane Doe | Marketing Data Scientist
  • Home
  • Case Studies
    • Marketing Mix Modeling (Scrollytelling)
    • A/B Testing Pitch (Deck)

Attributing Revenue: A Marketing Mix Model

How we reallocated $2M in ad spend to increase total ROAS by 12%.

Python
MMM
Scrollytelling
ROI
Author

[Your Name]

Published

April 15, 2026

Executive Summary

Following iOS privacy updates, our standard last-click attribution model began failing. The marketing team was spending $5M across Meta, Google Ads, and linear TV, but couldn’t accurately prove incrementality. We deployed a Bayesian Marketing Mix Model (MMM) to take a top-down view of our spend and revenue.

Uncovering the True Drivers of Growth

We began by looking at our weekly revenue over the past two years. At a high level, growth looked steady, with expected seasonal peaks.

However, if we zoom into Q4 of last year, there is an anomalous spike in revenue that our existing models attributed 100% to Google Branded Search.

To understand why, we mapped our media spend across the three major channels. Notice the large, periodic spikes in our Linear TV budget.

By applying ad-stock (carryover) and diminishing returns transformations in our MMM, we discovered the truth: TV was driving the search volume.

While Google Search captured the final click, Linear TV had the highest true Return on Ad Spend (ROAS) when accounting for halo effects.

As a result of this analysis, we shifted 15% of our budget out of saturated Meta campaigns and into upper-funnel TV buys, generating an incremental $1.2M in revenue.

Methodology Notes for Technical Review

For the data science team, the complete code for the Bayesian model (built using PyMC) is available below. We utilized Half-Normal priors for the media coefficients and Beta distributions for the ad-stock decay rates.

# This code block is visible but not executed in the portfolio
import pymc as pm
import pytensor.tensor as pt

with pm.Model() as mmm:
    # Priors
    intercept = pm.Normal('intercept', mu=0, sigma=1)
    beta_media = pm.HalfNormal('beta_media', sigma=1, shape=3)
    alpha_adstock = pm.Beta('alpha_adstock', alpha=1, beta=3, shape=3)
    
    # ... (Truncated for portfolio readability)