Python & Data Science
Explainability Under review

Christoph Molnar S Interpretable Machine Learning

Have you ever been in this meeting? The one where a stakeholder leans across the table, looks you dead in the eye, and asks, “Why did the model reject this loan application?” You built the model. You tuned it. The AUC is fantastic. But in that moment, all you can do is shrug and mumble something about “complex feature interactions.” The trust in the room evaporates. You’ve just collided with the black-box problem.

This is the pain Christoph Molnar’s book, Interpretable Machine Learning, is designed to cure. It’s not a book of abstract theory. It’s a field guide for practitioners like us who need to answer that question before it gets asked.

In plain English, interpretability is the ability to answer two questions: “How does your model work overall?” and “Why did it make this specific prediction?” Molnar gives us a powerful taxonomy to think about this. First, there’s the difference between intrinsic interpretability—models like linear regression that are simple enough to read directly—and post-hoc interpretation, where we use a separate tool to explain a black box. Then, we choose between model-specific methods (like TreeSHAP for tree-based models) and model-agnostic ones (like LIME, which works on anything). Finally, we decide if we need a global explanation of the entire model’s behavior or a local explanation for a single prediction.

This article is your practitioner’s summary of Molnar’s book. We won’t just list methods. We’ll walk through the ones you’ll actually use, expose their sharp edges with real code, and build a decision framework so you know exactly what to reach for when that stakeholder comes calling.

Start Simple: The Models That Explain Themselves

Before we build a black box and then struggle to explain it, Molnar makes a radical suggestion: what if we just… didn’t? What if we used a model that’s already transparent? This isn’t a beginner’s compromise. It’s a deliberate design choice for high-stakes decisions.

Linear Regression: The Gold Standard

Linear regression is the gold standard of interpretability. Every coefficient is a plain-English sentence. “Holding all else constant, one more square foot adds $$150 to the predicted house price.” You can say that to a loan officer, a doctor, or a judge, and they’ll understand it.

Let’s see this in action on the Palmer Penguins dataset, a favorite example in the book’s third edition. We’ll predict a penguin’s body mass using its flipper length and bill length.

# This block is fully self-contained.
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

# Load and prepare the penguins dataset
penguins = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv')
penguins = penguins.dropna(subset=['body_mass_g', 'flipper_length_mm', 'bill_length_mm'])

# Select features and target
features = ['flipper_length_mm', 'bill_length_mm']
X = penguins[features]
y = penguins['body_mass_g']

# Train a linear regression model
model = LinearRegression()
model.fit(X, y)

# Print the coefficients in a readable way
for feature, coef in zip(features, model.coef_):
    print(f"{feature}: {coef:.2f}")
print(f"intercept: {model.intercept_:.2f}")

The output tells a clear story. For every additional millimeter of flipper length, the model predicts the penguin’s body mass increases by roughly 50 grams, assuming the bill length stays the same. For every additional millimeter of bill length, the predicted body mass increases by about 60 grams. The intercept is deeply negative, which brings us to a subtle but critical warning from the book.

The Meaningless-Zero Problem

The intercept is the predicted body mass when all features are zero. A penguin with zero-millimeter flippers and a zero-millimeter bill doesn’t exist. Molnar flags this explicitly: the intercept is often a meaningless number. You should never interpret it as a baseline unless zero is a realistic value for all your features.

The Catch: Multicollinearity

Here’s the catch with that beautiful “holding all else constant” promise. It’s a lie when your features are correlated. What happens if we add a feature that’s almost a copy of one we already have? Let’s check that against the data by creating a noisy duplicate of flipper length.

# This block is fully self-contained.
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression

# Load and prepare the penguins dataset
penguins = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv')
penguins = penguins.dropna(subset=['body_mass_g', 'flipper_length_mm', 'bill_length_mm'])

# Create a correlated feature: flipper length plus a tiny bit of noise
np.random.seed(42)
penguins['flipper_noisy'] = penguins['flipper_length_mm'] + np.random.normal(0, 0.5, size=len(penguins))

# First, the original two features
X_original = penguins[['flipper_length_mm', 'bill_length_mm']]
y = penguins['body_mass_g']
model_orig = LinearRegression().fit(X_original, y)
print("Original model coefficients:")
for f, c in zip(X_original.columns, model_orig.coef_):
    print(f"  {f}: {c:.2f}")

# Now, with the correlated duplicate added
X_correlated = penguins[['flipper_length_mm', 'flipper_noisy', 'bill_length_mm']]
model_corr = LinearRegression().fit(X_correlated, y)
print("\nModel with correlated duplicate coefficients:")
for f, c in zip(X_correlated.columns, model_corr.coef_):
    print(f"  {f}: {c:.2f}")

Look at what happened. The original flipper length coefficient was positive and meaningful. Now, with the noisy duplicate added, the coefficients for flipper_length_mm and flipper_noisy have jumped around wildly—one might even be negative while the other is hugely positive. The model’s overall predictions haven’t changed much, but the explanation has become nonsense. This is multicollinearity, and Molnar warns it makes coefficients unstable and uninterpretable.

Lasso to the Rescue

The book’s recommendation for this mess is L1 regularization, or Lasso. Lasso doesn’t just prevent overfitting; it actively pushes the coefficients of useless or redundant features to exactly zero. This keeps your linear model interpretable even when you have many features.

Decision Trees and Rules: Flowcharts You Can Read

If linear regression is a sentence, a decision tree is a flowchart. You can draw it on a whiteboard. “If flipper length > 210mm, predict 5000g; otherwise, check bill length.” A stakeholder can trace the path and understand the logic instantly. The downside? Trees are high-variance. Feed them a slightly different sample of data, and a completely different tree may grow. Molnar also covers decision rules and RuleFit, which extract sparse if-then rules from trees, acting as a bridge between the flowchart’s simplicity and the stability of linear models.

Global Explanations: What the Model Thinks Is Important, Overall

Sometimes you can’t use a simple model. A random forest or gradient-boosted tree might be twice as accurate, and that accuracy matters for the business. When you’re stuck with a black box, you need post-hoc explanations. Global methods answer the question: “Across all predictions, what drives this model?”

Permutation Feature Importance: The Intuitive Workhorse

The idea behind permutation feature importance is beautifully simple. If a feature is important, scrambling it should make the model much worse. If it’s useless, scrambling it won’t matter. Molnar’s exact definition: randomly shuffle a single feature column, breaking its relationship with the target, and measure the increase in prediction error.

Let’s train a random forest and compute this. We’ll do it on test data, which is a critical rule from the book.

# This block is fully self-contained.
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.inspection import permutation_importance

# Load and prepare the penguins dataset
penguins = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv')
penguins = penguins.dropna(subset=['body_mass_g', 'flipper_length_mm', 'bill_length_mm', 'bill_depth_mm'])

features = ['flipper_length_mm', 'bill_length_mm', 'bill_depth_mm']
X = penguins[features]
y = penguins['body_mass_g']

# Split into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# Train a random forest
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

# Compute permutation importance on TEST data
perm_result = permutation_importance(rf, X_test, y_test, n_repeats=10, random_state=42, scoring='neg_mean_squared_error')

print("Permutation Feature Importance (on test data):")
for i, feature in enumerate(features):
    print(f"  {feature}: {perm_result.importances_mean[i]:.2f} +/- {perm_result.importances_std[i]:.2f}")

The output tells us that scrambling flipper length increases the model’s error the most. Bill length is second. Bill depth is a distant third. This is a global explanation: across all penguins in the test set, flipper length is the most important driver of body mass predictions.

The Correlated-Feature Trap

Now here’s the interesting part—and this is the hardest part of this method to get right. What if we compute importance on the training data instead of the test data? Let’s see what happens.

# This block is fully self-contained.
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.inspection import permutation_importance

# Load and prepare the penguins dataset
penguins = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv')
penguins = penguins.dropna(subset=['body_mass_g', 'flipper_length_mm', 'bill_length_mm', 'bill_depth_mm'])

features = ['flipper_length_mm', 'bill_length_mm', 'bill_depth_mm']
X = penguins[features]
y = penguins['body_mass_g']

# Split into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# Train a random forest
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

# Compute permutation importance on TRAINING data (the wrong way)
perm_train = permutation_importance(rf, X_train, y_train, n_repeats=10, random_state=42, scoring='neg_mean_squared_error')

print("Permutation Feature Importance (on TRAINING data - DON'T DO THIS):")
for i, feature in enumerate(features):
    print(f"  {feature}: {perm_train.importances_mean[i]:.2f} +/- {perm_train.importances_std[i]:.2f}")

The importance scores have collapsed. Everything looks equally unimportant. What’s actually going on here? The random forest overfit the training data. It memorized noise. When you scramble a feature on data the model has already memorized, it can use the other noisy correlations to recover the prediction, so the error barely increases. Molnar is explicit: always compute permutation importance on held-out test data.

There’s another, deeper trap. Imagine shuffling a feature like “height” while “weight” stays fixed. You create impossible humans—someone 2 meters tall and 30 kilograms. The model has never seen this combination, so its predictions go haywire. You wrongly conclude height is incredibly important, when really you just fed the model garbage data. This is the correlated-feature trap, and it haunts many interpretability methods.

Partial Dependence and ALE: The Visual Upgrade

A Partial Dependence Plot (PDP) shows the average prediction as you slide one feature up and down. It’s intuitive: “As flipper length increases, the predicted body mass goes up, but the effect levels off around 220mm.” The catch? PDPs assume features are independent, which means they average over those impossible human-style combinations.

Molnar recommends an upgrade: Accumulated Local Effects (ALE) plots. Instead of averaging over all data, ALE uses conditional distributions—what actually occurs in the data. It’s faster to compute and less biased when features are correlated. Think of ALE as the correlation-safe version of PDP, just as test-data importance is the safe version of permutation importance.

Local Explanations: Why This Specific Prediction?

Global methods are great for model debugging, but they don’t answer the question that gets asked in meetings: “Why did the model predict this for this customer?” For that, we need local explanations.

LIME: The Intuitive, Unstable One

LIME (Local Interpretable Model-agnostic Explanations) has a beautifully intuitive workflow. To explain one prediction, you wiggle the inputs around it, see how the black box responds, and fit a simple, interpretable model—like a linear regression—to those responses. That simple model is your explanation.

But wait—there’s a catch, and Molnar is more skeptical of LIME than of almost any other method. The size of the neighborhood you wiggle in is controlled by a kernel width, and there’s no principled way to choose it. As the book notes, you have to try different kernel settings and see for yourself. Two different kernel widths can give you two completely different explanations for the exact same prediction. That’s a trust problem. LIME is also unstable; slightly different perturbations can produce very different explanations. And worse, it’s manipulable—you can tune the kernel to hide bias.

Let’s generate a LIME explanation for a single penguin and see it in action.

# This block is fully self-contained.
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
import lime.lime_tabular

# Load and prepare the penguins dataset
penguins = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv')
penguins = penguins.dropna(subset=['body_mass_g', 'flipper_length_mm', 'bill_length_mm', 'bill_depth_mm'])

features = ['flipper_length_mm', 'bill_length_mm', 'bill_depth_mm']
X = penguins[features]
y = penguins['body_mass_g']

# Split into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# Train a random forest
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

# Create the LIME explainer
explainer = lime.lime_tabular.LimeTabularExplainer(
    X_train.values,
    feature_names=features,
    mode='regression',
    random_state=42
)

# Pick a single penguin from the test set to explain
penguin_idx = 0
penguin_instance = X_test.iloc[penguin_idx].values

# Generate LIME explanation with a specific kernel width
exp = explainer.explain_instance(penguin_instance, rf.predict, num_features=3, kernel_width=1.0)
print(f"LIME explanation for penguin {penguin_idx} (kernel_width=1.0):")
print(f"  Actual body mass: {y_test.iloc[penguin_idx]:.0f}g, Predicted: {rf.predict([penguin_instance])[0]:.0f}g")
for feature, weight in exp.as_list():
    print(f"  {feature}: {weight:.2f}")

# Now change the kernel width and see how the explanation shifts
print(f"\nLIME explanation for the SAME penguin (kernel_width=5.0):")
exp_wide = explainer.explain_instance(penguin_instance, rf.predict, num_features=3, kernel_width=5.0)
for feature, weight in exp_wide.as_list():
    print(f"  {feature}: {weight:.2f}")

Look at the two explanations. The feature weights have shifted. One might say bill length is the most important local factor; the other might emphasize flipper length. Same model, same penguin, different story. This is LIME’s fundamental instability, and it’s why Molnar urges caution.

SHAP: The Game-Theoretic One

SHAP (SHapley Additive exPlanations) takes a different approach. It borrows Shapley values from cooperative game theory. Think of it this way: each feature is a “player” in a game, and the prediction is the “payout.” Shapley values fairly distribute that payout among the players based on what each contributed, considering all possible coalitions of features.

Molnar calls this “Shapley values 2.0” because SHAP adapts the classic idea for machine learning. There are two engines: KernelSHAP (slow, model-agnostic, the reference implementation) and TreeSHAP (fast, tree-specific, what you’ll actually use in practice).

Let’s generate a SHAP explanation for the same penguin using TreeSHAP.

# This block is fully self-contained.
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
import shap

# Load and prepare the penguins dataset
penguins = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv')
penguins = penguins.dropna(subset=['body_mass_g', 'flipper_length_mm', 'bill_length_mm', 'bill_depth_mm'])

features = ['flipper_length_mm', 'bill_length_mm', 'bill_depth_mm']
X = penguins[features]
y = penguins['body_mass_g']

# Split into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# Train a random forest
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

# Create the TreeSHAP explainer
explainer = shap.TreeExplainer(rf)
shap_values = explainer.shap_values(X_test)

# Explain the same single penguin
penguin_idx = 0
print(f"SHAP explanation for penguin {penguin_idx}:")
print(f"  Actual body mass: {y_test.iloc[penguin_idx]:.0f}g, Predicted: {rf.predict([X_test.iloc[penguin_idx]])[0]:.0f}g")
print(f"  Base value (average prediction): {explainer.expected_value:.0f}g")
for i, feature in enumerate(features):
    shap_val = shap_values[penguin_idx][i]
    direction = "increased" if shap_val > 0 else "decreased"
    print(f"  {feature} = {X_test.iloc[penguin_idx][feature]:.1f}: {direction} prediction by {abs(shap_val):.1f}g")

This output is a complete accounting. The base value is the average prediction across all penguins. Each SHAP value tells you how much a specific feature pushed this specific prediction up or down from that average. “Your flipper length of 215mm increased the predicted body mass by 200 grams.” That’s a sentence a stakeholder can understand.

SHAP’s Big Caveat

But SHAP has the same trap as permutation importance and PDP. It uses a marginal-distribution approach that ignores feature dependence. It can overweight unrealistic feature combinations. The book explicitly warns that Shapley values “can be misinterpreted” and could be used to “create intentionally misleading interpretations” that mask bias. A feature might look like it’s driving the prediction fairly, but the SHAP value could be computed using impossible coalitions of features. This is the hardest part of trusting SHAP.

The Methods You Might Not Need (Yet)

Molnar’s book covers more than the big four. Here’s a quick-reference map of the remaining methods, so you know they exist without getting lost.

  • Individual Conditional Expectation (ICE) Plots: Like PDP, but instead of showing the average line, it shows every individual line. Useful when the average hides heterogeneity—maybe flipper length matters a lot for some penguins and not at all for others. The downside: it gets visually overwhelming fast.
  • Counterfactual Explanations: “What would need to change for the model to predict APPROVED instead of DENIED?” This is incredibly intuitive for stakeholders. The book notes that there are often many valid counterfactuals, and choosing one is non-trivial.
  • Anchors: LIME’s higher-fidelity cousin. Instead of a local linear model, Anchors find if-then rules that “anchor” a prediction with high precision. More interpretable, but computationally expensive.
  • Prototypes and Criticisms: Select representative examples (prototypes) and outliers the model struggles with (criticisms). This is useful for understanding your data, not just your model.
  • Neural Network Interpretation: Saliency maps, concept detection, adversarial examples. The book covers these, but they’re domain-specific. Most tabular-data practitioners can skip this chapter on first reading.

The Practitioner’s Decision Framework

All these methods, all these caveats—how do you actually choose? Here’s a synthesis of the book’s guidance into a decision framework you can use on Monday morning.

Fork 1: Local vs. Global. Is the stakeholder asking about a specific decision (“Why was my loan denied?”) or about the model overall (“What drives loan denials in general?”)? For a specific decision, you need local methods: SHAP, LIME, or counterfactuals. For model-wide behavior, you need global methods: permutation importance, PDP, or ALE.

Fork 2: Can you use an interpretable model? If yes, do it. A linear regression with Lasso, a shallow decision tree, or RuleFit is more trustworthy than any post-hoc explanation. No explanation tool is as honest as a model you can read directly.

Fork 3: Are your features correlated? If yes, permutation importance, PDP, and SHAP all share the same marginal-distribution trap. For global explanations, ALE is the safer choice. For local explanations, you need to acknowledge the limitation and interpret SHAP values with that asterisk.

Fork 4: How much time do you have? KernelSHAP is slow. TreeSHAP is fast. LIME is fast but unstable. Anchors are slow but high-fidelity. Prototypes and criticisms require no model retraining—just data selection.

Molnar focuses on tabular data, and his unspoken hierarchy is clear: ALE is the upgrade to PDP. TreeSHAP is the practical SHAP. And interpretable models are the ideal whenever feasible.

What the Book Doesn’t Cover (and Where to Go Next)

No book does everything, and naming the gaps honestly builds trust.

Molnar focuses on tabular data. Deep-learning-specific interpretability—saliency maps, integrated gradients, transformer attention—gets one chapter but isn’t the core audience. If you work primarily with images or text, you’ll need supplementary resources.

More importantly, the book explains what features drive predictions, but not whether those features cause the outcome. A feature can be highly important for prediction but have zero causal effect. This is the difference between prediction and causal inference, and it’s a whole separate field.

The book mentions that interpretability can mask bias—that SHAP warning about misleading interpretations—but it’s not a fairness textbook. For fairness-specific audits, you’ll need dedicated resources.

Time-series and sequential data are not covered. The methods assume independent, identically distributed data.

Where to go next? The book’s own references section is a goldmine. Dive into Cynthia Rudin’s work on inherently interpretable models. Read the original ALE paper by Apley and Zhu. Explore the SHAP paper by Lundberg and Lee. And get hands-on with the growing ecosystem of tools: the shap and lime libraries you’ve already used here, plus Microsoft’s InterpretML, the Dalex package in R and Python, and Seldon’s Alibi.

Recap and Next Steps

We’ve traveled from the black-box problem to a full toolkit for interpretability. Here’s what to remember:

  • Permutation Feature Importance is your global workhorse—fast, intuitive, but compute it on test data and watch out for correlated features.
  • PDP and ALE give you global, visual explanations. ALE is the correlation-safe upgrade.
  • LIME gives you local explanations that are intuitive but unstable. The kernel width is a choice with no right answer.
  • SHAP gives you local explanations with a game-theoretic foundation. TreeSHAP is fast; KernelSHAP is slow. But it shares the marginal-distribution trap.
  • The decision hierarchy: Prefer interpretable models. If you can’t, choose local vs. global, then choose based on feature correlation and compute budget.

The book’s core philosophy is this: interpretability is not a single technique. It’s a practice of asking “Can I explain this to a stakeholder?” at every stage of the machine learning lifecycle.

Check Your Understanding

Remember: What are the two main questions that interpretability seeks to answer?

Understand: In your own words, why does multicollinearity make linear regression coefficients unstable?

Apply: You’ve trained a random forest on customer data. A stakeholder asks, “What drives churn overall?” Which method do you reach for first, and why?

Analyze: You compute permutation importance on your training data and find that all features have near-zero importance. What’s the most likely explanation?

Evaluate: A colleague says, “I used LIME with the default kernel width, so my explanation is objective.” What would you tell them?

Create: Design a one-page “interpretability report” template for your team. What sections would it include? Which methods would you use for each section?

Apply What You Learned is for Supporter and Insider subscribers.

Subscribe to unlock the exercises on this post.

See plans

Looking for something else?

Search every article by title, summary or topic.