Unlike stepwise Cox survfit curves, spsurv
evaluates survival on a dense time grid using the
fitted Bernstein baseline. Kaplan–Meier stays a step
function; Bernstein-polynomial curves are always smooth
lines (geom_line, or
plot(survfit(fit))). This vignette connects observed KM to
model-based predictions.
library(spsurv)
library(KMsurv)
library(survival)
library(ggplot2)
data(larynx)
larynx$stage <- factor(larynx$stage)km_stage <- survfit(Surv(time, delta) ~ stage, data = larynx)
km_long <- data.frame(
time = km_stage$time,
surv = km_stage$surv,
stage = rep(levels(larynx$stage), km_stage$strata)
)
ggplot(km_long, aes(x = time, y = surv, color = stage)) +
geom_step(linewidth = 0.6) +
geom_point(
data = subset(larynx, delta == 0),
aes(x = time, y = 0, color = stage),
shape = 3,
size = 1.5,
alpha = 0.5,
inherit.aes = FALSE
) +
labs(x = "Time (years)", y = "Survival probability", color = "Stage") +
theme_bw() +
theme(legend.position = "bottom")We compare predictions at age = 70 across all stages — a clinically interpretable profile that holds age fixed while varying disease stage.
KM curves show unadjusted stage separation.
Censoring marks (+) remind you that late follow-up
estimates rest on fewer subjects.
stage in the
model is justified.fit <- bpph(
Surv(time, delta) ~ age + stage,
degree = 5,
data = larynx,
approach = "mle",
init = 0
)
summary(fit)
#> Call:
#> bpph(formula = Surv(time, delta) ~ age + stage, degree = 5, data = larynx,
#> approach = "mle", init = 0, model = "ph")
#>
#> Bernstein PH model:
#> Regression coefficients:
#> Estimate 2.5% 97.5% Std. Error z value Pr(>|z|)
#> age 0.0197 -0.0085 0.0478 0.0143 1.4 0.17
#> stage2 0.1730 -0.7324 1.0783 0.4619 0.4 0.71
#> stage3 0.6521 -0.0450 1.3492 0.3557 1.8 0.07 .
#> stage4 1.7778 0.9471 2.6086 0.4239 4.2 3e-05 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Exponentiated coefficients:
#> Estimate 2.5% 97.5%
#> age 1.02 0.99 1.0
#> stage2 1.19 0.48 2.9
#> stage3 1.92 0.96 3.9
#> stage4 5.92 2.58 13.6
#>
#> ---
#> loglik = -141 AIC = 299predict() — tidy data framepr <- predict(fit, newdata = newdata, times = plot_times)
head(pr)
#> age stage id time surv lower upper cumhaz std.err
#> 1 70 1 1 0.00000000 1.0000000 1.0000000 1 0.000000000 0.00000000
#> 1.1 70 1 1 0.08916667 0.9922984 0.9666007 1 0.007731445 0.01338715
#> 1.2 70 1 1 0.17833333 0.9847620 0.9390056 1 0.015355286 0.02427520
#> 1.3 70 1 1 0.26750000 0.9773821 0.9160002 1 0.022877617 0.03309298
#> 1.4 70 1 1 0.35666667 0.9701500 0.8964400 1 0.030304583 0.04031670
#> 1.5 70 1 1 0.44583333 0.9630573 0.8792141 1 0.037642381 0.04647250Panel A: observed KM (steps) vs model prediction (smooth dashed) per stage.
pr$stage <- newdata$stage[match(as.character(pr$id), as.character(seq_len(nrow(newdata))))]
ggplot() +
geom_step(
data = km_long,
aes(x = time, y = surv, color = stage),
linewidth = 0.5,
alpha = 0.8
) +
geom_line(
data = pr,
aes(x = time, y = surv, color = stage),
linetype = "dashed",
linewidth = 0.6
) +
labs(x = "Time (years)", y = "Survival probability", color = "Stage") +
theme_bw() +
theme(legend.position = "bottom")Panel B: model-based ribbons (adjusted prediction
with uncertainty), still drawn with geom_line rather than
geom_step.
ggplot(pr, aes(x = time, y = surv, color = stage, fill = stage)) +
geom_ribbon(aes(ymin = lower, ymax = upper), alpha = 0.2, colour = NA) +
geom_line(linewidth = 0.5) +
labs(x = "Time (years)", y = "Survival probability", color = NULL, fill = NULL) +
theme_bw() +
theme(legend.position = "bottom")Panel A connects the smooth BP fit to observed risk sets. Panel B shows covariate-adjusted uncertainty — ribbons often widen late in follow-up when data are sparse.
predict() output directly to
your ggplot pipeline.survfit() with tidy = TRUEsf <- survfit(fit, newdata = newdata, times = plot_times, tidy = TRUE)
head(sf)
#> age stage id time surv lower upper cumhaz std.err
#> 1 70 1 1 0.00000000 1.0000000 1.0000000 1 0.000000000 0.00000000
#> 1.1 70 1 1 0.08916667 0.9922984 0.9666007 1 0.007731445 0.01338715
#> 1.2 70 1 1 0.17833333 0.9847620 0.9390056 1 0.015355286 0.02427520
#> 1.3 70 1 1 0.26750000 0.9773821 0.9160002 1 0.022877617 0.03309298
#> 1.4 70 1 1 0.35666667 0.9701500 0.8964400 1 0.030304583 0.04031670
#> 1.5 70 1 1 0.44583333 0.9630573 0.8792141 1 0.037642381 0.04647250For approach = "bayes", credible bands use posterior
draws with interval.type = "hpd" and
monotone = TRUE (default for Bayes).
Log–log intervals are undefined at S = 1
(time = 0), so we evaluate at time > 0
only:
loglog_times <- plot_times[plot_times > 0]
head(predict(fit, newdata = newdata, times = loglog_times, type = "log-log"))
#> age stage id time surv lower upper cumhaz
#> 1 70 1 1 0.08916667 0.9922984 0.7943630 0.9997404 0.007731445
#> 1.1 70 1 1 0.17833333 0.9847620 0.7115221 0.9993075 0.015355286
#> 1.2 70 1 1 0.26750000 0.9773821 0.6772829 0.9986577 0.022877617
#> 1.3 70 1 1 0.35666667 0.9701500 0.6629301 0.9977685 0.030304583
#> 1.4 70 1 1 0.44583333 0.9630573 0.6549430 0.9966575 0.037642381
#> 1.5 70 1 1 0.53500000 0.9560957 0.6459815 0.9953977 0.044897252
#> std.err
#> 1 0.01338715
#> 1.1 0.02427520
#> 1.2 0.03309298
#> 1.3 0.04031670
#> 1.4 0.04647250
#> 1.5 0.05212577