Build a Fraud Detection API on AWS
Train a fraud detection model and deploy it as a live API on AWS Lambda.
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. Behind the scenes, these models face a brutal challenge: in a typical dataset of hundreds of thousands of transactions, fewer than 0.2% are actually fraudulent.
In this project, you will train a fraud detection model on real-world transaction data, handle extreme class imbalance, and deploy it as a live API on AWS Lambda that anyone can call over HTTPS.
What You'll Build
A complete fraud detection system that goes from raw transaction data to a live, serverless prediction API on AWS.
By the end of this project, you'll have:
- A trained fraud detection model that handles extreme class imbalance (0.17% fraud rate) using SMOTE oversampling and is evaluated with production-grade metrics.
- A live HTTPS endpoint on AWS Lambda that accepts transaction features and returns fraud predictions with confidence scores and risk tiers.
- A deployable project structure demonstrating the notebook-to-production pattern that hiring managers look for in ML engineering candidates.
- Secret Mission: Add SHAP explainability so your API explains which features drove each fraud prediction.
Do I need machine learning experience?
Basic Python knowledge and some familiarity with scikit-learn is helpful. You do not need deep ML expertise.
The project walks you through every concept, from handling class imbalance to deploying on AWS. You will also need a free AWS account and a free Kaggle account to download the dataset.
Explore the Data and Handle Class Imbalance
Your environment is set up and your dataset is waiting. Now it's time to look at the data and understand the core challenge of fraud detection.
In real-world credit card data, fraud is extremely rare. Only 0.17% of transactions in this dataset are fraudulent. A model that simply predicts "not fraud" every single time would achieve 99.83% accuracy, but it would catch zero actual fraud. This step tackles that imbalance head-on.
In this step, get ready to:
- Load the dataset and explore its class distribution.
- Preprocess features by dropping unnecessary columns and scaling.
- Balance the training data using SMOTE oversampling.
Load and explore the dataset
Before you can train a model, you need to understand what the data looks like. Loading the CSV and printing the class distribution reveals just how extreme the imbalance is.
- Open train.py in VS Code and add the following imports and data loading code:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from imblearn.over_sampling import SMOTE
import joblib
# Load data
df = pd.read_csv("creditcard.csv")
print(f"Dataset shape: {df.shape}")
print(f"Class distribution:\n{df['Class'].value_counts()}")
print(f"Fraud percentage: {df['Class'].mean() * 100:.3f}%")
What does this code do?
The imports bring in everything you will need across this step and the next. Some (like RandomForestClassifier and joblib) are used in Step 3 but are imported here to keep the top of the file clean.
The three print statements give you an immediate picture of the dataset: 284,807 rows, 31 columns, and only 492 fraud cases (0.173%).
- Save train.py and run it in the terminal:
python train.py
- You should see the dataset shape (284807, 31), a class distribution showing 284,315 legitimate (0) vs 492 fraud (1), and a fraud percentage of 0.173%.
Seeing a FileNotFoundError?
Make sure creditcard.csv is in the same folder as train.py (inside fraud-detection-api). Also confirm your terminal is in the fraud-detection-api directory by running cd fraud-detection-api if needed.
Ask for help: Why is Python saying it can't find creditcard.csv even though I downloaded it?
Now you need to clean up the features before splitting the data. The Time column records seconds elapsed since the first transaction in the dataset. It does not help predict fraud. The Amount column needs scaling because its range is much larger than the PCA-transformed V1-V28 features.
- Add the following preprocessing code below your print statements in train.py:
# Drop Time column, scale Amount
df = df.drop("Time", axis=1)
scaler = StandardScaler()
df["Amount"] = scaler.fit_transform(df[["Amount"]])
What does this code do?
Dropping Time removes a column that adds noise without predictive value.
The StandardScaler transforms Amount so its values have a mean of 0 and a standard deviation of 1. This puts it on the same scale as the V1-V28 features, which are already PCA-normalized.
- Save train.py.
- You won't see new output yet. The next section splits this preprocessed data for training.
Split and resample
With the data cleaned, you need to split it into training and test sets. The critical rule: you will balance the training data with SMOTE, but the test set must stay imbalanced to reflect the real world.
- Add the train/test split code below your preprocessing in train.py:
# Split features and target
X = df.drop("Class", axis=1)
y = df["Class"]
# Train/test split with stratification
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"\nBefore SMOTE:")
print(f"Training set class distribution:\n{y_train.value_counts()}")
What does this code do?
The stratify=y parameter ensures the 80/20 split preserves the original class ratio in both sets. Without stratification, the tiny fraud class might end up unevenly distributed between train and test.
The print statement shows the training set before SMOTE. You will see roughly 393 fraud samples out of ~227,000 total training rows.
- Save train.py and run it:
python train.py
- You should see a new "Before SMOTE" section showing the training set has around 227,451 legitimate (0) and 393 fraud (1) samples.
Seeing a NameError for train_test_split?
Confirm that the import line from sklearn.model_selection import train_test_split is at the top of your file. A missing import causes this error.
Ask for help: Why am I getting a NameError when I try to use train_test_split in my train.py?
Now apply SMOTE to generate synthetic fraud samples for the training set. This balances the classes so the model learns to detect fraud patterns instead of ignoring the minority class.
- Add the SMOTE resampling code below the "Before SMOTE" print statements in train.py:
# Apply SMOTE to training data only
smote = SMOTE(random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
print(f"\nAfter SMOTE:")
print(f"Training set class distribution:\n{pd.Series(y_train_resampled).value_counts()}")
What is SMOTE?
SMOTE (Synthetic Minority Oversampling Technique) creates new synthetic fraud samples by interpolating between existing fraud examples. For each fraud sample, it picks a nearby fraud neighbor and generates a new data point on the line segment between them.
This is applied to the training set only. The test set stays imbalanced because it needs to mirror what the model will face in production.
- Save train.py and run it:
python train.py
- You should see an "After SMOTE" section showing both classes now have roughly 227,451 samples each. The training data is balanced.
Script running very slowly?
SMOTE on 227,000+ samples can take 30-60 seconds depending on your machine. Let it finish. If it exceeds 2 minutes, check that your virtual environment is activated (you should see (venv) in your terminal prompt) and that you installed imbalanced-learn correctly.
Ask for help: My SMOTE resampling seems stuck or is taking forever. How can I check if something is wrong?
Understand why this matters
Look at the output from your last run. The training set went from ~393 fraud samples to matching the legitimate count at ~227,451.
Why not just use accuracy?
A naive model that always predicts "legitimate" gets 99.83% accuracy on this dataset. That sounds impressive but catches zero fraud. Balancing the training data with SMOTE forces the model to actually learn fraud patterns.
The test set stays imbalanced on purpose. When you evaluate the model in the next step, you want to measure how well it performs against the real-world distribution, not an artificial 50/50 split.
Why SMOTE instead of just duplicating fraud rows?
Simple duplication (random oversampling) makes the model memorize the exact fraud examples. SMOTE generates new synthetic points by interpolating between existing fraud neighbors, which gives the model a richer view of the fraud feature space.
✔️ Awesome, I've got everything!
Great. Double check you have saved train.py and your terminal output shows balanced classes after SMOTE.
ⓧ I'd like to double check the full code
Here is the complete train.py at this point:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from imblearn.over_sampling import SMOTE
import joblib
# Load data
df = pd.read_csv("creditcard.csv")
print(f"Dataset shape: {df.shape}")
print(f"Class distribution:\n{df['Class'].value_counts()}")
print(f"Fraud percentage: {df['Class'].mean() * 100:.3f}%")
# Drop Time column, scale Amount
df = df.drop("Time", axis=1)
scaler = StandardScaler()
df["Amount"] = scaler.fit_transform(df[["Amount"]])
# Split features and target
X = df.drop("Class", axis=1)
y = df["Class"]
# Train/test split with stratification
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"\nBefore SMOTE:")
print(f"Training set class distribution:\n{y_train.value_counts()}")
# Apply SMOTE to training data only
smote = SMOTE(random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
print(f"\nAfter SMOTE:")
print(f"Training set class distribution:\n{pd.Series(y_train_resampled).value_counts()}")
Your data is preprocessed and balanced. Next up, you will train a Random Forest model on this resampled data and evaluate it with metrics that actually matter for fraud detection.
Train and Evaluate with Production Metrics
In the previous step, you balanced your training data using SMOTE to give the model a fighting chance at learning fraud patterns.
Now it's time to train the actual model and evaluate whether it works. The critical insight here is that accuracy is a terrible metric for fraud detection. A model that always predicts "not fraud" gets 99.83% accuracy but catches zero fraud. You need metrics designed for imbalanced problems.
In this step, get ready to:
- Train a Random Forest classifier on your SMOTE-balanced data.
- Evaluate with production-grade metrics (precision, recall, F1, AUPRC).
- Serialize the trained model for deployment to AWS Lambda.
Train the model
Random Forest is a strong baseline for tabular fraud detection. It trains fast, serializes compactly with joblib, and is already part of scikit-learn so you won't need extra dependencies in your Lambda deployment package.
- In train.py, find the last line of your file (the one that prints the SMOTE class distribution).
- Add the following code below it to train the model:
# Train Random Forest
model = RandomForestClassifier(
n_estimators=100,
random_state=42,
class_weight="balanced",
n_jobs=-1
)
model.fit(X_train_resampled, y_train_resampled)
print("\nModel trained successfully!")
What does this code do?
This creates a Random Forest with 100 decision trees and trains it on your SMOTE-balanced data.
- n_estimators=100 sets the number of trees. More trees improve accuracy but increase model file size.
- class_weight="balanced" further adjusts the loss function to penalize missing fraud more heavily. This works alongside SMOTE for better recall.
- n_jobs=-1 uses all available CPU cores to speed up training.
- random_state=42 makes your results reproducible.
You won't run train.py yet. The next substep adds the code that saves the trained model to disk so you can run the full pipeline at once.
Evaluate with the right metrics
Your evaluation script loads the saved model and tests it against the original imbalanced test set. This simulates real-world performance where fraud is rare.
Why not use accuracy?
With 99.83% of transactions being legitimate, a model that always says "not fraud" gets 99.83% accuracy. That score looks great but catches zero fraud.
Instead, you will use precision (how many flagged transactions are actually fraud), recall (how many actual frauds you caught), and AUPRC (Area Under Precision-Recall Curve). The dataset authors specifically recommend AUPRC as the primary metric.
- Open evaluate.py in VS Code and paste the following code:
import joblib
import numpy as np
from sklearn.metrics import (
classification_report,
confusion_matrix,
precision_recall_curve,
auc,
)
# Load model and test data
model = joblib.load("fraud_model.joblib")
X_test, y_test = joblib.load("test_data.joblib")
# Predict
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
# Classification report
print("Classification Report:")
print(classification_report(y_test, y_pred, target_names=["Legitimate", "Fraud"]))
# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
print(f"Confusion Matrix:")
print(f" True Negatives (correct legitimate): {cm[0][0]}")
print(f" False Positives (legitimate flagged): {cm[0][1]}")
print(f" False Negatives (fraud missed): {cm[1][0]}")
print(f" True Positives (fraud caught): {cm[1][1]}")
# AUPRC - the recommended metric for this dataset
precision_vals, recall_vals, _ = precision_recall_curve(y_test, y_proba)
auprc = auc(recall_vals, precision_vals)
print(f"\nArea Under Precision-Recall Curve (AUPRC): {auprc:.4f}")
print(f"Baseline AUPRC (random): {y_test.mean():.4f}")
print(f"Your model is {auprc / y_test.mean():.0f}x better than random!")
What does this code do?
This script loads the saved model and runs it against the real-world imbalanced test set to produce three key outputs:
- classification_report shows precision, recall, and F1-score for both classes side by side.
- The confusion matrix breaks down predictions into four categories. False Positives (legitimate transactions flagged as fraud) annoy customers. False Negatives (fraud missed) cost money.
- AUPRC measures overall model quality across all thresholds. A random classifier's AUPRC equals the fraud rate (0.0017). Your model should be hundreds of times better.
- Save evaluate.py.
You can't run this yet because it needs the saved model files. The next substep adds the saving code to train.py and then you will run both scripts.
Save the model and run the pipeline
For your model to work inside AWS Lambda, it needs to be serialized to a file. Lambda has a 250 MB unzipped deployment limit, so the model file size matters.
- Switch back to train.py in VS Code.
- Add the following code at the bottom of the file, below the print("\nModel trained successfully!") line:
# Save model and scaler
joblib.dump(model, "fraud_model.joblib")
joblib.dump(scaler, "scaler.joblib")
# Save test data for evaluation
joblib.dump((X_test, y_test), "test_data.joblib")
import os
model_size = os.path.getsize("fraud_model.joblib") / (1024 * 1024)
print(f"Model file size: {model_size:.1f} MB")
print("Model and scaler saved!")
What does this code do?
joblib.dump() serializes Python objects to files that can be loaded later. You are saving three things:
- fraud_model.joblib contains the trained Random Forest model (typically 5-15 MB for 100 trees).
- scaler.joblib contains the fitted StandardScaler so your Lambda function can scale the Amount feature identically to training.
- test_data.joblib saves the imbalanced test set so evaluate.py can load it without re-running the entire data pipeline.
The file size check confirms the model fits within Lambda's 50 MB zipped upload limit.
- Save train.py.
- Run the training script by entering this command in your terminal:
python train.py
You should see the dataset shape, class distributions before and after SMOTE, a "Model trained successfully!" message, and the model file size (typically 5-15 MB).
Seeing a MemoryError?
The dataset is large (284,807 rows) and SMOTE doubles the training set size. If you see a memory error, close other applications to free up RAM. Training typically needs 4-6 GB of available memory.
help me debug a MemoryError when training my Random Forest on the credit card fraud dataset after SMOTE
- Now run the evaluation script:
python evaluate.py
You should see the classification report, confusion matrix, and AUPRC score. Your AUPRC should be significantly above the baseline of 0.0017 (the fraud rate).
What do the results mean?
High recall for the Fraud class means you are catching most fraudulent transactions. Reasonable precision means you are not flooding customers with false alerts.
The confusion matrix gives you the raw numbers. False Negatives (fraud missed) are the most costly in a real system. False Positives (legitimate flagged) are annoying but recoverable.
Seeing a FileNotFoundError?
If evaluate.py cannot find fraud_model.joblib or test_data.joblib, make sure train.py ran successfully first. Check that both .joblib files appear in your fraud-detection-api folder.
help me fix a FileNotFoundError when running evaluate.py for the fraud detection project
✔️ Awesome, I've got everything!
Great. Double check you have saved both train.py and evaluate.py.
ⓧ I'd like to double check the full code
Here is the full train.py at this point:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from imblearn.over_sampling import SMOTE
import joblib
# Load data
df = pd.read_csv("creditcard.csv")
print(f"Dataset shape: {df.shape}")
print(f"Class distribution:\n{df['Class'].value_counts()}")
print(f"Fraud percentage: {df['Class'].mean() * 100:.3f}%")
# Drop Time column, scale Amount
df = df.drop("Time", axis=1)
scaler = StandardScaler()
df["Amount"] = scaler.fit_transform(df[["Amount"]])
# Split features and target
X = df.drop("Class", axis=1)
y = df["Class"]
# Train/test split with stratification
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"\nBefore SMOTE:")
print(f"Training set class distribution:\n{y_train.value_counts()}")
# Apply SMOTE to training data only
smote = SMOTE(random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
print(f"\nAfter SMOTE:")
print(f"Training set class distribution:\n{pd.Series(y_train_resampled).value_counts()}")
# Train Random Forest
model = RandomForestClassifier(
n_estimators=100,
random_state=42,
class_weight="balanced",
n_jobs=-1
)
model.fit(X_train_resampled, y_train_resampled)
print("\nModel trained successfully!")
# Save model and scaler
joblib.dump(model, "fraud_model.joblib")
joblib.dump(scaler, "scaler.joblib")
# Save test data for evaluation
joblib.dump((X_test, y_test), "test_data.joblib")
import os
model_size = os.path.getsize("fraud_model.joblib") / (1024 * 1024)
print(f"Model file size: {model_size:.1f} MB")
print("Model and scaler saved!")
Here is the full evaluate.py:
import joblib
import numpy as np
from sklearn.metrics import (
classification_report,
confusion_matrix,
precision_recall_curve,
auc,
)
# Load model and test data
model = joblib.load("fraud_model.joblib")
X_test, y_test = joblib.load("test_data.joblib")
# Predict
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
# Classification report
print("Classification Report:")
print(classification_report(y_test, y_pred, target_names=["Legitimate", "Fraud"]))
# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
print(f"Confusion Matrix:")
print(f" True Negatives (correct legitimate): {cm[0][0]}")
print(f" False Positives (legitimate flagged): {cm[0][1]}")
print(f" False Negatives (fraud missed): {cm[1][0]}")
print(f" True Positives (fraud caught): {cm[1][1]}")
# AUPRC - the recommended metric for this dataset
precision_vals, recall_vals, _ = precision_recall_curve(y_test, y_proba)
auprc = auc(recall_vals, precision_vals)
print(f"\nArea Under Precision-Recall Curve (AUPRC): {auprc:.4f}")
print(f"Baseline AUPRC (random): {y_test.mean():.4f}")
print(f"Your model is {auprc / y_test.mean():.0f}x better than random!")
Your model is trained, evaluated, and saved to disk. Next up, you will package it into an AWS Lambda function and deploy it as a live API that anyone can call over HTTPS.
Package and Deploy to AWS Lambda
You have a trained fraud detection model and a set of production-grade evaluation metrics. The model works locally, but nobody else can use it yet.
In this step, you will write an inference handler that AWS Lambda can execute, package it with your model files and dependencies, and deploy the whole thing as a serverless function.
In this step, get ready to:
- Write a Lambda handler that loads your model and returns fraud predictions.
- Package your handler, model, and Python dependencies into a deployment zip.
- Deploy the function to AWS Lambda using the CLI.
Write the Lambda handler
Your Lambda function needs an entry point that AWS invokes whenever a request comes in. This handler loads the model once (on cold start), then reuses it for every subsequent request.
- In VS Code, open lambda_function.py (the empty file you created in Step 1).
- Add the imports and global model loading by pasting this code at the top of the file:
import json
import joblib
import numpy as np
# Load model and scaler at cold start (persists across warm invocations)
model = joblib.load("fraud_model.joblib")
scaler = joblib.load("scaler.joblib")
Why load the model outside the function?
Code at the module level runs once when Lambda first starts your function (a "cold start"). On subsequent requests ("warm invocations"), Lambda reuses the same process. Loading the model globally means you pay the deserialization cost only once, not on every API call.
- You won't see a change yet. The next chunk adds the handler function that uses these globals.
- Below the global loading code, add the lambda_handler function by pasting:
def lambda_handler(event, context):
try:
# Parse input
if isinstance(event.get("body"), str):
body = json.loads(event["body"])
else:
body = event.get("body", event)
features = body["features"]
# features should be 29 values: V1-V28 + Amount (Time already dropped)
if len(features) != 29:
return {
"statusCode": 400,
"body": json.dumps(
{"error": f"Expected 29 features, got {len(features)}"}
),
}
# Scale the Amount feature (last element)
features_array = np.array(features).reshape(1, -1)
features_array[0, -1] = scaler.transform([[features_array[0, -1]]])[0][0]
# Predict
prediction = int(model.predict(features_array)[0])
probability = float(model.predict_proba(features_array)[0][1])
# Risk tier
if probability < 0.3:
risk_tier = "Low"
elif probability < 0.7:
risk_tier = "Medium"
else:
risk_tier = "High"
return {
"statusCode": 200,
"body": json.dumps(
{
"prediction": prediction,
"fraud_probability": round(probability, 4),
"risk_tier": risk_tier,
"label": "FRAUD" if prediction == 1 else "LEGITIMATE",
}
),
}
except KeyError:
return {
"statusCode": 400,
"body": json.dumps(
{"error": "Missing 'features' key in request body"}
),
}
except Exception as e:
return {
"statusCode": 500,
"body": json.dumps({"error": str(e)}),
}
What does this code do?
The handler parses an incoming JSON body expecting a features array of 29 values (V1-V28 plus Amount). It validates the input length, scales the Amount feature using the same scaler from training, then runs the model's predict() and predict_proba() methods.
The response includes four fields: a binary prediction (0 or 1), the raw fraud probability, a human-readable risk tier (Low/Medium/High based on probability thresholds), and a label string (FRAUD or LEGITIMATE).
- Save lambda_function.py.
- You cannot run this file locally because it depends on Lambda's invocation format. You will test it after deployment in the next step.
Seeing a syntax error in VS Code?
Make sure there is exactly one blank line between the global loading section and the def lambda_handler line. Python requires consistent spacing between top-level definitions.
Also check that the indentation uses spaces (not tabs) throughout the file.
help me fix a syntax error in my lambda_function.py
✔️ Awesome, I've got everything!
Great. Double check you have saved lambda_function.py before moving on.
ⓧ I'd like to double check the full code
import json
import joblib
import numpy as np
# Load model and scaler at cold start (persists across warm invocations)
model = joblib.load("fraud_model.joblib")
scaler = joblib.load("scaler.joblib")
def lambda_handler(event, context):
try:
# Parse input
if isinstance(event.get("body"), str):
body = json.loads(event["body"])
else:
body = event.get("body", event)
features = body["features"]
# features should be 29 values: V1-V28 + Amount (Time already dropped)
if len(features) != 29:
return {
"statusCode": 400,
"body": json.dumps(
{"error": f"Expected 29 features, got {len(features)}"}
),
}
# Scale the Amount feature (last element)
features_array = np.array(features).reshape(1, -1)
features_array[0, -1] = scaler.transform([[features_array[0, -1]]])[0][0]
# Predict
prediction = int(model.predict(features_array)[0])
probability = float(model.predict_proba(features_array)[0][1])
# Risk tier
if probability < 0.3:
risk_tier = "Low"
elif probability < 0.7:
risk_tier = "Medium"
else:
risk_tier = "High"
return {
"statusCode": 200,
"body": json.dumps(
{
"prediction": prediction,
"fraud_probability": round(probability, 4),
"risk_tier": risk_tier,
"label": "FRAUD" if prediction == 1 else "LEGITIMATE",
}
),
}
except KeyError:
return {
"statusCode": 400,
"body": json.dumps(
{"error": "Missing 'features' key in request body"}
),
}
except Exception as e:
return {
"statusCode": 500,
"body": json.dumps({"error": str(e)}),
}
Create the deployment package
Lambda needs your code AND its Python dependencies bundled together in a single zip file. You cannot rely on pip install at runtime because Lambda has no internet access by default.
- Create a deployment/ folder and install dependencies into it by running these commands:
mkdir deployment
pip install scikit-learn==1.9.0 joblib numpy -t deployment/
What does the -t flag do?
The -t deployment/ flag tells pip to install packages into the specified folder instead of your virtual environment. This puts the library files exactly where Lambda can find them (next to your handler).
- Copy your handler and model files into the deployment folder by running:
copy lambda_function.py deployment\
copy fraud_model.joblib deployment\
copy scaler.joblib deployment\
- Create the zip archive by running:
cd deployment
Compress-Archive -Path * -DestinationPath ..\fraud-detection.zip
cd ..
- Confirm the zip was created by running:
ls fraud-detection.zip
- You should see fraud-detection.zip listed in the output.
Zip exceeds 50 MB?
AWS Lambda has a 50 MB limit for direct zip uploads. If your zip exceeds this, upload it to S3 first and reference the S3 path when creating the function:
aws s3 cp fraud-detection.zip s3://your-bucket-name/
Then use --code S3Bucket=your-bucket-name,S3Key=fraud-detection.zip instead of --zip-file in the deploy command. For most Random Forest models, the zip should be well under 50 MB.
Compress-Archive not recognized?
This command only works in PowerShell. If your VS Code terminal is running Command Prompt (cmd), switch to PowerShell by clicking the dropdown arrow next to the + icon in the terminal panel and selecting **PowerShell**.
help me create a zip file for my Lambda deployment package
Deploy to Lambda
Before creating the Lambda function, you need an IAM execution role that grants Lambda permission to write logs. You will create this role in the AWS Console.
- Open the AWS Console in your browser and navigate to the IAM service.
- Click **Roles** in the left sidebar.
- Click **Create role**.
- Under **Trusted entity type**, select **AWS service**.
- Under **Use case**, select **Lambda**.
- Click **Next**.
- In the search box, type AWSLambdaBasicExecutionRole and check the box next to it.
- Click **Next**.
- Enter lambda-execution-role as the **Role name**.
- Click **Create role**.
- After creation, click into the lambda-execution-role role and copy the **ARN** from the top of the page. It looks like arn:aws:iam::123456789012:role/lambda-execution-role.
What is an execution role?
Every Lambda function needs an execution role that defines what AWS services it can access. The AWSLambdaBasicExecutionRole policy grants permission to write logs to CloudWatch. That is the minimum your function needs to run.
- Deploy the Lambda function by running this command (replace the role ARN with your own):
aws lambda create-function --function-name fraud-detection --runtime python3.12 --handler lambda_function.lambda_handler --role [[ROLE_ARN="arn:aws:iam::YOUR_ACCOUNT_ID:role/lambda-execution-role"]] --zip-file fileb://fraud-detection.zip --timeout 30 --memory-size 512
What do these flags mean?
- --function-name fraud-detection names your function in AWS.
- --runtime python3.12 tells Lambda which Python version to use.
- --handler lambda_function.lambda_handler points to the file (lambda_function) and function (lambda_handler) that Lambda should call.
- --timeout 30 gives the function 30 seconds to respond (model loading on cold start takes a few seconds).
- --memory-size 512 allocates 512 MB of RAM for loading the model into memory.
- You should see a JSON response containing your function's ARN, runtime, and other configuration details.
- Verify in the AWS Console by navigating to **Lambda** and clicking **Functions**. You should see fraud-detection in the list.
Getting an InvalidParameterValueException?
This usually means the role ARN is incorrect or the role has not finished propagating. Wait 10 seconds and try the command again. IAM role creation can take a few seconds to propagate across AWS.
Double check that your ARN follows the format arn:aws:iam::ACCOUNT_ID:role/lambda-execution-role with your actual 12-digit account ID and no extra spaces.
help me fix my aws lambda create-function command
Your model is now live on AWS. Next up, you will create a public URL for your function and test it with real transaction data.
Create an Endpoint and Test Your Live API
Your fraud detection model is deployed to AWS Lambda and ready to process predictions. The missing piece is a way for the outside world to reach it.
In this step, you will create a Function URL that gives your Lambda function a public HTTPS endpoint. Then you will test it with real transaction data to confirm your fraud detection system works end-to-end.
In this step, get ready to:
- Create a public HTTPS endpoint for your Lambda function.
- Grant public invocation access with a resource-based policy.
- Test the live API with legitimate and fraudulent transaction data.
Create a Function URL
Lambda Function URLs are dedicated HTTPS endpoints built into Lambda. They are simpler than API Gateway and have no additional cost beyond standard Lambda pricing.
- Create a Function URL for your Lambda function by running this command:
aws lambda create-function-url-config --function-name fraud-detection --auth-type NONE
What does this command do?
This creates an HTTPS endpoint for your fraud-detection function. The --auth-type NONE flag means anyone with the URL can invoke your function without IAM authentication.
The output includes a FunctionUrl field. This is your live API endpoint. Copy it and save it somewhere handy.
You should see JSON output with a FunctionUrl field in the format https://xxxxxxxx.lambda-url.us-east-1.on.aws/.
Since you created the Function URL via the CLI (not the console), you need to manually add a resource-based policy that allows public invocation.
- Grant public access to your Function URL by running this command:
aws lambda add-permission --function-name fraud-detection --statement-id public-access --action lambda:InvokeFunctionUrl --principal * --function-url-auth-type NONE
What does this command do?
This adds a resource-based policy to your Lambda function. The policy allows any principal (*) to invoke the function through its URL. Without this policy, requests to your Function URL would return a 403 Forbidden error.
You should see JSON output confirming the statement was added, including a Statement field.
Seeing a ResourceConflictException?
This error means a statement with the same ID already exists on the function. If you ran the command before and it succeeded, you can safely ignore this. If you need to re-run it, first remove the old policy with:
aws lambda remove-permission --function-name fraud-detection --statement-id public-access
Then re-run the add-permission command.
Help me fix this ResourceConflictException error
Test with real transaction data
Your API is live. Time to send it real transaction data and see if it correctly identifies fraud.
Your Lambda function expects 29 features: V1 through V28 (the PCA-transformed values) plus the raw Amount. The first request will simulate a legitimate transaction.
- Test with a legitimate transaction by running this command in your terminal:
Invoke-RestMethod -Uri "[[FUNCTION_URL="your Function URL from the previous command"]]" -Method POST -Body '{"features": [-1.36, -0.07, 2.54, 1.38, -0.34, 0.46, 0.24, 0.10, 0.36, 0.09, -0.55, -0.62, -0.99, -0.31, 1.47, -0.47, 0.21, 0.03, 0.40, 0.25, -0.02, 0.28, -0.11, 0.07, 0.13, -0.19, 0.13, -0.02, 149.62]}' -ContentType "application/json"
What should I see?
You should get a JSON response with four fields:
- prediction: 0 (not fraud).
- fraud_probability: a low decimal value.
- risk_tier: "Low".
- label: "LEGITIMATE".
If you see this, your model is correctly identifying a normal transaction. The first cold start may take 3-5 seconds. Subsequent calls will be much faster.
- Now test with a likely fraudulent transaction by running this command:
Invoke-RestMethod -Uri "[[FUNCTION_URL="your Function URL from the previous command"]]" -Method POST -Body '{"features": [1.19, 0.27, 0.17, 0.45, -0.35, -0.81, -0.19, -0.14, -2.26, -5.02, 3.68, -1.27, 0.05, -7.05, 0.73, -4.52, -5.22, -1.37, 0.81, 0.30, 0.76, 0.27, -0.11, -0.34, 0.08, -0.63, 0.18, 0.03, 1.00]}' -ContentType "application/json"
What should I see?
This transaction has extreme values on features like V14 and V16, which are typical fraud indicators. You should see:
- prediction: 1 (fraud).
- fraud_probability: a high decimal value (above 0.7).
- risk_tier: "High".
- label: "FRAUD".
If both tests return sensible predictions, your fraud detection API is working in production.
Getting an error response?
- If you see Expected 29 features, double check that your JSON body contains exactly 29 values in the features array.
- If you see a 403 Forbidden error, the resource-based policy was not applied. Re-run the add-permission command from earlier.
- If you see a 500 Internal Server Error, check your Lambda logs in the AWS Console under CloudWatch > Log groups > /aws/lambda/fraud-detection.
Help me debug my Lambda Function URL response
Understand the cost implications
Your fraud detection API is now live. Here is what this costs you going forward.
What does this cost?
Lambda Function URLs have no additional charge beyond standard Lambda pricing. Your function only incurs cost when someone invokes it.
Lambda's always-free tier covers 1,000,000 requests per month and 400,000 GB-seconds of compute. For a portfolio project that receives occasional demo traffic, you will stay well within these limits indefinitely. There is nothing to clean up unless you want to delete the function.
Why Function URLs over API Gateway?
API Gateway's free tier expires after 12 months, but Lambda Function URLs are included in Lambda's always-free tier permanently. For a portfolio project you want to keep running, Function URLs mean zero ongoing cost.
Your fraud detection system is fully deployed. You have a live HTTPS endpoint that accepts transaction data, runs it through your trained model, and returns fraud predictions with confidence scores. In the Secret Mission, you will add SHAP explainability so your API can explain why it flagged a transaction.
Secret mission
Add SHAP Explainability
Your API currently tells users whether a transaction is fraudulent, but not WHY. In this secret mission, you will add SHAP explainability so your API returns the top features that drove each prediction. This is a genuine differentiator on a fresher resume.
Clean Up Your Resources
Clean Up Your Resources
Your Lambda function is covered by the AWS always-free tier, which includes 1 million requests and 400,000 GB-seconds per month with no expiration. This means your fraud detection API costs nothing when idle and nothing when invoked at portfolio-demo levels.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- AWS Lambda function (fraud-detection). Covered by the always-free tier. No cost when idle.
- IAM role (lambda-execution-role). No cost.
- S3 bucket (only if you uploaded your deployment zip via S3). Free tier includes 5 GB storage for 12 months.
- Local project folder (fraud-detection-api) with all Python files, model artifacts, the dataset, and your virtual environment.
Keep everything running
No action needed. Choose this if you want to keep your live fraud detection API as a portfolio piece.
- Your Lambda function costs $0 when not in use. It only incurs charges when invoked, and the always-free tier covers 1 million requests per month.
- The Function URL remains active and callable. You can share it on your resume or in interviews.
- If you used an S3 bucket for deployment, it stores only the zip file. The S3 free tier covers this for 12 months.
- Your local project files remain on your machine for future reference.
Pause - I'll come back to this later
Lambda is serverless and only charges per invocation. There is no running process to pause. Your function sits idle at zero cost until the next request arrives.
- No action needed on AWS. Your Lambda function and Function URL remain available but cost nothing while idle.
- Locally, you can deactivate your virtual environment by running the command below. Your project files stay intact for when you return.
deactivate
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each item below to fully clean up.
- Delete the Lambda function by running this command in your terminal:
aws lambda delete-function --function-name fraud-detection
- Delete the IAM role in the AWS Console:
- Open the IAM console at https://console.aws.amazon.com/iam/.
- In the navigation pane, choose Roles.
- Select the checkbox next to lambda-execution-role.
- Click Delete at the top of the page.
- Type the role name lambda-execution-role in the confirmation field and click Delete.
- If you created an S3 bucket for deployment, delete it by running:
aws s3 rb s3://[[S3_BUCKET="your-bucket-name"]] --force
- Delete your local project files. On macOS or Linux:
cd ~/Desktop
rm -rf fraud-detection-api
- On Windows (PowerShell):
cd ~\Desktop
Remove-Item -Recurse -Force fraud-detection-api
Nice Work!
Nice Work!
You just built a complete fraud detection system from raw data to a live API endpoint. That is the exact notebook-to-production workflow that ML engineering roles demand.
You've learned how to:
- Train a Random Forest classifier on real-world data (284,807 transactions) with extreme class imbalance, using SMOTE oversampling and evaluating with production metrics like AUPRC instead of misleading accuracy.
- Serialize a trained model with joblib and deploy it as a serverless inference API on AWS Lambda, including IAM roles, deployment packaging, and memory configuration.
- Create a live HTTPS endpoint using Lambda Function URLs that accepts transaction data and returns fraud predictions with confidence scores and risk tiers.
- Secret Mission: Added SHAP explainability so your API explains which features drove each fraud prediction, not just a yes/no answer.
Ready to quiz yourself?