01Loading the dataset
We'll use the SMS Spam Collection dataset from the UCI Machine Learning Repository — 5,572 real SMS messages, each labelled as "spam" or "ham" (legitimate).
import pandas as pd
# UCI SMS Spam Collection dataset (real labelled SMS messages)
url = "https://raw.githubusercontent.com/justmarkham/pycon-2016-tutorial/master/data/sms.tsv"
df = pd.read_csv(url, sep='\t', header=None, names=['label', 'message'])
print(df.shape)
print(df.head())
print(df['label'].value_counts())
(5572, 2) — 4,825 ham messages and 747 spam messages (~13.4% spam rate). Like our churn and loan datasets, this is imbalanced, so we'll keep that in mind during evaluation.
02Exploratory data analysis
For text data, useful EDA includes message length distribution and the most common words in each class — spam messages are often noticeably longer and contain distinctive vocabulary (free, win, claim, urgent).
import matplotlib.pyplot as plt
import seaborn as sns
df['length'] = df['message'].apply(len)
plt.figure(figsize=(8, 5))
sns.histplot(data=df, x='length', hue='label', bins=50,
palette=['#1a4a3a', '#e24b4a'], alpha=0.6)
plt.title('Message Length by Class')
plt.savefig('message_length.png')
print(df.groupby('label')['length'].mean())
Spam messages average ~139 characters vs ~71 characters for ham — spam tends to be noticeably longer, since it usually contains a call-to-action, a link, or promotional text packed into the message.
03Text preprocessing & TF-IDF
Machine learning models need numbers, not raw text. We clean the text (lowercase, remove punctuation), then convert it to numeric vectors using TF-IDF (Term Frequency-Inverse Document Frequency), which weighs words by how distinctive they are to a message, not just how often they appear.
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
def clean_text(text):
text = text.lower()
text = re.sub(r'[^a-z\s]', '', text) # remove punctuation/numbers
return text
df['clean_message'] = df['message'].apply(clean_text)
df['label_num'] = df['label'].map({'ham': 0, 'spam': 1})
# Train/test split on raw text first
X_train_text, X_test_text, y_train, y_test = train_test_split(
df['clean_message'], df['label_num'],
test_size=0.2, random_state=42, stratify=df['label_num']
)
# TF-IDF vectorisation — fit only on training data to avoid data leakage
vectorizer = TfidfVectorizer(stop_words='english', max_features=3000)
X_train = vectorizer.fit_transform(X_train_text)
X_test = vectorizer.transform(X_test_text)
print(f"Vocabulary size: {len(vectorizer.vocabulary_)}")
print(f"Training matrix shape: {X_train.shape}")
04Training the model
Multinomial Naive Bayes is the classical algorithm for text classification — it's fast, works well on high-dimensional sparse data like TF-IDF vectors, and despite its simplicity (it "naively" assumes word independence), performs remarkably well on spam detection.
from sklearn.naive_bayes import MultinomialNB
model = MultinomialNB()
model.fit(X_train, y_train)
# Inspect which words are most indicative of spam
feature_names = vectorizer.get_feature_names_out()
spam_log_prob = model.feature_log_prob_[1]
top_spam_words = pd.DataFrame({
'word': feature_names,
'log_prob': spam_log_prob
}).sort_values('log_prob', ascending=False).head(15)
print(top_spam_words)
Words like "free", "win", "call", "claim", "prize", and "urgent" top the list — exactly the vocabulary a human would intuitively flag, confirming the model has learned genuinely meaningful patterns.
05Evaluation
For spam filters, precision matters enormously — a false positive (legitimate message marked as spam) is far more annoying to a user than an occasional spam message slipping through. We check this tradeoff explicitly.
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred, target_names=['Ham', 'Spam']))
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Greens',
xticklabels=['Ham', 'Spam'], yticklabels=['Ham', 'Spam'])
plt.title('Confusion Matrix')
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.savefig('spam_confusion_matrix.png')
# Try it on a new message
new_message = ["Congratulations! You've won a free iPhone. Click here to claim now!"]
new_vec = vectorizer.transform(new_message)
prediction = model.predict(new_vec)
print("Spam" if prediction[0] == 1 else "Ham")
| Metric | Spam Class | What it means |
|---|---|---|
| Precision | 0.97 | 97% of messages flagged as spam are actually spam |
| Recall | 0.89 | Model catches 89% of all actual spam messages |
| F1-Score | 0.93 | Strong overall balance of precision and recall |
The high precision (0.97) is exactly what you want in a spam filter — very few legitimate messages get wrongly blocked. The slightly lower recall (0.89) means a small fraction of spam still gets through, which is a far more acceptable failure mode than blocking real messages.
06Real-world use case
Where this exact technique is used in industry
This project introduces the foundation of NLP: turning unstructured text into structured numeric features (TF-IDF) that any classical ML algorithm can use. The same pipeline — clean text → vectorise → classify — underlies sentiment analysis, topic classification and many other text-based ML tasks.
Want to build this with guided support?
Text preprocessing, TF-IDF and NLP fundamentals are all covered hands-on in our Artificial Intelligence training programme.
View Training Programs WhatsApp Us