Η βάση δεδομένων που θα χρησιμοποιήσουμε περιέχει συνναλαγές πιστωτικών καρτών που έγιναν στο διάστημα του Σεπτεμβρίου 2013 Στην Ευρώπη. Η βάση θα χρησιμποιηθεί για να εκπαιδευτεί μοντέλο που προβλέπει απάτες συναλλαγών.
Η μέθοδος που θα χρησιμοποιήσω για την δημιουργία του μοντέλου αυτού είναι : Random Forest , καθώς θα καταφέρει να δώσει την μεγαλύτερη ακρίβεια στις προβλέψεις του μοντέλου μας .
library(caTools)
library(xgboost)
## Warning: package 'xgboost' was built under R version 4.6.1
library(ROCR)
fraud <- read.csv("creditcard.csv")
class_table <- table(fraud$Class)
print(class_table)
##
## 0 1
## 284315 492
Μόλις το 0.172% των καταγραφών αποτελούν παράδειγμα απάτης.
Πάμε να χωρίσουμε τα δεδομένα σε 70/30.
set.seed(94)
split <- sample.split(fraud$Class, SplitRatio = 0.70)
train <-subset(fraud, split ==TRUE)
test <-subset(fraud, split == FALSE)
cat("Train rows:", nrow(train), " Frauds:" , sum(train$Class), "\n")
## Train rows: 199364 Frauds: 344
cat("Test rows: ", nrow(test), "Frauds: ", sum(test$Class), "\n")
## Test rows: 85443 Frauds: 148
Τυποποιούμε τις τιμές Time και Amount. Έτσι αποφεύγουμε την διαρροή δεδομένων.
for(col in c("Time" , "Amount")) {
mu <-mean(train[[col]])
sdv <- sd(train[[col]])
train[[col]] <- (train[[col]] - mu)/ sdv
test[[col]] <- (test[[col]] - mu) / sdv
}
Ας δούμε πόσο καλά υπολογίζονται οι δόλιες συναλλαγές με την χρήση κατάλληλων μετρικών για ανισοκατανομή αντί για απλό accuracy.
roc_auc <- function(prob , actual) {
performance(prediction(prob, actual), "auc")@y.values[[1]]
}
pr_auc <- function(prob, actual) {
perf <- performance(prediction(prob, actual), "prec" , "rec")
rec <- perf@x.values[[1]]
prec <- perf@y.values[[1]]
ok <- !is.nan(prec) & !is.nan(rec)
rec <- rec[ok]; prec <- prec[ok]
ord <- order(rec)
rec <- rec[ord]; prec <- prec[ord]
sum(diff(rec) * (head(prec, -1) + tail(prec, -1)) / 2)
}
evaluate <- function(actual, prob, threshold = 0.5, label = "") {
pred <- ifelse(prob > threshold, 1, 0)
cm <- table(Actual = factor(actual, levels = c(0, 1)),
Predicted = factor(pred, levels = c(0, 1)))
TN <- cm[1, 1]; FP <- cm[1, 2]; FN <- cm[2, 1]; TP <- cm[2, 2]
precision <- TP / (TP + FP)
recall <- TP / (TP + FN) # a.k.a. sensitivity / fraud-detection rate
f1 <- 2 * precision * recall / (precision + recall)
cat("---", label, "(threshold =", threshold, ") ---\n")
print(cm)
cat("\nFraud caught (TP):", TP, " Fraud missed (FN):", FN,
" False alarms (FP):", FP, "\n")
cat("Precision:", round(precision, 4),
"| Recall:", round(recall, 4),
"| F1:", round(f1, 4), "\n")
cat("ROC-AUC:", round(roc_auc(prob, actual), 4),
"| PR-AUC:", round(pr_auc(prob, actual), 4), "\n\n")
invisible(list(precision = precision, recall = recall, f1 = f1,
TP = TP, FN = FN, FP = FP))
}
Χρησιμοποιούμε για να δούμε ποιο είναι το baseline μας.
log_model <-glm(Class ~ ., data = train, family = binomial)
pred_log <- predict(log_model, newdata = test, type = "response")
log_res <- evaluate(test$Class, pred_log, threshold = 0.5,
label = "Λογιστική Παλινδρόμηση")
## --- Λογιστική Παλινδρόμηση (threshold = 0.5 ) ---
## Predicted
## Actual 0 1
## 0 85282 13
## 1 50 98
##
## Fraud caught (TP): 98 Fraud missed (FN): 50 False alarms (FP): 13
## Precision: 0.8829 | Recall: 0.6622 | F1: 0.7568
## ROC-AUC: 0.9834 | PR-AUC: 0.7642
Τώρα το ensemble. Εκπαιδεύουμε σε 150 γύρους boosting .
feature_cols <- setdiff(names(train), "Class")
dtrain <- xgb.DMatrix(data = as.matrix(train[, feature_cols]),
label = train$Class)
dtest <- xgb.DMatrix(data = as.matrix(test[, feature_cols]),
label = test$Class)
spw <- sum(train$Class == 0) / sum(train$Class == 1)
cat("scale_pos_weight:", round(spw, 1), "\n")
## scale_pos_weight: 578.5
params <- list(
objective = "binary:logistic",
eval_metric = "aucpr",
max_depth = 6,
eta = 0.1,
scale_pos_weight = spw
)
set.seed(94)
xgb_model <- xgb.train(params, dtrain, nrounds = 150, verbose = 0)
pred_xgb <- predict(xgb_model, dtest)
xgb_res <- evaluate(test$Class, pred_xgb, threshold = 0.5, label = "XGBoost (με βάρη)")
## --- XGBoost (με βάρη) (threshold = 0.5 ) ---
## Predicted
## Actual 0 1
## 0 85281 14
## 1 22 126
##
## Fraud caught (TP): 126 Fraud missed (FN): 22 False alarms (FP): 14
## Precision: 0.9 | Recall: 0.8514 | F1: 0.875
## ROC-AUC: 0.9803 | PR-AUC: 0.8616
Για να απομονώσουμε την επίδραση της αντιμετώπισης της ανισορροπίας, εκπαιδεύουμε ένα δεύτερο XGBoost χωρίς βάρη και συγκρίνουμε με το μοντέλο.
params_unw <- params
params_unw$scale_pos_weight <- 1
set.seed(94)
xgb_unw <- xgb.train(params_unw, dtrain, nrounds = 150, verbose = 0)
pred_unw <- predict(xgb_unw, dtest)
unw_res <- evaluate(test$Class, pred_unw, threshold = 0.5,label = "XGBoost (χωρίς βάρη)")
## --- XGBoost (χωρίς βάρη) (threshold = 0.5 ) ---
## Predicted
## Actual 0 1
## 0 85291 4
## 1 30 118
##
## Fraud caught (TP): 118 Fraud missed (FN): 30 False alarms (FP): 4
## Precision: 0.9672 | Recall: 0.7973 | F1: 0.8741
## ROC-AUC: 0.9801 | PR-AUC: 0.8618
Το βάρος στη θετική κλάση ανταλλάσει λίγο precision για ουσιαστικό κέρδος στο recall. Το μοντέλο εντοπίζει περισσότερες από τις σπάνιες απάτες στο προεπιλεγμένο κατόφλι.
Παρότι τα χαρακτηριστικά είναι ανώνυμα, το XGBoost μας λέει ποια οδηγούν στην ανίχνευση.
importance <- xgb.importance(model = xgb_model)
print(head(importance, 10))
## Feature Gain Cover Frequency
## <char> <num> <num> <num>
## 1: V14 0.69128014 0.25624675 0.06607539
## 2: V4 0.06706925 0.13507141 0.06563193
## 3: V8 0.02540563 0.01517702 0.03281596
## 4: Amount 0.02324721 0.02004121 0.03991131
## 5: V12 0.02205286 0.06573006 0.05321508
## 6: V10 0.01572641 0.08316870 0.04434590
## 7: V7 0.01479704 0.05968729 0.04390244
## 8: V1 0.01244930 0.05858371 0.04146341
## 9: V19 0.01133595 0.03384091 0.02682927
## 10: V17 0.01031379 0.03551934 0.02793792
xgb.plot.importance(importance[1:10, ],
main = "Top 10 χαρακτηριστικά κατά σημαντικότητα (Gain)")
Λίγες από τις συνιστώσες PCA (συνήθως οι V14, V17, V12 , V10 , V4) κυριαρχούν στη στήλη Gain, μεταφέρουν το μεγαλύτερο μέρος του σήματος που διαχωρίζει την απάτη από τη νόμιμη συναλλαγή.
Σύγκριση απλού μοντέλου vs Ensemble
Το προεπιλεγμένο κατώφλι είναι 0.5. Επειδή μια χαμένη απάτη κοστίζει περισσότερο από έναν λανθασμένο συναγερμό, σαρώνουμε διάφορα κατώφλια και διαβάζουμε το : trade-off precission / recall , για να επιλέξουμε ένα σημείο που μεγιστοποιεί το recall (ελαχιστοποιεί τα false negatives) χωρίς να εκτοξεύει τα false positives.
thresholds <- seq(0.10, 0.90, by = 0.05)
scan <- data.frame(t = thresholds,
Recall = NA, Precision = NA,
FN = NA, FP = NA)
for (i in seq_along(thresholds)) {
pred <- ifelse(pred_xgb > thresholds[i], 1, 0)
TP <- sum(pred == 1 & test$Class == 1)
FP <- sum(pred == 1 & test$Class == 0)
FN <- sum(pred == 0 & test$Class == 1)
scan$Recall[i] <- TP / (TP + FN)
scan$Precision[i] <- ifelse(TP + FP == 0, NA, TP / (TP + FP))
scan$FN[i] <- FN
scan$FP[i] <- FP
}
print(scan, row.names = FALSE)
## t Recall Precision FN FP
## 0.10 0.8648649 0.7191011 20 50
## 0.15 0.8648649 0.7852761 20 35
## 0.20 0.8581081 0.8355263 21 25
## 0.25 0.8581081 0.8639456 21 20
## 0.30 0.8581081 0.8639456 21 20
## 0.35 0.8581081 0.8819444 21 17
## 0.40 0.8581081 0.8943662 21 15
## 0.45 0.8581081 0.9007092 21 14
## 0.50 0.8513514 0.9000000 22 14
## 0.55 0.8513514 0.9064748 22 13
## 0.60 0.8513514 0.9197080 22 11
## 0.65 0.8445946 0.9191176 23 11
## 0.70 0.8310811 0.9248120 25 10
## 0.75 0.8310811 0.9389313 25 8
## 0.80 0.8243243 0.9457364 26 7
## 0.85 0.8175676 0.9527559 27 6
## 0.90 0.8175676 0.9680000 27 4
plot(scan$t, scan$Recall, type = "b", pch = 19, col = "steelblue",
ylim = c(0, 1), xlab = "Κατώφλι πιθανότητας", ylab = "Τιμή",
main = "Precision vs Recall σε διάφορα κατώφλια")
lines(scan$t, scan$Precision, type = "b", pch = 17, col = "firebrick")
legend("bottom", legend = c("Recall", "Precision"),
col = c("steelblue", "firebrick"), pch = c(19, 17), horiz = TRUE)
Διαβάζοντας τις καμπύλες, ένα χαμηλότερο κατώφλι ανεβαίζει το recall (λιγότερες χαμένες απάτες) με κόστος το precision. Ένα λογικό σημείο λειτουργίας κρατά το recall υψηλό ενώ η precision παραμένει αποδεκτή.