survalis: An Interpretable Survival Machine Learning Framework in R

One fit/predict contract across 19 survival learners, with benchmarking, calibration, and model-agnostic interpretation built in

Imad El Badisy

2026-03-30

Comparing survival models in R usually means learning a different interface for each one: coxph(), randomForestSRC::rfsrc(), xgboost::xgb.train(), and a deep survival network all expect different data shapes and return different prediction objects. survalis was built to close that gap: a unified framework for survival machine learning with a consistent contract for fitting, prediction, evaluation, and interpretation.

Core philosophy

Every learner in survalis follows the same pattern:

Why explicit fit_coxph() / fit_rsf() instead of one fit(learner = ...)

This is a deliberate departure from funcml, my general-purpose ML package, where fit(y ~ ., data, learner = "ranger") collapses every learner behind one verb and a string argument. survalis keeps a named function per learner, fit_coxph(), fit_rsf(), fit_survdnn(), and so on, instead of a single dispatcher.

The reason is that the survival setting is not just “regression with a different loss.” Every learner here already has to negotiate right-censoring, risk sets, and time-varying prediction targets on its own terms, and each one leans on a different underlying package (survival, randomForestSRC, xgboost, torch, flexsurv, …) with its own quirks around ties, time horizons, and baseline hazard estimation. Hiding fit_coxph and fit_rsf behind a shared fit(learner = "rsf") string would trade a small amount of typing for a layer of indirection between the call site and the model-specific machinery it actually invokes, machinery that, in survival analysis, is rarely interchangeable enough to be worth hiding. Keeping the learner name in the function name is a compactness choice: the API surface is larger (nineteen fit_*()/predict_*() pairs instead of one), but each call is self-describing and there is one fewer level of abstraction between what you write and what runs.

What survalis does unify is everything after fitting: predict_*() always returns the same survival-probability matrix shape, so cindex_survmat(), brier(), cv_survlearner(), and benchmark() never need to know which fit_*() produced it. The genericity lives at the evaluation/interpretation layer, not at the fitting layer.

That output shape was a deliberate constraint, not an incidental one. A Cox model naturally hands back a linear predictor or a hazard ratio; a random survival forest hands back an ensemble of terminal-node curves; a deep survival network hands back whatever its loss was trained on (a Cox partial likelihood, an AFT loss, a discrete-time hazard). fit_survdnn() itself can predict a linear predictor if asked, since its loss functions include a Cox-style formulation. survalis standardizes on the survival-probability matrix anyway, over times t=100, t=200, and so on, because a matrix is, by construction, a richer object than a scalar: a linear predictor collapses a subject’s entire risk trajectory into one number and one implicit ranking, while a survival curve preserves how that risk actually unfolds over the follow-up window, which is what a fixed-horizon metric, a time-varying AUC, or a calibration curve each need to read back out at a different point. Forcing every predict_*() to that one shape throws away learner-specific detail on the way out (a Cox model’s clean linear score, a forest’s raw ensemble votes), but it is exactly what makes cindex_survmat(), brier(), timeroc_survmat(), and every interpretation function in the package learner-agnostic, without asking any of them to reconstruct a survival curve from a scalar first. The alternative, keeping each learner’s native output format and writing metric functions that branch on model class, is the design survalis specifically avoids.

Nineteen learners, one interface

library(survalis)
list_survlearners()
            learner                 fit                 predict
             <char>              <char>                  <char>
 1:           coxph           fit_coxph           predict_coxph
 2:           aalen           fit_aalen           predict_aalen
 3:          glmnet          fit_glmnet          predict_glmnet
 4:       selectcox       fit_selectcox       predict_selectcox
 5:          aftgee          fit_aftgee          predict_aftgee
 6:     flexsurvreg     fit_flexsurvreg     predict_flexsurvreg
 7:           stpm2           fit_stpm2           predict_stpm2
 8:         bnnsurv         fit_bnnsurv         predict_bnnsurv
 9:           rpart           fit_rpart           predict_rpart
10:            bart            fit_bart            predict_bart
11:         xgboost         fit_xgboost         predict_xgboost
12:          ranger          fit_ranger          predict_ranger
13:             rsf             fit_rsf             predict_rsf
14:         cforest         fit_cforest         predict_cforest
15:      blackboost      fit_blackboost      predict_blackboost
16:         survsvm         fit_survsvm         predict_survsvm
17:         survdnn         fit_survdnn         predict_survdnn
18:            orsf            fit_orsf            predict_orsf
19: survmetalearner fit_survmetalearner predict_survmetalearner
                tune has_fit has_predict has_tune available
              <char>  <lgcl>      <lgcl>   <lgcl>    <lgcl>
 1:             <NA>    TRUE        TRUE    FALSE      TRUE
 2:             <NA>    TRUE        TRUE    FALSE      TRUE
 3:      tune_glmnet    TRUE        TRUE     TRUE      TRUE
 4:   tune_selectcox    TRUE        TRUE     TRUE      TRUE
 5:             <NA>    TRUE        TRUE    FALSE      TRUE
 6: tune_flexsurvreg    TRUE        TRUE     TRUE      TRUE
 7:             <NA>    TRUE        TRUE    FALSE      TRUE
 8:     tune_bnnsurv    TRUE        TRUE     TRUE      TRUE
 9:       tune_rpart    TRUE        TRUE     TRUE      TRUE
10:        tune_bart    TRUE        TRUE     TRUE      TRUE
11:     tune_xgboost    TRUE        TRUE     TRUE      TRUE
12:      tune_ranger    TRUE        TRUE     TRUE      TRUE
13:         tune_rsf    TRUE        TRUE     TRUE      TRUE
14:     tune_cforest    TRUE        TRUE     TRUE      TRUE
15:  tune_blackboost    TRUE        TRUE     TRUE      TRUE
16:     tune_survsvm    TRUE        TRUE     TRUE      TRUE
17:     tune_survdnn    TRUE        TRUE     TRUE      TRUE
18:        tune_orsf    TRUE        TRUE     TRUE      TRUE
19:             <NA>    TRUE        TRUE    FALSE      TRUE

That list spans semiparametric (coxph, aalen), parametric (flexsurvreg, stpm2, aftgee), tree-based and ensemble (rpart, rsf, cforest, orsf), boosting (xgboost, blackboost), kernel (survsvm), and deep-learning (survdnn, bnnsurv) learners, plus survmetalearner for combining several of them. Most also ship a matching tune_*() function for grid or random hyperparameter search.

A minimal example

library(survival)

times <- c(90, 180, 270)

mod <- fit_coxph(Surv(time, status) ~ age + karno + celltype, data = veteran)
sp <- predict_coxph(mod, newdata = veteran, times = times)

head(sp)
       t=90      t=180       t=270
1 0.6640601 0.37822072 0.257579996
2 0.7361432 0.48310054 0.362410259
3 0.6104309 0.30966354 0.194867900
4 0.6541127 0.36490330 0.245015675
5 0.7375010 0.48521958 0.364629899
6 0.1897037 0.01929484 0.004055248
times_fine <- seq(0, 300, by = 10)
sp_fine <- predict_coxph(mod, newdata = veteran[1:8, ], times = times_fine)

p <- plot_survmat(sp_fine)
ggplot2::ggsave("featured.png", p, width = 7, height = 4.5, dpi = 150)
p

Discrimination and calibration are single function calls away, at a fixed horizon or cross-validated across folds:

y <- Surv(veteran$time, veteran$status)

c(
  cindex = cindex_survmat(y, sp, t_star = 180),
  brier  = survalis::brier(y, sp[["t=180"]], t_star = 180)
)
cindex.C index    brier.brier 
      0.733744       0.134603 

Swap fit_coxph/predict_coxph for any other pair in list_survlearners() and every downstream call, cindex_survmat(), brier(), cv_survlearner(), benchmark(), stays exactly the same.

Beyond prediction: interpretation

Because every learner returns the same survival-probability shape, survalis’s interpretability layer works uniformly across all of them: permutation variable importance (compute_varimp()), accumulated local effects and partial dependence (compute_ale(), compute_pdp()), SHAP-based attribution (compute_shap()), pairwise interaction strength (compute_interactions()), counterfactual recommendations (compute_counterfactual()), and surrogate-tree explanations (compute_surrogate(), compute_tree_surrogate()), each with a matching plot_*() function.

Install

install.packages("survalis")