{"spec_id":"roc-curve","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' roc-curve: ROC Curve with AUC\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\nlibrary(scales)\n\nset.seed(42)\n\n# --- Theme tokens -------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data -----------------------------------------------------------------\n# Diagnostic test scores for two candidate classifiers separating\n# disease-positive from disease-negative patients.\nn_patients <- 400\ndisease <- rbinom(n_patients, 1, 0.4)\n\nscore_logistic <- ifelse(disease == 1,\n                          rnorm(n_patients, mean = 2.2, sd = 1.0),\n                          rnorm(n_patients, mean = 0.0, sd = 1.0))\nscore_forest <- ifelse(disease == 1,\n                        rnorm(n_patients, mean = 1.2, sd = 1.1),\n                        rnorm(n_patients, mean = 0.0, sd = 1.1))\n\nroc_points <- function(scores, labels) {\n  thresholds <- sort(unique(c(scores, Inf, -Inf)), decreasing = TRUE)\n  n_pos <- sum(labels == 1)\n  n_neg <- sum(labels == 0)\n  tpr <- sapply(thresholds, function(t) sum(scores >= t & labels == 1) / n_pos)\n  fpr <- sapply(thresholds, function(t) sum(scores >= t & labels == 0) / n_neg)\n  tibble::tibble(fpr = fpr, tpr = tpr)\n}\n\nmodel_names <- c(\"Logistic Regression\", \"Random Forest\")\n\nroc_df <- bind_rows(\n  roc_points(score_logistic, disease) %>% mutate(model = model_names[1]),\n  roc_points(score_forest, disease) %>% mutate(model = model_names[2])\n) %>%\n  mutate(model = factor(model, levels = model_names))\n\n# Trapezoidal-rule AUC as a single vectorized expression per model (fpr is\n# already monotonic non-decreasing within each model from roc_points()).\nauc_df <- roc_df %>%\n  group_by(model) %>%\n  summarise(auc = sum(diff(fpr) * (head(tpr, -1) + tail(tpr, -1)) / 2), .groups = \"drop\") %>%\n  mutate(label = sprintf(\"%s (AUC = %.2f)\", model, auc))\n\nroc_df <- roc_df %>%\n  left_join(select(auc_df, model, label), by = \"model\") %>%\n  mutate(label = factor(label, levels = auc_df$label))\n\n# Youden's J optimal threshold on the stronger (logistic) curve, for the\n# storytelling marker on the plot.\noptimal_point <- roc_df %>%\n  filter(model == model_names[1]) %>%\n  mutate(youden = tpr - fpr) %>%\n  slice_max(youden, n = 1, with_ties = FALSE)\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot(roc_df, aes(x = fpr, y = tpr, color = label)) +\n  geom_abline(intercept = 0, slope = 1, linetype = \"dashed\",\n              linewidth = 0.6, color = INK_SOFT) +\n  geom_line(linewidth = 1.2) +\n  geom_point(data = optimal_point, aes(x = fpr, y = tpr),\n             shape = 21, size = 3, stroke = 1.2,\n             color = IMPRINT_PALETTE[1], fill = PAGE_BG, inherit.aes = FALSE) +\n  annotate(\"text\", x = pmin(optimal_point$fpr + 0.10, 0.97),\n           y = optimal_point$tpr - 0.05, label = \"Optimal threshold\",\n           hjust = 0, size = 3, color = INK_SOFT) +\n  annotate(\"text\", x = 0.98, y = 0.90, label = \"Random classifier\",\n           hjust = 1, size = 3.5, color = INK_SOFT, angle = 41) +\n  scale_color_manual(values = IMPRINT_PALETTE[1:2]) +\n  scale_x_continuous(limits = c(0, 1), breaks = seq(0, 1, 0.25),\n                      expand = expansion(mult = c(0.01, 0.03))) +\n  scale_y_continuous(limits = c(0, 1), breaks = seq(0, 1, 0.25),\n                      expand = expansion(mult = c(0.01, 0.03))) +\n  coord_fixed(ratio = 1) +\n  labs(\n    title = \"Disease Diagnostic Test · roc-curve · r · ggplot2 · anyplot.ai\",\n    x = \"False Positive Rate\",\n    y = \"True Positive Rate\",\n    color = NULL\n  ) +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background       = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background      = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major.y    = element_line(color = alpha(INK, 0.15), linewidth = 0.5),\n    panel.grid.major.x    = element_blank(),\n    panel.grid.minor      = element_blank(),\n    axis.title            = element_text(color = INK, size = 10),\n    axis.text              = element_text(color = INK_SOFT, size = 8),\n    axis.line              = element_line(color = INK_SOFT),\n    plot.title              = element_text(color = INK, size = 12),\n    legend.position        = \"inside\",\n    legend.position.inside = c(0.68, 0.14),\n    legend.background      = element_rect(fill = ELEVATED_BG, color = NA),\n    legend.text            = element_text(color = INK_SOFT, size = 8),\n    legend.title            = element_blank()\n  )\n\n# --- Save -------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}