Skip to contents

This vignette describes the practical contract used by SuperSurv() to call survival learners and screening methods. The goal is to make small extensions possible without changing the ensemble algorithm.

How Learners Are Found

Learners are supplied to SuperSurv() by name:

event.library <- c("surv.coxph", "my.surv.km")

During cross-validation and final fitting, SuperSurv() looks up each name as an R function and calls it directly. A custom learner can therefore live in a user script, package, or analysis file, as long as it is available in the R session before calling SuperSurv().

Learner Wrapper Contract

A survival learner wrapper should have this shape:

my.surv.learner <- function(time, event, X, newdata, new.times,
                            obsWeights, id, ...) {
  # fit model on X, time, event
  # predict survival probabilities for newdata at new.times
  list(pred = pred_matrix, fit = fitted_object)
}

The required arguments are:

Argument Meaning
time numeric observed follow-up time
event event indicator, with 1 for the event and 0 for censoring
X training covariates as a data frame
newdata covariates for prediction as a data frame
new.times time grid where survival probabilities are needed
obsWeights observation weights, possibly NULL
id optional cluster or subject identifier, possibly NULL
... learner-specific tuning parameters

The returned pred must be a numeric matrix with:

  • one row per row of newdata,
  • one column per value of new.times,
  • entries on the survival-probability scale, usually between 0 and 1.

If possible, each row should be non-increasing across time. Wrappers commonly clamp small numerical drift back into [0, 1].

The returned fit can be any object needed for future prediction. When control = list(saveFitLibrary = TRUE), SuperSurv() stores these fitted objects and predict.SuperSurv() later calls predict() on them.

SuperSurv validates this contract at each cross-validation fold, during the full-data refit, and when predicting on new data. If a custom learner returns the wrong object type, omits pred or a required saved fit, returns a matrix with the wrong dimensions, or produces non-finite values or probabilities outside [0, 1], the error identifies the learner, fitting stage, and expected shape. Screener outputs are similarly required to be complete logical vectors with one entry per input feature and at least one selected feature.

Prediction Method Contract

If a custom learner should support prediction on new data after fitting, store a classed object in fit and define a matching S3 prediction method:

predict.my.surv.learner <- function(object, newdata, new.times, ...) {
  pred_matrix
}

This method should return the same type of matrix as the wrapper’s pred component: nrow(newdata) rows by length(new.times) columns.

Direct Survival Curves vs Risk Scores

Some survival learners directly return survival curves. In that case, the wrapper usually interpolates the native prediction grid to new.times.

Other learners return a risk score or linear predictor. Those wrappers need one extra calibration step:

  1. fit the risk-score model,
  2. estimate a baseline cumulative hazard on the training data,
  3. predict risk scores for newdata,
  4. convert scores to survival curves with S(t | x) = exp(-exp(eta(x)) H0(t)).

Several built-in Cox-style wrappers follow this pattern. The internal helper safe_breslow_step() is used by some package wrappers, but it is not exported; custom wrappers should either use a standard package prediction method that already returns survival curves or implement their own baseline-hazard calibration carefully.

Optional Additional Learners

The following adapters use the same wrapper contract and remain optional:

Wrapper Backend Prediction route Observation weights
surv.mboost mboost risk score plus weighted baseline-hazard calibration supported
surv.flexsurvspline flexsurv native flexible-parametric survival curve supported
surv.flexsurvreg flexsurv native parametric survival curve, including generalized gamma supported
surv.survPen survPen native penalized smooth hazard survival curve uniform only
surv.grf grf native generalized random-forest survival curve supported
surv.deepsurv survivalmodels and Python pycox native Cox neural-network survival curve uniform only
surv.deephit survivalmodels and Python pycox native discrete-time neural-network survival curve uniform only
surv.coxtime survivalmodels and Python pycox native neural survival curve with time-dependent effects uniform only

The DeepSurv, DeepHit, and Cox-Time adapters remain experimental; local smoke tests do not establish portability across R/Python configurations. Their backend spans R and Python. They require survivalmodels, reticulate, and importable Python modules torch, torchtuples, and pycox. SuperSurv never installs or changes a Python environment automatically. These adapters stop with an explicit error when the backend is unavailable or when nonuniform obsWeights are supplied; silently ignoring case weights would change the requested fit.

Python fitted objects can be reused within the active R/Python session, but plain saveRDS() is not a portable way to store them. Prediction after restoring an invalid Python reference gives an explicit error. Native neural curves are mapped as right-continuous steps to requested times; extending the grid past observed follow-up does not establish reliable extrapolation.

All of these wrappers can be tuned through create_grid() in the same way as other learners. Test a small direct wrapper fit before launching cross-validation, especially for a Python-backed learner.

surv.survPen uses a smooth baseline log hazard and linear feature effects by default. Supply a one-sided formula for nonlinear or time-varying effects, using .supersurv_time for follow-up time. For example:

# X contains x1 and x2. Use screen.all if naming features explicitly.
smooth_fit <- surv.survPen(
  time, event, X, newdata, new.times,
  formula = ~ tensor(.supersurv_time, x1, df = c(4, 4)) + x2
)

The smooth-model adapter rejects nonuniform observation weights because its backend does not expose case-weighted fitting. It is restricted to overall single-event, right-censored survival, not relative survival or left truncation. The quadrature order n.legendre controls numerical integration accuracy.

Minimal Custom Learner

The following learner ignores covariates and fits a weighted Kaplan-Meier curve. It is intentionally simple, but it satisfies the full SuperSurv() learner contract.

my.surv.km <- function(time, event, X, newdata, new.times,
                       obsWeights = NULL, id = NULL, ...) {
  if (is.null(obsWeights)) {
    obsWeights <- rep(1, length(time))
  }

  fit_km <- survival::survfit(
    survival::Surv(time, event) ~ 1,
    weights = obsWeights
  )

  step_surv <- stats::stepfun(fit_km$time, c(1, fit_km$surv), right = FALSE)
  surv_probs <- step_surv(new.times)

  pred <- matrix(
    surv_probs,
    nrow = nrow(newdata),
    ncol = length(new.times),
    byrow = TRUE
  )

  fit <- list(object = fit_km)
  class(fit) <- "my.surv.km"

  list(pred = pred, fit = fit)
}

predict.my.surv.km <- function(object, newdata, new.times, ...) {
  fit_km <- object$object
  step_surv <- stats::stepfun(fit_km$time, c(1, fit_km$surv), right = FALSE)
  surv_probs <- step_surv(new.times)

  matrix(
    surv_probs,
    nrow = nrow(newdata),
    ncol = length(new.times),
    byrow = TRUE
  )
}

You can test the wrapper outside the ensemble before adding it to a library:

data("metabric", package = "SuperSurv")
dat <- metabric[1:40, ]
x_cols <- grep("^x", names(dat))[1:3]
X <- dat[, x_cols, drop = FALSE]
times <- seq(50, 150, by = 50)

wrapper_out <- my.surv.km(
  time = dat$duration,
  event = dat$event,
  X = X,
  newdata = X[1:5, , drop = FALSE],
  new.times = times
)

dim(wrapper_out[["pred"]])
#> [1] 5 3
dim(predict(wrapper_out[["fit"]], newdata = X[1:5, , drop = FALSE], new.times = times))
#> [1] 5 3

The custom learner can be mixed with built-in learners:

fit <- SuperSurv(
  time = train$duration,
  event = train$event,
  X = X_train,
  newdata = X_test,
  new.times = seq(50, 300, by = 50),
  event.library = c("surv.coxph", "my.surv.km"),
  cens.library = c("surv.coxph"),
  control = list(saveFitLibrary = TRUE)
)

event_weights(fit)

Screening Method Contract

Screening methods are also supplied by name. A screener should accept the same basic training inputs and return a logical vector with one value per column of X:

my.screen.first3 <- function(time, event, X, obsWeights = NULL, id = NULL, ...) {
  keep <- rep(FALSE, ncol(X))
  keep[seq_len(min(3, ncol(X)))] <- TRUE
  names(keep) <- names(X)
  keep
}

Use a list entry to pair a learner with one or more screeners:

fit <- SuperSurv(
  time = train$duration,
  event = train$event,
  X = X_train,
  newdata = X_test,
  new.times = seq(50, 300, by = 50),
  event.library = list(c("surv.coxph", "screen.all", "my.screen.first3")),
  cens.library = c("surv.coxph")
)

selected_variables(fit, learner = 2)

Development Checklist

Before using a custom wrapper in a large ensemble, check:

  • pred is a numeric matrix with dimensions nrow(newdata) by length(new.times),
  • predictions are on the survival-probability scale,
  • all prediction entries are finite and in [0, 1],
  • predict(wrapper_out[["fit"]], newdata, new.times) returns the same shape when saved fits are needed,
  • the wrapper tolerates obsWeights = NULL and id = NULL,
  • all required packages are checked with requireNamespace(),
  • any screener returns a logical vector aligned with the columns of X.

These rules are deliberately small. They allow new learners to be added without changing the SuperSurv ensemble machinery.