Skip to contents

Introduction

This example looks at the Kaggle Credit Card Fraud Detection dataset to demonstrate how to train a classification model on data with highly imbalanced classes. You can download the data by clicking “Download” at the link, or if you’re setup with a kaggle API key at "~/.kaggle/kagle.json", you can run the following:

reticulate::py_install("kaggle", pip = TRUE)
reticulate::py_available(TRUE) # ensure 'kaggle' is on the PATH
system("kaggle datasets download -d mlg-ulb/creditcardfraud")
zip::unzip("creditcardfraud.zip", files = "creditcard.csv")

First, load the data

library(readr)
df <- read_csv("creditcard.csv", col_types = cols(
  Class = col_integer(),
  .default = col_double()
))
tibble::glimpse(df)
## Rows: 284,807
## Columns: 31
## $ Time   <dbl> 0, 0, 1, 1, 2, 2, 4, 7, 7, 9, 10, 10, 10, 11, 12, 12, 12, 1…
## $ V1     <dbl> -1.3598071, 1.1918571, -1.3583541, -0.9662717, -1.1582331, …
## $ V2     <dbl> -0.07278117, 0.26615071, -1.34016307, -0.18522601, 0.877736…
## $ V3     <dbl> 2.53634674, 0.16648011, 1.77320934, 1.79299334, 1.54871785,…
## $ V4     <dbl> 1.37815522, 0.44815408, 0.37977959, -0.86329128, 0.40303393…
## $ V5     <dbl> -0.33832077, 0.06001765, -0.50319813, -0.01030888, -0.40719…
## $ V6     <dbl> 0.46238778, -0.08236081, 1.80049938, 1.24720317, 0.09592146…
## $ V7     <dbl> 0.239598554, -0.078802983, 0.791460956, 0.237608940, 0.5929…
## $ V8     <dbl> 0.098697901, 0.085101655, 0.247675787, 0.377435875, -0.2705…
## $ V9     <dbl> 0.3637870, -0.2554251, -1.5146543, -1.3870241, 0.8177393, -…
## $ V10    <dbl> 0.09079417, -0.16697441, 0.20764287, -0.05495192, 0.7530744…
## $ V11    <dbl> -0.55159953, 1.61272666, 0.62450146, -0.22648726, -0.822842…
## $ V12    <dbl> -0.61780086, 1.06523531, 0.06608369, 0.17822823, 0.53819555…
## $ V13    <dbl> -0.99138985, 0.48909502, 0.71729273, 0.50775687, 1.34585159…
## $ V14    <dbl> -0.31116935, -0.14377230, -0.16594592, -0.28792375, -1.1196…
## $ V15    <dbl> 1.468176972, 0.635558093, 2.345864949, -0.631418118, 0.1751…
## $ V16    <dbl> -0.47040053, 0.46391704, -2.89008319, -1.05964725, -0.45144…
## $ V17    <dbl> 0.207971242, -0.114804663, 1.109969379, -0.684092786, -0.23…
## $ V18    <dbl> 0.02579058, -0.18336127, -0.12135931, 1.96577500, -0.038194…
## $ V19    <dbl> 0.40399296, -0.14578304, -2.26185710, -1.23262197, 0.803486…
## $ V20    <dbl> 0.25141210, -0.06908314, 0.52497973, -0.20803778, 0.4085423…
## $ V21    <dbl> -0.018306778, -0.225775248, 0.247998153, -0.108300452, -0.0…
## $ V22    <dbl> 0.277837576, -0.638671953, 0.771679402, 0.005273597, 0.7982…
## $ V23    <dbl> -0.110473910, 0.101288021, 0.909412262, -0.190320519, -0.13…
## $ V24    <dbl> 0.06692807, -0.33984648, -0.68928096, -1.17557533, 0.141266…
## $ V25    <dbl> 0.12853936, 0.16717040, -0.32764183, 0.64737603, -0.2060095…
## $ V26    <dbl> -0.18911484, 0.12589453, -0.13909657, -0.22192884, 0.502292…
## $ V27    <dbl> 0.133558377, -0.008983099, -0.055352794, 0.062722849, 0.219…
## $ V28    <dbl> -0.021053053, 0.014724169, -0.059751841, 0.061457629, 0.215…
## $ Amount <dbl> 149.62, 2.69, 378.66, 123.50, 69.99, 3.67, 4.99, 40.80, 93.…
## $ Class  <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…

Prepare a validation set

val_idx <- nrow(df) %>% sample.int(., round( . * 0.2))
val_df <- df[val_idx, ]
train_df <- df[-val_idx, ]

cat("Number of training samples:", nrow(train_df), "\n")
## Number of training samples: 227846
cat("Number of validation samples:", nrow(val_df), "\n")
## Number of validation samples: 56961

Analyze class imbalance in the targets

counts <- table(train_df$Class)
counts
##
##      0      1
## 227450    396
cat(sprintf("Number of positive samples in training data: %i (%.2f%% of total)",
            counts["1"], 100 * counts["1"] / sum(counts)))
## Number of positive samples in training data: 396 (0.17% of total)
weight_for_0 = 1 / counts["0"]
weight_for_1 = 1 / counts["1"]

Normalize the data using training set statistics

feature_names <- colnames(train_df) %>% setdiff("Class")

train_features <- as.matrix(train_df[feature_names])
train_targets <- as.matrix(train_df$Class)

val_features <- as.matrix(val_df[feature_names])
val_targets <- as.matrix(val_df$Class)

train_features %<>% scale()
val_features %<>% scale(center = attr(train_features, "scaled:center"),
                        scale = attr(train_features, "scaled:scale"))

Build a binary classification model

model <-
  keras_model_sequential(input_shape = ncol(train_features)) |>
  layer_dense(256, activation = "relu") |>
  layer_dense(256, activation = "relu") |>
  layer_dropout(0.3) |>
  layer_dense(256, activation = "relu") |>
  layer_dropout(0.3) |>
  layer_dense(1, activation = "sigmoid")

model
## Model: "sequential"
## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
## ┃ Layer (type)                     Output Shape                  Param # 
## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
## │ dense (Dense)                   │ (None, 256)            │         7,936
## ├─────────────────────────────────┼────────────────────────┼───────────────┤
## │ dense_1 (Dense)                 │ (None, 256)            │        65,792
## ├─────────────────────────────────┼────────────────────────┼───────────────┤
## │ dropout (Dropout)               │ (None, 256)            │             0
## ├─────────────────────────────────┼────────────────────────┼───────────────┤
## │ dense_2 (Dense)                 │ (None, 256)            │        65,792
## ├─────────────────────────────────┼────────────────────────┼───────────────┤
## │ dropout_1 (Dropout)             │ (None, 256)            │             0
## ├─────────────────────────────────┼────────────────────────┼───────────────┤
## │ dense_3 (Dense)                 │ (None, 1)              │           257
## └─────────────────────────────────┴────────────────────────┴───────────────┘
##  Total params: 139,777 (546.00 KB)
##  Trainable params: 139,777 (546.00 KB)
##  Non-trainable params: 0 (0.00 B)

Train the model with class_weight argument

metrics <- list(
  metric_false_negatives(name = "fn"),
  metric_false_positives(name = "fp"),
  metric_true_negatives(name = "tn"),
  metric_true_positives(name = "tp"),
  metric_precision(name = "precision"),
  metric_recall(name = "recall")
)
model |> compile(
  optimizer = optimizer_adam(1e-2),
  loss = "binary_crossentropy",
  metrics = metrics
)
callbacks <- list(
  callback_model_checkpoint("fraud_model_at_epoch_{epoch}.keras")
)

class_weight <- list("0" = weight_for_0,
                     "1" = weight_for_1)

model |> fit(
  train_features, train_targets,
  validation_data = list(val_features, val_targets),
  class_weight = class_weight,
  batch_size = 2048,
  epochs = 30,
  callbacks = callbacks,
  verbose = 2
)
## Epoch 1/30
## 112/112 - 3s - 27ms/step - fn: 40.0000 - fp: 23705.0000 - loss: 2.2728e-06 - precision: 0.0148 - recall: 0.8990 - tn: 203745.0000 - tp: 356.0000 - val_fn: 8.0000 - val_fp: 1502.0000 - val_loss: 0.1089 - val_precision: 0.0553 - val_recall: 0.9167 - val_tn: 55363.0000 - val_tp: 88.0000
## Epoch 2/30
## 112/112 - 1s - 6ms/step - fn: 33.0000 - fp: 8009.0000 - loss: 1.4949e-06 - precision: 0.0434 - recall: 0.9167 - tn: 219441.0000 - tp: 363.0000 - val_fn: 10.0000 - val_fp: 1176.0000 - val_loss: 0.0912 - val_precision: 0.0681 - val_recall: 0.8958 - val_tn: 55689.0000 - val_tp: 86.0000
## Epoch 3/30
## 112/112 - 0s - 2ms/step - fn: 32.0000 - fp: 7704.0000 - loss: 1.3369e-06 - precision: 0.0451 - recall: 0.9192 - tn: 219746.0000 - tp: 364.0000 - val_fn: 9.0000 - val_fp: 1202.0000 - val_loss: 0.0870 - val_precision: 0.0675 - val_recall: 0.9062 - val_tn: 55663.0000 - val_tp: 87.0000
## Epoch 4/30
## 112/112 - 0s - 2ms/step - fn: 28.0000 - fp: 8366.0000 - loss: 1.2269e-06 - precision: 0.0421 - recall: 0.9293 - tn: 219084.0000 - tp: 368.0000 - val_fn: 10.0000 - val_fp: 1560.0000 - val_loss: 0.0967 - val_precision: 0.0522 - val_recall: 0.8958 - val_tn: 55305.0000 - val_tp: 86.0000
## Epoch 5/30
## 112/112 - 0s - 2ms/step - fn: 19.0000 - fp: 6217.0000 - loss: 8.5492e-07 - precision: 0.0572 - recall: 0.9520 - tn: 221233.0000 - tp: 377.0000 - val_fn: 9.0000 - val_fp: 2626.0000 - val_loss: 0.1236 - val_precision: 0.0321 - val_recall: 0.9062 - val_tn: 54239.0000 - val_tp: 87.0000
## Epoch 6/30
## 112/112 - 0s - 2ms/step - fn: 18.0000 - fp: 6566.0000 - loss: 7.4300e-07 - precision: 0.0544 - recall: 0.9545 - tn: 220884.0000 - tp: 378.0000 - val_fn: 8.0000 - val_fp: 2302.0000 - val_loss: 0.1081 - val_precision: 0.0368 - val_recall: 0.9167 - val_tn: 54563.0000 - val_tp: 88.0000
## Epoch 7/30
## 112/112 - 0s - 2ms/step - fn: 14.0000 - fp: 6819.0000 - loss: 7.1838e-07 - precision: 0.0530 - recall: 0.9646 - tn: 220631.0000 - tp: 382.0000 - val_fn: 7.0000 - val_fp: 1376.0000 - val_loss: 0.0800 - val_precision: 0.0608 - val_recall: 0.9271 - val_tn: 55489.0000 - val_tp: 89.0000
## Epoch 8/30
## 112/112 - 0s - 2ms/step - fn: 19.0000 - fp: 7345.0000 - loss: 8.3921e-07 - precision: 0.0488 - recall: 0.9520 - tn: 220105.0000 - tp: 377.0000 - val_fn: 10.0000 - val_fp: 416.0000 - val_loss: 0.0406 - val_precision: 0.1713 - val_recall: 0.8958 - val_tn: 56449.0000 - val_tp: 86.0000
## Epoch 9/30
## 112/112 - 0s - 2ms/step - fn: 13.0000 - fp: 6282.0000 - loss: 8.3488e-07 - precision: 0.0575 - recall: 0.9672 - tn: 221168.0000 - tp: 383.0000 - val_fn: 10.0000 - val_fp: 933.0000 - val_loss: 0.0450 - val_precision: 0.0844 - val_recall: 0.8958 - val_tn: 55932.0000 - val_tp: 86.0000
## Epoch 10/30
## 112/112 - 0s - 2ms/step - fn: 15.0000 - fp: 7597.0000 - loss: 1.1098e-06 - precision: 0.0478 - recall: 0.9621 - tn: 219853.0000 - tp: 381.0000 - val_fn: 10.0000 - val_fp: 2417.0000 - val_loss: 0.4261 - val_precision: 0.0344 - val_recall: 0.8958 - val_tn: 54448.0000 - val_tp: 86.0000
## Epoch 11/30
## 112/112 - 0s - 2ms/step - fn: 24.0000 - fp: 10269.0000 - loss: 1.9062e-06 - precision: 0.0350 - recall: 0.9394 - tn: 217181.0000 - tp: 372.0000 - val_fn: 9.0000 - val_fp: 1434.0000 - val_loss: 0.1261 - val_precision: 0.0572 - val_recall: 0.9062 - val_tn: 55431.0000 - val_tp: 87.0000
## Epoch 12/30
## 112/112 - 0s - 2ms/step - fn: 17.0000 - fp: 6634.0000 - loss: 1.0711e-06 - precision: 0.0540 - recall: 0.9571 - tn: 220816.0000 - tp: 379.0000 - val_fn: 8.0000 - val_fp: 2263.0000 - val_loss: 0.1654 - val_precision: 0.0374 - val_recall: 0.9167 - val_tn: 54602.0000 - val_tp: 88.0000
## Epoch 13/30
## 112/112 - 0s - 2ms/step - fn: 10.0000 - fp: 5620.0000 - loss: 5.3600e-07 - precision: 0.0643 - recall: 0.9747 - tn: 221830.0000 - tp: 386.0000 - val_fn: 12.0000 - val_fp: 914.0000 - val_loss: 0.0414 - val_precision: 0.0842 - val_recall: 0.8750 - val_tn: 55951.0000 - val_tp: 84.0000
## Epoch 14/30
## 112/112 - 0s - 2ms/step - fn: 8.0000 - fp: 5103.0000 - loss: 5.1168e-07 - precision: 0.0707 - recall: 0.9798 - tn: 222347.0000 - tp: 388.0000 - val_fn: 11.0000 - val_fp: 1382.0000 - val_loss: 0.0660 - val_precision: 0.0579 - val_recall: 0.8854 - val_tn: 55483.0000 - val_tp: 85.0000
## Epoch 15/30
## 112/112 - 0s - 2ms/step - fn: 7.0000 - fp: 4497.0000 - loss: 4.4632e-07 - precision: 0.0796 - recall: 0.9823 - tn: 222953.0000 - tp: 389.0000 - val_fn: 10.0000 - val_fp: 1805.0000 - val_loss: 0.0786 - val_precision: 0.0455 - val_recall: 0.8958 - val_tn: 55060.0000 - val_tp: 86.0000
## Epoch 16/30
## 112/112 - 0s - 2ms/step - fn: 5.0000 - fp: 5056.0000 - loss: 6.1038e-07 - precision: 0.0718 - recall: 0.9874 - tn: 222394.0000 - tp: 391.0000 - val_fn: 13.0000 - val_fp: 889.0000 - val_loss: 0.0676 - val_precision: 0.0854 - val_recall: 0.8646 - val_tn: 55976.0000 - val_tp: 83.0000
## Epoch 17/30
## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 4639.0000 - loss: 4.5763e-07 - precision: 0.0781 - recall: 0.9924 - tn: 222811.0000 - tp: 393.0000 - val_fn: 11.0000 - val_fp: 1185.0000 - val_loss: 0.0669 - val_precision: 0.0669 - val_recall: 0.8854 - val_tn: 55680.0000 - val_tp: 85.0000
## Epoch 18/30
## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 4355.0000 - loss: 4.5299e-07 - precision: 0.0826 - recall: 0.9899 - tn: 223095.0000 - tp: 392.0000 - val_fn: 9.0000 - val_fp: 1405.0000 - val_loss: 0.0772 - val_precision: 0.0583 - val_recall: 0.9062 - val_tn: 55460.0000 - val_tp: 87.0000
## Epoch 19/30
## 112/112 - 0s - 2ms/step - fn: 6.0000 - fp: 5779.0000 - loss: 4.8324e-07 - precision: 0.0632 - recall: 0.9848 - tn: 221671.0000 - tp: 390.0000 - val_fn: 10.0000 - val_fp: 1135.0000 - val_loss: 0.0546 - val_precision: 0.0704 - val_recall: 0.8958 - val_tn: 55730.0000 - val_tp: 86.0000
## Epoch 20/30
## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 4748.0000 - loss: 3.7424e-07 - precision: 0.0764 - recall: 0.9924 - tn: 222702.0000 - tp: 393.0000 - val_fn: 9.0000 - val_fp: 895.0000 - val_loss: 0.0396 - val_precision: 0.0886 - val_recall: 0.9062 - val_tn: 55970.0000 - val_tp: 87.0000
## Epoch 21/30
## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 3798.0000 - loss: 3.1417e-07 - precision: 0.0938 - recall: 0.9924 - tn: 223652.0000 - tp: 393.0000 - val_fn: 10.0000 - val_fp: 652.0000 - val_loss: 0.0309 - val_precision: 0.1165 - val_recall: 0.8958 - val_tn: 56213.0000 - val_tp: 86.0000
## Epoch 22/30
## 112/112 - 0s - 2ms/step - fn: 0.0000e+00 - fp: 2188.0000 - loss: 1.8112e-07 - precision: 0.1533 - recall: 1.0000 - tn: 225262.0000 - tp: 396.0000 - val_fn: 10.0000 - val_fp: 427.0000 - val_loss: 0.0250 - val_precision: 0.1676 - val_recall: 0.8958 - val_tn: 56438.0000 - val_tp: 86.0000
## Epoch 23/30
## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 3066.0000 - loss: 3.4932e-07 - precision: 0.1134 - recall: 0.9899 - tn: 224384.0000 - tp: 392.0000 - val_fn: 11.0000 - val_fp: 1053.0000 - val_loss: 0.0494 - val_precision: 0.0747 - val_recall: 0.8854 - val_tn: 55812.0000 - val_tp: 85.0000
## Epoch 24/30
## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 3175.0000 - loss: 5.1994e-07 - precision: 0.1099 - recall: 0.9899 - tn: 224275.0000 - tp: 392.0000 - val_fn: 11.0000 - val_fp: 1012.0000 - val_loss: 0.0997 - val_precision: 0.0775 - val_recall: 0.8854 - val_tn: 55853.0000 - val_tp: 85.0000
## Epoch 25/30
## 112/112 - 0s - 2ms/step - fn: 7.0000 - fp: 5874.0000 - loss: 6.5862e-07 - precision: 0.0621 - recall: 0.9823 - tn: 221576.0000 - tp: 389.0000 - val_fn: 9.0000 - val_fp: 3090.0000 - val_loss: 0.1296 - val_precision: 0.0274 - val_recall: 0.9062 - val_tn: 53775.0000 - val_tp: 87.0000
## Epoch 26/30
## 112/112 - 0s - 2ms/step - fn: 6.0000 - fp: 5395.0000 - loss: 5.9253e-07 - precision: 0.0674 - recall: 0.9848 - tn: 222055.0000 - tp: 390.0000 - val_fn: 9.0000 - val_fp: 1320.0000 - val_loss: 0.0928 - val_precision: 0.0618 - val_recall: 0.9062 - val_tn: 55545.0000 - val_tp: 87.0000
## Epoch 27/30
## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 3887.0000 - loss: 7.1278e-07 - precision: 0.0916 - recall: 0.9899 - tn: 223563.0000 - tp: 392.0000 - val_fn: 12.0000 - val_fp: 572.0000 - val_loss: 0.0352 - val_precision: 0.1280 - val_recall: 0.8750 - val_tn: 56293.0000 - val_tp: 84.0000
## Epoch 28/30
## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 2209.0000 - loss: 5.8153e-07 - precision: 0.1510 - recall: 0.9924 - tn: 225241.0000 - tp: 393.0000 - val_fn: 11.0000 - val_fp: 1373.0000 - val_loss: 0.0808 - val_precision: 0.0583 - val_recall: 0.8854 - val_tn: 55492.0000 - val_tp: 85.0000
## Epoch 29/30
## 112/112 - 0s - 2ms/step - fn: 1.0000 - fp: 2506.0000 - loss: 2.9031e-07 - precision: 0.1362 - recall: 0.9975 - tn: 224944.0000 - tp: 395.0000 - val_fn: 11.0000 - val_fp: 690.0000 - val_loss: 0.0401 - val_precision: 0.1097 - val_recall: 0.8854 - val_tn: 56175.0000 - val_tp: 85.0000
## Epoch 30/30
## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 2719.0000 - loss: 6.1097e-07 - precision: 0.1260 - recall: 0.9899 - tn: 224731.0000 - tp: 392.0000 - val_fn: 13.0000 - val_fp: 861.0000 - val_loss: 0.0362 - val_precision: 0.0879 - val_recall: 0.8646 - val_tn: 56004.0000 - val_tp: 83.0000
val_pred <- model %>%
  predict(val_features) %>%
  { as.integer(. > 0.5) }
## 1781/1781 - 1s - 311us/step
pred_correct <- val_df$Class == val_pred
cat(sprintf("Validation accuracy: %.2f", mean(pred_correct)))
## Validation accuracy: 0.98
fraudulent <- val_df$Class == 1

n_fraudulent_detected <- sum(fraudulent & pred_correct)
n_fraudulent_missed <- sum(fraudulent & !pred_correct)
n_legitimate_flagged <- sum(!fraudulent & !pred_correct)

Conclusions

At the end of training, out of 56,961 validation transactions, we are:

  • Correctly identifying 83 of them as fraudulent
  • Missing 13 fraudulent transactions
  • At the cost of incorrectly flagging 861 legitimate transactions

In the real world, one would put an even higher weight on class 1, so as to reflect that False Negatives are more costly than False Positives.

Next time your credit card gets declined in an online purchase – this is why.