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

Shapley value attribution, from first principles

Markov asked "what breaks if this channel leaves?" Shapley asks a fairer question borrowed from game theory: "across every possible order the channels could have joined, what did this one add on average?" We derive the formula, hand-verify a three-channel example that sums cleanly to the grand coalition, code exact Shapley with itertools, then Monte-Carlo it for when the channel count makes exact impossible.

🟡 Core Builders · practitioner Python + itertools 45 min
0-3 · Setup 3-18 · The game 18-40 · Build it 40-45 · Recap
Part 0

A fairness idea from 1953, aimed at your media mix

In 1953 the mathematician Lloyd Shapley solved a question that had nothing to do with marketing: when a team produces value together, how much did each member fairly earn? His answer - the Shapley value - is the only credit split that satisfies four intuitive fairness axioms at once, and it turns out to be exactly the tool we want for attribution. Treat channels as players in a cooperative game, treat conversions as the payout, and Shapley tells you each channel's fair share.

Live - built together in session Self-study - read after class ★ Build-along - everyone runs it The data: Lumen Skincare
★ What you walk out with today A from-scratch exact Shapley attributor - the characteristic function from Lumen coalitions, the factorial-weighted marginal-contribution sum, and a hand-verified reconciliation to the efficiency axiom - plus a Monte-Carlo version for when you have too many channels to enumerate every subset, and the bridge to SHAP that carries you into next session.
Part 1 · the game

Channels as a team: marginal contribution 7 min live

The core idea is marginal contribution. A channel's value is how much conversion rises when it joins a group of channels that were already working together. But that number depends on who was already in the room, so Shapley averages the marginal contribution across every possible order the channels could have joined. Fair credit is your average lift, taken over all the coalitions you might have joined.

One ordering: the empty set, then A joins, then B, then C { } = 0.00 {A} = 0.20 {A,B} = 0.35 {ABC}=0.45 A adds +0.20 B adds +0.15 C adds +0.10 This is just ONE of the 6 orderings. A's credit φ(A) = its marginal contribution averaged over ALL orderings. Here A joined first (+0.20); in "B then A" it would add v(AB) − v(B) = 0.25 instead. The marginal contribution changes with who joined first - averaging over every order is what makes the split fair.
🔍 Click to zoom - channel A's marginal contribution as it joins a growing coalition (values are Lumen conversion rates for the A/B/C slice)
LiveThe characteristic function, and the formula4 min

Everything rests on one function, v(S) - the "characteristic function" - which says how much value a coalition S of channels produces. For attribution we define v(S) as the conversion rate of journeys that used exactly the channel set S. Then channel i's Shapley value is:

φi = ΣS ⊆ N∖{i} [ |S|! · (n − |S| − 1)! / n! ] · ( v(S ∪ {i}) − v(S) )

  • v(S ∪ {i}) − v(S) is i's marginal contribution: how much conversion rises when i joins coalition S.
  • |S|! · (n − |S| − 1)! / n! is the weight - the fraction of orderings in which exactly the members of S come before i. It is just the bookkeeping that turns "sum over subsets" into "average over orderings".
  • Sum that weighted marginal over every subset S that excludes i, and you have i's fair share.
Two ways to say the same thing You can sum over subsets with factorial weights, or you can literally average the marginal contribution over all n! orderings. They give identical answers - the weights are exactly the count of orderings each subset represents. We will code the subset form; it is faster.
Self-studyThe four axioms that make Shapley "fair"3 min read

Shapley is not just a way to split credit - it is the unique split satisfying all four of these at once:

  • Efficiency: the shares sum to exactly the grand-coalition value v(N). Every conversion is fully accounted for, nothing invented or lost.
  • Symmetry: two channels that add the same marginal value to every coalition get the same credit. Identity does not matter, contribution does.
  • Null player: a channel that never changes any coalition's value gets zero. No free credit for being present.
  • Additivity: credit over two combined games equals the sum of credit in each - so you can decompose, say, two product lines and add them.
Real world

The efficiency axiom is what makes finance trust Shapley: because the shares provably sum to the total conversions, you can multiply each share by revenue and the channel credits reconcile to the top line to the penny. Markov needs a normalization step to get there; Shapley gets there by construction.

Part 2 · worked by hand

The three-channel example + the efficiency axiom 6 min live

Take a three-channel Lumen slice - A (paid social), B (email), C (display) - with these coalition conversion rates: v(A)=.20, v(B)=.10, v(C)=.05, v(AB)=.35, v(AC)=.30, v(BC)=.20, v(ABC)=.45. We average each channel's marginal contribution over all 6 orderings and watch the shares reconcile to v(ABC).

Ordering A adds B adds C adds A → B → C0.200.150.10 A → C → B0.200.150.10 B → A → C0.250.100.10 B → C → A0.250.100.10 C → A → B0.250.150.05 C → B → A0.250.150.05 Average = φ 0.233 0.133 0.083 φA + φB + φC = 0.449 ≈ v(ABC) = 0.45 Normalized credit A 51.9% · B 29.6% · C 18.5% The shares summing to the grand-coalition value is the efficiency axiom in action - no normalization step needed, it holds by construction.
🔍 Click to zoom - six orderings, each channel's marginal contribution averaged into its Shapley value, summing to v(ABC)
LiveRead one column, then trust the average3 min

Take channel A's column. In the two orderings where A joins first, it adds v(A) − v({}) = 0.20. In the four orderings where A joins after someone, it adds more, because it lifts an existing coalition: after B it adds v(AB) − v(B) = 0.25, after C it adds v(AC) − v(C) = 0.25, and after both it adds v(ABC) − v(BC) = 0.25. Average all six: (0.20 + 0.20 + 0.25 + 0.25 + 0.25 + 0.25) / 6 = 0.233. Do the same for B and C to get 0.133 and 0.083.

  • Sum check: 0.233 + 0.133 + 0.083 = 0.449, and v(ABC) = 0.45. The tiny gap is rounding - the true values sum exactly. That is efficiency, guaranteed.
  • Normalize for reporting: divide by the sum to express as percentages of credit - A 51.9%, B 29.6%, C 18.5% - which you multiply against revenue.
Shapley shares synergy Notice A and B together (0.35) beat their solo sum (0.20 + 0.10 = 0.30). That 0.05 of synergy is not thrown away or handed to one channel - Shapley splits it fairly across the pair through the averaging. That surplus-sharing is exactly what heuristics cannot do.
Part 3 · the wall

The O(2ⁿ) wall, Monte-Carlo Shapley, the SHAP bridge 5 min live

Exact Shapley is beautiful and expensive. The formula sums over every subset of channels, and there are 2ⁿ of them - fine for Lumen's handful of channels, ruinous once you have thirty. Here is the wall, and the two escape routes.

LiveExact is O(2ⁿ) - so sample instead3 min

Every channel's Shapley value sums over all subsets of the others, so the work grows as 2ⁿ. At 10 channels that is ~1,000 coalitions - instant. At 15 it is ~33,000 - still fine. At 25 it is 33 million, and at 40 it is a trillion. You hit the wall fast.

  • Monte-Carlo Shapley: instead of enumerating every ordering, sample random orderings, compute each channel's marginal contribution in each sampled order, and average. It converges to the exact Shapley value and lets you trade accuracy for runtime with the sample count.
  • Restrict to observed coalitions: most of the 2ⁿ subsets never actually occur in your data. Defining v(S) only over coalitions your customers really traversed prunes the space enormously and keeps every number grounded in real journeys.
Real world

Defining v(S) is itself a modeling choice, and it matters. The simplest is the observed conversion rate of journeys using exactly set S. A richer alternative feeds S into a trained model and uses its predicted conversion probability as v(S) - which handles unseen coalitions gracefully and leads straight into the next idea.

Self-studyThe SHAP bridge into production ML2 min read

The same Shapley math powers SHAP (SHapley Additive exPlanations), the standard way to explain any trained machine-learning model. Swap "channels" for "features" and "conversion rate of a coalition" for "the model's prediction given a subset of features", and each feature's SHAP value is its fair contribution to that one prediction - computed with Shapley's exact same averaging.

  • Why it is the pragmatic path: you train a strong conversion model on all your touch features, then let SHAP attribute credit - getting Shapley fairness plus a model that generalizes to coalitions you never observed.
  • Where we are headed: B6 builds exactly this - a LightGBM conversion model explained with the shap library - so today's from-scratch Shapley is the conceptual key that unlocks production ML attribution.
Build-along 1 of 3

Define v(S) from Lumen coalition conversion rates ★ 7 min · everyone

The characteristic function first. From the sessionized Lumen journeys, we compute the conversion rate of the set of channels each journey used, then look v(S) up by coalition. This is the value function every Shapley computation calls.

Demo 1 · the value function v(S) import pandas as pd # Lumen journeys: the unordered SET of channels used + whether it converted. # (Sets, because v(S) is defined on coalitions, not ordered paths.) journeys = pd.DataFrame({ "channel_set": [frozenset({"A"}), frozenset({"A", "B"}), frozenset({"A", "B", "C"}), frozenset({"B", "C"})], "converted": [1, 1, 1, 0], "n": [100, 100, 100, 100], # journeys of that exact set }) # v(S) = conversion rate over journeys whose channel set is a SUBSET of S. def make_v(journeys): rates = {} for S in journeys["channel_set"]: mask = journeys["channel_set"].apply(lambda cs: cs.issubset(S)) conv = (journeys.loc[mask, "converted"] * journeys.loc[mask, "n"]).sum() tot = journeys.loc[mask, "n"].sum() rates[frozenset(S)] = conv / tot if tot else 0.0 return lambda S: rates.get(frozenset(S), 0.0) v = make_v(journeys) # For the taught slice we use these hand-set rates directly: V = {frozenset(): 0.0, frozenset("A"): .20, frozenset("B"): .10, frozenset("C"): .05, frozenset("AB"): .35, frozenset("AC"): .30, frozenset("BC"): .20, frozenset("ABC"): .45} v = lambda S: V[frozenset(S)]
v(S) is the one real modeling choice Everything downstream is mechanical; how you define v(S) is where the judgement lives. Observed-coalition conversion rate keeps it simple and grounded. A trained model's predicted probability generalizes to unseen coalitions. Pick deliberately and write down which you used.
Build-along 2 of 3

Exact Shapley with itertools + factorial weights ★ 8 min · everyone

Now the formula, exactly. For each channel we sum over every subset of the others (the powerset via itertools), weight each marginal contribution by |S|!·(n−|S|−1)!/n!, and add them up. Run it on the slice and confirm 0.233 / 0.133 / 0.083.

Demo 2 · exact Shapley from itertools import combinations from math import factorial def powerset(players): for r in range(len(players) + 1): for combo in combinations(players, r): yield frozenset(combo) def shapley(players, v): n = len(players) phi = {} for i in players: others = [p for p in players if p != i] total = 0.0 for S in powerset(others): # every coalition without i weight = factorial(len(S)) * factorial(n - len(S) - 1) / factorial(n) marginal = v(S | {i}) - v(S) # what i adds to S total += weight * marginal phi[i] = total return phi phi = shapley(["A", "B", "C"], v) print({k: round(x, 3) for k, x in phi.items()}) # {'A': 0.233, 'B': 0.133, 'C': 0.083} print(round(sum(phi.values()), 3), "vs v(ABC) =", v({"A","B","C"})) # 0.45 vs v(ABC) = 0.45 -> efficiency holds total = sum(phi.values()) credit = {k: round(x / total, 3) for k, x in phi.items()} print(credit) # {'A': 0.519, 'B': 0.296, 'C': 0.185}
Real world

Always print the sum-vs-v(ABC) line as your unit test. If Shapley values do not sum to the grand-coalition value, your v(S) is inconsistent (often the empty coalition is not zero, or a subset rate was mistyped). The efficiency axiom is a free correctness check you get on every run.

Build-along 3 of 3

Monte-Carlo Shapley for many channels ★ 7 min · everyone

Lumen has 9 channels - exact is fine. But the moment you cross ~20 you need to sample. Monte-Carlo Shapley draws random orderings, adds each channel to a growing coalition in that order, and averages the marginal contributions. More samples, tighter estimate.

Demo 3 · Monte-Carlo Shapley import random def shapley_mc(players, v, samples=20000, seed=42): random.seed(seed) # reproducible, per workspace rule n = len(players) running = {i: 0.0 for i in players} for _ in range(samples): order = random.sample(players, n) # one random ordering coalition = set() prev = v(frozenset()) # value of empty coalition for i in order: coalition.add(i) cur = v(frozenset(coalition)) running[i] += cur - prev # i's marginal in this order prev = cur return {i: running[i] / samples for i in players} approx = shapley_mc(["A", "B", "C"], v, samples=20000) print({k: round(x, 3) for k, x in approx.items()}) # ~ {'A': 0.233, 'B': 0.133, 'C': 0.083} -> converges to exact
From here, the library does the heavy lifting Once you understand this loop, reach for tooling: DP6's marketing-attribution-models has a Shapley implementation for path data, and for model-based attribution the shap library computes the same values over a trained conversion model - the exact bridge into B6. Build it once to trust it, then let the library scale it.
Before Session 6

This week ◐ 40 min total

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · A channel's Shapley value is...

Shapley averages how much a channel adds when it joins a coalition, across all possible join orders. That average marginal contribution is its fair share of the payout.

2 · The efficiency axiom guarantees that the Shapley values...

Efficiency means the shares add up to exactly v(N). Unlike Markov removal effects, Shapley values need no normalization to reconcile - it holds automatically, and doubles as a correctness check.

3 · Exact Shapley is O(2ⁿ). With 30 channels you should...

2³⁰ is over a billion coalitions - infeasible. Monte-Carlo Shapley approximates the exact value by sampling orderings, and restricting to observed coalitions prunes the space to what actually occurred.

Source material

What this session covers

This session owns Coverage Gap #1 for Shapley: the game-theoretic derivation, the exact itertools implementation, the efficiency check and the Monte-Carlo scale-up - built from scratch, then bridged to SHAP. Most courses cite Shapley and jump straight to a library; we derive it so you can defend every number.

Coverage Gap #1 - Shapley from scratch (original)coalitions, marginal contribution, exact + Monte-Carlo - Parts 1-3, Demos 1-3
Shapley value paper (arXiv 1804.05327)the four axioms and the formula - Part 1
DP6 marketing-attribution-models (Python)Shapley on path data at scale - Demo 3
SHAP for trained conversion modelsbridged here; built with LightGBM in B6
GA4 data-driven attribution (Shapley + time-decay)the black-box production version - B7

Builder Session 5 cheat sheet · pin this

The ideaChannels = players in a cooperative game; conversions = payout. Each channel's credit = its average marginal contribution over every join order (Shapley, 1953).
The formulaφᵢ = Σ over S ⊆ N∖{i} of [ |S|!·(n−|S|−1)! / n! ] · ( v(S∪{i}) − v(S) ). The weight is just the count of orderings each subset represents.
v(S)The characteristic function - conversion rate of a coalition. The one real modeling choice: observed-coalition rate vs a trained model's predicted probability.
EfficiencyShapley values sum to v(grand coalition) by construction - no normalization needed, and a free correctness check every run.
The O(2ⁿ) wallExact sums over every subset - fine to ~10-15 channels, explodes beyond. Use Monte-Carlo Shapley (sample orderings) or restrict to observed coalitions.
SHAP bridgeSame math on a trained model's features → SHAP. The pragmatic production path: train a conversion model, explain it with shap. Leads into B6.