Credit Card Fraud Detection Engine
Build an XGBoost fraud classifier on 1.3M transactions with SHAP explainability.
Introduction
30 Second Summary
Every time you tap your credit card, a machine learning model decides in milliseconds whether to approve or block the transaction. Fraud costs billions annually, and the models that catch it need to be fast, accurate, and explainable.
In this project, you will build a fraud detection engine that processes over 1.3 million credit card transactions, engineers meaningful features from raw messy data, and classifies each transaction as fraudulent or legitimate using XGBoost.
What You'll Build
A Jupyter Notebook that tells a data story: you start with a naive model that reports 99% accuracy but misses most fraud, then build a proper solution that catches criminals and explains exactly why each transaction was flagged.
By the end of this project, you'll have:
- A working fraud classifier that catches 85%+ of fraudulent transactions, with a confusion matrix showing how many frauds you catch vs how many legitimate transactions you accidentally block.
- SHAP waterfall plots explaining exactly which features made a specific transaction suspicious, making every prediction auditable.
- A side-by-side comparison proving why accuracy is a terrible metric for fraud detection, and how recall and ROC-AUC tell the real story.
- Secret Mission: Build a production-style risk scoring function with High/Medium/Low business thresholds that mimics how real fraud systems work.
Are there any prerequisites?
You should be comfortable with basic Python (variables, loops, functions) and have a general familiarity with machine learning concepts. No prior experience with fraud detection or XGBoost is needed.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
You're going to build a fraud detection engine that processes 1.3 million credit card transactions, proves why naive models fail on imbalanced data, and produces portfolio-ready explainability visualizations.
Set Up Libraries and Load Data
Before you can detect fraud, you need the right tools loaded into your workspace. Machine learning projects depend on specialized libraries for training models, explaining predictions, and handling large datasets.
In this step, you'll install six Python packages, import them into a Jupyter notebook, and pull a 1.3 million row credit card transaction dataset from Hugging Face.
In this step, get ready to:
- Install the ML packages your project needs.
- Import all libraries into your notebook.
- Load and preview the credit card fraud dataset.
Install the required packages
Your fraud detection engine uses XGBoost for classification, SHAP for explainability, and scikit-learn for evaluation metrics.
- Install all six packages at once by running this command in a new Jupyter cell:
pip install xgboost shap datasets pandas scikit-learn matplotlib
What does this command do?
This installs six packages in one go. xgboost is the gradient boosting model. shap explains predictions. datasets pulls data from Hugging Face. pandas handles DataFrames. scikit-learn provides metrics. matplotlib draws charts.
You'll see installation progress messages as each package downloads. This may take a minute or two depending on your internet connection.
- Confirm the packages installed correctly by running this in a new cell:
import xgboost; import shap; print("Ready")
You should see Ready printed with no errors.
Seeing an import error?
If you see ModuleNotFoundError, the pip install may not have completed successfully. Scroll up in your output to look for red error text during installation.
Make sure you're running pip install in the same Python environment that Jupyter is using. If you're in a virtual environment, confirm it's activated.
help me fix this import error after running pip install
Import all libraries
Now that the packages are installed, you'll create the first real cell of your notebook. This cell imports everything the project needs across all future steps.
- Delete the two test cells you just ran (the pip install and the import check).
- Create a new cell at the top of your notebook and paste the following code:
# Cell 1: Setup
# !pip install xgboost shap datasets pandas scikit-learn matplotlib
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datasets import load_dataset
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix, RocCurveDisplay
from sklearn.preprocessing import LabelEncoder
import xgboost as xgb
import shap
What does this code do?
- pandas and numpy handle data manipulation and math operations.
- matplotlib draws visualizations like ROC curves and bar charts.
- load_dataset pulls datasets directly from Hugging Face with one function call.
- The sklearn imports provide model training, evaluation metrics, and data splitting.
- xgb is the XGBoost classifier, and shap explains each prediction.
- Run the cell.
If no errors appear, all imports succeeded. The cell produces no output on success.
Seeing a version conflict?
If you see a warning about package versions, check that your Python version is 3.10 or higher. Run python --version in your terminal to check.
help me resolve this version conflict with my imports
Load the dataset from Hugging Face
The dataset you'll use contains 1.3 million credit card transactions generated by the Sparkov simulator. It has real-world-shaped features like transaction amounts, merchant locations, timestamps, and a binary fraud label.
- Create a new cell below your imports and paste this code:
# Cell 2: Load Data
print("Loading dataset from Hugging Face...")
ds = load_dataset("NeerajCodz/creditCardFraudDetection", split="train")
df = ds.to_pandas()
print(f"Dataset shape: {df.shape}")
df.head()
What does this code do?
- load_dataset downloads the training split directly from Hugging Face into a Dataset object.
- .to_pandas() converts it into a familiar pandas DataFrame for easy manipulation.
- df.shape prints the row and column count so you can confirm the full dataset loaded.
- df.head() displays the first five rows so you can see what the raw data looks like.
Before you run this cell, take a guess: how many columns do you think a credit card transaction dataset needs?
- Run the cell.
You should see Dataset shape: followed by a tuple showing 1.3 million+ rows and 22 columns. Below that, a table previews the first five transactions with columns like amt, category, lat, long, and is_fraud.
Download taking too long or failing?
The dataset is several hundred MB. On a slow connection, the download can take a few minutes. If it times out, restart the cell and try again.
If you see a connection error, check that your network allows access to huggingface.co.
help me troubleshoot my Hugging Face dataset download
✔️ Awesome, I've got everything!
Great. Make sure both cells are saved in your notebook before moving on.
ⓧ I'd like to double check the full code
Here is everything your notebook should contain so far:
# Cell 1: Setup
# !pip install xgboost shap datasets pandas scikit-learn matplotlib
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datasets import load_dataset
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix, RocCurveDisplay
from sklearn.preprocessing import LabelEncoder
import xgboost as xgb
import shap
# Cell 2: Load Data
print("Loading dataset from Hugging Face...")
ds = load_dataset("NeerajCodz/creditCardFraudDetection", split="train")
df = ds.to_pandas()
print(f"Dataset shape: {df.shape}")
df.head()
Your workspace is ready and loaded with over a million transactions. Next up, you'll explore this data, discover how wildly imbalanced it is, and train a naive model that looks great on paper but fails spectacularly at catching fraud.
Explore Data and Train a Naive Baseline
Your notebook is loaded with 1.3 million transactions. Now it's time to ask the critical first question: how much of this data is actually fraud?
In this step, you'll train a basic Logistic Regression model and discover a trap that catches most beginners. A model can report 99% accuracy while being completely useless at its actual job.
In this step, get ready to:
- Check the class distribution to see how imbalanced the dataset is.
- Train a naive Logistic Regression on raw numeric features.
- Evaluate the model and discover why accuracy is a terrible metric for fraud detection.
Check the class distribution
Before training any model, you need to understand what the data looks like. In fraud detection, the split between legitimate and fraudulent transactions is rarely 50/50.
- Create a new cell below Cell 2 and add this code to check the class balance and visualize it:
# Cell 3: Explore Class Distribution
print("Class distribution:")
print(df['is_fraud'].value_counts())
print(f"\nFraud rate: {df['is_fraud'].mean()*100:.2f}%")
fig, ax = plt.subplots(figsize=(6, 4))
df['is_fraud'].value_counts().plot(kind='bar', ax=ax, color=['steelblue', 'coral'])
ax.set_title('Transaction Class Distribution')
ax.set_xticklabels(['Legitimate', 'Fraud'], rotation=0)
ax.set_ylabel('Count')
plt.tight_layout()
plt.show()
What does this code do?
The first three print statements show the raw count of legitimate vs fraudulent transactions and the percentage of fraud in the dataset.
The bar chart makes the imbalance visual. You'll see a towering blue bar (legitimate) dwarfing a tiny coral bar (fraud). This is the class imbalance problem that makes fraud detection tricky.
- Run Cell 3.
You should see a fraud rate of approximately 5.7%. The bar chart shows a massive gap between legitimate and fraudulent transactions.
Why does 5.7% fraud matter?
If only ~6% of transactions are fraud, a model that blindly guesses "legitimate" for every single transaction would be right 94% of the time. That's the accuracy trap. A useless model can still post impressive-looking accuracy numbers.
Not seeing the chart?
Make sure you ran Cell 1 (imports) and Cell 2 (data loading) first. The plt and df variables must exist in memory before Cell 3 can use them.
Help me debug my class distribution chart.
Train a naive Logistic Regression
Now you'll build the simplest possible fraud detector. Pick a handful of numeric columns, split the data, and throw a Logistic Regression at it.
- Create a new Cell 4 and add this code to select features, split the data, and train the model:
# Cell 4: Naive Baseline - Logistic Regression
naive_features = ['amt', 'lat', 'long', 'city_pop', 'merch_lat', 'merch_long', 'unix_time']
X_naive = df[naive_features]
y = df['is_fraud']
X_train_n, X_test_n, y_train_n, y_test_n = train_test_split(
X_naive, y, test_size=0.2, random_state=42, stratify=y
)
lr = LogisticRegression(max_iter=1000, random_state=42)
lr.fit(X_train_n, y_train_n)
y_pred_lr = lr.predict(X_test_n)
What does this code do?
- naive_features picks only the raw numeric columns. No cleaning, no engineering. Just raw numbers the model might find patterns in.
- train_test_split with stratify=y ensures both the training and test sets keep the same ~5.7% fraud ratio. Without stratification, the test set might end up with almost no fraud samples.
- LogisticRegression is a simple binary classification model. It draws a line between "fraud" and "not fraud" based on the features.
Evaluate the model and discover the accuracy trap
The model is trained. Now add the evaluation code that prints accuracy, a full classification report, and counts the missed frauds.
- Add this code below your training code in the same Cell 4:
print("=== Naive Logistic Regression ===")
print(f"Accuracy: {(y_pred_lr == y_test_n).mean()*100:.2f}%")
print("\nClassification Report:")
print(classification_report(y_test_n, y_pred_lr, target_names=['Legitimate', 'Fraud']))
print("Confusion Matrix:")
print(confusion_matrix(y_test_n, y_pred_lr))
missed_frauds = confusion_matrix(y_test_n, y_pred_lr)[1][0]
total_frauds = y_test_n.sum()
print(f"\nFrauds missed: {missed_frauds} out of {total_frauds} ({missed_frauds/total_frauds*100:.1f}%)")
What does this code do?
- The first print shows overall accuracy. This is the number that looks great but lies.
- classification_report breaks performance down per class. The recall column for "Fraud" tells you what percentage of actual frauds the model caught.
- The confusion matrix shows raw counts: true positives, false positives, true negatives, and false negatives.
- missed_frauds counts how many fraudulent transactions the model let slip through undetected.
Before you run this cell, think: the dataset is ~94% legitimate transactions. If the model learns to mostly guess "legitimate", what accuracy would it report? And would that make it useful for catching fraud?
- Run Cell 4.
Look at the output. Accuracy reports ~99%. Looks incredible. Now look at the recall for the Fraud class. It's below 65%.
That means more than a third of all fraudulent transactions sailed right through. The model looks smart but lets criminals walk.
Why is this the expected result?
This is the designed failure. High accuracy on imbalanced data is meaningless. The model learned it could score 99% by mostly guessing "legitimate" since that's what 94% of the data looks like.
In the real world, the missed frauds are the ones that cost banks millions. The next steps will fix this with proper feature engineering and a model that understands the cost of missing a fraud.
Seeing a convergence warning?
If you see a warning about the solver not converging, your code is still working correctly. The max_iter=1000 parameter should prevent this in most cases, but the warning does not affect the results for this baseline.
Help me fix my Logistic Regression training.
✔️ Awesome, I've got everything!
Great. Make sure you've saved your notebook.
ⓧ I'd like to double check the full code
Scroll through your notebook and confirm these cells are present in order:
- Cell 1: Setup (imports from Step 1).
- Cell 2: Load Data (dataset loading from Step 1).
- Cell 3: Explore Class Distribution (value counts, fraud rate print, and bar chart).
- Cell 4: Naive Baseline - Logistic Regression (feature selection, train/test split, model training, accuracy print, classification report, confusion matrix, and missed frauds count).
You've proved that accuracy is a lie on imbalanced data. The naive model lets over a third of fraudsters escape. Next up, you'll engineer real fraud signals from the raw data so the model has something meaningful to learn from.
Engineer Meaningful Features
In Step 2, you exposed the ugly truth: a model trained on raw numeric columns reports 99% accuracy while missing most fraud. The columns lat, long, and unix_time mean nothing to a model in their raw form.
In this step, you will transform the raw data into fraud signals a model can actually learn from. Extracting hour-of-day, computing the physical distance between a customer and merchant, and encoding transaction categories gives the model meaningful patterns to detect.
In this step, get ready to:
- Extract time-based features (hour, day of week, age) from raw date strings.
- Calculate the geographic distance between customer and merchant locations.
- Encode the transaction category column into a numeric format the model can use.
Extract time-based features
Fraud patterns are often time-dependent. A $500 purchase at 3am behaves differently than the same purchase at 3pm. The raw trans_date_trans_time column is a string. You need to parse it into a datetime object, then extract the hour and day of week as standalone numbers.
- Create a new cell in your notebook and add the following code to parse dates and extract time features:
# Cell 5: Feature Engineering
df['trans_date_trans_time'] = pd.to_datetime(df['trans_date_trans_time'])
df['hour'] = df['trans_date_trans_time'].dt.hour
df['day_of_week'] = df['trans_date_trans_time'].dt.dayofweek
df['dob'] = pd.to_datetime(df['dob'])
df['age'] = (df['trans_date_trans_time'] - df['dob']).dt.days // 365
What does this code do?
This chunk converts raw date strings into useful numeric signals:
- pd.to_datetime() converts the text timestamps into proper datetime objects that pandas can do math on.
- .dt.hour extracts just the hour (0-23). Fraud spikes at unusual hours.
- .dt.dayofweek extracts the day (0 = Monday, 6 = Sunday). Spending patterns shift on weekends.
- The age calculation subtracts the date of birth from the transaction date to get the customer's age in years.
- Run the cell by pressing Shift+Enter.
- Verify the new columns exist by typing this in a new cell below and running it:
df[['hour', 'day_of_week', 'age']].head()
You should see a table with three columns showing integers. Hours range from 0-23, day_of_week from 0-6, and age should be realistic adult ages.
Seeing a TypeError?
If you get an error about subtracting dates, make sure both the transaction time and dob are converted with pd.to_datetime() before the subtraction line.
Help me debug my datetime feature extraction code.
Calculate distance to merchant
If a customer lives in New York but a transaction pings a merchant in Alaska, that is suspicious. The dataset gives you the customer's coordinates (lat, long) and the merchant's coordinates (merch_lat, merch_long). You need the actual distance in kilometers between them.
What is the Haversine formula?
The Haversine formula calculates the shortest distance between two points on a sphere (the Earth) given their latitude and longitude. It accounts for the curvature of the planet, unlike a simple Euclidean distance which would be inaccurate over large distances.
- In the same Cell 5 (below your time feature code), add the Haversine function and apply it:
def haversine_distance(lat1, lon1, lat2, lon2):
R = 6371 # Earth radius in km
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
c = 2 * np.arcsin(np.sqrt(a))
return R * c
df['distance_to_merchant'] = haversine_distance(
df['lat'], df['long'], df['merch_lat'], df['merch_long']
)
What does this code do?
- The function converts lat/long from degrees to radians (required for trig functions), then applies the Haversine formula to get the great-circle distance.
- R = 6371 is the Earth's radius in kilometers. This scales the angular result into a real-world distance.
- The last line applies the function to every row, creating a new distance_to_merchant column measured in km.
- Run Cell 5 again (Shift+Enter).
- Check the distance values by typing this in a new cell below and running it:
df['distance_to_merchant'].describe()
You should see statistics with a mean around 30-100 km and a max of several hundred km. These are realistic distances between cardholders and merchants.
Seeing all zeros or NaN?
Check that you passed the columns in the correct order to the function. The first pair should be the customer coordinates and the second pair should be the merchant coordinates.
Help me debug my Haversine distance calculation.
Encode the category column
The category column contains text values like "grocery_pos" and "shopping_net". Models only understand numbers, so you need to convert each category into a numeric code.
- At the bottom of Cell 5, add the category encoding and verification print:
le = LabelEncoder()
df['category_encoded'] = le.fit_transform(df['category'])
print(df[['hour', 'day_of_week', 'distance_to_merchant', 'category_encoded']].describe())
What does this code do?
- LabelEncoder() assigns each unique category a number (0, 1, 2, ...). For example, "grocery_pos" might become 5 and "shopping_net" might become 12.
- The final print statement shows summary statistics for all four engineered features at once. This confirms everything worked.
Before you run the cell, what kind of values do you expect to see for each feature? Think about the ranges. Hours should be 0-23. Day of week should be 0-6. Distance should be positive kilometers. Category codes should be small integers.
- Run Cell 5 one final time (Shift+Enter).
You should see a statistics table showing sensible distributions for all four engineered features. Hours between 0-23, days between 0-6, distances in the tens to hundreds of km, and category codes as small integers.
Seeing a KeyError for one of the columns?
Make sure all three chunks of code (time features, haversine, and encoding) are in the same cell and you ran the full cell. If you split them across cells, run them in order from top to bottom so each column exists before the final print.
Help me fix my feature engineering cell.
Why not drop the irrelevant columns now?
The dataset still has columns like first name, street address, and credit card number that are useless for modeling. You do not need to explicitly drop them. In the next step, you will select only the engineered features you want, which achieves the same result without permanently modifying the DataFrame.
✔️ Awesome, I've got everything!
Great. Make sure Cell 5 is saved in your notebook.
ⓧ I'd like to double check the full code
Here is the complete Cell 5. Confirm your code matches this exactly:
# Cell 5: Feature Engineering
df['trans_date_trans_time'] = pd.to_datetime(df['trans_date_trans_time'])
df['hour'] = df['trans_date_trans_time'].dt.hour
df['day_of_week'] = df['trans_date_trans_time'].dt.dayofweek
df['dob'] = pd.to_datetime(df['dob'])
df['age'] = (df['trans_date_trans_time'] - df['dob']).dt.days // 365
def haversine_distance(lat1, lon1, lat2, lon2):
R = 6371 # Earth radius in km
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
c = 2 * np.arcsin(np.sqrt(a))
return R * c
df['distance_to_merchant'] = haversine_distance(
df['lat'], df['long'], df['merch_lat'], df['merch_long']
)
le = LabelEncoder()
df['category_encoded'] = le.fit_transform(df['category'])
print(df[['hour', 'day_of_week', 'distance_to_merchant', 'category_encoded']].describe())
Your DataFrame now speaks the language a model can learn from. Raw timestamps became hour-of-day patterns, coordinates became meaningful distances, and text categories became numbers. Next up, you will feed these engineered features into XGBoost and watch it crush the naive baseline.
Train XGBoost with Imbalance Handling
In the last step, you engineered features that give the model real fraud signals to learn from. Now it is time to train a model that actually respects the class imbalance problem you exposed in Step 2.
XGBoost's scale_pos_weight parameter tells the model that missing a fraud case is far more costly than a false alarm. This single parameter replaces complex SMOTE pipelines and fixes the recall problem from your naive baseline.
In this step, get ready to:
- Calculate the class imbalance ratio and prepare the training data with engineered features.
- Train an XGBoost classifier that penalizes missed fraud cases.
- Plot a ROC curve comparison proving XGBoost crushes the naive baseline.
Calculate scale_pos_weight and split the data
The scale_pos_weight parameter is the ratio of legitimate transactions to fraudulent ones. It tells XGBoost how much more important it is to correctly classify the minority (fraud) class.
- Create a new cell in your notebook (Cell 6) and define the engineered feature list, perform the train/test split, and calculate the imbalance ratio by pasting this code:
# Cell 6: XGBoost with scale_pos_weight
engineered_features = [
'amt', 'hour', 'day_of_week', 'age', 'city_pop',
'distance_to_merchant', 'category_encoded'
]
X = df[engineered_features]
y = df['is_fraud']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
fraud_ratio = (y_train == 0).sum() / (y_train == 1).sum()
print(f"scale_pos_weight: {fraud_ratio:.1f}")
What does this code do?
engineered_features lists the seven features you built in the previous step. These replace the raw lat/long and unix_time columns that gave the naive model nothing useful to learn from.
The stratify=y argument ensures the train/test split maintains the same fraud rate in both sets. Without it, you could end up with almost no fraud examples in your test set.
fraud_ratio divides the count of legitimate transactions by the count of fraudulent ones. This number becomes the penalty multiplier for missed fraud cases.
- Run the cell by pressing Shift+Enter.
You should see a scale_pos_weight value around 16-17, confirming that legitimate transactions outnumber fraudulent ones by roughly that factor.
Seeing a NameError?
If you get an error about df or a missing column like hour, make sure you ran all previous cells (Cells 1 through 5) first. The feature engineering cell must complete before this cell can reference the new columns.
Why is my scale_pos_weight calculation failing?
Train the XGBClassifier and evaluate
Why scale_pos_weight instead of SMOTE?
SMOTE generates synthetic minority samples, adding complexity and computation. XGBoost's scale_pos_weight achieves the same goal by penalizing misclassification of the minority class during training. One parameter, no synthetic data pipeline, faster training.
- In the same cell (Cell 6), paste this code below your print statement to train the model and generate predictions:
model = xgb.XGBClassifier(
n_estimators=100,
scale_pos_weight=fraud_ratio,
eval_metric='aucpr',
random_state=42,
use_label_encoder=False
)
model.fit(X_train, y_train)
y_pred_xgb = model.predict(X_test)
print("\n=== XGBoost with scale_pos_weight ===")
print(classification_report(y_test, y_pred_xgb, target_names=['Legitimate', 'Fraud']))
print("Confusion Matrix:")
print(confusion_matrix(y_test, y_pred_xgb))
What does this code do?
- n_estimators=100 trains 100 sequential decision trees, each one correcting the errors of the previous ones.
- scale_pos_weight=fraud_ratio makes misclassifying a fraud case ~16x more costly than misclassifying a legitimate one.
- eval_metric='aucpr' evaluates using area under the precision-recall curve, which is more meaningful than accuracy for imbalanced data.
Before you run this cell, think back to the naive baseline. The Logistic Regression reported high accuracy but terrible fraud recall. What do you expect will change this time?
- Run the cell by pressing Shift+Enter.
The classification report now shows fraud recall at 75% or higher. The model catches the majority of fraudulent transactions instead of letting them slip through. Compare this to the naive model that missed over 35% of fraud cases.
Model training seems stuck or slow?
Training on 1M+ rows can take 15-30 seconds depending on your machine. If the cell shows [*] for more than a minute, your machine may be low on memory. Restart the kernel and re-run all cells from the top.
Why is my XGBoost model taking so long to train?
Plot the ROC curve comparison
A ROC curve plots true positive rate against false positive rate at every threshold. A perfect model hugs the top-left corner. A random guess follows the diagonal.
- Create a new cell (Cell 7) and plot both models on the same axes by pasting this code:
# Cell 7: ROC Curve Comparison
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_estimator(lr, X_test_n, y_test_n, ax=ax, name="Logistic Regression")
RocCurveDisplay.from_estimator(model, X_test, y_test, ax=ax, name="XGBoost")
ax.set_title('ROC Curve Comparison')
plt.tight_layout()
plt.show()
What does this code do?
RocCurveDisplay.from_estimator computes the ROC curve directly from a trained model and test data. Plotting both on the same ax overlays the two curves so you can visually compare performance at every decision threshold.
What shape do you expect the XGBoost curve to take compared to the Logistic Regression line?
- Run the cell by pressing Shift+Enter.
The XGBoost curve hugs the top-left corner with an AUC exceeding 0.97. The Logistic Regression curve sits noticeably lower. This is visual proof that your feature engineering plus imbalance handling transformed a useless model into a fraud-catching machine.
Only seeing one curve on the plot?
Make sure both RocCurveDisplay.from_estimator calls share the same ax=ax argument. If you see a NameError for lr, re-run Cell 4 where the Logistic Regression was trained.
Why is my ROC curve plot not showing both models?
✔️ Awesome, I've got everything!
Great. Double check you have both Cell 6 and Cell 7 saved in your notebook.
ⓧ I'd like to double check the full code
Scroll up through your notebook and confirm these cells are present in order:
- Cell 6: Defines engineered_features, performs the train/test split, calculates fraud_ratio, trains the XGBClassifier, and prints the classification report and confusion matrix.
- Cell 7: Plots the ROC curve comparison with both the Logistic Regression and XGBoost models on the same axes.
Your fraud detection model now catches 75%+ of fraudulent transactions with a ROC-AUC above 0.97. Next up, you will make every prediction explainable using SHAP so compliance teams and customers can see exactly why a transaction was flagged.
Explain Predictions with SHAP
Your XGBoost model catches fraud far better than the naive baseline. But catching fraud is only half the job.
Compliance teams and customers need to know WHY a transaction was flagged. SHAP values decompose each prediction into per-feature contributions, making your model auditable. In this step, you will make every fraud flag explainable.
In this step, get ready to:
- Compute SHAP values for a sample of test transactions using TreeExplainer.
- Visualize global feature importance with a beeswarm plot.
- Explain a single flagged fraud transaction with a waterfall plot.
Create the SHAP explainer and visualize global importance
SHAP's TreeExplainer is purpose-built for tree-based models like XGBoost. It computes exact SHAP values quickly by exploiting the tree structure, rather than using slow approximation methods.
Computing SHAP values on the full test set would take a long time, so you will sample 500 transactions for explanation.
- Create a new cell below Cell 7 and add the following code to compute SHAP values and plot global feature importance:
# Cell 8: SHAP Explainability
explainer = shap.TreeExplainer(model)
X_sample = X_test.sample(500, random_state=42)
shap_values = explainer(X_sample)
# Global feature importance
shap.summary_plot(shap_values, X_sample)
What does this code do?
shap.TreeExplainer(model) wraps your trained XGBoost model in a SHAP-compatible explainer that can decompose any prediction into feature contributions.
X_test.sample(500, random_state=42) draws a reproducible random subset of 500 transactions. This keeps SHAP computation fast while still being representative.
shap.summary_plot(shap_values, X_sample) produces a beeswarm plot. Each dot is one transaction. Features are ranked top-to-bottom by importance, and dot colour shows whether the feature value was high (red) or low (blue).
- Run the cell.
You should see a beeswarm plot with features listed on the y-axis and dots spread horizontally by their SHAP value. The feature at the top is the most important one for driving predictions across all 500 transactions.
Plot not appearing?
- Make sure you ran Cell 6 first so that model and X_test exist in memory.
- If you see an import error for shap, return to Cell 1 and re-run the import block.
- help me debug my SHAP summary plot
Why SHAP over LIME?
SHAP has a native TreeExplainer that computes exact Shapley values for tree models. LIME uses local approximation, which introduces noise. For tree-based models, SHAP is strictly more accurate and faster.
Explain a single fraudulent transaction
The beeswarm shows global importance across many transactions. Now you will zoom in on one specific fraud case and see exactly which features pushed the model toward its "fraud" decision.
- Create a new cell below Cell 8 and add the following code to find a correctly-flagged fraud transaction and explain it:
# Cell 9: Individual Fraud Explanation
fraud_indices = X_test[y_test == 1].index
correctly_flagged = X_test.loc[fraud_indices][model.predict(X_test.loc[fraud_indices]) == 1]
if len(correctly_flagged) > 0:
single_fraud = correctly_flagged.iloc[[0]]
single_shap = explainer(single_fraud)
shap.waterfall_plot(single_shap[0])
What does this code do?
fraud_indices grabs the row indices of all actual fraud transactions in the test set. correctly_flagged filters those down to only the ones the model correctly predicted as fraud.
shap.waterfall_plot(single_shap[0]) renders a waterfall chart for that single transaction. Each bar shows one feature's contribution. Bars pushing right increase the fraud score. Bars pushing left decrease it.
Before you run this cell, think about which feature you expect to push the prediction toward fraud the most. Is it the transaction amount? The hour of day? The distance to the merchant?
- Run the cell.
You should see a waterfall plot where each bar represents how much one feature pushed the prediction toward or away from fraud. The bars stack to produce the final fraud probability for this specific transaction.
Seeing an empty output?
- If the if block did not execute, it means correctly_flagged is empty. Confirm you ran Cell 6 so that model is trained on the engineered features.
- Check that X_test and y_test are from the same train/test split in Cell 6. Re-running Cell 6 resets them.
- help me debug my SHAP waterfall plot
How do I read this waterfall plot?
The plot starts from a base value (the model's average prediction across all transactions). Each bar shows how one feature moves the prediction up or down from that baseline.
For example, a large red bar on amt means a high transaction amount strongly pushed this prediction toward fraud. A blue bar on hour might mean the transaction happened during normal business hours, slightly reducing suspicion.
✔️ Awesome, I've got everything!
Great. Make sure you have saved your notebook with all cells through Cell 9.
ⓧ I'd like to double check the full code
Your notebook should have 9 cells in order. Scroll through and confirm each one is present:
- Cell 1: Setup (imports for pandas, numpy, matplotlib, datasets, sklearn, xgboost, shap).
- Cell 2: Load Data (load from Hugging Face, convert to pandas).
- Cell 3: Explore Class Distribution (value counts and bar chart).
- Cell 4: Naive Baseline (Logistic Regression on raw features).
- Cell 5: Feature Engineering (hour, day_of_week, age, distance, category encoding).
- Cell 6: XGBoost with scale_pos_weight.
- Cell 7: ROC Curve Comparison.
- Cell 8: SHAP Explainability (TreeExplainer, 500 sample, beeswarm plot).
- Cell 9: Individual Fraud Explanation (correctly_flagged, waterfall plot).
Your fraud detection model is now fully explainable. Every prediction can be broken down into exactly which features drove the decision. Next up, you will take on the Secret Mission and build a production-style risk scoring function that combines predictions with explanations into actionable business tiers.
Secret mission
Production Risk Scoring
Real fraud systems don't just say "fraud" or "not fraud". They assign risk tiers with different business actions. Can you build a production-style risk scoring function that returns HIGH, MEDIUM, or LOW risk with per-transaction explanations?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources available, pause them to come back later, or delete them entirely.
Resources you used:
- fraud_detection.ipynb notebook file.
- Python packages installed via pip: xgboost, shap, datasets, pandas, scikit-learn, matplotlib.
- Hugging Face datasets cache stored at ~/.cache/huggingface/ (several hundred MB of downloaded data).
Keep everything running
No action needed. Choose this if you plan to continue experimenting with your fraud detection model or want to revisit the notebook later.
- Your notebook and all pip packages remain available for future use.
- The Hugging Face cache stays on disk so reloading the dataset is instant next time.
Pause - I'll come back to this later
Shut down your Jupyter Notebook server to free up memory. Your files and packages stay on disk so you can resume quickly.
- In your terminal, press Ctrl+C to stop the Jupyter Notebook server.
- Your fraud_detection.ipynb file, installed packages, and cached dataset all remain on disk for next time.
Delete - I don't want to use this again
Remove all project resources and free up disk space completely.
- Stop the Jupyter Notebook server by pressing Ctrl+C in the terminal.
- Delete the notebook file by running:
macOS
rm fraud_detection.ipynb
Windows
del fraud_detection.ipynb
- Uninstall the project-specific packages by running:
pip uninstall xgboost shap datasets -y
What about pandas, scikit-learn, and matplotlib?
These are common data science libraries you will likely use in other projects. Only uninstall them if you are sure you do not need them elsewhere.
- Clear the Hugging Face datasets cache by running:
macOS
rm -rf ~/.cache/huggingface/datasets
rm -rf ~/.cache/huggingface/hub
Windows
rmdir /s /q %USERPROFILE%\.cache\huggingface\datasets
rmdir /s /q %USERPROFILE%\.cache\huggingface\hub
Nice Work!
Nice Work!
You just built a fraud detection engine that processes 1.3 million credit card transactions and catches fraudsters that a naive model completely misses. Even better, your model can explain exactly why it flagged each transaction.
Here's what you accomplished:
- Built a fraud detection model on 1.3 million messy transactions with real feature engineering including datetime parsing, Haversine geospatial distance, and categorical encoding.
- Proved that accuracy is meaningless on imbalanced data and that XGBoost with scale_pos_weight outperforms naive approaches by actually catching fraud.
- Added SHAP explainability to make every prediction auditable and interpretable with per-feature waterfall plots.
- Secret Mission: Created a production-style risk scoring function with HIGH/MEDIUM/LOW business thresholds and per-transaction explanations.
Ready to quiz yourself?