SaaS Churn Analysis

Other

SaaS churn and retention analysis: cohort-based churn rates, retention curves, revenue churn vs logo churn, at-risk customer identification, expansion vs contraction MRR, churn recovery playbooks, and net revenue retention (NRR) benchmarking. Produces investor-ready retention charts and actionable recovery plans. Use when: analyzing why customers are churning, building cohort retention tables, calculating NRR/GRR, identifying at-risk accounts before they cancel, or presenting retention data to investors/board. NOT for: executing churn recovery outreach (use CRM/email tools), real-time subscription billing changes (use billing platform APIs), general SaaS KPI dashboards (use saas-metrics-dashboard), or revenue forecasting without churn context (use startup-financial-model).

Install

openclaw skills install saas-churn-analysis

SaaS Churn Analysis Skill

Deep-dive churn and retention analysis for SaaS businesses. Build cohort tables, calculate NRR/GRR, identify at-risk accounts, and produce investor-ready retention metrics with actionable recovery playbooks.


When to Use This Skill

Trigger phrases:

  • "Why are customers churning?"
  • "What's our retention rate?"
  • "Build a cohort analysis"
  • "Show me net revenue retention"
  • "Which accounts are at risk of canceling?"
  • "Investor wants to see our logo churn"
  • "What's our gross/net dollar retention?"
  • "Analyze our expansion vs contraction MRR"

NOT for:

  • Executing recovery outreach (emails, calls) — use CRM/email tools
  • Billing changes, refunds, or cancellation processing — use billing platform
  • General MRR tracking — use saas-metrics-dashboard or subscription-revenue-tracker
  • Revenue forecasting — use startup-financial-model
  • Customer success management — use a CS platform skill

Core Churn Definitions

Logo Churn (Customer Churn)

Logo Churn Rate (monthly) = Customers Lost / Customers at Start of Period

Example:
  Start of month: 200 customers
  Canceled: 5
  Logo churn rate: 5/200 = 2.5%

Revenue Churn

Gross Revenue Churn Rate = MRR Lost to Cancellations / MRR at Start of Period

Example:
  Start MRR: $100,000
  Churned MRR: $4,000 (from cancellations)
  Gross churn: 4%

Net Revenue Retention (NRR / NDR)

NRR = (Beginning MRR + Expansion MRR - Contraction MRR - Churned MRR) / Beginning MRR × 100

Components:
  + Expansion MRR: upsells, upgrades, seat additions from existing customers
  - Contraction MRR: downgrades, reduced seats
  - Churned MRR: cancellations

Example:
  Beginning MRR: $100,000
  Expansion: +$8,000
  Contraction: -$2,000
  Churn: -$4,000
  NRR = ($100,000 + $8,000 - $2,000 - $4,000) / $100,000 = 102%

NRR Benchmarks (SaaS industry):

NRRSignal
>120%Elite (enterprise, product-led)
110–120%Strong — expansion > churn
100–110%Healthy
90–100%Adequate — watch churn trends
<90%Red flag — structural problem

Gross Revenue Retention (GRR)

GRR = (Beginning MRR - Contraction MRR - Churned MRR) / Beginning MRR × 100
     (excludes expansion — pure retention, no upsell credit)

Healthy GRR benchmarks:
  Enterprise SaaS: >90%
  Mid-market: >85%
  SMB SaaS: >75%

Cohort Analysis

Building a Cohort Retention Table

Track customers by their acquisition month and measure % remaining in each subsequent month:

import pandas as pd
from datetime import datetime

def build_cohort_table(subscriptions_df: pd.DataFrame) -> pd.DataFrame:
    """
    Build a cohort retention table from subscription data.
    
    Input DataFrame columns:
        - customer_id: str
        - signup_date: datetime
        - cancel_date: datetime | None (None = still active)
    
    Returns:
        Pivot table: rows = cohort month, columns = months_since_signup,
        values = retention percentage
    """
    df = subscriptions_df.copy()
    df['cohort_month'] = df['signup_date'].dt.to_period('M')
    df['active_through'] = df['cancel_date'].fillna(pd.Timestamp.now())
    
    rows = []
    for cohort, group in df.groupby('cohort_month'):
        cohort_size = len(group)
        for month_offset in range(0, 25):  # 0–24 months
            cutoff = cohort.to_timestamp() + pd.DateOffset(months=month_offset)
            active = group[group['active_through'] >= cutoff].shape[0]
            retention = active / cohort_size * 100
            rows.append({
                'cohort': str(cohort),
                'month': month_offset,
                'cohort_size': cohort_size,
                'active': active,
                'retention_pct': round(retention, 1)
            })
    
    result = pd.DataFrame(rows)
    pivot = result.pivot(index='cohort', columns='month', values='retention_pct')
    return pivot

Example cohort table output:

Cohort     | M0    | M1    | M3    | M6    | M12
-----------|-------|-------|-------|-------|------
2025-01    | 100%  | 91%   | 81%   | 72%   | 58%
2025-02    | 100%  | 93%   | 84%   | 76%   | —
2025-03    | 100%  | 89%   | 79%   | —     | —
2025-04    | 100%  | 94%   | —     | —     | —

Revenue Cohort (Dollar Retention)

Track MRR retained and expanded per cohort:

def revenue_cohort_table(mrr_events_df: pd.DataFrame) -> pd.DataFrame:
    """
    Revenue cohort analysis tracking MRR per acquisition cohort.
    
    Input DataFrame columns:
        - customer_id: str
        - event_date: datetime
        - event_type: str  # 'signup', 'expansion', 'contraction', 'churn'
        - mrr_change: float
    
    Returns:
        Cohort revenue retention table (% of original MRR retained+expanded)
    """
    # Group by signup cohort
    signups = mrr_events_df[mrr_events_df['event_type'] == 'signup'].copy()
    signups['cohort_month'] = signups['event_date'].dt.to_period('M')
    
    # For each cohort, track MRR over time
    # NRR by cohort = sum(all MRR changes for cohort customers) / initial MRR
    pass

Churn Curve Analysis

Identify when in the customer lifecycle churn peaks:

Early churn (M1-M3): Onboarding failure, value not delivered
  → Diagnosis: activation rate, time-to-first-value, support tickets
  
Mid-term churn (M4-M12): Competitive displacement, budget cuts
  → Diagnosis: NPS trends, feature adoption, renewal engagement
  
Late churn (M12+): Strategic shifts, contract terms, enterprise competition
  → Diagnosis: executive sponsor changes, usage trends, renewal conversations

Churn by tenure bucket:

def churn_by_tenure(subscriptions_df: pd.DataFrame) -> dict:
    """Calculate churn rate for different tenure buckets."""
    buckets = {
        '0-3mo': (0, 90),
        '3-6mo': (90, 180),
        '6-12mo': (180, 365),
        '12-24mo': (365, 730),
        '24mo+': (730, float('inf'))
    }
    
    results = {}
    for bucket_name, (min_days, max_days) in buckets.items():
        mask = (
            (subscriptions_df['tenure_days'] >= min_days) &
            (subscriptions_df['tenure_days'] < max_days)
        )
        bucket_df = subscriptions_df[mask]
        if len(bucket_df) == 0:
            continue
        churned = bucket_df[bucket_df['cancel_date'].notna()].shape[0]
        results[bucket_name] = {
            'total_customers': len(bucket_df),
            'churned': churned,
            'churn_rate_pct': round(churned / len(bucket_df) * 100, 1)
        }
    return results

At-Risk Customer Identification

Churn Risk Scoring

Score each active customer by leading indicators:

CHURN_RISK_WEIGHTS = {
    'days_since_last_login': 0.25,       # Usage drop
    'feature_adoption_pct': -0.20,       # Inverse: more features = lower risk
    'support_tickets_30d': 0.15,         # Escalations
    'nps_score': -0.15,                  # Inverse: high NPS = lower risk
    'days_to_renewal': -0.10,            # Closer renewal = higher urgency
    'billing_failures_90d': 0.15,        # Payment issues
}

def churn_risk_score(customer: dict) -> float:
    """
    Calculate 0-100 churn risk score for a customer.
    Higher = more likely to churn.
    
    Inputs:
        customer: dict with keys matching CHURN_RISK_WEIGHTS
    
    Returns:
        Risk score 0-100 (>70 = high risk, 40-70 = medium, <40 = low)
    """
    raw_score = 0
    for factor, weight in CHURN_RISK_WEIGHTS.items():
        if factor in customer:
            # Normalize each factor to 0-100 scale first
            normalized = normalize_factor(factor, customer[factor])
            raw_score += normalized * weight
    
    # Scale to 0-100
    return max(0, min(100, raw_score * 100 + 50))

def get_at_risk_accounts(customers: list, threshold: float = 70.0) -> list:
    """Return customers with churn risk score above threshold, sorted by MRR."""
    at_risk = [
        {**c, 'risk_score': churn_risk_score(c)}
        for c in customers
    ]
    return sorted(
        [c for c in at_risk if c['risk_score'] >= threshold],
        key=lambda x: x.get('mrr', 0),
        reverse=True  # Highest MRR first — prioritize by revenue impact
    )

Early Warning Signals

Usage-based signals (product telemetry):

🔴 High risk:
  - No login in 14+ days (was weekly user)
  - DAU/MAU ratio dropped >50% MoM
  - Core feature not used in 30 days
  - Below 20% feature adoption vs peers

🟡 Medium risk:
  - Login frequency dropped >30% MoM
  - Support ticket with "cancel" or "refund" keyword
  - NPS score ≤ 6 (detractor)
  - Seat count reduced

🟢 Healthy signals:
  - Expanded seats or upgraded tier
  - Used 3+ core features this month
  - NPS ≥ 9 (promoter)
  - Referred another customer

Financial signals:

🔴 High risk:
  - Payment failure (retry in progress)
  - Requested invoice-based payment shift (budget freeze)
  - Contract not opened with 30 days to renewal

🟡 Medium risk:
  - Asked about pricing alternatives
  - Billing contact changed
  - Discount request submitted

MRR Movement Analysis

MRR Bridge

Decompose monthly MRR change into components:

MRR Bridge: January → February

Beginning MRR:     $100,000
+ New Business:      +$8,500   (23 new customers × $370 avg)
+ Expansion:         +$3,200   (upgrades + seat additions)
- Contraction:       -$1,100   (downgrades + seat reductions)
- Churn:             -$4,300   (11 cancellations × $390 avg)
= Ending MRR:      $106,300

Net New MRR:        +$6,300
MoM Growth:          6.3%

Python MRR bridge calculation:

from dataclasses import dataclass

@dataclass
class MRRBridge:
    period: str
    beginning_mrr: float
    new_mrr: float           # New customers
    expansion_mrr: float     # Upsells/upgrades
    contraction_mrr: float   # Downgrades (negative or positive — store as positive)
    churned_mrr: float       # Cancellations (store as positive)
    
    @property
    def ending_mrr(self) -> float:
        return self.beginning_mrr + self.new_mrr + self.expansion_mrr - self.contraction_mrr - self.churned_mrr
    
    @property
    def net_new_mrr(self) -> float:
        return self.ending_mrr - self.beginning_mrr
    
    @property
    def growth_rate_pct(self) -> float:
        return self.net_new_mrr / self.beginning_mrr * 100 if self.beginning_mrr else 0
    
    @property
    def quick_ratio(self) -> float:
        """SaaS Quick Ratio = (New + Expansion) / (Contraction + Churn). >4 = healthy."""
        numerator = self.new_mrr + self.expansion_mrr
        denominator = self.contraction_mrr + self.churned_mrr
        return numerator / denominator if denominator else float('inf')
    
    def to_summary(self) -> str:
        return (
            f"MRR Bridge ({self.period})\n"
            f"  Beginning: ${self.beginning_mrr:,.0f}\n"
            f"  + New:       ${self.new_mrr:,.0f}\n"
            f"  + Expansion: ${self.expansion_mrr:,.0f}\n"
            f"  - Contraction: ${self.contraction_mrr:,.0f}\n"
            f"  - Churn:     ${self.churned_mrr:,.0f}\n"
            f"  = Ending:  ${self.ending_mrr:,.0f}\n"
            f"  Growth: {self.growth_rate_pct:.1f}% | Quick Ratio: {self.quick_ratio:.1f}x"
        )

SaaS Quick Ratio benchmarks:

Quick RatioSignal
>4Elite growth efficiency
2–4Healthy
1–2Growing but inefficient — churn drag
<1Shrinking — churn exceeds new + expansion

Churn Recovery Playbooks

Playbook 1: Early Churn (Month 1-3)

Root cause: Failed onboarding, didn't reach first value moment

Diagnosis questions:

□ Did they complete onboarding? (activation rate)
□ Did they use the core feature at least once? (activation event)
□ How long did it take to reach first value moment?
□ Did they get a human touchpoint in first 48 hours?

Recovery actions:

Day 1-7:   Personal outreach from CSM — "What would make this a 10/10?"
Day 7-14:  Offer 1:1 onboarding session + extend trial if applicable
Day 14-21: Share customer success story in their industry/use case
Day 21-30: Executive touchpoint if MRR > $500/mo

Playbook 2: Mid-Term Churn (Month 4-12)

Root cause: Value plateau, competitive evaluation, budget pressure

Diagnosis questions:

□ Usage trend: up, flat, or declining in last 60 days?
□ When did they last use the feature most tied to their stated goal?
□ Any support escalations or complaints in the last 90 days?
□ Have they been pitched by a competitor? (ask directly)
□ Is this a budget-driven decision or product-driven?

Recovery actions by root cause:

Budget:
  → Offer pause plan (90-day pause vs cancel)
  → Right-size to smaller plan vs lose them entirely
  → Annual prepay at 20% discount to lock in

Product gaps:
  → Roadmap call with PM — "here's what's coming"
  → Workaround documentation for their specific use case
  → Connect to power-user customer for peer validation

Competitor evaluation:
  → Direct competitive comparison matrix
  → Migration cost analysis (switching is expensive)
  → Win-back offer if they've already left (45-day re-engagement)

Playbook 3: Renewal-at-Risk (30-60 days to renewal)

Proactive renewal pipeline:

60 days out:
  □ Usage review: send personalized "Your results with [Product]" email
  □ Identify any open issues — resolve before renewal conversation

45 days out:
  □ QBR or check-in call — confirm value, surface upsell opportunity
  □ Flag to AE if NPS < 7 or usage declining

30 days out:
  □ Renewal proposal sent — include current plan + upsell option
  □ Executive sponsor confirmation (for accounts >$1k/mo)

14 days out:
  □ Follow-up if no response — switch to phone
  □ Escalate to manager if no reply

7 days out:
  □ Final decision call — accept reduced terms if needed to retain

Output Formats

Investor-Ready Retention Summary

{
  "period": "Q4 2025",
  "generated_at": "2026-01-15",
  "retention_metrics": {
    "logo_churn_rate_monthly": 2.1,
    "mrr_gross_churn_rate_monthly": 3.8,
    "net_revenue_retention_pct": 108,
    "gross_revenue_retention_pct": 96.2,
    "quick_ratio": 3.2
  },
  "mrr_bridge": {
    "beginning_mrr": 285000,
    "new_mrr": 42000,
    "expansion_mrr": 18500,
    "contraction_mrr": 4200,
    "churned_mrr": 10800,
    "ending_mrr": 330500
  },
  "at_risk_pipeline": {
    "high_risk_count": 8,
    "high_risk_mrr_at_risk": 24600,
    "medium_risk_count": 15,
    "medium_risk_mrr_at_risk": 38200
  },
  "cohort_highlights": {
    "best_cohort": { "month": "2025-03", "m12_retention": 74 },
    "worst_cohort": { "month": "2025-08", "m3_retention": 71 },
    "avg_m12_retention": 68.5
  },
  "benchmarks": {
    "nrr_vs_industry": "above_median",
    "grr_vs_industry": "top_quartile",
    "logo_churn_vs_industry": "median"
  }
}

CSV Export for Spreadsheets

cohort_retention_csv_template:
Cohort,Size,M1,M2,M3,M6,M9,M12,M18,M24
2025-01,45,91%,84%,81%,73%,67%,61%,55%,49%
2025-02,52,93%,87%,83%,—,—,—,—,—
...

Step-by-Step Workflow

Full Churn Audit

Step 1: Data collection

□ Customer list with signup date and cancel date (if churned)
□ MRR per customer per month (last 12 months)
□ Usage data: logins, feature events (from product analytics)
□ NPS scores if available
□ Cancellation reason codes (from offboarding flow)

Step 2: Calculate headline metrics

  • Logo churn rate (monthly and annualized)
  • Gross and net revenue retention
  • Quick ratio
  • Churn by tenure bucket

Step 3: Build cohort table

  • M0–M12 retention by acquisition cohort
  • Identify best and worst cohorts — find what's different

Step 4: MRR bridge (last 6 months)

  • New vs expansion vs contraction vs churn
  • Trend analysis: is churn improving or worsening?

Step 5: At-risk identification

  • Score all active customers by churn risk signals
  • Prioritize by MRR at risk (highest first)
  • Output: top 10 at-risk accounts with reasons

Step 6: Root cause analysis

  • What's driving churn? (onboarding failure, competition, budget, product gaps)
  • Which segments have highest churn? (plan size, industry, use case, acquisition channel)

Step 7: Recommend playbook

  • Match root cause to recovery playbook
  • Estimate MRR at stake if intervention succeeds (recovery potential)
  • Prioritize actions by expected ROI

Churn by Segment Analysis

Segment churn to find structural patterns:

def churn_by_segment(subscriptions_df: pd.DataFrame, segment_col: str) -> pd.DataFrame:
    """
    Calculate churn rate by customer segment.
    
    Args:
        segment_col: column name to segment by (e.g., 'plan', 'industry', 'company_size')
    
    Returns:
        DataFrame with churn rate per segment, sorted by MRR impact
    """
    results = []
    for segment, group in subscriptions_df.groupby(segment_col):
        total = len(group)
        churned = group[group['cancel_date'].notna()].shape[0]
        total_mrr = group['mrr'].sum()
        churned_mrr = group[group['cancel_date'].notna()]['mrr'].sum()
        
        results.append({
            'segment': segment,
            'total_customers': total,
            'churned_customers': churned,
            'logo_churn_pct': round(churned / total * 100, 1),
            'total_mrr': total_mrr,
            'churned_mrr': churned_mrr,
            'mrr_churn_pct': round(churned_mrr / total_mrr * 100, 1) if total_mrr else 0
        })
    
    return pd.DataFrame(results).sort_values('churned_mrr', ascending=False)

Key segments to analyze:

  • By plan tier (free trial → paid → enterprise)
  • By acquisition channel (organic, paid, referral)
  • By company size (SMB, mid-market, enterprise)
  • By industry vertical
  • By geographic region
  • By sales rep / CSM (is one rep's book churning faster?)

Integration Points

  • saas-metrics-dashboard — Display NRR, GRR, and churn rate KPIs in dashboard
  • kpi-alert-system — Trigger alerts when monthly churn exceeds threshold
  • startup-financial-model — Feed churn rate assumptions into revenue forecasts
  • subscription-revenue-tracker — MRR bridge data source for churn calculations
  • crypto-tax-agent — N/A (different domain)

Key Formulas Cheat Sheet

Logo Churn Rate (monthly)  = Customers Lost / Customers at Start × 100
Annual Logo Churn          = 1 - (1 - monthly_churn)^12 × 100
Gross Revenue Retention    = (BOM MRR - Contraction - Churn) / BOM MRR × 100
Net Revenue Retention      = (BOM MRR + Expansion - Contraction - Churn) / BOM MRR × 100
Quick Ratio                = (New MRR + Expansion MRR) / (Contraction MRR + Churned MRR)
LTV (with churn)           = ARPU / Monthly Churn Rate
Avg Customer Lifetime      = 1 / Monthly Churn Rate (in months)

Rule of Thumb:
  2% monthly logo churn  = ~21% annual churn (B2B SMB benchmark)
  0.5% monthly logo churn = ~6% annual churn (enterprise benchmark)
  NRR >100% means you grow from existing base alone — key investor signal