Chapter 8 Project 6: Real-Time Fraud Detection
Fraud detection demands millisecond-level latency, extreme class imbalance (often <0.1% fraud), and adaptability to evolving attack patterns. In this chapter, we build a hybrid system combining isolation forests for anomaly detection with XGBoost for supervised classification, backed by Redis for real-time velocity features.
8.1 Business Context
A payment processor needs to score every transaction in real time:
- Approve: Process immediately (< 50ms)
- Review: Send to manual review queue (50-500ms)
- Block: Decline transaction (> 500ms or high confidence fraud)
The system must minimize false positives (legitimate transactions blocked) while catching sophisticated fraud patterns.
8.2 Architecture Overview
Transaction ──► Velocity Features (Redis) ──┐
├──► Isolation Forest ──► XGBoost ──► Decision
Static Features (DB) ────────────────────────┘
8.3 Real-Time Velocity Features with Redis
Velocity features capture behavioral anomalies: is this user making unusually many transactions? Is this card being used across multiple countries?
library(redis)
library(jsonlite)
redis_con <- redux::hiredis(host = "redis-cluster", port = 6379)
compute_velocity_features_r <- function(transaction) {
user_id <- transaction$user_id
card_hash <- transaction$card_fingerprint
# User velocity (last 1 hour)
user_key <- paste0("user:", user_id, ":txns")
user_txns <- redis_con$ZRANGEBYSCORE(
user_key,
as.character(as.numeric(Sys.time()) - 3600),
as.character(as.numeric(Sys.time()))
)
# Card velocity (last 24 hours)
card_key <- paste0("card:", card_hash, ":txns")
card_txns <- redis_con$ZRANGEBYSCORE(
card_key,
as.character(as.numeric(Sys.time()) - 86400),
as.character(as.numeric(Sys.time()))
)
# Parse transaction amounts from JSON
user_amounts <- sapply(user_txns, function(t) fromJSON(t)$amount, USE.NAMES = FALSE)
card_amounts <- sapply(card_txns, function(t) fromJSON(t)$amount, USE.NAMES = FALSE)
# Historical baselines
user_avg <- as.numeric(redis_con$GET(paste0("user:", user_id, ":avg_amount")))
user_max <- as.numeric(redis_con$GET(paste0("user:", user_id, ":max_amount")))
list(
user_txn_count_1h = length(user_txns),
user_txn_amount_1h = sum(user_amounts, na.rm = TRUE),
card_txn_count_24h = length(card_txns),
card_txn_amount_24h = sum(card_amounts, na.rm = TRUE),
amount_vs_user_avg = transaction$amount / max(user_avg, 1),
amount_vs_user_max = transaction$amount / max(user_max, 1),
time_since_last_txn_min = ifelse(
length(user_txns) > 0,
(as.numeric(Sys.time()) -
as.numeric(redis_con$ZSCORE(user_key, tail(user_txns, 1)))) / 60,
9999
)
)
}Important: Redis sorted sets (ZADD, ZRANGEBYSCORE) provide O(log n) insertion and range queries, making them ideal for time-windowed aggregations. Set TTLs (time-to-live) on keys to prevent unbounded memory growth.
8.4 Isolation Forest for Anomaly Detection
Isolation forests isolate anomalies by randomly selecting features and split values. Anomalies require fewer splits to isolate.
8.5 XGBoost with Anomaly Score
We enhance the feature set with the isolation forest anomaly score, creating a powerful hybrid model.
fit_fraud_xgboost <- function(train_data, labels) {
# Add isolation forest anomaly score
if_model <- fit_isolation_forest(train_data)
if_scores <- if_model$predict(train_data)$anomaly_score
train_enhanced <- train_data %>%
mutate(iso_score = if_scores)
dtrain <- xgb.DMatrix(
data = as.matrix(train_enhanced),
label = labels
)
params <- list(
objective = "binary:logistic",
eval_metric = "aucpr",
eta = 0.1,
max_depth = 6,
subsample = 0.8,
colsample_bytree = 0.8,
scale_pos_weight = 50 # Fraud is ~50x rarer than legitimate
)
xgb_cv <- xgb.cv(
params = params,
data = dtrain,
nrounds = 300,
nfold = 5,
early_stopping_rounds = 20,
print_every_n = 50
)
xgb.train(
params = params,
data = dtrain,
nrounds = xgb_cv$best_iteration
)
}8.6 Real-Time Scoring Function
score_transaction <- function(transaction, xgb_model, if_model, threshold = 0.7) {
# Compute velocity features
velocity <- compute_velocity_features_r(transaction)
transaction_enhanced <- c(transaction, velocity)
# Isolation forest anomaly score
if_score <- if_model$predict(
as.data.frame(transaction_enhanced)
)$anomaly_score
# XGBoost fraud probability
features <- as.matrix(as.data.frame(transaction_enhanced))
fraud_prob <- predict(xgb_model, features)
# Decision logic
action <- case_when(
fraud_prob > 0.95 ~ "block",
fraud_prob > threshold ~ "review",
TRUE ~ "approve"
)
list(
transaction_id = transaction$transaction_id,
fraud_probability = round(fraud_prob, 4),
action = action,
iso_score = round(if_score, 4),
timestamp = format(Sys.time(), "%Y-%m-%d %H:%M:%S")
)
}8.7 Performance Optimization
8.7.1 Pre-warming Redis
Load historical baselines into Redis at startup:
warm_redis_baselines <- function(con) {
tbl(con, "transactions") %>%
filter(transaction_date >= Sys.Date() - 90) %>%
group_by(user_id) %>%
summarise(
avg_amount = mean(amount),
max_amount = max(amount),
.groups = "drop"
) %>%
collect() %>%
pwalk(function(user_id, avg_amount, max_amount) {
redis_con$SET(paste0("user:", user_id, ":avg_amount"), avg_amount)
redis_con$SET(paste0("user:", user_id, ":max_amount"), max_amount)
})
}8.9 Exercises
- Implement a device fingerprinting feature that flags transactions from new devices.
- Add a geo-velocity check: flag transactions impossible given time/distance from previous transaction.
- Build a feedback loop that retrains the model weekly on new labeled data.
- Implement a circuit breaker pattern: if Redis is down, fall back to database queries with a timeout.
Next, we’ll build an automated reporting pipeline that orchestrates ETL, transformations, and report generation.