SMS (Short Message Service) adalah salah satu fasilitas dari teknologi GSM (Global System for Mobile) yang memungkinkan mengirim dan menerima pesan-pesan singkat berupa teks dengan kapasitas maksimal 160 karakter. Meski sudah jarang digunakan sebagai media komunikasi sehari-hari, SMS tetap menjadi media penting untuk beberapa orang. Selain itu proses verifikasi hal penting seringkali dilakukan melalui SMS. Namun tidak dapat dipungkiri bahwa SMS yang kita terima bukanlah sebuah pesan yang penting atau bisa disebut sebagai (SPAM). Pada project kali ini akan dibuat sebuah model untuk memprediksi sebuah SMS apakah SPAM atau HAM dengan menggunakan dua jenis model yaitu Naive Bayes dan Random Forest.

1 Setup & Library

1.1 Setup

Setup yang digunakan pada project ini:

1.2 Library

Berikut ini serangkaian library yang digunakan untuk proses pemodelan.

# library
library(dplyr)
library(lubridate)
library(tm)
library(textclean)
library(devtools)
library(katadasaR)
library(tokenizers)
library(wordcloud)
library(stopwords)
library(e1071) #Naive Bayes
library(caret) #Confussion Matrix
library(ROCR)
library(lime)
library(ggplot2)
library(tidyr)
library(tibble)

2 Data Pre-Processing

2.1 Data Input

sms <- read.csv("data/data-train.csv",stringsAsFactors = FALSE, encoding = "UTF-8")
glimpse(sms)
#> Rows: 2,004
#> Columns: 3
#> $ datetime <chr> "2017-02-15T14:48:00Z", "2017-02-15T15:24:00Z", "2017-02-1...
#> $ text     <chr> "Telegram code 53784", "Rezeki Nomplok Dompetku Pengiriman...
#> $ status   <chr> "ham", "spam", "ham", "ham", "ham", "ham", "ham", "spam", ...

Data diatas masih memiliki type data yang belum sesuai, maka dari itu akan dilakukan transformasi type data:

  • datetime-> datetime
  • status-> factor

2.2 Data Wrangling

sms <- sms %>% 
  mutate(
    datetime = ymd_hms(datetime),
    status = as.factor(status)
  )

2.3 Exploratory Data Analysis (EDA)

2.3.1 Spam vs Ham Visualization

sms %>% 
   mutate(hour = hour(datetime)) %>% 
   group_by(hour) %>% 
   summarise(
      spam = sum(ifelse(status == "spam", 1, 0)),
      ham = sum(ifelse(status == "spam", 0, 1)),
   ) %>% 
   ungroup() %>% 
   pivot_longer(
      cols=c(ham, spam)
   ) %>% 
   ggplot(
      aes(
         x=hour,
         y=value,
         fill=name
      )
   ) +
   geom_col(
      stat="identity"
   ) +
   scale_fill_manual(values=c("#0890a8", "#a80813")) +
   labs(title = "Perbandingan Densitas SMS ham vs spam")+theme(panel.background = element_rect(fill = "white"), 
      plot.title = element_text(size = 16, colour = "#280759"),
      plot.background = element_rect(fill = "transparent"))

Plot diatas menunjukkan distribusi ham dan spam sepanjang waktu. Dari grafik diatas dapat diamati bahwa terjadi peningkatan jumlah sms yang diterima pada pukul 5 hingga pukul 9. Lalu kemudian menurun secara perlahan mengikuti larutnya hari. Dalam kasus ini waktu (datetime) tidak berpengaruh pada pengelompokkan spam dan ham, maka kolom datetime dapat kita keluarkan dari data.

sms <- sms %>% 
  select(-datetime)

2.3.2 Spam & Ham Characteristic

Pemahaman data yang kita gunakan amatlah penting mengingat hal tersebut sangat mempengaruhi proses apa yang akan kita lakukan selanjutnya. Salah satu cara memahami data adalah dengan mengkarakterisasi data. Karena tujuan projek ini adalah sebuah model yang dapat melakukan klasifikasi Spam/Ham pada SMS maka akan tepat bila kita dapat mengkarakterisasi kata atau keyword apa saja yang banyak muncul pada masing-masing status.

Berikut ini adalah kata-kata yang sering muncul pada data train yang digunakan.

library(wordcloud)
spam <- subset(sms, status == "spam")
wordcloud(spam$text, max.words = 60, colors = brewer.pal(5, "Dark2"), random.order = FALSE)

2.3.2.1 Spam Characteristic

sms %>%
   filter(status == "spam") %>% 
   tail()

Data sms yang tergolong ke dalam SPAM kebanyakan merupakan pesan iklan, promosi, dan penawaran produk. Kata kunci yang sering muncul antara lain adalah: “bonus”, “menang”, “gratis”.

2.3.2.2 Ham Characteristic

sms %>% 
   filter(status == "ham") %>% 
   tail()

Sedangkan yang tergolong ke dalam HAM biasanya berhubungan dengan verifikasi, pesan operator, dan pesan pribadi. Kata kunci yang sering muncul antara lain: “dimaana”, “code”.

2.4 Data Cleansing

Keterbatasan jumlah karakter yang dimiliki fitur SMS berakibat pada sering terjadinya penyingkattan kata pada pesan yang dikirim. Hal tersebut mudah dipahami manusia namun tidak oleh komputer. Selain itu penggunaan kata yang tidak baku juga seringkali terjadi antara percakapan kita dengan keluarga maupun orang terdekat. Atas dasar hal-hal tersebut Text Cleansing menjadi sebuah tahapan yang sangat penting agar kata-kata potensial yang terdapat dalam text pesan dapat dikenali dan di proses dengan baik oleh model dan mesin yang dibuat.

2.4.1 Convert to Corpus

sms.corpus <- sms %>% 
   # Convert to corpus
   VectorSource() %>% 
   VCorpus()

2.4.2 Text Cleansing

sms.corpus <- sms.corpus %>%
   tm_map(content_transformer(tolower)) %>% 
   tm_map(removeNumbers) %>% 
   tm_map(removeWords, stopwords("id", source="stopwords-iso")) %>% 
   tm_map(removePunctuation) %>%
   tm_map(function(x) { stemDocument(x, language="indonesian") }) %>%
   tm_map(stripWhitespace)

2.4.3 Convert to Document Term Matrix (DTM)

Untuk dapat membuat model prediksi, tidak dapat digunakan type data text. Dalam kasus ini biasanya data text di konversi menjadi Document-Term Matrix (DTM).

sms.dtm <- sms.corpus %>% 
   DocumentTermMatrix()
sms.dtm
#> <<DocumentTermMatrix (documents: 2, terms: 2821)>>
#> Non-/sparse entries: 2822/2820
#> Sparsity           : 50%
#> Maximal term length: 79
#> Weighting          : term frequency (tf)

2.4.4 Most Frequence Term

Kolom atau kata yang kita punya untuk prediksi sangat banyak. Untuk mengurangi noise (kata-kata yang jarang muncul), kita akan gunakan kata-kata yang cukup sering muncul, setidaknya muncul dalam 20 dokumen (sms). Proses ini juga dapat memangkas waktu training model.

sms.freq <- findFreqTerms(sms.dtm, lowfreq = 20)

sms.dtm <- sms.dtm[,sms.freq]

2.4.5 Convert to Bernoulli

Nilai pada matrix masih berupa nilai frekuensi. Untuk perhitungan peluang, frekuensi akan diubah menjadi hanya kondisi muncul (1) atau tidak (0). Salah satu caranya dengan menggunakan Bernoulli Converter. Berikut contoh transformasi yang dilakukan dengan menggunakan Bernoulli Converter.

bernoulli_conv <- function(x) {
  x <- as.factor(ifelse(x > 0, 1, 0))
  return(x)
}

bernoulli_conv(c(0,1,3))
#> [1] 0 1 1
#> Levels: 0 1

Sekarang kita aplikasikan pada data yang kita miliki.

sms.dtm <- sms.dtm %>% 
   apply(MARGIN = 2, FUN = bernoulli_conv)

sms.dtm[1:2, 1:20]
#>     Terms
#> Docs aja aks aktif aktifkan aplikasi app aspen axi axisnet ayo bala bank beba
#>    1 "1" "1" "1"   "1"      "1"      "1" "1"   "1" "1"     "1" "1"  "1"  "1" 
#>    2 "0" "0" "0"   "0"      "0"      "0" "0"   "0" "0"     "0" "0"  "0"  "0" 
#>     Terms
#> Docs beli berhasil berita berlaku bersifat biaya blm
#>    1 "1"  "1"      "1"    "1"     "1"      "1"   "1"
#>    2 "0"  "0"      "0"    "0"     "0"      "0"   "0"

2.4.6 Tokenization Function

Proses tokenization berperan memisahkan sebuah text menjadi pecahan kata yang terpisah dan dapat diindentifikasi satu-persatu.

tokenize_text <- function(x, is_bernoulli = TRUE) {
   data_dtm <- x %>% 
      # Convert to corpus
      VectorSource() %>% 
      VCorpus() %>% 
      
      # text cleaning
      tm_map(content_transformer(tolower)) %>% 
      tm_map(removeNumbers) %>% 
      tm_map(removeWords, stopwords("id", source="stopwords-iso")) %>% 
      tm_map(removePunctuation) %>%
      tm_map(stemDocument) %>%
      tm_map(stripWhitespace) %>% 

      # Convert DTM
      DocumentTermMatrix()
   
   data_freq <- findFreqTerms(data_dtm, lowfreq = 20)

   if (is_bernoulli) {
      data_dtm[,data_freq] %>% 
         apply(MARGIN = 2, FUN = bernoulli_conv) %>% 
         return()
   } else {
      data_dtm[,data_freq] %>% 
         return()
   }
}

2.5 Cross Validation

Cleansing data telah selesai dilakukan, selanjutnya dilakukan pembagian data train dan data test dengan proporsi secara berurutan adalah 75% dan 25%.

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

index <- sample(nrow(sms), nrow(sms)*0.75)

sms_clean <- tokenize_text(sms$text)

data_train_clean <- sms_clean[index,]
data_test_clean <- sms_clean[-index,]

label_train <- sms[index, "status"]
label_test <- sms[-index, "status"]
data_train <- sms[index,]
data_test <- sms[-index,]

3 Model

Pada pemodelan ini akan digunakan dua jenis tipe model agar dapat dilakukan perbandingan, tipe model tersebut antara lain adalah Naive Bayes dan Random Forest

3.1 Naive Bayes

model_nb <- naiveBayes(
   x = data_train_clean, 
   y = label_train,
   laplace = 1
)

3.2 Random Forest

set.seed(521)

ctrl <- trainControl(method="repeatedcv", number = 5, repeats = 3)

model_forest <- train(
   x = data_train_clean,
   y = label_train,
   method = "rf",
   trControl = ctrl
)

saveRDS(model_forest, "spam_forest.RDS") # save model
model_forest <- readRDS("spam_forest.RDS")

4 Predicting

4.1 Naive Bayes

sms_pred_naive <- predict(model_nb, newdata = data_test_clean, type="class")
head(sms_pred_naive)
#> [1] ham  spam spam ham  ham  ham 
#> Levels: ham spam

4.2 Random Forest

sms_pred_rf <- predict(model_forest, newdata = data_test_clean, type="raw")
head(sms_pred_rf)
#> [1] ham  spam spam ham  spam spam
#> Levels: ham spam

5 Model Evaluation

Model evaluation dilakukan untuk dapat mengetahui performa masing-masing model yang digunakan untuk prediksi.

5.1 Confussion Matrix

Salah satu cara mengevaluasi model yang kita gunakan adalah dengan menggunakan Confussion Matrix.

5.1.1 Naive Bayes

confusionMatrix(data = sms_pred_naive, reference = label_test, positive = "spam")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction ham spam
#>       ham  257   17
#>       spam  27  200
#>                                              
#>                Accuracy : 0.9122             
#>                  95% CI : (0.8839, 0.9355)   
#>     No Information Rate : 0.5669             
#>     P-Value [Acc > NIR] : <0.0000000000000002
#>                                              
#>                   Kappa : 0.8221             
#>                                              
#>  Mcnemar's Test P-Value : 0.1748             
#>                                              
#>             Sensitivity : 0.9217             
#>             Specificity : 0.9049             
#>          Pos Pred Value : 0.8811             
#>          Neg Pred Value : 0.9380             
#>              Prevalence : 0.4331             
#>          Detection Rate : 0.3992             
#>    Detection Prevalence : 0.4531             
#>       Balanced Accuracy : 0.9133             
#>                                              
#>        'Positive' Class : spam               
#> 

5.1.2 Random Forest

confusionMatrix(data = sms_pred_rf, reference = label_test, positive = "spam")
#> Confusion Matrix and Statistics
#> 
#>           Reference
#> Prediction ham spam
#>       ham  270    6
#>       spam  14  211
#>                                              
#>                Accuracy : 0.9601             
#>                  95% CI : (0.939, 0.9754)    
#>     No Information Rate : 0.5669             
#>     P-Value [Acc > NIR] : <0.0000000000000002
#>                                              
#>                   Kappa : 0.9191             
#>                                              
#>  Mcnemar's Test P-Value : 0.1175             
#>                                              
#>             Sensitivity : 0.9724             
#>             Specificity : 0.9507             
#>          Pos Pred Value : 0.9378             
#>          Neg Pred Value : 0.9783             
#>              Prevalence : 0.4331             
#>          Detection Rate : 0.4212             
#>    Detection Prevalence : 0.4491             
#>       Balanced Accuracy : 0.9615             
#>                                              
#>        'Positive' Class : spam               
#> 

Hasil evaluasi menggunakan confussion matrix menunjukkan bahwa akurasi prediksi dengan model Naive Bayes adalah 91% sedangkan prediksi dengan model Random Forest menghasilkan akurasi sebesar 96%.

Karena akurasi yang dihasilkan dengan menggunakan model random forest lebih besar, model inilah yang akn dipilih.

5.2 False Prediction

Mencari tahu di bagian mana model kita salah memprediksi.

pred.false <- data_test %>% 
   mutate(
      pred.rf = sms_pred_rf,
   ) %>% 
   filter(pred.rf != status)
pred.false %>% filter(pred.rf == "spam")

6 Model Interpretation

6.1 Variable of Importance

caret::varImp(model_forest, 20)$importance %>% 
   as.data.frame() %>%
   rownames_to_column() %>%
   arrange(-Overall) %>%
   mutate(rowname = forcats::fct_inorder(rowname))

Dari tabel diatas kita dapat mengetahui bahwa kata yang memiliki bobot pengaruh paling besar adalah info. Sebaliknya, kata yang memiliki bobot pengaruh paling kecil ialah mempengaruhi

6.2 LIME

Local Interpretable Model-agnostic Explanations (LIME) merupakan salah satu teknik interpretasi hasil prediksi yang dilakukan sebuah model. LIME dapat memprediksi model appaun dan menganggapnya sebagai black box model. Pada kesempatan kali ini LIME akan digunakan untuk meninterpretasikan model naive bayes.

model_type.naiveBayes <- function(x){
  return("classification")
}
predict_model.naiveBayes <- function(x, newdata, type = "raw") {
    res <- predict(x, newdata, type = "raw") %>% as.data.frame()
    return(res)
}

Persipkan data input untuk LIME.

text_train <- data_train$text %>% as.character()
text_test <- data_test$text

explainer <- lime(
   text_train,
   model=model_nb,
   preprocess=tokenize_text
)
set.seed(123)
explanation <- explain(
   text_test[1:5],
   explainer = explainer, 
   n_labels = 1, # show only 1 label (recommend or not recommend)
   n_features = 5, 
   feature_select = "none", # use all terms to explain the model
   single_explanation = F
)

Visualisasi LIME

plot_text_explanations(explanation)

Dari observasi diatas dapat diamati bahwa probabilitas text di klasifikasikan menjadi HAM adalah 98%. LIME cukup baik dalam menginterpretasikan prediksi pengamatan. Hal ini bersesuaian dengan angka 76% yang merepresentasikannya.

Teks berlabel biru berarti kata support / tingkatkan kemungkinan menjadi SPAM, dengan pengaruh kata promo dan belaku yang paling berpengaruh.

Teks berlabel merah berarti kata tersebut bertentangan / mengurangi probabilitas review menjadi HAM, seperti your, hari atau nasi.

7 Submission

7.1 Import Data Test

submission <- read.csv("data/data-test.csv")

7.2 Text Cleansing

submission.clean <- tokenize_text(submission$text)
submission.clean[1:5,1:10]
#>     Terms
#> Docs aplikasi axi axisnet bala beli berlaku bonus bronet dgn diblokir
#>    1 "0"      "0" "0"     "0"  "1"  "0"     "0"   "0"    "1" "0"     
#>    2 "0"      "0" "0"     "0"  "0"  "0"     "0"   "0"    "0" "0"     
#>    3 "0"      "0" "0"     "0"  "0"  "0"     "0"   "0"    "0" "0"     
#>    4 "0"      "0" "0"     "0"  "0"  "0"     "0"   "0"    "0" "0"     
#>    5 "0"      "0" "0"     "0"  "0"  "0"     "1"   "0"    "0" "0"

7.3 Optimize Data

trimRfPredictor <- function(x, train_data) {
   x %>%
      as.data.frame() %>% 
      fncols(colnames(train_data)) %>% 
      select(colnames(train_data)) %>% 
      mutate_all(as.factor) %>% 
      as.matrix.data.frame() %>% 
      return()
}
fncols <- function(data, cname) {
  add <-cname[!cname%in%names(data)]

  if(length(add)!=0) data[add] <- as.factor("0")
  data
}
submission.clean.df <- trimRfPredictor(submission.clean, data_train_clean)
submission.clean.df[1:5,1:20]
#>      aja aks aktif aktifkan aplikasi app aspen axi axisnet ayo bala bank beba
#> [1,] "0" "0" "0"   "0"      "0"      "0" "0"   "0" "0"     "0" "0"  "0"  "0" 
#> [2,] "0" "0" "0"   "0"      "0"      "0" "0"   "0" "0"     "0" "0"  "0"  "0" 
#> [3,] "0" "0" "0"   "0"      "0"      "0" "0"   "0" "0"     "0" "0"  "0"  "0" 
#> [4,] "0" "0" "0"   "0"      "0"      "0" "0"   "0" "0"     "0" "0"  "0"  "0" 
#> [5,] "0" "0" "0"   "0"      "0"      "0" "0"   "0" "0"     "0" "0"  "0"  "0" 
#>      beli berhasil berita berlaku bersifat biaya blm
#> [1,] "1"  "0"      "0"    "0"     "0"      "0"   "0"
#> [2,] "0"  "0"      "0"    "0"     "0"      "0"   "0"
#> [3,] "0"  "0"      "0"    "0"     "0"      "0"   "0"
#> [4,] "0"  "0"      "0"    "0"     "0"      "0"   "0"
#> [5,] "0"  "0"      "0"    "0"     "0"      "0"   "0"

7.4 Predict Submission

7.4.1 Naive Bayes

submission.nb <- submission %>% 
   select(datetime)
submission.nb$status <- predict(model_nb, newdata = submission.clean.df, type="class")

head(submission.nb)
write.csv(submission.nb, "data/submission_nb.csv")

7.4.2 Random Forest

submission.rf <- submission %>% 
   select(datetime)
submission.rf$status <- predict(model_forest, newdata = submission.clean.df, type="raw")

head(submission.rf)
write.csv(submission.rf, "data/submission_rf.csv")

8 Conclusion

Pembuatan model klasifikasi sebuah text pada SMS adalah sebuah SPAM atau HAM sudah berhasil dibuat. Pada kasus ini digunakan dua model berbeda yakni Naive Bayes dan Random Forest. Pada percobaan dengan menggunakan data test (25% dari data train) dihasilkan akurasi Naive Bayes 91% dan Random Forest 96%. Hal tersebut menunjukkan bahwa performa yang lebih baik dihasilkan dengan menggunakan model Random Forest. Hal tersebut juga ikut dibuktikan oleh hasil test dengan data test langsung yang menghasilkan akurasi lebih baik pada model Random Forest.

Model klasifikasi ini sangat berpotensi untuk pengembangan bisnis online. Dengan automasi dan klasifikasi menggunakan machine learning, sebuah usaha akan sangat dimudahkan dalam memanajemen pesanan maupun keluhan dari pelanggannya.