diff --git a/R/LongitudinalSteinFojo.R b/R/LongitudinalSteinFojo.R index 686644b6..f8b684a6 100755 --- a/R/LongitudinalSteinFojo.R +++ b/R/LongitudinalSteinFojo.R @@ -36,6 +36,8 @@ NULL #' #' @param sigma (`Prior`)\cr for the variance of the longitudinal values `sigma`. #' +#' @param scaled_variance (`logical`)\cr whether the variance should be scaled by the expected value +#' (see the "Statistical Specifications" vignette for more details) #' @param centred (`logical`)\cr whether to use the centred parameterization. #' #' @export @@ -51,12 +53,14 @@ LongitudinalSteinFojo <- function( sigma = prior_lognormal(log(0.1), 1), + scaled_variance = TRUE, centred = FALSE ) { sf_model <- StanModule(decorated_render( .x = read_stan("lm-stein-fojo/model.stan"), - centred = centred + centred = centred, + scaled_variance = scaled_variance )) # Apply constriants diff --git a/R/SimLongitudinalSteinFojo.R b/R/SimLongitudinalSteinFojo.R index 656ae522..5c992847 100644 --- a/R/SimLongitudinalSteinFojo.R +++ b/R/SimLongitudinalSteinFojo.R @@ -18,6 +18,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. @@ -31,6 +33,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 (`logical`)\cr See arguments. #' #' @family SimLongitudinal #' @name SimLongitudinalSteinFojo-class @@ -50,7 +53,8 @@ NULL link_ttg = "numeric", link_identity = "numeric", link_growth = "numeric", - link_shrinkage = "numeric" + link_shrinkage = "numeric", + scaled_variance = "logical" ) ) @@ -69,7 +73,8 @@ SimLongitudinalSteinFojo <- 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)) @@ -89,7 +94,8 @@ SimLongitudinalSteinFojo <- 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 ) } @@ -144,7 +150,8 @@ sampleObservations.SimLongitudinalSteinFojo <- function(object, times_df) { dplyr::mutate(mu_sld = sf_sld(.data$time, .data$psi_b, .data$psi_s, .data$psi_g)) |> dplyr::mutate(dsld = sf_dsld(.data$time, .data$psi_b, .data$psi_s, .data$psi_g)) |> dplyr::mutate(ttg = sf_ttg(.data$time, .data$psi_b, .data$psi_s, .data$psi_g)) |> - 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-stein-fojo/model.stan b/inst/stan/lm-stein-fojo/model.stan index 64221adf..889c8760 100755 --- a/inst/stan/lm-stein-fojo/model.stan +++ b/inst/stan/lm-stein-fojo/model.stan @@ -63,13 +63,21 @@ transformed parameters{ long_obvs_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_sf_sigma + {%- if scaled_variance -%} + Ypred[subject_tumour_index_obs] * lm_sf_sigma + {% else %} + rep_vector(lm_sf_sigma, n_tumour_obs) + {%- endif -%} ); if (n_tumour_cens > 0 ) { long_obvs_log_lik[subject_tumour_index_cens] = vect_normal_log_cum( tumour_value_lloq, Ypred[subject_tumour_index_cens], - Ypred[subject_tumour_index_cens] * lm_sf_sigma + {%- if scaled_variance -%} + Ypred[subject_tumour_index_cens] * lm_sf_sigma + {% else %} + rep_vector(lm_sf_sigma, n_tumour_cens) + {%- endif -%} ); } } diff --git a/man/LongitudinalSteinFojo-class.Rd b/man/LongitudinalSteinFojo-class.Rd index 839c4055..74f1a01e 100644 --- a/man/LongitudinalSteinFojo-class.Rd +++ b/man/LongitudinalSteinFojo-class.Rd @@ -15,6 +15,7 @@ LongitudinalSteinFojo( omega_ks = prior_lognormal(log(0.2), 1), omega_kg = prior_lognormal(log(0.2), 1), sigma = prior_lognormal(log(0.1), 1), + scaled_variance = TRUE, centred = FALSE ) } @@ -33,6 +34,9 @@ LongitudinalSteinFojo( \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/SimLongitudinalSteinFojo-class.Rd b/man/SimLongitudinalSteinFojo-class.Rd index 96369e83..77b52ad2 100644 --- a/man/SimLongitudinalSteinFojo-class.Rd +++ b/man/SimLongitudinalSteinFojo-class.Rd @@ -20,7 +20,8 @@ SimLongitudinalSteinFojo( link_ttg = 0, link_identity = 0, link_growth = 0, - link_shrinkage = 0 + link_shrinkage = 0, + scaled_variance = TRUE ) } \arguments{ @@ -49,6 +50,9 @@ SimLongitudinalSteinFojo( \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 Stein-Fojo Model @@ -79,6 +83,8 @@ Simulate Longitudinal Data from a Stein-Fojo Model \item{\code{link_growth}}{(\code{numeric})\cr See arguments.} \item{\code{link_shrinkage}}{(\code{numeric})\cr See arguments.} + +\item{\code{scaled_variance}}{(\code{logical})\cr See arguments.} }} \seealso{ diff --git a/tests/testthat/test-LongitudinalSteinFojo.R b/tests/testthat/test-LongitudinalSteinFojo.R index 9b77e377..34604da6 100644 --- a/tests/testthat/test-LongitudinalSteinFojo.R +++ b/tests/testthat/test-LongitudinalSteinFojo.R @@ -265,6 +265,11 @@ test_that("Can recover known distributional parameters from a SF joint model", { expect_true(all(dat$ess_bulk > 100)) }) + + + + + test_that("Can recover known distributional parameters from a SF joint model with growth link", { skip_if_not(is_full_test()) @@ -431,6 +436,9 @@ test_that("Can recover known distributional parameters from a SF joint model wit + + + test_that("Quantity models pass the parser", { mock_samples <- .JointModelSamples( model = JointModel(longitudinal = LongitudinalSteinFojo(centred = TRUE)), @@ -454,6 +462,11 @@ test_that("Quantity models pass the parser", { + + + + + test_that("Can generate valid initial values", { pars <- c( @@ -500,3 +513,163 @@ test_that("Can generate valid initial values", { expect_true(all(vals > 0)) }) + + + + + + +test_that("Unscaled variance SF mode pass the parser", { + jm <- JointModel( + longitudinal = LongitudinalSteinFojo(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 SF model", { + + skip_if_not(is_full_test()) + + sim_params <- list( + sigma = 0.4, + mu_s = log(c(0.15, 0.3)), + mu_g = log(c(0.4, 0.25)), + mu_b = log(60), + omega_b = 0.1, + omega_s = c(0.1, 0.1), + omega_g = c(0.2, 0.2), + link_ttg = 0, + link_dsld = 0, + link_growth = 0, + lambda = 2, + lambda_cen = 1 / 9000, + beta_cat_b = -0.1, + beta_cat_c = 0.5, + beta_cont = 0.3 + ) + + set.seed(2338) + ## Generate Test data with known parameters + jlist <- SimJointData( + design = list( + SimGroup(200, "Arm-A", "Study-X"), + SimGroup(200, "Arm-B", "Study-X") + ), + longitudinal = SimLongitudinalSteinFojo( + times = c(1, 50, 100, 150, 200, 300, 400, 500, 600, 700) / 365, + sigma = sim_params$sigma, + mu_s = sim_params$mu_s, + mu_g = sim_params$mu_g, + mu_b = sim_params$mu_b, + omega_b = sim_params$omega_b, + omega_s = sim_params$omega_s, + omega_g = sim_params$omega_g, + link_ttg = sim_params$link_ttg, + link_dsld = sim_params$link_dsld, + link_growth = sim_params$link_growth, + scaled_variance = FALSE + ), + survival = SimSurvivalExponential( + time_max = 4, + time_step = 1 / 365, + lambda = sim_params$lambda, + lambda_cen = 1 / 9000, + beta_cat = c( + "A" = 0, + "B" = sim_params$beta_cat_b, + "C" = sim_params$beta_cat_c + ), + beta_cont = sim_params$beta_cont + ), + .silent = TRUE + ) + + # nolint startā  + ### Diagnostics helpers + # plot(survival::survfit(Surv(time, event) ~ 1, data = jlist@survival)) + # median(jlist@survival$time) + # nolint end + + + jm <- JointModel( + longitudinal = LongitudinalSteinFojo( + mu_bsld = prior_normal(log(60), 0.5), + mu_ks = prior_normal(log(0.2), 0.5), + mu_kg = prior_normal(log(0.3), 0.5), + omega_bsld = prior_lognormal(log(0.1), 0.5), + omega_ks = prior_lognormal(log(0.1), 0.5), + omega_kg = prior_lognormal(log(0.1), 0.5), + sigma = prior_lognormal(log(0.5), 0.5), + centred = TRUE, + scaled_variance = FALSE + ) + ) + + jdat <- DataJoint( + subject = DataSubject( + data = jlist@survival, + subject = "subject", + arm = "arm", + study = "study" + ), + longitudinal = DataLongitudinal( + data = jlist@longitudinal, + formula = sld ~ time, + threshold = 5 + ) + ) + + + set.seed(2213) + mp <- run_quietly({ + sampleStanModel( + jm, + data = jdat, + iter_warmup = 750, + iter_sampling = 750, + chains = 2, + parallel_chains = 2 + ) + }) + + summary_post <- function(model, vars, exp = FALSE) { + dat <- model$summary( + vars, + mean = mean, + q01 = \(x) purrr::set_names(quantile(x, 0.01), ""), + q99 = \(x) purrr::set_names(quantile(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_sf_mu_bsld", "lm_sf_mu_ks", "lm_sf_mu_kg", + "lm_sf_sigma", "lm_sf_omega_bsld", "lm_sf_omega_kg", "lm_sf_omega_ks" + ) + ) + true_values <- c( + sim_params$mu_b, sim_params$mu_s, sim_params$mu_g, + sim_params$sigma, sim_params$omega_b, sim_params$omega_g, sim_params$omega_s + ) + 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-SimLongitudinalSteinFojo.R b/tests/testthat/test-SimLongitudinalSteinFojo.R index 4392f4e2..bc6940bb 100644 --- a/tests/testthat/test-SimLongitudinalSteinFojo.R +++ b/tests/testthat/test-SimLongitudinalSteinFojo.R @@ -53,7 +53,7 @@ test_that("SimLongitudinalSteinFojo works as expected", { names(res_obvs), c( "subject", "arm", "study", "psi_b", "psi_s", "psi_g", "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 75dbd94b..13617cf1 100644 --- a/vignettes/statistical-specification.Rmd +++ b/vignettes/statistical-specification.Rmd @@ -185,7 +185,10 @@ 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