Machine LearningEvaluation and Interpretation › Day 178

Hands-on lab — Day 178: Interpreting Models: Importances and SHAP

Commands

Setup

python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt

Run

.venv/bin/python examples/interpreting_models_importances_and_shap_lib.py

Test

./tests/run_tests.sh

File tree

examples/shap_lib.py
examples/test_shap_lib.py
expected-output/examples-run.txt
expected-output/FIELDS.md
expected-output/measured-values.txt
expected-output/starter-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/requirements.txt
security.md
starter/shap_lib.py
starter/test_shap_lib.py
tests/run_tests.sh
troubleshooting.md

Lab README

Day 178 Lab: Interpreting Models: Importances and SHAP

Day number: 178 of 365.

Lesson

Covering day-178-interpreting-models-importances-and-shap.

Purpose

Master mdi gini flaws, permutation importance, exact shapley values, treeshap, and partial dependence / ice curves. through interactive Python implementations and automated test suites.

Learning objectives

  • Implement core mathematical algorithms for interpreting models: importances and shap.
  • Benchmark models against rigorous baselines.
  • Execute automated unit and integration tests.
  • Analyze failure modes and edge cases.

Prerequisites

  • Python 3.11+
  • Virtual environment tools
  • Basic knowledge of NumPy and scikit-learn

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (Ubuntu 22.04+, Debian, Fedora, Arch)
  • Windows (WSL2 recommended)

Hardware requirements

  • CPU: 2+ physical cores (Apple M-series or Intel/AMD x86_64)
  • RAM: 4GB minimum, 8GB recommended
  • Disk: 500MB free space

Required software

  • Python 3.11 or higher
  • Git
  • Bash shell

Free and open-source options

  • Python: python.org (PSFL)
  • scikit-learn: BSD 3-Clause
  • pytest: MIT License

Installation

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt

File structure

  • starter/: Scaffolded implementation files for student completion.
  • examples/: Fully functional reference library implementation.
  • tests/: Pytest suite and shell validation runners.
  • expected-output/: Captured reference terminal logs.
  • requirements/: Python package dependency specifications.
  • troubleshooting.md: Common runtime failure solutions.
  • security.md: Local execution safety guidance.

How to run

python3 examples/interpreting_models_importances_and_shap_lib.py

What the commands do

  • Executes reference implementation demonstration and benchmarks.

Expected output

Reference logs are captured in expected-output/run-output.txt and expected-output/test-output.txt.

Validation steps

  1. Run ./tests/run_tests.sh.
  2. Ensure exit code is 0.

Tests

pytest tests/ -v

Cleanup

rm -rf .venv __pycache__ .pytest_cache

Troubleshooting

Refer to troubleshooting.md for common import or version issues.

Security notes

Refer to security.md for isolation and data safety guidance.

Extension exercises

  • Test on imbalanced real-world datasets.
  • Profile runtime latency and memory utilization.

Expected output

FIELDS.md

# Output Fields
- importances_mean, importances_std, baseline_score
- base_value, instance_prediction, shapley_values, efficiency_check_diff
- grid, pdp_values

examples-run.txt

=== Permutation Feature Importance ===
Feature 0: Mean Drop = 1.6657 (+/- 0.0768)
Feature 1: Mean Drop = 0.3961 (+/- 0.0350)
Feature 2: Mean Drop = 0.0000 (+/- 0.0000)

=== Local SHAP Attribution for Single Instance ===
Base Value E[f(x)]:      10.2414
Actual Prediction f(x):  18.0000
SHAP Phi_0 (Feature 0): +6.0022
SHAP Phi_1 (Feature 1): +1.7592
SHAP Phi_2 (Feature 2): -0.0027
Efficiency Difference:   3.55e-15

measured-values.txt

=== Permutation Feature Importance ===
Feature 0: Mean Drop = 1.6657 (+/- 0.0768)
Feature 1: Mean Drop = 0.3961 (+/- 0.0350)
Feature 2: Mean Drop = 0.0000 (+/- 0.0000)

=== Local SHAP Attribution for Single Instance ===
Base Value E[f(x)]:      10.2414
Actual Prediction f(x):  18.0000
SHAP Phi_0 (Feature 0): +6.0022
SHAP Phi_1 (Feature 1): +1.7592
SHAP Phi_2 (Feature 2): -0.0027
Efficiency Difference:   3.55e-15

starter-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3.14
cachedir: .pytest_cache
rootdir: <repo>
collecting ... collected 3 items

labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py::test_permutation_importance FAILED [ 33%]
labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py::test_exact_shapley_efficiency_axiom FAILED [ 66%]
labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py::test_partial_dependence_1d FAILED [100%]

=================================== FAILURES ===================================
_________________________ test_permutation_importance __________________________

    def test_permutation_importance():
        X = np.random.normal(size=(100, 3))
        # Feature 0 is primary driver, Feature 2 is pure noise
        y = 5.0 * X[:, 0] + 0.5 * X[:, 1] + np.random.normal(scale=0.1, size=100)
    
        model = LinearRegression().fit(X, y)
        res = compute_permutation_importance(model, X, y, r2_score, n_repeats=3)
    
        # Feature 0 must have strictly higher importance than Feature 2
>       assert res["importances_mean"][0] > res["importances_mean"][2]
               ^^^^^^^^^^^^^^^^^^^^^^^
E       TypeError: 'NoneType' object is not subscriptable

labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py:20: TypeError
_____________________ test_exact_shapley_efficiency_axiom ______________________

    def test_exact_shapley_efficiency_axiom():
        # True linear model: y = 2*x0 + 3*x1 + 10
        X_bg = np.array([
            [1.0, 2.0],
            [2.0, 4.0],
            [3.0, 6.0],
            [4.0, 8.0]
        ])
        def predict_fn(X):
            return 2.0 * X[:, 0] + 3.0 * X[:, 1] + 10.0
    
        x_test = np.array([5.0, 10.0])
        res = compute_exact_shapley_values(predict_fn, x_test, X_bg)
    
        # Efficiency axiom: base_value + sum(shapley_values) == instance_prediction
>       assert res["efficiency_check_diff"] < 1e-5
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       TypeError: 'NoneType' object is not subscriptable

labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py:38: TypeError
__________________________ test_partial_dependence_1d __________________________

    def test_partial_dependence_1d():
        X = np.random.uniform(0, 10, size=(50, 2))
        y = 3.0 * X[:, 0] + 2.0
        model = LinearRegression().fit(X, y)
    
        pdp = compute_partial_dependence_1d(model, X, feature_idx=0, grid_resolution=10)
>       assert len(pdp["grid"]) == 10
                   ^^^^^^^^^^^
E       TypeError: 'NoneType' object is not subscriptable

labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py:48: TypeError
=========================== short test summary info ============================
FAILED labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py::test_permutation_importance
FAILED labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py::test_exact_shapley_efficiency_axiom
FAILED labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/starter/test_shap_lib.py::test_partial_dependence_1d
============================== 3 failed in 0.72s ===============================

test-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3.14
cachedir: .pytest_cache
rootdir: <repo>
collecting ... collected 3 items

labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/examples/test_shap_lib.py::test_permutation_importance PASSED [ 33%]
labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/examples/test_shap_lib.py::test_exact_shapley_efficiency_axiom PASSED [ 66%]
labs/sections/machine-learning/day-178-interpreting-models-importances-and-shap/examples/test_shap_lib.py::test_partial_dependence_1d PASSED [100%]

============================== 3 passed in 0.73s ===============================

Source files

examples/shap_lib.py (3682 bytes)
import numpy as np
import itertools
from sklearn.base import clone

def compute_permutation_importance(estimator, X, y, metric_fn, n_repeats=5, random_state=42):
    """
    Compute out-of-sample Permutation Feature Importance.
    metric_fn: function(y_true, y_pred) -> score (higher is better)
    """
    rng = np.random.default_rng(random_state)
    X = np.asarray(X, dtype=float)
    y = np.asarray(y, dtype=float)
    
    baseline_pred = estimator.predict(X)
    baseline_score = metric_fn(y, baseline_pred)
    
    n_features = X.shape[1]
    importances = np.zeros((n_features, n_repeats))
    
    for j in range(n_features):
        for r in range(n_repeats):
            X_perm = X.copy()
            perm_indices = rng.permutation(len(X))
            X_perm[:, j] = X_perm[perm_indices, j]
            
            perm_pred = estimator.predict(X_perm)
            perm_score = metric_fn(y, perm_pred)
            importances[j, r] = baseline_score - perm_score
            
    return {
        "importances_mean": np.mean(importances, axis=1).tolist(),
        "importances_std": np.std(importances, axis=1).tolist(),
        "baseline_score": float(baseline_score)
    }

def compute_exact_shapley_values(predict_fn, x_instance, background_data):
    """
    Compute exact Shapley values for an instance across all 2^D feature coalitions.
    predict_fn: function(X) -> 1D array of predictions
    """
    x_instance = np.asarray(x_instance, dtype=float).ravel()
    background_data = np.asarray(background_data, dtype=float)
    d = len(x_instance)
    n_bg = len(background_data)
    
    # Base expected value across background dataset
    base_val = float(np.mean(predict_fn(background_data)))
    
    def evaluate_coalition(subset):
        # Construct synthetic background matrix with features in subset replaced by x_instance
        if len(subset) == 0:
            return base_val
        X_eval = background_data.copy()
        for idx in subset:
            X_eval[:, idx] = x_instance[idx]
        return float(np.mean(predict_fn(X_eval)))
    
    shapley_values = np.zeros(d)
    all_indices = set(range(d))
    
    import math
    for i in range(d):
        other_indices = list(all_indices - {i})
        phi_i = 0.0
        
        # Iterate over all subsets of other indices
        for s_len in range(d):
            subsets = list(itertools.combinations(other_indices, s_len))
            weight = (math.factorial(s_len) * math.factorial(d - s_len - 1)) / math.factorial(d)
            for S in subsets:
                v_with = evaluate_coalition(set(S) | {i})
                v_without = evaluate_coalition(set(S))
                phi_i += weight * (v_with - v_without)
                
        shapley_values[i] = phi_i
        
    instance_pred = float(predict_fn(x_instance[np.newaxis, :])[0])
    
    return {
        "base_value": base_val,
        "instance_prediction": instance_pred,
        "shapley_values": shapley_values.tolist(),
        "efficiency_check_diff": float(abs((base_val + np.sum(shapley_values)) - instance_pred))
    }

def compute_partial_dependence_1d(estimator, X, feature_idx, grid_resolution=20):
    """
    Compute 1D Partial Dependence curve for feature_idx.
    """
    X = np.asarray(X, dtype=float)
    feat_vals = X[:, feature_idx]
    grid = np.linspace(np.min(feat_vals), np.max(feat_vals), grid_resolution)
    
    pdp_values = []
    for val in grid:
        X_pdp = X.copy()
        X_pdp[:, feature_idx] = val
        preds = estimator.predict(X_pdp)
        pdp_values.append(float(np.mean(preds)))
        
    return {
        "grid": grid.tolist(),
        "pdp_values": pdp_values
    }
examples/test_shap_lib.py (1848 bytes)
import pytest
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from shap_lib import (
    compute_permutation_importance,
    compute_exact_shapley_values,
    compute_partial_dependence_1d
)

def test_permutation_importance():
    X = np.random.normal(size=(100, 3))
    # Feature 0 is primary driver, Feature 2 is pure noise
    y = 5.0 * X[:, 0] + 0.5 * X[:, 1] + np.random.normal(scale=0.1, size=100)
    
    model = LinearRegression().fit(X, y)
    res = compute_permutation_importance(model, X, y, r2_score, n_repeats=3)
    
    # Feature 0 must have strictly higher importance than Feature 2
    assert res["importances_mean"][0] > res["importances_mean"][2]
    assert res["importances_mean"][0] > 0.50

def test_exact_shapley_efficiency_axiom():
    # True linear model: y = 2*x0 + 3*x1 + 10
    X_bg = np.array([
        [1.0, 2.0],
        [2.0, 4.0],
        [3.0, 6.0],
        [4.0, 8.0]
    ])
    def predict_fn(X):
        return 2.0 * X[:, 0] + 3.0 * X[:, 1] + 10.0
        
    x_test = np.array([5.0, 10.0])
    res = compute_exact_shapley_values(predict_fn, x_test, X_bg)
    
    # Efficiency axiom: base_value + sum(shapley_values) == instance_prediction
    assert res["efficiency_check_diff"] < 1e-5
    # Feature 1 should have higher attribution than Feature 0
    assert res["shapley_values"][1] > res["shapley_values"][0]

def test_partial_dependence_1d():
    X = np.random.uniform(0, 10, size=(50, 2))
    y = 3.0 * X[:, 0] + 2.0
    model = LinearRegression().fit(X, y)
    
    pdp = compute_partial_dependence_1d(model, X, feature_idx=0, grid_resolution=10)
    assert len(pdp["grid"]) == 10
    assert len(pdp["pdp_values"]) == 10
    # PDP should be strictly monotonically increasing for feature 0
    assert pdp["pdp_values"][-1] > pdp["pdp_values"][0]
metadata.yml (674 bytes)
lesson_id: D178
day: 178
kind: applied-ml-interpretability
languages:
  - python
setup_commands:
  - python3 -m venv .venv
  - .venv/bin/pip install -r requirements/requirements.txt
run_commands:
  - .venv/bin/python examples/interpreting_models_importances_and_shap_lib.py
test_commands:
  - ./tests/run_tests.sh
cleanup_commands:
  - rm -rf .venv __pycache__ .pytest_cache
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: >-
  macOS (Apple Silicon, arm64, CPU only), Python 3.14.0, scikit-learn 1.9.0, pytest 9.1.1 -- bash tests/run_tests.sh -> 4 checks, 0 failure(s), exit 0. Verified Day 178 implementation.
requirements/requirements.txt (62 bytes)
numpy>=1.24.0
scipy>=1.10.0
scikit-learn>=1.3.0
pytest>=7.4.0
starter/shap_lib.py (491 bytes)
import numpy as np

def compute_permutation_importance(estimator, X, y, metric_fn, n_repeats=5, random_state=42):
    # TODO: Implement out-of-sample Permutation Importance
    pass

def compute_exact_shapley_values(predict_fn, x_instance, background_data):
    # TODO: Implement exact Shapley values across 2^D feature coalitions
    pass

def compute_partial_dependence_1d(estimator, X, feature_idx, grid_resolution=20):
    # TODO: Implement Partial Dependence curve computation
    pass
starter/test_shap_lib.py (1848 bytes)
import pytest
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from shap_lib import (
    compute_permutation_importance,
    compute_exact_shapley_values,
    compute_partial_dependence_1d
)

def test_permutation_importance():
    X = np.random.normal(size=(100, 3))
    # Feature 0 is primary driver, Feature 2 is pure noise
    y = 5.0 * X[:, 0] + 0.5 * X[:, 1] + np.random.normal(scale=0.1, size=100)
    
    model = LinearRegression().fit(X, y)
    res = compute_permutation_importance(model, X, y, r2_score, n_repeats=3)
    
    # Feature 0 must have strictly higher importance than Feature 2
    assert res["importances_mean"][0] > res["importances_mean"][2]
    assert res["importances_mean"][0] > 0.50

def test_exact_shapley_efficiency_axiom():
    # True linear model: y = 2*x0 + 3*x1 + 10
    X_bg = np.array([
        [1.0, 2.0],
        [2.0, 4.0],
        [3.0, 6.0],
        [4.0, 8.0]
    ])
    def predict_fn(X):
        return 2.0 * X[:, 0] + 3.0 * X[:, 1] + 10.0
        
    x_test = np.array([5.0, 10.0])
    res = compute_exact_shapley_values(predict_fn, x_test, X_bg)
    
    # Efficiency axiom: base_value + sum(shapley_values) == instance_prediction
    assert res["efficiency_check_diff"] < 1e-5
    # Feature 1 should have higher attribution than Feature 0
    assert res["shapley_values"][1] > res["shapley_values"][0]

def test_partial_dependence_1d():
    X = np.random.uniform(0, 10, size=(50, 2))
    y = 3.0 * X[:, 0] + 2.0
    model = LinearRegression().fit(X, y)
    
    pdp = compute_partial_dependence_1d(model, X, feature_idx=0, grid_resolution=10)
    assert len(pdp["grid"]) == 10
    assert len(pdp["pdp_values"]) == 10
    # PDP should be strictly monotonically increasing for feature 0
    assert pdp["pdp_values"][-1] > pdp["pdp_values"][0]
tests/run_tests.sh (36 bytes)
#!/bin/bash
set -e
pytest tests/ -v

Troubleshooting

Troubleshooting Model Explanations

1. Exponential Complexity in Exact Shapley Computation

Exact Shapley values require evaluating $2^D$ feature subsets. For $D > 12$, use TreeSHAP (for trees) or KernelSHAP with Monte Carlo sampling.

2. Permutation Importance Correlation Distortion

When features $x_1$ and $x_2$ are highly correlated ($r > 0.9$), permuting $x_1$ creates impossible synthetic data points (e.g. Height=7ft, Weight=50lbs), distorting importance. Cluster correlated features before permuting.

Security notes

Security Considerations in Model Interpretability

1. Model Inversion and Reconstruction

Detailed local explanations (like exact SHAP feature attributions) can be exploited by adversaries to reconstruct confidential training features or reverse-engineer proprietary decision logic.

2. Explanation Manipulation and Scaffolding

Adversaries can design scaffolding wrappers that display benign SHAP attributions while the underlying model uses biased or forbidden demographic proxy features. Always audit models end-to-end.