#!/usr/bin/env python3 """ funnel_drop_analyzer.py — Signup Funnel Drop-Off Analyzer 100% stdlib, no pip installs required. Usage: python3 funnel_drop_analyzer.py # demo mode python3 funnel_drop_analyzer.py --steps steps.json python3 funnel_drop_analyzer.py --steps steps.json --json echo '[{"step":"Visit","count":10000}]' | python3 funnel_drop_analyzer.py --stdin steps.json format: [ {"step": "Landing Page Visit", "count": 10000}, {"step": "Clicked Sign Up", "count": 4200}, {"step": "Filled Form", "count": 2800}, {"step": "Email Verified", "count": 1900}, {"step": "Onboarding Done", "count": 1100} ] """ import argparse import json import math import sys # --------------------------------------------------------------------------- # Recommendation engine # --------------------------------------------------------------------------- RECOMMENDATIONS = { "high_drop": { "threshold": 0.50, # >50% drop "landing_page": [ "Value proposition may be unclear — run a 5-second test.", "Add social proof (testimonials, logos, user count) above the fold.", "Ensure CTA button is prominent and benefit-focused ('Start Free' not 'Submit').", ], "clicked_sign_up": [ "CTA label or placement may not resonate — A/B test button copy and colour.", "Users may not trust the product — add trust badges and reviews near CTA.", "Consider a sticky header CTA for long landing pages.", ], "filled_form": [ "Form has too many fields — reduce to email + password minimum.", "Try progressive disclosure: collect extra info post-signup.", "Add inline validation so errors appear in real-time, not on submit.", "Show a progress indicator if multi-step.", ], "email_verified": [ "Verification email may land in spam — check SPF/DKIM/DMARC.", "Send a plain-text follow-up 30 min after signup nudging verification.", "Consider SMS or magic-link alternatives to email verification.", "Reduce time-to-value: show a useful screen before requiring verification.", ], "default": [ "Significant drop detected — instrument with session recordings (Hotjar/FullStory).", "Run exit surveys at this step to capture qualitative reasons.", "Check for UI bugs or broken flows on mobile.", ], }, "medium_drop": { "threshold": 0.25, # 25–50% drop "default": [ "Moderate friction — review copy and UX at this step.", "Ensure mobile experience is frictionless (test on real devices).", "Add micro-copy explaining why information is requested.", ], }, "healthy": { "default": [ "Step conversion is healthy — focus optimisation effort elsewhere.", ], }, } def classify_step_name(name: str) -> str: """Map step name to a known category for targeted recommendations.""" n = name.lower() if any(k in n for k in ["land", "visit", "page", "home"]): return "landing_page" if any(k in n for k in ["cta", "click", "signup", "sign up", "register", "start"]): return "clicked_sign_up" if any(k in n for k in ["form", "fill", "detail", "info", "enter"]): return "filled_form" if any(k in n for k in ["email", "verif", "confirm", "activate"]): return "email_verified" return "default" def get_recommendation(step_name: str, drop_rate: float) -> list: if drop_rate > RECOMMENDATIONS["high_drop"]["threshold"]: bucket = RECOMMENDATIONS["high_drop"] cat = classify_step_name(step_name) return bucket.get(cat, bucket["default"]) elif drop_rate > RECOMMENDATIONS["medium_drop"]["threshold"]: return RECOMMENDATIONS["medium_drop"]["default"] else: return RECOMMENDATIONS["healthy"]["default"] # --------------------------------------------------------------------------- # Core analysis # --------------------------------------------------------------------------- def analyze_funnel(steps: list) -> dict: """ Analyse a funnel step list and return full metrics + recommendations. Each step: {"step": , "count": } """ if not steps: raise ValueError("steps list is empty") if len(steps) < 2: raise ValueError("Need at least 2 steps to analyse a funnel") top_count = steps[0]["count"] if top_count <= 0: raise ValueError("Top-of-funnel count must be > 0") step_metrics = [] worst_step = None worst_drop_rate = -1.0 for i, s in enumerate(steps): name = s["step"] count = s["count"] cumulative_rate = count / top_count if i == 0: step_to_step_rate = 1.0 drop_count = 0 drop_rate = 0.0 recommendations = ["Top of funnel — all visitors enter here."] else: prev_count = steps[i - 1]["count"] step_to_step_rate = count / prev_count if prev_count > 0 else 0.0 drop_count = prev_count - count drop_rate = 1 - step_to_step_rate recommendations = get_recommendation(name, drop_rate) if drop_rate > worst_drop_rate: worst_drop_rate = drop_rate worst_step = name step_metrics.append({ "step": name, "count": count, "step_conversion_pct": round(step_to_step_rate * 100, 2), "step_drop_pct": round(drop_rate * 100, 2), "drop_count": drop_count, "cumulative_conversion_pct": round(cumulative_rate * 100, 2), "recommendations": recommendations, }) # Overall funnel health score (0-100) overall_conv = steps[-1]["count"] / top_count score = _funnel_score(step_metrics, overall_conv) return { "summary": { "total_steps": len(steps), "top_of_funnel_count": top_count, "bottom_of_funnel_count": steps[-1]["count"], "overall_conversion_pct": round(overall_conv * 100, 2), "worst_performing_step": worst_step, "worst_step_drop_pct": round(worst_drop_rate * 100, 2), "funnel_health_score": score, "funnel_health_label": _score_label(score), }, "steps": step_metrics, "top_priority": _top_priority(step_metrics), } def _funnel_score(step_metrics: list, overall_conv: float) -> int: """ Score = 100 * overall_conversion adjusted for worst-step severity. - Base: log-scale overall conversion (capped at a 10% target = 100 pts) - Penalty: each step with >60% drop deducts points """ target_conv = 0.10 # 10% overall = score 100 base = min(100, math.log1p(overall_conv) / math.log1p(target_conv) * 100) penalty = 0 for m in step_metrics[1:]: if m["step_drop_pct"] > 60: penalty += 10 elif m["step_drop_pct"] > 40: penalty += 5 score = max(0, round(base - penalty)) return score def _score_label(s: int) -> str: if s >= 80: return "Excellent" if s >= 60: return "Good" if s >= 40: return "Fair" if s >= 20: return "Poor" return "Critical" def _top_priority(step_metrics: list) -> dict: """Return the single highest-impact step to fix first.""" # Pick step with largest absolute drop count (not just rate) candidates = step_metrics[1:] if not candidates: return {} top = max(candidates, key=lambda m: m["drop_count"]) return { "step": top["step"], "drop_count": top["drop_count"], "drop_pct": top["step_drop_pct"], "why": "Largest absolute visitor loss — highest revenue impact.", "quick_wins": top["recommendations"], } # --------------------------------------------------------------------------- # Pretty-print # --------------------------------------------------------------------------- def pretty_print(result: dict) -> None: s = result["summary"] tp = result["top_priority"] print("\n" + "=" * 65) print(" SIGNUP FUNNEL DROP-OFF ANALYZER") print("=" * 65) print(f"\n📊 FUNNEL OVERVIEW") print(f" Top of funnel : {s['top_of_funnel_count']:,} visitors") print(f" Bottom of funnel : {s['bottom_of_funnel_count']:,} converted") print(f" Overall conversion : {s['overall_conversion_pct']}%") print(f" Funnel health : {s['funnel_health_score']}/100 ({s['funnel_health_label']})") print(f" Worst step : {s['worst_performing_step']} " f"({s['worst_step_drop_pct']}% drop)") print(f"\n{'Step':<28} {'Count':>8} {'Step Conv':>10} {'Step Drop':>10} {'Cumul Conv':>10}") print("─" * 75) for m in result["steps"]: bar = "█" * int(m["cumulative_conversion_pct"] / 5) print(f" {m['step']:<26} {m['count']:>8,} " f"{m['step_conversion_pct']:>9.1f}% " f"{m['step_drop_pct']:>9.1f}% " f"{m['cumulative_conversion_pct']:>9.1f}% {bar}") print(f"\n🚨 TOP PRIORITY FIX: {tp.get('step', 'N/A')}") print(f" Lost visitors : {tp.get('drop_count', 0):,} ({tp.get('drop_pct', 0)}% drop)") print(f" Why fix first : {tp.get('why', '')}") print(" Quick wins:") for qw in tp.get("quick_wins", []): print(f" • {qw}") print(f"\n💡 STEP-BY-STEP RECOMMENDATIONS") for m in result["steps"][1:]: if m["step_drop_pct"] > 10: print(f"\n [{m['step']}] ↓{m['step_drop_pct']}% drop") for r in m["recommendations"]: print(f" • {r}") print() # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- DEMO_STEPS = [ {"step": "Landing Page Visit", "count": 12000}, {"step": "Clicked Sign Up CTA", "count": 4560}, {"step": "Filled Registration", "count": 2800}, {"step": "Email Verified", "count": 1540}, {"step": "Onboarding Completed", "count": 880}, {"step": "First Core Action", "count": 420}, ] def parse_args(): parser = argparse.ArgumentParser( description="Analyse signup funnel drop-off by step (stdlib only).", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument("--steps", type=str, default=None, help="Path to JSON file with funnel steps") parser.add_argument("--stdin", action="store_true", help="Read steps JSON from stdin") parser.add_argument("--json", action="store_true", help="Output results as JSON") return parser.parse_args() def main(): args = parse_args() steps = None if args.stdin: steps = json.load(sys.stdin) elif args.steps: with open(args.steps) as f: steps = json.load(f) else: print("🔬 DEMO MODE — using sample SaaS signup funnel\n") steps = DEMO_STEPS result = analyze_funnel(steps) if args.json: print(json.dumps(result, indent=2)) else: pretty_print(result) if __name__ == "__main__": main()