learn-marketing-attribution-with-phoebe / Builder session 3 of 10
Learn Marketing Attribution with Phoebe · Builder session 3 of 10

Why heuristics fail, and the data-driven leap

Six models, six answers, and not one of them learned anything - they asserted. This session is the hinge of the whole builder track: we name the three failures baked into every heuristic, then cross the bridge Shao and Li built in 2011, predicting conversion from the set of channels a customer touched. You will fit a bagged logistic regression on Lumen, read per-channel credit straight off the coefficients, and see why plain logistic goes haywire on correlated channels while bagging steadies it.

🟡 Core Builders scikit-learn 45 min
0-3 · Recap 3-20 · The failures + the idea 20-42 · Fit it on Lumen 42-45 · Wrap
Part 0

The three failures of heuristics

Heuristics assert credit; they never learn it. Once you have coded all six (Session 2), the cracks are obvious and they come in three shapes. Naming them precisely is what makes the data-driven model feel inevitable rather than fancy.

Live - built in session Self-study - read after class ★ Build-along - everyone codes it The data: Lumen Skincare
★ What you walk out with today A Lumen journey turned into a channel-presence feature matrix, a bagged logistic regression fit on it, per-channel credit read from the stabilized coefficients, and a side-by-side that shows how far data-driven credit moves from last-touch. Plus the intuition for why bagging - over sampled rows and sampled channels - is the fix Shao and Li reached for.
Part 1 · the bridge

The Shao-Li idea: predict conversion from the channel set 6 min live

In 2011 Shao and Li published the paper that launched data-driven multi-touch attribution. The move is simple and profound: stop assigning credit by rule, and instead predict whether a journey converts from the set of channels it contains. Fit a model, and the per-channel weights it learns are the attribution. Credit becomes something estimated from data, not decreed.

Journeys → presence matrix disp soc email srch y 1 1 1 1 1 0 1 0 1 1 1 0 0 0 0 0 1 1 0 1 rows = journeys · 1 = channel present · y = converted Logistic model P(convert | channel set) learns one weight/channel Per-channel coefficient = credit paid_social +0.91 ▇▇▇▇▇ email +0.74 ▇▇▇▇ organic +0.55 ▇▇▇ paid_search +0.48 ▇▇▇ display +0.30 ▇▇ higher weight = bigger lift to conversion odds The model never sees a hand-picked weight. It estimates each channel's contribution from which sets of channels actually converted. This is the exact moment credit stops being asserted and starts being LEARNED - the root of every data-driven attribution method.
🔍 Click to zoom - Shao-Li: journeys → feature vectors → logistic → per-channel credit (learned, not asserted)
LiveWhat "learned credit" actually buys you3 min

The featurization is the key mental shift. A journey stops being an ordered path and becomes a bag of channels: a row of 0/1 flags saying which channels were present, with a label y for whether it converted. The model asks one honest question - when this channel is in the set, do the odds of conversion go up, and by how much? - and answers it from the whole dataset, not one path.

  • It captures the base idea of interaction because it weighs channels against each other simultaneously, not one at a time. Shao and Li also propose a companion probabilistic model using first- and second-order conditional probabilities - P(conv | channel) and P(conv | channel pair) - to catch pairwise lift directly.
  • It is correlational, not causal. A learned coefficient says "journeys with this channel convert more", not "this channel caused the sale". Keep that honesty - it is why Sessions 8 and 9 add MMM and incrementality.
  • It is the historical root of data-driven attribution. GA4's DDA, Markov, and Shapley all descend from this reframe: predict the outcome, read the credit off the model.
Part 2 · the stability fix

Why plain logistic is unstable, and why bagging fixes it 5 min live

There is a catch Shao and Li had to solve. Marketing channels are heavily correlated - customers who see paid_social often also get email, journeys that hit organic often hit paid_search. Feed correlated dummy variables to a plain logistic regression and the coefficients thrash: tiny changes in the sample flip a channel's weight from strongly positive to negative. Attribution you cannot reproduce is attribution you cannot defend.

Plain logistic · correlated channels coefficients swing +/- run to run - some go negative (coral). "email hurts conversion"? No - it is correlation, not signal. Bagged logistic · sample rows + channels averaging many resampled fits cancels the noise - stable, all-positive, reproducible per-channel credit.
🔍 Click to zoom - correlated channels wreck a single logistic fit; bagging averages the thrash away
LiveBagging over rows AND channels3 min

Shao and Li's answer is bagging - bootstrap aggregating. Fit the logistic model many times, each on a resample, then average the coefficients. Their twist is to sample in two directions:

  • Sample the instances (rows / journeys), standard bootstrap. Different journey mixes each fit.
  • Sample the channels (features), so no single correlated pair dominates every model. Each base learner sees a subset of channels, breaking the collinearity that makes weights swing.
  • Average the results. The noise that pushed a coefficient negative in one fit is cancelled by the fits where it landed positive. What survives the averaging is the stable, defensible signal.
The one-sentence why Plain logistic on correlated channel dummies gives unstable coefficients; bagging over sampled rows and sampled channels stabilizes the per-channel attribution. That trade - a little variance for a lot of reproducibility - is the whole reason the paper bags.
Self-studyThe simple probabilistic model2 min read

Alongside the bagged logistic, Shao and Li offer a lighter model built purely from conditional probabilities. Estimate the first-order term P(conversion | channel present) for each channel, and the second-order term P(conversion | channel pair present) for each pair. The pairwise term minus the two solo terms captures the interaction lift between two channels directly - the very thing heuristics cannot express. It is transparent, cheap, and a great sanity check against the logistic coefficients: when they disagree, something in your featurization is off.

Part 3 · the honest caveat

Learned, but still correlational 4 min live

Crossing from heuristics to data-driven is a real upgrade - credit now responds to the data - but it is not a leap to causality. Hold two truths at once so you never oversell the result upstairs.

LiveWhat the data-driven model can and cannot claim2 min
  • Can claim: "Journeys containing paid_social convert at higher odds, controlling for the other channels present." That is a learned, dataset-wide statement - genuinely more than any heuristic offers.
  • Cannot claim: "paid_social caused the extra conversions." Correlated exposure and self-selection still lurk - the people who see paid_social may already be likelier to buy. Only a holdout or geo experiment (Session 9) settles cause.
  • The discipline: use data-driven credit for tactical channel decisions, and reserve big budget reallocations for the causal tools. Same lesson as the leader track's "match the era to the decision", now in your own model.
Real world

A team shipped a bagged-logistic model, saw email's coefficient spike, and doubled the email budget - only for incrementality tests to show email was mostly reaching people who would have bought anyway. The model was not wrong; it was correlational, and someone read it as causal. Data-driven attribution earns trust precisely by naming that boundary out loud.

Build-along 1 of 3

Featurize Lumen journeys into a channel-presence matrix ★ 7 min · everyone

First turn Lumen's path table into the Shao-Li feature matrix: one row per journey, one 0/1 column per channel, and a converted label. This is the input every data-driven model in the course reuses.

Pivot the touchpoint table to channel presence and attach the conversion label from conversions:

demo1_featurize.py
import pandas as pd

CHANNELS = ["paid_search","paid_social","display","organic_search",
            "email","affiliate","influencer","ctv","direct"]

tp = pd.read_parquet("lumen_fct_touchpoints.parquet")

# one row per customer, 1 if the channel appears anywhere in the journey
X = (tp.assign(flag=1)
       .pivot_table(index="customer_id", columns="channel",
                    values="flag", aggfunc="max", fill_value=0)
       .reindex(columns=CHANNELS, fill_value=0))

# label: did this journey convert? (include non-converting journeys too!)
conv = pd.read_parquet("lumen_conversions.parquet")
y = X.index.isin(conv["customer_id"]).astype(int)

print(X.shape, y.mean().round(3))   # feature matrix + base conversion rate

The critical detail: you must include non-converting journeys. A model that only sees winners cannot learn what separates a converting channel set from a losing one - it would be all signal, no contrast.

Check the correlation matrix X.corr(). You will see paid_social/email and organic/paid_search light up - that collinearity is exactly what makes plain logistic unstable in the next step.

Build-along 2 of 3

Fit a bagged logistic regression, read per-channel credit ★ 8 min · everyone

Now the Shao-Li model itself. Wrap LogisticRegression in scikit-learn's BaggingClassifier, sampling both rows and features, then average the base estimators' coefficients into stable per-channel credit.

Fit the ensemble - max_features < 1.0 is what samples channels, the Shao-Li twist:

demo2_bagged_logistic.py
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import BaggingClassifier

bag = BaggingClassifier(
    estimator=LogisticRegression(max_iter=1000),
    n_estimators=200,
    max_samples=0.8,      # bootstrap the journeys (rows)
    max_features=0.6,     # sample the channels (features) - the Shao-Li twist
    bootstrap=True,
    random_state=42,
).fit(X.values, y)

# average each channel's coefficient across the base learners that used it
coef_sum = np.zeros(X.shape[1]); coef_cnt = np.zeros(X.shape[1])
for est, feats in zip(bag.estimators_, bag.estimators_features_):
    coef_sum[feats] += est.coef_[0]
    coef_cnt[feats] += 1
avg_coef = coef_sum / np.maximum(coef_cnt, 1)

credit = pd.Series(avg_coef, index=X.columns).sort_values(ascending=False)
# turn positive coefficients into a % credit split
credit_share = (credit.clip(lower=0) / credit.clip(lower=0).sum() * 100).round(1)
print(credit_share)

Compare against a single plain LogisticRegression().fit(X, y). Refit it a few times on bootstrap resamples and watch individual coefficients swing - some flip sign. The bagged version barely moves. That contrast is the whole Part 2 lesson, reproduced on Lumen.

Read credit_share as the data-driven attribution: the channels whose presence most lifts conversion odds, credit learned from Lumen's data rather than a rule you typed.

Why average, not just call bag.predict BaggingClassifier gives you predictions out of the box, but attribution needs the coefficients, not the predictions. Averaging estimators_ weights over the estimators_features_ they were trained on is how you recover a stable per-channel number from the ensemble.
Build-along 3 of 3

Compare data-driven credit vs last-touch on Lumen ★ 7 min · everyone

The moment it pays off: line the learned credit up against the last-touch rollup from Session 2 and see exactly which channels last-touch was starving.

Join the two channel-level views:

demo3_compare.py
# last_touch_share from B2's channel rollup (% of credit)
compare = pd.DataFrame({
    "last_touch":  last_touch_share,
    "data_driven": credit_share,
}).fillna(0)
compare["delta"] = (compare["data_driven"] - compare["last_touch"]).round(1)
print(compare.sort_values("delta", ascending=False))

# Expect: paid_social / email / display GAIN credit (they create demand);
# paid_search LOSES the inflated last-touch share it never earned alone.

Read the delta column. Positive deltas are the demand-creating channels last-touch systematically underpaid; the large negative delta on paid_search is the over-crediting the whole course has been warning about, now measured on Lumen.

Write the one-line finding a stakeholder can act on: "Under a model that learns from the data instead of crowning the closer, paid_social and email carry far more of Lumen's conversions than the last-touch dashboard admits."

Real world

This delta table is often the single artifact that unlocks a budget conversation. It does not say "trust me"; it shows the same channels, two models, and the exact dollars that moved between them - and it names honestly that the data-driven number is correlational, so the final call still waits on Session 9's experiments. That mix of insight and humility is what gets a data-driven model adopted instead of argued with.

Before Session 4

This week ◐ 40 min total

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · The core thing every heuristic fails to capture is...

Heuristics score each touch in isolation with a fixed weight. They cannot express "paid_social only lifts conversion when email follows" - the interaction effects real journeys are full of.

2 · Why did Shao and Li bag the logistic regression instead of fitting it once?

Marketing channels are heavily correlated, so a plain logistic's weights swing run to run - even going negative. Averaging many resampled fits (rows and channels) cancels the noise into reproducible credit.

3 · The essential difference between heuristics and data-driven attribution is that data-driven...

Data-driven estimates each channel's contribution from which channel sets actually converted - learned, not decreed. But it is still correlational; only incrementality experiments (Session 9) establish cause.

Source material

What this session covers

This session builds the historical root of data-driven attribution directly from the founding paper, on Lumen data - the bridge no heuristics course crosses and the foundation Markov and Shapley extend.

Shao & Li (2011) - data-driven MTAbagged logistic + conditional-probability model - Parts 1-2, all build-alongs
The three failures of heuristicsno interactions, path-blind, arbitrary weights - Part 0
Correlational vs causal boundarywhat a learned model may and may not claim - Part 3
Markov removal-effect attributionbuilt from scratch in Builder Session 4
Incrementality / causal creditholdout + geo-lift experiments in Builder Session 9

Builder Session 3 cheat sheet · pin this

3 failures of heuristicsNo interactions · path-blind weighting · arbitrary constants (why 40/40/20?). All asserted, none learned.
The Shao-Li move (2011)Predict conversion from the SET of channels touched; the model's per-channel weights ARE the attribution.
FeaturizeOne row per journey, 0/1 per channel, y = converted. Must include non-converting journeys for contrast.
Why bagCorrelated channels → unstable coefficients. Bag over sampled rows AND channels → stable, reproducible credit.
Simple probabilistic modelP(conv | channel) + P(conv | channel pair) captures pairwise interaction lift directly. Great sanity check.
Still correlationalLearned ≠ causal. Use for tactics; reserve budget reallocation for the incrementality tests in Session 9.