<jupyter_start><jupyter_text>SMS Spam Detection Using NLP and ML  Introduction - Natural language processing (NLP) is a subfield of computer science and artificial intelligence concerned with the interactions between computers and human (natural) languages.- In this project, we will try to build a simple classifier using machine learning which will help in identifying whether a given SMS is a spam or not.  Context - The SMS Spam Collection is a set of SMS tagged messages that have been collected for SMS Spam research. It contains one set of SMS messages in English of 5,574 messages, tagged acording being ham (legitimate) or spam.  Content - The files contain one message per line. Each line is composed by two columns: v1 contains the label (ham or spam) and v2 contains the raw text.  1. Importing Dependencies<jupyter_code># Basic Libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Sk-learn Libraries 
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn import metrics
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier

# NLP Libraries
import re
import itertools
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords

# Other Libraries
import string
from collections import Counter<jupyter_output><empty_output><jupyter_text>2. Reading the dataset<jupyter_code># Reading the data
data = pd.read_csv('spam.csv', encoding = 'latin-1')
data.dropna(how = "any", inplace = True, axis = 1)
data.columns = ['label', 'message']
data.head()<jupyter_output><empty_output><jupyter_text>3. Exploratory Data Analysis (EDA)<jupyter_code># Let's check out some of the stats with some plots and the built-in methods in pandas!
data.describe()
# Let's use groupby to use describe by label, this way we can begin to think about the features that separate 
# ham and spam!

data.groupby('label').describe()<jupyter_output><empty_output><jupyter_text>**Observations**- We have 4825 ham messages and 747 spam messages<jupyter_code># convert label to a numerical variable

data['numeric_label'] = data.label.map({'ham':0, 'spam':1})
data.head()
# Let's make a new column to detect how long the text messages are:
data['message_length'] = data['message'].apply(len)
data.head()
# Analysing the 'message_length' feature
data['message_length'].describe()
# Max is 910 characters, let's use masking to find this message
data[data['message_length'] == 910]['message'].iloc[0]
# Lets see if message length is a distinguishing feature between ham and spam:

fig,ax = plt.subplots(nrows = 1, ncols = 2, figsize = (12,6))

data[data['label'] == 'ham'].hist(column = 'message_length', bins = 150,ax = ax[0], color = 'green')
ax[0].set(xlabel = 'Length Class', ylabel = 'Frequency', title = 'HAM')

data[data['label'] == 'spam'].hist(column = 'message_length', bins = 50, ax = ax[1], color = 'red')
ax[1].set(xlabel = 'Length Class',ylabel = 'Frequency',title = 'SPAM')<jupyter_output><empty_output><jupyter_text>**Observations:**- Thats interesting! Through the basic EDA we've been able to discover a trend that spam messages tend to have more characters.<jupyter_code># Lets Look statastically
print(data[data.label == 'ham'].describe())
print('*'*110)
print(data[data.label == 'spam'].describe())<jupyter_output>numeric_label  message_length
count         4825.0     4825.000000
mean             0.0       71.023627
std              0.0       58.016023
min              0.0        2.000000
25%              0.0       33.000000
50%              0.0       52.000000
75%              0.0       92.000000
max              0.0      910.000000
**************************************************************************************************************
       numeric_label  message_length
count          747.0      747.000000
mean             1.0      138.866131
std              0.0       29.183082
min              1.0       13.000000
25%              1.0      132.500000
50%              1.0      149.000000
75%              1.0      157.000000
max              1.0      224.000000<jupyter_text>4. Preprocessing - Our main issue with our data is that it is all in text format (strings). The classification algorithms that we've learned about so far will need some sort of numerical feature vector in order to perform the classification task.- There are actually many methods to convert a corpus to a vector format. The simplest is the the bag-of-words approach, where each unique word in a text will be represented by one number.  Data Cleaning - text_preprocessing() function takes a sentence as it’s input and returns a cleaned sentence. This function takes care of the following<jupyter_code># Defining Certain Functions
def text_preprocessing(message):
    """
    Takes in a string of text, then performs the following:
    1. Remove all punctuation
    2. Remove all stopwords
    3. Returns a list of the cleaned text
    """
    
    STOPWORDS = stopwords.words('english') + ['u', 'ü', 'ur', '4', '2', 'im', 'dont', 'doin', 'ure']
    
    # Check characters to see if they are in punctuation
    nopunc = [char for char in message if char not in string.punctuation]

    # Join the characters again to form the string.
    nopunc = ''.join(nopunc)
    
    # Now just remove any stopwords
    return ' '.join([word for word in nopunc.split() if word.lower() not in STOPWORDS])
# Now let's "tokenize" these messages. Tokenization is just the term used to describe the process of converting the
# normal text strings in to a list of tokens (words that we actually want).

data['cleaned_message'] = data.message.apply(text_preprocessing)
data.head()
# Lets visualise the most_common words from the data
words = data[data.label == 'ham'].cleaned_message.apply(lambda x: [word.lower() for word in x.split()])
ham_words = Counter()

for msg in words:
    ham_words.update(msg)
    
print(ham_words.most_common(50))
words = data[data.label == 'spam'].cleaned_message.apply(lambda x: [word.lower() for word in x.split()])
spam_words = Counter()

for msg in words:
    spam_words.update(msg)
    
print(spam_words.most_common(50))<jupyter_output>[('call', 347), ('free', 216), ('txt', 150), ('mobile', 123), ('text', 120), ('claim', 113), ('stop', 113), ('reply', 101), ('prize', 92), ('get', 83), ('new', 69), ('send', 67), ('nokia', 65), ('urgent', 63), ('cash', 62), ('win', 60), ('contact', 56), ('service', 55), ('please', 52), ('guaranteed', 50), ('customer', 49), ('16', 49), ('week', 49), ('tone', 48), ('per', 46), ('phone', 45), ('18', 43), ('chat', 42), ('awarded', 38), ('draw', 38), ('latest', 36), ('å£1000', 35), ('line', 35), ('150ppm', 34), ('mins', 34), ('receive', 33), ('camera', 33), ('1', 33), ('every', 33), ('message', 32), ('holiday', 32), ('landline', 32), ('shows', 31), ('å£2000', 31), ('go', 31), ('box', 30), ('number', 30), ('apply', 29), ('code', 29), ('live', 29)]<jupyter_text>Train_Test_Splitting<jupyter_code>x = data.cleaned_message
y = data.numeric_label
print(x.shape)
print(y.shape)
# split X and y into training and testing sets 

x_train, x_test, y_train, y_test = train_test_split (x, y, random_state = 1, test_size = 0.2)

print(x_train.shape)
print(x_test.shape)
print(y_train.shape)
print(y_test.shape)
x_train
y_train
x_test
y_test<jupyter_output><empty_output><jupyter_text>Vectorization  1. Bag - of - Words  - Currently, we have the messages as lists of tokens (also known as lemmas) and now we need to convert each of those messages into a vector the SciKit Learn's algorithm models can work with.- Now we'll convert each message, represented as a list of tokens (lemmas) above, into a vector that machine learning models can understand.- We'll do that in three steps using the bag-of-words model:    1. Count how many times does a word occur in each message (Known as term frequency)    2. Weigh the counts, so that frequent tokens get lower weight (inverse document frequency)    3. Normalize the vectors to unit length, to abstract from the original text length (L2 norm)<jupyter_code># Instantiate the vectorizer
cv = CountVectorizer(stop_words = 'english', ngram_range = (1, 3), max_df = 0.5, min_df = 2)

# Learn the training data vocabulary, then use it to create a document-term matrix
bow_train = cv.fit_transform(x_train)
print('Shape of Sparse Matrix: ', bow_train.shape)
print('Amount of Non-Zero occurences: ', bow_train.nnz)
# Transform test data (using fitted vocabulary) into a document-term matrix
bow_test = cv.transform(x_test)
bow_test
print('Shape of Sparse Matrix: ', bow_test.shape)
print('Amount of Non-Zero occurences: ', bow_test.nnz)<jupyter_output>Shape of Sparse Matrix:  (1115, 9445)
Amount of Non-Zero occurences:  8865<jupyter_text>2. TF-IDF - TF-IDF stands for term frequency-inverse document frequency, and the tf-idf weight is a weight often used in information retrieval and text mining. - This weight is a statistical measure used to evaluate how important a word is to a document in a collection or corpus. - The importance increases proportionally to the number of times a word appears in the document but is offset by the frequency of the word in the corpus. - Variations of the tf-idf weighting scheme are often used by search engines as a central tool in scoring and ranking a document's relevance given a user query.<jupyter_code># Instantiate the vectorizer
tv = TfidfTransformer()

# Learn the training data vocabulary, then use it to create a document-term matrix
tfidf_train = tv.fit_transform(bow_train)
print('Shape of Sparse Matrix: ', tfidf_train.shape)
print('Amount of Non-Zero occurences: ', tfidf_train.nnz)
# Transform test data (using fitted vocabulary) into a document-term matrix
tfidf_test = tv.transform(bow_test)
tfidf_test
print('Shape of Sparse Matrix: ', tfidf_test.shape)
print('Amount of Non-Zero occurences: ', tfidf_test.nnz)<jupyter_output>Shape of Sparse Matrix:  (1115, 9445)
Amount of Non-Zero occurences:  8865<jupyter_text>5. Building and evaluating a model  1. Naive Bayes - The multinomial Naive Bayes classifier is suitable for classification with discrete features (e.g., word counts for text classification). - The multinomial distribution normally requires integer feature counts. However, in practice, fractional counts such as tf-idf may also work.- It is the baseline model for NLP classification process   Training<jupyter_code># Instantiate the classifier
nb = MultinomialNB()

# train the model using tfidf_train (timing it with an IPython "magic command")
%time nb.fit(tfidf_train, y_train)<jupyter_output>Wall time: 3.99 ms<jupyter_text>Evaluation<jupyter_code># Make class predictions for tfidf_test
y_pred_nb = nb.predict(tfidf_test)
# Calculate accuracy of class predictions
metrics.accuracy_score(y_test, y_pred_nb)
# Print the confusion matrix
metrics.confusion_matrix(y_test, y_pred_nb)
# print message text for false positives (ham incorrectly classifier)
x_test[(y_pred_nb == 1) & (y_test == 0)]
# x_test[y_pred_nb > y_test]
# print message text for false negatives (spam incorrectly classifier)
x_test[(y_pred_nb == 0) & (y_test == 1)]
# x_test[y_pred_nb < y_test]
# Calculate AUC
metrics.roc_auc_score(y_test, y_pred_nb)<jupyter_output><empty_output><jupyter_text>**Observations:**- In the above "Evaluation", we evaluated accuracy on on test data whcih is representative of true predictive performance.- We get the accuracy of about 97.04% on unseen data.  2. Logistic Regression - Logistic regression, despite its name, is a linear model for classification rather than regression. Logistic regression is also known in the literature as logit regression, maximum-entropy classification (MaxEnt) or the log-linear classifier. - In this model, the probabilities describing the possible outcomes of a single trial are modeled using a logistic function.  Training<jupyter_code># Instantiate the classifier
log_reg = LogisticRegression(solver = 'liblinear')

# train the model using tfidf_train (timing it with an IPython "magic command")
%time log_reg.fit(tfidf_train, y_train)<jupyter_output>Wall time: 3.48 s<jupyter_text>Evaluation<jupyter_code># Make class predictions for tfidf_test
y_pred_lr = log_reg.predict(tfidf_test)
# Calculate accuracy of class predictions
metrics.accuracy_score(y_test, y_pred_lr)
# Print the confusion matrix
metrics.confusion_matrix(y_test, y_pred_lr)
# print message text for false positives (ham incorrectly classifier)
x_test[(y_pred_lr == 1) & (y_test==0)]
# x_test[y_pred_nb > y_test]
# print message text for false negatives (spam incorrectly classifier)
x_test[y_pred_lr < y_test]<jupyter_output><empty_output><jupyter_text>**Observations:**- In the above "Evaluation", we evaluated accuracy on on test data whcih is representative of true predictive performance.- We get the accuracy of about 97.04% on unseen data.  3. SVM  Training<jupyter_code># Instantiate the classifier
svms = SVC()

# train the model using tfidf_train (timing it with an IPython "magic command")
%time svms.fit(tfidf_train, y_train)<jupyter_output>Wall time: 4.22 s<jupyter_text>Evaluation<jupyter_code># Make class predictions for tfidf_test
y_pred_svm = svms.predict(tfidf_test)
# Calculate accuracy of class predictions
metrics.accuracy_score(y_test, y_pred_svm)
# Print the confusion matrix
metrics.confusion_matrix(y_test, y_pred_svm)
# print message text for false positives (ham incorrectly classifier)
x_test[(y_pred_svm == 1) & (y_test==0)]
# x_test[y_pred_nb > y_test]
# print message text for false negatives (spam incorrectly classifier)
x_test[y_pred_svm < y_test]<jupyter_output><empty_output><jupyter_text>**Observations:**- In the above "Evaluation", we evaluated accuracy on on test data whcih is representative of true predictive performance.- We get the accuracy of about 98.56% on unseen data.  4. Random Forest Classifier  Training<jupyter_code># Instantiate the classifier
rf = RandomForestClassifier()

# train the model using tfidf_train (timing it with an IPython "magic command")
%time rf.fit(tfidf_train, y_train)<jupyter_output>Wall time: 3.95 s<jupyter_text>Evaluation<jupyter_code># Make class predictions for tfidf_test
y_pred_rf = rf.predict(tfidf_test)
# Calculate accuracy of class predictions
metrics.accuracy_score(y_test, y_pred_rf)
# Print the confusion matrix
metrics.confusion_matrix(y_test, y_pred_rf)
# print message text for false positives (ham incorrectly classifier)
x_test[(y_pred_rf == 1) & (y_test==0)]
# x_test[y_pred_nb > y_test]
# print message text for false negatives (spam incorrectly classifier)
x_test[y_pred_rf < y_test]<jupyter_output><empty_output><jupyter_text>**Observations:**- In the above "Evaluation", we evaluated accuracy on on test data whcih is representative of true predictive performance.- We get the accuracy of about 98.47% on unseen data.  Model Comparisons<jupyter_code>print ("Accuracy Scores :")
print ("---------------------------------------------------------------------------------------------------------------")
print ("Naive Bayes                             -    Accuracy By Using  Naive Bayes              :: 0.9704035874439462")
print ("Logistic Regression                     -    Accuracy By Using  Logistic Regression      :: 0.9704035874439462")
print ("Support Vector Machine                  -    Accuracy By Using  Support Vector Machine   :: 0.9856502242152466")
print ("Random Forest Classifier                -    Accuracy By Using  Random Forest Classifier :: 0.9847533632286996")
print ("---------------------------------------------------------------------------------------------------------------")<jupyter_output>Accuracy Scores :
---------------------------------------------------------------------------------------------------------------
Naive Bayes                             -    Accuracy By Using  Naive Bayes              :: 0.9704035874439462
Logistic Regression                     -    Accuracy By Using  Logistic Regression      :: 0.9704035874439462
Support Vector Machine                  -    Accuracy By Using  Support Vector Machine   :: 0.9856502242152466
Random Forest Classifier                -    Accuracy By Using  Random Forest Classifier :: 0.9847533632286996
---------------------------------------------------------------------------------------------------------------
{'fraction_non_alphanumeric': 0.12826271700861963, 'fraction_numerical': 0.03520699283719801, 'mean_word_length': 3.9355901737567405, 'pattern_counts': {'":': 0, '<': 68, '<?xml version=': 0, '>': 68, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '229667', 'license': 'MIT', 'n_tokens_mistral': 5340, 'n_tokens_neox': 4777, 'n_words': 1785, 'path': 'SMS Spam Detection Using NLP and ML.ipynb', 'repo_name': 'sarthak-kasturiwale/SMS-Spam-Detection-Using-NLP-and-ML', 'size': '16474'}