---
title: "10. Extending SuperSurv"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{10. Extending SuperSurv}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

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:

```r
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:

```r
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:

```r
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:

```{r smooth-hazard-example, eval=FALSE}
# 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.

```{r custom-learner}
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:

```{r custom-learner-check}
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"]])
dim(predict(wrapper_out[["fit"]], newdata = X[1:5, , drop = FALSE], new.times = times))
```

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

```{r custom-fit, eval=FALSE}
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`:

```r
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:

```{r custom-screener, eval=FALSE}
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.
