exnexSurv fits Bayesian EXNEX survival models for
right-censored log-normal data. The current interface supports:
x/y interface,theta_j, beta, and
sigma2.library(exnexSurv)
library(survival)
simulate_surv_data <- function(
theta,
sigma2,
beta = NULL,
n_per_group = 50,
censor_min = 4,
censor_max = 12,
seed = NULL
) {
if (!is.null(seed)) {
set.seed(seed)
}
groups <- rep(seq_along(theta), each = n_per_group)
age_std <- rnorm(length(groups), mean = 0, sd = 1)
mean_log_time <- rep(theta, each = n_per_group)
if (!is.null(beta)) {
mean_log_time <- mean_log_time + beta * age_std
}
log_time <- rnorm(length(groups), mean = mean_log_time, sd = sqrt(sigma2))
true_time <- exp(log_time)
censor_time <- runif(length(groups), min = censor_min, max = censor_max)
data.frame(
time = pmin(true_time, censor_time),
event = as.integer(true_time <= censor_time),
group = factor(groups),
age_std = age_std
)
}
sim_data <- simulate_surv_data(
theta = c(1.1, 1.6, 2.0),
sigma2 = 0.25,
beta = -0.30,
n_per_group = 50,
seed = 6421
)
head(sim_data)
#> time event group age_std
#> 1 3.339087 1 1 0.6071040
#> 2 3.330683 1 1 -1.1474496
#> 3 2.469950 1 1 -0.5987154
#> 4 4.277826 1 1 -1.6561367
#> 5 2.422691 1 1 0.8235441
#> 6 2.831290 1 1 0.5040783
mean(sim_data$event)
#> [1] 0.7866667fit_formula <- exnex_surv(
Surv(time, event) ~ group + age_std,
data = sim_data,
iter = 1200,
warmup = 400,
chains = 1,
seed = 6421
)
print(fit_formula, show_trace = FALSE)
#> <exnex_surv model>
#> Draws: 800 total post-warmup samples
#> 800 post-warmup samples per chain
#> Groups: 3 | Covariates: 1
#> MCMC: iter = 1200 , warmup = 400 , chains = 1
#>
#> parameter mean sd q05 q50 q95
#> theta_1 0.9861505 0.06849275 0.8788059 0.9862140 1.0958766
#> theta_2 1.5326028 0.07082872 1.4121900 1.5318814 1.6524219
#> theta_3 1.9841454 0.07555761 1.8632978 1.9833107 2.1082712
#> beta_1 -0.3075793 0.04281121 -0.3797194 -0.3074616 -0.2362734
#> sigma2 0.2347691 0.03216346 0.1856459 0.2331866 0.2883242
summary(fit_formula)
#> parameter mean sd q05 q50 q95
#> 1 theta_1 0.9861505 0.06849275 0.8788059 0.9862140 1.0958766
#> 2 theta_2 1.5326028 0.07082872 1.4121900 1.5318814 1.6524219
#> 3 theta_3 1.9841454 0.07555761 1.8632978 1.9833107 2.1082712
#> 4 beta_1 -0.3075793 0.04281121 -0.3797194 -0.3074616 -0.2362734
#> 5 sigma2 0.2347691 0.03216346 0.1856459 0.2331866 0.2883242The fitted object stores the processed data, posterior draws, and MCMC settings. You can inspect the model structure with the usual S3 methods.
fit_parallel <- exnex_surv(
Surv(time, event) ~ group + age_std,
data = sim_data,
iter = 1200,
warmup = 400,
chains = 2,
parallel_chains = 2,
seed = 6421
)
print(fit_parallel, show_trace = FALSE)
#> <exnex_surv model>
#> Draws: 1600 total post-warmup samples
#> 800 post-warmup samples per chain
#> Groups: 3 | Covariates: 1
#> MCMC: iter = 1200 , warmup = 400 , chains = 2
#>
#> parameter mean sd q05 q50 q95
#> theta_1 0.9870040 0.06958675 0.8748666 0.9858982 1.0979172
#> theta_2 1.5334538 0.07136513 1.4126375 1.5335034 1.6516964
#> theta_3 1.9815311 0.07540195 1.8590322 1.9798331 2.1025855
#> beta_1 -0.3047351 0.04333179 -0.3782277 -0.3047538 -0.2341006
#> sigma2 0.2337765 0.03210020 0.1862492 0.2316788 0.2892194
plot(
fit_parallel,
parameters = c("theta_1", "theta_2", "theta_3", "beta_1", "sigma2"),
ask = FALSE
)When parallel_chains is greater than 1, the chains are
evaluated concurrently by R and the traceplots show all chains in the
same panel for each parameter.
If you do not want to use all available workers, set
parallel_chains smaller than chains. For
example, chains = 4 and parallel_chains = 2
runs four independent chains while evaluating two at a time.
The fitted object stores the combined post-warmup draws from all
chains in one table. Because each chain contributes the same number of
post-warmup samples, the total number of rows is
(iter - warmup) * chains.
str(fit_formula$data)
#> List of 9
#> $ time : num [1:150] 3.34 3.33 2.47 4.28 2.42 ...
#> $ event : num [1:150] 1 1 1 1 1 1 1 1 1 1 ...
#> $ group : num [1:150] 1 1 1 1 1 1 1 1 1 1 ...
#> $ X : num [1:150, 1] 0.607 -1.147 -0.599 -1.656 0.824 ...
#> ..- attr(*, "dimnames")=List of 2
#> .. ..$ : NULL
#> .. ..$ : chr "age_std"
#> $ n : int 150
#> $ n_groups : num 3
#> $ n_covariates: int 1
#> $ cov_names : chr "age_std"
#> $ chain_seeds : int 979729617
plot(
fit_formula,
parameters = c("theta_1", "theta_2", "sigma2"),
ask = FALSE
)x and yfit_xy <- exnex_surv(
x = sim_data[c("group", "age_std")],
y = Surv(sim_data$time, sim_data$event),
iter = 1200,
warmup = 400,
chains = 1,
seed = 6421
)
summary(fit_xy)
#> parameter mean sd q05 q50 q95
#> 1 theta_1 0.9861505 0.06849275 0.8788059 0.9862140 1.0958766
#> 2 theta_2 1.5326028 0.07082872 1.4121900 1.5318814 1.6524219
#> 3 theta_3 1.9841454 0.07555761 1.8632978 1.9833107 2.1082712
#> 4 beta_1 -0.3075793 0.04281121 -0.3797194 -0.3074616 -0.2362734
#> 5 sigma2 0.2347691 0.03216346 0.1856459 0.2331866 0.2883242
all.equal(fit_formula$draws, fit_xy$draws)
#> [1] TRUEThis is useful if predictors and outcomes are prepared separately.
If the model only contains the subgroup variable, no
beta terms are estimated.
fit_no_cov <- exnex_surv(
Surv(time, event) ~ group,
data = sim_data,
iter = 1200,
warmup = 400,
chains = 1,
seed = 6421
)
summary(fit_no_cov)
#> parameter mean sd q05 q50 q95
#> 1 theta_1 0.9610428 0.07667525 0.8240873 0.9646688 1.0782239
#> 2 theta_2 1.5114113 0.08415282 1.3716898 1.5122529 1.6479938
#> 3 theta_3 1.9543858 0.08866337 1.8051221 1.9554501 2.0994038
#> 4 sigma2 0.3194790 0.04445914 0.2526799 0.3156426 0.4000848
plot(fit_no_cov, ask = FALSE)summary(fit_formula)
#> parameter mean sd q05 q50 q95
#> 1 theta_1 0.9861505 0.06849275 0.8788059 0.9862140 1.0958766
#> 2 theta_2 1.5326028 0.07082872 1.4121900 1.5318814 1.6524219
#> 3 theta_3 1.9841454 0.07555761 1.8632978 1.9833107 2.1082712
#> 4 beta_1 -0.3075793 0.04281121 -0.3797194 -0.3074616 -0.2362734
#> 5 sigma2 0.2347691 0.03216346 0.1856459 0.2331866 0.2883242Whatever you pass to priors, the fitted object records
the exact hyperparameters the sampler used, with defaults merged into
the fields you did not supply. This is useful to confirm your
customization was applied and to reproduce a fit.
fit_formula$resolved_priors
#> $a_sigma
#> [1] 2
#>
#> $b_sigma
#> [1] 2
#>
#> $a_tau
#> [1] 2
#>
#> $b_tau
#> [1] 2
#>
#> $p_mix
#> [1] 0.5
#>
#> $m_mu
#> [1] 0
#>
#> $v_mu
#> [1] 10000
#>
#> $m_nex
#> [1] 0
#>
#> $v_nex
#> [1] 10000
#>
#> $v_beta
#> [1] 10000To customize a prior, pass a named list to priors; for
example, priors = list(p_mix = 0.7, a_tau = 3, b_tau = 3).
The fields p_mix, m_nex, and
v_nex also accept a vector of length equal to the number of
baskets, assigning one value per basket. See the vignette The EXNEX
Model, Priors, and Data Augmentation for the full list of
hyperparameters and how to set them.
The current implementation supports multiple chains, including
R-level parallel execution through parallel_chains. For a
more careful convergence check, fit more than one chain and compare the
traceplots and posterior summaries across chains.