{"spec_id":"shap-waterfall","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' shap-waterfall: SHAP Waterfall Plot for Feature Attribution\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 95/100 | Created: 2026-09-09\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\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\n\n# Semantic exception (Imprint palette): SHAP explainability plots follow the\n# domain convention of red = pushes prediction up, blue = pushes it down.\nPOSITIVE_COLOR <- \"#AE3030\"  # Imprint position 5 — matte red\nNEGATIVE_COLOR <- \"#4467A3\"  # Imprint position 3 — blue\n\n# --- Data -----------------------------------------------------------------\n# Credit-scoring model: explaining one applicant's predicted default\n# probability. base_value is the mean predicted probability across the\n# training set; final_value is this applicant's actual prediction.\nbase_value  <- 0.35\nfinal_value <- 0.275\n\nshap_df <- tibble::tibble(\n  feature = c(\n    \"Credit Score\", \"Debt-to-Income Ratio\", \"Late Payments (12mo)\",\n    \"Employment Length\", \"Annual Income\", \"Loan Amount\",\n    \"Credit Utilization\", \"Age\", \"Open Credit Accounts\", \"Home Ownership\"\n  ),\n  shap_value = c(\n    -0.220, 0.150, 0.110, -0.090, -0.070,\n    0.060, 0.050, -0.030, -0.020, -0.015\n  )\n)\n\nwaterfall_df <- shap_df %>%\n  arrange(desc(abs(shap_value))) %>%\n  mutate(\n    rank      = row_number(),\n    y_pos     = n() - rank + 1,\n    cum_end   = base_value + cumsum(shap_value),\n    cum_start = cum_end - shap_value,\n    xmin_bar  = pmin(cum_start, cum_end),\n    xmax_bar  = pmax(cum_start, cum_end),\n    sign      = factor(if_else(shap_value > 0, \"positive\", \"negative\"),\n                        levels = c(\"positive\", \"negative\")),\n    label     = sprintf(\"%+.2f\", shap_value),\n    label_x   = if_else(shap_value > 0, xmax_bar + 0.006, xmin_bar - 0.006),\n    label_hjust = if_else(shap_value > 0, 0, 1)\n  )\n\nconnector_df <- waterfall_df %>%\n  arrange(rank) %>%\n  transmute(\n    x        = cum_end,\n    y_top    = y_pos - 0.35,\n    y_bottom = lead(y_pos) + 0.35\n  ) %>%\n  filter(!is.na(y_bottom))\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot() +\n  geom_rect(\n    data = waterfall_df,\n    aes(xmin = xmin_bar, xmax = xmax_bar,\n        ymin = y_pos - 0.35, ymax = y_pos + 0.35, fill = sign),\n    color = PAGE_BG, linewidth = 0.4\n  ) +\n  geom_segment(\n    data = connector_df,\n    aes(x = x, xend = x, y = y_top, yend = y_bottom),\n    color = INK_SOFT, linetype = \"dotted\", linewidth = 0.5\n  ) +\n  geom_vline(xintercept = base_value, color = INK_SOFT, linetype = \"dashed\", linewidth = 0.6) +\n  geom_vline(xintercept = final_value, color = INK, linetype = \"solid\", linewidth = 0.8) +\n  geom_label(\n    data = waterfall_df,\n    aes(x = label_x, y = y_pos, label = label, hjust = label_hjust),\n    size = 3.2, color = INK, fill = PAGE_BG, label.size = 0,\n    label.padding = unit(0.12, \"lines\")\n  ) +\n  annotate(\n    \"text\", x = base_value, y = 10.75,\n    label = paste0(\"Base value  E[f(x)] = \", percent(base_value, accuracy = 0.1)),\n    hjust = 0.5, vjust = 0, color = INK_SOFT, size = 3.0\n  ) +\n  annotate(\n    \"text\", x = final_value, y = 0.3,\n    label = paste0(\"Prediction  f(x) = \", percent(final_value, accuracy = 0.1)),\n    hjust = 0.5, vjust = 1, color = INK, size = 3.2, fontface = \"bold\"\n  ) +\n  scale_y_continuous(\n    breaks = waterfall_df$y_pos,\n    labels = waterfall_df$feature,\n    expand = expansion(add = c(0.8, 1.3))\n  ) +\n  scale_x_continuous(\n    labels = percent_format(accuracy = 1),\n    expand = expansion(mult = c(0.08, 0.15))\n  ) +\n  scale_fill_manual(\n    values = c(positive = POSITIVE_COLOR, negative = NEGATIVE_COLOR),\n    labels = c(positive = \"Increases risk\", negative = \"Decreases risk\"),\n    name   = NULL\n  ) +\n  labs(\n    title = \"shap-waterfall · r · ggplot2 · anyplot.ai\",\n    x     = \"Predicted Default Probability\",\n    y     = 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_blank(),\n    panel.grid.minor    = element_blank(),\n    panel.grid.major.x  = element_line(color = INK, linewidth = 0.15),\n    axis.title.x        = element_text(color = INK, size = 10),\n    axis.text.x         = element_text(color = INK_SOFT, size = 8),\n    axis.text.y         = element_text(color = INK, size = 9),\n    axis.ticks          = element_blank(),\n    plot.title          = element_text(color = INK, size = 12),\n    legend.position     = \"bottom\",\n    legend.text         = element_text(color = INK_SOFT, size = 8),\n    legend.key          = element_rect(fill = PAGE_BG, color = NA)\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"}