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>
460 lines
18 KiB
Python
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()
|