Action completed
manmarsci Logo manmarsci
Projects Experience Contact
Back to Projects | CASE STUDY ATTRIBUTION

Bespoke Attribution Modeling

Built a custom Markov-chain attribution engine to account for the multi-touch reality of modern B2B SaaS journeys, moving beyond last-click to uncover $450,000 in misallocated spend.

The Problem

A B2B SaaS company with a 45-day sales cycle was using last-click attribution to allocate a $1.2M annual ad budget. The data showed a clear problem: brand search and retargeting were getting 78% of credit, while top-of-funnel content and LinkedIn awareness campaigns were being defunded despite clear correlation with pipeline generation. The CMO needed an attribution model that reflected the true contribution of each touchpoint.

The Approach

  • Extracted 2.3M customer journeys from GA4, CRM, and ad platforms into a unified BigQuery dataset
  • Built a first-order Markov chain model calculating transition probabilities between 14 channel states
  • Simulated removal effect for each channel (10,000 Monte Carlo iterations) to quantify true contribution
  • Validated the model against holdout cohorts and actual revenue data with 94% correlation
  • Automated a Tableau dashboard updating weekly for stakeholder reporting and budget reallocation

Impact

Measurable Results

The Markov model revealed that LinkedIn awareness and organic content were being undervalued by 340% and 280% respectively under last-click attribution. By reallocating $450K from over-credited bottom-funnel retargeting to underfunded awareness, the company increased qualified pipeline by 23% while keeping total spend flat.

$450K
Reallocated
94%
Model Accuracy
+23%
Pipeline Growth
340%
Awareness Undervalued

Technical Implementation

# Markov Chain Attribution Engine
import pandas as pd
import numpy as np
from collections import defaultdict

# Build transition matrix from journeys
journeys = df['channel_path'].str.split(' > ')

# Count transitions
transitions = defaultdict(lambda: defaultdict(int))
for journey in journeys:
    for i in range(len(journey) - 1):
        transitions[journey[i]][journey[i+1]] += 1

# Convert to probability matrix
channels = list(transitions.keys())
n = len(channels)
P = np.zeros((n, n))
for i, ch_i in enumerate(channels):
    total = sum(transitions[ch_i].values())
    for j, ch_j in enumerate(channels):
        P[i][j] = transitions[ch_i][ch_j] / total if total > 0 else 0

# Calculate removal effect
baseline_conversion = simulate_journey(P, channels)
removal_effects = {}

for removed in channels:
    if removed in ['start', 'conversion']:
        continue
    P_removed = remove_channel(P, channels, removed)
    conv_without = simulate_journey(P_removed, [c for c in channels if c != removed])
    removal_effects[removed] = (baseline_conversion - conv_without) / baseline_conversion

# Normalize to attribution weights
attribution = {k: v/sum(removal_effects.values()) for k, v in removal_effects.items()}
print(pd.Series(attribution).sort_values(ascending=False))

Tools & Stack

PythonPandasNetworkXBigQueryTableauGA4SalesforceMonte Carlo

Key Learnings

  • Last-click systematically undervalues awareness by 3-5x in long sales cycles
  • Model validation against holdout data was essential — stakeholders trust what they can verify
  • The removal effect approach is more robust than Shapley value for sparse B2B journeys
  • Weekly automated dashboards were critical for maintaining budget reallocation discipline