Credit card fraud detection 更


這次探勘的主題是信用卡的詐騙偵測,我們將根據資料的特性對於個案最後是否為詐騙人士,
資料一樣來自Kaggle
https://www.kaggle.com/mlg-ulb/creditcardfraud
和過往資料不同的是目標的比例非常懸殊,畢竟詐騙集團的新聞雖然常見,但說老實話,和一般小老百姓作比例的話,也算是冰山一小角,這樣的資料難點是在於如何在詐騙比例很少的情況下仍然能得到充分的訓練(Training),詐騙份數少容易導致模型訓練太多非詐騙的資料集進而影響最後Testing的結果。

面對這樣的情況,最直接的作法是
1. 再去收集小比例的資料(廢話?)
2. 缺樣本,那我們反覆抽取更多給它 (Over Sample)
3. 缺樣本,那我們讓高比例的取樣相對沒那麼多就好(Under Sample)
4. SMOTE (Synthetic Minority Oversampling Technique) ,最有名的樣本合成方法,
原理跟KNN一樣,找取最鄰近的詐騙樣本,生成新的樣本。
(有關Sampling : https://www.zhihu.com/question/269698662)
(有關SMOTE: https://reurl.cc/5gDgX7)

方法1有現實上的困難
方法2的缺點是過分強調詐騙的比例可能會導致Overfitting
方法3的缺點是捨棄大部分的數據可能因此失去非詐騙比例可以收集的資訊
方法4雖然有不錯的分類效果,但是一樣有過份強調的後果導致最後模型失真,一樣有Overffitting 的風險。

每個方法都有各自的風險,而本次分析決定採用:
1. Over-Undersampling,將詐騙比例oversampling;再將非詐騙比例undersampling
2. SMOTE生成樣本後進行分析
3. 不對資料集作處理直接分析


直接開始分析吧!


library(tidyverse) # everything
library(reshape2) # melting tables
library(caret) # training, cross-validation, hyperparameter search
library(xgboost) # XGBoost
library(Matrix) # XGBoost sparse matrices
library(tictoc) # timing models
install.packages('PRROC')
library(PRROC) # AUROC and AUPRC
library(smotefamily) # synthetic minority over-sampling
library(ROSE) # random majority under-sampling
library(gridExtra) # combining graphs

summary(data)
     Time              V1                  V2                  V3          
 Min.   :     0   Min.   :-56.40751   Min.   :-72.71573   Min.   :-48.3256  
 1st Qu.: 54202   1st Qu.: -0.92037   1st Qu.: -0.59855   1st Qu.: -0.8904  
 Median : 84692   Median :  0.01811   Median :  0.06549   Median :  0.1799  
 Mean   : 94814   Mean   :  0.00000   Mean   :  0.00000   Mean   :  0.0000  
 3rd Qu.:139321   3rd Qu.:  1.31564   3rd Qu.:  0.80372   3rd Qu.:  1.0272  
 Max.   :172792   Max.   :  2.45493   Max.   : 22.05773   Max.   :  9.3826  

     V27                  V28                Amount        
 Min.   :-22.565679   Min.   :-15.43008   Min.   :    0.00  
 1st Qu.: -0.070840   1st Qu.: -0.05296   1st Qu.:    5.60  
 Median :  0.001342   Median :  0.01124   Median :   22.00  
 Mean   :  0.000000   Mean   :  0.00000   Mean   :   88.35  
 3rd Qu.:  0.091045   3rd Qu.:  0.07828   3rd Qu.:   77.17  
 Max.   : 31.612198   Max.   : 33.84781   Max.   :25691.16  
     Class         
 Min.   :0.000000  
 1st Qu.:0.000000  
 Median :0.000000  
 Mean   :0.001728  
 3rd Qu.:0.000000  
 Max.   :1.000000  

這筆資料是經過PCA的手續生成的,因此變數都命名為V1~V28
Time則是由凌晨開始的起迄時間
Amount 交易數量
Class就是我們要預測的對象了, Mean 只有0.001728,比例很懸殊
同時也發現沒有NA值~

 >table(data$Class)

     0      1 
284315    492 
原始資料詐騙只有492項,非詐騙則是有28萬左右,天差地遠

整理一下時間的變數
#hour of day
data$hour_of_day <- (data$Time/3600) %% 24 


EDA:

1.我們發現詐騙案件大都發生在凌晨
2.詐騙數量跟詐騙案件沒太大的關係
#We found fraud occurs in midnight
ggplot(data, aes(hour_of_day, fill = Class, colour = Class)) +
  geom_density(alpha = 0.5) +
  xlab("Hour of day") + ylab("Density") +
  ggtitle("Density Plot of Hour of day: Fraud") +
  theme_bw()
data$Time <- NULL

#Amount :  not completely clear from the density graph
ggplot(data, aes(Amount, fill = as.factor(Class), colour = as.factor(Class))) +
  geom_density(alpha = 0.5) +
  scale_x_continuous(limits = c(0, 500), breaks = seq(0, 500, 100)) + 
  xlab("Transaction Amount") + ylab("Density") +
  ggtitle("Density Plot of Amount: Fraud") +
  theme_bw()





抽樣
1. 正常抽樣
#General Sampling(not rescale)
set.seed(715)
train_index <- createDataPartition(data$Class, p=0.75, list=FALSE)
train <- data[train_index, ] # training data (75% of data)
test <- data[-train_index, ] # testing data (25% of data)

2. Over and Under Sampling
#Over and under Sampling(not resscale)
bothsample_train <- ovun.sample(Class~., data = train, method = "both", N= nrow(data), p= 0.5, seed = 0)
both_train <- bothsample_train$data
table(both_train$Class)

3. SMOTE
#Over and under Sampling(not resscale)
smote_train <- SMOTE(X = train, target = train$Class, dup_size = 29)
smote_train <- smote_train$data %>% rename(Class = class)
smote_train <- smote_train[,1:31] #remove class

3.來看看不同抽樣後的target 比例,省一點時間直接寫成function吧
all.table = function(smote_train,both_train,train,test){
  smote <- table(smote_train$Class)
  bothsample <- table(both_train$Class)
  tr <- table(train$Class)
  te <- table(test$Class)
  return(c(smote,bothsample, tr, te))
}
all.table(smote_train,both_train,train,test)
> all.table(smote_train,both_train,train,test)
     0      1      0      1      0      1      0      1 
213222  11520 106593 107013 213222    384  71093    108 

建模
#Training-testing split

dtrain <- xgb.DMatrix(data=as.matrix(train[,-30]), label=train$Class)
dtest <- xgb.DMatrix(data=as.matrix(test[,-30]), label=test$Class)
dtrainboth <- xgb.DMatrix(data=as.matrix(both_train[,-30]), label=both_train$Class)
dtrainsm <- xgb.DMatrix(data=as.matrix(smote_train[,-30]), label=smote_train$Class)
#Modeling
#正常作
xg.model <- xgb.train(data = dtrain
                      , params = list(objective = "reg:logistic"
                                      , eta = 0.1
                                      , max.depth = 3
                                      , min_child_weight = 100
                                      , subsample = 1
                                      , colsample_bytree = 1
                                      , nthread = 3
                                      , eval_metric = "auc"
                      )
                      , watchlist = list(test =dtest)
                      , nrounds = 500
                      , early_stopping_rounds = 40
                      , print_every_n = 20)


#Over-Under Sampling
xg.modelboth <- xgb.train(data = dtrainboth
                      , params = list(objective = "reg:logistic"
                                      , eta = 0.1
                                      , max.depth = 3
                                      , min_child_weight = 100
                                      , subsample = 1
                                      , colsample_bytree = 1
                                      , nthread = 3
                                      , eval_metric = "auc"
                      )
                      , watchlist = list(test =dtestboth)
                      , nrounds = 500
                      , early_stopping_rounds = 40
                      , print_every_n = 20)

#SMOTE 
xg.SM <- xgb.train(data = dtrainsm
                          , params = list(objective = "reg:logistic"
                                          , eta = 0.1
                                          , max.depth = 3
                                          , min_child_weight = 100
                                          , subsample = 1
                                          , colsample_bytree = 1
                                          , nthread = 3
                                          , eval_metric = "auc"
                          )
                          , watchlist = list(test =dtest)
                          , nrounds = 500
                          , early_stopping_rounds = 40
                          , print_every_n = 20)

Importance:
重要的變數大致為V14 / V10 / V12 / V4 
#正常作
xgb.importance(xg.model, feature_names = colnames(dtrain))

 Feature         Gain        Cover   Frequency
1:     V14 3.993762e-01 0.3981632931 0.391061453
2:     V17 2.221651e-01 0.2703114949 0.094972067
3:     V10 2.152868e-01 0.1750372328 0.128491620
4:     V12 9.211315e-02 0.0370317266 0.067039106
5:      V4 7.030643e-02 0.0736593449 0.284916201
#Over-Under Sampling
> xgb.importance(xg.modelboth, feature_names = colnames(dtrainboth))

        Feature         Gain        Cover   Frequency
 1:         V14 0.6420629307 0.2537647822 0.124260355
 2:          V4 0.0920395329 0.1833874657 0.134122288
 3:         V10 0.0611494742 0.0581967596 0.053254438
 4:         V12 0.0568560307 0.1143986827 0.092702170
 5:          V7 0.0159940471 0.0449076399 0.053254438

#SMOTE
> xgb.importance(xg.modelboth, feature_names = colnames(dtrainboth))
        Feature         Gain        Cover   Frequency
 1:         V14 0.7123239680 0.4363474110 0.120575221
 2:          V4 0.0593578149 0.1976161923 0.130530973
 3:         V17 0.0382703385 0.1638692300 0.038716814
 4:         V10 0.0299795698 0.0158836222 0.033185841
 5:         V12 0.0210288143 0.0175175071 0.045353982

Best performance:
兩個模型的表現都差不多,AUC面積都有很優異的表現
#正常作
> xg.model[["best_score"]]
test-auc 
0.98436 
#Over-Under Sampling
> xg.modelboth[["best_score"]]
test-auc 
0.9896 
#SMOTE
> xg.SM[["best_score"]]
test-auc 
0.99503 

Prediction:
#Predicting and Result-Raw
xg_prediction <- predict(xg.model, dtest, type = "prob")
xg_result <- ifelse(xg_prediction >= 0.50,1,0)
result1<- cbind(xg_result,test)

#Predicting and Result-sample
xgboth_prediction <- predict(xg.modelboth, dtest, type = "prob")
xgboth_result <- ifelse(xgboth_prediction >= 0.50,1,0)
result2<- cbind(xgboth_result,test)

#Predicting and Result-SMOTE 
xgSM_prediction <- predict(xg.SM, dtest, type = "prob")
xgSM_result <- ifelse(xgSM_prediction >= 0.50,1,0)
result3<- cbind(xgSM_result,test)

Result: 
> table(result1$xg_result,result1$Class)
               
        0     1
  0 71075    20
  1    18    88

> table(result2$xgboth_result,result2$Class)
   
        0     1
  0 69965     7
  1  1128   101

> table(result3$xgSM_result,result3$Class)
   
        0     1
  0 71028     9
  1    65    99

在這裡插入個話題:

(Credit to :https://taweihuang.hpd.io/2018/12/28/imbalanced-data-performance-metrics/ )

三個模型都有很好的表現,但是未作任何處理的抽樣模型,卻有高比例的偽陰性,換句話說,它辨認盜刷的能力是非常差的,
此時我們換一個標準看看

I.召回度 (Recall) = TP / (TP + FN) : 所有為詐騙的個體中,被正確判斷為詐騙之比率:

未抽樣模型  : 88/(88+20)= 81% 
Over-Under模型  : 93.5 %
SMOTE : 91.6 %

II. 精確度 (Precision) = TP / (TP + FP),代表在所有被正確判斷為詐騙的個體中,確實為詐騙之比率

未抽樣模型  : 88/(88+18)= 83 % 
Over-Under模型  : 8.2 %
SMOTE : 60.3 %



結論:
       顯而易見,Accuracy當標準時,模型都表現得不錯,但當標準變成衡量Recall時,未作任何處理的模型,其實不能真的有效篩選出詐騙人士,反而是Over-Under Sampling 與 SMOTE生成樣本的兩種方法相對較能篩選出確實為詐騙的案例,這就是標準的寧可錯殺不可放過的最佳寫照!
     
           那兩種生成樣本的方法為什麼 Precision表現的不好?
        Over-Under sampling為重複抽樣,在函數中預設的比例為1:1,這意味著本來只有384個詐騙案例經過處理後變成107013個重複案例(差了很多倍阿!),我們將詐騙比例抬高的同時,就意味著類似屬性但非詐騙的無辜人士很容易被抓出來,這也是為什麼Over-Under在Precision表現的不好的原因。
        SMOTE則是類似KNN的過採樣生成方法,意味著如果參考到的詐騙標準已經偏差樣本,那他也會跟著生成偏差樣本,筆者認為SMOTE跟Over-Under的差別在於前者為演算法新生成,而後者為重複抽樣,因此SMOTE又在Precision表現得更好。

        人工增加 positive/negative的樣本,意味著 False positive/negative也會增加 !
       發現了嗎?當我們企圖用人工方法增加少數類別樣本的時候,就意味著False Positive的比例一定會提高,白話文一點,當我生出更多詐騙人士(Over & Under sampling)來訓練時,就已經註定我在現實生活中(testing data)會錯抓無辜人士。

       對於訓練結果的成效評估,必須時刻從商業與統計的角度去綜合判斷要用什麼樣的標準才能有最好的綜效,而本次筆者認為以SMOTE抽樣的模型有很好的Recall也不至於會抓太多無辜的人,是個綜合成效較佳的模型,當然,如果不怕錯抓的話Over-Under Sampling也可以試用~

      本次介紹的是準確度悖論的示範案例,慎選評估標準 / 抽樣方法,才能作出對組織/公司最有利的決策。


----------------------------------------------------------------------
註:
<最後>
這篇文章作出更正主要是因為發現上一次的作法是有問題的,Raw data應在切好training set後在作生成樣本,而非直接拿整組data進行生成,寫作時其實有想到這點,但看到kaggle上有人進行這樣的方式,也不疑有他,不過在自己想過以及向專業人士討教後,因此決定更改,以後在kaggle上看範例也要自己過濾一下,不過同時也感到開心,自己已經有過濾的能力了。
繼續加油 ^_^!

留言

這個網誌中的熱門文章

Word Vector & Word embedding 初探 - with n-Gram & GLOVE Model

文字探勘之關鍵字萃取 : TF-IDF , text-rank , RAKE

多元迴歸分析- subsets and shrinkage