@@ -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
0 commit comments