-
Notifications
You must be signed in to change notification settings - Fork 0
Add chapter2.qmd: fake-data simulation and correlated model setup #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4f67049
b5e1e34
4da647e
b80f01c
6136396
b03ff39
b74acce
6babf6b
b0da6a4
5d65537
4542495
57acb60
016630b
6b9f7c0
cea08b3
8b33b12
afce016
3fa3f7d
7a8bbce
7a2541c
07a8c69
cfa9ba9
1e99f82
e1fa818
7be5887
e31e676
da29284
57365b7
a2dfbd5
8264c15
9795759
63f55a8
64ed173
d02b6a3
60989cc
aa66c9e
b54fa85
9988e89
709dd60
00450c6
df911e7
1efbbcc
355d2cd
9279040
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,22 @@ | |||||||
| #' - t1 = time to peak | ||||||||
| #' - shape = shape parameter | ||||||||
| #' - alpha = decay rate | ||||||||
| #' @details | ||||||||
| #' When `correlated = TRUE`, `run_mod()` fits a Chapter-2 Kronecker prior | ||||||||
| #' across biomarkers: \eqn{\mathrm{Cov}(\mathrm{vec}(\Theta_i)) = | ||||||||
| #' \Sigma_P \otimes \Sigma_B}. | ||||||||
| #' The likelihood for observed antibody data is unchanged; only the prior | ||||||||
| #' covariance differs. Internally this mode: | ||||||||
| #' \itemize{ | ||||||||
| #' \item calls \code{clean_priors()} on the base priors from | ||||||||
| #' \code{prep_priors()}, | ||||||||
| #' \item adds Kronecker hyperpriors via \code{prep_priors_multi_b()} | ||||||||
| #' (OmegaP, nuP, OmegaB, nuB) | ||||||||
| #' and \code{n_blocks = <# biomarkers>}, | ||||||||
| #' \item uses \code{inits_kron()} to avoid conflicting legacy inits, | ||||||||
| #' \item and monitors \code{TauB} and \code{TauP} in addition to the | ||||||||
| #' core parameters. | ||||||||
| #' } | ||||||||
| #' @param data A [base::data.frame()] with the following columns. | ||||||||
| #' @param file_mod The name of the file that contains model structure. | ||||||||
| #' @param nchain An [integer] between 1 and 4 that specifies | ||||||||
|
|
@@ -26,6 +42,11 @@ | |||||||
| #' should be included as an element of the [list] object returned by `run_mod()` | ||||||||
| #' (see `Value` section below for details). | ||||||||
| #' Note: These objects can be large. | ||||||||
| #' @param correlated Logical; use Chapter-2 Kronecker prior across biomarkers. | ||||||||
| #' Default FALSE (independence). | ||||||||
| #' @param file_mod_kron Path to a JAGS file for the Kronecker model.If | ||||||||
| #' `correlated = TRUE` and this path does not exist, a temporary | ||||||||
| #' `model_ch2_kron.jags` is written and used automatically. | ||||||||
| #' @returns An `sr_model` class object: a subclass of [dplyr::tbl_df] that | ||||||||
| #' contains MCMC samples from the joint posterior distribution of the model | ||||||||
| #' parameters, conditional on the provided input `data`, | ||||||||
|
|
@@ -60,18 +81,22 @@ | |||||||
| #' - An optional `"jags.post"` attribute, included when argument | ||||||||
| #' `with_post` = TRUE. | ||||||||
| #' @inheritDotParams prep_priors | ||||||||
| #' @seealso clean_priors, prep_priors_multi_b, inits_kron, write_model_ch2_kron | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please cross-reference wherever possible:
Suggested change
|
||||||||
| #' @export | ||||||||
| #' @example inst/examples/run_mod-examples.R | ||||||||
| run_mod <- function(data, | ||||||||
| file_mod = serodynamics_example("model.jags"), | ||||||||
| nchain = 4, | ||||||||
| nadapt = 0, | ||||||||
| nburn = 0, | ||||||||
| nmc = 100, | ||||||||
| niter = 100, | ||||||||
| strat = NA, | ||||||||
| with_post = FALSE, | ||||||||
| ...) { | ||||||||
| file_mod = serodynamics_example("model.jags"), | ||||||||
| nchain = 4, | ||||||||
| nadapt = 0, | ||||||||
| nburn = 0, | ||||||||
| nmc = 100, | ||||||||
| niter = 100, | ||||||||
| strat = NA, | ||||||||
| with_post = FALSE, | ||||||||
| ..., | ||||||||
| correlated = FALSE, # (used when correlated = TRUE) | ||||||||
| file_mod_kron = "model_ch2_kron.jags" | ||||||||
| ) { | ||||||||
| ## Conditionally creating a stratification list to loop through | ||||||||
| if (is.na(strat)) { | ||||||||
| strat_list <- "None" | ||||||||
|
|
@@ -106,8 +131,41 @@ run_mod <- function(data, | |||||||
|
|
||||||||
| # prepare data for modeline | ||||||||
| longdata <- prep_data(dl_sub) | ||||||||
| priorspec <- prep_priors(max_antigens = longdata$n_antigen_isos, | ||||||||
| ...) | ||||||||
|
|
||||||||
| # ---------- CHOOSE MODEL/PRIORS DEPENDING ON `correlated` ---------- | ||||||||
| if (!correlated) { | ||||||||
| # original (independence) behavior | ||||||||
| priorspec <- prep_priors( | ||||||||
| max_antigens = longdata$n_antigen_isos, ... | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| ) | ||||||||
| model_path <- file_mod # UNCHANGED for independence | ||||||||
| init_fun <- initsfunction | ||||||||
| to_monitor <- c("y0", "y1", "t1", "alpha", "shape") | ||||||||
| } else { | ||||||||
| # CH2 behavior: Kronecker prior across biomarkers | ||||||||
| base_priors <- prep_priors( | ||||||||
| max_antigens = longdata$n_antigen_isos, ... | ||||||||
|
Comment on lines
+138
to
+147
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since and are the same function call, let's move them out of the if-else and combine them into a single call? |
||||||||
| ) | ||||||||
| base_priors <- serodynamics::clean_priors(base_priors) | ||||||||
|
|
||||||||
| kron_priors <- serodynamics::prep_priors_multi_b( | ||||||||
| n_blocks = longdata$n_antigen_isos | ||||||||
| ) | ||||||||
| B_scalar <- list(n_blocks = longdata$n_antigen_isos) | ||||||||
|
|
||||||||
| priorspec <- c(base_priors, kron_priors, B_scalar) | ||||||||
|
Comment on lines
+149
to
+156
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we decompose this section into a single function that takes |
||||||||
|
|
||||||||
| # Changed: use file_mod_kron when correlated = TRUE | ||||||||
| model_path <- file_mod_kron | ||||||||
| if (!file.exists(model_path)) { | ||||||||
| model_path <- serodynamics::write_model_ch2_kron( | ||||||||
| file.path(tempdir(), "model_ch2_kron.jags") | ||||||||
| ) | ||||||||
| } | ||||||||
|
|
||||||||
| init_fun <- function(chain) serodynamics::inits_kron(chain) | ||||||||
| to_monitor <- c("y0", "y1", "t1", "alpha", "shape", "TauB", "TauP") | ||||||||
| } | ||||||||
|
|
||||||||
| # inputs for jags model | ||||||||
| nchains <- nchain # nr of MC chains to run simultaneously | ||||||||
|
|
@@ -117,19 +175,17 @@ run_mod <- function(data, | |||||||
| niter <- niter # nr of iterations for posterior sample | ||||||||
| nthin <- round(niter / nmc) # thinning needed to produce nmc from niter | ||||||||
|
|
||||||||
| tomonitor <- c("y0", "y1", "t1", "alpha", "shape") | ||||||||
|
|
||||||||
| jags_post <- runjags::run.jags( | ||||||||
| model = file_mod, | ||||||||
| model = model_path, | ||||||||
| data = c(longdata, priorspec), | ||||||||
| inits = initsfunction, | ||||||||
| inits = init_fun, | ||||||||
| method = "parallel", | ||||||||
| adapt = nadapt, | ||||||||
| burnin = nburnin, | ||||||||
| thin = nthin, | ||||||||
| sample = nmc, | ||||||||
| n.chains = nchains, | ||||||||
| monitor = tomonitor, | ||||||||
| monitor = to_monitor, | ||||||||
| summarise = FALSE | ||||||||
| ) | ||||||||
| # Assigning the raw jags output to a list. | ||||||||
|
|
@@ -190,9 +246,11 @@ run_mod <- function(data, | |||||||
| current_atts <- c(current_atts, mod_atts) | ||||||||
| attributes(jags_out) <- current_atts | ||||||||
|
|
||||||||
| # Adding priors | ||||||||
| # Changed: Attach priors robustly | ||||||||
| used_priors <- attr(priorspec, "used_priors", exact = TRUE) | ||||||||
| if (is.null(used_priors)) used_priors <- names(priorspec) | ||||||||
| jags_out <- jags_out |> | ||||||||
| structure("priors" = attributes(priorspec)$used_priors) | ||||||||
| structure("priors" = used_priors) | ||||||||
|
|
||||||||
| # Calculating fitted and residuals | ||||||||
| # Renaming columns using attributes from as_case_data | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| #' @title Drop legacy/unused prior fields | ||
| #' @author Kwan Ho Lee | ||
| #' @description | ||
| #' `clean_priors()` removes elements from a priors list that are not | ||
| #' used by the Kronecker model. This helps avoid passing unused or | ||
| #' conflicting parameters (like `omega`, `wishdf`, or `prec.par`) into JAGS. | ||
| #' | ||
| #' @param x A [base::list()] of priors (e.g., from [prep_priors()]). | ||
| #' | ||
| #' @return A [base::list()] with the legacy fields removed. | ||
| #' | ||
| #' @export | ||
| #' @example inst/examples/examples-clean_priors.R | ||
| clean_priors <- function(x) { | ||
| drop <- intersect(names(x), c("omega", "wishdf", "Omega", "WishDF", | ||
| "prec.par")) | ||
| # keep a copy of attributes we care about | ||
| used <- attr(x, "used_priors", exact = TRUE) | ||
|
|
||
| y <- if (length(drop)) x[setdiff(names(x), drop)] else x | ||
|
|
||
| # restore attribute if it existed | ||
| if (!is.null(used)) attr(y, "used_priors") <- used | ||
|
|
||
| y | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| #' @title Safe inits for the Kronecker model | ||
| #' @author Kwan Ho Lee | ||
| #' @description | ||
| #' `inits_kron()` wraps a base initializer (if provided) and makes sure | ||
| #' that legacy pieces (`prec.par`) and Kronecker precision terms | ||
| #' (`TauB`, `TauP`) are not preset. This avoids conflicts when running | ||
| #' the multi-biomarker Kronecker model. | ||
| #' | ||
| #' @param chain Integer chain index passed through to the base inits function. | ||
| #' @param base_inits A function with signature `function(chain)` that returns a | ||
| #' named list of initial values. Defaults to a simple RNG seed initializer. | ||
| #' | ||
| #' @return A [base::list()] of inits suitable for \pkg{runjags}. | ||
| #' | ||
| #' @export | ||
| #' @example inst/examples/examples-inits_kron.R | ||
| inits_kron <- function(chain, | ||
| base_inits = NULL) { | ||
| if (is.null(base_inits)) { | ||
| base_inits <- function(ch) { | ||
| list( | ||
| .RNG.name = "base::Mersenne-Twister", | ||
| .RNG.seed = 123 + as.integer(ch) | ||
| ) | ||
| } | ||
| } | ||
| z <- base_inits(chain) | ||
| z$TauB <- NULL | ||
| z$TauP <- NULL | ||
| z$prec.par <- NULL | ||
| z | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||
| #' @title Priors for the Kronecker (multi-biomarker) model | ||||||||||||||
| #' @author Kwan Ho Lee | ||||||||||||||
| #' @description | ||||||||||||||
| #' `prep_priors_multi_b()` builds Wishart hyperparameters for the | ||||||||||||||
| #' within-biomarker precision matrix `T_P` and the across-biomarker precision | ||||||||||||||
| #' matrix `T_B` used in the Kronecker prior `T = T_B %x% T_P`. | ||||||||||||||
| #' | ||||||||||||||
| #' @param n_blocks Integer scalar (B): number of biomarkers. | ||||||||||||||
| #' @param omega_p_scale Numeric length-5 vector for the diagonal of Omega_P | ||||||||||||||
| #' (parameter scale). | ||||||||||||||
| #' @param nu_p Numeric scalar: degrees of freedom for | ||||||||||||||
| #' `T_P ~ Wishart(Omega_P, nu_p)`. | ||||||||||||||
| #' @param omega_b_scale Numeric length-`n_blocks` vector for the diagonal of | ||||||||||||||
| #' Omega_B (biomarker scale). | ||||||||||||||
| #' @param nu_b Numeric scalar: degrees of freedom for | ||||||||||||||
| #' `T_B ~ Wishart(Omega_B, nu_b)`. | ||||||||||||||
| #' | ||||||||||||||
| #' @return A list with elements `OmegaP`, `nuP`, `OmegaB`, `nuB`. | ||||||||||||||
| #' @export | ||||||||||||||
| #' @example inst/examples/examples-prep_priors_multi_b.R | ||||||||||||||
| prep_priors_multi_b <- function( | ||||||||||||||
| n_blocks, | ||||||||||||||
| omega_p_scale = rep(0.1, 5), | ||||||||||||||
| nu_p = 6, | ||||||||||||||
| omega_b_scale = rep(1.0, n_blocks), | ||||||||||||||
| nu_b = n_blocks + 1 | ||||||||||||||
| ) { | ||||||||||||||
| # validations (cli::cli_abort is linter-approved) | ||||||||||||||
| if (!is.numeric(n_blocks) || length(n_blocks) != 1L || n_blocks < 1 || | ||||||||||||||
| !isTRUE(all.equal(n_blocks, as.integer(n_blocks)))) { | ||||||||||||||
| cli::cli_abort("`n_blocks` must be a positive integer of length 1.") | ||||||||||||||
| } | ||||||||||||||
| if (!is.numeric(omega_p_scale) || length(omega_p_scale) != 5L) { | ||||||||||||||
| cli::cli_abort("`omega_p_scale` must be a numeric vector of length 5.") | ||||||||||||||
| } | ||||||||||||||
| if (!is.numeric(omega_b_scale) || length(omega_b_scale) != n_blocks) { | ||||||||||||||
| cli::cli_abort("`omega_b_scale` must be a numeric vector of length | ||||||||||||||
| `n_blocks`.") | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+37
to
+39
|
||||||||||||||
| cli::cli_abort("`omega_b_scale` must be a numeric vector of length | |
| `n_blocks`.") | |
| } | |
| cli::cli_abort("`omega_b_scale` must be a numeric vector of length `n_blocks`.") | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after period in 'model.If' - should be 'model. If'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Kwan-Jenny please consider this suggestion and explain your decision one way or the other?