Kaplan–Meier survival curves in Python with canvasxpress

Python
canvasxpress
survival
bioinformatics
visualization
Turn a survival table into an interactive, reproducible Kaplan–Meier plot stratified by treatment arm — with censoring ticks — in a few lines of Python.
Author

Isaac Neuhaus

Published

September 23, 2026

Kaplan–Meier curves summarize time-to-event data: the step-down survival probability over time, usually split by a group like treatment arm. Here we build one in Python with canvasxpress — it computes the survival steps for you from a time column and a censoring column — and the result is interactive and reproducible.

Install

pip install canvasxpress

A survival table

Swap this simulated block for your own data: per subject you need a follow-up time, a censoring indicator (1 = censored, 0 = event), and the grouping variable.

import numpy as np

rng  = np.random.default_rng(1)
n    = 200
arm  = rng.choice(["Treatment", "Control"], n)
base = np.where(arm == "Treatment", 60, 40)     # treatment survives longer
time = np.round(rng.exponential(base), 1)
cens = rng.binomial(1, 0.3, n)                  # 1 = censored, 0 = event
subj = [f"Subj{i+1}" for i in range(n)]

Build the spec

The two columns — the time and its matching censor flag — go in as the variable samples, and the arm rides along as a variable annotation. The key is graphType="KaplanMeier" and passing both columns to xAxis.

from canvasxpress.canvas import CanvasXpress
from canvasxpress.render.jupyter import CXNoteBook
from IPython.display import display

data = {
    # Subjects are the variables (rows); each row holds [Time, Time-Censor].
    "y": {"vars": subj, "smps": ["Time", "Time-Censor"],
          "data": [[t, c] for t, c in zip(time.tolist(), cens.tolist())]},
    "z": {"Arm": arm.tolist()},
}

config = {
    "graphType": "KaplanMeier",
    "xAxis": ["Time", "Time-Censor"],
    "colorBy": "Arm",
    "title": "Overall survival by treatment arm",
    "xAxisTitle": "Time (weeks)", "yAxisTitle": "Probability of survival",
    "invertCensored": True,               # draw censoring ticks
    "showKMConfidenceIntervals": False,
}

cx = CanvasXpress(render_to="km", data=data, config=config, width=650, height=500)
for part in CXNoteBook(cx).render():
    display(part)

Hover any step for the time and survival probability; the tick marks are censored subjects. canvasxpress derives the survival curve from the raw table, so you never precompute the steps — and the same spec re-renders the identical curve from R or JavaScript.

A common gotcha

The survival transform only fires with graphType="KaplanMeier". A bare "Scatter2D" gives a raw scatter of the censor values instead — survival probability is computed by the engine, not a column in your data.

Next