#!/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()