{"spec_id":"calibration-curve","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' calibration-curve: Calibration Curve\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 93/100 | Created: 2026-09-02\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\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\")\nBRAND       <- IMPRINT_PALETTE[1]\nANYPLOT_NEUTRAL <- INK  # theme-adaptive anchor for the reference (baseline) line\nGRID_COLOR  <- scales::alpha(INK, 0.15)  # faint grid, distinct from the INK/INK_SOFT text tokens\n\n# --- Data -----------------------------------------------------------------\n# A diagnostic screening model that is systematically underconfident: bagged\n# ensembles (e.g. random forests) average many trees, which pulls predicted\n# probabilities toward the middle relative to the true underlying risk, so\n# observed outcomes end up more extreme than predicted at both ends.\nn_samples <- 4000\ny_prob <- rbeta(n_samples, 2, 2)\ntrue_logit <- 1.8 * log(y_prob / (1 - y_prob))\ntrue_risk <- 1 / (1 + exp(-true_logit))\ny_true <- rbinom(n_samples, 1, true_risk)\n\ndf <- tibble::tibble(y_prob = y_prob, y_true = y_true)\n\n# --- Calibration binning ----------------------------------------------------\nbin_edges <- seq(0, 1, by = 0.1)\ndf <- df %>%\n  mutate(bin = cut(y_prob, breaks = bin_edges, include.lowest = TRUE))\n\ncalib <- df %>%\n  group_by(bin) %>%\n  summarise(mean_pred = mean(y_prob), frac_pos = mean(y_true), n = n(), .groups = \"drop\")\n\nbrier_score <- mean((df$y_prob - df$y_true)^2)\nece <- sum(calib$n / nrow(df) * abs(calib$frac_pos - calib$mean_pred))\n\n# --- Title (scale fontsize to length, see plot-generator.md) ---------------\n# The 67-char baseline in plot-generator.md assumes the 8in-wide landscape\n# canvas; this plot uses the 6in-wide square canvas, so the safe character\n# budget shrinks proportionally (67 * 6/8) before the fontsize formula applies.\nplot_title <- \"calibration-curve · r · ggplot2 · anyplot.ai\"\nplot_subtitle <- sprintf(\"Brier score %.3f · ECE %.3f\", brier_score, ece)\nsquare_baseline_chars <- 67 * 6 / 8\ntitle_fontsize <- round(12 * min(1, square_baseline_chars / nchar(plot_title)))\ntitle_fontsize <- max(title_fontsize, 8)\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot(calib, aes(x = mean_pred, y = frac_pos)) +\n  geom_abline(intercept = 0, slope = 1, linetype = \"dashed\",\n              linewidth = 0.7, color = ANYPLOT_NEUTRAL, alpha = 0.5) +\n  geom_line(color = BRAND, linewidth = 1.0) +\n  geom_point(aes(size = n), color = BRAND, alpha = 0.9) +\n  scale_x_continuous(limits = c(0, 1), breaks = seq(0, 1, 0.2), labels = scales::percent) +\n  scale_y_continuous(limits = c(0, 1), breaks = seq(0, 1, 0.2), labels = scales::percent) +\n  scale_size_continuous(range = c(3, 11), name = \"Samples per bin\") +\n  labs(\n    title = plot_title,\n    subtitle = plot_subtitle,\n    x = \"Mean predicted probability\",\n    y = \"Observed fraction of positives\"\n  ) +\n  coord_fixed() +\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   = element_line(color = GRID_COLOR, linewidth = 0.3),\n    panel.grid.minor   = element_blank(),\n    panel.border       = element_blank(),\n    axis.line          = element_line(color = INK_SOFT, linewidth = 0.4),\n    axis.title         = element_text(color = INK, size = 10),\n    axis.text          = element_text(color = INK_SOFT, size = 8),\n    axis.ticks         = element_blank(),\n    plot.title         = element_text(color = INK, size = title_fontsize, face = \"bold\"),\n    plot.subtitle      = element_text(color = INK_SOFT, size = 9, margin = margin(b = 8)),\n    legend.background  = element_rect(fill = PAGE_BG, color = NA),\n    legend.text        = element_text(color = INK_SOFT, size = 8),\n    legend.title       = element_text(color = INK, size = 9),\n    legend.key.size    = unit(0.8, \"lines\"),\n    legend.spacing.y   = unit(2, \"pt\"),\n    legend.position    = \"right\",\n    plot.margin        = margin(t = 12, r = 16, b = 8, l = 8)\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"}