
Marketing Analyst
- 293 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
marketing-analyst is a Claude Code skill that turns campaign, funnel, and channel data into actionable spend, messaging, and growth experiment recommendations for developers running live SaaS, content, and ecommerce prod
About
marketing-analyst is a growth analytics skill for Claude Code that ingests campaign, funnel, and channel performance data and returns clear recommendations on budget allocation, messaging tests, and experiment priorities. The skill is designed for developers and product engineers who own metrics for live SaaS, content, or ecommerce surfaces and need analyst-style synthesis without standing up a separate BI workflow. It works by structuring raw or exported performance inputs into comparable channel views, flagging underperforming spend, and proposing testable messaging and funnel changes. Reach for marketing-analyst when you have real traffic or ad data and need decision-ready guidance on what to scale, cut, or A/B test next.
- Interprets campaign and funnel performance metrics
- Estimates channel ROI and budget tradeoffs
- Recommends A/B tests and messaging experiments
- Links acquisition trends to lifecycle retention signals
- Translates dashboards into distribution and content actions
Marketing Analyst by the numbers
- 293 all-time installs (skills.sh)
- Ranked #244 of 853 Sales & Marketing skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill marketing-analystAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 293 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
How do you turn campaign data into growth recommendations?
Turn campaign, funnel, and channel data into clear recommendations on spend, messaging tests, and growth experiments for live SaaS, content, and ecommerce products.
Who is it for?
Developers and product engineers who own live SaaS, content, or ecommerce metrics and need analyst-style recommendations from campaign and funnel exports.
Skip if: Teams with no live traffic or conversion data yet, or developers who only need one-off copywriting without performance analysis.
When should I use this skill?
The user shares campaign, funnel, or channel performance data and asks where to shift spend, what to test, or which growth experiments to run next.
What you get
Channel performance summaries, spend reallocation recommendations, messaging test ideas, and prioritized growth experiment backlog
- Spend recommendations
- Messaging test plan
- Growth experiment backlog
Files
Marketing Analyst
The agent operates as a senior marketing analyst, delivering campaign performance analysis, multi-touch attribution, marketing mix modeling, ROI measurement, and data-driven budget optimization.
Clarify First
Before running the analysis, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- [ ] Campaigns/channels in scope — which campaigns or channels and the date range (defines the dataset and report boundaries)
- [ ] KPIs and their targets — CPL, CAC, ROAS, pipeline, revenue, each with a target and a data source (drives the target-vs-actual performance table)
- [ ] Sales-cycle length — short vs long B2B cycle (determines attribution model and whether to report pipeline vs closed revenue)
- [ ] Report audience — exec summary vs ops deep-dive (sets the altitude and which sections matter most)
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Workflow
1. Define measurement objectives - Identify which campaigns, channels, or initiatives require analysis. Confirm KPIs (CPL, CAC, ROAS, pipeline, revenue). Checkpoint: every KPI has a target and a data source. 2. Collect and validate data - Pull campaign data from ad platforms, CRM, and analytics tools. Validate completeness and consistency. Checkpoint: no channel has >5% missing data. 3. Run attribution analysis - Apply multiple attribution models (first-touch, last-touch, linear, time-decay, position-based) and compare channel credit allocation. Checkpoint: results are compared across at least 3 models. 4. Analyze campaign performance - Calculate ROI, ROAS, CPL, CAC, and conversion rates per campaign. Identify top and bottom performers. Checkpoint: performance table includes target vs. actual for every metric. 5. Optimize budget allocation - Use marketing mix modeling or ROI data to recommend budget shifts. Checkpoint: reallocation recommendations are backed by expected ROI per channel. 6. Build executive report - Summarize headline metrics, wins, challenges, and next-period focus. Checkpoint: report passes the "so what" test (every data point has an actionable insight).
Marketing Metrics Reference
Acquisition Metrics
| Metric | Formula | Benchmark |
|---|---|---|
| CPL | Spend / Leads | Varies by industry |
| CAC | S&M Spend / New Customers | LTV/CAC > 3:1 |
| CPA | Spend / Acquisitions | Target specific |
| ROAS | Revenue / Ad Spend | > 4:1 |
Engagement Metrics
| Metric | Formula | Benchmark |
|---|---|---|
| Engagement Rate | Engagements / Impressions | 1-5% |
| CTR | Clicks / Impressions | 0.5-2% |
| Conversion Rate | Conversions / Visitors | 2-5% |
| Bounce Rate | Single-page sessions / Total | < 50% |
Retention Metrics
| Metric | Formula | Benchmark |
|---|---|---|
| Churn Rate | Lost Customers / Total | < 5% monthly |
| NRR | (MRR - Churn + Expansion) / MRR | > 100% |
| LTV | ARPU x Gross Margin x Lifetime | 3x+ CAC |
Attribution Modeling
Model Comparison
The agent should apply multiple models and compare results to identify channel over/under-valuation:
| Model | Logic | Best For |
|---|---|---|
| First-touch | 100% credit to first interaction | Measuring awareness channels |
| Last-touch | 100% credit to final interaction | Measuring conversion channels |
| Linear | Equal credit across all touches | Balanced view of full journey |
| Time-decay | More credit to recent touches | Short sales cycles |
| Position-based | 40% first, 40% last, 20% middle | Most B2B scenarios |
Attribution Calculator
def calculate_attribution(touchpoints, model='position'):
"""Calculate attribution credit for a conversion journey.
Args:
touchpoints: List of channel names in order of interaction
model: One of 'first', 'last', 'linear', 'time_decay', 'position'
Returns:
Dict mapping channel -> credit (sums to 1.0)
Example:
>>> calculate_attribution(['paid_search', 'email', 'organic', 'direct'], 'position')
{'paid_search': 0.4, 'email': 0.1, 'organic': 0.1, 'direct': 0.4}
"""
n = len(touchpoints)
credits = {}
if model == 'first':
credits[touchpoints[0]] = 1.0
elif model == 'last':
credits[touchpoints[-1]] = 1.0
elif model == 'linear':
for tp in touchpoints:
credits[tp] = credits.get(tp, 0) + 1.0 / n
elif model == 'time_decay':
decay = 0.7
total = sum(decay ** i for i in range(n))
for i, tp in enumerate(reversed(touchpoints)):
credits[tp] = credits.get(tp, 0) + (decay ** i) / total
elif model == 'position':
if n == 1:
credits[touchpoints[0]] = 1.0
elif n == 2:
credits[touchpoints[0]] = 0.5
credits[touchpoints[-1]] = credits.get(touchpoints[-1], 0) + 0.5
else:
credits[touchpoints[0]] = 0.4
credits[touchpoints[-1]] = credits.get(touchpoints[-1], 0) + 0.4
for tp in touchpoints[1:-1]:
credits[tp] = credits.get(tp, 0) + 0.2 / (n - 2)
return creditsExample: Campaign Analysis Report
# Campaign Analysis: Q1 2026 Product Launch
## Performance Summary
| Metric | Target | Actual | vs Target |
|--------------|---------|---------|-----------|
| Impressions | 500K | 612K | +22% |
| Clicks | 25K | 28.4K | +14% |
| Leads | 1,200 | 1,350 | +13% |
| MQLs | 360 | 410 | +14% |
| Pipeline | $1.2M | $1.45M | +21% |
| Revenue | $380K | $425K | +12% |
## Channel Breakdown
| Channel | Spend | Leads | CPL | Pipeline |
|--------------|---------|-------|-------|----------|
| Paid Search | $45K | 520 | $87 | $580K |
| LinkedIn Ads | $30K | 310 | $97 | $420K |
| Email | $5K | 380 | $13 | $350K |
| Content/SEO | $8K | 140 | $57 | $100K |
## Key Insight
Email delivers lowest CPL ($13) and strong pipeline. Recommend shifting
10% of LinkedIn budget to email nurture sequences for Q2.Budget Optimization Framework
Budget Allocation Recommendation
Channel Current Optimal Change Expected ROI
Paid Search 30% 35% +5% 4.2x
Social Paid 25% 20% -5% 2.8x
Display 15% 10% -5% 1.5x
Email 10% 15% +5% 8.5x
Content 10% 12% +2% 5.2x
Events 10% 8% -2% 2.2x
Projected Impact: +15% pipeline with same budgetA/B Test Statistical Analysis
from scipy import stats
import numpy as np
def analyze_ab_test(control_conv, control_total, treatment_conv, treatment_total, alpha=0.05):
"""Analyze A/B test for statistical significance.
Example:
>>> result = analyze_ab_test(150, 5000, 195, 5000)
>>> result['significant']
True
>>> f"{result['lift_pct']:.1f}%"
'30.0%'
"""
p_c = control_conv / control_total
p_t = treatment_conv / treatment_total
p_pool = (control_conv + treatment_conv) / (control_total + treatment_total)
se = np.sqrt(p_pool * (1 - p_pool) * (1/control_total + 1/treatment_total))
z = (p_t - p_c) / se
p_value = 2 * (1 - stats.norm.cdf(abs(z)))
return {
'control_rate': p_c,
'treatment_rate': p_t,
'lift_pct': ((p_t - p_c) / p_c) * 100,
'p_value': p_value,
'significant': p_value < alpha,
}Scripts
# Campaign analyzer
python scripts/campaign_analyzer.py --data campaigns.csv --output report.html
# Attribution calculator
python scripts/attribution.py --touchpoints journeys.csv --model position
# ROI calculator
python scripts/roi_calculator.py --spend spend.csv --revenue revenue.csv
# Forecast generator
python scripts/forecast.py --historical data.csv --periods 6Reference Materials
references/metrics.md- Marketing metrics guidereferences/attribution.md- Attribution modelingreferences/reporting.md- Reporting best practicesreferences/forecasting.md- Forecasting methods
---
Troubleshooting
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Attribution models give wildly different channel credit allocations | No single model captures full truth; each has structural bias | Run 3+ models (first-touch, last-touch, position-based) and compare; use position-based as default for B2B |
| ROAS calculations look great but pipeline is flat | Revenue attribution counting existing customers, not new pipeline | Separate new business attribution from expansion; report pipeline separately from revenue |
| Marketing reports and sales reports show different lead counts | Marketing counts MQLs at form fill, sales counts at CRM entry with different criteria | Align on shared definitions: document exact MQL, SQL, and opportunity criteria in a shared SLA |
| Forecast consistently over-predicts by 20%+ | Model uses linear extrapolation without accounting for seasonality or saturation | Apply dampening factors for longer forecasts; use ensemble method (linear + growth rate + moving average) |
| Executive dashboard takes too long to build each month | Manual data pulls from 5+ platforms with different schemas | Automate data collection; standardize UTM and naming conventions so cross-platform analysis is consistent |
| Channel ROI is negative but still generating pipeline | Long B2B sales cycle means revenue attribution has not caught up to spend | Use pipeline-based attribution for channels with 3+ month sales cycles rather than closed-won revenue |
---
Success Criteria
- Multi-touch attribution model deployed comparing 3+ models with documented channel credit differences
- Monthly marketing report delivered within 3 business days of month close
- Budget reallocation recommendations backed by per-channel ROI data and implemented quarterly
- Forecast accuracy within 15% of actual for 3-month projections
- Campaign performance reports include target vs actual for every KPI
- Every data point in executive reports has an actionable insight (passes "so what" test)
- Channel data completeness above 95% (no channel has >5% missing data)
---
Scope & Limitations
In Scope: Campaign performance analysis, multi-touch attribution modeling, marketing mix optimization, ROI/ROAS calculation, budget allocation recommendations, executive reporting, cohort retention analysis, marketing forecasting.
Out of Scope: Analytics implementation and tracking setup (see analytics-tracking skill), product analytics (see product-team skills), financial modeling beyond marketing metrics (see finance skill), data engineering and warehouse management.
Limitations: Attribution models are approximations — no model perfectly captures the buyer journey, especially for high-touch B2B sales. Forecasting uses historical extrapolation with dampening; it does not account for market disruptions or competitive moves. Budget optimization assumes linear channel scaling; most channels have diminishing returns at scale.
---
Scripts
| Script | Purpose | Usage |
|---|---|---|
scripts/channel_mix_optimizer.py | Analyze channel performance and recommend optimal budget allocation | python scripts/channel_mix_optimizer.py channels.json --budget 100000 --demo |
scripts/cohort_analyzer.py | Analyze user retention by cohort, identify trends and best/worst performers | python scripts/cohort_analyzer.py cohort_data.json --demo |
scripts/marketing_forecast_generator.py | Generate marketing forecasts using linear, growth rate, and ensemble methods | python scripts/marketing_forecast_generator.py historical.json --periods 6 |
#!/usr/bin/env python3
"""Channel Mix Optimizer - Optimize marketing budget allocation across channels.
Analyzes channel performance data (spend, leads, revenue) and recommends
optimal budget reallocation to maximize ROI.
Usage:
python channel_mix_optimizer.py channels.json
python channel_mix_optimizer.py channels.json --budget 100000 --json
python channel_mix_optimizer.py --demo
"""
import argparse
import json
import sys
def analyze_channels(channels, total_budget=None):
"""Analyze channel performance and recommend budget allocation."""
results = []
total_spend = sum(ch.get("spend", 0) for ch in channels)
total_leads = sum(ch.get("leads", 0) for ch in channels)
total_revenue = sum(ch.get("revenue", 0) for ch in channels)
if total_budget is None:
total_budget = total_spend
for ch in channels:
name = ch.get("name", ch.get("channel", "Unknown"))
spend = ch.get("spend", 0)
leads = ch.get("leads", 0)
revenue = ch.get("revenue", 0)
customers = ch.get("customers", 0)
impressions = ch.get("impressions", 0)
clicks = ch.get("clicks", 0)
# Calculate metrics
cpl = spend / max(leads, 1)
cac = spend / max(customers, 1) if customers else None
roas = revenue / max(spend, 1)
roi = ((revenue - spend) / max(spend, 1)) * 100
ctr = (clicks / max(impressions, 1)) * 100 if impressions else None
conversion_rate = (leads / max(clicks, 1)) * 100 if clicks else None
spend_share = (spend / max(total_spend, 1)) * 100
revenue_share = (revenue / max(total_revenue, 1)) * 100
efficiency_index = revenue_share / max(spend_share, 0.1)
results.append({
"channel": name,
"spend": spend,
"leads": leads,
"revenue": revenue,
"customers": customers,
"cpl": round(cpl, 2),
"cac": round(cac, 2) if cac else None,
"roas": round(roas, 2),
"roi": round(roi, 1),
"ctr": round(ctr, 2) if ctr else None,
"conversion_rate": round(conversion_rate, 2) if conversion_rate else None,
"spend_share": round(spend_share, 1),
"revenue_share": round(revenue_share, 1),
"efficiency_index": round(efficiency_index, 2),
})
# Rank by efficiency
results.sort(key=lambda x: x["efficiency_index"], reverse=True)
# Calculate optimal allocation
# Weighted by efficiency index
total_efficiency = sum(r["efficiency_index"] for r in results)
recommendations = []
for r in results:
weight = r["efficiency_index"] / max(total_efficiency, 0.01)
# Blend current allocation with efficiency-based allocation (70/30)
current_share = r["spend_share"] / 100
optimal_share = weight
recommended_share = (current_share * 0.3 + optimal_share * 0.7)
recommended_budget = int(total_budget * recommended_share)
budget_change = recommended_budget - r["spend"]
change_pct = ((recommended_budget - r["spend"]) / max(r["spend"], 1)) * 100
# Project ROI at new budget (linear assumption with diminishing returns)
diminishing_factor = 0.85 if budget_change > 0 else 1.1
projected_revenue = r["revenue"] * (recommended_budget / max(r["spend"], 1)) * diminishing_factor
projected_roas = projected_revenue / max(recommended_budget, 1)
recommendations.append({
"channel": r["channel"],
"current_budget": r["spend"],
"recommended_budget": recommended_budget,
"budget_change": budget_change,
"change_pct": round(change_pct, 1),
"current_roas": r["roas"],
"projected_roas": round(projected_roas, 2),
"efficiency_rank": results.index(r) + 1,
"action": "increase" if budget_change > 0 else ("decrease" if budget_change < 0 else "maintain"),
})
# Normalize recommendations to total budget
total_recommended = sum(r["recommended_budget"] for r in recommendations)
if total_recommended > 0:
scale_factor = total_budget / total_recommended
for r in recommendations:
r["recommended_budget"] = int(r["recommended_budget"] * scale_factor)
r["budget_change"] = r["recommended_budget"] - [ch for ch in results if ch["channel"] == r["channel"]][0]["spend"]
# Summary
current_total_roas = total_revenue / max(total_spend, 1)
projected_total_revenue = sum(
r["projected_roas"] * r["recommended_budget"] for r in recommendations
)
projected_total_roas = projected_total_revenue / max(total_budget, 1)
return {
"summary": {
"total_budget": total_budget,
"current_spend": total_spend,
"total_leads": total_leads,
"total_revenue": total_revenue,
"current_roas": round(current_total_roas, 2),
"projected_roas": round(projected_total_roas, 2),
"projected_revenue": round(projected_total_revenue, 2),
"revenue_improvement": round(
((projected_total_revenue - total_revenue) / max(total_revenue, 1)) * 100, 1
),
},
"channels": results,
"recommendations": sorted(recommendations, key=lambda x: x["budget_change"], reverse=True),
}
def get_demo_data():
return [
{"name": "Paid Search", "spend": 45000, "leads": 520, "revenue": 180000, "customers": 52, "impressions": 250000, "clicks": 12500},
{"name": "LinkedIn Ads", "spend": 30000, "leads": 310, "revenue": 145000, "customers": 35, "impressions": 180000, "clicks": 5400},
{"name": "Email Marketing", "spend": 5000, "leads": 380, "revenue": 120000, "customers": 40, "impressions": 0, "clicks": 0},
{"name": "Content/SEO", "spend": 12000, "leads": 250, "revenue": 95000, "customers": 28, "impressions": 0, "clicks": 0},
{"name": "Display Ads", "spend": 15000, "leads": 90, "revenue": 22000, "customers": 8, "impressions": 500000, "clicks": 3500},
{"name": "Social Organic", "spend": 3000, "leads": 120, "revenue": 38000, "customers": 15, "impressions": 100000, "clicks": 8000},
]
def format_report(analysis):
"""Format human-readable report."""
lines = []
lines.append("=" * 75)
lines.append("CHANNEL MIX OPTIMIZATION REPORT")
lines.append("=" * 75)
s = analysis["summary"]
lines.append(f"Total Budget: ${s['total_budget']:,.0f}")
lines.append(f"Current ROAS: {s['current_roas']:.1f}x")
lines.append(f"Projected ROAS: {s['projected_roas']:.1f}x")
lines.append(f"Revenue Improvement: {s['revenue_improvement']:+.1f}%")
lines.append("")
# Channel performance
lines.append("--- CHANNEL PERFORMANCE (ranked by efficiency) ---")
lines.append(f"{'Channel':<20} {'Spend':>10} {'Revenue':>10} {'ROAS':>6} {'CPL':>8} {'Efficiency':>10}")
lines.append("-" * 70)
for ch in analysis["channels"]:
lines.append(
f"{ch['channel']:<20} ${ch['spend']:>9,} ${ch['revenue']:>9,} "
f"{ch['roas']:>5.1f}x ${ch['cpl']:>7,.0f} {ch['efficiency_index']:>10.2f}"
)
lines.append("")
# Recommendations
lines.append("--- BUDGET RECOMMENDATIONS ---")
lines.append(f"{'Channel':<20} {'Current':>10} {'Recommended':>12} {'Change':>10} {'Action':>10}")
lines.append("-" * 65)
for r in analysis["recommendations"]:
lines.append(
f"{r['channel']:<20} ${r['current_budget']:>9,} ${r['recommended_budget']:>11,} "
f"{r['change_pct']:>+9.0f}% {r['action']:>10}"
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Optimize marketing budget allocation across channels")
parser.add_argument("input", nargs="?", help="JSON file with channel performance data")
parser.add_argument("--budget", type=float, help="Total budget to allocate")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output JSON")
parser.add_argument("--demo", action="store_true", help="Run with demo data")
args = parser.parse_args()
if args.demo:
channels = get_demo_data()
elif args.input:
try:
with open(args.input, "r", encoding="utf-8") as f:
data = json.load(f)
channels = data if isinstance(data, list) else data.get("channels", [])
except FileNotFoundError:
print(f"Error: File not found: {args.input}", file=sys.stderr)
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
analysis = analyze_channels(channels, args.budget)
if args.json_output:
print(json.dumps(analysis, indent=2))
else:
print(format_report(analysis))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Cohort Analyzer - Analyze user retention and behavior by cohort.
Generates cohort retention tables, identifies trends, and calculates
key retention metrics (D1/D7/D30, average lifetime, LTV).
Usage:
python cohort_analyzer.py cohort_data.json
python cohort_analyzer.py cohort_data.json --json
python cohort_analyzer.py --demo
"""
import argparse
import json
import sys
def analyze_cohorts(cohort_data):
"""Analyze cohort retention data."""
cohorts = cohort_data if isinstance(cohort_data, list) else cohort_data.get("cohorts", [])
results = []
all_retention_by_period = {}
for cohort in cohorts:
name = cohort.get("name", cohort.get("cohort", "Unknown"))
initial_users = cohort.get("initial_users", cohort.get("users", 0))
retention = cohort.get("retention", cohort.get("active_users", []))
# Calculate retention rates
retention_rates = []
for i, active in enumerate(retention):
rate = (active / max(initial_users, 1)) * 100
retention_rates.append({
"period": i,
"active_users": active,
"retention_rate": round(rate, 1),
"churned": initial_users - active if i == 0 else retention[i - 1] - active,
})
if i not in all_retention_by_period:
all_retention_by_period[i] = []
all_retention_by_period[i].append(rate)
# Key retention milestones
d1 = retention_rates[1]["retention_rate"] if len(retention_rates) > 1 else None
d7 = retention_rates[7]["retention_rate"] if len(retention_rates) > 7 else None
d30 = retention_rates[30]["retention_rate"] if len(retention_rates) > 30 else None
w1 = retention_rates[1]["retention_rate"] if len(retention_rates) > 1 else None
# Calculate average lifetime (simplified)
total_active_periods = sum(r["active_users"] for r in retention_rates)
avg_lifetime = total_active_periods / max(initial_users, 1)
results.append({
"cohort": name,
"initial_users": initial_users,
"retention_rates": retention_rates,
"milestones": {
"period_1": round(d1, 1) if d1 else None,
"period_7": round(d7, 1) if d7 else None,
"period_30": round(d30, 1) if d30 else None,
},
"avg_lifetime_periods": round(avg_lifetime, 1),
"final_retention": retention_rates[-1]["retention_rate"] if retention_rates else 0,
})
# Cross-cohort trend analysis
trends = {}
for period, rates in all_retention_by_period.items():
if len(rates) >= 2:
trends[period] = {
"avg_retention": round(sum(rates) / len(rates), 1),
"min_retention": round(min(rates), 1),
"max_retention": round(max(rates), 1),
"improving": rates[-1] > rates[0] if len(rates) >= 2 else None,
}
# Identify best and worst cohorts
if results:
final_retentions = [(r["cohort"], r["final_retention"]) for r in results]
best_cohort = max(final_retentions, key=lambda x: x[1])
worst_cohort = min(final_retentions, key=lambda x: x[1])
else:
best_cohort = worst_cohort = None
return {
"cohorts": results,
"trends": trends,
"summary": {
"total_cohorts": len(results),
"best_cohort": {"name": best_cohort[0], "final_retention": best_cohort[1]} if best_cohort else None,
"worst_cohort": {"name": worst_cohort[0], "final_retention": worst_cohort[1]} if worst_cohort else None,
"avg_final_retention": round(
sum(r["final_retention"] for r in results) / max(len(results), 1), 1
),
},
}
def get_demo_data():
return {
"cohorts": [
{"name": "Jan W1", "initial_users": 1000, "retention": [1000, 450, 350, 280, 250]},
{"name": "Jan W2", "initial_users": 1100, "retention": [1100, 528, 418, 352, 308]},
{"name": "Jan W3", "initial_users": 950, "retention": [950, 494, 399, 333, 295]},
{"name": "Jan W4", "initial_users": 1050, "retention": [1050, 578, 473, 399, 357]},
{"name": "Feb W1", "initial_users": 1200, "retention": [1200, 684, 564, 480, 432]},
],
}
def format_report(analysis):
"""Format human-readable cohort report."""
lines = []
lines.append("=" * 70)
lines.append("COHORT RETENTION ANALYSIS")
lines.append("=" * 70)
s = analysis["summary"]
lines.append(f"Cohorts Analyzed: {s['total_cohorts']}")
lines.append(f"Avg Final Retention: {s['avg_final_retention']:.1f}%")
if s["best_cohort"]:
lines.append(f"Best Cohort: {s['best_cohort']['name']} ({s['best_cohort']['final_retention']:.1f}%)")
if s["worst_cohort"]:
lines.append(f"Worst Cohort: {s['worst_cohort']['name']} ({s['worst_cohort']['final_retention']:.1f}%)")
lines.append("")
# Retention table
if analysis["cohorts"]:
max_periods = max(len(c["retention_rates"]) for c in analysis["cohorts"])
period_headers = [f"P{i}" for i in range(min(max_periods, 8))]
lines.append("--- RETENTION TABLE (%) ---")
header = f"{'Cohort':<12} {'Users':>6} " + " ".join(f"{h:>6}" for h in period_headers)
lines.append(header)
lines.append("-" * len(header))
for cohort in analysis["cohorts"]:
rates = [f"{r['retention_rate']:>5.1f}%" for r in cohort["retention_rates"][:8]]
line = f"{cohort['cohort']:<12} {cohort['initial_users']:>6} " + " ".join(rates)
lines.append(line)
lines.append("")
# Trends
if analysis["trends"]:
lines.append("--- PERIOD TRENDS ---")
for period, trend in sorted(analysis["trends"].items()):
if period < 8:
improving = "improving" if trend.get("improving") else "declining" if trend.get("improving") is False else "stable"
lines.append(
f" Period {period}: avg {trend['avg_retention']:.1f}% "
f"(range: {trend['min_retention']:.1f}%-{trend['max_retention']:.1f}%) [{improving}]"
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Analyze user retention by cohort")
parser.add_argument("input", nargs="?", help="JSON file with cohort data")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output JSON")
parser.add_argument("--demo", action="store_true", help="Run with demo data")
args = parser.parse_args()
if args.demo:
data = get_demo_data()
elif args.input:
try:
with open(args.input, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input}", file=sys.stderr)
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
analysis = analyze_cohorts(data)
if args.json_output:
print(json.dumps(analysis, indent=2))
else:
print(format_report(analysis))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Marketing Forecast Generator - Generate marketing performance forecasts.
Uses historical data to project leads, revenue, and pipeline using
linear regression, moving averages, and growth rate extrapolation.
Usage:
python marketing_forecast_generator.py historical.json --periods 6
python marketing_forecast_generator.py historical.json --periods 6 --json
python marketing_forecast_generator.py --demo
"""
import argparse
import json
import sys
import math
def linear_regression(x_vals, y_vals):
"""Simple linear regression returning slope and intercept."""
n = len(x_vals)
if n < 2:
return 0, y_vals[0] if y_vals else 0
sum_x = sum(x_vals)
sum_y = sum(y_vals)
sum_xy = sum(x * y for x, y in zip(x_vals, y_vals))
sum_x2 = sum(x * x for x in x_vals)
denom = n * sum_x2 - sum_x * sum_x
if denom == 0:
return 0, sum_y / n
slope = (n * sum_xy - sum_x * sum_y) / denom
intercept = (sum_y - slope * sum_x) / n
return slope, intercept
def moving_average(values, window=3):
"""Calculate moving average forecast."""
if len(values) < window:
return sum(values) / max(len(values), 1)
return sum(values[-window:]) / window
def growth_rate_forecast(values, periods):
"""Forecast using compound growth rate."""
if len(values) < 2:
return [values[-1] if values else 0] * periods
# Calculate average period-over-period growth
growth_rates = []
for i in range(1, len(values)):
if values[i - 1] > 0:
rate = (values[i] - values[i - 1]) / values[i - 1]
growth_rates.append(rate)
if not growth_rates:
return [values[-1]] * periods
avg_growth = sum(growth_rates) / len(growth_rates)
# Dampen growth rate for longer forecasts
forecasts = []
last_val = values[-1]
for i in range(periods):
dampened_growth = avg_growth * (0.95 ** i) # 5% dampening per period
next_val = last_val * (1 + dampened_growth)
forecasts.append(round(next_val, 2))
last_val = next_val
return forecasts
def generate_forecast(historical_data, forecast_periods=6):
"""Generate multi-method forecast from historical data."""
metrics = {}
# Handle different input formats
if isinstance(historical_data, list):
# List of period objects
for period in historical_data:
for key, value in period.items():
if key in ("period", "month", "date", "label"):
continue
if isinstance(value, (int, float)):
if key not in metrics:
metrics[key] = []
metrics[key].append(value)
elif isinstance(historical_data, dict):
for key, values in historical_data.items():
if isinstance(values, list) and all(isinstance(v, (int, float)) for v in values):
metrics[key] = values
results = {}
for metric_name, values in metrics.items():
x_vals = list(range(len(values)))
n = len(values)
# Method 1: Linear regression
slope, intercept = linear_regression(x_vals, values)
linear_forecast = [round(slope * (n + i) + intercept, 2) for i in range(forecast_periods)]
# Method 2: Moving average
ma_value = moving_average(values)
ma_forecast = [round(ma_value, 2)] * forecast_periods
# Method 3: Growth rate
growth_forecast_vals = growth_rate_forecast(values, forecast_periods)
# Ensemble: weighted average of methods
ensemble = []
for i in range(forecast_periods):
avg = (linear_forecast[i] * 0.4 + growth_forecast_vals[i] * 0.4 + ma_forecast[i] * 0.2)
ensemble.append(round(max(0, avg), 2))
# Confidence intervals (simple approach: +/- based on historical variance)
if len(values) > 2:
mean = sum(values) / len(values)
variance = sum((v - mean) ** 2 for v in values) / len(values)
std_dev = math.sqrt(variance)
ci_low = [round(max(0, e - 1.96 * std_dev * (1 + i * 0.1)), 2) for i, e in enumerate(ensemble)]
ci_high = [round(e + 1.96 * std_dev * (1 + i * 0.1), 2) for i, e in enumerate(ensemble)]
else:
ci_low = [round(e * 0.8, 2) for e in ensemble]
ci_high = [round(e * 1.2, 2) for e in ensemble]
# Historical stats
avg_val = sum(values) / max(len(values), 1)
total_growth = ((values[-1] - values[0]) / max(values[0], 1) * 100) if len(values) > 1 else 0
results[metric_name] = {
"historical": values,
"historical_stats": {
"count": len(values),
"mean": round(avg_val, 2),
"min": min(values),
"max": max(values),
"total_growth_pct": round(total_growth, 1),
"avg_period_growth": round(total_growth / max(len(values) - 1, 1), 1),
},
"forecast": {
"ensemble": ensemble,
"linear": linear_forecast,
"growth_rate": growth_forecast_vals,
"moving_average": ma_forecast,
"confidence_low": ci_low,
"confidence_high": ci_high,
},
"forecast_periods": forecast_periods,
}
return results
def get_demo_data():
return [
{"month": "Sep", "leads": 850, "revenue": 125000, "pipeline": 380000},
{"month": "Oct", "leads": 920, "revenue": 138000, "pipeline": 415000},
{"month": "Nov", "leads": 1050, "revenue": 152000, "pipeline": 460000},
{"month": "Dec", "leads": 980, "revenue": 145000, "pipeline": 435000},
{"month": "Jan", "leads": 1120, "revenue": 168000, "pipeline": 505000},
{"month": "Feb", "leads": 1250, "revenue": 185000, "pipeline": 555000},
]
def format_report(results):
"""Format human-readable forecast."""
lines = []
lines.append("=" * 70)
lines.append("MARKETING FORECAST REPORT")
lines.append("=" * 70)
for metric_name, data in results.items():
label = metric_name.replace("_", " ").title()
lines.append(f"\n--- {label} ---")
stats = data["historical_stats"]
lines.append(f" Historical: {stats['count']} periods, mean={stats['mean']:,.0f}, growth={stats['total_growth_pct']:+.1f}%")
lines.append(f" {'Period':>8} {'Forecast':>12} {'Low':>12} {'High':>12}")
lines.append(" " + "-" * 50)
for i in range(data["forecast_periods"]):
f_val = data["forecast"]["ensemble"][i]
low = data["forecast"]["confidence_low"][i]
high = data["forecast"]["confidence_high"][i]
lines.append(f" {f'P+{i+1}':>8} {f_val:>12,.0f} {low:>12,.0f} {high:>12,.0f}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Generate marketing performance forecasts")
parser.add_argument("input", nargs="?", help="JSON file with historical data")
parser.add_argument("--periods", type=int, default=6, help="Forecast periods")
parser.add_argument("--json", action="store_true", dest="json_output", help="Output JSON")
parser.add_argument("--demo", action="store_true", help="Run with demo data")
args = parser.parse_args()
if args.demo:
data = get_demo_data()
elif args.input:
try:
with open(args.input, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input}", file=sys.stderr)
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
results = generate_forecast(data, args.periods)
if args.json_output:
print(json.dumps(results, indent=2))
else:
print(format_report(results))
if __name__ == "__main__":
main()
Related skills
How it compares
Choose marketing-analyst when you have performance data to interpret; use headline or content skills when the task is writing copy without analytics inputs.
FAQ
What data does marketing-analyst need?
marketing-analyst works best with campaign, funnel, and channel performance inputs such as ad spend by channel, conversion rates, and cohort or funnel step metrics for live SaaS, content, or ecommerce products.
When should developers use marketing-analyst?
marketing-analyst fits after a product has live traffic and measurable conversions, when a developer needs spend, messaging, and experiment recommendations synthesized from real performance data rather than generic marketing advice.