Analytics
Building attribution models: from last-click to data-driven
A practical walkthrough of marketing attribution, from simple rule-based models to a data-driven Shapley-value approach, with Python implementations you can run against your own conversion paths.
Ask five people in a marketing meeting which channel deserves credit for a sale, and you will get five different answers, because the honest answer depends entirely on which attribution model you are implicitly using. Last-click gives everything to the final touch before conversion, which flatters paid search and punishes the awareness content that started the journey weeks earlier. First-click does the reverse. Neither is wrong exactly; they are both simplifications of a genuinely multi-touch reality, and the gap between them is often the difference between funding a channel and cutting it.
This guide walks from the simple rule-based models most teams start with, through a Markov-chain removal-effect model, to a Shapley-value implementation that treats attribution as a proper credit-allocation problem. Each is real, runnable Python against a table of conversion paths you can build from the joined GA4 and Search Console data, or from your CRM’s touch history.
Start from a clean conversion-path table
Every model in this guide consumes the same shape of data: a list of paths, where each path is the ordered sequence of channels a converting user touched, ending in a conversion. Building this table well is most of the work; the models themselves are short once the data is right.
paths = [
["organic-search", "email", "paid-search"],
["paid-social", "direct"],
["organic-search", "paid-search"],
["email", "email", "direct"],
["paid-social", "organic-search", "direct"],
]
Collapse consecutive repeats of the same channel, since three emails in a row before a conversion usually represent one nurture sequence rather than three independent touches, and leaving them raw will inflate that channel’s apparent influence.
def collapse_repeats(path):
out = []
for ch in path:
if not out or out[-1] != ch:
out.append(ch)
return out
paths = [collapse_repeats(p) for p in paths]
Rule-based models: fast, transparent, limited
The simplest models assign credit by position and require no data beyond the paths themselves. Last-click and first-click are the two extremes; linear and position-based sit between them. Implementing all four side by side is cheap and gives you a baseline every stakeholder already understands.
from collections import defaultdict
def last_click(paths):
credit = defaultdict(float)
for p in paths:
credit[p[-1]] += 1
return dict(credit)
def first_click(paths):
credit = defaultdict(float)
for p in paths:
credit[p[0]] += 1
return dict(credit)
def linear(paths):
credit = defaultdict(float)
for p in paths:
share = 1 / len(p)
for ch in p:
credit[ch] += share
return dict(credit)
def position_based(paths, first_weight=0.4, last_weight=0.4):
credit = defaultdict(float)
for p in paths:
if len(p) == 1:
credit[p[0]] += 1
continue
middle_weight = (1 - first_weight - last_weight) / max(1, len(p) - 2)
for i, ch in enumerate(p):
if i == 0:
credit[ch] += first_weight
elif i == len(p) - 1:
credit[ch] += last_weight
else:
credit[ch] += middle_weight
return dict(credit)
These models are useful precisely because they are simple enough to explain in one sentence, and that transparency has real value in a stakeholder conversation. Their limitation is that the weights are chosen by convention, not by evidence from your own conversion behaviour.
A data-driven step: the Markov removal effect
A genuinely data-driven model asks a sharper question: how much does the overall conversion rate drop if a given channel is removed from every path entirely? A channel whose removal collapses conversions is important regardless of where it sits in the sequence; one whose removal barely matters was probably riding alongside more influential channels. This is the removal-effect idea behind Markov-chain attribution, and a workable version does not require a specialised library.
from itertools import product
import random
def build_transition_counts(paths):
counts = defaultdict(lambda: defaultdict(int))
for p in paths:
seq = ["start"] + p + ["conversion"]
for a, b in zip(seq, seq[1:]):
counts[a][b] += 1
return counts
def to_probabilities(counts):
probs = {}
for state, nxt in counts.items():
total = sum(nxt.values())
probs[state] = {k: v / total for k, v in nxt.items()}
return probs
def conversion_rate(probs, exclude=None, trials=20000):
conversions = 0
for _ in range(trials):
state = "start"
for _ in range(20): # cap path length
options = probs.get(state, {})
options = {k: v for k, v in options.items() if k != exclude}
if not options:
break
state = random.choices(list(options), weights=list(options.values()))[0]
if state == "conversion":
conversions += 1
break
if state == "null":
break
else:
continue
return conversions / trials
def removal_effect(paths):
counts = build_transition_counts(paths)
probs = to_probabilities(counts)
base = conversion_rate(probs)
channels = {ch for p in paths for ch in p}
effects = {}
for ch in channels:
reduced = conversion_rate(probs, exclude=ch)
effects[ch] = max(0, (base - reduced) / base) if base else 0
total = sum(effects.values()) or 1
return {ch: v / total for ch, v in effects.items()}
This is a simplified, Monte Carlo version of the idea, not a production-grade Markov solver, but it captures the essential logic and is transparent enough to audit line by line. For most mid-sized businesses it produces materially more defensible channel weights than any rule-based model, without needing a specialised platform.
Shapley value: attribution as fair credit allocation
The Shapley value comes from cooperative game theory and answers a precise question: across every possible order in which channels could have contributed, what is each channel’s average marginal contribution to the outcome. Applied to attribution, it treats each unique combination of channels appearing together in converting paths as a coalition and computes each channel’s fair share of the credit for that coalition’s success.
from itertools import permutations
def path_sets(paths):
# count conversions per unique unordered set of channels
counts = defaultdict(int)
for p in paths:
counts[frozenset(p)] += 1
return counts
def shapley_values(paths):
sets = path_sets(paths)
channels = sorted({ch for s in sets for ch in s})
value = {ch: 0.0 for ch in channels}
def coalition_value(coalition):
# conversions attributable to any path fully covered by this coalition
return sum(n for s, n in sets.items() if s <= coalition)
n = len(channels)
for perm in permutations(channels):
coalition = frozenset()
prev_value = 0
for ch in perm:
coalition = coalition | {ch}
v = coalition_value(coalition)
value[ch] += (v - prev_value)
prev_value = v
total_perms = 1
for i in range(1, n + 1):
total_perms *= i
return {ch: v / total_perms for ch, v in value.items()}
Full Shapley computation is factorial in the number of channels, so this exact form only stays practical for a handful of channels. Beyond six or seven, sample permutations randomly instead of enumerating every one, and the estimate converges close enough for allocation decisions with a few thousand samples.
Compare the models before you trust any of them
No single model is universally correct, so the useful exercise is running several against the same path table and looking at where they agree and disagree. Channels that rank highly under every model are safe bets to keep funding. Channels that rank well only under last-click and poorly everywhere else are usually harvesting credit for demand that awareness channels created, and are the first place to scrutinise when a budget conversation gets political.
Handle the paths that never convert
Every one of the models above is fit or evaluated only on paths that ended in a conversion, but the paths that did not convert carry information too, and ignoring them entirely biases the removal-effect model in particular. A channel that appears constantly in non-converting paths, with no offsetting presence in converting ones, is a channel whose true contribution is lower than a converting-paths-only view would suggest. Including a representative sample of non-converting paths, tagged with a terminal “null” state instead of “conversion”, lets the Markov model estimate a more honest baseline conversion rate and produces removal effects that better reflect reality.
paths_with_outcome = [
(["organic-search", "email", "paid-search"], "conversion"),
(["paid-social", "direct"], "conversion"),
(["organic-search", "paid-search"], "null"),
(["email", "direct"], "null"),
(["paid-social", "organic-search", "direct"], "conversion"),
]
def build_transition_counts_with_null(paths_with_outcome):
counts = defaultdict(lambda: defaultdict(int))
for path, outcome in paths_with_outcome:
seq = ["start"] + path + [outcome]
for a, b in zip(seq, seq[1:]):
counts[a][b] += 1
return counts
Most analytics platforms will happily export non-converting paths alongside converting ones; the only discipline required is remembering to pull both, rather than only exporting the successes because they are the ones anyone thinks to look at.
Present the result as a range, not a single number
However the model is built, resist the temptation to present its output as a single, precise figure a stakeholder will remember forever. Run the model on a few different reasonable configurations, such as varying the lookback window or the removal-effect sampling count, and present the resulting range alongside the point estimate. A channel that consistently lands between eighteen and twenty-four percent of credit across configurations is a channel you can make a real budget decision about. A channel that swings from five percent to thirty percent depending on an arbitrary modelling choice is telling you the data does not yet support a confident allocation, and that is itself a useful, honest finding to bring to the table.
The political dimension of choosing a model
It would be incomplete to treat model selection as a purely technical decision, because in most organisations the channel that receives the most credit tends to receive the most budget, and the team responsible for that channel knows it. This is precisely why last-click attribution has persisted for so long despite its well-documented flaws: it is simple to explain, hard to argue with in the moment, and it consistently flatters whichever channel sits closest to the point of purchase, which is usually paid search or direct traffic rather than the awareness and content work that made the buyer aware a solution existed in the first place. Introducing a data-driven model into an organisation that has run on last-click for years is therefore not just a modelling upgrade; it is a change to how credit and budget get allocated internally, and it will be met with more scrutiny from whichever team stands to lose ground under the new numbers. The honest response is not to hide that dynamic but to surface it directly: run the new model alongside the old one for a full quarter before making any budget decision on it, let every stakeholder see both sets of numbers side by side, and let the removal-effect and Shapley results earn trust through consistency over that period rather than being imposed as an immediate replacement. A model that changes budget allocation without that transition period tends to generate more resistance to the model than genuine engagement with what it is showing.
Where the conversion-path data actually comes from
None of these models are useful without a genuinely reliable source of conversion-path data, and building that source well is worth its own attention. A GA4 property configured with proper channel grouping and cross-domain tracking, joined to your CRM’s touch history where a contact record shows every campaign and channel that reached them before a deal closed, is usually the richest available source, because it lets you extend a path beyond the browser session into the sales cycle itself, capturing touches like a sales call or a webinar attendance that a purely web-analytics view would never see. Building that joined view is itself the kind of extraction and warehousing work described elsewhere in this series, and it is worth treating as a prerequisite rather than an afterthought: an attribution model is only as trustworthy as the path data feeding it, and a model built on an incomplete or poorly joined path table will produce confident, precise-looking numbers that are quietly wrong from the very first step.
Key takeaways
- Build a clean, collapsed conversion-path table first; it feeds every model that follows.
- Rule-based models are transparent and useful as a baseline, but their weights are convention, not evidence.
- Removal-effect attribution asks how much conversions drop without a channel, which is a genuinely data-driven question.
- Shapley value allocates credit by average marginal contribution across every possible channel ordering.
- Run several models side by side; agreement across models is the strongest signal a channel truly matters.
- Beyond a handful of channels, sample Shapley permutations rather than enumerating them exhaustively.
Attribution is never a single true number waiting to be discovered; it is a modelling choice with consequences. Running the data-driven models alongside the simple ones is how you make that choice deliberately instead of by default.
References
Apply this to your business
Tracking and analytics
Trustworthy measurement from first click to revenue, visible in dashboards the team actually uses.
Conversion tracking, GA4, Tag Manager and dashboards implemented properly, so every marketing decision is made on data you can trust.
- Google Tag Manager
- GA4
- Looker Studio
- Microsoft Clarity
Marketing strategy
A clear positioning, channel plan and KPI framework that the whole organisation can execute against.
Market analysis, positioning and a growth plan your team can actually execute, built by someone who also implements the systems behind it.
- GA4
- Semrush
- HubSpot
- Looker Studio
Automation and AI
Documented, monitored automation that gives the team hours back and makes processes reliable.
n8n workflows and AI-assisted systems that remove repetitive marketing work, connect your tools and keep humans in control of what ships.
- n8n
- Zapier
- Make
- OpenAI API

