rpsurv fits the Royston and Parmar (2002) family of
flexible parametric survival models: the baseline distribution is a
restricted cubic spline in log time on a chosen transformed scale
(proportional hazards, proportional odds, or probit), which gives a
smooth, fully parametric, extrapolable fit without committing to a
Weibull, log-normal, or other single distribution.
The log-likelihood and its analytic gradient are evaluated in C++ via
RcppParallel, so fitting stays fast at large sample sizes
(see the Benchmark section below). Output mirrors coxph():
print()/summary() separate interpretable
covariate effects from the baseline spline nuisance parameters, and
there are predict()/plot()/residual-diagnostic
methods.
All results below are real output from rstpm2::brcancer
(686 rows, 299 events).
# install.packages("rpsurv") # once on CRAN
# development version:
# remotes::install_github("ielbadisy/rpsurv")library(rpsurv)
library(survival)
data(brcancer)
brcancer$hormon <- as.numeric(brcancer$hormon)
fit <- rpsurv(Surv(rectime, censrec) ~ hormon, data = brcancer, df = 4, scale = "hazard")
fit
#> Royston-Parmar flexible parametric survival model
#> Call:
#> rpsurv(formula = Surv(rectime, censrec) ~ hormon, data = brcancer,
#> df = 4, scale = "hazard")
#>
#> Scale: hazard Baseline df: 4
#> n = 686 , number of events = 299
#> Log-likelihood = -2606 AIC = 5225
summary(fit)
#> Royston-Parmar flexible parametric survival model
#> Call:
#> rpsurv(formula = Surv(rectime, censrec) ~ hormon, data = brcancer,
#> df = 4, scale = "hazard")
#>
#> Covariate effects (hazard scale):
#> coef exp(coef) [HR] se(coef) z Pr(>|z|)
#> hormon -0.3641 0.6948 0.1249 -2.914 0.00356 **
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Baseline spline terms:
#> coef exp(coef) se(coef) z Pr(>|z|)
#> (Intercept) -1.960e+01 3.086e-09 3.257e+00 -6.016 1.79e-09
#> s(logt)1 2.993e+00 1.994e+01 5.966e-01 5.016 5.26e-07
#> s(logt)2 -4.429e-01 6.422e-01 5.711e-01 -0.776 0.4380
#> s(logt)3 1.357e+00 3.884e+00 8.188e-01 1.657 0.0974
#> s(logt)4 -8.841e-01 4.131e-01 4.176e-01 -2.117 0.0342
#>
#> n = 686 , number of events = 299 , parameters = 6
#> Log-likelihood = -2606 AIC = 5225 BIC = 5247The summary keeps the interpretable covariate hazard ratio
(hormon) separate from the baseline spline coefficients,
which describe the shape of the baseline hazard and are not individually
interpretable, matching rstpm2/flexsurv
convention.
predict(fit, newdata = data.frame(hormon = c(0, 1)), times = c(500, 1000, 2000), type = "survival")
#> id time est
#> 1 1 500 0.829
#> 2 1 1000 0.628
#> 3 1 2000 0.408
#> 4 2 500 0.878
#> 5 2 1000 0.724
#> 6 2 2000 0.536
plot(fit, newdata = data.frame(hormon = c(0, 1)),
col = c("steelblue", "firebrick"), main = "rpsurv: predicted survival")predict() also returns type = "hazard",
"cumhaz", or "link" (the linear predictor),
with se.fit = TRUE adding delta-method 95% confidence
limits for "survival", "cumhaz", and
"link".
A covariate’s value is fixed for a subject, but its
coefficient can be allowed to change over follow-up via its own
spline in log time, requested with tve:
fit_tve <- rpsurv(Surv(rectime, censrec) ~ hormon, data = brcancer,
df = 4, tve = "hormon", tve.df = 2)
coef(fit_tve)A likelihood-ratio test of fit_tve against
fit (both maximum-likelihood fits, nested models) tests the
proportional-hazards assumption for hormon directly.
This is distinct from a time-varying covariate,
where the covariate’s value itself changes during follow-up.
That is a data-representation question, not a modeling one: split each
subject’s follow-up into intervals over which the covariate is constant
and supply counting-process data via
Surv(start, stop, status). rpsurv detects the
3-column Surv object and fits the left-truncated likelihood
automatically, correctly accounting for each interval’s dependence on
survival to its start time under the covariate value of the
previous interval. The same mechanism also handles ordinary
left truncation (delayed entry) when covariate values don’t change. See
vignette("rpsurv") for a full worked example and the
underlying likelihood.
residuals(fit, type = "coxsnell") # also "martingale", "deviance"
coxsnell_plot(fit) # Cox-Snell residual plot
km_compare_plot(fit) # fitted vs. Kaplan-Meier overlayCoefficients, standard errors, and log-likelihoods agree with
rstpm2::stpm2 to 4-5 decimal places across all three scales
(hazard, odds, normal) and with
flexsurv::flexsurvspline on the hazard scale. See
tests/testthat/test-rpsurv.R for the full parity suite,
which also covers left truncation.
rstpm2 and flexsurvWall-clock fit time on simulated Weibull-hazard data with two
covariates, reproducible via data-raw/benchmark.R (shipped
with the package source, not installed).
flexsurv::flexsurvspline is materially slower per fit, so
it was only run up to n=2e4 to keep the benchmark tractable;
rstpm2::stpm2 was run up to n=2e5.
| n | rpsurv (s) | rstpm2 (s) | flexsurvspline (s) |
|---|---|---|---|
| 1,000 | 0.031 | 0.017 | 0.121 |
| 5,000 | 0.037 | 0.058 | 0.636 |
| 20,000 | 0.080 | 0.238 | 2.330 |
| 50,000 | 0.165 | 1.290 | - |
| 100,000 | 0.247 | 2.410 | - |
| 200,000 | 0.413 | 4.912 | - |
| 500,000 | 1.182 | - | - |
At n=200,000, rpsurv is about 12x faster than
rstpm2::stpm2 and continues to scale to n=500,000
(rstpm2 was not run at that size). At n=20,000,
rpsurv is about 29x faster than
flexsurv::flexsurvspline. The advantage widens with n in
both comparisons: rpsurv does O(n) work per optimizer step
in a single fused parallel pass over the log-likelihood and its analytic
gradient, computed together (RcppParallel::parallelReduce),
against repeated R-level likelihood evaluation (numerical gradients, or
gradients built from R-level matrix algebra) in the other two packages.
See vignette("rpsurv") for the full benchmark code and what
else is delegated to C++ (the spline basis itself, and closed-form
starting values from a bounded Kaplan-Meier subsample).
rpsurv ships three datasets, each chosen for a specific
feature of the package rather than as a generic example:
brcancer: German breast cancer recurrence data (686
rows), the baseline fitting example used throughout this README and the
vignette. Identical to rstpm2::brcancer, bundled so
examples do not require rstpm2 to be installed.veteran: Veterans’ Administration lung cancer trial
(137 rows). celltype is a well-known
non-proportional-hazards effect, which makes this a direct illustration
of the tve argument. Identical to
survival::veteran.heart: Stanford heart transplant data (172 rows),
already in counting-process (start, stop,
event) format with a time-varying transplant
covariate. Illustrates Surv(start, stop, status) fitting on
a real dataset rather than a constructed one. Identical to
survival::heart.scale selects the transformation of the survival
function the spline models: "hazard" (proportional hazards,
the Royston-Parmar default), "odds" (proportional odds), or
"normal" (probit).default_knots()), matching
rstpm2/flexsurv conventions.SystemRequirements: GNU make (for
RcppParallel); no other external dependencies.