| name | diagnostic-accuracy |
| description | Diagnostic accuracy analysis in R, including sensitivity, specificity, ROC curves, likelihood ratios, and decision curves. |
Diagnostic Accuracy Analysis in R
Overview
Comprehensive diagnostic test accuracy analysis covering ROC curve analysis, optimal cutpoint determination, sensitivity and specificity estimation, likelihood ratios, decision curve analysis, inter-rater reliability measures, and diagnostic meta-analysis.
Basic Diagnostic Measures
2x2 Table Analysis
library(epiR)
diag_table <- matrix(c(85, 15,
20, 180),
nrow = 2, byrow = TRUE,
dimnames = list(
Test = c("Positive", "Negative"),
Disease = c("Present", "Absent")
))
results <- epi.tests(as.table(diag_table), method = "exact")
print(results)
results$detail
Manual Calculations
TP <- 85; FN <- 15; FP <- 20; TN <- 180
sensitivity <- TP / (TP + FN)
specificity <- TN / (TN + FP)
ppv <- TP / (TP + FP)
npv <- TN / (TN + FN)
lr_pos <- sensitivity / (1 - specificity)
lr_neg <- (1 - sensitivity) / specificity
dor <- (TP * TN) / (FP * FN)
youden <- sensitivity + specificity - 1
accuracy <- (TP + TN) / (TP + TN + FP + FN)
library(binom)
se_ci <- binom.confint(TP, TP + FN, method = "wilson")
sp_ci <- binom.confint(TN, TN + FP, method = "wilson")
ROC Curve Analysis
Basic ROC Curve
library(pROC)
roc_obj <- roc(
response = df$disease,
predictor = df$biomarker,
levels = c(0, 1),
direction = "<"
)
print(roc_obj)
auc(roc_obj)
ci.auc(roc_obj, method = "delong")
ci.auc(roc_obj, method = "bootstrap", boot.n = 2000)
ROC Curve Plotting
library(pROC)
plot(roc_obj, print.auc = TRUE, print.thres = TRUE)
ggroc(roc_obj) +
geom_abline(intercept = 1, slope = 1, linetype = "dashed", color = "gray") +
annotate("text", x = 0.25, y = 0.25,
label = paste0("AUC = ", round(auc(roc_obj), 3))) +
theme_bw() +
labs(x = "Specificity", y = "Sensitivity",
title = "ROC Curve for Biomarker X")
roc1 <- roc(df$disease, df$biomarker1)
roc2 <- roc(df$disease, df$biomarker2)
roc3 <- roc(df$disease, df$biomarker3)
ggroc(list(Biomarker1 = roc1, Biomarker2 = roc2, Biomarker3 = roc3)) +
geom_abline(intercept = 1, slope = 1, linetype = "dashed") +
theme_bw() +
scale_color_brewer(palette = "Set1")
Comparing ROC Curves
library(pROC)
roc_test <- roc.test(roc1, roc2, method = "delong")
print(roc_test)
roc_test_boot <- roc.test(roc1, roc2, method = "bootstrap", boot.n = 2000)
roc_test_venk <- roc.test(roc1, roc2, method = "venkatraman")
comparison <- data.frame(
Marker = c("Biomarker1", "Biomarker2", "Biomarker3"),
AUC = c(auc(roc1), auc(roc2), auc(roc3)),
CI_Lower = c(ci.auc(roc1)[1], ci.auc(roc2)[1], ci.auc(roc3)[1]),
CI_Upper = c(ci.auc(roc1)[3], ci.auc(roc2)[3], ci.auc(roc3)[3])
)
Partial AUC
library(pROC)
pauc_spec <- auc(roc_obj, partial.auc = c(1, 0.9),
partial.auc.focus = "specificity")
pauc_sens <- auc(roc_obj, partial.auc = c(0.9, 1),
partial.auc.focus = "sensitivity")
pauc_std <- auc(roc_obj, partial.auc = c(1, 0.9),
partial.auc.focus = "specificity",
partial.auc.correct = TRUE)
ci.auc(roc_obj, partial.auc = c(1, 0.9),
partial.auc.focus = "specificity")
Optimal Cutpoint Selection
Using cutpointr Package
library(cutpointr)
cp_youden <- cutpointr(
data = df,
x = biomarker,
class = disease,
method = maximize_metric,
metric = youden
)
summary(cp_youden)
plot(cp_youden)
cp_constrained <- cutpointr(
df, biomarker, disease,
method = maximize_metric,
metric = sensitivity,
tol_metric = specificity,
tol_threshold = 0.9
)
cp_cost <- cutpointr(
df, biomarker, disease,
method = minimize_metric,
metric = misclassification_cost,
cost_fp = 1,
cost_fn = 5
)
cp_multi <- multi_cutpointr(
df, biomarker, disease,
method = maximize_metric,
metric = youden,
boot_cut = 1000
)
Using OptimalCutpoints Package
library(OptimalCutpoints)
opt_cut <- optimal.cutpoints(
X = "biomarker",
status = "disease",
methods = c("Youden", "MaxSpSe", "MaxProdSpSe", "ROC01",
"MinValueSp", "MinValueSe", "MaxEfficiency"),
data = df,
tag.healthy = 0
)
summary(opt_cut)
opt_cost <- optimal.cutpoints(
X = "biomarker",
status = "disease",
methods = "CB",
data = df,
tag.healthy = 0,
costs.ratio = 5,
prevalence = 0.10
)
Manual Cutpoint Selection
library(pROC)
coords_youden <- coords(roc_obj, "best", best.method = "youden")
print(coords_youden)
coords_closest <- coords(roc_obj, "best", best.method = "closest.topleft")
coords_se90 <- coords(roc_obj, x = 0.90, input = "sensitivity",
ret = c("threshold", "sensitivity", "specificity"))
all_coords <- coords(roc_obj, x = "all",
ret = c("threshold", "sensitivity", "specificity",
"ppv", "npv", "accuracy"))
Decision Curve Analysis
Using dcurves Package
library(dcurves)
model1 <- glm(cancer ~ age + psa, data = df, family = binomial)
model2 <- glm(cancer ~ age + psa + dre, data = df, family = binomial)
df$pred1 <- predict(model1, type = "response")
df$pred2 <- predict(model2, type = "response")
dca_result <- dca(
cancer ~ pred1 + pred2,
data = df,
thresholds = seq(0, 0.5, by = 0.01),
label = list(pred1 = "PSA Model", pred2 = "PSA + DRE Model")
)
plot(dca_result)
plot(dca_result, smooth = TRUE) +
ggplot2::coord_cartesian(ylim = c(-0.05, 0.2)) +
ggplot2::labs(x = "Threshold Probability",
y = "Net Benefit",
title = "Decision Curve Analysis")
Net Benefit Calculation
library(dcurves)
nb_data <- as_tibble(dca_result)
net_intervention_avoided(dca_result)
standardized_net_benefit <- function(nb, prevalence, threshold) {
max_nb <- prevalence - (1 - prevalence) * threshold / (1 - threshold)
nb / max_nb
}
Clinical Utility Visualization
library(dcurves)
dca_result |>
plot(type = "clinical_impact")
dca_boot <- dca(
cancer ~ pred1,
data = df,
thresholds = seq(0, 0.5, by = 0.05)
)
Inter-Rater Reliability
Cohen's Kappa (Two Raters)
library(irr)
ratings <- data.frame(
rater1 = c(1, 2, 3, 1, 2, 3, 1, 2, 3, 1),
rater2 = c(1, 2, 3, 1, 2, 2, 1, 3, 3, 2)
)
kappa_unweighted <- kappa2(ratings, weight = "unweighted")
print(kappa_unweighted)
kappa_linear <- kappa2(ratings, weight = "equal")
kappa_quadratic <- kappa2(ratings, weight = "squared")
Fleiss' Kappa (Multiple Raters)
library(irr)
ratings_multi <- matrix(c(
1, 1, 1, 2,
2, 2, 2, 2,
3, 3, 2, 3,
1, 1, 1, 1,
2, 3, 2, 2
), nrow = 5, byrow = TRUE)
fleiss_k <- kappam.fleiss(ratings_multi)
print(fleiss_k)
light_k <- kappam.light(ratings_multi)
Intraclass Correlation Coefficient (ICC)
library(irr)
measurements <- data.frame(
rater1 = c(2.5, 3.1, 4.2, 2.8, 3.5),
rater2 = c(2.4, 3.3, 4.0, 2.9, 3.4),
rater3 = c(2.6, 3.0, 4.1, 2.7, 3.6)
)
icc_result <- icc(measurements, model = "twoway", type = "agreement", unit = "single")
print(icc_result)
icc_avg <- icc(measurements, model = "twoway", type = "consistency", unit = "average")
Agreement for Continuous Data
library(BlandAltmanLeh)
ba <- bland.altman.stats(
method1 = df$measurement1,
method2 = df$measurement2
)
bland.altman.plot(
method1 = df$measurement1,
method2 = df$measurement2,
main = "Bland-Altman Plot",
xlab = "Mean of Methods",
ylab = "Difference (Method 1 - Method 2)"
)
ba$mean.diffs
ba$lower.limit
ba$upper.limit
library(ggplot2)
df_ba <- data.frame(
mean = (df$method1 + df$method2) / 2,
diff = df$method1 - df$method2
)
ggplot(df_ba, aes(x = mean, y = diff)) +
geom_point() +
geom_hline(yintercept = mean(df_ba$diff), color = "blue") +
geom_hline(yintercept = mean(df_ba$diff) + 1.96 * sd(df_ba$diff),
linetype = "dashed", color = "red") +
geom_hline(yintercept = mean(df_ba$diff) - 1.96 * sd(df_ba$diff),
linetype = "dashed", color = "red") +
labs(title = "Bland-Altman Plot",
x = "Mean of Two Methods",
y = "Difference Between Methods") +
theme_bw()
Diagnostic Meta-Analysis
Bivariate Model
library(mada)
diag_ma_data <- data.frame(
study = paste("Study", 1:10),
TP = c(45, 38, 52, 41, 55, 48, 39, 44, 50, 47),
FN = c(5, 7, 8, 9, 5, 7, 11, 6, 5, 8),
FP = c(8, 12, 10, 15, 7, 11, 14, 9, 12, 10),
TN = c(142, 143, 130, 135, 133, 134, 136, 141, 133, 135)
)
fit <- reitsma(diag_ma_data)
summary(fit)
sens_summary <- plogis(coef(fit)["tsens.(Intercept)"])
spec_summary <- plogis(-coef(fit)["tfpr.(Intercept)"])
plot(fit, sroclwd = 2, main = "SROC Curve")
points(fit)
HSROC Model
library(mada)
fit_hsroc <- reitsma(diag_ma_data, method = "ml")
crosshair(fit_hsroc)
Forest Plots for Diagnostic MA
library(mada)
forest(madad(diag_ma_data), type = "sens",
main = "Sensitivity by Study")
forest(madad(diag_ma_data), type = "spec",
main = "Specificity by Study")
study_est <- madad(diag_ma_data)
print(study_est)
Publication Bias in Diagnostic MA
library(mada)
deeks_test <- mada:::.deeks(diag_ma_data)
Three-Group ROC Analysis
library(DiagTest3Grp)
vus_result <- VUS(
marker = df$biomarker,
group = df$severity
)
print(vus_result)
cut3 <- DiagTest3Grp.optimalCutoff(
x = df$biomarker,
group = df$severity
)
Pre-test and Post-test Probability
calculate_post_test_prob <- function(pre_test_prob, lr) {
pre_test_odds <- pre_test_prob / (1 - pre_test_prob)
post_test_odds <- pre_test_odds * lr
post_test_prob <- post_test_odds / (1 + post_test_odds)
return(post_test_prob)
}
pre_test <- 0.10
lr_positive <- 5
lr_negative <- 0.1
post_test_pos <- calculate_post_test_prob(pre_test, lr_positive)
post_test_neg <- calculate_post_test_prob(pre_test, lr_negative)
cat("Pre-test probability:", pre_test, "\n")
cat("Post-test probability (positive test):", round(post_test_pos, 3), "\n")
cat("Post-test probability (negative test):", round(post_test_neg, 3), "\n")
Fagan Nomogram
library(ggplot2)
fagan_nomogram <- function(pre_test_prob, lr_pos, lr_neg) {
pre_odds <- pre_test_prob / (1 - pre_test_prob)
post_odds_pos <- pre_odds * lr_pos
post_odds_neg <- pre_odds * lr_neg
post_prob_pos <- post_odds_pos / (1 + post_odds_pos)
post_prob_neg <- post_odds_neg / (1 + post_odds_neg)
cat("Pre-test probability:", round(pre_test_prob * 100, 1), "%\n")
cat("LR+:", lr_pos, "-> Post-test probability:", round(post_prob_pos * 100, 1), "%\n")
cat("LR-:", lr_neg, "-> Post-test probability:", round(post_prob_neg * 100, 1), "%\n")
return(list(
pre_test = pre_test_prob,
post_test_positive = post_prob_pos,
post_test_negative = post_prob_neg
))
}
fagan_nomogram(pre_test_prob = 0.20, lr_pos = 8, lr_neg = 0.15)
Reporting Diagnostic Study Results
create_diagnostic_report <- function(roc_obj, cutpoint, df) {
pred_class <- ifelse(df$biomarker >= cutpoint, 1, 0)
cm <- table(Predicted = pred_class, Actual = df$disease)
TP <- cm[2, 2]; FN <- cm[1, 2]
FP <- cm[2, 1]; TN <- cm[1, 1]
metrics <- data.frame(
Metric = c("AUC", "Cutpoint", "Sensitivity", "Specificity",
"PPV", "NPV", "LR+", "LR-", "Accuracy", "Youden Index"),
Value = c(
round(auc(roc_obj), 3),
round(cutpoint, 2),
round(TP / (TP + FN), 3),
round(TN / (TN + FP), 3),
round(TP / (TP + FP), 3),
round(TN / (TN + FN), 3),
round((TP / (TP + FN)) / (FP / (FP + TN)), 2),
round((FN / (TP + FN)) / (TN / (FP + TN)), 2),
round((TP + TN) / (TP + TN + FP + FN), 3),
round(TP / (TP + FN) + TN / (TN + FP) - 1, 3)
)
)
return(metrics)
}
Key Packages Summary
| Package | Purpose |
|---|
| pROC | ROC curve analysis and AUC |
| cutpointr | Optimal cutpoint selection |
| OptimalCutpoints | Multiple cutpoint methods |
| dcurves | Decision curve analysis |
| irr | Inter-rater reliability (kappa, ICC) |
| mada | Diagnostic meta-analysis |
| BlandAltmanLeh | Method agreement plots |
| epiR | Diagnostic test evaluation |
| DiagTest3Grp | Three-group ROC analysis |
| caret | Confusion matrix utilities |
Best Practices
- Report multiple metrics: Sensitivity, specificity, PPV, NPV, and likelihood ratios
- Account for prevalence: PPV/NPV depend heavily on disease prevalence
- Use appropriate cutpoint method: Consider clinical consequences (cost of FN vs FP)
- Provide confidence intervals: Especially for AUC and diagnostic measures
- Check calibration: Predicted probabilities should match observed frequencies
- Decision curve analysis: Evaluates clinical utility across threshold range
- Consider spectrum bias: Ensure representative disease severity range
- Report according to STARD: Standards for Reporting of Diagnostic Accuracy Studies