Files
claude-skills-reference/marketing-skill/campaign-analytics/scripts/campaign_roi_calculator.py
Alireza Rezvani eef020c9e0 feat(skills): add 5 new skills via factory methodology (#176)
Build campaign-analytics, financial-analyst, customer-success-manager,
sales-engineer, and revenue-operations skills using the Claude Skills
Factory workflow. Each skill includes SKILL.md, Python CLI tools,
reference guides, and asset templates. All 16 Python scripts use
standard library only with --format json/text support.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 23:51:58 +01:00

460 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Campaign ROI Calculator - Comprehensive campaign ROI and performance metrics.
Calculates:
- ROI (Return on Investment)
- ROAS (Return on Ad Spend)
- CPA (Cost per Acquisition/Customer)
- CPL (Cost per Lead)
- CAC (Customer Acquisition Cost)
- CTR (Click-Through Rate)
- CVR (Conversion Rate - Leads to Customers)
Includes industry benchmarking and underperformance flagging.
Usage:
python campaign_roi_calculator.py campaign_data.json
python campaign_roi_calculator.py campaign_data.json --format json
"""
import argparse
import json
import sys
from typing import Any, Dict, List, Optional
# Industry benchmark ranges by channel
# Format: {metric: {channel: (low, target, high)}}
BENCHMARKS: Dict[str, Dict[str, tuple]] = {
"ctr": {
"email": (1.0, 2.5, 5.0),
"paid_search": (1.5, 3.5, 7.0),
"paid_social": (0.5, 1.2, 3.0),
"display": (0.05, 0.1, 0.5),
"organic_search": (1.5, 3.0, 8.0),
"organic_social": (0.5, 1.5, 4.0),
"referral": (1.0, 3.0, 6.0),
"direct": (2.0, 4.0, 8.0),
"default": (0.5, 2.0, 5.0),
},
"roas": {
"email": (30.0, 42.0, 60.0),
"paid_search": (2.0, 4.0, 8.0),
"paid_social": (1.5, 3.0, 6.0),
"display": (0.5, 1.5, 3.0),
"organic_search": (5.0, 10.0, 20.0),
"organic_social": (3.0, 6.0, 12.0),
"referral": (3.0, 5.0, 10.0),
"direct": (4.0, 8.0, 15.0),
"default": (2.0, 4.0, 8.0),
},
"cpa": {
"email": (5.0, 15.0, 40.0),
"paid_search": (20.0, 50.0, 150.0),
"paid_social": (15.0, 40.0, 100.0),
"display": (30.0, 75.0, 200.0),
"organic_search": (5.0, 20.0, 60.0),
"organic_social": (10.0, 30.0, 80.0),
"referral": (10.0, 25.0, 70.0),
"direct": (5.0, 15.0, 50.0),
"default": (15.0, 45.0, 120.0),
},
}
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Safely divide two numbers, returning default if denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def get_benchmark(metric: str, channel: str) -> tuple:
"""Get benchmark range for a metric and channel.
Returns:
Tuple of (low, target, high) for the given metric and channel.
"""
metric_benchmarks = BENCHMARKS.get(metric, {})
return metric_benchmarks.get(channel, metric_benchmarks.get("default", (0, 0, 0)))
def assess_performance(value: float, benchmark: tuple, higher_is_better: bool = True) -> str:
"""Assess a metric value against its benchmark range.
Args:
value: The metric value to assess.
benchmark: Tuple of (low, target, high).
higher_is_better: Whether higher values are better (True for CTR, ROAS; False for CPA).
Returns:
Performance assessment string.
"""
low, target, high = benchmark
if higher_is_better:
if value >= high:
return "excellent"
elif value >= target:
return "good"
elif value >= low:
return "below_target"
else:
return "underperforming"
else:
# For cost metrics, lower is better
if value <= low:
return "excellent"
elif value <= target:
return "good"
elif value <= high:
return "below_target"
else:
return "underperforming"
def calculate_campaign_metrics(campaign: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate all ROI metrics for a single campaign.
Args:
campaign: Dict with keys: name, channel, spend, revenue, impressions, clicks, leads, customers.
Returns:
Dict with all calculated metrics, benchmarks, and assessments.
"""
name = campaign.get("name", "Unnamed Campaign")
channel = campaign.get("channel", "default")
spend = campaign.get("spend", 0.0)
revenue = campaign.get("revenue", 0.0)
impressions = campaign.get("impressions", 0)
clicks = campaign.get("clicks", 0)
leads = campaign.get("leads", 0)
customers = campaign.get("customers", 0)
# Core metrics
roi = safe_divide(revenue - spend, spend) * 100
roas = safe_divide(revenue, spend)
cpa = safe_divide(spend, customers) if customers > 0 else None
cpl = safe_divide(spend, leads) if leads > 0 else None
cac = safe_divide(spend, customers) if customers > 0 else None
ctr = safe_divide(clicks, impressions) * 100 if impressions > 0 else None
cvr = safe_divide(customers, leads) * 100 if leads > 0 else None
cpc = safe_divide(spend, clicks) if clicks > 0 else None
cpm = safe_divide(spend, impressions) * 1000 if impressions > 0 else None
lead_conversion_rate = safe_divide(leads, clicks) * 100 if clicks > 0 else None
# Profit
profit = revenue - spend
# Benchmark assessments
assessments: Dict[str, Any] = {}
flags: List[str] = []
if ctr is not None:
benchmark = get_benchmark("ctr", channel)
assessment = assess_performance(ctr, benchmark, higher_is_better=True)
assessments["ctr"] = {
"value": round(ctr, 2),
"benchmark_range": {"low": benchmark[0], "target": benchmark[1], "high": benchmark[2]},
"assessment": assessment,
}
if assessment == "underperforming":
flags.append(f"CTR ({ctr:.2f}%) is below industry low ({benchmark[0]}%) for {channel}")
if roas > 0:
benchmark = get_benchmark("roas", channel)
assessment = assess_performance(roas, benchmark, higher_is_better=True)
assessments["roas"] = {
"value": round(roas, 2),
"benchmark_range": {"low": benchmark[0], "target": benchmark[1], "high": benchmark[2]},
"assessment": assessment,
}
if assessment == "underperforming":
flags.append(f"ROAS ({roas:.2f}x) is below industry low ({benchmark[0]}x) for {channel}")
if cpa is not None:
benchmark = get_benchmark("cpa", channel)
assessment = assess_performance(cpa, benchmark, higher_is_better=False)
assessments["cpa"] = {
"value": round(cpa, 2),
"benchmark_range": {"low": benchmark[0], "target": benchmark[1], "high": benchmark[2]},
"assessment": assessment,
}
if assessment == "underperforming":
flags.append(f"CPA (${cpa:.2f}) exceeds industry high (${benchmark[2]:.2f}) for {channel}")
if profit < 0:
flags.append(f"Campaign is unprofitable: ${profit:,.2f} net loss")
# Recommendations
recommendations: List[str] = []
if ctr is not None and assessments.get("ctr", {}).get("assessment") in ("below_target", "underperforming"):
recommendations.append("Improve ad creative and targeting to increase CTR")
if assessments.get("roas", {}).get("assessment") in ("below_target", "underperforming"):
recommendations.append("Review targeting and bid strategy to improve ROAS")
if assessments.get("cpa", {}).get("assessment") in ("below_target", "underperforming"):
recommendations.append("Optimize landing pages and conversion flow to reduce CPA")
if cvr is not None and cvr < 10:
recommendations.append("Lead-to-customer conversion is low; review sales process and lead quality")
if lead_conversion_rate is not None and lead_conversion_rate < 2:
recommendations.append("Click-to-lead rate is low; improve landing page relevance and form experience")
if profit > 0 and assessments.get("roas", {}).get("assessment") in ("good", "excellent"):
recommendations.append("Campaign performing well; consider scaling budget")
return {
"name": name,
"channel": channel,
"metrics": {
"spend": round(spend, 2),
"revenue": round(revenue, 2),
"profit": round(profit, 2),
"roi_pct": round(roi, 2),
"roas": round(roas, 2),
"cpa": round(cpa, 2) if cpa is not None else None,
"cpl": round(cpl, 2) if cpl is not None else None,
"cac": round(cac, 2) if cac is not None else None,
"ctr_pct": round(ctr, 2) if ctr is not None else None,
"cvr_pct": round(cvr, 2) if cvr is not None else None,
"cpc": round(cpc, 2) if cpc is not None else None,
"cpm": round(cpm, 2) if cpm is not None else None,
"lead_conversion_rate_pct": round(lead_conversion_rate, 2) if lead_conversion_rate is not None else None,
"impressions": impressions,
"clicks": clicks,
"leads": leads,
"customers": customers,
},
"assessments": assessments,
"flags": flags,
"recommendations": recommendations,
}
def calculate_portfolio_summary(campaign_results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Calculate aggregate metrics across all campaigns.
Args:
campaign_results: List of individual campaign result dicts.
Returns:
Portfolio-level summary with totals and weighted averages.
"""
total_spend = sum(c["metrics"]["spend"] for c in campaign_results)
total_revenue = sum(c["metrics"]["revenue"] for c in campaign_results)
total_impressions = sum(c["metrics"]["impressions"] for c in campaign_results)
total_clicks = sum(c["metrics"]["clicks"] for c in campaign_results)
total_leads = sum(c["metrics"]["leads"] for c in campaign_results)
total_customers = sum(c["metrics"]["customers"] for c in campaign_results)
total_profit = total_revenue - total_spend
underperforming = [c["name"] for c in campaign_results if c["flags"]]
top_performers = sorted(
campaign_results,
key=lambda c: c["metrics"]["roi_pct"],
reverse=True,
)
# Channel breakdown
channel_totals: Dict[str, Dict[str, float]] = {}
for c in campaign_results:
ch = c["channel"]
if ch not in channel_totals:
channel_totals[ch] = {"spend": 0, "revenue": 0, "leads": 0, "customers": 0}
channel_totals[ch]["spend"] += c["metrics"]["spend"]
channel_totals[ch]["revenue"] += c["metrics"]["revenue"]
channel_totals[ch]["leads"] += c["metrics"]["leads"]
channel_totals[ch]["customers"] += c["metrics"]["customers"]
channel_summary = {}
for ch, totals in channel_totals.items():
channel_summary[ch] = {
"spend": round(totals["spend"], 2),
"revenue": round(totals["revenue"], 2),
"roi_pct": round(safe_divide(totals["revenue"] - totals["spend"], totals["spend"]) * 100, 2),
"roas": round(safe_divide(totals["revenue"], totals["spend"]), 2),
"leads": int(totals["leads"]),
"customers": int(totals["customers"]),
}
return {
"total_campaigns": len(campaign_results),
"total_spend": round(total_spend, 2),
"total_revenue": round(total_revenue, 2),
"total_profit": round(total_profit, 2),
"portfolio_roi_pct": round(safe_divide(total_profit, total_spend) * 100, 2),
"portfolio_roas": round(safe_divide(total_revenue, total_spend), 2),
"total_impressions": total_impressions,
"total_clicks": total_clicks,
"total_leads": total_leads,
"total_customers": total_customers,
"blended_ctr_pct": round(safe_divide(total_clicks, total_impressions) * 100, 2),
"blended_cpl": round(safe_divide(total_spend, total_leads), 2) if total_leads > 0 else None,
"blended_cpa": round(safe_divide(total_spend, total_customers), 2) if total_customers > 0 else None,
"underperforming_campaigns": underperforming,
"top_performer": top_performers[0]["name"] if top_performers else None,
"channel_summary": channel_summary,
}
def format_text(results: Dict[str, Any]) -> str:
"""Format full results as human-readable text."""
lines: List[str] = []
lines.append("=" * 70)
lines.append("CAMPAIGN ROI ANALYSIS")
lines.append("=" * 70)
# Portfolio summary
summary = results["portfolio_summary"]
lines.append("")
lines.append("PORTFOLIO SUMMARY")
lines.append(f" Total Campaigns: {summary['total_campaigns']}")
lines.append(f" Total Spend: ${summary['total_spend']:>12,.2f}")
lines.append(f" Total Revenue: ${summary['total_revenue']:>12,.2f}")
lines.append(f" Total Profit: ${summary['total_profit']:>12,.2f}")
lines.append(f" Portfolio ROI: {summary['portfolio_roi_pct']}%")
lines.append(f" Portfolio ROAS: {summary['portfolio_roas']}x")
lines.append(f" Blended CTR: {summary['blended_ctr_pct']}%")
if summary["blended_cpl"] is not None:
lines.append(f" Blended CPL: ${summary['blended_cpl']:>12,.2f}")
if summary["blended_cpa"] is not None:
lines.append(f" Blended CPA: ${summary['blended_cpa']:>12,.2f}")
if summary["top_performer"]:
lines.append(f" Top Performer: {summary['top_performer']}")
if summary["underperforming_campaigns"]:
lines.append(f" Flagged: {', '.join(summary['underperforming_campaigns'])}")
# Channel summary
if summary["channel_summary"]:
lines.append("")
lines.append("-" * 70)
lines.append("CHANNEL SUMMARY")
lines.append(f" {'Channel':<20} {'Spend':>12} {'Revenue':>12} {'ROI':>10} {'ROAS':>8}")
lines.append(f" {'-'*20} {'-'*12} {'-'*12} {'-'*10} {'-'*8}")
for ch, cs in sorted(summary["channel_summary"].items()):
lines.append(
f" {ch:<20} ${cs['spend']:>10,.2f} ${cs['revenue']:>10,.2f} "
f"{cs['roi_pct']:>8.1f}% {cs['roas']:>6.2f}x"
)
# Individual campaigns
for campaign in results["campaigns"]:
lines.append("")
lines.append("-" * 70)
lines.append(f"CAMPAIGN: {campaign['name']}")
lines.append(f"Channel: {campaign['channel']}")
lines.append("-" * 70)
m = campaign["metrics"]
lines.append(f" {'Metric':<25} {'Value':>15}")
lines.append(f" {'-'*25} {'-'*15}")
lines.append(f" {'Spend':<25} ${m['spend']:>13,.2f}")
lines.append(f" {'Revenue':<25} ${m['revenue']:>13,.2f}")
lines.append(f" {'Profit':<25} ${m['profit']:>13,.2f}")
lines.append(f" {'ROI':<25} {m['roi_pct']:>13.2f}%")
lines.append(f" {'ROAS':<25} {m['roas']:>13.2f}x")
if m["cpa"] is not None:
lines.append(f" {'CPA':<25} ${m['cpa']:>13,.2f}")
if m["cpl"] is not None:
lines.append(f" {'CPL':<25} ${m['cpl']:>13,.2f}")
if m["cac"] is not None:
lines.append(f" {'CAC':<25} ${m['cac']:>13,.2f}")
if m["ctr_pct"] is not None:
lines.append(f" {'CTR':<25} {m['ctr_pct']:>13.2f}%")
if m["cpc"] is not None:
lines.append(f" {'CPC':<25} ${m['cpc']:>13,.2f}")
if m["cpm"] is not None:
lines.append(f" {'CPM':<25} ${m['cpm']:>13,.2f}")
if m["cvr_pct"] is not None:
lines.append(f" {'Lead-to-Customer CVR':<25} {m['cvr_pct']:>13.2f}%")
if m["lead_conversion_rate_pct"] is not None:
lines.append(f" {'Click-to-Lead Rate':<25} {m['lead_conversion_rate_pct']:>13.2f}%")
# Benchmark assessments
if campaign["assessments"]:
lines.append("")
lines.append(" BENCHMARK ASSESSMENT")
for metric_name, a in campaign["assessments"].items():
br = a["benchmark_range"]
status = a["assessment"].upper().replace("_", " ")
lines.append(
f" {metric_name.upper()}: {a['value']} "
f"[low={br['low']}, target={br['target']}, high={br['high']}] "
f"-> {status}"
)
# Flags
if campaign["flags"]:
lines.append("")
lines.append(" WARNING FLAGS")
for flag in campaign["flags"]:
lines.append(f" ! {flag}")
# Recommendations
if campaign["recommendations"]:
lines.append("")
lines.append(" RECOMMENDATIONS")
for i, rec in enumerate(campaign["recommendations"], 1):
lines.append(f" {i}. {rec}")
lines.append("")
return "\n".join(lines)
def main() -> None:
"""Main entry point for the campaign ROI calculator."""
parser = argparse.ArgumentParser(
description="Calculate campaign ROI, ROAS, CPA, CPL, CAC with industry benchmarking.",
epilog="Example: python campaign_roi_calculator.py campaigns.json --format json",
)
parser.add_argument(
"input_file",
help="Path to JSON file containing campaign data",
)
parser.add_argument(
"--format",
choices=["json", "text"],
default="text",
dest="output_format",
help="Output format (default: text)",
)
args = parser.parse_args()
# Load input data
try:
with open(args.input_file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {args.input_file}: {e}", file=sys.stderr)
sys.exit(1)
campaigns = data.get("campaigns", [])
if not campaigns:
print("Error: No 'campaigns' array found in input data.", file=sys.stderr)
sys.exit(1)
# Calculate metrics for each campaign
campaign_results = [calculate_campaign_metrics(c) for c in campaigns]
# Calculate portfolio summary
portfolio_summary = calculate_portfolio_summary(campaign_results)
results = {
"portfolio_summary": portfolio_summary,
"campaigns": campaign_results,
}
if args.output_format == "json":
print(json.dumps(results, indent=2))
else:
print(format_text(results))
if __name__ == "__main__":
main()