{"spec_id":"shap-summary","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' shap-summary: SHAP Summary Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 86/100 | Created: 2026-09-09\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tibble)\nlibrary(scales)\nlibrary(ragg)\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\"\nMIDPOINT_BG <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\n\n# Imprint palette-derived diverging cmap (low=blue, mid=neutral, high=matte-red)\n# chosen over imprint_seq because low/high feature values are the domain's\n# universally recognised polarity (cold->hot / low->high), mirrored here for\n# SHAP's own \"low feature value -> blue, high -> red\" reading convention.\nSHAP_LOW  <- \"#4467A3\"\nSHAP_HIGH <- \"#AE3030\"\n\n# --- Data --------------------------------------------------------------------\n# Simulated SHAP values from a gradient-boosted house-price model. Each row is\n# one (sample, feature) pair: the feature's contribution to that sample's\n# predicted price, plus the feature's own value (min-max normalised for color).\nn_samples <- 220\n\nmake_feature <- function(name, raw, amplitude, noise_sd, shape = c(\"increasing\", \"decreasing\", \"hump\")) {\n  shape <- match.arg(shape)\n  value_norm <- (raw - min(raw)) / (max(raw) - min(raw))\n  trend <- switch(shape,\n    increasing = (value_norm - 0.5) * 2,\n    decreasing = (0.5 - value_norm) * 2,\n    hump       = (1 - 4 * (value_norm - 0.5)^2 - 0.5) * 2\n  )\n  tibble(\n    feature          = name,\n    shap_value       = amplitude * trend + rnorm(n_samples, 0, noise_sd),\n    feature_value    = raw,\n    feature_value_norm = value_norm\n  )\n}\n\nshap_df <- bind_rows(\n  make_feature(\"Living Area (sqft)\", rnorm(n_samples, 1900, 650) |> pmax(420),\n               amplitude = 42000, noise_sd = 7000, shape = \"increasing\"),\n  make_feature(\"Distance to Downtown (km)\", runif(n_samples, 1, 25),\n               amplitude = 16000, noise_sd = 5000, shape = \"hump\"),\n  make_feature(\"School Rating (1-10)\", runif(n_samples, 1, 10),\n               amplitude = 13000, noise_sd = 4500, shape = \"increasing\"),\n  make_feature(\"House Age (years)\", rnorm(n_samples, 25, 15) |> pmax(0),\n               amplitude = 10000, noise_sd = 4000, shape = \"decreasing\"),\n  make_feature(\"Bathrooms\", rpois(n_samples, 2) + 1,\n               amplitude = 7000, noise_sd = 3000, shape = \"increasing\"),\n  make_feature(\"Crime Rate Index\", runif(n_samples, 0, 100),\n               amplitude = 5500, noise_sd = 2500, shape = \"decreasing\"),\n  make_feature(\"Lot Size (sqft)\", rnorm(n_samples, 8000, 3000) |> pmax(1500),\n               amplitude = 4000, noise_sd = 2200, shape = \"increasing\"),\n  make_feature(\"Has Garage\", rbinom(n_samples, 1, 0.65),\n               amplitude = 3000, noise_sd = 1800, shape = \"increasing\")\n)\n\n# Order features by mean |SHAP value| — most important at top\nimportance <- shap_df |>\n  group_by(feature) |>\n  summarise(mean_abs_shap = mean(abs(shap_value)), .groups = \"drop\")\n\nshap_df <- shap_df |>\n  left_join(importance, by = \"feature\") |>\n  mutate(feature = factor(feature, levels = importance$feature[order(importance$mean_abs_shap)]))\n\n# Call out the top driver feature (highest mean |SHAP value|) with a star\n# prefix on its axis label — a light storytelling touch beyond the raw sort.\ntop_feature <- importance$feature[which.max(importance$mean_abs_shap)]\nfeature_levels <- levels(shap_df$feature)\naxis_labels <- feature_levels\naxis_labels[feature_levels == top_feature] <- paste0(\"★ \", top_feature)\n\n# --- Plot ----------------------------------------------------------------\n# shape=21 gives points a stroke independent of fill, so mid-range values\n# (fill == PAGE_BG at the diverging midpoint, per style guide) stay visible\n# as a faint outlined ring rather than vanishing into the background.\np <- ggplot(shap_df, aes(x = shap_value, y = feature, fill = feature_value_norm)) +\n  geom_vline(xintercept = 0, color = INK_SOFT, linewidth = 0.5, linetype = \"dashed\") +\n  geom_jitter(shape = 21, color = INK_SOFT, stroke = 0.25,\n              height = 0.30, width = 0, size = 1.9, alpha = 0.55) +\n  scale_fill_gradient2(\n    low = SHAP_LOW, mid = MIDPOINT_BG, high = SHAP_HIGH, midpoint = 0.5,\n    breaks = c(0.06, 0.94), labels = c(\"Low\", \"High\"),\n    name = \"Feature value\",\n    guide = guide_colorbar(barheight = unit(3.4, \"cm\"), barwidth = unit(0.35, \"cm\"))\n  ) +\n  scale_x_continuous(labels = label_dollar(scale = 1e-3, suffix = \"k\")) +\n  scale_y_discrete(labels = axis_labels) +\n  labs(\n    x = \"SHAP Value (Impact on Predicted Price)\",\n    y = NULL,\n    title = \"shap-summary · r · ggplot2 · anyplot.ai\"\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 = scales::alpha(INK, 0.18), linewidth = 0.6),\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.text.y           = element_text(color = INK, size = 9),\n    axis.ticks            = element_blank(),\n    plot.title            = element_text(color = INK, size = 12),\n    legend.background     = element_rect(fill = ELEVATED_BG, color = NA),\n    legend.text           = element_text(color = INK_SOFT, size = 8),\n    legend.title          = element_text(color = INK, size = 10),\n    legend.position        = \"right\",\n    plot.margin            = margin(t = 10, r = 12, 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"}