Beyond ROAS: Creating Secondary Media Efficiency Metrics for Channel-Level Performance Analysis
Abstract
Traditional media efficiency metrics like Return on Ad Spend (ROAS) and Cost Per Acquisition (CPA) provide essential baseline performance indicators but often fail to capture the nuanced efficiency dynamics across marketing channels. This article presents a framework for developing secondary efficiency metrics that offer deeper insights into channel performance, diminishing returns, and optimal budget allocation using Python-based analytical methods.
Introduction
Media mix optimization has evolved significantly from simple last-click attribution models to sophisticated multi-touch attribution and marketing mix modeling (MMM) approaches [1]. However, even advanced models often rely on primary metrics that may not fully illuminate channel-level efficiency patterns. Secondary metrics — derived measures that contextualize raw performance data — enable marketers to understand not just what is happening, but why it’s happening and how to optimize accordingly [2].
This article explores the construction, implementation, and application of secondary media efficiency metrics using Python, focusing on practical methodologies that data scientists and marketing analysts can implement immediately.
Primary vs. Secondary Metrics: Definitions
Primary Metrics are direct measurements of marketing performance, including:
- ROAS (Return on Ad Spend): Revenue / Ad Spend
- CPA (Cost Per Acquisition): Ad Spend / Conversions
- CPM (Cost Per Thousand Impressions): (Ad Spend / Impressions) × 1000
- CTR (Click-Through Rate): (Clicks / Impressions) × 100
Secondary Metrics are derived measures that provide contextual efficiency insights:
- Marginal ROAS
- Efficiency Decay Rate
- Channel Saturation Index
- Budget Allocation Efficiency Score
- Cross-Channel Synergy Coefficient
Why Secondary Metrics Matter
Marketing channels exhibit non-linear responses to budget changes due to several factors:
- Audience Saturation: As spend increases, the available qualified audience diminishes [3]
- Diminishing Returns: Each incremental dollar typically generates less return than the previous one [4]
- Competitive Dynamics: Increased spending may trigger competitive responses
- Creative Fatigue: Repeated exposure reduces effectiveness over time [5]
Secondary metrics help quantify these phenomena, enabling more sophisticated optimization strategies.
Core Secondary Efficiency Metrics
1. Marginal ROAS (mROAS)
Definition: The return generated by the last incremental unit of spend, measuring the immediate efficiency of marginal budget allocation.
Formula:
mROAS = ΔRevenue / ΔSpend
Interpretation: While average ROAS shows overall efficiency, mROAS reveals whether additional spending will be productive. A channel with high average ROAS but low mROAS may be saturated [6].
Data Requirements:
- Sequential time-series data (daily or weekly granularity)
- Spend variation across periods (minimum 10–15% range)
- Revenue data with consistent attribution window
- Minimum 8–10 data points for meaningful calculation
2. Efficiency Decay Rate
Definition: The rate at which incremental returns diminish as spending increases, typically expressed as a percentage decrease per spending unit.
Formula:
Decay Rate = (mROAS_t - mROAS_t-1) / mROAS_t-1
Interpretation: Negative decay rates indicate diminishing returns. Larger negative values suggest faster saturation.
Data Requirements:
- Marginal ROAS values across multiple time periods
- Spend data sorted by magnitude (low to high)
- Minimum 6–8 sequential observations
- Stable market conditions to isolate spend effects
3. Channel Saturation Index (CSI)
Definition: A normalized measure (0–1) indicating how close a channel is to its saturation point, where 1 represents full saturation.
Formula:
CSI = 1 - (mROAS_current / mROAS_initial)
Interpretation: Values above 0.7 typically indicate significant saturation, suggesting budget reallocation may be beneficial.
Data Requirements:
- Historical baseline of mROAS at low/initial spend levels
- Current mROAS at present spend levels
- Minimum 10–12 weeks of data to establish baseline
- Spend range covering at least 50% variation from baseline
- Clean measurement period without major external shocks
4. Efficiency Elasticity
Definition: The percentage change in ROAS resulting from a 1% change in spend, measuring spend sensitivity.
Formula:
Elasticity = (ΔROAS / ROAS) / (ΔSpend / Spend)
Interpretation: Negative elasticity indicates inverse relationships; values near zero suggest saturation.
Data Requirements:
- Time series spend and revenue data (minimum 8 weeks)
- Sufficient spend variation to observe meaningful changes (>20% range)
- Consistent measurement methodology across periods
- ROAS data with stable denominator (avoid near-zero spend periods)
- Control for seasonality and external factors when possible
5. Cross-Channel Efficiency Score (CCES)
Definition: A composite index (0–100) that synthesizes multiple efficiency dimensions into a single comparable metric across channels, enabling rank-ordering and portfolio optimization.
Formula:
CCES = w₁(ROAS_norm) + w₂(mROAS_health) + w₃(1 - Saturation) + w₄(Stability) Where: - w₁, w₂, w₃, w₄ are weights (default: 0.30, 0.30, 0.25, 0.15) - ROAS_norm = (Channel_ROAS / Portfolio_Avg_ROAS) × 50, capped at 50 - mROAS_health = (mROAS / ROAS) × 30 - Saturation = Current Channel Saturation Index - Stability = (1 - CV_ROAS) × 15, where CV = coefficient of variation
Interpretation:
- Scores >75: Excellent efficiency, potential for increased investment
- Scores 50–75: Good efficiency, maintain current levels
- Scores 25–50: Moderate efficiency, monitor for optimization opportunities
- Scores <25: Poor efficiency, consider reallocation
Data Requirements:
- All data required for component metrics (ROAS, mROAS, saturation)
- Minimum 12 weeks for stability calculation
- Normalized comparison requires portfolio-level benchmarks
Advantages:
- Single comparable metric across heterogeneous channels
- Balances multiple efficiency dimensions
- Easily communicated to non-technical stakeholders
Limitations:
- Weight selection can be subjective
- May obscure important nuances in individual components
- Requires recalibration for different industries/business models
Metrics Summary Comparison
Metric Purpose Complexity Data Req. Update Freq. Best For Marginal ROAS Measure incremental efficiency Low 8+ weeks, sequential data Weekly Budget optimization decisions Efficiency Decay Rate Track diminishing returns Medium 6+ weeks, sorted spend Bi-weekly Saturation early warning Channel Saturation Index Quantify saturation level Medium 10+ weeks, baseline + current Weekly Reallocation triggers Efficiency Elasticity Assess spend sensitivity Medium 8+ weeks, >20% variation Monthly Strategic planning Cross-Channel Efficiency Score Holistic comparison High 12+ weeks, all components Bi-weekly Portfolio management
Python Implementation
Let’s implement these metrics using real-world inspired data:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import optimize
from sklearn.preprocessing import StandardScaler
# Generate sample channel performance data
np.random.seed(42)
def generate_channel_data(channel_name, base_spend, weeks=12):
"""Generate realistic channel performance data with diminishing returns"""
spend = np.linspace(base_spend * 0.5, base_spend * 1.5, weeks)
# Simulate diminishing returns using logarithmic function
noise = np.random.normal(0, 0.1, weeks)
revenue = (base_spend * 2.5) * np.log1p(spend / base_spend) * (1 + noise)
conversions = revenue / np.random.uniform(45, 55, weeks)
impressions = spend * np.random.uniform(800, 1200, weeks)
clicks = impressions * np.random.uniform(0.02, 0.04, weeks)
return pd.DataFrame({
'week': range(1, weeks + 1),
'channel': channel_name,
'spend': spend,
'revenue': revenue,
'conversions': conversions,
'impressions': impressions,
'clicks': clicks
})
# Create multi-channel dataset
channels = ['Paid Search', 'Paid Social', 'Display', 'Video']
base_spends = [50000, 40000, 30000, 35000]
df = pd.concat([generate_channel_data(ch, sp) for ch, sp in zip(channels, base_spends)])
# Calculate primary metrics
df['roas'] = df['revenue'] / df['spend']
df['cpa'] = df['spend'] / df['conversions']
df['cpm'] = (df['spend'] / df['impressions']) * 1000
df['ctr'] = (df['clicks'] / df['impressions']) * 100
print("Sample Data with Primary Metrics:")
print(df.groupby('channel')[['spend', 'revenue', 'roas', 'cpa']].mean())Calculating Marginal ROAS
def calculate_marginal_roas(df, channel, window=3):
"""Calculate marginal ROAS using rolling windows"""
channel_data = df[df['channel'] == channel].sort_values('spend').copy()
# Calculate incremental changes
channel_data['spend_delta'] = channel_data['spend'].diff()
channel_data['revenue_delta'] = channel_data['revenue'].diff()
# Calculate marginal ROAS
channel_data['marginal_roas'] = (
channel_data['revenue_delta'] / channel_data['spend_delta']
)
# Smooth with rolling average to reduce noise
channel_data['marginal_roas_smooth'] = (
channel_data['marginal_roas'].rolling(window=window, min_periods=1).mean()
)
return channel_data
# Calculate for all channels
marginal_results = []
for channel in channels:
result = calculate_marginal_roas(df, channel)
marginal_results.append(result)
marginal_df = pd.concat(marginal_results)
print("\nMarginal ROAS Analysis:")
for channel in channels:
channel_data = marginal_df[marginal_df['channel'] == channel]
avg_roas = channel_data['roas'].mean()
avg_mroas = channel_data['marginal_roas_smooth'].mean()
print(f"{channel}:")
print(f" Average ROAS: {avg_roas:.2f}")
print(f" Average Marginal ROAS: {avg_mroas:.2f}")
print(f" Efficiency Gap: {(avg_roas - avg_mroas):.2f}\n")Efficiency Decay Rate
def calculate_decay_rate(df, channel):
"""Calculate efficiency decay rate"""
channel_data = df[df['channel'] == channel].sort_values('spend').copy()
# Get marginal ROAS
channel_data['marginal_roas'] = (
channel_data['revenue'].diff() / channel_data['spend'].diff()
)
# Calculate decay rate
channel_data['decay_rate'] = (
channel_data['marginal_roas'].pct_change() * 100
)
return channel_data
decay_results = []
for channel in channels:
result = calculate_decay_rate(df, channel)
decay_results.append(result)
decay_df = pd.concat(decay_results)
print("Efficiency Decay Rates:")
for channel in channels:
channel_data = decay_df[decay_df['channel'] == channel]
avg_decay = channel_data['decay_rate'].mean()
print(f"{channel}: {avg_decay:.2f}% per period")Channel Saturation Index
def calculate_saturation_index(df, channel):
"""Calculate channel saturation index (0-1 scale)"""
channel_data = df[df['channel'] == channel].sort_values('spend').copy()
# Calculate marginal ROAS
channel_data['marginal_roas'] = (
channel_data['revenue'].diff() / channel_data['spend'].diff()
)
# Get initial (best) marginal ROAS
initial_mroas = channel_data['marginal_roas'].iloc[1:4].mean()
# Calculate saturation index
channel_data['saturation_index'] = 1 - (
channel_data['marginal_roas'] / initial_mroas
)
# Clip to 0-1 range
channel_data['saturation_index'] = channel_data['saturation_index'].clip(0, 1)
return channel_data
saturation_results = []
for channel in channels:
result = calculate_saturation_index(df, channel)
saturation_results.append(result)
saturation_df = pd.concat(saturation_results)
print("\nChannel Saturation Analysis:")
for channel in channels:
channel_data = saturation_df[saturation_df['channel'] == channel]
current_saturation = channel_data['saturation_index'].iloc[-1]
if current_saturation < 0.3:
status = "Low saturation - room for growth"
elif current_saturation < 0.7:
status = "Moderate saturation - monitor closely"
else:
status = "High saturation - consider reallocation"
print(f"{channel}: {current_saturation:.2%} - {status}")Efficiency Elasticity
def calculate_elasticity(df, channel):
"""Calculate efficiency elasticity"""
channel_data = df[df['channel'] == channel].sort_values('spend').copy()
# Calculate percentage changes
channel_data['spend_pct_change'] = channel_data['spend'].pct_change()
channel_data['roas_pct_change'] = channel_data['roas'].pct_change()
# Calculate elasticity
channel_data['elasticity'] = (
channel_data['roas_pct_change'] / channel_data['spend_pct_change']
)
return channel_data
elasticity_results = []
for channel in channels:
result = calculate_elasticity(df, channel)
elasticity_results.append(result)
elasticity_df = pd.concat(elasticity_results)
print("\nEfficiency Elasticity:")
for channel in channels:
channel_data = elasticity_df[elasticity_df['channel'] == channel]
avg_elasticity = channel_data['elasticity'].mean()
if avg_elasticity > -0.5:
interpretation = "Relatively insensitive to spend changes"
elif avg_elasticity > -1.5:
interpretation = "Moderate sensitivity"
else:
interpretation = "Highly sensitive to spend changes"
print(f"{channel}: {avg_elasticity:.2f} - {interpretation}")Advanced Applications
1. Budget Allocation Optimization
Using secondary metrics to optimize budget allocation:
def optimize_budget_allocation(df, total_budget, channels):
"""Optimize budget allocation using marginal ROAS"""
# Get current performance for each channel
channel_performance = {}
for channel in channels:
channel_data = df[df['channel'] == channel].sort_values('spend')
# Fit diminishing returns curve (logarithmic)
spend = channel_data['spend'].values
revenue = channel_data['revenue'].values
# Fit: revenue = a * log(spend + 1) + b
def revenue_function(spend, a, b):
return a * np.log1p(spend) + b
params, _ = optimize.curve_fit(revenue_function, spend, revenue)
channel_performance[channel] = {
'params': params,
'revenue_func': revenue_function
}
# Optimization function: maximize total revenue
def objective(budget_allocation):
total_revenue = 0
for i, channel in enumerate(channels):
params = channel_performance[channel]['params']
revenue_func = channel_performance[channel]['revenue_func']
total_revenue += revenue_func(budget_allocation[i], *params)
return -total_revenue # Negative because we minimize
# Constraints: budgets sum to total, all non-negative
constraints = [
{'type': 'eq', 'fun': lambda x: np.sum(x) - total_budget},
]
bounds = [(0, total_budget) for _ in channels]
# Initial guess: equal allocation
initial_guess = [total_budget / len(channels)] * len(channels)
# Optimize
result = optimize.minimize(
objective,
initial_guess,
method='SLSQP',
bounds=bounds,
constraints=constraints
)
return dict(zip(channels, result.x))
# Optimize allocation
optimal_allocation = optimize_budget_allocation(df, 150000, channels)
print("\nOptimal Budget Allocation:")
current_allocation = df.groupby('channel')['spend'].mean()
for channel in channels:
current = current_allocation[channel]
optimal = optimal_allocation[channel]
change = ((optimal - current) / current) * 100
print(f"{channel}:")
print(f" Current: ${current:,.0f}")
print(f" Optimal: ${optimal:,.0f}")
print(f" Change: {change:+.1f}%\n")2. Saturation Detection and Alerting
def detect_saturation_alerts(df, threshold=0.7):
"""Detect channels approaching saturation"""
alerts = []
for channel in df['channel'].unique():
channel_data = calculate_saturation_index(df, channel)
current_saturation = channel_data['saturation_index'].iloc[-1]
# Calculate trend
recent_saturation = channel_data['saturation_index'].iloc[-4:]
saturation_trend = recent_saturation.diff().mean()
if current_saturation > threshold:
alert = {
'channel': channel,
'saturation_level': current_saturation,
'trend': saturation_trend,
'severity': 'HIGH' if current_saturation > 0.85 else 'MEDIUM',
'recommendation': (
'Reduce spend and reallocate' if current_saturation > 0.85
else 'Monitor closely and consider reallocation'
)
}
alerts.append(alert)
return pd.DataFrame(alerts)
alerts_df = detect_saturation_alerts(saturation_df)
if len(alerts_df) > 0:
print("\nSaturation Alerts:")
for _, alert in alerts_df.iterrows():
print(f"\n{alert['channel']} - Severity: {alert['severity']}")
print(f" Saturation Level: {alert['saturation_level']:.1%}")
print(f" Trend: {alert['trend']:.3f}")
print(f" Recommendation: {alert['recommendation']}")
else:
print("\nNo saturation alerts detected.")3. Cross-Channel Efficiency Scoring
def calculate_efficiency_score(df):
"""Calculate comprehensive efficiency score (0-100)"""
scores = []
for channel in df['channel'].unique():
channel_data = df[df['channel'] == channel]
# Component 1: Normalized ROAS (30% weight)
roas_norm = channel_data['roas'].mean() / df['roas'].mean()
roas_score = min(roas_norm * 50, 50) # Cap at 50
# Component 2: Marginal ROAS health (30% weight)
marginal_data = calculate_marginal_roas(df, channel)
mroas_avg = marginal_data['marginal_roas_smooth'].mean()
roas_avg = channel_data['roas'].mean()
mroas_ratio = mroas_avg / roas_avg if roas_avg > 0 else 0
mroas_score = max(mroas_ratio * 30, 0)
# Component 3: Low saturation (25% weight)
saturation_data = calculate_saturation_index(df, channel)
saturation_level = saturation_data['saturation_index'].iloc[-1]
saturation_score = (1 - saturation_level) * 25
# Component 4: Stability (15% weight)
roas_cv = channel_data['roas'].std() / channel_data['roas'].mean()
stability_score = max((1 - roas_cv) * 15, 0)
total_score = roas_score + mroas_score + saturation_score + stability_score
scores.append({
'channel': channel,
'efficiency_score': total_score,
'roas_component': roas_score,
'mroas_component': mroas_score,
'saturation_component': saturation_score,
'stability_component': stability_score
})
return pd.DataFrame(scores).sort_values('efficiency_score', ascending=False)
efficiency_scores = calculate_efficiency_score(df)
print("\nChannel Efficiency Scores (0-100):")
print(efficiency_scores.to_string(index=False))Visualization
Effective visualization of secondary metrics enables stakeholder communication:
import matplotlib.pyplot as plt
import seaborn as sns
# Set style
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (14, 10)
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# Plot 1: ROAS vs Marginal ROAS
for channel in channels:
channel_data = marginal_df[marginal_df['channel'] == channel]
axes[0, 0].plot(channel_data['spend'], channel_data['roas'],
label=f'{channel} ROAS', linewidth=2)
axes[0, 0].plot(channel_data['spend'], channel_data['marginal_roas_smooth'],
label=f'{channel} mROAS', linestyle='--', alpha=0.7)
axes[0, 0].set_xlabel('Spend ($)')
axes[0, 0].set_ylabel('ROAS')
axes[0, 0].set_title('Average ROAS vs Marginal ROAS by Channel')
axes[0, 0].legend(bbox_to_anchor=(1.05, 1), loc='upper left')
axes[0, 0].grid(True, alpha=0.3)
# Plot 2: Saturation Index
for channel in channels:
channel_data = saturation_df[saturation_df['channel'] == channel]
axes[0, 1].plot(channel_data['spend'], channel_data['saturation_index'],
label=channel, linewidth=2, marker='o')
axes[0, 1].axhline(y=0.7, color='r', linestyle='--', label='High Saturation Threshold')
axes[0, 1].set_xlabel('Spend ($)')
axes[0, 1].set_ylabel('Saturation Index')
axes[0, 1].set_title('Channel Saturation Index Over Spend')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)
# Plot 3: Efficiency Score Breakdown
efficiency_scores_plot = efficiency_scores.set_index('channel')[
['roas_component', 'mroas_component', 'saturation_component', 'stability_component']
]
efficiency_scores_plot.plot(kind='bar', stacked=True, ax=axes[1, 0])
axes[1, 0].set_xlabel('Channel')
axes[1, 0].set_ylabel('Score')
axes[1, 0].set_title('Efficiency Score Breakdown by Component')
axes[1, 0].legend(title='Components', bbox_to_anchor=(1.05, 1), loc='upper left')
axes[1, 0].set_xticklabels(axes[1, 0].get_xticklabels(), rotation=45)
# Plot 4: Elasticity Comparison
elasticity_summary = elasticity_df.groupby('channel')['elasticity'].mean().sort_values()
axes[1, 1].barh(elasticity_summary.index, elasticity_summary.values)
axes[1, 1].axvline(x=0, color='black', linestyle='-', linewidth=0.5)
axes[1, 1].set_xlabel('Elasticity')
axes[1, 1].set_title('Average Efficiency Elasticity by Channel')
axes[1, 1].grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.savefig('/mnt/user-data/outputs/efficiency_metrics_visualization.png', dpi=300, bbox_inches='tight')
print("\nVisualization saved to efficiency_metrics_visualization.png")Implementation Recommendations
1. Data Requirements
For accurate secondary metric calculation:
- Minimum 8–12 weeks of historical data per channel
- Granular spend data (daily or weekly preferred)
- Clean conversion tracking with consistent attribution
- Stable measurement methodology across time periods
2. Frequency of Analysis
- Weekly: Monitor saturation indices and marginal ROAS
- Bi-weekly: Review efficiency scores and trends
- Monthly: Conduct comprehensive budget optimization
- Quarterly: Evaluate metric framework effectiveness
3. Threshold Calibration
Recommended starting thresholds (adjust based on industry):
- Saturation Index: Alert at >0.70, critical at >0.85
- mROAS/ROAS Ratio: Healthy at >0.70, concerning at <0.50
- Efficiency Elasticity: Saturated at <-1.5
Limitations and Considerations
Statistical Considerations
- Sample Size: Small sample sizes increase metric volatility [7]
- Seasonality: Adjust for known seasonal patterns
- External Factors: Competitive activity, market conditions affect metrics
- Attribution Windows: Longer windows may distort short-term metrics [8]
Practical Limitations
- Cross-Channel Effects: Secondary metrics may not capture synergies
- Non-Stationary Environments: Rapid market changes require frequent recalibration
- Creative Variation: Changes in creative can confound spend-response relationships
Conclusion
Secondary media efficiency metrics provide essential context beyond primary KPIs, enabling data-driven optimization decisions. By quantifying marginal returns, saturation, and elasticity, marketers can:
- Identify optimal budget allocation across channels
- Detect saturation before performance significantly degrades
- Understand the true incremental value of marketing investments
- Build more sophisticated optimization frameworks
The Python implementations provided offer a practical starting point for implementing these metrics. However, each organization should calibrate thresholds and weights based on their specific business context, industry dynamics, and data quality.
As marketing measurement continues to evolve toward privacy-first approaches and AI-driven optimization, secondary metrics will become increasingly important for understanding the “why” behind performance changes and making informed strategic decisions.
About the Author
Brian Curry is a Kansas City based Data Scientist, Artificial Intelligence expert and founder of Vector1 Research, a part-time passion project where he explores the frontier of Artificial Intelligence, SEO, Agentic Systems, Machine Learning, Economic Systems, Marketing Modeling, and Econometrics.
His work focuses on building intelligent systems that reveal the causal structures driving performance across marketing, content, and economics. He is the creator of:
- PyCausalSim: A Python framework for causal discovery through simulation
- Papilon: An open-source framework for complex system optimization
- Daedalus: A platform for causal and economic analysis
- Memory-Node Encapsulation (MNE): A revolutionary data structure for artificial episodic memory and cognitive AI reasoning
Contact: brian at vector1.ai | Vector1 AI Economics Lab
This article may be shared with attribution under Creative Commons Attribution 4.0 International (CC BY 4.0).
License: Creative Commons Attribution 4.0 (CC BY 4.0)
References
[1] Shao, X., & Li, L. (2011). Data-driven multi-touch attribution models. Proceedings of the 17th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 258–266.
[2] Berman, R. (2018). Beyond the last touch: Attribution in online advertising. Marketing Science, 37(5), 771–792.
[3] Sahni, N. S., Wheeler, S. C., & Chintagunta, P. (2018). Personalization in email marketing: The role of noninformative advertising content. Marketing Science, 37(2), 236–258.
[4] Danaher, P. J., & Dagger, T. S. (2013). Comparing the relative effectiveness of advertising channels: A case study of a multimedia blitz campaign. Journal of Marketing Research, 50(4), 517–534.
[5] Campbell, M. C., & Keller, K. L. (2003). Brand familiarity and advertising repetition effects. Journal of Consumer Research, 30(2), 292–304.
[6] Chan, T. Y., & Hamilton, B. H. (2006). Learning, private information, and the economic evaluation of randomized experiments. Journal of Political Economy, 114(6), 997–1040.
[7] Lewis, R. A., & Rao, J. M. (2015). The unfavorable economics of measuring the returns to advertising. Quarterly Journal of Economics, 130(4), 1941–1973.
[8] Anderl, E., Becker, I., Von Wangenheim, F., & Schumann, J. H. (2016). Mapping the customer journey: Lessons learned from graph-based online attribution modeling. International Journal of Research in Marketing, 33(3), 457–474.
Author’s Note: All Python code examples are provided for educational purposes. Actual implementation should include proper error handling, data validation, and calibration specific to your organization’s data and business context.


