spsurv implements
three standard survival regression frameworks under one likelihood and
syntax:
| Family | Function | Covariate effect | Typical interpretation |
|---|---|---|---|
| PH | bpph() |
Multiplicative on hazard | Hazard ratio |
| PO | bppo() |
Multiplicative on cumulative odds | Odds ratio |
| AFT | bpaft() |
Additive on log-time scale | Time ratio |
library(spsurv)
library(survival)
library(ggplot2)
data(veteran)
veteran$celltype <- factor(veteran$celltype)km_ct <- survfit(Surv(time, status) ~ celltype, data = veteran)
km_long <- data.frame(
time = km_ct$time,
surv = km_ct$surv,
celltype = rep(levels(veteran$celltype), km_ct$strata)
)
ggplot(km_long, aes(x = time, y = surv, color = celltype)) +
geom_step(linewidth = 0.6) +
labs(x = "Time (days)", y = "Survival probability", color = "Cell type") +
theme_bw() +
theme(legend.position = "bottom", axis.text.x = element_text(angle = 45, hjust = 1))Kaplan-Meier by cell type.
ggplot(veteran, aes(x = factor(status), y = karno, fill = factor(status))) +
geom_violin(alpha = 0.6) +
geom_boxplot(width = 0.15, outlier.alpha = 0.4) +
scale_x_discrete(labels = c("Censored", "Event")) +
labs(x = NULL, y = "Karnofsky score", fill = NULL) +
theme_bw() +
theme(legend.position = "none")Karnofsky score by event status.
The paper examples sometimes restrict to prior == 0; we
use the full veteran dataset here for simplicity.
KM by cell type reveals crossing and separation patterns. Karnofsky differs between events and censoring — a strong candidate covariate.
AFT uses degree = 3 here so the Bernstein
gamma block stays numerically stable on veteran; PH and PO
use degree = 5. The overlay below plots point
estimates of survival; when
gamma_information_stable is FALSE, bands are
still computed but survfit() warns that they may be
unreliable.
Same covariate profiles under PH, PO, and AFT:
newdata <- data.frame(karno = c(30, 70), celltype = "squamous")
plot_times <- seq(0, max(veteran$time), length.out = 121)
pr_ph <- predict(fit_ph, newdata = newdata, times = plot_times)
pr_po <- predict(fit_po, newdata = newdata, times = plot_times)
pr_aft <- predict(fit_aft, newdata = newdata, times = plot_times)
pr_ph$family <- "PH"
pr_po$family <- "PO"
pr_aft$family <- "AFT"
pr_all <- rbind(pr_ph, pr_po, pr_aft)
pr_all$label <- paste0(
pr_all$family, " (Karnofsky ",
newdata$karno[match(as.character(pr_all$id), as.character(seq_len(nrow(newdata))))],
")"
)
ggplot(pr_all, aes(x = time, y = surv, color = label, linetype = family)) +
geom_line(linewidth = 0.5) +
labs(x = "Time (days)", y = "Survival probability", color = NULL, linetype = "Family") +
theme_bw() +
theme(legend.position = "bottom")Predicted survival: PH vs PO vs AFT (Karnofsky 30 and 70, squamous).
aic_tbl <- data.frame(
model = c("PH", "PO", "AFT"),
AIC = c(AIC(fit_ph), AIC(fit_po), AIC(fit_aft))
)
aic_tbl
#> model AIC
#> 1 PH 1450.026
#> 2 PO 1438.685
#> 3 AFT 1464.954The same covariates on different regression scales produce different survival curves. AIC summarizes fit but does not replace clinical judgment when curves diverge meaningfully.
All three share the same Bernstein baseline machinery and Stan backend.