{"spec_id":"learning-curve-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' learning-curve-basic: Model Learning Curve\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\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\"\n\nIMPRINT_PALETTE <- c(\n  \"#009E73\", # 1 — brand green, training score\n  \"#4467A3\"  # 3 — blue, validation score\n)\n\n# --- Data --------------------------------------------------------------------\n# Simulated sklearn-style learning_curve() output for a random-forest digit\n# classifier: 10 training-set sizes, 8 cross-validation folds each.\ntrain_sizes <- c(80, 160, 240, 360, 480, 600, 760, 900, 1080, 1300)\nn_folds <- 8\n\ntrain_asymptote <- 0.995\nval_asymptote <- 0.93\n\nlearning_df <- lapply(seq_along(train_sizes), function(i) {\n  size <- train_sizes[i]\n  decay <- exp(-size / 350)\n\n  train_mean <- train_asymptote - 0.10 * decay\n  val_mean <- val_asymptote - 0.22 * decay\n\n  tibble::tibble(\n    fold = seq_len(n_folds),\n    train_size = size,\n    train_score = pmin(1, train_mean + rnorm(n_folds, 0, 0.012 + 0.02 * decay)),\n    validation_score = pmin(1, val_mean + rnorm(n_folds, 0, 0.02 + 0.03 * decay))\n  )\n}) |>\n  bind_rows()\n\ncurve_df <- learning_df |>\n  group_by(train_size) |>\n  summarise(\n    train_mean = mean(train_score),\n    train_sd = sd(train_score),\n    validation_mean = mean(validation_score),\n    validation_sd = sd(validation_score),\n    .groups = \"drop\"\n  ) |>\n  pivot_longer(\n    cols = -train_size,\n    names_to = c(\"series\", \".value\"),\n    names_pattern = \"(train|validation)_(mean|sd)\"\n  ) |>\n  mutate(series = factor(series,\n    levels = c(\"train\", \"validation\"),\n    labels = c(\"Training score\", \"Validation score\")\n  ))\n\n# Tighten the y-axis to the actual data range (mean ± sd) instead of a fixed\n# 50%-100% span, so the bias-variance gap uses the available vertical space.\ny_lower <- max(0, floor(20 * min(curve_df$mean - curve_df$sd)) / 20 - 0.05)\n\n# Annotate the converging train/validation gap at the largest sample size to\n# give the chart a storytelling focal point beyond the raw curves.\ngap_row <- curve_df |>\n  filter(train_size == max(train_size)) |>\n  summarise(x = max(train_size), gap_mid = mean(mean), .groups = \"drop\")\n\n# --- Plot ---------------------------------------------------------------------\np <- ggplot(curve_df, aes(x = train_size, y = mean, color = series, fill = series)) +\n  geom_ribbon(aes(ymin = mean - sd, ymax = mean + sd), alpha = 0.15, color = NA) +\n  geom_line(linewidth = 1.0) +\n  geom_point(size = 2.5) +\n  annotate(\n    \"text\",\n    x = gap_row$x, y = gap_row$gap_mid,\n    label = \"Gap narrows\\nwith more data\",\n    hjust = 1.05, vjust = 0.5, size = 2.6, color = INK_SOFT, lineheight = 0.9\n  ) +\n  scale_color_manual(values = IMPRINT_PALETTE) +\n  scale_fill_manual(values = IMPRINT_PALETTE) +\n  scale_x_continuous(breaks = train_sizes, labels = scales::comma) +\n  scale_y_continuous(labels = scales::percent, limits = c(y_lower, 1.0)) +\n  labs(\n    title = \"learning-curve-basic · r · ggplot2 · anyplot.ai\",\n    x = \"Training set size\",\n    y = \"Accuracy\",\n    color = NULL,\n    fill = 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.x = element_blank(),\n    panel.grid.minor = element_blank(),\n    panel.grid.major.y = element_line(color = INK, linewidth = 0.2),\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 = 12),\n    legend.position = \"top\",\n    legend.justification = \"left\",\n    legend.text = element_text(color = INK_SOFT, size = 8),\n    legend.key = element_blank()\n  )\n\n# --- Save ----------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot = p,\n  device = ragg::agg_png,\n  width = 8,\n  height = 4.5,\n  units = \"in\",\n  dpi = 400\n)\n"}