Kaplan-Meier Survival Plot — Matplotlib

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 Matplotlib

Python source (Matplotlib)

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

import matplotlib.pyplot as plt
import numpy as np


# Data - Clinical trial survival data for two treatment groups
np.random.seed(42)

# Generate realistic survival times (exponential-like distribution)
n_per_group = 80

# Treatment group (better survival)
treatment_times = np.random.exponential(scale=24, size=n_per_group)
treatment_times = np.clip(treatment_times, 0.5, 60)  # Cap at 60 months
treatment_censored = np.random.binomial(1, 0.25, n_per_group)  # 25% censored
treatment_events = 1 - treatment_censored

# Control group (worse survival)
control_times = np.random.exponential(scale=16, size=n_per_group)
control_times = np.clip(control_times, 0.5, 60)
control_censored = np.random.binomial(1, 0.20, n_per_group)  # 20% censored
control_events = 1 - control_censored

# Kaplan-Meier calculation for Treatment group
order = np.argsort(treatment_times)
treat_t_sorted = treatment_times[order]
treat_e_sorted = treatment_events[order]
treat_unique = np.unique(treat_t_sorted[treat_e_sorted == 1])

treat_time_pts = [0.0]
treat_surv_probs = [1.0]
treat_std_errs = [0.0]
treat_surv = 1.0
treat_var = 0.0

for t in treat_unique:
    n_risk = np.sum(treat_t_sorted >= t)
    d = np.sum((treat_t_sorted == t) & (treat_e_sorted == 1))
    if n_risk > 0 and d > 0:
        treat_surv *= (n_risk - d) / n_risk
        if n_risk > d:
            treat_var += d / (n_risk * (n_risk - d))
    treat_time_pts.append(t)
    treat_surv_probs.append(treat_surv)
    treat_std_errs.append(np.sqrt(treat_var) * treat_surv if treat_surv > 0 else 0)

treat_times_km = np.array(treat_time_pts)
treat_surv_km = np.array(treat_surv_probs)
treat_se_km = np.array(treat_std_errs)

# Kaplan-Meier calculation for Control group
order = np.argsort(control_times)
ctrl_t_sorted = control_times[order]
ctrl_e_sorted = control_events[order]
ctrl_unique = np.unique(ctrl_t_sorted[ctrl_e_sorted == 1])

ctrl_time_pts = [0.0]
ctrl_surv_probs = [1.0]
ctrl_std_errs = [0.0]
ctrl_surv = 1.0
ctrl_var = 0.0

for t in ctrl_unique:
    n_risk = np.sum(ctrl_t_sorted >= t)
    d = np.sum((ctrl_t_sorted == t) & (ctrl_e_sorted == 1))
    if n_risk > 0 and d > 0:
        ctrl_surv *= (n_risk - d) / n_risk
        if n_risk > d:
            ctrl_var += d / (n_risk * (n_risk - d))
    ctrl_time_pts.append(t)
    ctrl_surv_probs.append(ctrl_surv)
    ctrl_std_errs.append(np.sqrt(ctrl_var) * ctrl_surv if ctrl_surv > 0 else 0)

ctrl_times_km = np.array(ctrl_time_pts)
ctrl_surv_km = np.array(ctrl_surv_probs)
ctrl_se_km = np.array(ctrl_std_errs)

# Get censored observation times for tick marks
treat_censor_times = treatment_times[treatment_events == 0]
ctrl_censor_times = control_times[control_events == 0]

# Interpolate survival at censored times for tick marks
treat_censor_surv = np.interp(treat_censor_times, treat_times_km, treat_surv_km)
ctrl_censor_surv = np.interp(ctrl_censor_times, ctrl_times_km, ctrl_surv_km)

# Plot
fig, ax = plt.subplots(figsize=(16, 9))

# Python colors
treatment_color = "#306998"
control_color = "#FFD43B"

# Treatment group curve with CI
ax.step(treat_times_km, treat_surv_km, where="post", color=treatment_color, linewidth=3, label="Treatment Group")
treat_upper = np.clip(treat_surv_km + 1.96 * treat_se_km, 0, 1)
treat_lower = np.clip(treat_surv_km - 1.96 * treat_se_km, 0, 1)
ax.fill_between(treat_times_km, treat_lower, treat_upper, step="post", alpha=0.2, color=treatment_color)

# Control group curve with CI
ax.step(ctrl_times_km, ctrl_surv_km, where="post", color=control_color, linewidth=3, label="Control Group")
ctrl_upper = np.clip(ctrl_surv_km + 1.96 * ctrl_se_km, 0, 1)
ctrl_lower = np.clip(ctrl_surv_km - 1.96 * ctrl_se_km, 0, 1)
ax.fill_between(ctrl_times_km, ctrl_lower, ctrl_upper, step="post", alpha=0.3, color=control_color)

# Censored observation tick marks
ax.scatter(treat_censor_times, treat_censor_surv, marker="|", s=400, color=treatment_color, linewidth=2, zorder=5)
ax.scatter(ctrl_censor_times, ctrl_censor_surv, marker="|", s=400, color="#CC9A00", linewidth=2, zorder=5)

# Calculate median survival times
treat_median_idx = np.where(treat_surv_km <= 0.5)[0]
ctrl_median_idx = np.where(ctrl_surv_km <= 0.5)[0]

treat_median = treat_times_km[treat_median_idx[0]] if len(treat_median_idx) > 0 else None
ctrl_median = ctrl_times_km[ctrl_median_idx[0]] if len(ctrl_median_idx) > 0 else None

# Add median survival annotation lines
if treat_median:
    ax.axhline(y=0.5, color="gray", linestyle=":", linewidth=1.5, alpha=0.5)
    ax.axvline(x=treat_median, color=treatment_color, linestyle=":", linewidth=1.5, alpha=0.7)
if ctrl_median:
    ax.axvline(x=ctrl_median, color="#CC9A00", linestyle=":", linewidth=1.5, alpha=0.7)

# Add median text
median_text = ""
if treat_median:
    median_text += f"Treatment median: {treat_median:.1f} mo"
if ctrl_median:
    median_text += f"\nControl median: {ctrl_median:.1f} mo"

ax.text(
    0.98,
    0.02,
    median_text.strip(),
    transform=ax.transAxes,
    fontsize=16,
    verticalalignment="bottom",
    horizontalalignment="right",
    bbox={"boxstyle": "round,pad=0.5", "facecolor": "white", "edgecolor": "gray", "alpha": 0.8},
)

# Styling
ax.set_xlabel("Time (months)", fontsize=20)
ax.set_ylabel("Survival Probability", fontsize=20)
ax.set_title("survival-kaplan-meier · matplotlib · pyplots.ai", fontsize=24)
ax.tick_params(axis="both", labelsize=16)
ax.set_xlim(0, 65)
ax.set_ylim(0, 1.05)
ax.legend(fontsize=16, loc="upper right")
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