Naive Bayes SMS Message Classifier
This analysis demonstrates the naive bayes classifier in determining the spam or ham status of 4837 SMS messages. The data set is retrieved from: http://archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection. The data needs explicit cleaning to prepare it to run through a naive bayes classifier. A training and test set is created to train and validate the predictive ability ofy the model. There are summary plots of the words found in abundance in the data. The summary will show the model’s predictive accuracy and the simplicity of running the data. Crossfold validation method is used and the Accuracy and Kappa are the metrics to judge performance.
Load Libraries
library(tidyverse)
library(e1071)
library(tm)
library(SnowballC)
library(wordcloud)
library(gmodels)
library(caret)
Download the Dataset
# place data into frame
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip"
# create temp file for the zip file
zip_file = tempfile(fileext = ".zip")
# unzip data into temp location
download.file(url, zip_file, method = 'libcurl', mode = "wb")
# read data into a tibble and specify data type for each variable, see EPA data source
SMSSpamCollection = read_delim(zip_file, delim = "\t", col_names = c("type", "text"), col_types = "ff")
# remove spaces in variables and replace with underscore
#names(SMSSpamCollection) = gsub("\\s", "_", names(SMSSpamCollection))
unlink(zip_file)
msg <- SMSSpamCollection #save to data frame
glimpse(msg) #check data
## Rows: 4,837
## Columns: 2
## $ type <fct> ham, ham, spam, ham, ham, spam, ham, ham, spam, spam, ham, spa...
## $ text <fct> "Go until jurong point, crazy.. Available only in bugis n grea...
table(msg$type) # 13.2% spam 86.8% ham
##
## ham spam
## 4199 638
The first part of this analysis uses a dataset size of 3184 and runs through to prediction and accuracy measurement of the model. It was unknown that the data set is 4837 observations. That section will follow last. Thus this will be treated as an exercise in smaller verses larger data sets and how the two compare in accuracy. It is assumed the data is random and the model should work.
msg_corpus <- VCorpus(VectorSource(msg$text)) #create source object and sent to frame
print(msg_corpus) #check to see if documents are now stored
## <<VCorpus>>
## Metadata: corpus specific: 0, document level (indexed): 0
## Content: documents: 4837
inspect(msg_corpus[1:2]) #check summary of first two messages.
## <<VCorpus>>
## Metadata: corpus specific: 0, document level (indexed): 0
## Content: documents: 2
##
## [[1]]
## <<PlainTextDocument>>
## Metadata: 7
## Content: chars: 111
##
## [[2]]
## <<PlainTextDocument>>
## Metadata: 7
## Content: chars: 29
as.character(msg_corpus[[1]]) #view actual first message
## [1] "Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat..."
lapply(msg_corpus[1:2], as.character) #view fisrt two messages
## $`1`
## [1] "Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat..."
##
## $`2`
## [1] "Ok lar... Joking wif u oni..."
msg_clean <- tm_map(msg_corpus, content_transformer(tolower))#transform to all lowercase letters
as.character(msg_clean[[1]]) # check that it worked on first line
## [1] "go until jurong point, crazy.. available only in bugis n great world la e buffet... cine there got amore wat..."
msg_clean <- tm_map(msg_clean, removeNumbers) #removes numbers from data
msg_clean <- tm_map(msg_clean, removeWords, stopwords()) #removes filler words
msg_clean <- tm_map(msg_clean, removePunctuation) #removes punctuation but ... can merge unwanted strings error
#This function creates error in DocumentTermMatrix step
#replacePunctuation <- function(x) {
# gsub("[[:punct:]]+"," ", x)
#}
#msg_clean <- tm_map(msg_clean, replacePunctuation) #Error
#msg_clean <- tm_map(msg_clean, PlainTextDocument) #showing alternate may work without error
as.character(msg_clean[[1]]) # check that punctuation is removed.
## [1] "go jurong point crazy available bugis n great world la e buffet cine got amore wat"
as.character(msg_clean[[14]]) #check line 14 before stemming
## [1] " searching right words thank breather promise wont take help granted will fulfil promise wonderful blessing times"
msg_clean <- tm_map(msg_clean, stemDocument) #remove suffix from words
as.character(msg_clean[[14]]) #confirm line 14 for removed suffix's
## [1] "search right word thank breather promis wont take help grant will fulfil promis wonder bless time"
msg_clean <- tm_map(msg_clean, stripWhitespace) #removes extra white spaces between words
as.character(msg_clean[[14]]) #confirm even spacing now
## [1] "search right word thank breather promis wont take help grant will fulfil promis wonder bless time"
msg_dtm <- DocumentTermMatrix(msg_clean) #had errors needed to change removePunctuation function above
msg_dtm #check
## <<DocumentTermMatrix (documents: 4837, terms: 6605)>>
## Non-/sparse entries: 40119/31908266
## Sparsity : 100%
## Maximal term length: 40
## Weighting : term frequency (tf)
dtm <- TermDocumentMatrix(msg_clean)
m <- as.matrix(dtm)
v <- sort(rowSums(m), decreasing=TRUE)
d <- data.frame(word =names(v), freq=v)
Displaying the top 15 words and the frequency they are occurring in the data. The words call, now, get, free all seem like good spam words. The graphic is a word cloud and also demonstrated the 10 most dominant words. The list is more precise in this aspect of identifying top words.
head(d,15)
## word freq
## call call 658
## ham ham 629
## now now 483
## get get 451
## can can 405
## will will 391
## just just 369
## come come 301
## free free 278
## ltgt ltgt 276
## know know 272
## day day 260
## like like 259
## love love 255
## want want 245
set.seed(1234)
wordcloud(words = d$word, freq = d$freq, min.freq = 10,
max.words=175, random.order=FALSE, rot.per=0.35,
colors=brewer.pal(8, "Dark2"))
In the bar plot we get more of the same and another way to see the data. We can probably ignore the “ham” word and focus on the remaining words.
barplot(d[1:15,]$freq, las = 2, names.arg = d[1:15,]$word,
col ="cornflowerblue", main ="15 Most Frequent Words",
ylab = "Frequency")
This word cloud doesn’t do much but show the dominant “ham” word. I doubt most will have the time to read through the text in this graphic.
ham <- subset(msg, type=="ham")
wordcloud(ham$text, min.freq = 50, colors=brewer.pal(8, "Dark2"))
This graphic is more clear and again just displays what we already know from the simple table presented first. “call” “now” “free” sure standout as marketing terms.
spam <- subset(msg, type=="spam")
wordcloud(spam$text, min.freq = 15, colors=brewer.pal(8, "Set1"))
Data Split - Training and Test Sets
msg_dtm_train <- msg_dtm[1:3385, ] #training set created
msg_dtm_test <- msg_dtm[3386:4837, ] #test set created
msg_train_labels <- msg[1:3385, ]$type #Create labels for table later
msg_test_labels <- msg[3386:4837, ]$type #create labels for table later
This is a check up on the proportions of train to test data and seems reasonable at 86% and 14%
#close to the first table 13% spam 87% ham.
prop.table(table(msg_train_labels)) #confirm labels are proportioned to train sets
## msg_train_labels
## ham spam
## 0.8691285 0.1308715
#staying in close precentiles 87% and 13%
prop.table(table(msg_test_labels)) #confirm labels are proportioned to test sets
## msg_test_labels
## ham spam
## 0.8657025 0.1342975
Back to the overall text map. We still find top 10 near middle of graphic. At this point we get it and need to move on to the predictive model.
wordcloud(msg_clean, min.freq = 32, random.order = FALSE, colors=brewer.pal(8, "Set2"))#display 1% of the repetative words
#wordcloud(msg_clean, min.freq = 32, random.order = FALSE, max.words = 5)
#wordcloud(msg$t, min.freq = 32, random.order = FALSE, max.words = 5)
msg_freq_words <- findFreqTerms(msg_dtm_train, 5) #get words that show at least 5 times, into frame
str(msg_freq_words) #check frame
## chr [1:1086] "abiola" "abl" "abt" "accept" "access" "account" "across" ...
msg_dtm_freq_train <- msg_dtm_train[ , msg_freq_words] #filter the DTM for the training set
msg_dtm_freq_test <- msg_dtm_test[ , msg_freq_words] #filter the DTM for the test set
#change numeric 0,1 to character values for classifer to work
convert_counts <- function(x) {
x <- ifelse(x > 0, "yes", "no")
}
msg_train <- apply(msg_dtm_freq_train, MARGIN = 2, convert_counts) #convert train set 0,1 to chars
msg_test <- apply(msg_dtm_freq_test, MARGIN = 2, convert_counts) #convert test set 0,1 to yes, no
msg_classifier <- naiveBayes(msg_train, msg_train_labels) #build the training model
# 3385 total
msg_classifier$apriori
## msg_train_labels
## ham spam
## 2942 443
glimpse(msg_test)
## chr [1:1452, 1:1086] "no" "no" "no" "no" "no" "no" "no" "no" "no" "no" ...
## - attr(*, "dimnames")=List of 2
## ..$ Docs : chr [1:1452] "3386" "3387" "3388" "3389" ...
## ..$ Terms: chr [1:1086] "abiola" "abl" "abt" "accept" ...
glimpse(msg_train)
## chr [1:3385, 1:1086] "no" "no" "no" "no" "no" "no" "no" "no" "no" "no" ...
## - attr(*, "dimnames")=List of 2
## ..$ Docs : chr [1:3385] "1" "2" "3" "4" ...
## ..$ Terms: chr [1:1086] "abiola" "abl" "abt" "accept" ...
glimpse(msg_train_labels)
## Factor w/ 2 levels "ham","spam": 1 1 2 1 1 2 1 1 2 2 ...
set.seed(98)
msg_test_pred <- predict(msg_classifier, msg_test) #test the classifer on new data
Here is the predictive analysis. The table shows a total of 28 (2 + 26) were incorrectly classified and that is 2.9%. 2 out of 820 messages were misidentified as spam, while 26 of 135 messages were incorrectly labeled as ham. For a first try at Naive Bayes classification these results seem good granted the data is smaller than the 5574 intended size. The accuracy of the model is 97% with a kappa of 0.87. 26 incorrectly identified is still a little high but 2 is not bad.
#Create table to compare perdictions to true values
CrossTable(msg_test_pred, msg_test_labels,
prop.chisq = FALSE, prop.t = FALSE,
dnn = c('predicted', 'actual'))
##
##
## Cell Contents
## |-------------------------|
## | N |
## | N / Row Total |
## | N / Col Total |
## |-------------------------|
##
##
## Total Observations in Table: 1452
##
##
## | actual
## predicted | ham | spam | Row Total |
## -------------|-----------|-----------|-----------|
## ham | 1251 | 25 | 1276 |
## | 0.980 | 0.020 | 0.879 |
## | 0.995 | 0.128 | |
## -------------|-----------|-----------|-----------|
## spam | 6 | 170 | 176 |
## | 0.034 | 0.966 | 0.121 |
## | 0.005 | 0.872 | |
## -------------|-----------|-----------|-----------|
## Column Total | 1257 | 195 | 1452 |
## | 0.866 | 0.134 | |
## -------------|-----------|-----------|-----------|
##
##
confusionMatrix(msg_test_pred, msg_test_labels, positive = 'spam')
## Confusion Matrix and Statistics
##
## Reference
## Prediction ham spam
## ham 1251 25
## spam 6 170
##
## Accuracy : 0.9787
## 95% CI : (0.9698, 0.9854)
## No Information Rate : 0.8657
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.9042
##
## Mcnemar's Test P-Value : 0.001225
##
## Sensitivity : 0.8718
## Specificity : 0.9952
## Pos Pred Value : 0.9659
## Neg Pred Value : 0.9804
## Prevalence : 0.1343
## Detection Rate : 0.1171
## Detection Prevalence : 0.1212
## Balanced Accuracy : 0.9335
##
## 'Positive' Class : spam
##
Here we add laplace 1 to the model to see if we can improve accuracy. Instead we increase our incorrectly classified variables by 4 and 1 (6 and 27) and our accuracy and kappa drops 96.5% and 0.84. Still pretty high but not perfect.
msg_classifier2 <- naiveBayes(msg_train, msg_train_labels, laplace = 1) #adjust laplace to 1
msg_test_pred2 <- predict(msg_classifier2, msg_test)
CrossTable(msg_test_pred2, msg_test_labels,
prop.chisq = FALSE, prop.t = FALSE, prop.r = FALSE,
dnn = c('predicted', 'actual'))
##
##
## Cell Contents
## |-------------------------|
## | N |
## | N / Col Total |
## |-------------------------|
##
##
## Total Observations in Table: 1452
##
##
## | actual
## predicted | ham | spam | Row Total |
## -------------|-----------|-----------|-----------|
## ham | 1251 | 31 | 1282 |
## | 0.995 | 0.159 | |
## -------------|-----------|-----------|-----------|
## spam | 6 | 164 | 170 |
## | 0.005 | 0.841 | |
## -------------|-----------|-----------|-----------|
## Column Total | 1257 | 195 | 1452 |
## | 0.866 | 0.134 | |
## -------------|-----------|-----------|-----------|
##
##
confusionMatrix(msg_test_pred2, msg_test_labels, positive = 'spam')
## Confusion Matrix and Statistics
##
## Reference
## Prediction ham spam
## ham 1251 31
## spam 6 164
##
## Accuracy : 0.9745
## 95% CI : (0.965, 0.982)
## No Information Rate : 0.8657
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.8841
##
## Mcnemar's Test P-Value : 7.961e-05
##
## Sensitivity : 0.8410
## Specificity : 0.9952
## Pos Pred Value : 0.9647
## Neg Pred Value : 0.9758
## Prevalence : 0.1343
## Detection Rate : 0.1129
## Detection Prevalence : 0.1171
## Balanced Accuracy : 0.9181
##
## 'Positive' Class : spam
##
#the laplace=1 estimator predicitve model did not improve performance because we can see (6+27)=33 incorrectly classified
#messages increases slightly from the 26 earlier.
Section 2 - Replay
Here we will redo the previous steps with more data, or correctly, the 5574 points in the data set that did not import the first time. We will focus on the differences and look for any improvements over less data (3184 verses 5574).
#Start all over again
spam_raw <- SMSSpamCollection
names(spam_raw) <- c("type", "text") #label V1 and V2.
## tibble [4,837 x 2] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ type: Factor w/ 2 levels "ham","spam": 1 1 2 1 1 2 1 1 2 2 ...
## $ text: Factor w/ 4522 levels "Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...",..: 1 2 3 4 5 6 7 8 9 10 ...
## - attr(*, "problems")= tibble [39 x 5] (S3: tbl_df/tbl/data.frame)
## ..$ row : int [1:39] 283 283 454 454 454 454 454 454 454 476 ...
## ..$ col : chr [1:39] "text" "text" "text" "text" ...
## ..$ expected: chr [1:39] "delimiter or quote" "delimiter or quote" "delimiter or quote" "delimiter or quote" ...
## ..$ actual : chr [1:39] " " "H" " " "Y" ...
## ..$ file : chr [1:39] "'C:\\Users\\Zeus\\AppData\\Local\\Temp\\RtmpOqCqTb\\fileca8340c50a2.zip'" "'C:\\Users\\Zeus\\AppData\\Local\\Temp\\RtmpOqCqTb\\fileca8340c50a2.zip'" "'C:\\Users\\Zeus\\AppData\\Local\\Temp\\RtmpOqCqTb\\fileca8340c50a2.zip'" "'C:\\Users\\Zeus\\AppData\\Local\\Temp\\RtmpOqCqTb\\fileca8340c50a2.zip'" ...
## - attr(*, "spec")=
## .. cols(
## .. type = col_factor(levels = NULL, ordered = FALSE, include_na = FALSE),
## .. text = col_factor(levels = NULL, ordered = FALSE, include_na = FALSE)
## .. )
glimpse(msg2$type)
## Factor w/ 2 levels "ham","spam": 1 1 2 1 1 2 1 1 2 2 ...
msg_corpus2 <- VCorpus(VectorSource(msg2$text)) #create source object and sent to frame
msg_clean2 <- tm_map(msg_corpus2, content_transformer(tolower))#transform to all lowercase letters
msg_clean2 <- tm_map(msg_clean2, removeNumbers) #removes numbers from data
msg_clean2 <- tm_map(msg_clean2, removeWords, stopwords()) #removes filler words
msg_clean2 <- tm_map(msg_clean2, removePunctuation) #removes punctuation but ... can merge unwanted strings
msg_clean2 <- tm_map(msg_clean2, stemDocument) #remove suffix from words
msg_clean2 <- tm_map(msg_clean2, stripWhitespace) #removes extra white spaces between words
msg_dtm2 <- DocumentTermMatrix(msg_clean2)
dtm2 <- TermDocumentMatrix(msg_clean2)#top 10 words
m2 <- as.matrix(dtm2)
v2 <- sort(rowSums(m2), decreasing=TRUE)
d2 <- data.frame(word =names(v2), freq=v2)
We can see “ham” and “spam” are out and “call”, “now”, “get”..“free” are still in the top words grouping.
head(d2,10)
## word freq
## call call 658
## ham ham 629
## now now 483
## get get 451
## can can 405
## will will 391
## just just 369
## come come 301
## free free 278
## ltgt ltgt 276
This graphic is working much better and the color coding is working to group similar frequent terms.
set.seed(4567)
wordcloud(words = d2$word, freq = d2$freq, min.freq = 1,
max.words=200, random.order=FALSE, rot.per=0.35,
colors=brewer.pal(8, "Dark2"))
Oddly, “ham”/“spam” are back in the bar plot. But it is better than before because we can see the other words that dominate spam messaging.
barplot(d2[1:10,]$freq, las = 2, names.arg = d[1:10,]$word,
col ="brown3", main ="Most frequent words",
ylab = "Word frequencies")
This graphic is producing some problems. The frequency was increased to 55 or 1% of the data size. And reduced the warning messages down to two from 10 prior before publishing these results. Interesting many messages have “sorry” in them or “need”….
ham2 <- subset(msg2, type=="ham")
wordcloud(ham2$text, min.freq = 55, colors=brewer.pal(8, "Dark2"))
This graphic is better and definitely shows some of the spammy type words: get, mobile, prize, call, reply, stop, etc. Also increased the frequency to 55 from the first attempt at this.
spam2 <- subset(msg, type=="spam")
wordcloud(spam2$text, min.freq = 15, colors=brewer.pal(8, "Dark2"))
msg_dtm_train2 <- msg_dtm2[1:3385, ] #training set created
msg_dtm_test2 <- msg_dtm2[3386:4837, ] #test set created
msg_train_labels2 <- msg2[1:3385, ]$type #Create labels for table later
msg_test_labels2 <- msg2[3386:4837, ]$type #create labels for table later
The proportions are the same as before because we used a calculator to figure a 70/30 split.
prop.table(table(msg_train_labels2)) #confirm labels are proportioned to train sets
## msg_train_labels2
## ham spam
## 0.8691285 0.1308715
prop.table(table(msg_test_labels2)) #confirm labels are proportioned to test sets
## msg_test_labels2
## ham spam
## 0.8657025 0.1342975
#wordcloud(msg_clean2, min.freq = 32, random.order = FALSE)#display 1% of the repetative words
#wordcloud(msg_clean2, min.freq = 32, random.order = FALSE, max.words = 5)
msg_freq_words2 <- findFreqTerms(msg_dtm_train2, 5) #get words that show at least 5 times, into frame
msg_dtm_freq_train2 <- msg_dtm_train2[ , msg_freq_words2] #filter the DTM for the training set
msg_dtm_freq_test2 <- msg_dtm_test2[ , msg_freq_words2] #filter the DTM for the test set
#change numeric 0,1 to character values for classifer to work
convert_counts2 <- function(x) {
x <- ifelse(x > 0, "yes", "no")
}
msg_train2 <- apply(msg_dtm_freq_train2, MARGIN = 2, convert_counts2) #convert train set 0,1 to chars
msg_test2 <- apply(msg_dtm_freq_test2, MARGIN = 2, convert_counts2) #convert test set 0,1 to yes, no
msg_classifier2 <- naiveBayes(msg_train2, msg_train_labels2, laplace = 1) #build the training model and place in frame
msg_test_pred3 <- predict(msg_classifier2, msg_test2) #test the classifer on new data
#Create table to compare perdictions to true values
This time we show the laplace 1 first and show how it increases the incorrectly identified units of 5 + 33 to 38 higher than our first model of 2+26= 28. However, the accuracy climbs to 97.7% and a bigger kappa or 0.898. Improvements on accuracy and kappa but slightly more incorrectly labeled terms.
CrossTable(msg_test_pred3, msg_test_labels2,
prop.chisq = FALSE, prop.t = FALSE,
dnn = c('predicted', 'actual'))
##
##
## Cell Contents
## |-------------------------|
## | N |
## | N / Row Total |
## | N / Col Total |
## |-------------------------|
##
##
## Total Observations in Table: 1452
##
##
## | actual
## predicted | ham | spam | Row Total |
## -------------|-----------|-----------|-----------|
## ham | 1251 | 31 | 1282 |
## | 0.976 | 0.024 | 0.883 |
## | 0.995 | 0.159 | |
## -------------|-----------|-----------|-----------|
## spam | 6 | 164 | 170 |
## | 0.035 | 0.965 | 0.117 |
## | 0.005 | 0.841 | |
## -------------|-----------|-----------|-----------|
## Column Total | 1257 | 195 | 1452 |
## | 0.866 | 0.134 | |
## -------------|-----------|-----------|-----------|
##
##
confusionMatrix(msg_test_pred3, msg_test_labels2, positive = 'spam')
## Confusion Matrix and Statistics
##
## Reference
## Prediction ham spam
## ham 1251 31
## spam 6 164
##
## Accuracy : 0.9745
## 95% CI : (0.965, 0.982)
## No Information Rate : 0.8657
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.8841
##
## Mcnemar's Test P-Value : 7.961e-05
##
## Sensitivity : 0.8410
## Specificity : 0.9952
## Pos Pred Value : 0.9647
## Neg Pred Value : 0.9758
## Prevalence : 0.1343
## Detection Rate : 0.1129
## Detection Prevalence : 0.1171
## Balanced Accuracy : 0.9181
##
## 'Positive' Class : spam
##
# more accurate 98%, Kappa at 0.92
msg_classifier4 <- naiveBayes(msg_train2, msg_train_labels2) #build the training model and place in frame
msg_test_pred4 <- predict(msg_classifier4, msg_test2) #test the classifer on new data
When the laplace 1 is dropped the model also drops the number of incorrectly labeled variables to 6+25= 31. This is still higher than the first model 2+26=28. But with it comes a higher accuracy of 98% and a higher kappa of 0.92! We show the highest accuracy and kappa of 4 trials using the larger data set (5574) however, the incorrectly labeled variables from the smaller data set produced lower mislabeling of 28 verse 31 in this model. So we can see a slight increase of one aspect and decrease in another. More model tuning should take place to refine this prediction to see where the error rate can be reduced.
CrossTable(msg_test_pred4, msg_test_labels2,
prop.chisq = FALSE, prop.t = FALSE,
dnn = c('predicted', 'actual'))
##
##
## Cell Contents
## |-------------------------|
## | N |
## | N / Row Total |
## | N / Col Total |
## |-------------------------|
##
##
## Total Observations in Table: 1452
##
##
## | actual
## predicted | ham | spam | Row Total |
## -------------|-----------|-----------|-----------|
## ham | 1251 | 25 | 1276 |
## | 0.980 | 0.020 | 0.879 |
## | 0.995 | 0.128 | |
## -------------|-----------|-----------|-----------|
## spam | 6 | 170 | 176 |
## | 0.034 | 0.966 | 0.121 |
## | 0.005 | 0.872 | |
## -------------|-----------|-----------|-----------|
## Column Total | 1257 | 195 | 1452 |
## | 0.866 | 0.134 | |
## -------------|-----------|-----------|-----------|
##
##
confusionMatrix(msg_test_pred4, msg_test_labels2, positive = 'spam')
## Confusion Matrix and Statistics
##
## Reference
## Prediction ham spam
## ham 1251 25
## spam 6 170
##
## Accuracy : 0.9787
## 95% CI : (0.9698, 0.9854)
## No Information Rate : 0.8657
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.9042
##
## Mcnemar's Test P-Value : 0.001225
##
## Sensitivity : 0.8718
## Specificity : 0.9952
## Pos Pred Value : 0.9659
## Neg Pred Value : 0.9804
## Prevalence : 0.1343
## Detection Rate : 0.1171
## Detection Prevalence : 0.1212
## Balanced Accuracy : 0.9335
##
## 'Positive' Class : spam
##