Plot a Kaplan–Meier curve (with confidence intervals and optional risk table)
using a supplied Surv object. If the grouping variable has exactly two
levels the function fits a Cox model and annotates hazard ratio (95% CI) and
p-value; if more than two groups it displays only the log-rank p-value.
Optional weights (e.g. IPTW) can be supplied to produce weighted survival
curves and a weighted Cox model annotation; see weights below.
Usage
plot_survival_curves(
surv_obj,
data,
group_var = "met_exercise_guidelines",
weights = NULL,
id = NULL,
confidence_bands = TRUE,
line_size = 1,
time_limits = NULL,
x_breaks = NULL,
annotate_y = 0.99,
annotate_x = NULL,
x_label = "Time (units??)",
y_label = NULL,
title = NULL,
custom_hr_pvalue_text = NULL,
type = c("survival", "risk"),
show_risktable = TRUE,
risktable_stats = c("n.risk", "cum.event"),
risktable_counts = c("both", "weighted", "unweighted"),
ristable_text_size = 3.5,
annotate_hjust = 1
)Arguments
- surv_obj
A
Survobject (can be right-censored or left-truncated).- data
A data frame containing the variables referenced by
surv_objandgroup_var.- group_var
Character, name of the grouping column in
data.- weights
Optional weights for producing weighted survival curves (e.g. inverse probability of treatment weights, IPTW). Either the name of a numeric column in
data, or a numeric vector with length equal tonrow(data). Weights must be strictly positive (an error is thrown otherwise);survival::coxph(), which this function always fits when weights are supplied, requires weights> 0, andsurvival::survfit.formula()treats zero weights as ambiguous, so observations that should be excluded should be filtered out ofdatabeforehand rather than given a zero weight. There is no required scale/normalization (e.g. weights do not need to sum to 1 or tonrow(data)); IPTW weights are commonly left unstabilized or stabilized to a mean of 1, and either is fine here. IfNULL(default), curves are unweighted. When supplied, the Cox model used for the HR/p-value annotation is fit withrobust = TRUE(sandwich variance), as is standard practice for IPTW-type weights. Ifgroup_varhas more than two levels, the omnibus p-value is a robust Wald test from a weighted Cox model rather than a log-rank test, sincesurvival::survdiff()does not support weights. Whenshow_risktable = TRUE, the risk table statistics are rounded to 1 decimal place (they are non-integer "effective" counts when weighted); seerisktable_countsto show unweighted counts instead of or alongside the weighted ones. Whensurv_objis a counting-process (left-truncated)Surv(time1, time2, event)object, the robust sandwich variance needs to know which rows belong to the same subject, soid(below) is required in that case; it is optional for a plain right-censoredSurv(time, event)object, where each row is always its own independent subject.- id
Character name of a subject identifier column in
data. Only relevant whenweightsis supplied, to correctly cluster rows belonging to the same subject for the robust sandwich variance used by the weighted Cox model. Required whenweightsis supplied andsurv_objis a counting-processSurv(time1, time2, event)object (an error is thrown otherwise), since such data may have a single subject contributing multiple(time1, time2]intervals (e.g. time-varying covariates) — silently guessing that every row is an independent subject risks an anti-conservative HR/p-value (standard errors/CIs too narrow). If every row ofdatais already its own independent subject (e.g. simple left truncation with one row per subject, such as age at entry/age at exit), add a row-number column and pass its name here. Optional (defaultNULL) whensurv_objis a plain right-censoredSurv(time, event)object, where each row is always its own independent subject andNULLis equivalent to a row-number id.- confidence_bands
Logical, if
TRUE(default) display confidence bands- line_size
Numeric, line size for the survival curves (default 1).
- time_limits
Numeric(2), x-axis limits for the plot. If
NULL, sensible limits are estimated from the observed event times.- x_breaks
Numeric vector of x-axis breaks. If
NULL, reasonable breaks are selected automatically.- annotate_y
Numeric, y position for annotation text (default 0.99).
- annotate_x
Numeric or
NULL, x position for annotation; ifNULLuses the rightmost value oftime_limits.- x_label
Character, label for the x axis.
- y_label
Character, label for the y axis. If
NULL, a default label is chosen based ontype.- title
Character or
NULL, plot title.- custom_hr_pvalue_text
Character or
NULL; if supplied, overrides the automatically generated HR/log-rank annotation text.- type
Character, one of
"survival"or"risk"; passed toggsurvfit(). If"survival", the default y-axis label is"Probability Event-Free". If"risk", the default y-axis label is"Probability of Event".- show_risktable
Logical; if
TRUE(default), display a risk table beneath the survival curve.- risktable_stats
Character vector specifying statistics shown in the risk table. Must contain one or more of:
c("n.risk", "cum.event", "cum.censor", "n.event", "n.censor"). The default isc("n.risk", "cum.event").- risktable_counts
Character, one of
"both"(default),"weighted", or"unweighted". Only relevant whenweightsis supplied andshow_risktable = TRUE."weighted"shows the (rounded) weighted counts, with row labels suffixed": Weighted"(e.g."At Risk: Weighted") to flag that they are non-integer "effective" counts rather than raw subject counts."unweighted"shows the raw unweighted subject counts in the risk table instead (curves, CI, and the HR/p-value annotation remain weighted), with unsuffixed row labels (e.g."At Risk")."both"shows each cell as"weighted (unweighted)"with row labels suffixed": Weighted (Raw)"; this is exact for"n.risk","cum.event", and"cum.censor", but the raw (non-cumulative)"n.event"/"n.censor"counts are totals over each displayed risk table interval, and the unweighted side of"both"can only be computed exactly at actual event/censoring times. Combiningrisktable_counts = "both"withrisktable_statscontaining"n.event"or"n.censor"therefore throws an error; use"cum.event"/"cum.censor"instead (exact in"both"mode), or setrisktable_countsto"weighted"or"unweighted".- ristable_text_size
Numeric, text size for the risk table (default 3.5).
- annotate_hjust
Numeric, horizontal justification of the annotation text relative to
annotate_x(default 1, i.e.annotate_xis the text's right edge, matching theNULL/rightmost default ofannotate_x). Set to 0 to left-justify the text againstannotate_xinstead (its left edge), e.g. when placing the annotation at the left side of the plot – with the default1, text placed near the left edge oftime_limitsextends further left and is clipped out of the plot entirely.Available statistics:
"n.risk"Number of patients at risk"cum.event"Cumulative number of observed events"cum.censor"Cumulative number of censored observations"n.event"Number of events in each time interval"n.censor"Number of censored observations in each time interval
Examples
data(cancer, package = "survival")
# Example with two groups
lung$sex <- factor(lung$sex, labels = c("Male", "Female"))
surv_obj <- with(lung, survival::Surv(time, status == 2))
plot_survival_curves(
surv_obj,
lung,
group_var = "sex"
) + theme_bw2()
#> Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
#> ℹ Please use `linewidth` instead.
#> ℹ The deprecated feature was likely used in the ggsurvfit package.
#> Please report the issue at <https://github.com/pharmaverse/ggsurvfit/issues>.
# Hide the risk table
plot_survival_curves(
surv_obj,
lung,
group_var = "sex",
show_risktable = FALSE
) + theme_bw2()
#> Warning: Removed 7 rows containing missing values or values outside the scale range
#> (`geom_step()`).
# Show only number at risk
plot_survival_curves(
surv_obj,
lung,
group_var = "sex",
risktable_stats = "n.risk"
) + theme_bw2()
# Example with more than two groups
lung$ph.ecog[lung$ph.ecog == 3] <- NA
lung$ph.ecog <- factor(lung$ph.ecog)
plot_survival_curves(
surv_obj,
lung,
group_var = "ph.ecog"
) + theme_bw2()
# Cumulative incidence plot
plot_survival_curves(
surv_obj,
lung,
group_var = "ph.ecog",
type = "risk"
) + theme_bw2()
# Weighted survival curves (e.g. IPTW)
lung$iptw <- runif(nrow(lung), 0.5, 2)
# risktable_counts = "both" (the default) shows each risk table cell as
# "weighted (unweighted)", so the actual number of observed events/at-risk
# subjects stays visible alongside the weighted ("effective") Ns used for
# the curves/CI/HR
plot_survival_curves(
surv_obj,
lung,
group_var = "sex",
weights = "iptw",
risktable_counts = "both"
) + theme_bw2()
# Weighted curves with only the (rounded) weighted Ns in the risk table
plot_survival_curves(
surv_obj,
lung,
group_var = "sex",
weights = "iptw",
risktable_counts = "weighted"
) + theme_bw2()
# Weighted curves with only the raw unweighted Ns in the risk table
plot_survival_curves(
surv_obj,
lung,
group_var = "sex",
weights = "iptw",
risktable_counts = "unweighted"
) + theme_bw2()