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.
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.
Setup
# 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.
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.
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]}")