Intro

About the Data

Dataset yang digunakan adalah dataset tentang kampanye pemasaran panggilan telepon langsung, yang bertujuan untuk mempromosikan deposito di antara pelanggan yang sudah ada, oleh lembaga perbankan Portugis. Kita akan menganalisis kampanye pemasaran terakhir yang dilakukan bank dan mengidentifikasi pola yang akan membantu dalam menemukan kesimpulan untuk mengembangkan strategi masa depan. Salah satunya ialah dengan memprediksi apakah seorang calon pelanggan akan setuju mengajukan program deposit atau tidak dengan kampanye yang telah dilakukan.

Preparation

library(dplyr)
library(tidyverse)
library(e1071)
library(rsample)
library(caret)

Read Data

bank <- read.csv("bank-full.csv", sep = ";")

head(bank)
#>   age          job marital education default balance housing loan contact day
#> 1  58   management married  tertiary      no    2143     yes   no unknown   5
#> 2  44   technician  single secondary      no      29     yes   no unknown   5
#> 3  33 entrepreneur married secondary      no       2     yes  yes unknown   5
#> 4  47  blue-collar married   unknown      no    1506     yes   no unknown   5
#> 5  33      unknown  single   unknown      no       1      no   no unknown   5
#> 6  35   management married  tertiary      no     231     yes   no unknown   5
#>   month duration campaign pdays previous poutcome  y
#> 1   may      261        1    -1        0  unknown no
#> 2   may      151        1    -1        0  unknown no
#> 3   may       76        1    -1        0  unknown no
#> 4   may       92        1    -1        0  unknown no
#> 5   may      198        1    -1        0  unknown no
#> 6   may      139        1    -1        0  unknown no

Deskripsi kolom:

  • age: umur
  • job: kategori pekerjaan
  • marital: status menikah
  • education: tingkat pendidikan
  • default: apakah memiliki kredit gagal bayar (default)?
  • balance: uang yang tersimpan dalam rekening
  • housing: apakah memiliki kredit rumah?
  • loan: apakah memiliki kredit pribadi?
  • contact: metode kontak/telefon
  • day: day-of-month dari kontak terakhir
  • month: bulan dari kontak terakhir
  • duration: durasi kontak pada campaign ini
  • campaign: jumlah kontak yang dilakukan pada campaign ini
  • pdays: jumlah hari berlalu setelah kontak dari campaign sebelumnya
  • previous: jumlah kontak yang dilakukan pada campaign sebelumnya
  • poutcome: outcome dari campaign sebelumnya
  • y: outcome dari campaign ini (target variable)

Data Wrangling

Ada beberapa kolom yang perlu dihilangkan, yaitu :

  • kolom month -> tidak penting, karena hanya menunjukkan apakah calon nasabah tersebut pernah ditawarkan atau tidak
  • kolom contact -> tidak penting, karena hanya menunjukan alat yang digunakan dalam melakukan penawran
  • kolom pdays -> - tidak penting, karena intuisinya sama saja dengan month, untuk mengetahui apakah calon nasabah tersebut pernah di hubungi atau tidak. Pada kolom ini juga terdapat banyak nilai negatif, dan nilai negatif bisa kita indikasikan sebagai nilai yang salah diinput
  • kolom day -> tidak penting, intuisinya sama dengan kolom month
  • kolom duration -> Data durasi baru kita dapatkan setelah penelfonan terjadi, dan pada data kedepannya kolom tersebut tidak kita miliki
  • kolom poutcome -> karena berisikan informasi unknown yang cukup banyak
bank_clean <- bank %>% 
  select(-c(month, contact, pdays, day, duration, poutcome))

colnames(bank_clean)  
#>  [1] "age"       "job"       "marital"   "education" "default"   "balance"  
#>  [7] "housing"   "loan"      "campaign"  "previous"  "y"

Kolom yang tidak perlu sudah berhasil dikeluarkan. Sekarang kita cek tipe data apakah sudah sesuai.

bank_clean %>% 
  glimpse()
#> Rows: 45,211
#> Columns: 11
#> $ age       <int> 58, 44, 33, 47, 33, 35, 28, 42, 58, 43, 41, 29, 53, 58, 57, …
#> $ job       <chr> "management", "technician", "entrepreneur", "blue-collar", "…
#> $ marital   <chr> "married", "single", "married", "married", "single", "marrie…
#> $ education <chr> "tertiary", "secondary", "secondary", "unknown", "unknown", …
#> $ default   <chr> "no", "no", "no", "no", "no", "no", "no", "yes", "no", "no",…
#> $ balance   <int> 2143, 29, 2, 1506, 1, 231, 447, 2, 121, 593, 270, 390, 6, 71…
#> $ housing   <chr> "yes", "yes", "yes", "yes", "no", "yes", "yes", "yes", "yes"…
#> $ loan      <chr> "no", "no", "yes", "no", "no", "no", "yes", "no", "no", "no"…
#> $ campaign  <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
#> $ previous  <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ y         <chr> "no", "no", "no", "no", "no", "no", "no", "no", "no", "no", …

Terlihat ada beberapa feature yang bertipe data character. Maka dari itu, kita akan diubah menjadi tipe data factor.

bank_clean <- bank_clean %>% 
  mutate_if(is.character, as.factor)

glimpse(bank_clean)
#> Rows: 45,211
#> Columns: 11
#> $ age       <int> 58, 44, 33, 47, 33, 35, 28, 42, 58, 43, 41, 29, 53, 58, 57, …
#> $ job       <fct> management, technician, entrepreneur, blue-collar, unknown, …
#> $ marital   <fct> married, single, married, married, single, married, single, …
#> $ education <fct> tertiary, secondary, secondary, unknown, unknown, tertiary, …
#> $ default   <fct> no, no, no, no, no, no, no, yes, no, no, no, no, no, no, no,…
#> $ balance   <int> 2143, 29, 2, 1506, 1, 231, 447, 2, 121, 593, 270, 390, 6, 71…
#> $ housing   <fct> yes, yes, yes, yes, no, yes, yes, yes, yes, yes, yes, yes, y…
#> $ loan      <fct> no, no, yes, no, no, no, yes, no, no, no, no, no, no, no, no…
#> $ campaign  <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
#> $ previous  <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
#> $ y         <fct> no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, …

Exploratory Data Analysis

Cek apakah terdapat missing value pada data.

bank_clean %>% 
  anyNA
#> [1] FALSE

Tidak terdapat missing value pada data. Maka dapat dilanjutkan ke proses berikutnya.

Cross Validation

set.seed(417)
split <- initial_split(data = bank_clean, prop = 0.8, strata = "y")

data_train <- training(split)
data_test <- testing(split)
prop.table(table(data_train$y))
#> 
#>        no       yes 
#> 0.8830181 0.1169819

Terlihat bahwa proporsi kelas target belum seimbang. Maka dilakukan downsample, yaitu mengurangi kelas mayoritas hingga seimbang dengan minoritas.

RNGkind(sample.kind = "Rounding")
set.seed(100)

down_train <- downSample(x = data_train %>% select(-y),
                         y = data_train$y,
                         yname = "y")
prop.table(table(down_train$y))
#> 
#>  no yes 
#> 0.5 0.5

Data sudah seimbang. Maka dapat dilakukan proses analisis selanjutnya.

Modelling

Naive Bayes

Naive Bayes memanfaatkan kejadian dependent events, namun menerapkan asumsi Naive pada hubungan antar prediktornya:
1. Hubungan prediktor dengan target variabel saling dependen
2. Hubungan antar prediktor saling independen

model_naive <- naiveBayes(y ~ .,
                          data = down_train)

Setelah membuat model, kita lakukan prediksi.

pred_naive <- predict(object = model_naive,
                newdata = data_test,
                type = "class")

Evaluasi Model

conf_matrix_naive <- table(pred_naive, data_test$y)
conf_matrix_naive
#>           
#> pred_naive   no  yes
#>        no  3379  224
#>        yes 4606  834

Hasil ConfusionMatrix menunjukkan bahwa klasifikasi Naive Bayes memperkirakan dengan benar 3379 orang tidak setuju mengajukan program deposit dan 224 prediksi salah. Demikian pula, model memprediksi dengan benar 834 orang setuju mengajukan program deposit dan 4606 prediksi salah. Bagimana dengan tingkat recall/sensitivity?? Mari kita lihat dibawah ini.

confusionMatrix(data = pred_naive,
                reference = data_test$y,
                positive = "yes")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction   no  yes
#>        no  3379  224
#>        yes 4606  834
#>                                           
#>                Accuracy : 0.4659          
#>                  95% CI : (0.4556, 0.4762)
#>     No Information Rate : 0.883           
#>     P-Value [Acc > NIR] : 1               
#>                                           
#>                   Kappa : 0.0756          
#>                                           
#>  Mcnemar's Test P-Value : <2e-16          
#>                                           
#>             Sensitivity : 0.78828         
#>             Specificity : 0.42317         
#>          Pos Pred Value : 0.15331         
#>          Neg Pred Value : 0.93783         
#>              Prevalence : 0.11700         
#>          Detection Rate : 0.09223         
#>    Detection Prevalence : 0.60157         
#>       Balanced Accuracy : 0.60572         
#>                                           
#>        'Positive' Class : yes             
#> 

Dari output model klasifikasi Naive Bayes, terlihat bahwa tingkat akurasi sebesar 67.8% dengan nilai recall/sensitivitynya sebesar 59.07%.

Selanjutnya, mari kita uji menggunakan model Decision Tree.

Decision Tree

Decision Tree merupakan tree-based model yang cukup sederhana dengan performa yang robust/powerful untuk prediksi. Decision Tree menghasilkan visualisasi berupa pohon keputusan yang dapat diinterpretasi dengan mudah.

library(partykit)

model_dtree <- ctree(formula = y ~.,
                     data = down_train)

plot(model_dtree, type = "simple")

Setelah membuat model, kita lakukan prediksi.

train_pred_dtree <- predict(object = model_dtree,
                      newdata = down_train,
                      type = "response")

test_pred_dtree <- predict(object = model_dtree,
                      newdata = data_test,
                      type = "response")

Evaluasi Model
Pada evaluasi model kali ini, kita akan membandingkan peforma pada data train dan data test.

# Confusion Matrix: data train

confusionMatrix(data = train_pred_dtree,
                reference = down_train$y,
                positive = "yes")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction   no  yes
#>        no  3157 1817
#>        yes 1074 2414
#>                                           
#>                Accuracy : 0.6584          
#>                  95% CI : (0.6481, 0.6685)
#>     No Information Rate : 0.5             
#>     P-Value [Acc > NIR] : < 2.2e-16       
#>                                           
#>                   Kappa : 0.3167          
#>                                           
#>  Mcnemar's Test P-Value : < 2.2e-16       
#>                                           
#>             Sensitivity : 0.5706          
#>             Specificity : 0.7462          
#>          Pos Pred Value : 0.6921          
#>          Neg Pred Value : 0.6347          
#>              Prevalence : 0.5000          
#>          Detection Rate : 0.2853          
#>    Detection Prevalence : 0.4122          
#>       Balanced Accuracy : 0.6584          
#>                                           
#>        'Positive' Class : yes             
#> 
# Confusion Matrix: data test

confusionMatrix(data = test_pred_dtree,
                reference = data_test$y,
                positive = "yes")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction   no  yes
#>        no  5838  467
#>        yes 2147  591
#>                                           
#>                Accuracy : 0.7109          
#>                  95% CI : (0.7015, 0.7203)
#>     No Information Rate : 0.883           
#>     P-Value [Acc > NIR] : 1               
#>                                           
#>                   Kappa : 0.1716          
#>                                           
#>  Mcnemar's Test P-Value : <2e-16          
#>                                           
#>             Sensitivity : 0.55860         
#>             Specificity : 0.73112         
#>          Pos Pred Value : 0.21585         
#>          Neg Pred Value : 0.92593         
#>              Prevalence : 0.11700         
#>          Detection Rate : 0.06535         
#>    Detection Prevalence : 0.30278         
#>       Balanced Accuracy : 0.64486         
#>                                           
#>        'Positive' Class : yes             
#> 

Kita akan menggunakan metrics recall/sensitivity sebab lebih memilih benar diprediksi positif, dari yang reality-nya (aktualnya) positif. Atau dengan kata lain, tidak mau ada positif yang tidak terprediksi positif.
Dapat kita lihat performa metricsnya :

  • data train : 0.5706
  • data test : 0.55860

Perbedaannya sebesar 1.2%. Dalam hal ini, performa model konsisten di data train maupun data test.
Karena nilai recallnya masih belum cukup tinggi, maka akan dilakukan tunning model menggunakan tree prunning.

model_dtree_tuned <- ctree(formula = y ~.,
                     data = down_train,
                     control = ctree_control(mincriterion = 0.97,
                                             minsplit = 30,
                                             minbucket = 50))

plot(model_dtree_tuned, type = "simple")

# prediksi ke data train
train_pred_tuned <- predict(object = model_dtree_tuned,
                            newdata = down_train, 
                            type = "response")

# prediksi ke data test
test_pred_tuned <- predict(object = model_dtree_tuned,
                            newdata = data_test, 
                            type = "response")
confusionMatrix(data = train_pred_tuned,
                reference = down_train$y,
                positive = "yes")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction   no  yes
#>        no  3072 1746
#>        yes 1159 2485
#>                                           
#>                Accuracy : 0.6567          
#>                  95% CI : (0.6465, 0.6668)
#>     No Information Rate : 0.5             
#>     P-Value [Acc > NIR] : < 2.2e-16       
#>                                           
#>                   Kappa : 0.3134          
#>                                           
#>  Mcnemar's Test P-Value : < 2.2e-16       
#>                                           
#>             Sensitivity : 0.5873          
#>             Specificity : 0.7261          
#>          Pos Pred Value : 0.6819          
#>          Neg Pred Value : 0.6376          
#>              Prevalence : 0.5000          
#>          Detection Rate : 0.2937          
#>    Detection Prevalence : 0.4306          
#>       Balanced Accuracy : 0.6567          
#>                                           
#>        'Positive' Class : yes             
#> 
confusionMatrix(data = test_pred_tuned,
                reference = data_test$y,
                positive = "yes")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction   no  yes
#>        no  5650  446
#>        yes 2335  612
#>                                          
#>                Accuracy : 0.6925         
#>                  95% CI : (0.6828, 0.702)
#>     No Information Rate : 0.883          
#>     P-Value [Acc > NIR] : 1              
#>                                          
#>                   Kappa : 0.1612         
#>                                          
#>  Mcnemar's Test P-Value : <2e-16         
#>                                          
#>             Sensitivity : 0.57845        
#>             Specificity : 0.70758        
#>          Pos Pred Value : 0.20767        
#>          Neg Pred Value : 0.92684        
#>              Prevalence : 0.11700        
#>          Detection Rate : 0.06768        
#>    Detection Prevalence : 0.32589        
#>       Balanced Accuracy : 0.64301        
#>                                          
#>        'Positive' Class : yes            
#> 

Setelah dilakukan proses tree pruning, diperoleh nilai recall pada data train sebesar 0.5873 sedangkan pada data testnya sebesar 0.57845. Pada tuning model ini, nilai recallnya tidak mengalami kenaikan yang signifikan. Maka dari itu, akan dicoba model berikutnya yaitu Random Forest.

Random Forest

Random Forest adalah salah satu jenis Ensemble Method yang terdiri dari banyak Decision Tree. Masing-masing Decision Tree memiliki karakteristik masing-masing dan tidak saling berhubungan. Random Forest memanfaatkan konsep Bagging (Bootstrap and Aggregation) dalam pembuatannya.

Cross Validation

library(rsample)
RNGkind(sample.kind = "Rounding")
set.seed(100)

index_bank <- sample(nrow(bank_clean), nrow(bank_clean)*0.8)

train_bank <- bank_clean[index_bank,]

test_bank <- bank_clean[-index_bank,]

K-Fold Cross Validation

Biasanya kita melakukan cross validation dengan membagi data menjadi data train dan test. K-Fold Cross Validation membagi data sebanyak k bagian sama banyak, sehingga tiap bagian sempat dijadikan data train dan data test.

RNGkind(sample.kind = "Rounding")
set.seed(100)

ctrl <- trainControl(method = "repeatedcv",
                     number = 5, # k-fold
                     repeats = 3) # repetisi

Model Random Forest

Kekurangan dari Random Forest adalah membutuhkan waktu komputasi yang cukup lama. Maka dari itu kita akan membuat modelnya terlebih dahulu, kemudian menyimpannya ke dalam format .rds

# model_rf <- train(y ~.,
#                   data = train_bank,
#                   method = "rf",
#                   trcontrol = ctrl)
# 
# saveRDS(model_rf, "model_rf.RDS")

Read Model

model_bank_rf <- readRDS("model_rf.RDS")
model_bank_rf
#> Random Forest 
#> 
#> 36168 samples
#>    10 predictor
#>     2 classes: 'no', 'yes' 
#> 
#> No pre-processing
#> Resampling: Bootstrapped (25 reps) 
#> Summary of sample sizes: 36168, 36168, 36168, 36168, 36168, 36168, ... 
#> Resampling results across tuning parameters:
#> 
#>   mtry  Accuracy   Kappa       
#>    2    0.8831878  0.0002680743
#>   12    0.8774059  0.1737377125
#>   23    0.8734002  0.1770089073
#> 
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was mtry = 2.

Interpretasi pada summary model di atas :
Dilakukan beberapa kali percobaan mtry (jumlah prediktor random yang digunakan saat splitting node).
Secara default akan dicoba sebanyak 3 nilai mtry, yaitu :

  1. Minimal mtry = 2
  2. Maksimal mtry sebanyak jumlah predictor (numerik + dummy variable). Pada kasus ini, terdapat 4 predictor numerik dan 19 dummy variable dari predictor kategorikal
  3. Rata-rata mtry minimal dan maksimal, yaitu 12

Model yang dipilih adalah mtry = 2 dengan nilai Accuracy tertinggi ketika diujikan ke data hasil bootstrap sampling (atau data in-sample, bisa dianggap sebagai data train seperti pada pembuatan model).

Out-of-Bag (OOB) Error

Pada tahap Bootstrap Sampling, terdapat data yang tidak digunakan dalam pembuatan model, ini yang disebut sebagai Out-of-Bag (OOB) data. OOB data dapat diibaratkan unseen data/data test. Model random forest akan otomatis menghitung OOB error untuk mengetahui performa random forest di OOB data atau diibaratkan sebagai unseen data.

Untuk mengetahui OOB error:

library(randomForest)
model_bank_rf$finalModel
#> 
#> Call:
#>  randomForest(x = x, y = y, mtry = param$mtry, trcontrol = ..1) 
#>                Type of random forest: classification
#>                      Number of trees: 500
#> No. of variables tried at each split: 2
#> 
#>         OOB estimate of  error rate: 11.71%
#> Confusion matrix:
#>        no yes class.error
#> no  31932   0           0
#> yes  4236   0           1
# accuracy: 1-error
100-11.71
#> [1] 88.29

Nilai OOB Error pada model model_bank_rf sebesar 11.71%. Dengan kata lain, akurasi model pada data OOB adalah 88.29%

Interpretasi

Pada machine learning model, terdapat trade-off antara sisi interpretability dan performance. Performance Random Forest dapat diunggulkan dibandingkan model yang lain, namun tidak terlalu dapat diinterpretasi karena banyak faktor random yang terlibat. Namun setidaknya kita dapat melihat predictor apa saja yang paling penting dalam pembuatan Random Forest melalui variable importancenya.

varImp(model_bank_rf)
#> rf variable importance
#> 
#>   only 20 most important variables shown (out of 23)
#> 
#>                    Overall
#> age                100.000
#> previous            93.909
#> balance             62.762
#> housingyes          59.821
#> campaign            27.240
#> jobretired          12.437
#> loanyes             11.710
#> jobstudent           9.895
#> jobblue-collar       9.874
#> educationtertiary    9.656
#> maritalsingle        7.329
#> maritalmarried       6.398
#> educationsecondary   4.320
#> jobmanagement        3.281
#> jobunemployed        2.692
#> jobtechnician        2.271
#> jobservices          1.542
#> jobentrepreneur      1.315
#> educationunknown     1.277
#> jobhousemaid         1.118

Dari hasil data diatas terlihat bahwa variabel age memiliki pengaruh paling tinggi terhadap hasil.

Kesimpulan

  • Beberapa variabel seperti age, previous, balance, housing, dan campaign sangat mempengaruhi seseorang setuju mengajukan program deposit atau tidak.

  • Berdasarkan urutan tingkat akurasi dan nilai recall, model klasifikasi Random Forest adalah model terbaik. Dengan tingkat akurasi 88.29%. Namun kita masih bisa mengubah nilai cutoff dari tiap model dan melakukan validasi ulang terhadap nilai k-fold khususnya pada model random forest.