Skip to content

Commit

Permalink
GSF Unscaled Variance (#404)
Browse files Browse the repository at this point in the history
  • Loading branch information
gowerc authored Aug 20, 2024
1 parent 7edd3a8 commit 7d4f9a3
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 9 deletions.
6 changes: 5 additions & 1 deletion R/LongitudinalGSF.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
15 changes: 11 additions & 4 deletions R/SimLongitudinalGSF.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -57,7 +60,8 @@ NULL
link_ttg = "numeric",
link_identity = "numeric",
link_growth = "numeric",
link_shrinkage = "numeric"
link_shrinkage = "numeric",
scaled_variance = "logical"
)
)

Expand All @@ -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))
Expand All @@ -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
)
}

Expand Down Expand Up @@ -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) +
Expand Down
12 changes: 10 additions & 2 deletions inst/stan/lm-gsf/model.stan
Original file line number Diff line number Diff line change
Expand Up @@ -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 -%}
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions man/LongitudinalGSF-class.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion man/SimLongitudinalGSF-class.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 162 additions & 0 deletions tests/testthat/test-LongitudinalGSF.R
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
2 changes: 1 addition & 1 deletion tests/testthat/test-SimLongitudinalGSF.R
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
)
})
Expand Down
6 changes: 6 additions & 0 deletions vignettes/statistical-specification.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down

0 comments on commit 7d4f9a3

Please sign in to comment.