<jupyter_start><jupyter_text>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. The files contain one message per line. Each line is composed by two columns: class (spam or ham) and raw message. This data will be our labeled training set. Using these ham/spam examples, we'll train a machine learning model to learn to discriminate between ham/spam automatically. Then, with a trained model, we'll be able to classify arbitrary unlabeled messages as ham or spam.  Goal - To build a predictive model which will determine whether a text message is spam or ham.   DATA ANALYSIS  Imports required packages<jupyter_code>import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
%matplotlib inline

# Warnings
import warnings
warnings.filterwarnings('ignore')

# Text Preprocessing
import nltk
nltk.download("stopwords")
nltk.download('wordnet')
nltk.download('punkt')
from nltk.corpus import stopwords
import string
from nltk.tokenize import word_tokenize
from nltk.stem import SnowballStemmer
stemmer = SnowballStemmer("english")

from wordcloud import WordCloud

from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import cross_val_score
from sklearn.metrics import fbeta_score
from sklearn.naive_bayes import MultinomialNB
from sklearn.calibration import CalibratedClassifierCV
from sklearn.metrics import classification_report, f1_score, accuracy_score, confusion_matrix

from textblob import TextBlob
import pickle
#reading the data
data = pd.read_csv("spam.csv", encoding = "latin-1")

# data = data[['v1', 'v2']]
# data = data.rename(columns = {'v1': 'label', 'v2': 'text'})
#checking first few rows
data.head()
#removing unnecessary columns

data = data.drop(['Unnamed: 2','Unnamed: 3','Unnamed: 4'], axis = 1)
#again check first few rows of data
display(data.head(n = 5))
# Lets look at the dataset info to see if everything is alright

data.info()
# check the statistics of data
data.groupby('class').describe()<jupyter_output><empty_output><jupyter_text>There are some repetative messages in the data. Lets check the top messages of data to find the most repetative messages<jupyter_code>#check the top spam/ham messages

topMessages = data.groupby("message")["class"].agg([len, np.max]).sort_values(by = "len", ascending = False).head(n = 5)
display(topMessages)<jupyter_output><empty_output><jupyter_text>So. People are really busy it seems. "Sorry, i'll call later" tops the ham message list with 30 counts with "I cant pick the phone right now. Pls send a message" comes second with 12 counts.Theres a quite lot of Ok..., Okie and Ok. in there too   How long are the messages?<jupyter_code>#add new column to the dataframe data to see the length of every message

data['length'] = data['message'].map(lambda text: len(text))
print (data.head())
data.length.plot(bins=100, kind='hist')<jupyter_output><empty_output><jupyter_text>We can see here maximum messages contains number of words in the range of [0, 200].<jupyter_code>#check the statistics of the length of the message

data.length.describe()<jupyter_output><empty_output><jupyter_text>The average length of the message is 80, while the minimum length is 2 and maximun length is 910.  Is there any difference in message length between spam and ham?<jupyter_code>f, ax = plt.subplots(1, 2, figsize = (20, 6))

sns.distplot(data[data["class"] == "spam"]["length"], bins = 20, ax = ax[0])
ax[0].set_xlabel("Spam Message Word Length")

sns.distplot(data[data["class"] == "ham"]["length"], bins = 20, ax = ax[1])
ax[0].set_xlabel("Ham Message Word Length")

plt.show()<jupyter_output><empty_output><jupyter_text>Looks like spam messages are usually longer. Maybe messageLength can become a feature to predict whether the message is spam/ ham ?<jupyter_code>#Check what precentage of our data is spam/ham

data["class"].value_counts().plot(kind = 'pie', explode = [0,0.1], figsize = (6,6), autopct = '%1.1f%%', shadow = True)
plt.ylabel("Spam vs Ham")
plt.legend(["Ham", "Spam"])
plt.show()<jupyter_output><empty_output><jupyter_text>A lot of messages are actually not spam. About 87% of our dataset consists of normal messages. While we split our data set into train and test or when we use cross validation, we will have to use stratified sampling, otherwise we have a chance of our training model being skewed towards normal messages. If the sample we choose to train our model consists majorly of normal messages, it may end up predicting everything as ham and we might not be able to figure this out since most of the messages we get are actually ham and will have a pretty good accuracy.A very basic model would be a model that predicts everything as ham. It would have a decent accuracy. But then again, is that right? No. We will then have to use an accuracy metric that keeps this in mind. Goal : We don't mind if we miss the odd spam message but we surely don't want to mark a ham message as spam i.e Precision is very important. Hence we will use fbeta score as our accuracy metric with inclination towards Precision<jupyter_code>#Study individual Spam/ham words

spam_messages = data[data["class"] == "spam"]["message"]
ham_messages = data[data["class"] == "ham"]["message"]

spam_words = []
ham_words = []

def extractSpamWords(spamMessages):
    global spam_words
    words = [word.lower() for word in word_tokenize(spamMessages) if word.lower() not in stopwords.words("english") and word.lower().isalpha()]
    spam_words = spam_words + words
    
def extractHamWords(hamMessages):
    global ham_words
    words = [word.lower() for word in word_tokenize(hamMessages) if word.lower() not in stopwords.words("english") and word.lower().isalpha()]
    ham_words = ham_words + words

spam_messages.apply(extractSpamWords)
ham_messages.apply(extractHamWords)
#Spam Word cloud

spam_wordcloud = WordCloud(width=600, height=400).generate(" ".join(spam_words))
plt.figure( figsize=(10,8), facecolor='k')
plt.imshow(spam_wordcloud)
plt.axis("off")
plt.tight_layout(pad=0)
plt.show()
#Ham word cloud

ham_wordcloud = WordCloud(width=600, height=400).generate(" ".join(ham_words))
plt.figure( figsize=(10,8), facecolor='k')
plt.imshow(ham_wordcloud)
plt.axis("off")
plt.tight_layout(pad=0)
plt.show()
# Top 10 spam words

spam_words = np.array(spam_words)
print("Top 10 Spam words are :\n")
pd.Series(spam_words).value_counts().head(n = 10)
# Top 10 Ham words

ham_words = np.array(ham_words)
print("Top 10 Ham words are :\n")
pd.Series(ham_words).value_counts().head(n = 10)<jupyter_output>Top 10 Ham words are :<jupyter_text>Data Preprocessing Cleaning textual data is a little different than regular data cleaning. There is a much heavier emphasis on text normalisation than removing outliers or leverage points. When used correctly, it reduces noise, groups terms with similar semantic meanings and reduces computational costs by giving us a smaller matrix to work with.There are many types of Text Normalization.Like Case normalisation, Removing stop words, Removing punctuations and special symbols, Lemmatising/Stemming etc.For this particular classification problem, we will use case normalisation and removing stop wordds.<jupyter_code>def cleanText(message):
    
    message = message.translate(str.maketrans('', '', string.punctuation))
#     words = [stemmer.stem(word) for word in message.split() if word.lower() not in stopwords.words("english")]
    message = message.lower()
    
    return message

data["message"] = data["message"].apply(cleanText)
data.head(n = 10)<jupyter_output><empty_output><jupyter_text>Convert our clean text into a representation that a machine learning model can understand. We will use the Tfifd for this. Now we will convert each message, represented as a list of tokens (lemmas) above, into a vector that machine learning models can understand.Doing that requires essentially three steps, in the bag-of-words model:1. counting how many times does a word occur in each message (term frequency)2. weighting the counts, so that frequent tokens get lower weight (inverse document frequency)3. normalizing the vectors to unit length, to abstract from the original text length (L2 norm) Here we will use the TF-IDF vectorizer (Term Frequency — Inverse Document Frequency), an embedding technique which takes into account the importance of each term to document. TF-IDF vectorizes documents by calculating a TF-IDF statistic between the document and each term in the vocabulary. The document vector is constructed by using each statistic as an element in the vector. The TF-IDF statistic for term i in document j is calculated as follows: ![image.png](attachment:image.png)<jupyter_code>#specifing features and labels
labels = data['class']
text = data['message']
#splitting data into three sets - train, test and validation

X_train, X_remain, y_train, y_remain = train_test_split(text, labels, stratify = labels, random_state=0, test_size=0.3)

X_test, X_val, y_test, y_val = train_test_split(X_remain,y_remain,stratify = y_remain, random_state = 0, test_size = 0.5)
#check the shape of all three datasets

print("Training Data:",X_train.shape,y_train.shape )

print("Testing Data:", X_test.shape,y_test.shape)

print("Validation Data:", X_val.shape,y_val.shape )
#The CountVectorizer provides a simple way to 
#both tokenize a collection of text documents and build a vocabulary of known words, 
#but also to encode new documents using that vocabulary.

# create the transform
count_vec = CountVectorizer(stop_words='english')

# Learn the vocabulary dictionary and return term-document matrix.
X_train_counts = count_vec.fit_transform(X_train)


#Learn vocabulary and idf from training set.
tf_transformer = TfidfTransformer().fit(X_train_counts)

# Learn vocabulary and idf, return term-document matrix.
X_train_transformed = tf_transformer.transform(X_train_counts)

#test data
X_test_counts = count_vec.transform(X_test)
X_test_transformed = tf_transformer.transform(X_test_counts)

#converts classes into integer
labels = LabelEncoder()
y_train_labels_fit = labels.fit(y_train)
y_train_lables_trf = labels.transform(y_train)
#label mapping of integer to class
label_mapping = dict(zip(labels.classes_,labels.transform(labels.classes_)))
print(label_mapping)<jupyter_output>{'ham': 0, 'spam': 1}<jupyter_text>Model Building<jupyter_code>model = MultinomialNB()

clf = model.fit(X_train_transformed,y_train_lables_trf)


prediction = clf.predict(X_test_transformed)<jupyter_output><empty_output><jupyter_text>PrecisionPrecision is the number of True Positives divided by the number of True Positives and False Positives. Put another way, it is the number of positive predictions divided by the total number of positive class values predicted. It is also called the Positive Predictive Value (PPV). RecallRecall is the number of True Positives divided by the number of True Positives and the number of False Negatives. Put another way it is the number of positive predictions divided by the number of positive class values in the test data. It is also called Sensitivity or the True Positive Rate. F1 ScoreThe F1 Score is the 2*((precision*recall)/(precision+recall)). It is also called the F Score or the F Measure. Put another way, the F1 score conveys the balance between the precision and the recall.  Validation and Prediction<jupyter_code>#Check accuracy on test data
print('Average accuracy on test set={}'.format(np.mean(prediction == labels.transform(y_test))))
print ('Accuracy', accuracy_score(labels.transform(y_test), prediction))
print()
results = confusion_matrix(labels.transform(y_test), prediction)
print(results)
print()
print ('Report:')
print (classification_report(labels.transform(y_test), prediction))<jupyter_output>Accuracy 0.9629186602870813

[[724   0]
 [ 31  81]]

Report:
              precision    recall  f1-score   support

           0       0.96      1.00      0.98       724
           1       1.00      0.72      0.84       112

    accuracy                           0.96       836
   macro avg       0.98      0.86      0.91       836
weighted avg       0.96      0.96      0.96       836<jupyter_text>Deployment<jupyter_code>weights = 'spam_detection_mnb.sav'
pickle.dump(clf,open(weights,'wb'))

vocab = 'vocabulary_mnb.sav'
pickle.dump(count_vec, open(vocab, 'wb'))

tfidf = 'tfidf_mnb.sav'
pickle.dump(tf_transformer,open(tfidf,'wb'))<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup