library(knitr)
library(tidymodels)
library(dplyr)
library(readr)
library(DALEX)
library(ceterisParibus) # Load the ceterisParibus package
# Load and prepare data
#data <- read_csv(url(sprintf("https://docs.google.com/uc?id=%s&export=download",
# "1dnCK79T45Qa7RZrDg1qv6-EdGBoxCPjv")))
data <- read_csv("data/data_acspubliccoverage.csv")
data <- data %>% mutate(across(where(is.character), as.factor))
# Split the data
set.seed(123)
data_split <- initial_split(data, prop = 0.80, strata = public_coverage)
data_train <- training(data_split)
data_test <- testing(data_split)
# Define the recipe and model spec
recipe_rf <- recipe(public_coverage ~ ., data = data_train) %>%
step_impute_median(all_numeric_predictors()) %>% # Impute numeric NAs
step_impute_mode(all_nominal_predictors()) # Impute categorical NAs
model_rf <- rand_forest(mode = "classification") %>% set_engine("ranger")
# Create and fit the workflow
workflow_rf <- workflow() %>%
add_recipe(recipe_rf) %>%
add_model(model_rf)
fit_rf <- fit(workflow_rf, data = data_train)Outlook: Further interpretability methods
1 Ceteris Paribus Plots
1.1 Overview
- Ceteris Paribus (CP) Plots: Show a single feature’s effect on one prediction.
- Latin for “other things equal”.
- Isolate one feature’s impact while keeping all others constant.
- The fundamental building block for other methods:
- ICE Plot: A collection of all CP curves for a dataset.
- PDP: The average of all CP curves in a dataset.
- See algorithm in Molnar (2022)
- For code see chapter in Biecek and Burzykowski (2021)
1.2 CP Plots: Strengths and Limitations
1.3 Lab R
1.3.1 Prepare data and train model
First, we’ll run your setup code to train our Random Forest model on the public_coverage data.
1.3.2 Create a DALEX Explainer
The ceterisParibus package is part of the DALEX ecosystem, so it needs a DALEX explainer object to work with our model. We will create an explainer that focuses on predicting the probability of the “Yes” class for public_coverage.
# Define the predict function for the "Yes" class probability
p_fun <- function(object, newdata) {
predict(object, new_data = newdata, type = "prob")$.pred_Yes
}
# Create the explainer for our tidymodels workflow
explainer <- explain(
model = fit_rf,
data = select(data_test, -public_coverage),
y = data_test$public_coverage == "Yes",
predict_function = p_fun,
label = "Random Forest",
type = "classification",
verbose = FALSE
)1.3.3 Create and Plot Ceteris Paribus Profiles
Now, we select an individual from our test set and create Ceteris Paribus plots for him/her. The plot will show how the model’s prediction for that person would change if we varied his/her feature values one at a time, keeping all other features constant. * Below Top observations describes the data of the original, unchanged person we asked the model to explain. * Top profiles: contain the Ceteris Paribus simulation that shows what the model would have predicted (yhat) for this same person if only their income had been different, while all other features (age, education, etc.) were held constant.
Top profiles :
income age education marital_status sex disability
1 0 23 High School or GED Never married Female Without disability
2 300 23 High School or GED Never married Female Without disability
3 600 23 High School or GED Never married Female Without disability
4 900 23 High School or GED Never married Female Without disability
5 1200 23 High School or GED Never married Female Without disability
6 1500 23 High School or GED Never married Female Without disability
parent_employment citizenship mobility military_service ancestry
1 <NA> Citizen by birth (US) Same house Never served Single
2 <NA> Citizen by birth (US) Same house Never served Single
3 <NA> Citizen by birth (US) Same house Never served Single
4 <NA> Citizen by birth (US) Same house Never served Single
5 <NA> Citizen by birth (US) Same house Never served Single
6 <NA> Citizen by birth (US) Same house Never served Single
nativity hearing_difficulty vision_difficulty cognitive_difficulty
1 Native No No No
2 Native No No No
3 Native No No No
4 Native No No No
5 Native No No No
6 Native No No No
employment gave_birth race _yhat_ _vname_ _ids_
1 Employed Civilian No Some Other Race 0.2987949 income 1
2 Employed Civilian No Some Other Race 0.2366688 income 1
3 Employed Civilian No Some Other Race 0.2580072 income 1
4 Employed Civilian No Some Other Race 0.2740474 income 1
5 Employed Civilian No Some Other Race 0.2967339 income 1
6 Employed Civilian No Some Other Race 0.2777668 income 1
_label_
1 Random Forest
2 Random Forest
3 Random Forest
4 Random Forest
5 Random Forest
6 Random Forest
Top observations:
income age education marital_status sex disability
1 5000 23 High School or GED Never married Female Without disability
parent_employment citizenship mobility military_service ancestry
1 <NA> Citizen by birth (US) Same house Never served Single
nativity hearing_difficulty vision_difficulty cognitive_difficulty
1 Native No No No
employment gave_birth race _yhat_ _label_ _ids_
1 Employed Civilian No Some Other Race 0.2666677 Random Forest 1

This plot contains a panel for each continuous variable (like age and income).
- Within each panel:
- The x-axis shows the range of possible values for that feature.
- The y-axis shows the model’s predicted probability of having public coverage.
- The plot shows what the model would have predicted for that person if only that one feature had been different.
2 Counterfactual Explanations
2.1 Introduction: The “What If” Explanation
- LIME: “Why did the model decide this?”
- Counterfactual: “What could I do to change the decision?”
- Specific “what if” scenarios to flip outcomes (e.g., “loan denied” \(\rightarrow\) “loan approved”).
- Analogy: GPS shows the wrong location.
- LIME (Why): “You are here because you turned left on Main St.”
- Counterfactual (How): “To get there, go back and turn right on Main St.”
- Focus on actionable advice \(\rightarrow\) A cornerstone of responsible AI.

2.2 How Counterfactuals are Generated
- Goal: Smallest data change for the desired prediction.
- Core Idea: Algorithm finds a nearby, hypothetical point that “flips” the outcome.
- Process: An optimization problem with two goals:
- New point gets the desired prediction.
- Changes from the original point are minimal.
2.3 Key Properties of Good Counterfactuals
- Sparsity: Minimal feature changes (e.g., “increase income by $5k” vs. 10 changes).
- Actionability: Changes must be possible (e.g., no immutable features like age).
- Plausibility: The resulting hypothetical data point must be realistic.
2.4 Counterfactuals and Fairness
- A direct way to implement fairness via actionable recourse.
- Providing Recourse: A clear path to appeal or change automated decisions (fulfills legal needs, e.g., GDPR).
- Uncovering Bias: The counterfactuals themselves can reveal deep model biases.
- Unequal Burdens: Man told “increase salary by $5k,” woman told “$15k” for the same outcome.
- Unrealistic Suggestions: Consistently less plausible paths for one group \(\rightarrow\) model bias.
2.5 Summary & Key Takeaways
- “What if” explanations that provide actionable advice.
- Answers: “What must change for a better outcome?”
- Found by seeking the smallest feature change that flips the prediction.
- A key fairness tool: provides recourse and audits if the “path to success” is fair for all groups.
2.6 Lab R
In this exercise, we will use the counterfactuals package to answer the question: “For a person predicted to not have public coverage, what are the smallest changes to their features that would flip the prediction to Yes?”
In this exercise, we will use the counterfactuals package to find out what would need to change for an individual to receive a different prediction from our model. We’ll train a randomForest model and then find counterfactuals for a person who the model predicts does not have public health coverage. * Problem: We can not simply use the tidymodels pipeline because counterfactuals does not know how to properly interpret tidymodels objects. We tidymodels to prepare our data but then resort to the simple randomForest function.
2.6.1 1. Fitting a model
First, we load and prepare our data. The randomForest function cannot handle missing values, so we will remove any rows with NAs from our training data for this exercise. To do so we recur to functions prep and bake from the tidymodels workflow. Then, we train the model to predict public_coverage.
# Load and prepare data
data <- read_csv("data/data_acspubliccoverage.csv")
data <- data %>% mutate(across(where(is.character), as.factor))
# Split the data
set.seed(123)
data_split <- initial_split(data, prop = 0.80, strata = public_coverage)
data_train <- training(data_split)
data_test <- testing(data_split)
# Define the recipe and model spec
recipe_rf <- recipe(public_coverage ~ ., data = data_train) %>%
step_impute_median(all_numeric_predictors()) %>%
step_impute_mode(all_nominal_predictors())
prepped_recipe <- prep(recipe_rf, data_train)
baked_train_data <- bake(prepped_recipe, new_data = data_train)
# Train the model on the complete training data
rf_model <- randomForest(public_coverage ~ ., data = baked_train_data)2.6.2 2. Setting up an iml::Predictor Object
Next, we create an iml::Predictor object. This is a wrapper that contains our trained model and the data used to train it, which the counterfactuals package needs to work.
2.6.3 3. Finding Counterfactuals
Now, let’s find an interesting person from our test set to explain. We’ll pick someone for whom the model predicts they do not have public coverage and find out what would need to change for them to be predicted as “Yes”.
# A tibble: 1 × 19
public_coverage income age education marital_status sex disability
<fct> <dbl> <dbl> <fct> <fct> <fct> <fct>
1 No 0 17 No HS Diploma Never married Female Without disa…
# ℹ 12 more variables: parent_employment <fct>, citizenship <fct>,
# mobility <fct>, military_service <fct>, ancestry <fct>, nativity <fct>,
# hearing_difficulty <fct>, vision_difficulty <fct>,
# cognitive_difficulty <fct>, employment <fct>, gave_birth <fct>, race <fct>
No Yes
1 0.792 0.208
# We want to find changes that would make the
# prediction for class "Yes" at least 50%
whatif_classif <- WhatIfClassif$new(predictor, n_counterfactuals = 5L)
# Find the counterfactuals
cfactuals <- whatif_classif$find_counterfactuals(
person_of_interest,
desired_class = "Yes",
desired_prob = c(0.5, 1)
)2.6.4 4. The Counterfactuals Object
The cfactuals object contains the results. We can now print it to see a summary, view the data, and plot the findings to understand the suggested changes.
First, let’s see the summary of the counterfactuals found.
5 Counterfactual(s)
Desired class: Yes
Desired predicted probability range: [0.5, 1]
Head:
income age education marital_status sex disability
<num> <num> <fctr> <fctr> <fctr> <fctr>
1: 0 17 No HS Diploma Never married Female Without disability
2: 0 17 No HS Diploma Never married Female Without disability
3: 0 17 No HS Diploma Never married Female Without disability
parent_employment citizenship mobility military_service
<fctr> <fctr> <fctr> <fctr>
1: 2 parents, both work Citizen by birth (US) Same house Never served
2: 2 parents, mother works Citizen by birth (US) Same house Never served
3: Mother only, no work Citizen by birth (US) Same house Never served
ancestry nativity hearing_difficulty vision_difficulty
<fctr> <fctr> <fctr> <fctr>
1: Not reported Native No No
2: Single Native No No
3: Single Native No No
cognitive_difficulty employment gave_birth race
<fctr> <fctr> <fctr> <fctr>
1: No Not in labor force No Some Other Race
2: No Not in labor force No Black or African American
3: No Not in labor force No White
The counterfactuals are stored in the .data field of the cfactuals object. Each row is a slightly modified version of our original person that now gets the desired prediction.
| income | age | education | marital_status | sex | disability | parent_employment | citizenship | mobility | military_service | ancestry | nativity | hearing_difficulty | vision_difficulty | cognitive_difficulty | employment | gave_birth | race |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | 2 parents, both work | Citizen by birth (US) | Same house | Never served | Not reported | Native | No | No | No | Not in labor force | No | Some Other Race |
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | 2 parents, mother works | Citizen by birth (US) | Same house | Never served | Single | Native | No | No | No | Not in labor force | No | Black or African American |
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | Mother only, no work | Citizen by birth (US) | Same house | Never served | Single | Native | No | No | No | Not in labor force | No | White |
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | Mother only, works | Citizen by birth (US) | Same house | Never served | Multiple | Native | No | No | No | Not in labor force | No | White |
| 0 | 17 | High School or GED | Never married | Female | Without disability | Mother only, works | Citizen by birth (US) | Same house | Never served | Not reported | Native | No | No | No | Not in labor force | No | White |
With the evaluate() method, we can see how good our counterfactuals are. Lower is generally better for dist_x_interest (less change) and no_changed (fewer features changed).
| income | age | education | marital_status | sex | disability | parent_employment | citizenship | mobility | military_service | ancestry | nativity | hearing_difficulty | vision_difficulty | cognitive_difficulty | employment | gave_birth | race | dist_x_interest | no_changed | dist_train | dist_target | minimality |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | 2 parents, both work | Citizen by birth (US) | Same house | Never served | Not reported | Native | No | No | No | Not in labor force | No | Some Other Race | 0.0555556 | 1 | 0 | 0 | 0 |
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | 2 parents, mother works | Citizen by birth (US) | Same house | Never served | Single | Native | No | No | No | Not in labor force | No | Black or African American | 0.1666667 | 3 | 0 | 0 | 1 |
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | Mother only, no work | Citizen by birth (US) | Same house | Never served | Single | Native | No | No | No | Not in labor force | No | White | 0.1666667 | 3 | 0 | 0 | 2 |
| 0 | 17 | No HS Diploma | Never married | Female | Without disability | Mother only, works | Citizen by birth (US) | Same house | Never served | Multiple | Native | No | No | No | Not in labor force | No | White | 0.1666667 | 3 | 0 | 0 | 2 |
| 0 | 17 | High School or GED | Never married | Female | Without disability | Mother only, works | Citizen by birth (US) | Same house | Never served | Not reported | Native | No | No | No | Not in labor force | No | White | 0.1666667 | 3 | 0 | 0 | 2 |
2.6.5 5. Visualizing the Results
The frequency plot shows which features had to be changed most often to achieve the desired outcome. This tells us what the model is most sensitive to for this individual.
Finally, the parallel plot connects the feature values of each counterfactual (in black) and highlights our original person (person_of_interest) in blue. This makes it easy to see exactly what changed to flip the prediction (apparently graph is not optimized for many features..).
References
Footnotes
This makes them a great entry point for beginners and for communicating model-agnostic explainability to non-experts.↩︎
Attribution-based methods don’t show how sensitive the prediction is to local changes; CP plots do, completing the explanation.↩︎
They can be combined across models, classes, and features to create nuanced insights and are the basis for other interpretation methods.↩︎
This means we don’t automatically see how features interact, though manually changing two features is possible.↩︎
When features are correlated, some parts of the curve represent unlikely or unrealistic data points, which can be misleading.↩︎
This is a general problem, but the risk is higher for CP plots because their simplicity makes them easy to show to non-experts who may misinterpret them.↩︎

