Lesson 4 of 6

Partial Dependence, ICE, and ALE

In Lesson 3, SHAP gave you one number per feature per row: for this customer, how much did monthly charge push the prediction? That is a single point on a story. This lesson draws the whole story: as one feature sweeps from its lowest value to its highest, what shape does the prediction trace out?

We will draw that shape three ways, each fixing a blind spot in the one before. Partial dependence (PDP) draws the average shape. ICE draws it for every customer at once, revealing when the average is a lie. ALE repairs the average when features move together.

By the end you will be able to:

  • Compute a partial-dependence curve: sweep one feature over a grid, force it for every row, and average the predictions
  • Draw ICE curves (one line per customer) and read the fan they make as a sign the feature interacts with another
  • Diagnose the PDP's extrapolation flaw: forcing a value asks the model about customer combinations that never existed
  • Compute ALE, which accumulates small local effects on the data's real distribution, and say when to trust it over a PDP

Prerequisites: you can fit and use a model in R such as a random forest and know that predict() returns a per-row score, and you have done Lesson 3: SHAP Values (baseline, per-feature contribution, "explains the model, not the world").

The setup

One question: what shape is a feature's effect?

Here is the churn model from Lessons 2 and 3, rebuilt so this page runs on its own. Each row is a customer; churned is "yes" if they left. Two features carry the two surprises in this lesson: contract (month-to-month or annual) and total_spend (lifetime dollars, which naturally tracks the monthly charge). Build it and fit the forest once.

RInteractive R
library(randomForest) set.seed(42) n <- 400 contract <- factor(sample(c("monthly", "annual"), n, TRUE, prob = c(0.6, 0.4))) tenure <- round(runif(n, 1, 60)) # months as a customer monthly <- round(runif(n, 20, 120), 1) # monthly charge (dollars) support_calls <- rpois(n, 1.2) total_spend <- round(monthly * 22 + rnorm(n, 0, 200)) # tracks the monthly charge # churn risk: a high charge hurts, but FAR more on month-to-month contracts (an interaction); # long tenure and high total spend are protective mm <- as.integer(contract == "monthly") lp <- -3.0 + 0.055 * monthly * mm + 0.004 * monthly * (1 - mm) - 0.04 * tenure + 0.30 * support_calls - 0.0004 * total_spend churn <- data.frame(tenure, monthly, support_calls, contract, total_spend, churned = factor(ifelse(rbinom(n, 1, plogis(lp)) == 1, "yes", "no"))) set.seed(1) rf <- randomForest(churned ~ ., data = churn, ntree = 150) cat("churned no:", sum(churn$churned == "no"), " yes:", sum(churn$churned == "yes"), "\n") cat("mean predicted P(churn):", round(mean(predict(rf, churn, type = "prob")[, "yes"]), 3), "\n") #> churned no: 304 yes: 96 #> mean predicted P(churn): 0.238

  

The forest is a black box: hundreds of trees vote, and there is no slope to read. The product lead asks a plain question anyway: as the monthly charge rises from $20 to $120, how does predicted churn move? We want a curve, not a single number. The next three tools are three honest ways to draw it.