using CairoMakie
using DataFrames
using Statistics
using RandomSurvival Curve (Julia)
The Kaplan–Meier estimator reads out survival probability over time from right-censored follow-up data. There is no maintained Kaplan–Meier package in the Julia ecosystem (registry-checked 2026-10), so this tutorial implements the estimator and its Greenwood confidence band directly — twenty lines of base Julia — and renders the step curves with stairs!. The mechanics ARE the tutorial.
Example

Setup
- System Requirements: Cross-platform (Linux/MacOS/Windows)
- Programming Language: Julia
- Dependencies:
CairoMakie,DataFrames,Statistics
Data Preparation
We simulate 60 patients per arm (Control, Treatment) with exponential survival — median 12 vs 24 months — and uniform administrative censoring between 10 and 72 months.
function simulate_arm(rng, median; n = 60)
true_time = -median / log(2) .* log.(rand(rng, n))
censor = 10 .+ 62 .* rand(rng, n)
DataFrame(Months = min.(true_time, censor),
Event = true_time .<= censor)
end
rng = Xoshiro(141)
control = simulate_arm(rng, 12)
treat = simulate_arm(rng, 24)
control.Arm = fill("Control", nrow(control))
treat.Arm = fill("Treatment", nrow(treat))
surv = vcat(control, treat)| Row | Months | Event | Arm |
|---|---|---|---|
| Float64 | Bool | String | |
| 1 | 30.2539 | true | Control |
| 2 | 40.2043 | false | Control |
| 3 | 10.2504 | false | Control |
| 4 | 17.2625 | true | Control |
| 5 | 14.8126 | true | Control |
| 6 | 26.9555 | true | Control |
| 7 | 4.53743 | true | Control |
| 8 | 12.991 | true | Control |
| 9 | 26.3117 | true | Control |
| 10 | 23.9808 | true | Control |
| 11 | 2.33873 | true | Control |
| 12 | 9.1478 | true | Control |
| 13 | 46.349 | true | Control |
| ⋮ | ⋮ | ⋮ | ⋮ |
| 109 | 9.95473 | true | Treatment |
| 110 | 6.26144 | true | Treatment |
| 111 | 18.4051 | true | Treatment |
| 112 | 33.0882 | true | Treatment |
| 113 | 35.1187 | false | Treatment |
| 114 | 2.9565 | true | Treatment |
| 115 | 14.5833 | false | Treatment |
| 116 | 27.706 | true | Treatment |
| 117 | 7.00252 | true | Treatment |
| 118 | 10.8413 | true | Treatment |
| 119 | 51.6202 | false | Treatment |
| 120 | 26.747 | true | Treatment |
Visualization
Kaplan–Meier Estimator, Step by Step
At each distinct event time the survival multiplies by the at-risk-adjusted survival fraction; stairs! renders the characteristic step curve.
function km_fit(times, events)
order = sortperm(times)
t = times[order]; e = events[order]
n = length(t)
s = 1.0
ts = Float64[0.0]; ss = Float64[1.0]
for tt in sort(unique(t))
d = count(j -> t[j] == tt && e[j], 1:n)
atrisk = count(j -> t[j] >= tt, 1:n)
if d > 0
s *= 1 - d / atrisk
push!(ts, tt); push!(ss, s)
end
end
(ts, ss)
end
arm_styles = Dict("Control" => :steelblue, "Treatment" => :tomato)
fig = Figure(size = (460, 360))
ax = Axis(fig[1, 1], xlabel = "Months since treatment",
ylabel = "Survival probability")
for arm in ["Control", "Treatment"]
sub = surv[surv.Arm .== arm, :]
ts, ss = km_fit(sub.Months, sub.Event)
stairs!(ax, ts, ss; step = :post, color = arm_styles[arm],
linewidth = 1.8, label = "$arm (n=$(nrow(sub)))")
end
hlines!(ax, [0.5]; color = :gray60, linestyle = :dot, linewidth = 0.8)
axislegend(ax; framevisible = false, position = :lb)
ylims!(ax, 0, 1.02)
hidespines!(ax, :t, :r)
figGreenwood Confidence Band
The Greenwood variance S² Σ dᵢ / (nᵢ(nᵢ − dᵢ)) gives pointwise 95% bands; plotting the band behind the step curve completes the publication layout.
function km_fit_ci(times, events)
order = sortperm(times)
t = times[order]; e = events[order]
n = length(t)
s = 1.0; cum = 0.0
ts = Float64[0.0]; ss = Float64[1.0]; lo = Float64[1.0]; hi = Float64[1.0]
for tt in sort(unique(t))
d = count(j -> t[j] == tt && e[j], 1:n)
atrisk = count(j -> t[j] >= tt, 1:n)
if d > 0
s *= 1 - d / atrisk
cum += d / (atrisk * (atrisk - d))
se = s * sqrt(cum)
push!(ts, tt); push!(ss, s)
push!(lo, max(s - 1.96 * se, 0.0)); push!(hi, min(s + 1.96 * se, 1.0))
end
end
(ts, ss, lo, hi)
end
sub = surv[surv.Arm .== "Control", :]
ts, ss, lo, hi = km_fit_ci(sub.Months, sub.Event)
# Band has no step attribute: repeat points to build the step outline
xs_step = Float64[]; lo_step = Float64[]; hi_step = Float64[]
for i in eachindex(ts)
if i > 1
push!(xs_step, ts[i]); push!(lo_step, lo[i-1]); push!(hi_step, hi[i-1])
end
push!(xs_step, ts[i]); push!(lo_step, lo[i]); push!(hi_step, hi[i])
end
fig = Figure(size = (460, 360))
ax = Axis(fig[1, 1], xlabel = "Months since treatment",
ylabel = "Survival probability")
band!(ax, xs_step, lo_step, hi_step; color = (:steelblue, 0.15))
stairs!(ax, ts, ss; step = :post, color = :steelblue, linewidth = 1.8)
hlines!(ax, [0.5]; color = :gray60, linestyle = :dot, linewidth = 0.8)
ylims!(ax, 0, 1.02)
hidespines!(ax, :t, :r)
figReferences
- Kaplan, E. L., & Meier, P. (1958). Nonparametric Estimation from Incomplete Observations. Journal of the American Statistical Association, 53(282), 457-481.
- Borgan, Ø., & Liestøl, K. (1990). A Note on Confidence Intervals and Bands for the Kaplan-Meier Estimator. Scandinavian Journal of Statistics, 17(1), 35-41.
