Just to close this off, I have put all of this example code together here, in case useful for others. (hopefully I haven’t introduced and errors!)
Thanks again for all the assistance.
#required libraries
library(tidyverse)
library(brms)
library(tidybayes)
library(marginaleffects)
#load some data
d <- ISLR::Default
#prepare data
#note rescaling balance to between 0:100, just to match my real data
d <- d %>%
mutate(default = factor(case_when(
default=="Yes" ~ 1,
default=="No" ~ 0))) %>%
mutate(balance_z = round((balance - min(d$balance)) / (max(d$balance) - min(d$balance)) * 100)) %>%
mutate(pid = row_number())
#what does the distribution of threshold scores look like?
d %>%
ggplot() +
geom_density(aes(x=balance_z, colour=default, fill=default), alpha=0.5) +
scale_fill_manual(values = c("mediumseagreen", "purple")) +
scale_colour_manual(values = c("mediumseagreen", "purple"))

#regression model
m1 <- brm(
default ~ mo(balance_z),
data = d,
family=bernoulli(),
backend="cmdstanr",
cores=4,
chains=4
)
#function to summarise draws
brms_summary <- function(model, data) {
summary <- model %>%
posterior_epred(
newdata = data
) %>%
summarise_draws("mean", ~quantile(.x, probs = c(.025, .975))) %>%
rename(meanY = mean,
mean2_5 = `2.5%`,
mean97_5 = `97.5%`)
bind_cols(data, summary)
}
#sensitivity and specificity function
#calculates sensitivity and specificity against reference standard for a predictor above a particular threshold
#TP=True Positive
#FP=False Positive
#TN=True Negative
#FN=False Negative
#Sensitivity= TP/(TP+FN)
#Specificity= TN/(TN+FP)
sens_spec_func <- function(data, outcome, predictor, threshold){
data %>%
summarise(
#sensitivity calculations
sensitivity_mean = sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==1), meanY, 0)) /#TP
(sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==1), meanY, 0)) +
(sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==1), meanY, 0)))),#TPs/(TPs+FNs)
sensitivity_lb = sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==1), mean2_5, 0)) /#TP
(sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==1), mean2_5, 0)) +
(sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==1), mean2_5, 0)))), #TPs/(TPs+FNs)
sensitivity_ub = sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==1), mean97_5, 0)) /#TP
(sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==1), mean97_5, 0)) +
(sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==1), mean97_5, 0)))), #TPs/(TPs+FNs)
#specificity calculations
specificity_mean = sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==0), meanY, 0)) /#TN
(sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==0), meanY, 0)) +
(sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==0), meanY, 0)))), #TNs/(TNs+FPs)
specificity_lb = sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==0), mean2_5, 0)) /#TN
(sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==0), mean2_5, 0)) +
(sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==0), mean2_5, 0)))), #TNs/(TNs+FPs)
specificity_ub = sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==0), mean97_5, 0)) /#TN
(sum(if_else(({{predictor}} < {{threshold}} & {{outcome}}==0), mean97_5, 0)) +
(sum(if_else(({{predictor}} >= {{threshold}} & {{outcome}}==0), mean97_5, 0)))) #TNs/(TNs+FPs)
)
}
#test it out for one threshold value of `balance_z`
#Set an example threshold for balance_z
balance_z_threshold <- 50
#run the function
brms_summary(m1, d) %>%
sens_spec_func(data=., outcome=default, predictor=balance_z, threshold=balance_z_threshold) %>%
mutate(balance_z_threshold=balance_z_threshold)
#> sensitivity_mean sensitivity_lb sensitivity_ub specificity_mean
#> 1 0.9959157 0.9969205 0.9948712 0.1649944
#> specificity_lb specificity_ub balance_z_threshold
#> 1 0.1294146 0.1963148 50
#Now wrap all of this up into a single function
accuracy_func <- function(data, model, predictor, outcome, threshold){
brms_summary(model, data) %>%
sens_spec_func(data=., outcome={{outcome}}, predictor={{predictor}}, threshold={{threshold}}) %>%
mutate(balance_z_threshold={{threshold}})
}
#test it out for one value of balance_z_threshold
accuracy_func(data=d, model=m1, predictor=balance_z, outcome=default, threshold=50)
#> sensitivity_mean sensitivity_lb sensitivity_ub specificity_mean
#> 1 0.9959157 0.9969205 0.9948712 0.1649944
#> specificity_lb specificity_ub balance_z_threshold
#> 1 0.1294146 0.1963148 50
#make a vector of thresholds
threshold_vec <- seq(from=0, to=100, by=1)
#run the function for all values of `balance_z_threshold`
all_results <- map_df(threshold_vec, ~accuracy_func(data = d, model = m1, predictor=balance_z, outcome=default, threshold = .x))
#now plot
all_results %>%
ggplot() +
geom_ribbon(aes(x=balance_z_threshold, ymin=sensitivity_lb, ymax=sensitivity_ub, fill="Sensitivity"), alpha=0.5) +
geom_ribbon(aes(x=balance_z_threshold, ymin=specificity_lb, ymax=specificity_ub, fill="Specificity"), alpha=0.5) +
geom_line(aes(x=balance_z_threshold, y=sensitivity_mean, colour="Sensitivity")) +
geom_line(aes(x=balance_z_threshold, y=specificity_mean, colour="Specificity")) +
scale_colour_manual(values = c("mediumseagreen", "purple"), name="") +
scale_fill_manual(values = c("mediumseagreen", "purple"), name="") +
scale_y_continuous(labels=scales::percent, name="")

Created on 2023-09-05 with reprex v2.0.2