saas-churn-analysis

GitHub 作者 PrecisionLedger v1.0.0

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).

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install github:LeoYeAI~openclaw-master-skills~saas-churn-analysis
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/github%3ALeoYeAI~openclaw-master-skills~saas-churn-analysis/file -o saas-churn-analysis.md
# 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):**
| NRR | Signal |
|-----|--------|
| >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:

```python
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:

```python
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:**
```python
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:

```python
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_scor