Skip to content

Commit 3165fbe

Browse files
committed
Added ne wvignettes and COP modelling
1 parent 88acec7 commit 3165fbe

File tree

11 files changed

+2982
-43
lines changed

11 files changed

+2982
-43
lines changed

R/SeroCOP.R

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,54 @@ SeroCOP <- R6::R6Class(
243243
predictions <- matrix(NA, nrow = n_iter, ncol = n_new)
244244

245245
for (i in 1:n_iter) {
246-
predictions[i, ] <- params$floor[i] +
247-
(params$ceiling[i] - params$floor[i]) /
248-
(1 + exp(params$slope[i] * (newdata - params$ec50[i])))
246+
# Using the new formula: ceiling * (floor + (1-floor) * inv_logit(-slope * (titre - ec50)))
247+
logit_part <- 1 / (1 + exp(params$slope[i] * (newdata - params$ec50[i])))
248+
predictions[i, ] <- params$ceiling[i] * (params$floor[i] + (1 - params$floor[i]) * logit_part)
249249
}
250250

251251
return(predictions)
252252
}
253253
},
254254

255+
#' @description
256+
#' Extract probability of protection from the fitted model
257+
#' @param newdata Optional vector of new titre values for prediction
258+
#' @return Matrix of protection probabilities (rows = MCMC samples, cols = observations)
259+
#' @examples
260+
#' sero <- SeroCOP$new()
261+
#' sero$fit_model()
262+
#' protection <- sero$predict_protection()
263+
predict_protection = function(newdata = NULL) {
264+
if (is.null(self$fit)) {
265+
stop("Model has not been fitted yet. Run fit_model() first.")
266+
}
267+
268+
if (is.null(newdata)) {
269+
# Extract fitted protection probabilities
270+
prob_protection <- rstan::extract(self$fit, pars = "prob_protection")[[1]]
271+
return(prob_protection)
272+
} else {
273+
# Predict protection for new data
274+
# Get infection probabilities first
275+
prob_infection <- self$predict(newdata = newdata)
276+
277+
# Extract ceiling samples
278+
params <- rstan::extract(self$fit)
279+
ceiling_samples <- params$ceiling
280+
281+
# Calculate protection: 1 - (prob_infection / ceiling)
282+
n_iter <- nrow(prob_infection)
283+
n_new <- ncol(prob_infection)
284+
prob_protection <- matrix(NA, nrow = n_iter, ncol = n_new)
285+
286+
for (i in 1:n_iter) {
287+
prob_protection[i, ] <- 1 - (prob_infection[i, ] / ceiling_samples[i])
288+
}
289+
290+
return(prob_protection)
291+
}
292+
},
293+
255294
#' @description
256295
#' Get summary statistics for model parameters
257296
#' @return Data frame with parameter summaries
@@ -482,6 +521,29 @@ SeroCOP <- R6::R6Class(
482521
)
483522

484523
return(p)
524+
},
525+
526+
#' @description
527+
#' Extract the correlate of protection conditional on exposure.
528+
#' @param correlate_of_risk Numeric vector of correlates of risk.
529+
#' @param upper_bound Numeric value for the upper bound (default: 0.7).
530+
#' @return Numeric vector of correlates of protection.
531+
#' @examples
532+
#' sero <- SeroCOP$new(titre = titre, infected = infected)
533+
#' sero$fit_model()
534+
#' cor <- sero$extract_cop(correlate_of_risk = c(0.1, 0.2), upper_bound = 0.7)
535+
extract_cop = function(correlate_of_risk, upper_bound = 0.7) {
536+
if (missing(correlate_of_risk)) {
537+
stop("correlate_of_risk must be provided")
538+
}
539+
if (!is.numeric(correlate_of_risk) || any(correlate_of_risk < 0 | correlate_of_risk > 1)) {
540+
stop("correlate_of_risk must be a numeric vector with values between 0 and 1")
541+
}
542+
if (!is.numeric(upper_bound) || upper_bound <= 0) {
543+
stop("upper_bound must be a positive numeric value")
544+
}
545+
cop <- (1 - correlate_of_risk) / upper_bound
546+
return(cop)
485547
}
486548
),
487549

R/SeroCOPMulti.R

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,33 @@ SeroCOPMulti <- R6::R6Class(
300300
)
301301

302302
return(p)
303+
},
304+
305+
#' @description
306+
#' Extract the correlate of protection conditional on exposure for all biomarkers.
307+
#' @param correlate_of_risk_list List of numeric vectors of correlates of risk for each biomarker.
308+
#' @param upper_bound Numeric value for the upper bound (default: 0.7).
309+
#' @return List of numeric vectors of correlates of protection for each biomarker.
310+
#' @examples
311+
#' multi_model <- SeroCOPMulti$new(titre = titre_matrix, infected = infected)
312+
#' multi_model$fit_all()
313+
#' cor_list <- list(IgG = c(0.1, 0.2), IgA = c(0.3, 0.4))
314+
#' cop_list <- multi_model$extract_cop_multi(correlate_of_risk_list = cor_list, upper_bound = 0.7)
315+
extract_cop_multi = function(correlate_of_risk_list, upper_bound = 0.7) {
316+
if (!is.list(correlate_of_risk_list)) {
317+
stop("correlate_of_risk_list must be a list of numeric vectors")
318+
}
319+
if (!is.numeric(upper_bound) || upper_bound <= 0) {
320+
stop("upper_bound must be a positive numeric value")
321+
}
322+
cop_list <- lapply(correlate_of_risk_list, function(cor) {
323+
if (!is.numeric(cor) || any(cor < 0 | cor > 1)) {
324+
stop("Each correlate_of_risk must be a numeric vector with values between 0 and 1")
325+
}
326+
1 - (cor / upper_bound)
327+
})
328+
names(cop_list) <- names(correlate_of_risk_list)
329+
return(cop_list)
303330
}
304331
)
305332
)

inst/stan/logistic_model.stan

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Four-parameter logistic model for correlates of protection
22
// Models the probability of infection as a function of antibody titre
33
// Parameters:
4-
// - floor: lower asymptote (minimum infection probability)
5-
// - ceiling: upper asymptote (maximum infection probability)
6-
// - ec50: titre at 50% between floor and ceiling (inflection point)
7-
// - slope: steepness of the curve
4+
// - floor: proportion of maximum risk remaining at high titre (relative protection at high titre)
5+
// - ceiling: maximum infection probability (at low antibody titre)
6+
// - ec50: titre at inflection point (50% reduction from ceiling to ceiling*floor)
7+
// - slope: steepness of the protective curve (higher = steeper decline in risk with titre)
88

99
data {
1010
int<lower=0> N; // number of observations
@@ -29,18 +29,21 @@ data {
2929
}
3030

3131
parameters {
32-
real<lower=0,upper=1> floor; // lower asymptote
33-
real<lower=0,upper=1> ceiling; // upper asymptote
34-
real ec50; // titre at midpoint
35-
real<lower=0> slope; // slope parameter (steepness)
32+
real<lower=0,upper=1> floor; // proportion of maximum risk at high titre
33+
real<lower=0,upper=1> ceiling; // maximum infection probability at low titre
34+
real ec50; // titre at inflection point
35+
real<lower=0> slope; // steepness of protective curve
3636
}
3737

3838
transformed parameters {
3939
vector[N] prob_infection;
40-
40+
vector[N] prob_protection;
4141
// Four-parameter logistic function
42+
// At low titre: prob → ceiling (maximum risk)
43+
// At high titre: prob → ceiling * floor (minimum risk, as proportion of ceiling)
4244
for (n in 1:N) {
43-
prob_infection[n] = floor + (ceiling - floor) / (1 + exp(slope * (titre[n] - ec50)));
45+
prob_infection[n] = ceiling * (inv_logit(-slope * (titre[n] - ec50)) * (1 - floor) + floor);
46+
prob_protection[n] = 1 - (prob_infection[n] / ceiling);
4447
}
4548
}
4649

0 commit comments

Comments
 (0)