A SHAP beeswarm summary plot rendered as a glass sculpture, with rows of small spheres in a blue-to-red gradient showing feature value and impact.

Explaining Complex Machine Learning Models with SHAP

Modern machine learning models often achieve strong predictive performance, but their internal logic can be difficult to interpret. In domains such as healthcare, finance, and policy, this lack of transparency creates problematic limitations. Decisions must be justified, validated, and aligned with domain knowledge. Interpretability becomes an important requirement.

SHAP, short for SHapley Additive exPlanations, provides a practical and theoretically grounded approach to understanding model predictions. The per-feature contributions it assigns, called SHAP values, support both detailed explanations of individual predictions and broader insights into overall model behavior.

SHAP values in practice

Many commonly used models such as gradient boosting, random forests, and neural networks are considered black boxes. While they capture complex relationships, they do not offer straightforward explanations for their outputs.

SHAP bridges this gap by translating model predictions into feature-level contributions. Each prediction is decomposed into additive components, making it possible to answer a simple but critical question. Which features drove this prediction, and in what direction?

That matters most in settings where:
• Model outputs influence high-stakes decisions
• Domain experts need to validate model behavior
• Regulatory or audit requirements demand transparency

Shapley values

SHAP is based on Shapley values from cooperative game theory. In that setting, each player contributes to a collective outcome, and the goal is to fairly assign credit.

In machine learning, the players are features and the outcome is the model prediction. SHAP assigns each feature a contribution value that reflects how much it shifts the prediction away from a base value, typically the average model output.

This leads to an additive explanation:
• Baseline prediction
• Plus contributions from each feature
• Equals the final prediction

The additive structure is more than a formality. It guarantees that the base value plus the feature contributions add up exactly to the model output for the prediction being explained.

The useful question is how each feature earns its share of that total. In the Shapley framework a feature is valued by its marginal contribution, the amount the model output moves when the feature joins a coalition of the features already present, that is, any subset of the others. Because that amount can depend on which features are already there, the contribution is averaged across every possible coalition, weighted so that each coalition size carries equal weight. No single ordering of the features decides the result. Figure 1 works through this averaging one feature at a time.

Figure 1

How a feature earns its SHAP value

A feature’s SHAP value is the average of its marginal contributions across the coalitions of the other features, weighted so each coalition size counts equally. Choose a feature and step through the coalitions.

Value the feature:
Already present:
Marginal contribution on the model-output line
Model output (log-odds)−2−10base −1.05f(x) −0.23+0.59v(S)v(S,i)
Coalition ledger
Every coalition Fasting glucose can join (the other three features present or absent). SHAP averages the marginal contributions, weighted so each coalition size counts equally.
Coalition already presentMarginalWeight
{ } none+0.591/4
Waist+0.931/12
HDL+0.591/12
Family history+0.591/12
Waist, HDL+0.931/12
Waist, Family history+0.931/12
HDL, Family history+0.591/12
Waist, HDL, Family history+0.931/4
Weighted sum = SHAP value+0.761
Weighted sum over 1 of 8 coalitions: +0.15 (keep stepping toward +0.76).
With the empty coalition already present, adding Fasting glucose moves the model output from −1.05 to −0.46, a marginal contribution of +0.59 log-odds. Because fasting glucose and waist circumference interact, this marginal changes with the coalition, so the SHAP value +0.76 log-odds is the weighted average across all eight coalitions.

Numbers come from a transparent illustrative model of type 2 diabetes risk in log-odds (a fasting glucose by waist circumference interaction plus additive terms), with SHAP computed exactly over all coalitions against a fixed background sample. This is a separate example from the cardiovascular model worked below. It illustrates the mechanism only. All values are rounded to two decimals.

Figure 1. A feature’s SHAP value is the weighted average of its marginal contributions across every coalition of the other features. Select a feature and step through the coalitions, or press Play to watch the weighted sum build toward the final value. Fasting glucose contributes more when waist circumference is already present, because the two interact, while an additive feature such as high-density lipoprotein (HDL) cholesterol contributes the same amount to every coalition. Weighting every coalition by size is what defines the SHAP value.

Computational considerations

Exact computation of Shapley values requires evaluating all possible feature subsets, which is computationally infeasible for most real-world models.

In practice, more efficient adaptations are used:
• TreeSHAP for tree-based models
• KernelSHAP for model-agnostic settings
• Sampling-based approaches for large feature spaces

TreeSHAP is particularly important in applied work since it provides exact SHAP values for tree ensembles with polynomial complexity. That aspect makes it feasible to use SHAP routinely in production workflows involving gradient boosting models. A practical best practice is to use model-specific SHAP implementations whenever available. They are faster, and for tree ensembles TreeSHAP returns exact values where the generic methods approximate.

Local interpretability: explaining individual predictions

SHAP is well suited to explaining single predictions. Several visualization tools support this.

Waterfall plots

Waterfall plots show how each feature contributes to a prediction step by step. Starting from the base value, each feature pushes the prediction higher or lower.

This is especially useful in case-level analysis. For example, a clinical model may show that:
• Age and blood pressure increase predicted risk
• Favorable lab values reduce it

The result is a clear narrative that aligns with domain reasoning.

One detail matters before reading a waterfall, namely, the scale on which the contributions add up. For a classifier such as the gradient boosted model used later, SHAP explains the output in log-odds, so the base value and every contribution are expressed in log-odds rather than in probability. Figure 2 builds a single prediction on that scale and reports the probability separately.

Figure 2

Building one prediction from the base value

Starting from the base value, each feature adds its SHAP value in log-odds until the sum reaches the model output for that patient. Step through the contributions or switch patients.

Patient:
Prediction on the model-output scale log-odds
f(x) = −0.23−2−10123base value E[f(X)] = −1.05Family historyabsent−0.24HDL62 mg/dL−0.30Waist104 cm+0.61Glucose116 mg/dL+0.76
Reading the total
base −1.05 + contributions +0.82 = −0.23 log-odds

On the probability scale, the logistic link maps this total from a base risk of 26% to 44% for Patient A.
Probability is the logistic transform of the log-odds total, so each value is read directly from its endpoint on the curve. The probabilities themselves do not add. SHAP is additive on the log-odds scale used here. Values are rounded to two decimals, so the bars can add up to within 0.01 of the total.

Patient A has elevated fasting glucose and a large waist, but high HDL and no family history of diabetes. Illustrative values from the same transparent model as Figure 1.

Figure 2. Beginning at the base value E[f(X)], the average model output across the background patients, each feature adds its SHAP value in log-odds until the running total reaches the model output f(x) for that patient. Switch between the three patients or step through the contributions. The probability beside the chart is the logistic transform of the log-odds total, read off the two endpoints. The contributions sum to the log-odds, and the curve does the rest. Keeping the two scales separate avoids a common misreading.

Force plots

Force plots are among the most distinctive SHAP visualizations. They represent the additive nature of SHAP values through a dynamic balance of forces pushing and pulling the model’s output from the baseline toward the final prediction. Each force plot starts from the expected value, marked as the base value on the output axis, and the opposing forces meet at the model output f(x). Arrows extending to the right represent features that increase the model’s output, while those to the left reduce it. The width of each arrow corresponds to the magnitude of the SHAP value. The final prediction is the result of all these forces acting together.

Force plots are ideal for explaining individual predictions in settings such as clinical consults or patient education. For instance, in a model assessing the risk of postoperative complications, a clinician can use the force plot to show that while the patient’s age increased the predicted risk, favorable factors like normal renal function and low surgical complexity brought it back down. This clear visualization bridges the gap between statistical reasoning and clinical storytelling.

Beeswarm plots

The beeswarm plot is a dense summary of a SHAP analysis. It combines feature importance with the direction and spread of each feature’s contribution.
Each point represents a SHAP value for a single observation. The horizontal position shows the SHAP value, and color often encodes the original feature value.

That layout supports several readings at once:
• Which features are most important overall
• Whether higher values increase or decrease predictions
• Whether contributions are consistent or vary from patient to patient

In practice, beeswarm plots often reveal non-linear patterns or interactions that are not obvious from model coefficients alone.

Figure 3 connects one of these points to the whole population, and to the bar chart of mean absolute SHAP values that usually sits beside a beeswarm.

Figure 3

From single explanations to a global summary

Each patient has one SHAP value per feature (left). Averaging the absolute size of those values across patients gives the global importance bars (right). Select a feature or filter by value to see how local spread becomes one number.

Filter dots:
Local explanations, one lane per feature 60 patients
−1012
Global summary mean |SHAP| (magnitude)
Bars rank features by average attribution magnitude. This is the usual global importance chart. Direction is read from the dots on the left: a feature that raises the prediction for some patients and lowers it for others still ranks high.
Dot color = feature value low to highDot position = SHAP value: left of the dashed zero line lowers the prediction, right of it raises it
Each dot on the left is one patient’s SHAP value for the feature in its lane (60 patients per lane). The bars on the right average the absolute SHAP values into a single importance score. Select a feature, or filter by feature value, to see how the local spread produces the global number.

A population of 60 illustrative patients from the same model as Figures 1 and 2, with SHAP computed exactly per patient. Mean |SHAP| aggregates magnitude. Direction sits in the per-patient values on the left, and causal effect comes from study design.

Figure 3. Each dot is one patient’s SHAP value for a feature. Averaging the absolute values across patients produces the global importance bars on the right. Select a feature, or filter the dots by feature value, to see how the local spread becomes a single number. The bars rank features by average magnitude, which discards direction. Waist circumference and HDL cholesterol have large average magnitude. They raise the prediction for some patients and lower it for others, and the bars cannot show that. Averaging the signed values does not recover it either, since the opposing contributions cancel. The same two features average −0.07 and −0.10 in signed terms, against magnitudes above 0.4. Reading the beeswarm alongside the bars keeps magnitude and direction distinct, and neither is a statement about cause.

Heatmaps

SHAP heatmaps provide a matrix-like overview of SHAP values across many instances and features. The rows correspond to features, ordered by mean absolute SHAP, and the columns represent individual patients. Each cell’s color reflects the SHAP value, often with red tones indicating positive contributions and blue tones indicating negative ones. This can uncover subgroups in the data where the model behaves differently. In healthcare applications, this often aligns with clinically meaningful subpopulations.

Example: Explaining a gradient boosted trees model

Explaining Predictions. How SHAP turns an opaque gradient boosting model for cardiovascular risk into clear, additive explanations, from a global feature ranking down to a single patient's prediction. Key topics covered: Cardiovascular dataset, Gradient boosting model, Beeswarm summary, Shapley values, Waterfall plot, Force plot, SHAP heatmap, Top features by mean |SHAP|.

Hover any card to explore

This notebook provides a complete, reproducible workflow for training a gradient-boosted classifier on cardiovascular data and interpreting its predictions with SHAP (SHapley Additive exPlanations).

SHAP is grounded in cooperative game theory. Each feature is treated as a player in a coalition and assigned a SHAP value, which is its fair marginal contribution to a given prediction relative to the model’s baseline (expected) output. SHAP values satisfy three key properties that make them particularly suitable for clinical interpretation:

  • Local accuracy: SHAP values sum to the model output: base_value + Σφᵢ = f(x).
  • Consistency: if a model changes so that a feature contributes more no matter which other features are present, its SHAP value will not decrease.
  • Missingness: features absent from a prediction receive a SHAP value of zero.

Workflow overview

Section Content
1–2 Environment setup and data loading
3–5 Feature selection, train-test split, and missing value imputation
6 Gradient Boosting Classifier training
7 TreeSHAP initialization and global SHAP value computation
8–9 Global summary: beeswarm and layered violin plots
10 Decision plot: cumulative feature effects per patient
11 Heatmap: SHAP values across the population
12–13 Local explanations: waterfall and force plots
14 SHAP vs impurity (MDI) feature importance comparison

Dataset: Cardiovascular Disease dataset (Kaggle – Aidan, https://www.kaggle.com/datasets/colewelkins/cardiovascular-disease).
Contains anonymized records for approximately 70,000 patients. Features include blood pressure, cholesterol, body mass index (BMI), age, and physical activity.
Target: binary indicator of cardiovascular disease (1 = present, 0 = absent).

1. Setup: import dependencies

Library Purpose
shap SHAP value computation and visualization (pip install shap)
numpy / pandas Numerical arrays and tabular data manipulation
matplotlib Plot rendering backend
sklearn Model training and imputation utilities
string Letter labels for anonymizing individual patients in plots

Note: enable_iterative_imputer must be imported before IterativeImputer because the class is still marked experimental in scikit-learn. The import registers it. The # noqa comment suppresses the “imported but unused” linter warning.

Python
import string

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import shap

from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier, RandomForestRegressor
from sklearn.experimental import enable_iterative_imputer  # noqa: F401 - activates IterativeImputer
from sklearn.impute import IterativeImputer, SimpleImputer

2. Load data and clean feature names

The raw CSV uses abbreviated column names (e.g., ap_hi for systolic arterial pressure, ap_lo for diastolic). We rename every column to a readable clinical label before anything else happens. Those names then carry through every downstream plot, which cuts the risk of misreading one.

Two features are immediately cast to pandas category dtype:

  • Cholesterol, an ordinal three-level scale: 1 = normal, 2 = above normal, 3 = well above normal.
  • Physical Activity, a binary flag: 1 = physically active, 0 = inactive.

units_map stores display labels that append measurement units (e.g., "Systolic BP (mmHg)"). These richer labels are reserved for individual-patient plots where the exact scale aids clinical interpretation.

Python
df = pd.read_csv("cardio_data_processed.csv")

# Rename abbreviated raw column names to human-readable clinical labels
df.rename(
    columns={
        "ap_hi":       "Systolic BP",
        "ap_lo":       "Diastolic BP",
        "cholesterol": "Cholesterol",
        "gluc":        "Glucose",
        "active":      "Physical Activity",
        "cardio":      "Cardiovascular Disease",
        "age_years":   "Age",
        "bmi":         "Body Mass Index",
    },
    inplace=True,
)

# Encode ordinal / binary columns as categorical to signal their discrete nature
df["Physical Activity"] = df["Physical Activity"].astype("category")
df["Cholesterol"]       = df["Cholesterol"].astype("category")

# Full labels with units, used in per-patient plots where scale context matters
units_map = {
    "Age":              "Age (years)",
    "Physical Activity": "Physical Activity (0 | 1)",
    "Systolic BP":      "Systolic BP (mmHg)",
    "Diastolic BP":     "Diastolic BP (mmHg)",
    "Cholesterol":      "Cholesterol (1 | 2 | 3)",
    "Body Mass Index":  "Body Mass Index (kg/m\u00b2)",
}

3. Feature selection

We predict cardiovascular disease from six clinically established risk factors:

Feature Type Clinical Relevance
Age Continuous Cardiovascular risk increases substantially with age
Physical Activity Binary Regular exercise is a major protective factor
Systolic BP Continuous Primary hypertension marker; key cardiovascular disease (CVD) risk driver
Diastolic BP Continuous Complements systolic in assessing overall BP burden
Cholesterol Ordinal (1-3) Elevated cholesterol is a well-established CVD risk factor
Body Mass Index Continuous Obesity correlates with hypertension and metabolic syndrome

Two parallel name lists are maintained:

  • features_short: used as model input column names and for concise axis labels in summary plots.
  • features_full: annotated with measurement units; used in per-patient plots (waterfall, force) where the exact scale aids clinical reading.
Python
# Short names: used as model column names and in summary plot axes
features_short = [
    "Age",
    "Physical Activity",
    "Systolic BP",
    "Diastolic BP",
    "Cholesterol",
    "Body Mass Index",
]

# Full names with units: used in per-patient plots for clinical readability
features_full = [units_map[c] for c in features_short]

X = df[features_short].copy()
y = df["Cardiovascular Disease"]

4. Train–test split

We split the data 80% training / 20% test.

Key design choices:

  • stratify=y: preserves the class balance (CVD : non-CVD ratio) in both splits. Without stratification, random chance could produce a test set with a different prevalence than the training set, leading to misleading evaluation metrics.
  • random_state=42: fixes the split for reproducibility.
  • BMI is rounded to two decimal places in the test set, reflecting realistic clinical measurement precision.

Data leakage principle: all preprocessing statistics (imputer parameters, scalers, encoders) must be estimated on the training set only and then applied to the test set. Fitting on the combined dataset would allow test-set information to influence training, inflating apparent model performance. We enforce this throughout.

Python
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.20, random_state=42, stratify=y
)

# Make an explicit independent copy before modifying to avoid SettingWithCopyWarning
X_test = X_test.copy()
X_test["Body Mass Index"] = X_test["Body Mass Index"].round(2)

5. Missing value imputation

Real-world clinical datasets routinely contain missing values. We apply different imputation strategies depending on the measurement scale of each feature.

Continuous features: Iterative Imputation (MICE)

IterativeImputer implements MICE (Multivariate Imputation by Chained Equations). It models each feature with missing values as a function of all other features, iterating until the imputed values converge. The scikit-learn class departs from the procedure the acronym names in one respect that matters here. It returns a single completed dataset rather than pooling several imputations, so it does not carry imputation uncertainty forward. A RandomForestRegressor is used as the base estimator, which handles nonlinear feature interactions without distributional assumptions.

  • initial_strategy='median': seeds the first iteration with the column median, which is robust to outliers.
  • max_iter=20: maximum number of imputation rounds.
  • n_estimators=500: sufficient trees for stable per-iteration estimates.

Categorical features: Mode imputation

SimpleImputer(strategy='most_frequent') replaces missing categories with the most frequent observed value. This is a conservative, non-parametric approach appropriate for ordinal and binary features.

Preventing data leakage

Both imputers are fit on the training set only (fit_transform), then applied to the test set with transform. The category dtype is restored after imputation because scikit-learn imputers return plain NumPy arrays and do not preserve pandas metadata.

Python
print("Performing mixed-type imputation...")

# Separate features by measurement scale for targeted imputation strategies
num_cols = [c for c in features_short if c not in ("Physical Activity", "Cholesterol")]
cat_cols = ["Physical Activity", "Cholesterol"]

print(f"  Continuous columns (MICE):  {num_cols}")
print(f"  Categorical columns (mode): {cat_cols}")

X_train_imp = X_train.copy()
X_test_imp  = X_test.copy()

# --- Continuous features: iterative imputation (MICE) ---
if num_cols:
    num_imputer = IterativeImputer(
        estimator=RandomForestRegressor(n_estimators=500, random_state=42),
        max_iter=20,
        random_state=42,
        initial_strategy="median",  # robust first-pass estimate
    )
    # Fit on training data only, then transform both sets
    X_train_imp[num_cols] = num_imputer.fit_transform(X_train_imp[num_cols])
    X_test_imp[num_cols]  = num_imputer.transform(X_test_imp[num_cols])

# --- Categorical features: mode imputation ---
if cat_cols:
    cat_imputer = SimpleImputer(strategy="most_frequent")
    # Fit on training data only, then transform both sets
    X_train_imp[cat_cols] = cat_imputer.fit_transform(X_train_imp[cat_cols])
    X_test_imp[cat_cols]  = cat_imputer.transform(X_test_imp[cat_cols])

X_train = X_train_imp.copy()
X_test  = X_test_imp.copy()

# Restore category dtype (scikit-learn imputers return plain numpy arrays)
for col in cat_cols:
    X_train[col] = X_train[col].astype("category")
    X_test[col]  = X_test[col].astype("category")

# Ensure continuous columns are stored as float64
if num_cols:
    X_train[num_cols] = X_train[num_cols].astype(np.float64)
    X_test[num_cols]  = X_test[num_cols].astype(np.float64)

print("Imputation complete.")

# Validate: report any remaining missing values
for label, frame in (("Training", X_train), ("Test", X_test)):
    n_missing = frame.isnull().sum().sum()
    if n_missing > 0:
        print(f"  Warning: {n_missing} missing values remain in {label} set")
        print(frame.isnull().sum()[frame.isnull().sum() > 0])
    else:
        print(f"  {label} set: no missing values.")
Performing mixed-type imputation...
  Continuous columns (MICE):  ['Age', 'Systolic BP', 'Diastolic BP', 'Body Mass Index']
  Categorical columns (mode): ['Physical Activity', 'Cholesterol']
Imputation complete.
  Training set: no missing values.
  Test set: no missing values.

If no warnings are printed above, imputation completed without residual missing values. Note that category dtypes are explicitly restored post-imputation. Scikit-learn’s imputers operate on NumPy arrays internally and return float arrays, discarding pandas metadata such as categorical encoding.

6. Training the gradient boosting classifier

GradientBoostingClassifier builds an ensemble of decision trees sequentially. Each successive tree is trained to correct the residual errors of the current ensemble, a strategy known as gradient boosting (Friedman, 2001).

Hyperparameter rationale:

Parameter Value Rationale
n_estimators 1000 Large ensemble for high expressive power
learning_rate 0.05 Low shrinkage: each tree contributes modestly, reducing overfitting
subsample 0.8 Stochastic GBM: each tree sees 80% of training samples, adding variance reduction analogous to bagging
random_state 42 Reproducibility

The pairing of many trees with a low learning rate is a well-established regularization strategy. The model learns slowly but thoroughly. subsample < 1.0 introduces randomness that further guards against overfitting and typically improves generalization.

Python
gb_clf = GradientBoostingClassifier(
    n_estimators=1000,
    learning_rate=0.05,
    subsample=0.8,
    random_state=42,
)

gb_clf.fit(X_train, y_train)
GradientBoostingClassifier(learning_rate=0.05, n_estimators=1000,
                           random_state=42, subsample=0.8)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

7. Computing SHAP values with TreeSHAP

Why TreeSHAP?

shap.TreeExplainer uses the exact TreeSHAP algorithm (Lundberg et al., 2020), which exploits the tree structure to compute SHAP values in polynomial time, orders of magnitude faster than evaluating all 2ⁿ feature subsets. That speed makes exact SHAP computation practical for large ensembles.

Output scale: raw log-odds

model_output="raw" returns SHAP values on the model’s internal log-odds (decision function) scale:

f(x) = base_value + φ₁ + φ₂ + ... + φₙ

where base_value is the average log-odds prediction across the training set and φᵢ is the SHAP value for feature i. To convert to a probability: p = 1 / (1 + exp(-f(x))).

Keeping values on the log-odds scale is preferred for gradient boosting models because it preserves the additive structure of the model’s internal representation.

Sampling for efficiency

We draw up to 10,000 test instances for global SHAP summaries. This subset is large enough to represent the full distribution while keeping computation time manageable.

API version note: older SHAP versions return shap_values() as a list with one array per class for binary classifiers. Modern versions return a single array for the positive class. The isinstance(raw_shap, list) check handles both behaviors transparently.

Python
# Build the SHAP explainer for the trained gradient boosting ensemble.
# model_output="raw" returns SHAP values on the log-odds scale.
explainer = shap.TreeExplainer(gb_clf, model_output="raw")

# Sample up to 10,000 test instances for efficient global visualisation
sample_n = min(10000, len(X_test))
X_sample = X_test.sample(sample_n, random_state=42)

# Compute SHAP values for the sampled test set
raw_shap = explainer.shap_values(X_sample)

# Handle both old SHAP API (list per class) and new API (single array)
if isinstance(raw_shap, list):
    shap_vals = raw_shap[1]              # index 1 = positive class (CVD = 1)
    base_val  = explainer.expected_value[1]
else:
    shap_vals = raw_shap
    base_val  = explainer.expected_value

# Guarantee base_val is a plain Python float regardless of SHAP version.
# Some versions return a 0-d numpy array, which cannot be used in f-string
# format specs (:.4f) or passed to np.full() as a scalar reliably.
base_val = float(np.squeeze(base_val))

# Wrap into a SHAP Explanation object, required by modern plot functions
expl_global = shap.Explanation(
    values=shap_vals,
    base_values=np.full(sample_n, base_val),
    data=X_sample.values,
    feature_names=features_short,
)

print(f"Base value (expected log-odds):  {base_val:.4f}")
print(f"Equivalent baseline probability: {1 / (1 + np.exp(-base_val)):.4f}")
print(f"SHAP value array shape:          {shap_vals.shape}  (samples x features)")
Base value (expected log-odds):  -0.0219
Equivalent baseline probability: 0.4945
SHAP value array shape:          (10000, 6)  (samples x features)

8. Beeswarm plot: global feature importance and effect direction

The beeswarm plot is the primary SHAP summary visualization. Each point represents one patient–feature pair:

  • Horizontal position (x-axis): SHAP value: how much this feature moved the prediction (in log-odds) away from the baseline. Positive = increased predicted CVD risk; negative = decreased predicted risk.
  • Color: raw feature value (red = high, blue = low).
  • Vertical ordering: features sorted top-to-bottom by mean |SHAP|, i.e., overall importance.

In a single plot, the beeswarm reveals three dimensions of information simultaneously:

  1. Feature importance: the vertical rank.
  2. Effect direction: whether high or low feature values push the prediction up or down.
  3. Effect heterogeneity: the spread of the point cloud reflects how consistently a feature acts across patients.
Python
shap.plots.beeswarm(expl_global, show=False)
plt.title("SHAP Beeswarm: Cardiovascular Disease Model")
plt.tight_layout()
plt.savefig("shap_beeswarm.png", dpi=200)
plt.show()
Figure 4

SHAP beeswarm across 10,000 test patients

Beeswarm plot of SHAP values across 10,000 test patients
Figure 4. One point per patient and feature for the 10,000 sampled test patients. Horizontal position is the SHAP value in log-odds, color is the raw feature value (red high, blue low), and rows are ordered by mean |SHAP|, which ranks by magnitude, with direction read from horizontal position. Systolic blood pressure (BP) leads at 0.828 and spreads widest, ahead of Age at 0.287 and Cholesterol at 0.196, each with red points sitting mostly right of zero. Physical Activity is the one row where red falls left. The spread shows the attribution varying from patient to patient. Whether the feature moves a patient’s risk is a causal question, answered by study design.

Interpreting the beeswarm plot:

  • Systolic BP (top row) is the dominant predictor. High values (red) produce large positive SHAP values, strongly increasing predicted CVD risk. The effect is approximately monotone. Progressively higher blood pressure maps to progressively higher log-odds.
  • Age shows a consistent positive gradient. Older patients (red) carry higher SHAP values, reflecting the well-established age–CVD relationship.
  • Cholesterol follows the same direction. A higher ordinal level (red = level 3) is associated with increased predicted risk.
  • Body Mass Index contributes positively at high values, consistent with clinical evidence linking obesity to cardiovascular outcomes.
  • Diastolic BP has a smaller but visible positive contribution. The beeswarm cannot show whether that contribution is independent of systolic BP. The two are strongly correlated, and how credit is split between correlated features depends on how the explainer treats the features left out of a coalition.
  • Physical Activity is the only feature with a predominantly negative SHAP contribution for high values. Active patients (red, value = 1) receive lower predicted risk. That describes how the model uses the feature, and stops short of any claim about exercise and the disease.

The wide spread of points along the x-axis shows that a feature’s contribution varies from patient to patient. A linear model produces spread too, whenever the feature itself varies, so width alone is not evidence of nonlinearity.

9. Layered violin plot: SHAP value distribution by feature

The layered violin plot conveys the same information as the beeswarm but replaces individual points with kernel density estimates. For large datasets (here 10,000 instances), this produces a less cluttered view and makes the density of SHAP values at each position more visible.

  • Width of each layer: density of SHAP values at that point on the x-axis.
  • Color encodes feature value (red = high, blue = low), as in the beeswarm.
  • Vertical ordering is identical to the beeswarm, with features ranked by mean |SHAP|.

Use the violin when you want to emphasize the shape of the SHAP distribution (e.g., bimodal effects for binary features), and the beeswarm when you want to reveal individual outliers.

Python
shap.plots.violin(expl_global, plot_type="layered_violin", show=False)
plt.title("SHAP Layered Violin Plot: Cardiovascular Disease Model")
plt.tight_layout()
plt.savefig("shap_layered_violin.png", dpi=200)
plt.show()
Figure 5

Layered violin of SHAP distributions

Layered violin plot of SHAP values by feature
Figure 5. The same 10,000 log-odds SHAP values as the beeswarm, redrawn as kernel densities so that layer width reports how many patients sit at each position and color still tracks the feature value. Row order is unchanged: Systolic BP, Age, Cholesterol, Body Mass Index, Diastolic BP, Physical Activity. Systolic BP shows the widest separation between its low and high layers, and Physical Activity is bimodal, which is what a binary feature should produce. Densities smooth away the individual outliers the beeswarm keeps, so the two plots answer different questions.

Interpreting the violin plot:

The violin plot shows the same SHAP values as the beeswarm in a different form, restating those findings without adding to them. For Systolic BP, the wide right-shifted (positive) density mass for high values and the left-shifted density for low values show a clear, wide separation, consistent with its top rank by mean absolute SHAP. The Physical Activity violin shows a characteristically bimodal shape. The two density layers (active vs inactive) are separated and point in opposite directions, reflecting the binary nature of this feature and the consistent, directionally opposing contributions the model assigns to its two levels.

10. Decision plot: cumulative feature effects across patients

The decision plot visualizes how each feature cumulatively shifts a prediction from the model baseline to its final output, with one line per patient. Reading the plot from the bottom up, each row adds one feature’s SHAP value to the running total.

  • x-axis: predicted value. We use link="logit" to convert from log-odds to probability (0–1), which is more clinically intuitive. The decomposition is additive in log-odds only. The link warps the axis, so equal SHAP values are not equal horizontal distances and the contributions do not sum on the plotted scale.
  • y-axis: features, listed in the order their SHAP values are accumulated (bottom to top by default).
  • Each line: one patient. Lines that end at the same point on the top row share similar final predictions. Diverging paths reveal that different features drive the prediction for different patients.
  • Line color: reflects the final predicted probability (cool = low risk, warm = high risk).

To keep the plot readable, 26 patients are drawn at random. The letter map assigns each row a letter from its position in the full dataframe modulo 26, so a random draw of 26 repeats some letters and misses others rather than covering A to Z once each, and decision_plot does not render the letters on the figure.

Python
# Map each original DataFrame row index to an anonymised letter label (A–Z, cycling)
LETTERS = string.ascii_uppercase

def num_to_letter(n: int) -> str:
    """Convert a 0-based integer to a letter: 0->A, 1->B, ..., 25->Z, 26->A, ..."""
    return LETTERS[n % 26]

letter_map = {idx: num_to_letter(i) for i, idx in enumerate(df.index)}

# Select 26 random patients for a readable decision plot
np.random.seed(42)
subset_idx = np.random.choice(
    X_sample.index,
    size=min(26, sample_n),
    replace=False,
)
X_decision = X_sample.loc[subset_idx].copy()
X_decision.index = [letter_map[i] for i in X_decision.index]

# Compute SHAP values for the decision plot subset
dec_shap = explainer.shap_values(X_decision)
dec_shap = dec_shap[1] if isinstance(dec_shap, list) else dec_shap

plt.figure(figsize=(11, 7))
shap.decision_plot(
    base_val,
    dec_shap,
    X_decision,
    feature_names=features_short,
    link="logit",   # convert log-odds to probability for a clinically meaningful x-axis
    highlight=None,
    show=False,
)
plt.title("SHAP Decision Plot: 26 Randomly Selected Patients")
plt.tight_layout()
plt.savefig("shap_decision_plot.png", dpi=200)
plt.show()
Figure 6

Decision plot for 26 random patients

Decision plot of SHAP values for 26 random patients
Figure 6. One line per patient for 26 cases drawn at random from the sampled test set. Each line starts at the base value and shifts left or right as one feature’s SHAP value is added, row by row from the bottom up to that patient’s prediction at the top, with color tracking the final value. The call passes link="logit", which warps the axis to probability. The contributions add up in log-odds, so equal contributions are equal steps on that scale. Lines fan apart most across the Systolic BP row. Near-vertical lines mark patients left close to the 0.495 baseline.

Interpreting the decision plot:

Lines clustered near the right of the plot represent high-risk patients. Those near the left are low-risk. What decision plots show is where divergence occurs. Lines begin to separate most strongly at the Systolic BP band, the same feature the beeswarm ranks first. Both readings come from the same SHAP values, so the plot shows that ranking again and adds no independent support. For some patients, lines cross the midline, where an initially below-average predicted risk is pushed up by the contributions from blood pressure or age, making each feature’s share of the total transparent and quantified. The bottom-to-top order is a plotting convention, and the model follows no such sequence. Patients whose lines stay near-vertical across multiple features (no large SHAP contributions) have a predicted risk close to the population baseline.

11. SHAP heatmap: population-level feature attribution

The SHAP heatmap provides a matrix view of SHAP values across many patients simultaneously:

  • Rows: features, ordered by mean absolute SHAP with the most important at the top.
  • Columns: individual patients (up to 5,000), ordered by hierarchical clustering of their SHAP profiles.
  • Cell color: SHAP value: red = positive contribution to the predicted risk, blue = negative.
  • Side panels: an f(x) trace above the matrix tracking the prediction for each patient, and a mean absolute SHAP bar panel at the right. The function draws no dendrogram.

This visualization is good for spotting patient clusters with distinct explanatory structures. For example, a cluster whose predicted risk is attributed mostly to blood pressure versus one attributed mostly to age and metabolic factors. Such patterns may guide population-level clinical strategies or suggest model validation across subgroups.

Python
# Select up to 5,000 patients for the heatmap; more samples improve the cluster resolution
np.random.seed(42)
heat_idx = np.random.choice(
    X_sample.index,
    size=min(5000, sample_n),
    replace=False,
)
X_heat = X_sample.loc[heat_idx].copy()

# Compute SHAP values for the heatmap subset
heat_sh = explainer.shap_values(X_heat)
heat_sh = heat_sh[1] if isinstance(heat_sh, list) else heat_sh

# Note: shap.plots.heatmap creates its own figure internally.
# Do NOT call plt.figure() beforehand, or an empty blank figure will be rendered.
shap.plots.heatmap(
    shap.Explanation(
        values=heat_sh,
        base_values=np.full(len(X_heat), base_val),
        data=X_heat.values,
        feature_names=features_short,
    ),
    show=False,
)
plt.title("SHAP Heatmap: 5,000 Test Patients")
plt.tight_layout()
plt.savefig("shap_heatmap.png", dpi=200)
plt.show()
Figure 7

SHAP heatmap of 5,000 test patients

Heatmap of SHAP values for 5,000 test patients
Figure 7. The six features run down the vertical axis and the 5,000 sampled patients run across the horizontal axis, ordered by hierarchical clustering of their SHAP vectors so patients with similar explanations sit side by side. Cell color is the SHAP value in log-odds, red positive and blue negative. The trace above the grid is the model output f(x) for each patient, and the bars at right are mean |SHAP| per feature. A long same-colored run along a row marks patients the model treats alike on that feature. A patchy row marks attribution that shifts with the rest of the record.

Interpreting the heatmap:

The hierarchical clustering typically reveals two broad patient cohorts, those with predominantly red cells along the Systolic BP row (high positive SHAP → high predicted risk) and those with predominantly blue cells (low or negative BP contribution → low predicted risk). Within the high-risk cluster, secondary separation often reflects age and cholesterol contributions. Bands of consistent color across many patients along a single row show that feature’s globally consistent direction of contribution, which is also visible in the beeswarm. Diagonal or patchy patterns along a feature row suggest heterogeneous, nonlinear, or interaction-dependent contributions.

12. Waterfall plots: dissecting individual predictions

The waterfall plot provides a detailed, per-patient decomposition of a single prediction:

  • The plot starts at E[f(X)]: the model’s baseline (expected) log-odds output.
  • Each bar adds or subtracts that feature’s SHAP value, building step-by-step toward the final prediction f(x) shown at the top.
  • Red bars (rightward): features that push the log-odds prediction up.
  • Blue bars (leftward): features that push the log-odds prediction down.
  • Bar width reflects SHAP magnitude. The feature value for this specific patient is shown alongside each label.

We generate waterfall plots for four randomly selected patients. The title shows the model’s predicted class and probability, giving immediate clinical context for the decomposition.

Python
# Fix the random seed so the same four patients are selected each run
np.random.seed(42)

plot_width  = 9   # inches
plot_height = 5   # inches

for idx in np.random.choice(X_sample.index, size=4, replace=False):
    letter = letter_map[idx]
    x_i = X_sample.loc[[idx]]

    # Compute SHAP values for this single patient
    sv_i = explainer.shap_values(x_i)
    sv_i = sv_i[1] if isinstance(sv_i, list) else sv_i

    # Use features_full (with units) so axis labels are clinically informative
    shap.plots.waterfall(
        shap.Explanation(
            values=sv_i[0],
            base_values=base_val,
            data=x_i.values[0],
            feature_names=features_full,
        ),
        show=False,
    )

    proba = gb_clf.predict_proba(x_i)[0, 1]
    pred  = gb_clf.predict(x_i)[0]

    plt.gcf().set_size_inches(plot_width, plot_height)
    plt.title(f"Patient {letter} \u2014 {'CVD' if pred else 'Non-CVD'} (P = {proba:.3f})")
    plt.tight_layout()
    plt.savefig(f"waterfall_plot_patient_{letter}.png", dpi=200)
    plt.show()
Figure 8

Waterfall decomposition for patient M

Waterfall plot of SHAP values for patient M
Figure 8. A single prediction taken apart. The plot starts at E[f(X)], the average log-odds output of −0.022, and each bar adds one feature’s SHAP value until the running total reaches f(x) at the top. Red bars point right and raise the log-odds, blue bars point left and lower them. Bars are ordered by magnitude, and each label carries this patient’s own reading with its units. The title reports the predicted class and probability, which comes from passing that log-odds total through the logistic function. The bars themselves sum to the log-odds total.
Figure 9

Waterfall decomposition for patient L

Waterfall plot of SHAP values for patient L
Figure 9. The second patient from the same four-patient loop, on the same layout. The bar order is set by the size of each contribution for this patient alone, so it need not follow the global mean |SHAP| ranking. A feature that dominates the population summary can sit low in one person’s decomposition. Labels carry this patient’s readings in clinical units. Add the six bars to the base value of −0.022 and the log-odds total is exactly f(x), which is the additivity guarantee that defines a SHAP explanation.
Figure 10

Waterfall decomposition for patient T

Waterfall plot of SHAP values for patient T
Figure 10. A third decomposition from the same loop. Read it by which bars are long, letting the colors take second place. A long bar means that feature moved the model’s output far from the baseline for this patient, given everything else in the record. The plot attributes a prediction, so a blue bar records that the model assigned lower log-odds here.
Figure 11

Waterfall decomposition for patient O

Waterfall plot of SHAP values for patient O
Figure 11. The last of the four patients. Read against the other three, it shows what a per-patient decomposition is for. Two patients can reach similar predicted probabilities with different bars behind them, so one total can rest on blood pressure and another on age and cholesterol. The decomposition describes how the model reaches each number.

Interpreting the waterfall plots:

Each patient’s waterfall tells a unique clinical story. For a high-risk patient (CVD, high probability), expect large red bars for Systolic BP and/or Age driving the prediction well above the baseline. For a low-risk patient (Non-CVD, low probability), wide blue bars, most commonly reflecting normal blood pressure, younger age, and physical activity, pull the prediction below the baseline. Comparing two patients with similar final probabilities but different bar patterns reveals that the model’s internal reasoning differs. The predicted risk rests mostly on blood pressure for one patient and mostly on age and cholesterol for another. This patient-level narrative is the primary value of waterfall plots in clinical decision support.

13. Force plot: visualizing feature forces for a single patient

The force plot is an alternative single-instance visualization that emphasizes the balance of opposing forces acting on a prediction:

  • The prediction starts at base value: the model’s population-average output.
  • Red arrows (rightward): features pushing toward higher CVD risk.
  • Blue arrows (leftward): features pushing toward lower risk.
  • Arrow width encodes SHAP magnitude.
  • The final prediction is where all forces come to rest.

Compared to the waterfall, the force plot sacrifices the step-by-step accumulation view in favor of a more immediate visual impression of the dominant factors. It is particularly effective for patient communication and clinical case presentations. The tug-of-war metaphor is intuitive for non-technical audiences.

Selecting a patient: adjust PATIENT_POSITION (0-indexed) to examine a different test-set patient.

Python
# Select a patient by their 0-based position in the test set.
# Change PATIENT_POSITION to examine a different patient.
PATIENT_POSITION = 22
USER_PATIENT_IDX = X_test.index[PATIENT_POSITION]

x_sel = X_test.loc[[USER_PATIENT_IDX]]

# Compute SHAP values for the selected patient
sv_sel = explainer.shap_values(x_sel)
sv_sel = sv_sel[1] if isinstance(sv_sel, list) else sv_sel

letter_sel = letter_map[USER_PATIENT_IDX]

shap.force_plot(
    base_val,
    sv_sel[0],
    x_sel.values,
    feature_names=features_full,
    matplotlib=True,
    figsize=(24, 4),
    show=False,
)

proba_sel = gb_clf.predict_proba(x_sel)[0, 1]
pred_sel  = gb_clf.predict(x_sel)[0]

plt.title(
    f"Patient {letter_sel} \u2014 {'CVD' if pred_sel else 'Non-CVD'} (P = {proba_sel:.3f})"
)
plt.tight_layout()
plt.savefig(f"force_plot_patient_{letter_sel}.png", dpi=200)
plt.show()
Figure 12

Force plot for patient K

Force plot of SHAP values for patient K
Figure 12. The same additive decomposition as the waterfalls, drawn for one patient as a single wide bar. The base value of −0.022 sits on the log-odds axis, red segments push right and blue segments push left, each segment as wide as that feature’s SHAP value, until they meet at f(x). Labels carry the patient’s readings with units. The layout trades the waterfall’s step-by-step ordering for one glance at which contributions dominate, and the title reports the predicted class and probability.

Interpreting the force plot:

The width and direction of each arrow immediately communicate which features moved this patient’s prediction most. A patient classified as CVD will show dominant wide red arrows, typically Systolic BP and/or Age, that overpower any blue (negative) contributions. A Non-CVD patient will show wide blue arrows, most commonly from normal blood pressure and an active lifestyle, with modest rightward contributions from age. This intuitive balance makes force plots effective for explaining individual model decisions to clinicians or patients unfamiliar with statistical model internals, bridging the gap between data-driven inference and clinical storytelling.

14. SHAP importance vs impurity (MDI) feature importance

To contextualize SHAP’s ranking, we compare it against the model’s built-in impurity importance (mean decrease in impurity, MDI). scikit-learn’s feature_importances_ is conventionally called Gini importance, and the code below labels the column that way, but for a gradient boosting classifier the base learners are regression trees fitted to pseudo-residuals, so the criterion is squared error (friedman_mse by default) rather than the Gini index of a classification forest:

Metric Definition Computed from
Mean abs(SHAP) Average absolute SHAP value across test instances: average impact on model output magnitude Model output (log-odds)
Impurity importance (MDI) Proportion of total node impurity reduction attributable to each feature across all trees Tree split structure

Known limitations of impurity importance (MDI):

  • Cardinality bias: continuous features with many possible split points (e.g., Systolic BP) have more opportunities to appear in splits and tend to be overweighted.
  • Disconnected from output: MDI is computed from the tree split structure, with the model output playing no part. It adds up the impurity reduction achieved at each split, weighted by the number of samples reaching the node, and never measures how much a feature changes predictions on held-out data.
  • No directionality: MDI cannot indicate whether a feature increases or decreases the prediction.

SHAP importance is generally preferred for interpretation because it is derived directly from the model’s output function, measured on held-out data, and decomposes each prediction exactly.

Python
# Mean absolute SHAP value per feature (computed on the 10,000-sample test subset)
mean_abs_shap = np.abs(shap_vals).mean(axis=0)

summary_df = (
    pd.DataFrame(
        {
            "Feature":          features_short,
            "Mean |SHAP|": mean_abs_shap,
            "Gini Importance":  gb_clf.feature_importances_,
        }
    )
    .sort_values("Mean |SHAP|", ascending=False)
)

print("Mean absolute SHAP vs Gini importance (sorted by SHAP):")
print(summary_df.to_string(index=False))
Mean absolute SHAP vs Gini importance (sorted by SHAP):
          Feature  Mean |SHAP|  Gini Importance
      Systolic BP     0.828423         0.686283
              Age     0.286570         0.131229
      Cholesterol     0.195615         0.073295
  Body Mass Index     0.147117         0.078425
     Diastolic BP     0.106140         0.021533
Physical Activity     0.076715         0.009235

Interpreting the results:

Both methods agree that Systolic BP is overwhelmingly the feature this model leans on most, but the rank ordering is not identical across both metrics. The two agree on the top two and the bottom two ranks and disagree on the middle pair, where mean absolute SHAP places Cholesterol above Body Mass Index and MDI reverses them. Agreement where it does occur is a consistency check on the model’s feature importance structure, and falls well short of proving it robust.

However, MDI assigns even greater relative dominance to Systolic BP (68.6% of total, a larger share than it takes in a normalized SHAP ranking), partly due to its cardinality bias toward continuous features with many split points.

Notably, Diastolic BP and Physical Activity gain the most relative importance in the move from MDI to SHAP, though every feature except Systolic BP gains some. This is consistent with MDI’s tendency to credit features that reduce impurity at many splits, whether or not they move the output. Two candidate explanations sit close to hand, that Physical Activity is binary and that Diastolic BP is strongly correlated with Systolic BP, which takes the splits first, but the notebook tests neither.

Both metrics are computed from the same fitted model, so their agreement is a consistency check and carries no independent evidence for the overall importance ranking. What the SHAP ranking does bear on, separately, is the model’s clinical plausibility. The top-ranked features, systolic blood pressure, age, and cholesterol, are precisely the risk factors most strongly supported by epidemiological evidence for cardiovascular disease. This alignment between data-driven SHAP values and established clinical knowledge is a plausibility check. External validation would need an external dataset. The notebook’s subject is the explanation itself, and held-out accuracy, AUC and calibration are the measures that speak to the quality of the model being explained.

Summary

Key Takeaways

An attribution describes the model

A SHAP value records how far a feature moved this model’s output from the base value. What would follow if the feature were changed lies outside its scope, because the object being explained is the model rather than the disease.

The contributions add up in log-odds

For the gradient boosted classifier here the explainer returns the raw decision function, so the base value of −0.022 and every contribution are log-odds. Probability is the logistic transform of the total, and probabilities themselves do not add.

Mean absolute SHAP discards direction

Ranking features by average attribution magnitude drops the sign, so a feature that raises the prediction for some patients and lowers it for others still ranks high. The beeswarm beside the bars is what restores direction and spread.

Exact Shapley is exponential, TreeSHAP polynomial

The definition averages over every coalition of features, one model evaluation per subset, so cost grows exponentially with the feature count. TreeSHAP exploits the tree structure to return exact values for tree ensembles in polynomial time.

Correlated features blur the split

Systolic and diastolic pressure carry overlapping information, and every SHAP value rests on a convention for the features left out of a coalition. No background sample is passed here, so that convention comes from the fitted trees themselves.

Both rankings rest on the same fit

Since both are computed from the same fitted model, their agreement is not independent evidence. They do not fully agree here in any case: SHAP places cholesterol above body mass index, and MDI reverses the pair.

Data & License

Dataset

Cardiovascular Disease dataset (about 70,000 patient records) published on Kaggle by Aidan (user “colewelkins”). Source: kaggle.com/datasets/colewelkins/cardiovascular-disease.License: Open Database License (ODbL) v1.0 for the database and Database Contents License (DbCL) v1.0 for its contents; used with attribution. ODbL 1.0 · DbCL 1.0 This dataset appears to be a re-upload; the license shown is the one declared on Kaggle, and its upstream chain of rights is unverified.

Article

© 2025 Philip Sarajlic. All rights reserved for the article’s original text and figures; the third-party data remains under the license shown above.

Code

Code examples in this article are licensed under the Common Public Attribution License Version 1.0 (CPAL-1.0), an OSI-approved copyleft license based on the Mozilla Public License 1.1. Initial Developer: Philip Sarajlic.

Attribution required by CPAL Exhibit B: © 2025 Philip Sarajlic · “Based on code by Philip Sarajlic” · philipsarajlic.com · no graphic image. This attribution must be displayed in Larger Works.

Modifications must be released in Source Code form under CPAL-1.0. Making the code usable by anyone other than you over a network is External Deployment under the license and is treated as distribution, so the Source Code must be made available to those users.

Full text: opensource.org/license/cpal-1-0 (SPDX identifier CPAL-1.0)

Scroll to Top

Free diagnostic

Would your model hold up to an external review?

Answer 39 questions in about nine minutes and get a clear picture of where your model stands.

You’ll receive a readiness score, a breakdown across 15 areas, your biggest evidence gaps, and the five questions an external reviewer would be most likely to ask first.

The assessment draws on guidance from:

  • TRIPOD+AI
  • PROBAST+AI
  • FDA GMLP
  • NIST AI RMF
  • SR 11-7

We’ll send you one email with your link, and your results when you finish. Your email address is carried through when you complete the assessment, so you won’t need to enter it again.

We don’t ask for your data, and there are no free-text fields in the assessment. See how we use your email address.