Documentation · Attribution · Marketing Mix Modeling

Marketing Mix Modeling: where the next dollar should go

Adstock + Hill saturation + bootstrap CIs — the same pipeline Robyn (Meta) and LightweightMMM (Google) ship in their open-source MMMs. Free on every Admaxxer plan; Northbeam ships the same surface at ~$10k+/yr.

What is MMM? Adstock + saturation Marginal ROAS vs Robyn FAQ

What is MMM?

Marketing Mix Modeling is a regression-based attribution framework that decomposes observed revenue into per-channel contributions while accounting for two structural facts that MTA ignores: adstock (today's spend produces orders for several days, not just today) and saturation (the 2nd $1k/day on a channel is worth less than the 1st $1k/day).

The two open-source gold standards are Robyn (Meta, R) and LightweightMMM (Google, Python+NumPyro). Both ship adstock + Hill saturation + ridge regression — the pipeline Admaxxer's v1 most closely tracks. Robyn uses Nevergrad for hyperparameter search (~2-4h per fit); LightweightMMM uses MCMC sampling (~30+ min). Admaxxer's v1 ships an OLS + grid-search variant that fits in <2s — the Pareto-optimal tradeoff for an in-product tile that needs interactive lookback adjustments.

Adstock + Hill saturation

Geometric adstock: x_adstocked[t] = x[t] + lambda * x_adstocked[t-1]. lambda is the carryover decay (0..1). lambda=0.5 → half of yesterday's spend impact carries forward each day. The MMM card shows adstock half-life per channel — the number of days for spend impact to halve, derived from lambda. Typical DTC values: Meta paid 5-10 day half-life; Google paid (intent-driven) 1-3 day half-life; organic 14+ days.

Hill saturation: revenue(s) = alpha * s^n / (K^n + s^n). K is the half-saturation spend (50% of the ceiling); n is the shape (steepness). Below K the curve is roughly linear; at K it's at 50% of the ceiling; at 5K+ it's nearly flat. The MMM card surfaces K + n per channel. When your daily spend > K, you're past the inflection point — marginal ROAS drops fast.

Why marginal ROAS is the answer to "where does the next dollar go?"

Operators look at blended ROAS — total revenue / total spend — and try to scale the channel with the highest number. This is wrong once Hill saturation kicks in. Blended ROAS is the AVERAGE return across your spend curve; the NEXT dollar's return is the SLOPE of the curve at your current spend, which is always lower past the inflection point.

Example: Meta blended ROAS 4.5×, marginal ROAS 1.2×. Google blended ROAS 2.8×, marginal ROAS 2.5×. Operator instinct says "scale Meta, it's higher". The MMM says: scale Google. Meta's at the top of its saturation curve; Google still has runway.

Use marginal ROAS when deciding where to add the next $5k. Use blended ROAS when reporting last-month's performance. Conflating them is the most common DTC budgeting mistake — the MMM card's marginal-ROAS column makes the distinction legible.

MMM in DTC tools — Admaxxer vs the field

ToolShips MMMNotes
Admaxxer Yes Lightweight MMM v1.0 — adstock + Hill saturation + bootstrap CIs. OLS + grid-search core (~80% of value, ~20% of runtime vs Bayesian). Free on every plan. Ships marginal-ROAS column.
Robyn (Meta open-source) Yes Open-source R package from Meta. Adstock + Hill saturation + ridge regression + Nevergrad hyper-search. Best-in-class methodology, but requires R + 2-4h/run for hyperparameter search.
LightweightMMM (Google open-source) Yes Open-source Python (NumPyro) package from Google. Bayesian (MCMC) version of Robyn's pipeline. Strongest priors but slowest runtime — 30+ min per fit.
TripleWhale Limited TW ships an MMM module but documentation is thin and the methodology isn't openly published. Bundled in higher-tier plans ($249+/mo). No marginal-ROAS surface visible.
Northbeam Yes Ships a proper MMM as part of Probabilistic 1.0. Enterprise pricing (~$10k+/yr); robust methodology but inaccessible to sub-$10M brands.
Hyros No Hyros focuses on click-attribution + server-side identity stitching; does not ship MMM. ~$799/mo with a 6-month full-setup requirement — orthogonal scope to MMM, no adstock or saturation modeling.
Datafast No Datafast doesn't ingest paid spend; MMM isn't applicable. UTM-only attribution is the entire scope.

Read the MMM card — 4 steps

  1. Step 1. Make sure you have >=60 days of complete history. MMM needs n >= 60 days of joint Meta-spend, Google-spend, organic-sessions, and revenue rows. Below that the X'X matrix is too small for a stable fit and the endpoint returns data_status='insufficient_data'.
  2. Step 2. Pick a lookback that matches your operational rhythm. 60 / 90 / 180 days are supported. 60 = sensitive to recent shifts; 180 = smoothest baseline + adstock half-life estimates. 90 is a good default. Stay consistent.
  3. Step 3. Read the contribution % column for top-of-mind allocation. Each channel's contribution % tells you how much of observed revenue the model attributes to that channel under fitted adstock + saturation curves. This accounts for the carryover effect and saturation that MTA ignores.
  4. Step 4. Read the marginal-ROAS column for next-dollar decisions. Marginal ROAS is the SLOPE of the saturation curve at your current spend. ALWAYS lower than blended ROAS once Hill saturation kicks in. The right channel for the next dollar is the one with the highest marginal ROAS, not the highest blended ROAS.

Curl example

Every MMM call is a single GET against /api/v1/ads/mmm. Pass lookback_days (60 / 90 / 180); get back per-channel contribution %, adstock half-life, saturation K + n, blended + marginal ROAS, and the 95% bootstrap CI. No SDK required — copy, paste, swap $TOKEN.

# Read the MMM contribution + adstock + marginal-ROAS table for a 90-day lookback.
# Replace $TOKEN with a workspace API key from /settings/api.
curl -H "Authorization: Bearer $TOKEN" \
  "https://admaxxer.com/api/v1/ads/mmm?lookback_days=90" \
  | jq

# Sample response (truncated):
# {
#   "data_status": "ok",
#   "lookback_days": 90,
#   "channels": [
#     {
#       "channel": "meta_paid",
#       "contribution_pct": 0.42,
#       "contribution_revenue": 184320,
#       "adstock_half_life_days": 7.4,
#       "saturation_K": 4800,
#       "saturation_n": 1.6,
#       "blended_roas": 4.5,
#       "marginal_roas": 1.2,
#       "ci_95": { "lower": 154200, "upper": 218460 }
#     },
#     {
#       "channel": "google_paid",
#       "contribution_pct": 0.28,
#       "contribution_revenue": 122880,
#       "adstock_half_life_days": 1.8,
#       "saturation_K": 2200,
#       "saturation_n": 1.2,
#       "blended_roas": 2.8,
#       "marginal_roas": 2.5,
#       "ci_95": { "lower": 98140, "upper": 148610 }
#     }
#   ],
#   "bootstrap_samples": 200
# }

FAQ

What is MMM in one sentence?
Marketing Mix Modeling is a regression-based attribution framework that decomposes observed revenue into per-channel contributions while accounting for adstock (the carryover effect of historical spend) and saturation (the diminishing-returns curve as spend increases).
How is MMM different from MTA / Markov / incrementality?
MTA gives credit based on path topology. MMM gives credit based on regression contribution while explicitly modeling adstock + saturation. Incrementality gives credit based on causal experiments. The four together are the modern DTC attribution stack: MTA for path-level, MMM for budget allocation, incrementality for high-stakes scaling, PPS for ground-truth.
What's adstock?
Adstock (carryover effect) models that today's spend influences not just today's conversions but the next ~T days. Geometric adstock: x_adstocked[t] = x[t] + lambda * x_adstocked[t-1], where lambda is the carryover decay (0..1). Higher half-life = longer awareness tail. Meta typically shows 5-10 day half-lives in DTC; Google paid 1-3 day half-lives.
What's Hill saturation?
Saturation is the diminishing-returns curve. Hill function: revenue(s) = alpha * s^n / (K^n + s^n), where K is the half-saturation spend (50% of the ceiling) and n is the shape (steepness). At low spend the curve is roughly linear; at K it's at 50%; at 5K+ it's nearly flat. Captures diminishing returns.
Why is the marginal ROAS column different from blended ROAS?
Blended ROAS = revenue / spend at your CURRENT spend level — averaged across the whole curve. Marginal ROAS = dRevenue/dSpend at your current spend — the slope of the curve right where you are. Once you're past the inflection point, marginal ROAS is always lower than blended. The next-dollar decision should always be made on marginal ROAS.
Why does Admaxxer's MMM use OLS instead of Bayesian?
Two reasons: (1) Runtime — Bayesian MCMC takes 30+ min per fit (LightweightMMM); OLS + grid-search runs in <2 seconds. For an in-product tile, sub-second is required UX. (2) Marginal value — for the typical DTC operator, Bayesian priors don't move the answer enough to justify the runtime. v1.5 will add a Bayesian core when priors start to matter.
How are 95% credible intervals computed?
Bootstrap. Sample n daily rows with replacement (200 bootstrap samples by default), refit the OLS + grid-search adstock+saturation pipeline on each resampled set, and take the 2.5th + 97.5th percentiles of the per-channel contribution-revenue distribution.
Can I use MMM without Meta connected?
Yes — source-additive. The pipe (p_mmm_inputs) returns whatever spend series exist; the model only fits regressors for the connected channels. Per GL#256 / GL#313 source-additive principle: missing channels show as contribution_revenue = 0; the model itself never breaks.
When should I use MMM?
MMM is the right tool once you have (1) at least 60 days of joint Meta-spend, Google-spend, organic-sessions, and revenue rows (the same minimum Robyn and LightweightMMM both demand — below that the X'X matrix is too small for a stable fit), (2) at least 100 orders/month so the per-channel coefficients have enough signal to lift above noise, and (3) a multi-channel mix where you're spending meaningfully on at least 2 paid channels plus organic — single-channel stores get no MMM benefit because there's nothing to allocate between. Below these thresholds, stop here and stick with click-attribution: the 6-model lineup on /documentation/attribution-models, especially time-decay (H=7d) as the default, plus the Reconciliation Panel for the 3-way platform-vs-pixel-vs-Shopify view. MMM only starts paying back once those are saturated and you need the marginal-ROAS column to make next-dollar allocation calls. The card returns data_status='insufficient_data' and shows a premium empty state pointing back at the attribution-models lineup if your workspace is below the thresholds.