Back to writing

Building Predictive Churn Models That Actually Work

7 min read

The Retention Problem

Most teams handle churn reactively:

  1. User churns
  2. Analyze why (if you're lucky)
  3. Fix issues for future users
  4. Repeat

By the time you notice, it's too late. The user already left.

AI flips this: predict churn before it happens, then prevent it.

Here's how to build a production churn prediction system.

What is Churn?

Define it clearly. Common definitions:

SaaS:

Consumer apps:

Pick one definition and stick with it. Your model predicts this specific outcome.

The Churn Prediction Pipeline

1. Feature Engineering

Transform user behavior into ML features:

def extract_churn_features(user_id: str, lookback_days=30) -> dict:
    """Extract features predicting churn"""
    events = get_user_events(user_id, days=lookback_days)
    prev_events = get_user_events(user_id, days=lookback_days*2, end=lookback_days)
    
    return {
        # Engagement trends
        'sessions_last_7d': count_sessions(events, days=7),
        'sessions_prev_7d': count_sessions(prev_events, days=7),
        'session_trend': trend(sessions_over_time(events)),
        
        # Feature usage
        'core_feature_usage': count_core_features(events),
        'feature_depth': unique_features(events) / total_features(),
        'last_feature_used': days_since_last_feature(events),
        
        # Behavioral signals
        'error_rate': calculate_error_rate(events),
        'search_frequency': count_searches(events),
        'help_doc_views': count_help_views(events),
        
        # Time patterns
        'days_since_signup': (datetime.now() - get_signup_date(user_id)).days,
        'days_since_last_login': days_since_last(events),
        'avg_session_duration': mean_duration(events),
        
        # Value indicators
        'content_created': count_created(events),
        'invites_sent': count_invites(user_id),
        'plan_type': get_plan(user_id),
    }

2. Training Data

Label historical users as churned or retained:

def create_training_data(lookback_days=30, prediction_window=30):
    """Generate labeled dataset"""
    
    # Users active 60+ days ago
    users = get_users_active_before(days=60)
    
    X = []  # Features
    y = []  # Labels (1=churned, 0=retained)
    
    for user_id in users:
        # Features from 60-30 days ago
        features = extract_churn_features(
            user_id, 
            lookback_days=lookback_days
        )
        
        # Did they churn in the next 30 days?
        churned = did_user_churn(
            user_id,
            window_start=60,
            window_end=30
        )
        
        X.append(features)
        y.append(1 if churned else 0)
    
    return pd.DataFrame(X), pd.Series(y)

3. Model Training

XGBoost works well for churn prediction:

import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, precision_recall_curve

def train_churn_model():
    """Train churn prediction model"""
    
    X, y = create_training_data()
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, stratify=y
    )
    
    # Handle class imbalance
    scale_pos_weight = (y_train == 0).sum() / (y_train == 1).sum()
    
    model = xgb.XGBClassifier(
        max_depth=6,
        learning_rate=0.1,
        n_estimators=100,
        scale_pos_weight=scale_pos_weight,
        eval_metric='auc'
    )
    
    model.fit(
        X_train, y_train,
        eval_set=[(X_test, y_test)],
        early_stopping_rounds=10,
        verbose=False
    )
    
    # Evaluate
    y_pred_proba = model.predict_proba(X_test)[:, 1]
    auc = roc_auc_score(y_test, y_pred_proba)
    
    print(f"Test AUC: {auc:.3f}")
    
    return model

4. Feature Importance

Understand what drives churn:

def analyze_churn_drivers(model, feature_names):
    """Identify top churn indicators"""
    
    importance = model.feature_importances_
    feature_importance = sorted(
        zip(feature_names, importance),
        key=lambda x: x[1],
        reverse=True
    )
    
    print("Top churn predictors:")
    for feature, score in feature_importance[:10]:
        print(f"  {feature}: {score:.3f}")
    
    return feature_importance

Common high-signal features:

Prediction in Production

Daily Scoring

Score all active users:

def score_users_for_churn():
    """Daily batch job to identify at-risk users"""
    
    model = load_model('churn_model.pkl')
    active_users = get_active_users(days=30)
    
    at_risk_users = []
    
    for user_id in active_users:
        features = extract_churn_features(user_id)
        churn_prob = model.predict_proba([features])[0][1]
        
        if churn_prob > 0.7:  # High risk threshold
            at_risk_users.append({
                'user_id': user_id,
                'churn_probability': churn_prob,
                'risk_factors': explain_prediction(model, features)
            })
    
    # Trigger interventions
    for user in at_risk_users:
        create_intervention_task(user)
    
    return at_risk_users

Model Explainability

Show why a user is at risk:

import shap

def explain_prediction(model, features):
    """Explain why user is predicted to churn"""
    
    explainer = shap.TreeExplainer(model)
    shap_values = explainer.shap_values(features)
    
    # Top contributing features
    feature_contributions = sorted(
        zip(features.keys(), shap_values),
        key=lambda x: abs(x[1]),
        reverse=True
    )
    
    return {
        'top_factors': [
            {
                'feature': feat,
                'contribution': contrib,
                'value': features[feat]
            }
            for feat, contrib in feature_contributions[:5]
        ]
    }

Intervention System

Selecting Interventions

Match intervention to churn reason:

def select_intervention(user_id: str, risk_factors: list) -> dict:
    """Choose optimal intervention"""
    
    # Identify primary churn driver
    top_factor = risk_factors[0]['feature']
    
    if 'session_trend' in top_factor:
        # Declining engagement
        return {
            'type': 'reengagement_email',
            'content': generate_personalized_email(user_id, 'reengagement'),
            'timing': 'immediate'
        }
    
    elif 'core_feature_usage' in top_factor:
        # Not using key features
        return {
            'type': 'feature_education',
            'content': recommend_unused_features(user_id),
            'timing': 'next_login'
        }
    
    elif 'error_rate' in top_factor:
        # Technical issues
        return {
            'type': 'support_outreach',
            'content': 'proactive_support',
            'timing': 'immediate',
            'escalate_to_human': True
        }
    
    elif 'help_doc_views' in top_factor:
        # Confusion/stuck
        return {
            'type': 'guided_tutorial',
            'content': contextual_help(user_id),
            'timing': 'next_session'
        }
    
    else:
        # Generic re-engagement
        return {
            'type': 'value_reminder',
            'content': highlight_user_achievements(user_id),
            'timing': 'next_day'
        }

Intervention Types

1. Personalized Emails

def generate_reengagement_email(user_id: str) -> dict:
    """Create personalized re-engagement message"""
    
    user_data = get_user_data(user_id)
    unused_features = get_unused_features(user_id)
    
    return {
        'subject': f"You're missing out on {unused_features[0]['name']}",
        'body': f"""
        Hey {user_data['name']},
        
        I noticed you haven't logged in recently. 
        
        Users like you find {unused_features[0]['name']} incredibly useful 
        for {unused_features[0]['use_case']}.
        
        [Try it now - takes 2 minutes]
        """,
        'cta': unused_features[0]['link']
    }

2. In-App Prompts

def show_retention_prompt(user_id: str):
    """Display prompt on next login"""
    
    achievements = calculate_achievements(user_id)
    
    return {
        'type': 'modal',
        'title': f"You've accomplished {achievements['count']} milestones!",
        'content': f"""
        - {achievements['items'][0]}
        - {achievements['items'][1]}
        - {achievements['items'][2]}
        
        Keep the momentum going!
        """,
        'cta': 'Continue where I left off'
    }

3. Human Touchpoints

def trigger_support_outreach(user_id: str):
    """Flag for manual outreach"""
    
    user = get_user_profile(user_id)
    
    if user['ltv'] > 1000:  # High-value user
        create_task({
            'type': 'manual_outreach',
            'user_id': user_id,
            'priority': 'high',
            'context': f"High LTV user at risk of churn. Recent issues: {get_recent_errors(user_id)}",
            'assigned_to': 'customer_success_team'
        })

Measuring Intervention Impact

A/B Test Interventions

def test_intervention_effectiveness():
    """Compare outcomes with/without intervention"""
    
    at_risk_users = identify_at_risk_users()
    
    # Randomly assign
    treatment = random.sample(at_risk_users, len(at_risk_users) // 2)
    control = [u for u in at_risk_users if u not in treatment]
    
    # Apply interventions to treatment
    for user_id in treatment:
        apply_intervention(user_id)
    
    # Measure retention after 30 days
    treatment_retained = sum(is_retained(u, days=30) for u in treatment)
    control_retained = sum(is_retained(u, days=30) for u in control)
    
    results = {
        'treatment_retention': treatment_retained / len(treatment),
        'control_retention': control_retained / len(control),
        'absolute_lift': (treatment_retained - control_retained) / len(control),
        'relative_lift': ((treatment_retained / len(treatment)) / (control_retained / len(control))) - 1
    }
    
    return results

Real Results

Well-tuned churn prediction + intervention systems achieve:

Advanced Techniques

Time-to-Churn Prediction

Predict when user will churn:

from lifelines import CoxPHFitter

def train_time_to_churn_model():
    """Survival analysis for time-to-churn"""
    
    data = []
    for user_id in get_all_users():
        features = extract_churn_features(user_id)
        
        if user_churned(user_id):
            duration = days_until_churn(user_id)
            event = 1
        else:
            duration = days_since_signup(user_id)
            event = 0
        
        data.append({**features, 'duration': duration, 'churned': event})
    
    df = pd.DataFrame(data)
    
    cph = CoxPHFitter()
    cph.fit(df, duration_col='duration', event_col='churned')
    
    return cph

Cohort-Specific Models

Different user types churn for different reasons:

def train_cohort_models():
    """Separate models by user type"""
    
    models = {}
    
    for cohort in ['enterprise', 'smb', 'individual']:
        users = get_users_by_cohort(cohort)
        X, y = create_training_data(users)
        
        models[cohort] = train_churn_model(X, y)
    
    return models

def predict_churn_cohort_aware(user_id: str):
    """Use cohort-specific model"""
    
    cohort = get_user_cohort(user_id)
    model = load_model(f'churn_model_{cohort}.pkl')
    
    features = extract_churn_features(user_id)
    churn_prob = model.predict_proba([features])[0][1]
    
    return churn_prob

Common Mistakes

1. Training on all users: Include only users who had a chance to churn

2. Data leakage: Features that wouldn't be available at prediction time

3. Ignoring class imbalance: Most users don't churn

4. Not retraining: User behavior changes

Implementation Checklist

  1. Define churn clearly for your product
  2. Collect events (if not already instrumented)
  3. Build feature pipeline with 30 days of historical data
  4. Train initial model on 6+ months of users
  5. Set up daily scoring batch job
  6. Create 3 intervention types (email, in-app, human)
  7. A/B test interventions to measure lift
  8. Monitor and retrain monthly

The Compound Effect

Churn reduction compounds:

Start small, measure everything, iterate.


Resources:

Enjoying this article?

Get deep technical guides like this delivered weekly.

Get AI growth insights weekly

Join engineers and product leaders building with AI. No spam, unsubscribe anytime.

Keep reading