Conditional Naive Bayes Survival Modelling for Right-Censored Data

Fits conditional naive Bayes survival models for right-censored outcomes using inverse-probability of censoring weighting. The package provides model fitting, prediction, resampling-based evaluation, cross-validation, hyper-parameter tuning, and permutation variable importance utilities for horizon-specific survival prediction. The model is the censored naive Bayes classifier of Wolfson et al. (2015) , which combines the marginal Kaplan-Meier survivor function with horizon-specific class-conditional covariate densities and inverse-probability-of-censoring weights. Resampling evaluation uses the inverse-probability-of-censoring-weighted Brier score of Gerds and Schumacher (2006) .


nbsurv

R-CMD-check License: GPL-3

nbsurv fits conditional naive Bayes survival models for right-censored time-to-event data. The package focuses on horizon-specific survival prediction with inverse-probability of censoring weighting, and includes tools for model fitting, prediction, evaluation, cross-validation, hyper-parameter tuning, and permutation variable importance.

All results below are real output from lung (from the survival package, rows with missing predictors dropped).

Main workflow

library(nbsurv)
library(survival)

lung <- na.omit(lung)
lung$status <- as.integer(lung$status == 2)

fit <- nbsurv(
  Surv(time, status) ~ age + sex + ph.ecog,
  data = lung
)

surv_prob <- predict(fit, newdata = lung[1:5, ], times = c(100, 200, 400))
round(surv_prob, 3)
#>   t_100 t_200 t_400
#> 1 0.839 0.717 0.449
#> 2 0.919 0.708 0.381
#> 3 0.747 0.427 0.252
#> 4 0.801 0.692 0.405
#> 5 0.676 0.534 0.275

event_prob <- predict(fit, newdata = lung[1:5, ], times = c(100, 200, 400), type = "event")
round(event_prob, 3)
#>   t_100 t_200 t_400
#> 1 0.161 0.283 0.551
#> 2 0.081 0.292 0.619
#> 3 0.253 0.573 0.748
#> 4 0.199 0.308 0.595
#> 5 0.324 0.466 0.725

plot(fit, times = c(50, 100, 200, 400, 600, 800))

Evaluation

metrics <- evaluate_nbsurv(
  fit,
  newdata = lung,
  times = c(100, 200, 400)
)

metrics
#>   time     brier concordance
#> 1  100 0.1249025   0.5176070
#> 2  200 0.2299579   0.5055850
#> 3  400 0.2454511   0.4982014

evaluate_nbsurv() currently returns:

  • IPCW Brier score
  • Concordance

for each requested prediction horizon.

Cross-validation

cv_fit <- cv_nbsurv(
  Surv(time, status) ~ age + sex + ph.ecog,
  data = lung,
  folds = 3,
  times = c(100, 200, 400),
  seed = 1
)

cv_fit$summary
#>   time     brier concordance
#> 1  100 0.1233948   0.5029725
#> 2  200 0.2305867   0.4899689
#> 3  400 0.2683234   0.4907712

plot(cv_fit)

Hyper-parameter tuning

grid <- data.frame(
  scale = c(TRUE, FALSE),
  laplace = c(1, 2),
  min_sd = c(0.05, 0.10)
)
grid$time_grid <- I(list(NULL, NULL))

tuned <- tune_nbsurv(
  Surv(time, status) ~ age + sex + ph.ecog,
  data = lung,
  param_grid = grid,
  folds = 3,
  times = c(100, 200, 400),
  seed = 1
)

tuned$best_params
#>   scale laplace min_sd time_grid mean_metric
#> 1  TRUE       1   0.05              0.207435

plot(tuned)

Variable importance

vi <- varimp_nbsurv(
  fit,
  newdata = lung,
  times = c(100, 200, 400),
  n_repeats = 10,
  seed = 1
)

vi
#> nbsurv permutation variable importance
#> Metric: brier | repeats: 10
#>
#>  feature  baseline  permuted   importance
#>      age 0.2001038 0.2066950  0.006591213
#>  ph.ecog 0.2001038 0.2047293  0.004625528
#>      sex 0.2001038 0.1973498 -0.002753981

plot(vi)

age and ph.ecog degrade the Brier score the most when permuted (most important); sex slightly improves it when shuffled here, i.e. it contributes essentially nothing beyond noise on this fit/horizon set.

calibration_plot_nbsurv(fit, newdata = lung, horizon = 200) is also available for visual calibration assessment at a single horizon.

Benchmark vs. coxph

A quick, honest comparison against survival::coxph() on the same formula: 20 random 70/30 train/test splits of lung, IPCW Brier score and concordance computed identically for both models (nbsurv's own internal metric functions, applied to coxph's linear predictor and survfit()-derived survival probabilities too), means reported below.

#>   time nb_brier cox_brier nb_conc cox_conc
#> 1  100   0.1312    0.1304  0.6011   0.6294
#> 2  200   0.2051    0.1950  0.6107   0.6294
#> 3  400   0.2397    0.2344  0.5792   0.6294

coxph edges out nbsurv on both metrics at every horizon on this dataset - expected: age + sex + ph.ecog have a roughly log-linear, non-interacting effect on the hazard here, which is exactly what a correctly-specified Cox model is built for, while nbsurv's conditional-independence assumption between predictors costs it some accuracy in exchange for a much simpler, distribution-light estimation procedure. The gap is real but small (concordance within ~0.03-0.05, Brier within ~0.01-0.05).

cov_structure = "full" (below) relaxes that independence assumption between continuous predictors, but on this dataset (age/ph.ecog correlated at only r=0.31, n=167) it doesn't close the gap - it's a genuine, validated improvement when continuous predictors are more strongly correlated (see below), not a guaranteed win on every dataset.

Relaxing the independence assumption: cov_structure = "full"

By default, continuous predictors are modeled as independent given the class (survivor/event) - the classic naive Bayes assumption. cov_structure = "full" instead models them jointly as a single multivariate Gaussian per class, capturing correlation between them:

fit_full <- nbsurv(
  Surv(time, status) ~ age + sex + ph.ecog,
  data = lung,
  cov_structure = "full"
)

On a synthetic case with two continuous predictors correlated at r=0.85 (20 held-out 70/30 splits), "full" gives a consistent ~5-6% relative improvement in Brier score over "diagonal" at every horizon tested; its effect on concordance is small and inconsistent even at that correlation strength, since concordance only depends on the ranking of risk scores, which the naive product often preserves even when miscalibrated. This is a real, tested effect (tests/testthat/test-cov-structure.R) - but whether it helps on your data depends on how correlated your continuous predictors actually are.

Borrowing strength across horizons: time_smooth = TRUE

The remaining inefficiency vs. coxph: every horizon is fit completely independently by default, discarding the fact that the true class-conditional statistics should vary smoothly with time. This costs accuracy right where one of the two horizon-defined classes (already failed vs. known to survive) is smallest - typically near the earliest or latest requested horizon. time_smooth = TRUE precomputes class-conditional statistics at every point of the internal time_grid once at fit time, then blends them at predict() time via kernel regression across nearby horizons:

fit_smooth <- nbsurv(
  Surv(time, status) ~ age + sex + ph.ecog,
  data = lung,
  time_smooth = TRUE,
  bandwidth = 300  # tune via tune_nbsurv() for your data
)

Re-running the coxph benchmark above with time_smooth = TRUE at a few bandwidths (still 20 held-out 70/30 splits):

#>                  brier (t=100,200,400)   concordance (t=100,200,400)
#> diagonal         0.1312, 0.2051, 0.2397  0.601, 0.611, 0.579
#> smooth (bw=101)  0.1284, 0.2030, 0.2413  0.601, 0.613, 0.608
#> smooth (bw=200)  0.1273, 0.2023, 0.2463  0.608, 0.613, 0.618
#> smooth (bw=500)  0.1265, 0.2013, 0.2528  0.614, 0.614, 0.617
#> coxph            0.1304, 0.1950, 0.2344  0.629, 0.629, 0.629

A genuine bias-variance tradeoff, confirmed rather than assumed: wider bandwidths consistently improve concordance (closing roughly half to two-thirds of the gap to coxph) but consistently worsen Brier score at the most extreme horizon (t=400), the expected cost of over-smoothing away real horizon-specific calibration. No single bandwidth wins on both metrics at every horizon - tune bandwidth via tune_nbsurv()'s param_grid for your own data rather than trusting the default.

Even at the best bandwidths tried here, the gap to coxph narrows but does not fully close. age/ph.ecog have close-to-log-linear, non-interacting effects on this dataset - exactly what a correctly-specified Cox model is built to exploit. nbsurv's value proposition is flexibility (genuinely time-varying effects a fixed-coefficient Cox model can't represent), not guaranteed accuracy dominance on every dataset - reported honestly here rather than oversold.

Notes

  • predict() returns monotone survival curves by applying a cumulative minimum across increasing horizons.
  • Continuous predictors are modeled with Gaussian class-conditional densities (independent by default, or jointly via cov_structure = "full").
  • Categorical predictors use Laplace-smoothed probabilities.
  • The package exposes a single clean public API centered on nbsurv(), predict(), evaluate_nbsurv(), cv_nbsurv(), tune_nbsurv(), and varimp_nbsurv().

Reference manual

It appears you don't have a PDF plugin for this browser. You can click here to download the reference manual.

install.packages("nbsurv")

0.5.1 by Imad El Badisy, a month ago


https://github.com/ielbadisy/nbsurv


Report a bug at https://github.com/ielbadisy/nbsurv/issues


Browse source code at https://github.com/cran/nbsurv


Authors: Imad El Badisy [aut, cre]


Documentation:   PDF Manual  


GPL-3 license


Imports graphics, survival, stats, utils

Suggests knitr, pkgload, pec, quarto, ranger, rmarkdown, testthat


See at CRAN