Skip to contents

Computes restricted mean survival time (RMST), the area under a fitted ertte model's survival curve up to one or more fixed horizons tau, with confidence intervals.

Usage

ertte_rmst(object, newdata = NULL, tau, conf_level = 0.95, ...)

# S3 method for class 'ertte_aft'
ertte_rmst(object, newdata = NULL, tau, conf_level = 0.95, ...)

# S3 method for class 'ertte_coxph'
ertte_rmst(object, newdata = NULL, tau, conf_level = 0.95, ...)

Arguments

object

An ertte model, as returned by ertte_aft() or ertte_coxph().

newdata

Data frame containing cases to be predicted. Defaults to the data the model was fitted to.

tau

Numeric vector of restriction horizons at which to compute RMST(tau) = integral of S(t) from 0 to tau.

conf_level

Confidence level for the intervals. Defaults to .95.

...

Passed to methods.

Value

A tibble with one row per combination of newdata row and tau, plus fit_rmst, ci_lower, and ci_upper.

Details

ertte_rmst() reduces a TTE endpoint to a scalar exposure-response value – restricted mean survival time, the area under the survival curve up to a fixed horizon tau – the other scalar reduction the package's design issue mentions alongside landmark-binary (see ertte_landmark()). Unlike ertte_landmark(), ertte_rmst() is a generic (not a thin wrapper around ertte_predict()): computing an area under the curve genuinely needs engine-specific logic (the whole curve, not a single time point), so there are per-engine methods – see ertte_rmst.ertte_aft() and ertte_rmst.ertte_coxph().

Confidence intervals are symmetric Wald intervals on the RMST scale (fit_rmst +/- z * se_rmst) for both engines. This is not automatically bounded to [0, tau] the way ertte_predict()'s survival-probability intervals are bounded to [0, 1] by construction (their back-transform through a CDF keeps them there) – an unclipped Wald interval on RMST can, in principle, dip below 0 or exceed tau for small samples or near-boundary cases. conf_level must be a single number between 0 and 1 (inclusive); other values error rather than silently producing a reversed or NaN interval.

A zero-row newdata returns a zero-row tibble with the expected columns for both engines, rather than erroring (see issue #10 for why this needed an explicit guard on the ertte_coxph side).

The ertte_coxph method delegates to survival::survfit(object, newdata, conf.int = conf_level, se.fit = TRUE), the same call ertte_predict.ertte_coxph() uses. Because the fitted baseline hazard (and therefore every covariate-adjusted survival curve) is a right-continuous step function, fit_rmst is an exact finite sum of rectangle areas between consecutive jump times up to tau – not a numerical-quadrature approximation. se_rmst comes from a delta method that reuses the same rectangle/tail-weighted construction as survival:::survmean()'s classic Greenwood-based RMST variance, but with the variance-increment term replaced by increments of the profile-specific std.err(t)^2 returned by survfit()survmean()'s own Greenwood term is based on population-level risk sets shared across every covariate profile, which understates uncertainty for a profile far from the mean covariate values.

If any value of tau exceeds the last observed follow-up time across the whole fitted cohort, ertte_rmst() warns: RMST integrates the entire curve up to tau, so silently assuming survival stays flat beyond the observed range (the same extrapolation convention ertte_predict.ertte_coxph() uses for a single time point) has a larger effect on an area than on a point-in-time prediction.

conf_level = 0/1 are legitimate degenerate endpoints and are supported here directly, since the delta-method interval is built from z_scale (qnorm()-derived, 0 or Inf at these boundaries) rather than survfit()'s own conf.int machinery – the latter is only used to request $surv/ $std.err, which don't depend on the requested confidence level (see issue #11); survfit() is always called with a fixed, valid placeholder value internally.

A zero-row newdata returns a zero-row tibble with the expected columns rather than erroring: survival::survfit() itself rejects an entirely-missing newdata with a cryptic "all rows of newdata have missing values" error (see issue #10).

AFT method

The ertte_aft method computes fit_rmst by numerically integrating the closed-form survival function `S(t|x) = 1 - F((log(t)

  • mu) / scale)from 0 totauvia [stats::integrate()], wheremu(and its standard error) comes frompredict(object, newdata, type = "linear", se.fit = TRUE), matching [ertte_predict.ertte_aft()]. The standard error is an analytic delta method that differentiates under the integral sign: d/dmu RMST(tau|x) = integral of dbase(z) / scale from 0 to tau, where dbaseis the base distribution's density -- propagating onlyVar(mu), not Var(scale), the same simplification ertte_predict.ertte_aft()` already makes for its own confidence intervals.

Integrating on the log-time scale

Both integrals are actually evaluated on the u = log(t) scale (substituting t = exp(u), dt = exp(u) du) rather than directly over t in [0, tau]: for a tau many orders of magnitude larger than the fitted model's natural timescale, stats::integrate()'s adaptive quadrature can silently fail on the raw time scale – returning 0 with no error or warning, since the interval [0, tau] is enormous relative to the (comparatively tiny) region where S(t|x) actually differs from 0 (see issue #12). On the log scale, the upper integration bound is log(tau), which grows only logarithmically with tau; the substituted integrand (S(t|x) * t / (dbase(z) / scale) * t, as a function of u) decays fast enough for every supported distribution that adaptive quadrature stays numerically reliable even for absurdly large tau. This changes nothing for ordinary tau values (confirmed numerically to agree with the untransformed integral to quadrature tolerance).

Reliability for extreme tau

Even with this fix, stats::integrate() isn't unconditionally reliable for arbitrarily extreme tau: fit_rmst stays accurate to quadrature tolerance for tau many orders of magnitude beyond the fitting data's own follow-up range, but the delta-method gradient behind se_rmst was found (empirically, not from a general proof) to occasionally become unreliable somewhat sooner – the density-like integrand it evaluates is a much narrower "bump" than the broad-plateau survival curve fit_rmst integrates, and is correspondingly harder for adaptive quadrature to reliably locate once the integration domain is stretched far enough. ertte_rmst() warns (rather than silently risking an unreliable interval) if any tau exceeds 10,000 times the last observed follow-up time in the fitting data – a threshold with a wide empirical safety margin below where any instability was actually observed, not a hard numerical guarantee.

Examples

mod <- ertte_aft(Surv(time, event) ~ aucss, ertte_data)
ertte_rmst(mod, ertte_data[1:5, ], tau = c(60, 90))
#> # A tibble: 10 × 15
#>       id sex      age weight  dose treatment aucss cmaxss  time event
#>    <int> <fct>  <int>  <dbl> <dbl> <fct>     <dbl>  <dbl> <dbl> <dbl>
#>  1     1 Female    27     70   200 Drug      1114.  187.   77.4     1
#>  2     1 Female    27     70   200 Drug      1114.  187.   77.4     1
#>  3     2 Female    27     59   100 Drug       561.   49.1  26.8     0
#>  4     2 Female    27     59   100 Drug       561.   49.1  26.8     0
#>  5     3 Female    24     65     0 Placebo      0     0   180       0
#>  6     3 Female    24     65     0 Placebo      0     0   180       0
#>  7     4 Female    29     63     0 Placebo      0     0    16.8     0
#>  8     4 Female    29     63     0 Placebo      0     0    16.8     0
#>  9     5 Male      27     91   200 Drug      1416.  143.   33.9     1
#> 10     5 Male      27     91   200 Drug      1416.  143.   33.9     1
#> # ℹ 5 more variables: admin_censor <dbl>, tau <dbl>, fit_rmst <dbl>,
#> #   ci_lower <dbl>, ci_upper <dbl>

mod_cox <- ertte_coxph(Surv(time, event) ~ aucss, ertte_data)
ertte_rmst(mod_cox, ertte_data[1:5, ], tau = c(60, 90))
#> # A tibble: 10 × 15
#>       id sex      age weight  dose treatment aucss cmaxss  time event
#>    <int> <fct>  <int>  <dbl> <dbl> <fct>     <dbl>  <dbl> <dbl> <dbl>
#>  1     1 Female    27     70   200 Drug      1114.  187.   77.4     1
#>  2     1 Female    27     70   200 Drug      1114.  187.   77.4     1
#>  3     2 Female    27     59   100 Drug       561.   49.1  26.8     0
#>  4     2 Female    27     59   100 Drug       561.   49.1  26.8     0
#>  5     3 Female    24     65     0 Placebo      0     0   180       0
#>  6     3 Female    24     65     0 Placebo      0     0   180       0
#>  7     4 Female    29     63     0 Placebo      0     0    16.8     0
#>  8     4 Female    29     63     0 Placebo      0     0    16.8     0
#>  9     5 Male      27     91   200 Drug      1416.  143.   33.9     1
#> 10     5 Male      27     91   200 Drug      1416.  143.   33.9     1
#> # ℹ 5 more variables: admin_censor <dbl>, tau <dbl>, fit_rmst <dbl>,
#> #   ci_lower <dbl>, ci_upper <dbl>