Kaplan-Meier Survival Plot — Seaborn

A Kaplan-Meier survival plot visualizes the probability of survival (or event-free time) over a time period using a step function. It is the standard method for estimating survival functions from time-to-event data, handling censored observations where the event has not yet occurred. The plot shows how survival probability decreases over time, with optional confidence intervals and comparison between groups.

Kaplan-Meier Survival Plot rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
survival-kaplan-meier: Kaplan-Meier Survival Plot
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 72/100 | Updated: 2026-05-11
"""

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns


# Kaplan-Meier estimator function
def kaplan_meier(time, event):
    """Calculate Kaplan-Meier survival estimates with confidence intervals."""
    df = pd.DataFrame({"time": time, "event": event}).sort_values("time")

    # Get unique event times (only where events occurred)
    event_times = df[df["event"] == 1]["time"].unique()
    event_times = np.sort(event_times)

    survival = []
    ci_lower = []
    ci_upper = []
    times = [0]
    survival.append(1.0)
    ci_lower.append(1.0)
    ci_upper.append(1.0)

    s = 1.0
    var_sum = 0.0

    for t in event_times:
        n_at_risk = len(df[df["time"] >= t])
        n_events = len(df[(df["time"] == t) & (df["event"] == 1)])

        if n_at_risk > 0:
            s *= 1 - n_events / n_at_risk
            if n_at_risk > n_events:
                var_sum += n_events / (n_at_risk * (n_at_risk - n_events))

        times.append(t)
        survival.append(s)

        # Greenwood's formula for confidence intervals
        if s > 0 and var_sum > 0:
            se = s * np.sqrt(var_sum)
            ci_lower.append(max(0, s - 1.96 * se))
            ci_upper.append(min(1, s + 1.96 * se))
        else:
            ci_lower.append(s)
            ci_upper.append(s)

    return np.array(times), np.array(survival), np.array(ci_lower), np.array(ci_upper)


# Generate synthetic clinical trial data
np.random.seed(42)
n_patients = 150

# Treatment group (better survival)
n_treatment = 75
treatment_time = np.random.exponential(scale=24, size=n_treatment)
treatment_time = np.clip(treatment_time, 0, 36)  # Max follow-up 36 months
treatment_event = np.random.binomial(1, 0.6, size=n_treatment)
# Censor some patients (still alive at end of study)
treatment_event[treatment_time >= 35] = 0

# Control group (worse survival)
n_control = 75
control_time = np.random.exponential(scale=16, size=n_control)
control_time = np.clip(control_time, 0, 36)
control_event = np.random.binomial(1, 0.75, size=n_control)
control_event[control_time >= 35] = 0

# Calculate Kaplan-Meier estimates for each group
treat_times, treat_surv, treat_ci_lo, treat_ci_hi = kaplan_meier(treatment_time, treatment_event)
ctrl_times, ctrl_surv, ctrl_ci_lo, ctrl_ci_hi = kaplan_meier(control_time, control_event)

# Get censored observations for marking
treat_censor_times = treatment_time[treatment_event == 0]
ctrl_censor_times = control_time[control_event == 0]

# Interpolate survival at censoring times for tick marks
treat_censor_surv = np.interp(treat_censor_times, treat_times, treat_surv)
ctrl_censor_surv = np.interp(ctrl_censor_times, ctrl_times, ctrl_surv)

# Colors
treatment_color = "#306998"  # Python Blue
control_color = "#FFD43B"  # Python Yellow

# Create plot
sns.set_style("whitegrid")
fig, ax = plt.subplots(figsize=(16, 9))

# Plot treatment group survival curve (step function)
ax.step(treat_times, treat_surv, where="post", color=treatment_color, linewidth=3, label="Treatment")
ax.fill_between(treat_times, treat_ci_lo, treat_ci_hi, step="post", alpha=0.2, color=treatment_color, linewidth=0)

# Plot control group survival curve (step function)
ax.step(ctrl_times, ctrl_surv, where="post", color=control_color, linewidth=3, label="Control")
ax.fill_between(ctrl_times, ctrl_ci_lo, ctrl_ci_hi, step="post", alpha=0.3, color=control_color, linewidth=0)

# Mark censored observations with tick marks
ax.scatter(treat_censor_times, treat_censor_surv, marker="|", s=300, color=treatment_color, linewidths=2, zorder=5)
ax.scatter(ctrl_censor_times, ctrl_censor_surv, marker="|", s=300, color=control_color, linewidths=2, zorder=5)

# Calculate median survival times
treat_median_idx = np.where(treat_surv <= 0.5)[0]
treat_median = treat_times[treat_median_idx[0]] if len(treat_median_idx) > 0 else None
ctrl_median_idx = np.where(ctrl_surv <= 0.5)[0]
ctrl_median = ctrl_times[ctrl_median_idx[0]] if len(ctrl_median_idx) > 0 else None

# Add median survival annotation
if treat_median is not None:
    ax.axhline(y=0.5, color="gray", linestyle="--", alpha=0.5, linewidth=1.5)
    annotation_text = f"Median survival:\nTreatment: {treat_median:.1f} months"
    if ctrl_median is not None:
        annotation_text += f"\nControl: {ctrl_median:.1f} months"
    ax.annotate(
        annotation_text,
        xy=(0.98, 0.55),
        xycoords="axes fraction",
        fontsize=16,
        ha="right",
        va="bottom",
        bbox={"boxstyle": "round,pad=0.5", "facecolor": "white", "edgecolor": "gray", "alpha": 0.9},
    )

# Labels and styling
ax.set_xlabel("Time (months)", fontsize=20)
ax.set_ylabel("Survival Probability", fontsize=20)
ax.set_title("survival-kaplan-meier · seaborn · pyplots.ai", fontsize=24)
ax.tick_params(axis="both", labelsize=16)

ax.set_xlim(0, 38)
ax.set_ylim(0, 1.05)

# Legend
ax.legend(fontsize=18, loc="lower left", framealpha=0.9)

# Subtle grid
ax.grid(True, alpha=0.3, linestyle="--")

plt.tight_layout()
plt.savefig("plot.png", dpi=300, bbox_inches="tight")

Part of Kaplan-Meier Survival Plot on anyplot.ai.

Other implementations