100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
ML Ops & Data Science in Production
55 minadvanced

Practice — SHAP Analysis on a Boosted Model

What You'll Build

In this hands-on exercise you will train an XGBoost churn classifier on a synthetic IPL player dataset, then use the SHAP library to explain the model's predictions at both global and individual level. You will produce a summary bar plot showing which features most influence churn across all players, a beeswarm plot showing the direction and magnitude of each feature's impact, and a waterfall plot for a single player — Rohit Sharma — breaking down why the model assigned him a specific churn probability. Finally, you will expose a FastAPI endpoint that accepts player stats and returns SHAP values as JSON, making explainability a first-class part of your production API.

Analogy🏏Cricket
🏏 Think of it like cricket: Before the IPL auction, a franchise's analytics team evaluates every player across dozens of trial matches — tracking Rohit Sharma's strike rate in power plays, Virat Kohli's average against pace, MS Dhoni's finishing rate in the death overs, Shubman Gill's consistency across pitches, and Jasprit Bumrah's economy in the middle overs. Each trial is recorded in a shared logbook so the selectors can compare and pick the best combination. MLflow is exactly that shared logbook for your ML experiments — every training run is a trial match, every metric is a scorecard entry, and the Model Registry is the final squad announcement. Keep the auction framing in mind throughout the exercise, because it fixes the discipline the steps teach: a franchise never signs a player off one good highlight reel, and you never register a model off one lucky run — you log every trial, compare them on identical conditions, and promote only with the full scorecard in front of you.

Prerequisites

Before starting this exercise you should be comfortable with Python 3.9 or later, basic scikit-learn usage including fit and predict, and pandas DataFrame operations. You do not need prior SHAP or XGBoost experience — this exercise introduces both from scratch. Ensure you have a terminal with a virtual environment where you can install packages freely. The SHAP library requires matplotlib for plot rendering; ensure matplotlib is installed alongside shap. For the FastAPI section you will need a second terminal tab to run the uvicorn server while keeping your notebook or script open in the first tab.

Analogy🏏Cricket
🏏 Think of it like cricket: the kit check before a net session. Just as a batter arriving for practice needs pads, gloves, and a bat they already know how to use — but doesn't need to have faced the new bowling machine before, because today's session is exactly where they'll learn it — this exercise expects you to arrive comfortable with Python, basic scikit-learn (fit, predict, train_test_split), and pandas, while MLflow itself is taught from scratch. Just as the coach insists on a properly prepared practice pitch — a clean, dedicated strip rather than the match square — you need a clean virtual environment or Conda environment where packages can be installed freely. And just as the only outside help needed is the equipment delivery van arriving once before practice, network access is required only for the initial pip install; after that everything runs locally, whether your 'net' is a laptop, a Docker container, or a cloud notebook. The payoff: checking your kit now means the session ahead is pure skill-building, with no stoppages for missing gear.

Setup

python
# Install all required dependencies for this SHAP exercise
# Run this block once before executing any other section

# Option 1: pip
!pip install xgboost shap fastapi uvicorn pandas numpy matplotlib scikit-learn

# Option 2: conda
# conda install -c conda-forge xgboost shap fastapi uvicorn pandas numpy matplotlib scikit-learn

import xgboost
import shap
import fastapi
import pandas as pd
import numpy as np
import matplotlib
import sklearn

print(f"XGBoost     : {xgboost.__version__}")
print(f"SHAP        : {shap.__version__}")
print(f"FastAPI     : {fastapi.__version__}")
print(f"Pandas      : {pd.__version__}")
print(f"NumPy       : {np.__version__}")
print(f"Matplotlib  : {matplotlib.__version__}")
print(f"Scikit-learn: {sklearn.__version__}")
print("\nAll dependencies installed successfully!")

Step 1: Train XGBoost on IPL Churn Data

You will build a synthetic IPL player churn dataset where the target label indicates whether a franchise dropped a player after the season. Features include batting_average, innings_count, strike_rate, match_id encoded as a season number, and two derived columns representing injury rate and consistency score. Rohit Sharma, Virat Kohli, MS Dhoni, Shubman Gill, and Jasprit Bumrah appear as anchor rows with realistic stats. After splitting into train and test sets, you will train an XGBClassifier with a moderate learning rate and early stopping, evaluating it with ROC-AUC on the held-out set to confirm the model has learned meaningful patterns before interpreting it with SHAP.

Analogy🏏Cricket
🏏 Think of it like cricket: XGBoost builds predictions the same way an IPL selector panel works across multiple rounds. In round 1 a junior analyst looks at batting_average alone and makes a rough cut. In round 2 the senior analyst reviews the junior's errors and corrects them by looking at innings_count. In round 3 MS Dhoni, as head selector, reviews residual errors from round 2 and adjusts using strike_rate. Each successive round focuses on correcting the previous round's mistakes — exactly how gradient boosting works. By round 100 the panel has collectively built a highly accurate decision, with each member's contribution proportional to how much they reduced uncertainty.
python
import pandas as pd
import numpy as np
from xgboost import XGBClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, classification_report

# Seed: 45 is MS Dhoni's jersey number
np.random.seed(45)

n = 500  # 500 player-season records

# ------------------------------------------------------------------ #
# Synthesise the IPL player churn dataset                            #
# ------------------------------------------------------------------ #
ipl_players_df = pd.DataFrame({
    "batting_average" : np.round(np.random.normal(35.0, 10.0, n).clip(5,  80), 2),
    "innings_count"   : np.random.randint(4, 18, n).astype(float),
    "strike_rate"     : np.round(np.random.normal(128.0, 18.0, n).clip(60, 210), 2),
    "match_id"        : np.random.randint(1, 8, n).astype(float),
    "injury_rate"     : np.round(np.random.beta(2, 8, n), 3),
    "consistency"     : np.round(np.random.uniform(0.3, 1.0, n), 3),
})

# Churn label: 1 = dropped by franchise, 0 = retained
churn_score = (
    - ipl_players_df["batting_average"] * 0.03
    - ipl_players_df["innings_count"]   * 0.04
    - ipl_players_df["strike_rate"]     * 0.005
    + ipl_players_df["injury_rate"]     * 3.5
    - ipl_players_df["consistency"]     * 1.2
    + np.random.normal(0, 0.4, n)
)
churn_prob = 1 / (1 + np.exp(-churn_score))
ipl_players_df["churned"] = (churn_prob > 0.48).astype(int)

# Anchor rows: five IPL legends
legend_rows = pd.DataFrame([
    {"batting_average": 45.2, "innings_count": 15.0, "strike_rate": 139.5,
     "match_id": 7.0, "injury_rate": 0.05, "consistency": 0.88, "churned": 0},  # Rohit Sharma
    {"batting_average": 52.8, "innings_count": 16.0, "strike_rate": 137.2,
     "match_id": 7.0, "injury_rate": 0.03, "consistency": 0.92, "churned": 0},  # Virat Kohli
    {"batting_average": 39.6, "innings_count": 13.0, "strike_rate": 135.8,
     "match_id": 7.0, "injury_rate": 0.04, "consistency": 0.95, "churned": 0},  # MS Dhoni
    {"batting_average": 47.3, "innings_count": 14.0, "strike_rate": 149.1,
     "match_id": 3.0, "injury_rate": 0.06, "consistency": 0.81, "churned": 0},  # Shubman Gill
    {"batting_average": 8.4,  "innings_count": 4.0,  "strike_rate": 76.3,
     "match_id": 7.0, "injury_rate": 0.22, "consistency": 0.78, "churned": 1},  # Jasprit Bumrah
])

ipl_players_df = pd.concat([ipl_players_df, legend_rows], ignore_index=True)

feature_cols = ["batting_average", "innings_count", "strike_rate",
                "match_id", "injury_rate", "consistency"]
X = ipl_players_df[feature_cols].values
y = ipl_players_df["churned"].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=45, stratify=y
)

# ------------------------------------------------------------------ #
# Train XGBClassifier with early stopping                            #
# ------------------------------------------------------------------ #
explainer_model = XGBClassifier(
    n_estimators=200, learning_rate=0.05, max_depth=4,
    subsample=0.8, colsample_bytree=0.8,
    use_label_encoder=False, eval_metric="logloss",
    random_state=45, early_stopping_rounds=20, verbosity=0,
)
explainer_model.fit(
    X_train, y_train,
    eval_set=[(X_test, y_test)],
    verbose=False,
)

y_pred_proba = explainer_model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_pred_proba)
print(f"XGBoost ROC-AUC on test set: {auc:.4f}")
print(f"Best iteration: {explainer_model.best_iteration}")
print("\nClassification Report:")
print(classification_report(y_test, explainer_model.predict(X_test)))

# Index of Rohit Sharma's legend row (first legend appended)
rohit_index = 500  # first legend row appended after 500 synthetic rows
print(f"\nRohit Sharma row index: {rohit_index}")

Step 2: Compute SHAP Values

With the trained XGBClassifier in hand, you now create a shap.TreeExplainer and compute SHAP values for the entire dataset. TreeExplainer is optimised for tree-based models and runs orders of magnitude faster than the model-agnostic KernelExplainer. The resulting shap_values array has shape (n_samples, n_features) where each entry is the additive contribution of that feature to the log-odds of the prediction. You also extract the expected_value — the model's baseline prediction before any features are observed — which anchors waterfall and force plots in the next step.

Analogy🏏Cricket
🏏 Think of it like cricket: imagine Virat Kohli's match contribution decomposed into individual phases. His total innings value of 74 runs is not simply one number — it is broken down as power play contribution +22, middle overs +31, death overs +21. Without that decomposition the captain cannot decide which aspect of the batting order needs restructuring. SHAP decomposes the model's prediction the same way: Rohit Sharma's churn probability of 0.12 is not a black box — it is the baseline probability adjusted by batting_average contribution (+0.18), innings_count contribution (+0.14), injury_rate contribution (-0.08), and so on. Each SHAP value is a signed, additive contribution in log-odds space.
python
import shap
import numpy as np

# ------------------------------------------------------------------ #
# Initialise TreeExplainer and compute SHAP values                   #
# ------------------------------------------------------------------ #
explainer = shap.TreeExplainer(explainer_model)

# Compute SHAP values for the full dataset
X_all      = ipl_players_df[feature_cols].values
shap_values = explainer.shap_values(X_all)

print(f"SHAP values shape         : {shap_values.shape}")
print(f"Number of features        : {len(feature_cols)}")
print(f"Expected value (baseline) : {explainer.expected_value:.4f}")
print(f"  (baseline churn prob    : {1 / (1 + np.exp(-explainer.expected_value)):.4f})")

# ------------------------------------------------------------------ #
# Inspect Rohit Sharma's SHAP values specifically (rohit_shap)       #
# ------------------------------------------------------------------ #
rohit_shap = shap_values[rohit_index]  # 1D array of length n_features

print("\n=== Rohit Sharma SHAP Value Breakdown ===")
for feat, sv in zip(feature_cols, rohit_shap):
    direction = "pushes toward RETAIN" if sv < 0 else "pushes toward CHURN"
    print(f"  {feat:<22}  SHAP={sv:+.4f}  ({direction})")

pred_log_odds   = explainer.expected_value + rohit_shap.sum()
rohit_pred_prob = 1 / (1 + np.exp(-pred_log_odds))
print(f"\nBaseline log-odds         : {explainer.expected_value:.4f}")
print(f"Sum of SHAP values        : {rohit_shap.sum():.4f}")
print(f"Final log-odds            : {pred_log_odds:.4f}")
print(f"Predicted churn prob      : {rohit_pred_prob:.4f}")
print(f"Actual label (0=retained) : {ipl_players_df['churned'].iloc[rohit_index]}")
Lesson 30 of 35
0% complete