Why Last-Click Attribution Breaks Down at Scale (and How to Fix It)
In performance marketing, the "last-click" attribution model is a relic. When you're managing thousands of partners driving high-velocity traffic, crediting only the final touchpoint before a conversion badly distorts partner value. It rewards bottom-of-funnel aggregators — the partner who happened to place the last link — while starving the top-of-funnel partners who actually generated the initial demand.
To build a sustainable, high-volume partner network, you need algorithmic multi-touch attribution. Below is a technical breakdown of how modern attribution engines actually process these events, with the math and pseudocode to back it up.
1. Markov Chains and the Removal Effect
Instead of assigning static credit to touchpoints (e.g. "40% first click, 40% last click, 20% middle"), Markov Chain modeling treats the user journey as a sequence of probabilistic state transitions between partners and a final "conversion" or "null" absorbing state.
You build a transition matrix from historical paths — the probability of moving from Partner A to Partner B, from Partner B to Conversion, and so on. Once you have that matrix, you can calculate each partner's Removal Effect: the probability that a conversion would not have occurred if that specific partner were removed from every path they appear in.
# Simplified removal-effect calculation
# paths: list of tuples, e.g. ("start", "partner_a", "partner_b", "conversion")
def build_transition_matrix(paths):
counts = defaultdict(lambda: defaultdict(int))
for path in paths:
for a, b in zip(path, path[1:]):
counts[a][b] += 1
probs = {}
for state, transitions in counts.items():
total = sum(transitions.values())
probs[state] = {s: c / total for s, c in transitions.items()}
return probs
def conversion_probability(matrix, start="start", target="conversion", visited=None):
visited = visited or set()
if start == target:
return 1.0
if start in visited or start not in matrix:
return 0.0
visited.add(start)
return sum(
p * conversion_probability(matrix, s, target, visited)
for s, p in matrix[start].items()
)
def removal_effect(paths, partner):
baseline_matrix = build_transition_matrix(paths)
baseline_cvr = conversion_probability(baseline_matrix)
pruned_paths = [tuple(s for s in p if s != partner) for p in paths]
pruned_matrix = build_transition_matrix(pruned_paths)
pruned_cvr = conversion_probability(pruned_matrix)
return (baseline_cvr - pruned_cvr) / baseline_cvr
Each partner's fractional credit is then their removal effect normalized against the sum of all partners' removal effects. This assigns credit based on actual mathematical necessity in the conversion path, not chronological position — which is why it survives scrutiny in a way heuristic models ("40/20/40") don't.
2. Time-Decay: the Half-Life of a Click
In high-velocity networks, a click from 30 days ago doesn't carry the same weight as a click from 3 hours ago. Time-decay models apply an exponential decay function to the credit assigned to earlier touchpoints:
credit(t) = credit_base * 0.5^(t / half_life)
Where t is the time elapsed between the touchpoint and the conversion, and half_life is a tunable parameter (commonly 7 days for e-commerce, often shorter for high-intent verticals like iGaming or fintech, where the consideration window is measured in hours, not weeks).
If a user clicks a partner link on Day 1 and converts on Day 14 with a 7-day half-life, that partner's raw credit is scaled by 0.5^(14/7) = 0.25 — a quarter of what a same-day click would earn. The practical implication for engineering teams: attribution weights aren't static once a conversion happens. If your model recalculates cohort windows retroactively (which most do, to catch late-arriving conversion events), you need a daily cron job or equivalent scheduled recompute — not a one-time batch job — or your weights silently drift stale.
3. Filtering Fraud Before It Pollutes the Model
Multi-touch models are only as good as the events feeding them, and high-volume partner networks are a constant target for cookie stuffing and bot traffic. Fraudulent touchpoints don't just inflate individual partner numbers — they distort the transition matrix itself, which means fraud filtering has to happen upstream of attribution, not as a cleanup pass after.
Common heuristic filters worth building in:
- Impossible travel — flagging a click in one geo and a conversion in another geo minutes later, faster than physical travel allows.
- Sub-minute conversions — click-to-conversion windows under 60 seconds are a strong signal of automated form-filling rather than a real user reading a landing page and deciding.
- User-agent / IP cross-referencing — checking incoming traffic against known data-center IP ranges and inconsistent user-agent/device fingerprint combinations.
- Velocity anomalies — a single partner ID generating an implausible click-to-conversion ratio spike relative to its own trailing baseline, which is often a better fraud signal than any single-event heuristic.
None of these are perfect in isolation — they're meant to run as a layered filter, flagging events for review or exclusion rather than making a single rule the final word.
The Engineering Takeaway
Last-click attribution survives mostly because it's simple to implement, not because it's accurate. Markov-chain removal effects and time-decay models both require more infrastructure — a transition matrix that updates as new path data arrives, a scheduled recompute for decay weights, and a fraud-filtering layer sitting in front of both — but they produce credit assignment that actually reflects which partners moved the user toward conversion. At scale, that difference compounds: it changes who gets paid, which partners you invest in growing, and ultimately which channels your network optimizes toward.
Disclosure: I write technical content for iGamingXpert, a partner-management platform built for regulated operator environments, which is where a lot of the fraud-heuristic patterns above come from in production. Their technical glossary covers more of this terminology if you want to go deeper.
Top comments (0)