--- title: "Palmer Penguins — SHAP walkthrough" author: "Aparna Pandey and Stephan Peischl" format: html: toc: true code-tools: true engine: knitr --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE) suppressPackageStartupMessages({ library(tidymodels) library(palmerpenguins) library(shapviz) library(kernelshap) library(dplyr) library(tidyr) library(ggplot2) }) ``` # Overview Self-contained SHAP example on **Palmer Penguins**. - Task: predict `sex` from morphometrics + island/year (no `species`) - One model: random forest (`ranger`) - SHAP outputs: **beeswarm with individual points**, **mean absolute SHAP bar plot**, and **waterfall plots** for individual penguins Related: [Module 06 SHAP section](../modules/module-06-evaluation-and-interpretability.qmd#shap-shapley-values-on-penguins-sex) ## Prepare data ```{r} pg_sex <- palmerpenguins::penguins |> tidyr::drop_na( sex, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g, island, year ) |> mutate( sex = droplevels(sex), year = as.numeric(year) ) |> select(-species) ``` ```{r} pg_sex |> count(sex) |> knitr::kable(col.names = c("Sex", "n")) ``` ## Fit one explicit model (random forest) ```{r} rec_sex <- recipe( sex ~ bill_length_mm + bill_depth_mm + flipper_length_mm + body_mass_g + island + year, data = pg_sex ) |> step_zv(all_predictors()) |> step_dummy(all_nominal_predictors()) |> step_normalize(all_numeric_predictors()) rf_spec <- rand_forest(trees = 300, mtry = 3, min_n = 2) |> set_engine("ranger", probability = TRUE) |> set_mode("classification") wf_sex <- workflow() |> add_recipe(rec_sex) |> add_model(rf_spec) fit_sex <- fit(wf_sex, data = pg_sex) ``` ## Compute SHAP values explicitly This section does the SHAP workflow in small steps: 1. **Extract trained preprocessing** from the fitted workflow. 2. **Bake predictors** so we get the numeric model matrix used by the forest. 3. Choose a subset of rows to explain (`X_shap`) and a background reference set (`bg_X`). 4. Define `pred_fun` to return **P(male)** from the fitted `ranger` model. 5. Run `kernelshap()` to estimate contributions, then wrap with `shapviz()`. 6. Identify one male and one female row for waterfall examples. ```{r} rec_est <- extract_recipe(fit_sex, estimated = TRUE) X_model <- bake(rec_est, new_data = pg_sex, all_predictors()) set.seed(11) X_shap <- dplyr::slice_sample(X_model, n = min(80, nrow(X_model))) bg_X <- dplyr::slice_sample(X_model, n = min(35, nrow(X_model))) rf_engine <- extract_fit_parsnip(fit_sex)$fit pred_fun <- function(object, X_new) { as.numeric(predict(object, data = X_new)$predictions[, "male"]) } ks <- kernelshap::kernelshap(rf_engine, X = X_shap, pred_fun = pred_fun, bg_X = bg_X) shp <- shapviz::shapviz(ks, X_pred = X_shap) sex_sample <- pg_sex$sex[as.integer(rownames(X_shap))] male_row <- which(sex_sample == "male")[1] female_row <- which(sex_sample == "female")[1] ``` ## What positive/negative SHAP means - We explain **P(male)**, so each SHAP value is a push on that probability scale. - **Positive SHAP value**: this feature pushes the prediction toward **male** (higher P(male)). - **Negative SHAP value**: this feature pushes the prediction away from male (toward **female**). - In a waterfall plot, baseline + all SHAP contributions = final model output for that row. ## SHAP beeswarm (individual points) Each dot is one penguin; horizontal position is SHAP contribution. ```{r fig.width=9, fig.height=6} shapviz::sv_importance( shp, kind = "beeswarm", max_display = 12 ) + theme_minimal(base_size = 13) + labs( title = "SHAP beeswarm (individual penguins)", subtitle = "Color = feature value; x-axis = push toward/away from P(male)" ) ``` ## Mean absolute SHAP values (global importance) This aggregates absolute contributions across rows. ```{r fig.width=8, fig.height=5} shapviz::sv_importance( shp, kind = "bar", max_display = 12 ) + theme_minimal(base_size = 13) + labs( title = "Mean absolute SHAP values", subtitle = "Average |SHAP| per feature" ) ``` ## Waterfall plot for individual penguins ### Example 1 — one male penguin ```{r fig.width=9, fig.height=5.5} shapviz::sv_waterfall( shp, row_id = male_row ) + labs( title = "Waterfall: one male penguin", subtitle = "Positive bars push toward male, negative bars push toward female" ) ``` ### Example 2 — one female penguin ```{r fig.width=9, fig.height=5.5} shapviz::sv_waterfall( shp, row_id = female_row ) + labs( title = "Waterfall: one female penguin", subtitle = "Same model, opposite pushes can lead to a female prediction" ) ``` ## Quick interpretation checklist - Beeswarm = distribution of local effects across many rows - Mean |SHAP| bar = global ranking of influential features - Waterfall = local explanation for one row only - SHAP explains this fitted model; it does **not** prove causality