| Title: | Bayesian EXNEX Models for Survival Analysis in Basket Trials |
|---|---|
| Description: | Implements the Bayesian Exchangeable Non-Exchangeable (EXNEX) framework for right-censored log-normal survival data in basket trials. Based on 'Rcpp' and 'RcppArmadillo', the package provides a fast Gibbs sampler supporting EXNEX, complete pooling, and no pooling models to facilitate methodological comparisons and simulation studies. |
| Authors: | Victor Ney [aut, cre] |
| Maintainer: | Victor Ney <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.4.0 |
| Built: | 2026-10-13 08:14:38 UTC |
| Source: | https://github.com/victorney/exnexsurv |
Computes [compute_waic()] for each supplied fit and reports them in a single 'data.frame', sorted by ascending WAIC.
compare_waic(..., digit = 2)compare_waic(..., digit = 2)
... |
Two or more fitted 'pooling_surv' objects. |
digit |
Number of decimal places for the reported statistics. |
Fits are labelled with the variable names used in the call when supplied unnamed (e.g. 'fit_exnex', 'fit_complete'), or by the names given to '...'; when even those are unavailable, the fitted 'pooling' variant is used, with a final 'fit1'/'fit2' fallback for duplicate or anonymous labels.
A 'data.frame' with one row per model and columns 'model', 'waic', 'se_elpd_waic', 'lpd', 'p_waic', 'elpd_waic'.
Computes the Watanabe-Akaike Information Criterion (WAIC) for a fitted
'pooling_surv' model. WAIC is a fully Bayesian information criterion that uses
the posterior draws of the log-likelihood and adds a penalty for effective
number of parameters .
compute_waic(fit, ...)compute_waic(fit, ...)
fit |
A fitted 'pooling_surv' object. |
... |
Unused. |
For each observation , with posterior draws indexed by
, we compute the pointwise log-likelihood
, where the log-normal AFT model gives
for an observed event ()
and for a censored observation ()
where is the linear predictor built from the draws.
WAIC is then
Rows with extreme leverage ('p_waic' above a large threshold) may flag issues; the function returns a 'pointwise' matrix so users can investigate.
A named list with elements 'waic', 'se_elpd_waic', 'lpd', 'p_waic', 'elpd_waic', and 'pointwise' (an 'n' by 3 matrix with columns 'lpd', 'p_waic', 'waic').
Computes R-hat, bulk effective sample size (ESS), and tail ESS for every parameter of a fitted 'pooling_surv' object, using the 'posterior' package. All three follow the definitions in Vehtari, Gelman, Simpson, Carpenter & Buerkner (2021, "Rank-normalization, folding, and localization"): the R-hat is split and rank-normalized, bulk ESS is computed on rank-normalized draws, and tail ESS quantifies sampling adequacy in the 5 diagnostic is computed (so 'chains = 1' still gets a meaningful R-hat).
convergence_diagnostics(fit, ...)convergence_diagnostics(fit, ...)
fit |
A fitted 'pooling_surv' object (result of 'pooling_surv()'). |
... |
Unused. |
Rules of thumb: R-hat close to 1.0 (say < 1.01) and ESS in the hundreds give reasonably stable summaries. Low ESS with a good R-hat means the chain is slow to move but honest; both R-hat above ~1.01 and very low ESS (e.g. below 100) are worth investigating before trusting the posterior.
A data frame with one row per parameter and columns 'parameter', 'rhat', 'ess_bulk', and 'ess_tail'. All values are 'NA' if the 'posterior' package is not installed.
set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) convergence_diagnostics(fit)set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) convergence_diagnostics(fit)
'exnex_surv()' is deprecated in favour of [pooling_surv()]. It emits a one-line deprecation message and forces 'pooling = "exnex"', so existing calls continue to fit the EXNEX hierarchy unchanged.
exnex_surv(x, ...)exnex_surv(x, ...)
x |
An object containing the predictors (subgroup assignment). Can be a data frame or a formula. |
... |
Additional arguments passed to methods. |
An object of class 'pooling_surv', identical to 'pooling_surv(..., pooling = "exnex")'.
## Not run: fit <- exnex_surv(survival::Surv(time, event) ~ group, data = d) ## End(Not run)## Not run: fit <- exnex_surv(survival::Surv(time, event) ~ group, data = d) ## End(Not run)
For the log-normal AFT model the median survival time for a linear
predictor is , since
when . Posterior draws of and
therefore induce a posterior distribution of whose quantiles
are reported.
median_survival(fit, newdata = NULL, level = 0.95, ...)median_survival(fit, newdata = NULL, level = 0.95, ...)
fit |
A fitted 'pooling_surv' object. |
newdata |
Optional data frame (one row gives one median). |
level |
Credible-interval level (default '0.95'). |
... |
Unused. |
A 'data.frame' with columns 'group', 'median', 'lower', 'upper'.
Generates one bayesplot traceplot per parameter.
## S3 method for class 'pooling_surv' plot(x, parameters = NULL, ask = interactive(), ...)## S3 method for class 'pooling_surv' plot(x, parameters = NULL, ask = interactive(), ...)
x |
A fitted 'pooling_surv' object. |
parameters |
Optional character vector of parameter names to plot. |
ask |
Should R pause between plots? Defaults to 'interactive()'. |
... |
Unused. |
Invisibly returns 'x'.
Draws the posterior median and credible band of the survival function for each group. Requires 'ggplot2'.
## S3 method for class 'survival_exnex' plot(x, ...)## S3 method for class 'survival_exnex' plot(x, ...)
x |
An object of class 'survival_exnex' returned by [survival_curves()]. |
... |
Unused. |
Invisibly returns 'x'.
Fits Bayesian models for right-censored log-normal survival data in basket trials using a data-augmented Gibbs sampler. The 'pooling' argument selects the structural assumption on the basket effects:
pooling_surv(x, ...) ## Default S3 method: pooling_surv(x, ...) ## S3 method for class 'formula' pooling_surv( formula, data, pooling = "exnex", priors = list(), iter = 2000, warmup = 1000, thin = 1, chains = 1, parallel_chains = FALSE, group_col = NULL, seed = NULL, verbose = TRUE, ... ) ## S3 method for class 'data.frame' pooling_surv( x, y, pooling = "exnex", priors = list(), iter = 2000, warmup = 1000, thin = 1, chains = 1, parallel_chains = FALSE, group_col = NULL, seed = NULL, verbose = TRUE, ... )pooling_surv(x, ...) ## Default S3 method: pooling_surv(x, ...) ## S3 method for class 'formula' pooling_surv( formula, data, pooling = "exnex", priors = list(), iter = 2000, warmup = 1000, thin = 1, chains = 1, parallel_chains = FALSE, group_col = NULL, seed = NULL, verbose = TRUE, ... ) ## S3 method for class 'data.frame' pooling_surv( x, y, pooling = "exnex", priors = list(), iter = 2000, warmup = 1000, thin = 1, chains = 1, parallel_chains = FALSE, group_col = NULL, seed = NULL, verbose = TRUE, ... )
x |
An object containing the predictors (subgroup assignment). Can be a data frame or a formula. |
... |
Additional arguments passed to methods. |
formula |
A model formula with structure: 'Surv(time, event) ~ group + covariates'. The first RHS variable is the group/basket assignment. Additional variables are covariates. |
data |
A data frame containing the variables in the formula. |
pooling |
Pooling mode for the basket effects: one of
|
priors |
Optional named list of prior hyperparameters. Supported fields:
|
iter |
Total number of MCMC iterations. Default is 2000. |
warmup |
Number of warmup iterations to discard. Default is 1000. Posterior samples will have 'ceiling((iter - warmup) / thin)' rows per chain. |
thin |
Thinning interval applied after warmup: keep every 'thin'-th post-warmup draw. Default is 1 (keep all draws). Use 'thin > 1' to reduce memory and autocorrelation. |
chains |
Number of independent MCMC chains. Default is 1. |
parallel_chains |
Logical. If 'TRUE', all 'chains' run concurrently on background R sessions (via the 'future' framework); if 'FALSE' (default), chains run sequentially, one after the other. |
group_col |
Name of the column in 'x' that represents the basket/group assignment. If NULL (default), assumes the first column in x is the group. |
seed |
Random seed for reproducibility (optional). |
verbose |
If TRUE (default), show a live progress bar ('progressr') that advances as the chains run, in both sequential and parallel execution. Silence it with 'verbose = FALSE'. |
y |
A Surv object or matrix containing outcome (time and event status). |
For patient , the model is
and the argument pooling selects the prior structure of the basket
effects :
"exnex" (default)latent EXNEX hierarchy,
so that each basket either borrows strength from the exchangeable
component or follows its own
non-exchangeable prior .
"complete"complete pooling: a single shared basket effect
for all baskets with prior
.
"none"no pooling: independent basket effects,
for .
The first predictor variable in the model formula is interpreted as the basket/group assignment; additional variables are covariates in the linear predictor on the log-survival scale.
Censored event times are handled by data augmentation: a censored log-time is
imputed from its truncated-Normal conditional distribution (inverse-CDF in log
space) before the remaining parameters are updated with their conjugate full
conditionals. The sampler keeps only the posterior draws of ,
, and .
An object of class 'pooling_surv' containing posterior samples and metadata.
Components include pooling (the fitted variant), draws (a data
frame with columns theta_1, ..., theta_K, beta_1, ...,
beta_P, and sigma2; under pooling = "complete" the
theta_* columns hold identical draws from the shared effect),
data (the processed data: time, event,
group, X, n, n_groups, n_covariates,
cov_names, chain_seeds), priors (the supplied priors),
resolved_priors (defaults merged with overrides), iter,
warmup, chains, and blueprint. Use summary(),
print(), and plot() to inspect it.
# Small simulated dataset: three baskets, one covariate, ~25% censoring set.seed(1) n <- 90 group <- factor(rep(1:3, each = 30)) x1 <- rnorm(n) eta <- rep(c(1.1, 1.6, 2.0), each = 30) + 0.5 * x1 true_time <- exp(eta + rnorm(n, 0, 0.6)) cens_time <- runif(n, 2, 9) d <- data.frame( time = pmin(true_time, cens_time), event = as.integer(true_time <= cens_time), group = group, x1 = x1 ) # Fit the EXNEX hierarchy with group effects and one covariate fit <- pooling_surv( survival::Surv(time, event) ~ group + x1, data = d, priors = list(p_mix = 0.7), iter = 300, warmup = 150, chains = 1 ) print(fit, show_trace = FALSE) summary(fit) # Complete and no pooling use the same data augmentation machinery fit_complete <- pooling_surv( survival::Surv(time, event) ~ group + x1, data = d, pooling = "complete", iter = 300, warmup = 150, chains = 1 ) fit_none <- pooling_surv( survival::Surv(time, event) ~ group + x1, data = d, pooling = "none", iter = 300, warmup = 150, chains = 1 )# Small simulated dataset: three baskets, one covariate, ~25% censoring set.seed(1) n <- 90 group <- factor(rep(1:3, each = 30)) x1 <- rnorm(n) eta <- rep(c(1.1, 1.6, 2.0), each = 30) + 0.5 * x1 true_time <- exp(eta + rnorm(n, 0, 0.6)) cens_time <- runif(n, 2, 9) d <- data.frame( time = pmin(true_time, cens_time), event = as.integer(true_time <= cens_time), group = group, x1 = x1 ) # Fit the EXNEX hierarchy with group effects and one covariate fit <- pooling_surv( survival::Surv(time, event) ~ group + x1, data = d, priors = list(p_mix = 0.7), iter = 300, warmup = 150, chains = 1 ) print(fit, show_trace = FALSE) summary(fit) # Complete and no pooling use the same data augmentation machinery fit_complete <- pooling_surv( survival::Surv(time, event) ~ group + x1, data = d, pooling = "complete", iter = 300, warmup = 150, chains = 1 ) fit_none <- pooling_surv( survival::Surv(time, event) ~ group + x1, data = d, pooling = "none", iter = 300, warmup = 150, chains = 1 )
Posterior predictive check of the fitted log-normal AFT model: the posterior predictive mean survival curve per group (averaged over all posterior draws) is overlaid with the observed Kaplan-Meier curve and a band of replicated Kaplan-Meier curves. Systematic separation of the observed KM from the posterior mean band indicates model misfit for that group.
posterior_predictive_check( fit, times = NULL, n_replicates = 200, newdata = NULL, plot = TRUE, ... )posterior_predictive_check( fit, times = NULL, n_replicates = 200, newdata = NULL, plot = TRUE, ... )
fit |
A fitted 'pooling_surv' object. |
times |
Optional vector of evaluation times; defaults to a grid of 100 points between 0 and the largest observed time. |
n_replicates |
Number of posterior draws used for the replicated Kaplan-Meier band. Default 200. |
newdata |
Optional data frame with one row per group giving the covariates for the predictive curves. If 'NULL', covariates are zero. |
plot |
Should a 'ggplot2' figure be drawn? Default 'TRUE'. |
... |
Unused. |
Invisibly returns a list with 'posterior' (data frame: 'group', 'time', 'mean', 'lwr', 'upr') and 'observed' (data frame: 'group', 'time', 'surv'). Requires 'ggplot2' when 'plot = TRUE'.
set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) ## Not run: posterior_predictive_check(fit)set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) ## Not run: posterior_predictive_check(fit)
Prints a compact summary and optionally traceplots of posterior draws.
## S3 method for class 'pooling_surv' print(x, show_trace = TRUE, parameters = NULL, max_parameters = Inf, ...)## S3 method for class 'pooling_surv' print(x, show_trace = TRUE, parameters = NULL, max_parameters = Inf, ...)
x |
A fitted 'pooling_surv' object. |
show_trace |
Should traceplots be shown? Default is 'TRUE'. |
parameters |
Optional character vector of parameter names for traceplots. |
max_parameters |
Maximum number of parameters to plot. |
... |
Unused. |
Invisibly returns 'x'.
Estimates the posterior probability that a certain scalar summary of the survival distribution for group 'a' is greater than the same summary for group 'b'. Supported summaries are the median survival time, the survival probability at a fixed time 'S(t_0)', and the restricted mean survival time (RMST). The function works draw-by-draw on the posterior, so the probability is computed on the joint posterior of the two groups.
probability_superiority( fit, a, b, function_of = "median", newdata = NULL, times = NULL, tmax = NULL, ... )probability_superiority( fit, a, b, function_of = "median", newdata = NULL, times = NULL, tmax = NULL, ... )
fit |
A fitted 'pooling_surv' object. |
a |
An integer index (or group label) of the first group. |
b |
An integer index (or group label) of the second group. |
function_of |
Character; one of '"median"', '"survival"' (alias '"S_t"'), or '"rmst"'. Selects which summary is compared. |
newdata |
Optional data frame with one row per group, used to fix covariates. If 'NULL', covariates are set to zero. |
times |
A scalar time at which to evaluate the survival function when 'function_of = "survival"'. |
tmax |
A positive horizon when 'function_of = "rmst"'. |
... |
Unused. |
A named list with elements 'prob' (posterior probability that the summary of group 'a' exceeds that of group 'b'), 'summary' (which summary was used), and 'groups' (the two groups compared).
For every posterior draw, the groups are ranked by a chosen survival summary (median survival, survival at a fixed time, or RMST) and the frequency of each rank is returned. Useful as a decision aid: the row of the output for rank 1 gives the posterior probability that each group is the best.
rank_probabilities( fit, newdata = NULL, function_of = "median", times = NULL, tmax = NULL, ... )rank_probabilities( fit, newdata = NULL, function_of = "median", times = NULL, tmax = NULL, ... )
fit |
A fitted 'pooling_surv' object. |
newdata |
Optional data frame with one row per group used to fix covariates. If 'NULL', covariates are set to zero. |
function_of |
Character; one of '"median"', '"survival"' (alias '"S_t"'), or '"rmst"'. |
times |
Scalar time when 'function_of = "survival"'. |
tmax |
Positive horizon when 'function_of = "rmst"'. |
... |
Unused. |
A data frame with columns 'group', 'rank', and 'prob'. Rank 1 is the largest summary value (best group).
set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) rank_probabilities(fit)set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) rank_probabilities(fit)
Computes the restricted mean survival time up to a horizon 'tmax':
The integral is evaluated numerically (trapezoidal rule over a fine grid) for every posterior draw, then summarised with posterior quantiles.
rmst(fit, tmax = NULL, newdata = NULL, level = 0.95, grid_points = 400, ...)rmst(fit, tmax = NULL, newdata = NULL, level = 0.95, grid_points = 400, ...)
fit |
A fitted 'pooling_surv' object. |
tmax |
A positive scalar horizon up to which the RMST is computed. Defaults to 'max(fit$data$time)'. |
newdata |
Optional data frame (one row gives one RMST). |
level |
Credible-interval level (default '0.95'). |
grid_points |
Resolution of the numerical quadrature (default '400'). |
... |
Unused. |
A 'data.frame' with columns 'group', 'rmst', 'lower', 'upper'.
Compares the hierarchical (EXNEX posterior) estimates of the group-specific log-median survival 'theta_j' (at zero covariates) against the unpooled, basket-only Kaplan-Meier median estimate computed from the original data. A short segment between the two points per group shows the amount of shrinkage (partial pooling) applied by the model.
shrinkage_plot(fit, ...)shrinkage_plot(fit, ...)
fit |
A fitted 'pooling_surv' object. |
... |
Unused. |
Invisibly returns a data frame with columns 'group', 'posterior_mean', 'posterior_lwr', 'posterior_upr', and 'unpooled' – the log-scale values used in the plot. Requires 'ggplot2'.
set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) ## Not run: shrinkage_plot(fit)set.seed(2719) trial_data <- simulate_data(n = 90, K = 3, censoring_rate = 0.3) fit <- pooling_surv(survival::Surv(time, event) ~ group, data = trial_data, iter = 300, warmup = 150, chains = 2) ## Not run: shrinkage_plot(fit)
Generates a synthetic dataset from the log-normal AFT model used by 'exnexSurv':
Healthy baskets draw their location from a common baseline plus random noise; "outlier" baskets (specified via 'outlier_baskets') have their location shifted by 'resist_delta'. The function is useful for simulation studies and teaching examples.
simulate_data( n = 30, K = 9, beta = c(0.5, -0.2), sigma = 1.2, outlier_baskets = NULL, resist_delta = -0.8, censoring_rate = NULL, censor_upper = NULL, theta = 0, seed = NULL )simulate_data( n = 30, K = 9, beta = c(0.5, -0.2), sigma = 1.2, outlier_baskets = NULL, resist_delta = -0.8, censoring_rate = NULL, censor_upper = NULL, theta = 0, seed = NULL )
n |
Number of patients per basket. A scalar is replicated across all 'K' baskets; a numeric vector of length 'K' assigns a size to each basket individually. |
K |
Number of baskets (default '9'). |
beta |
Numeric vector of regression coefficients for the covariates. Length determines the number of covariates. |
sigma |
Residual standard deviation (default '1.2'). |
outlier_baskets |
Optional integer vector of basket indices (1 to K) whose true location is shifted away from the healthy population. These are the baskets the EXNEX model is designed to detect. |
resist_delta |
Additive shift applied to the 'theta' of outlier baskets (default '-0.8'). |
censoring_rate |
Approximate proportion of censoring after 'censor_upper'; if 'NULL', no censoring is applied. |
censor_upper |
Upper bound of the censoring-time uniform distribution. |
theta |
Baseline location for the healthy (non-outlier) baskets; a scalar used as the centre around which the healthy basket locations vary. |
seed |
Optional seed for reproducibility. |
A 'data.frame' with columns 'time', 'event', 'group' (a factor), and one covariate column ('x1', ...) per entry in 'beta'. True parameter values are stored as attributes 'true_theta', 'true_beta', and 'true_sigma'.
Summarize posterior draws from an pooling_surv fit
## S3 method for class 'pooling_surv' summary(object, probs = c(0.05, 0.5, 0.95), ...)## S3 method for class 'pooling_surv' summary(object, probs = c(0.05, 0.5, 0.95), ...)
object |
A fitted 'pooling_surv' object. |
probs |
Quantiles to report. |
... |
Unused. |
A data frame with posterior summaries.
Computes posterior survival curves from a fitted
'pooling_surv' model for a log-normal AFT specification:
For a fixed time and linear predictor , the survival
probability of the log-normal distribution is
where is the residual variance. Posterior draws of
are propagated through this expression to
obtain a full posterior distribution of at each time point.
survival_curves(fit, newdata = NULL, times = NULL, level = 0.95, ...)survival_curves(fit, newdata = NULL, times = NULL, level = 0.95, ...)
fit |
A fitted 'pooling_surv' object. |
newdata |
Optional data frame with columns matching the covariates of the model. If it contains a 'group' column, that is used for the group index; otherwise the first group is used for all rows. |
times |
Optional numeric vector of times at which to evaluate the curves. If 'NULL', a sensible grid is built from the observed follow-up times. |
level |
Credible-interval level (default '0.95'). |
... |
Unused. |
If 'newdata' is supplied, each row is evaluated at its own group and covariate values; otherwise the covariates are fixed at zero and the first group is used (with a warning if more than one group exists).
An object of class 'survival_exnex' (also a 'data.frame') with long format columns: 'time', 'median', 'lower', 'upper', and 'group'.