diff --git a/R/LongitudinalGSF.R b/R/LongitudinalGSF.R index df793f48..d7d0e70e 100755 --- a/R/LongitudinalGSF.R +++ b/R/LongitudinalGSF.R @@ -42,6 +42,8 @@ NULL #' @param sigma (`Prior`)\cr for the variance of the longitudinal values `sigma`. #' #' @param centred (`logical`)\cr whether to use the centred parameterization. +#' @param scaled_variance (`logical`)\cr whether the variance should be scaled by the expected value +#' (see the "Statistical Specifications" vignette for more details) #' #' @importFrom stats qlogis #' @export @@ -59,12 +61,14 @@ LongitudinalGSF <- function( sigma = prior_lognormal(log(0.1), 1), + scaled_variance = TRUE, centred = FALSE ) { gsf_model <- StanModule(decorated_render( .x = read_stan("lm-gsf/model.stan"), - centred = centred + centred = centred, + scaled_variance = scaled_variance )) # Apply constraints diff --git a/R/SimLongitudinalGSF.R b/R/SimLongitudinalGSF.R index 415b0caa..a502d9e2 100644 --- a/R/SimLongitudinalGSF.R +++ b/R/SimLongitudinalGSF.R @@ -22,6 +22,8 @@ NULL #' @param link_identity (`number`)\cr the link coefficient for the SLD Identity contribution. #' @param link_growth (`number`)\cr the link coefficient for the log-growth parameter contribution. #' @param link_shrinkage (`number`)\cr the link coefficient for the log-shrinkage parameter contribution. +#' @param scaled_variance (`logical`)\cr whether the variance should be scaled by the expected value +#' (see the "Statistical Specifications" vignette for more details) #' #' @slot sigma (`numeric`)\cr See arguments. #' @slot mu_s (`numeric`)\cr See arguments. @@ -37,6 +39,7 @@ NULL #' @slot link_identity (`numeric`)\cr See arguments. #' @slot link_growth (`numeric`)\cr See arguments. #' @slot link_shrinkage (`numeric`)\cr See arguments. +#' @slot scaled_variance (`numeric`)\cr See arguments. #' @family SimLongitudinal #' @name SimLongitudinalGSF-class #' @exportClass SimLongitudinalGSF @@ -57,7 +60,8 @@ NULL link_ttg = "numeric", link_identity = "numeric", link_growth = "numeric", - link_shrinkage = "numeric" + link_shrinkage = "numeric", + scaled_variance = "logical" ) ) @@ -78,7 +82,8 @@ SimLongitudinalGSF <- function( link_ttg = 0, link_identity = 0, link_growth = 0, - link_shrinkage = 0 + link_shrinkage = 0, + scaled_variance = TRUE ) { if (length(omega_b) == 1) omega_b <- rep(omega_b, length(mu_b)) @@ -101,7 +106,8 @@ SimLongitudinalGSF <- function( link_ttg = link_ttg, link_identity = link_identity, link_growth = link_growth, - link_shrinkage = link_shrinkage + link_shrinkage = link_shrinkage, + scaled_variance = scaled_variance ) } @@ -161,7 +167,8 @@ sampleObservations.SimLongitudinalGSF <- function(object, times_df) { dplyr::mutate(mu_sld = gsf_sld(.data$time, .data$psi_b, .data$psi_s, .data$psi_g, .data$psi_phi)) |> dplyr::mutate(dsld = gsf_dsld(.data$time, .data$psi_b, .data$psi_s, .data$psi_g, .data$psi_phi)) |> dplyr::mutate(ttg = gsf_ttg(.data$time, .data$psi_b, .data$psi_s, .data$psi_g, .data$psi_phi)) |> - dplyr::mutate(sld = stats::rnorm(dplyr::n(), .data$mu_sld, .data$mu_sld * object@sigma)) |> + dplyr::mutate(sld_sd = ifelse(object@scaled_variance, .data$mu_sld * object@sigma, object@sigma)) |> + dplyr::mutate(sld = stats::rnorm(dplyr::n(), .data$mu_sld, .data$sld_sd)) |> dplyr::mutate( log_haz_link = (object@link_dsld * .data$dsld) + diff --git a/inst/stan/lm-gsf/model.stan b/inst/stan/lm-gsf/model.stan index bb6efbd1..27d6fed4 100755 --- a/inst/stan/lm-gsf/model.stan +++ b/inst/stan/lm-gsf/model.stan @@ -76,13 +76,21 @@ transformed parameters{ Ypred_log_lik[subject_tumour_index_obs] = vect_normal_log_dens( tumour_value[subject_tumour_index_obs], Ypred[subject_tumour_index_obs], - Ypred[subject_tumour_index_obs] * lm_gsf_sigma + {%- if scaled_variance -%} + Ypred[subject_tumour_index_obs] * lm_gsf_sigma + {% else %} + rep_vector(lm_gsf_sigma, n_tumour_obs) + {%- endif -%} ); if (n_tumour_cens > 0 ) { Ypred_log_lik[subject_tumour_index_cens] = vect_normal_log_cum( tumour_value_lloq, Ypred[subject_tumour_index_cens], - Ypred[subject_tumour_index_cens] * lm_gsf_sigma + {%- if scaled_variance -%} + Ypred[subject_tumour_index_cens] * lm_gsf_sigma + {% else %} + rep_vector(lm_gsf_sigma, n_tumour_cens) + {%- endif -%} ); } } diff --git a/man/LongitudinalGSF-class.Rd b/man/LongitudinalGSF-class.Rd index 236b3b94..b243da12 100644 --- a/man/LongitudinalGSF-class.Rd +++ b/man/LongitudinalGSF-class.Rd @@ -17,6 +17,7 @@ LongitudinalGSF( omega_kg = prior_lognormal(log(0.2), 1), omega_phi = prior_lognormal(log(0.2), 1), sigma = prior_lognormal(log(0.1), 1), + scaled_variance = TRUE, centred = FALSE ) } @@ -40,6 +41,9 @@ affected by the treatment \code{omega_phi}.} \item{sigma}{(\code{Prior})\cr for the variance of the longitudinal values \code{sigma}.} +\item{scaled_variance}{(\code{logical})\cr whether the variance should be scaled by the expected value +(see the "Statistical Specifications" vignette for more details)} + \item{centred}{(\code{logical})\cr whether to use the centred parameterization.} } \description{ diff --git a/man/SimLongitudinalGSF-class.Rd b/man/SimLongitudinalGSF-class.Rd index ad5758ff..0c4879d0 100644 --- a/man/SimLongitudinalGSF-class.Rd +++ b/man/SimLongitudinalGSF-class.Rd @@ -22,7 +22,8 @@ SimLongitudinalGSF( link_ttg = 0, link_identity = 0, link_growth = 0, - link_shrinkage = 0 + link_shrinkage = 0, + scaled_variance = TRUE ) } \arguments{ @@ -56,6 +57,9 @@ affected by the treatment \code{omega_phi}.} \item{link_growth}{(\code{number})\cr the link coefficient for the log-growth parameter contribution.} \item{link_shrinkage}{(\code{number})\cr the link coefficient for the log-shrinkage parameter contribution.} + +\item{scaled_variance}{(\code{logical})\cr whether the variance should be scaled by the expected value +(see the "Statistical Specifications" vignette for more details)} } \description{ Simulate Longitudinal Data from a GSF Model @@ -90,6 +94,8 @@ Simulate Longitudinal Data from a GSF Model \item{\code{link_growth}}{(\code{numeric})\cr See arguments.} \item{\code{link_shrinkage}}{(\code{numeric})\cr See arguments.} + +\item{\code{scaled_variance}}{(\code{numeric})\cr See arguments.} }} \seealso{ diff --git a/tests/testthat/test-LongitudinalGSF.R b/tests/testthat/test-LongitudinalGSF.R index 2b9611f1..5aa4fa65 100644 --- a/tests/testthat/test-LongitudinalGSF.R +++ b/tests/testthat/test-LongitudinalGSF.R @@ -314,3 +314,165 @@ test_that("Can generate valid initial values", { expect_true(all(vals > 0)) }) + + + + +test_that("Unscaled variance GSF mode pass the parser", { + jm <- JointModel( + longitudinal = LongitudinalGSF(centred = FALSE, scaled_variance = FALSE), + survival = SurvivalLogLogistic(), + link = linkDSLD() + ) + x <- as.StanModule(jm) + expect_stan_syntax(x) +}) + + +test_that("Can recover known distributional parameters from unscaled variance GSF model", { + + skip_if_not(is_full_test()) + pars <- list( + sigma = 0.4, + mu_s = log(c(0.6, 0.4)), + mu_g = log(c(0.25, 0.35)), + mu_b = log(60), + mu_phi = qlogis(c(0.4, 0.6)), + omega_b = c(0.2), + omega_s = c(0.3, 0.1), + omega_g = c(0.1, 0.3), + omega_phi = c(0.3, 0.1), + link_dsld = 0, + link_ttg = 0, + link_identity = 0, + beta_cat_B = 0.5, + beta_cat_C = -0.1, + beta_cont = 0.3, + lambda = 1 / (400 / 365) + ) + + set.seed(7743) + jlist <- SimJointData( + design = list( + SimGroup(120, "Arm-A", "Study-X"), + SimGroup(140, "Arm-B", "Study-X") + ), + survival = SimSurvivalExponential( + lambda = pars$lambda, + time_max = 4, + time_step = 1 / 365, + lambda_censor = 1 / 9000, + beta_cat = c( + "A" = 0, + "B" = pars$beta_cat_B, + "C" = pars$beta_cat_C + ), + beta_cont = pars$beta_cont + ), + longitudinal = SimLongitudinalGSF( + times = c(-100, -50, 0, 1, 10, 50, 100, 150, 250, 300, 400, 500, 600) / 365, + sigma = pars$sigma, + mu_s = pars$mu_s, + mu_g = pars$mu_g, + mu_b = pars$mu_b, + mu_phi = pars$mu_phi, + omega_b = pars$omega_b, + omega_s = pars$omega_s, + omega_g = pars$omega_g, + omega_phi = pars$omega_phi, + link_dsld = pars$link_dsld, + link_ttg = pars$link_ttg, + link_identity = pars$link_identity, + scaled_variance = FALSE + ), + .silent = TRUE + ) + + jdat <- DataJoint( + subject = DataSubject( + data = jlist@survival, + subject = "subject", + arm = "arm", + study = "study" + ), + survival = DataSurvival( + data = jlist@survival, + formula = Surv(time, event) ~ cov_cat + cov_cont + ), + longitudinal = DataLongitudinal( + data = jlist@longitudinal, + formula = sld ~ time + ) + ) + + jm <- JointModel( + longitudinal = LongitudinalGSF( + mu_bsld = prior_normal(log(60), 0.4), + mu_ks = prior_normal(log(0.6), 0.4), + mu_kg = prior_normal(log(0.3), 0.4), + mu_phi = prior_normal(qlogis(0.5), 0.5), + omega_bsld = prior_lognormal(log(0.2), 0.4), + omega_ks = prior_lognormal(log(0.2), 0.4), + omega_kg = prior_lognormal(log(0.2), 0.4), + omega_phi = prior_lognormal(log(0.2), 0.4), + sigma = prior_lognormal(log(0.01), 0.4), + centred = TRUE, + scaled_variance = FALSE + ) + ) + + suppressWarnings({ + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_warmup = 400, + iter_sampling = 800, + chains = 2, + refresh = 200, + parallel_chains = 2 + ) + }) + }) + + summary_post <- function(model, vars, exp = FALSE) { + no_name_quant <- \(...) { + x <- quantile(...) + names(x) <- NULL + x + } + dat <- model$summary( + vars, + mean = mean, + q01 = \(x) no_name_quant(x, 0.01), + q99 = \(x) no_name_quant(x, 0.99), + rhat = posterior::rhat, + ess_bulk = posterior::ess_bulk, + ess_tail = posterior::ess_tail + ) + if (exp) { + dat$q01 <- dat$q01 |> exp() + dat$q99 <- dat$q99 |> exp() + dat$mean <- dat$mean |> exp() + } + dat + } + + + dat <- summary_post( + as.CmdStanMCMC(mp), + c( + "lm_gsf_mu_bsld", "lm_gsf_mu_ks", "lm_gsf_mu_kg", "lm_gsf_mu_phi", + "lm_gsf_sigma", "lm_gsf_omega_bsld", "lm_gsf_omega_kg", "lm_gsf_omega_ks", + "lm_gsf_omega_phi" + ) + ) + true_values <- c( + pars$mu_b, pars$mu_s, pars$mu_g, pars$mu_phi, + pars$sigma, pars$omega_b, pars$omega_g, pars$omega_s, + pars$omega_phi + ) + expect_true(all(dat$q01 <= true_values)) + expect_true(all(dat$q99 >= true_values)) + expect_true(all(dat$ess_bulk > 100)) +}) diff --git a/tests/testthat/test-SimLongitudinalGSF.R b/tests/testthat/test-SimLongitudinalGSF.R index 78d41541..32636520 100644 --- a/tests/testthat/test-SimLongitudinalGSF.R +++ b/tests/testthat/test-SimLongitudinalGSF.R @@ -55,7 +55,7 @@ test_that("SimLongitudinalGSF works as expected", { names(res_obvs), c( "subject", "arm", "study", "psi_b", "psi_s", "psi_g", "psi_phi", "time", - "mu_sld", "dsld", "ttg", "sld", "log_haz_link" + "mu_sld", "dsld", "ttg", "sld_sd", "sld", "log_haz_link" ) ) }) diff --git a/vignettes/statistical-specification.Rmd b/vignettes/statistical-specification.Rmd index 37066411..5daed1e5 100644 --- a/vignettes/statistical-specification.Rmd +++ b/vignettes/statistical-specification.Rmd @@ -315,6 +315,12 @@ Where: * $\eta_{\theta i}$ is a random effects offset on parameter $\theta$ for subject $i$ +If using the unscaled variance parameterisation then the following alternative formulation is used: +$$ +y_{ij} &\sim N(SLD_{ij},\ \sigma^2) +$$ + + ### Derivative of the SLD Trajectory Link