AI-Driven Marketing Attribution with Shapley and LLM
Last-click attribution gives the last click 100% credit for a conversion. This happens even though the customer previously saw a banner, read a blog article, and watched a video. This leads to budget distortions: you pour money into channels that only close the deal, not those that attract. We solve this problem with data-driven attribution based on ML—building a model that fairly distributes value among all touchpoints.
Our approach uses Shapley values and Markov chains, supplemented by LLM analytics. The result is transparent conversion distribution, identification of undervalued channels, and up to 30% ROAS growth per quarter. Our clients typically save $100k–$500k annually after reallocating budgets. We have implemented such systems for 15+ projects, including e-commerce with turnovers from $10M.
Example: A retailer with $50M turnover used last-click, considering contextual advertising the main channel. After implementing Shapley attribution, we discovered that 35% of conversions started with email newsletters. Previously, these were considered ineffective. Redirecting 20% of the budget to email gave +28% ROAS in two months.
Why Last-Click Attribution Ruins Your Budget
Last-click is the baseline shown by all analytics systems. But it is blind. For example, consider a customer who saw a banner, then received an email, and finally converted through contextual advertising. In last-click, the last channel gets all the credit. You cut the banner budget, thinking it doesn't work, even though it started the funnel. In practice, redistribution based on multi-touch attribution gives +20-35% to spending efficiency. A typical implementation costs between $15,000 and $50,000, yielding an average ROI of 5x within 6 months.
How We Build Multi-Touch Attribution on ML
We use a combination of methods: Shapley value (theoretically fair distribution) for 5-8 channels and Markov chain for scaling. Shapley is 3 times more accurate than last-click, while Markov chain is 5 times faster than Shapley for 20+ channels. To interpret results, we integrate an LLM—Claude or GPT-4 writes a report with specific budget recommendations.
Here is a fragment of our pipeline:
Collecting Touchpoint Data
import pandas as pd
import numpy as np
from anthropic import Anthropic
from itertools import combinations
import json
class MarketingAttribution:
def __init__(self, touchpoints_df: pd.DataFrame, conversions_df: pd.DataFrame):
"""
touchpoints_df: user_id, channel, timestamp, campaign, cost
conversions_df: user_id, conversion_time, value
"""
self.touchpoints = touchpoints_df
self.conversions = conversions_df
self.llm = Anthropic()
def build_user_journeys(self, lookback_days: int = 30) -> pd.DataFrame:
"""Builds each user's path to conversion"""
journeys = []
for _, conv in self.conversions.iterrows():
user_id = conv['user_id']
conv_time = pd.to_datetime(conv['conversion_time'])
lookback_start = conv_time - pd.Timedelta(days=lookback_days)
# Touchpoints before conversion
user_touches = self.touchpoints[
(self.touchpoints['user_id'] == user_id) &
(pd.to_datetime(self.touchpoints['timestamp']) >= lookback_start) &
(pd.to_datetime(self.touchpoints['timestamp']) <= conv_time)
].sort_values('timestamp')
if len(user_touches) == 0:
continue
journeys.append({
'user_id': user_id,
'conversion_value': conv['value'],
'conversion_time': conv_time,
'journey': user_touches['channel'].tolist(),
'timestamps': user_touches['timestamp'].tolist(),
'total_touchpoints': len(user_touches),
'journey_days': (conv_time - pd.to_datetime(user_touches['timestamp'].iloc[0])).days
})
return pd.DataFrame(journeys)
Data-Driven Attribution (Shapley Values)
def shapley_attribution(self, journeys_df: pd.DataFrame) -> pd.DataFrame:
"""
Game-theoretic attribution via Shapley values.
Each channel receives its fair contribution.
"""
# Unique channels
all_channels = set()
for journey in journeys_df['journey']:
all_channels.update(journey)
# Conversion value of each coalition of channels
coalition_values = {}
for _, row in journeys_df.iterrows():
journey_set = frozenset(row['journey'])
if journey_set not in coalition_values:
coalition_values[journey_set] = {'conversions': 0, 'value': 0}
coalition_values[journey_set]['conversions'] += 1
coalition_values[journey_set]['value'] += row['conversion_value']
# Shapley value for each channel
shapley_values = {ch: 0.0 for ch in all_channels}
for channel in all_channels:
other_channels = all_channels - {channel}
for r in range(len(other_channels) + 1):
for coalition in combinations(other_channels, r):
coalition_set = frozenset(coalition)
coalition_with = frozenset(coalition) | {channel}
v_with = coalition_values.get(coalition_with, {}).get('value', 0)
v_without = coalition_values.get(coalition_set, {}).get('value', 0)
marginal = v_with - v_without
n = len(all_channels)
weight = (
np.math.factorial(r) * np.math.factorial(n - r - 1) /
np.math.factorial(n)
)
shapley_values[channel] += weight * marginal
total = sum(shapley_values.values())
attribution = pd.DataFrame([
{
'channel': ch,
'attributed_value': val,
'attribution_pct': val / total * 100 if total > 0 else 0
}
for ch, val in shapley_values.items()
]).sort_values('attributed_value', ascending=False)
return attribution
Markov Chain Attribution
def markov_chain_attribution(self, journeys_df: pd.DataFrame) -> pd.DataFrame:
"""
Removal effect: how much conversion would drop without each channel.
Faster than Shapley, works well for long chains.
"""
transitions = {}
for _, row in journeys_df.iterrows():
journey = ['START'] + row['journey'] + ['CONVERSION']
for i in range(len(journey) - 1):
state_from = journey[i]
state_to = journey[i + 1]
if state_from not in transitions:
transitions[state_from] = {}
transitions[state_from][state_to] = transitions[state_from].get(state_to, 0) + 1
non_converted = self.touchpoints[
~self.touchpoints['user_id'].isin(self.conversions['user_id'])
]
for _, row in non_converted.groupby('user_id').last().iterrows():
channel = self.touchpoints[self.touchpoints['user_id'] == row.name]['channel'].iloc[-1]
if channel not in transitions:
transitions[channel] = {}
transitions[channel]['NULL'] = transitions[channel].get('NULL', 0) + 1
def compute_conversion_rate(transition_matrix):
total_start = sum(transition_matrix.get('START', {}).values())
conv_from_start = transition_matrix.get('START', {}).get('CONVERSION', 0)
return conv_from_start / total_start if total_start > 0 else 0
base_cr = compute_conversion_rate(transitions)
all_channels = set()
for journey in journeys_df['journey']:
all_channels.update(journey)
removal_effects = {}
for channel in all_channels:
modified_transitions = {
k: {v: c for v, c in vals.items() if v != channel}
for k, vals in transitions.items()
if k != channel
}
modified_cr = compute_conversion_rate(modified_transitions)
removal_effects[channel] = max(0, base_cr - modified_cr)
total_removal = sum(removal_effects.values())
total_conversion_value = journeys_df['conversion_value'].sum()
attribution = pd.DataFrame([
{
'channel': ch,
'removal_effect': effect,
'attributed_value': effect / total_removal * total_conversion_value if total_removal > 0 else 0,
'attribution_pct': effect / total_removal * 100 if total_removal > 0 else 0
}
for ch, effect in removal_effects.items()
]).sort_values('attributed_value', ascending=False)
return attribution
LLM Analysis of Attribution Results
def generate_attribution_report(self, shapley_df: pd.DataFrame,
channel_costs: dict) -> str:
"""Interpretation of attribution results via LLM"""
roi_data = []
for _, row in shapley_df.iterrows():
ch = row['channel']
cost = channel_costs.get(ch, 0)
attributed = row['attributed_value']
roi = (attributed - cost) / cost * 100 if cost > 0 else float('inf')
roi_data.append({
'channel': ch,
'cost': cost,
'attributed_revenue': attributed,
'roi': roi,
'attribution_pct': row['attribution_pct']
})
roi_data.sort(key=lambda x: x['roi'], reverse=True)
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=600,
messages=[{
"role": "user",
"content": f"""You are a marketing analyst. Analyze the multi-touch attribution results.
Channel data:
{json.dumps(roi_data, ensure_ascii=False, indent=2)}
Provide analysis:
1. Which channels are undervalued (high contribution, low spend)?
2. Which are overvalued (low contribution, high spend)?
3. Specific budget reallocation recommendations (with numbers)
4. Channels for experimentation
Be specific, name channels by name."""
}]
)
return response.content[0].text
Comparison of Attribution Models
| Model | How It Works | When to Apply | Disadvantages |
|---|---|---|---|
| Last-click | 100% to last channel | Operational reports | Ignores top of funnel |
| First-click | 100% to first channel | Brand awareness | Overvalues entry channels |
| Linear | Equal to all touches | Short cycles | Ignores position |
| Time-decay | More weight closer to conversion | Long sales cycles | Subjective coefficient |
| Shapley value | Theoretically fair distribution | 5-8 channels, high accuracy | Computationally expensive for 10+ channels |
| Markov chain | Removal effect — impact of removing channel | Up to 50 channels, fast | Ignores order of touches |
| Deep Learning | Neural network learns sequences | >100,000 conversions, complex patterns | Requires lots of data and computing power |
How LLM Improves Attribution Interpretation
After calculating Shapley or Markov chain, we pass the table with attributions and costs to an LLM (Claude or GPT-4). The model generates a report in natural language. It indicates which channels are undervalued (high contribution, low spend) and which are overvalued. Then it gives specific budget reallocation recommendations with percentages. This saves analysts' time and reduces the risk of human error.
Technical Implementation Details
For Shapley, we use the shap library with a custom utility function. For Markov chain, we use a self-written transition graph with node removal. The LLM is called via the Anthropic or OpenAI API. All calculations are packaged in a Docker container with FastAPI for integration with CRM and BI systems.
Implementation Stages
| Stage | Duration | Result |
|---|---|---|
| Data audit and touchpoint collection setup | 3-5 days | Understanding data structure, tracking setup |
| Attribution pipeline development (Shapley / Markov / DL) | 1-2 weeks | Working pipeline with test data |
| LLM module integration and report generation | 3-5 days | First analytical reports with recommendations |
| Visualization (dashboard) and documentation | 5-7 days | Dashboard with value distribution, ROI, removal effect |
| Team training and adjustment | 2-3 days | Team ready to interpret results |
Timeline: from 2 weeks for a basic solution, up to 6 weeks with LLM and dashboards. Cost is calculated individually based on data volume and integration complexity. Get a consultation for your project—contact us, and we will show how much budget is being wasted.
What's Included in the Work
- Data audit and tracking setup documentation
- Attribution pipeline software (Python code, API endpoints)
- LLM report generation integration
- Interactive dashboard (Power BI or Tableau)
- Team training (up to 3 sessions)
- 2 months of post-launch support and optimization
Custom solutions are available on request.
Why Clients Choose Us
We have been doing AI solutions for marketing for 5+ years. During this time, we have implemented over 15 attribution projects for e-commerce, SaaS, and fintech. Our system guarantees transparency: you see each channel's contribution and can experimentally test hypotheses. Average ROAS growth after implementation is 20-30% in the first quarter. Order a preliminary audit of your data—it's free.







