ROC Curves and Diagnostic Accuracy with ggrocplot()

A receiver operating characteristic (ROC) curve shows how well a continuous marker or score separates two groups, across every possible cut-point. The area under the curve (AUC) summarizes it in one number: 0.5 is no better than chance, 1.0 is perfect. ggrocplot() draws a publication-ready ROC with the AUC and its confidence interval annotated, and can overlay several markers for comparison – all in base R, with no modeling dependency.

library(ggpubr)

# A small biomarker study: disease status and two candidate markers
set.seed(2024)
n <- 120
dx <- data.frame(
  status = factor(
    rep(c("healthy", "diseased"), each = n / 2),
    levels = c("healthy", "diseased") # "diseased" = the positive class
  ),
  marker_A = c(rnorm(n / 2, 0), rnorm(n / 2, 1.4)),
  marker_B = c(rnorm(n / 2, 0), rnorm(n / 2, 0.6))
)

A single ROC curve

Pass the outcome (response) and the marker (predictor). The AUC and its 95% confidence interval – computed in closed form (Hanley and McNeil, 1982) – are printed on the plot.

ggrocplot(dx, response = "status", predictor = "marker_A")

The curve bows toward the top-left corner; the further from the diagonal (the grey chance line), the better the marker. Here the AUC is well above 0.5, so higher marker_A values indicate disease.

Which way round? The positive class and AUC below 0.5

ggrocplot() treats the second factor level of response as the positive class and assumes higher predictor values indicate it – it never silently flips the curve. If a marker is lower in cases, its AUC comes out below 0.5 and you get a message suggesting you reverse the predictor (or the factor levels):

# A protective marker: LOWER values indicate disease
dx$marker_low <- c(rnorm(n / 2, 1), rnorm(n / 2, 0))

ggrocplot(dx, response = "status", predictor = "marker_low")
#> ggrocplot: predictor 'marker_low' has an AUC below 0.5; higher values indicate the negative class. If unexpected, reverse the predictor or set the response factor levels so the positive class is the second level.

Negate the predictor to orient it (transform() a new column, or pass a pre-negated one) and the AUC flips above 0.5:

dx$marker_low_rev <- -dx$marker_low
ggrocplot(dx, response = "status", predictor = "marker_low_rev")

The optimal cut-point

For a diagnostic threshold, youden = TRUE marks the point that maximizes the Youden index (sensitivity + specificity - 1) – the cut-point furthest above the chance line – and labels its value:

ggrocplot(dx, response = "status", predictor = "marker_A", youden = TRUE)

Comparing several markers

Pass several predictors to overlay their curves on one plot; the legend shows each marker’s AUC and CI, so you can compare discriminative ability at a glance:

ggrocplot(
  dx,
  response = "status",
  predictor = c("marker_A", "marker_B"),
  palette = "jco"
)

marker_A (the higher AUC) is the better discriminator. Customize with palette, linetype, size, legend/legend.title, and toggle the diagonal (diag), the AUC print-out (print.auc) or the CI (ci).

Notes

  • The empirical ROC, the AUC (equal to the Mann-Whitney statistic) and the large-sample CI are all computed in base R, so ggrocplot() has no modeling dependency and runs in lightweight environments.
  • For DeLong or bootstrap confidence intervals, ROC smoothing, or formal tests between curves, use the pROC package.

See ?ggrocplot for the full argument list.