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.
- No interactions. A heuristic scores each touch in isolation. It cannot express "paid_social is only valuable when email follows it." Real journeys are full of these lifts, and a fixed rule is structurally blind to every one.
- Path-blind weighting. The value a heuristic assigns a channel does not depend on the rest of the path. Display gets 40% under position-based whether it opened a winning journey or a dead one - the weight is a constant, not a response to the data.
- Arbitrary weights. Why 40/40/20 and not 30/30/40? Why a 7-day half-life? Nobody can defend the constants because they were never estimated - they were chosen. The numbers look precise and are entirely made up.
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.
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)andP(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.
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.
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.
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.
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.
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.
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:
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.
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:
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.
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.
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:
# 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."
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.
This week ◐ 40 min total
- Run all three build-alongs on Lumen and save the
comparedelta table - Session 4 (Markov) will add a third column to it. - Demonstrate the instability yourself. Fit a plain logistic on 10 bootstrap resamples, plot each channel's coefficient across the runs, then do the same for the bagged model. The spread difference is the point of the whole session.
- Build the conditional-probability model. Compute
P(conv | channel)andP(conv | channel pair)on Lumen and compare its ranking to the bagged-logistic ranking. Where do they disagree, and why? - Optional: read Shao & Li (2011) sections 2-3. You have now implemented both models they propose - the paper reads like your own code.
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.
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.