{"spec_id":"gain-curve","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' gain-curve: Cumulative Gains Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 86/100 | Created: 2026-09-05\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\"\nINK_MUTED <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nGRID_COLOR <- scales::alpha(INK, 0.05)\nANYPLOT_NEUTRAL <- INK\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data ---------------------------------------------------------------\n# Churn-prevention scenario: a risk model scores customers by likelihood of\n# churning; the cumulative gains curve shows how much of the actual churn\n# base is captured as we contact increasing shares of the customer list,\n# ranked by predicted score.\nn_customers <- 2000\nchurn_rate <- 0.18\n\ntrue_risk <- rnorm(n_customers)\nnoise <- rnorm(n_customers, sd = 1.35)\nmodel_score <- true_risk + noise\n\nchurn_threshold <- quantile(true_risk, probs = 1 - churn_rate)\nchurned <- as.integer(true_risk >= churn_threshold)\n\norder_idx <- order(model_score, decreasing = TRUE)\nchurned_sorted <- churned[order_idx]\n\ncumulative_churners <- cumsum(churned_sorted)\ntotal_churners <- sum(churned_sorted)\n\ngains <- tibble::tibble(\n  pct_targeted = seq_len(n_customers) / n_customers * 100,\n  pct_captured = cumulative_churners / total_churners * 100,\n  series = \"Churn model\"\n)\ngains <- bind_rows(\n  tibble::tibble(pct_targeted = 0, pct_captured = 0, series = \"Churn model\"),\n  gains\n)\nbaseline <- tibble::tibble(\n  pct_targeted = c(0, 100), pct_captured = c(0, 100),\n  series = \"Random targeting\"\n)\ndf <- bind_rows(gains, baseline)\n\n# --- Plot -----------------------------------------------------------------\ntitle_text <- \"Churn Model · gain-curve · r · ggplot2 · anyplot.ai\"\n\np <- ggplot(\n  df,\n  aes(x = pct_targeted, y = pct_captured,\n      color = series, linetype = series, linewidth = series)\n) +\n  geom_line() +\n  scale_color_manual(values = c(\"Churn model\" = IMPRINT_PALETTE[1], \"Random targeting\" = ANYPLOT_NEUTRAL)) +\n  scale_linetype_manual(values = c(\"Churn model\" = \"solid\", \"Random targeting\" = \"dashed\")) +\n  scale_linewidth_manual(values = c(\"Churn model\" = 1.8, \"Random targeting\" = 1.0)) +\n  scale_x_continuous(\n    limits = c(0, 100), expand = expansion(mult = c(0, 0.02), add = c(0, 6)),\n    labels = scales::label_number(suffix = \"%\")\n  ) +\n  scale_y_continuous(\n    limits = c(0, 100), expand = expansion(mult = c(0, 0.02)),\n    labels = scales::label_number(suffix = \"%\")\n  ) +\n  labs(\n    title = title_text,\n    x = \"Percentage of Customers Targeted\",\n    y = \"Percentage of Churners Captured\"\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  = element_line(color = GRID_COLOR, linewidth = 0.25),\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.ticks        = element_blank(),\n    plot.title        = element_text(color = INK, size = 11),\n    panel.border      = element_blank(),\n    legend.title      = element_blank(),\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.background = element_rect(fill = ELEVATED_BG, color = NA),\n    legend.position        = \"inside\",\n    legend.position.inside = c(0.80, 0.18),\n    plot.margin            = margin(t = 8, r = 22, 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    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}