← All posts

Survival curves for SaaS: what your churn rate is hiding

The underlying idea

Every SaaS company reports a churn rate. It’s usually one number: “we lost 4% of customers last month.” Sometimes it’s annualized, sometimes cohort-adjusted, sometimes revenue-weighted. But it’s almost always a single scalar summarizing a fundamentally time-based process.

That summary hides everything that matters.

Two SaaS companies can have identical monthly churn rates and completely different businesses. Company A loses a steady fraction of customers every month, spread evenly across tenure. Company B loses a large share of new customers in the first 90 days, then almost nothing after month 6. Their churn numbers can look identical. Their retention economics are worlds apart.

Survival analysis is the statistical framework designed to see the difference. Instead of asking “what fraction of customers churned this month,” it asks “what fraction of customers are still active after 30, 60, 90, 180 days?” The output isn’t a scalar. It’s a curve. And that curve shows you exactly where in a customer’s lifecycle you’re losing them.

The core object is the survival function, denoted S(t)S(t): the probability that a customer is still active at time tt. It starts at 1 (everyone is active on day zero) and monotonically declines toward 0. The shape of that decline is the story.

A straight-line decline means constant hazard. Customers churn at the same rate throughout their tenure. A steep early drop followed by a flat plateau means you have an onboarding problem, not a product problem. A cliff at month 12 means annual contract renewals are working against you. A slow, gradual decline means product-market fit is holding.

None of these patterns are visible in a single monthly churn number. All of them are obvious in a survival curve.

Historical root

Survival analysis originated in medical research. In 1958, Edward Kaplan and Paul Meier published a paper in the Journal of the American Statistical Association solving a problem that had troubled clinical trial statisticians for decades: how do you estimate survival rates when your data is incomplete?

The problem was censoring. In a clinical trial, some patients die during the study, but others drop out, move away, or are still alive when the study ends. You know they survived at least until they left the study, but not how much longer. Traditional statistical methods either ignored these censored observations, which biased the estimate, or required assumptions about their eventual fate, which was unreliable.

Kaplan and Meier’s insight was to compute survival probabilities piecewise, using only the information available at each time point. At every moment a death occurred, they calculated the conditional probability of surviving past that moment given the number of patients still at risk. Multiplying these conditional probabilities together produced an unbiased estimate of the overall survival curve, even with censored observations.

The estimator is now one of the most-cited papers in the history of statistics. It underlies clinical trial reporting, FDA drug approval analysis, and, increasingly since the early 2010s, subscription business retention analysis.

The translation from medicine to SaaS is nearly one-to-one. A death in a clinical trial becomes a churn event in SaaS. A patient who drops out of the study becomes a customer who’s still active but hasn’t been observed long enough to know when they’ll churn. The censoring problem is identical, and Kaplan-Meier handles it the same way in both contexts.

Key assumptions

Survival curves are powerful but they require honesty about what you’re measuring.

Non-informative censoring. The reason a customer is censored, meaning they’re still active at the end of your observation window, must be independent of their churn risk. If you cut off your analysis in December and holiday-season customers are systematically different from year-round customers, your curve is biased. In practice this assumption fails often. Customers who signed up during a promotion are frequently different from organic signups, and if promotional customers are overrepresented among the censored, your survival estimate will be off.

A clearly defined origin. Time zero must mean the same thing for every customer. Is it the day of trial signup? First paid invoice? First successful login? These are all defensible choices, but you must pick one and apply it consistently. Mixing origins produces meaningless curves.

Independent observations. Each customer’s churn event should be statistically independent of every other customer’s. This is violated in businesses with strong network effects, viral cancellations, or bulk contract terminations. If a single decision by one enterprise account cancels many seats simultaneously, treating those as independent events overstates your confidence.

Homogeneous risk within the observation period. The underlying churn process should be reasonably stable. If your product changed dramatically halfway through your observation window, the curve you fit is an average of two different regimes, not a true picture of either.

When any of these assumptions break, the fix is usually stratification, computing separate curves for each customer segment, rather than abandoning the method.

The math

The Kaplan-Meier estimator is elegantly simple. For each distinct time tit_i at which a churn event occurred:

S^(t)=tit(1dini)\hat{S}(t) = \prod_{t_i \leq t} \left( 1 - \frac{d_i}{n_i} \right)

Where S^(t)\hat{S}(t) is the estimated survival probability at time tt, tit_i is the ii-th observed churn time, did_i is the number of churns at time tit_i, and nin_i is the number of customers still at risk immediately before tit_i.

The intuition: at each churn event, compute the fraction of at-risk customers who survived that moment. That fraction is 1di/ni1 - d_i/n_i. To find the probability of surviving to time tt, multiply all those individual survival probabilities together.

A concrete example. Suppose you have 100 customers. On day 30, 5 of them churn. On day 60, 3 of them churn from the 95 who remained. On day 90, 4 churn from the 92 who remained.

S^(30)=15100=0.95\hat{S}(30) = 1 - \frac{5}{100} = 0.95

S^(60)=0.95×(1395)=0.95×0.968=0.920\hat{S}(60) = 0.95 \times \left(1 - \frac{3}{95}\right) = 0.95 \times 0.968 = 0.920

S^(90)=0.920×(1492)=0.920×0.957=0.880\hat{S}(90) = 0.920 \times \left(1 - \frac{4}{92}\right) = 0.920 \times 0.957 = 0.880

By day 90, 88% of the original cohort is still active. A single-number monthly churn rate would tell you “roughly 4% monthly,” which is technically accurate but strategically useless. The curve tells you customers survive to 30 days at 95%, to 60 days at 92%, to 90 days at 88%, and lets you see whether the churn is front-loaded (an onboarding problem) or back-loaded (a contract renewal cliff).

Confidence intervals for the survival estimate come from Greenwood’s formula:

Var[S^(t)]=S^(t)2titdini(nidi)\text{Var}[\hat{S}(t)] = \hat{S}(t)^2 \sum_{t_i \leq t} \frac{d_i}{n_i(n_i - d_i)}

This gives you standard errors at every point on the curve, which turn into the shaded confidence bands on well-made survival plots. Narrow bands mean you have enough data to trust the shape. Wide bands mean you’re extrapolating from too few customers.

The hazard function. Closely related to survival is the hazard rate λ(t)\lambda(t), the instantaneous probability of churning at time tt given survival to that point. Where the survival curve shows how many customers are left, the hazard function shows when they’re leaving. Both views matter. Survival tells you the state of your customer base. Hazard tells you where in the lifecycle your product is losing people.

The code

Three panels showing the aggregate survival curve, the same curve stratified by plan tier, and a histogram of when churn events occur by plan

Two implementations. First, a from-scratch Kaplan-Meier estimator so you can see the math working. Then the production-ready version using lifelines.

From scratch:

import numpy as np
import pandas as pd

# Simulated SaaS customer data
# duration: days observed until churn or end of study
# event: 1 if churned, 0 if still active (censored)
np.random.seed(42)
n_customers = 500

# Simulate two hidden segments:
# Segment 1 (30%): high early-churn hazard (Basic plan)
# Segment 2 (70%): stable, low hazard (Pro plan)
segment = np.random.binomial(1, 0.3, n_customers)
durations = np.where(
    segment == 1,
    np.random.exponential(scale=45, size=n_customers),   # high churn segment
    np.random.exponential(scale=400, size=n_customers)   # stable segment
)

# Censor observations that exceed our 365-day observation window
observation_window = 365
events = (durations <= observation_window).astype(int)
durations = np.minimum(durations, observation_window)

df = pd.DataFrame({"duration": durations, "event": events})

def kaplan_meier(durations, events):
    """Compute the Kaplan-Meier survival estimator from scratch."""
    data = pd.DataFrame({"t": durations, "e": events}).sort_values("t").reset_index(drop=True)

    n_at_risk = len(data)
    survival = 1.0
    times, survivals = [0], [1.0]

    for t in data["t"].unique():
        events_at_t = data[(data["t"] == t) & (data["e"] == 1)].shape[0]
        censored_at_t = data[(data["t"] == t) & (data["e"] == 0)].shape[0]

        if events_at_t > 0:
            survival *= (1 - events_at_t / n_at_risk)
            times.append(t)
            survivals.append(survival)

        # After processing this time, both events and censored leave the risk set
        n_at_risk -= (events_at_t + censored_at_t)

    return np.array(times), np.array(survivals)

times, survivals = kaplan_meier(df["duration"], df["event"])

# What does the aggregate churn rate say?
monthly_churn_rate = df["event"].sum() / len(df) / 12
print(f"Aggregate monthly churn: {monthly_churn_rate:.2%}")

# What does the curve say at key milestones?
for milestone in [30, 90, 180, 365]:
    idx = np.searchsorted(times, milestone, side="right") - 1
    print(f"Survival at day {milestone}: {survivals[idx]:.2%}")

Running this produces:

Aggregate monthly churn: 5.82%
Survival at day 30:  77.60%
Survival at day 90:  58.60%
Survival at day 180: 43.40%
Survival at day 365: 30.20%

The aggregate churn rate suggests a moderate, manageable retention problem. But the curve reveals that survival drops sharply in the first 90 days and then stabilizes. A blended monthly figure can’t show you that split, because it’s averaging two very different populations.

Production version with lifelines:

from lifelines import KaplanMeierFitter
import matplotlib.pyplot as plt

kmf = KaplanMeierFitter()

# Fit and plot for the whole cohort
kmf.fit(durations=df["duration"], event_observed=df["event"], label="All customers")
ax = kmf.plot_survival_function(ci_show=True)

# Segment by plan tier to reveal what the aggregate curve hides
df["plan"] = np.where(segment == 1, "Basic", "Pro")

kmf_basic = KaplanMeierFitter().fit(
    df.loc[df["plan"] == "Basic", "duration"],
    df.loc[df["plan"] == "Basic", "event"],
    label="Basic plan"
)
kmf_pro = KaplanMeierFitter().fit(
    df.loc[df["plan"] == "Pro", "duration"],
    df.loc[df["plan"] == "Pro", "event"],
    label="Pro plan"
)

fig, ax = plt.subplots(figsize=(10, 6))
kmf_basic.plot_survival_function(ax=ax)
kmf_pro.plot_survival_function(ax=ax)
ax.set_xlabel("Days since signup")
ax.set_ylabel("Survival probability")
ax.set_title("Basic plan churns fast. Pro plan is stable.")
plt.tight_layout()
plt.show()

When you stratify, the story usually gets sharper. The aggregate curve is an average of your customer segments. Show them separately and the actionable insight appears. In the chart above, the middle panel splits the same customer base by plan tier. The right panel shows the density of churn events over time for each plan, making it clear that Basic-plan churn is concentrated early, while Pro-plan churn is rare and spread out.

Business application

Diagnosing onboarding vs. product problems. If your survival curve drops steeply in the first 30 days and then flattens, you have an activation problem. Customers who make it past day 30 stay for the long haul, but you’re losing too many before they see value. The fix is onboarding, not product. If the curve declines steadily throughout tenure, you have a genuine product-market fit problem: customers are consistently deciding you’re not worth the money. The fix is product.

Contract renewal analysis. Annual contracts create predictable churn cliffs at month 12, 24, and 36. Survival curves make these cliffs visible and quantifiable. If a large share of your annual customers don’t renew, that’s not “average churn.” It’s a specific event tied to contract renewal that should be addressed with renewal-specific outreach.

Cohort comparison. Customers acquired in Q1 might have different survival curves than Q2 customers. Overlaying cohort curves shows you whether your acquisition strategy is improving or degrading over time. This is more informative than tracking a monthly churn number that averages all cohorts together.

LTV calculation. Naive LTV formulas assume constant churn: LTV=ARPUmonthly churn rateLTV = \frac{ARPU}{\text{monthly churn rate}}. This is wrong for most SaaS businesses. Actual LTV is the integral of ARPU multiplied by the survival function over time. When early churn is much higher than late churn, as in the example above, the naive formula can meaningfully overstate LTV, because it assumes the early-tenure churn rate applies uniformly across the customer’s entire lifetime.

Segmentation analysis. Fitting survival curves separately by plan tier, acquisition channel, industry, or company size reveals which segments are actually driving retention. A common finding: the best-retained segment often looks nothing like the marketing team’s ideal customer profile.

Where survival curves lie. The censoring assumption fails silently when customers who churn are more likely to drop out of your analytics tracking entirely, for example by uninstalling an app or blocking emails before you record the churn event. This is common in freemium products and biases the survival estimate upward. Small segments also produce noisy curves. Confidence bands widen quickly when you stratify aggressively, and a survival curve fit on a handful of enterprise customers is barely more informative than a coin flip past year one. Bootstrap the intervals and report them honestly. Finally, the curve is descriptive, not causal. Two cohorts with different survival curves might differ because of acquisition channel, season, pricing tier, or a dozen confounded factors. Explaining why requires additional analysis, typically Cox proportional hazards regression or a properly designed experiment.

Used well, survival analysis is one of the highest-leverage tools a data team can adopt for a subscription business. It replaces one misleading number with a shape that actually tells you what’s happening to your customers.

Pius Oyedepo

Pius Oyedepo

Statistician and data analyst. Writing about the math behind the models.