SMS Spam Detection
with Machine Learning
A complete end-to-end NLP pipeline that classifies SMS messages as spam or legitimate using TF-IDF feature extraction and four machine learning classifiers — achieving 98.55% accuracy with a linear Support Vector Classifier.
🎯 Project Overview
This project builds and evaluates a complete machine learning pipeline for SMS spam detection using the UCI SMS Spam Collection dataset. The pipeline covers the full data science workflow: data loading and cleaning, exploratory data analysis, NLP text preprocessing, TF-IDF feature engineering, training and comparing four classifiers, hyperparameter tuning with GridSearchCV, and a live inference function for predicting new messages.
The project was designed to demonstrate proficiency in Natural Language Processing, supervised machine learning, data analysis, and the end-to-end modelling workflow using Python's scientific stack — core skills for data science and AI roles.
📦 Dataset
The UCI SMS Spam Collection contains 5,572 SMS messages labelled as either ham (legitimate) or spam. After removing 403 duplicate entries, the working dataset contains 5,169 messages.
| Class | Count | Proportion | Avg. Characters |
|---|---|---|---|
| HAM | 4,516 | 87.4% | ~70.5 |
| SPAM | 653 | 12.6% | ~137.9 |
| Total | 5,169 | 100% | — |
The dataset is imbalanced (87% ham / 13% spam), which means raw accuracy alone is a misleading metric. Precision, recall, and F1-Score are the primary evaluation measures throughout this project.
📊 Exploratory Data Analysis
Several engineered features reveal clear statistical separations between spam and ham even before any NLP is applied:
- Character count: Spam messages average ~138 characters — nearly double the ~71 characters in ham messages.
- Punctuation/symbol count: Spam contains significantly more exclamation marks, pound signs, dollar signs, and asterisks.
- Digit count: Phone numbers, prize amounts, and short codes make spam messages digit-heavy.
- Uppercase ratio: Spam frequently uses ALL CAPS for urgency signals like "WINNER", "FREE", "CLAIM".
- Unique words: Spam has a narrower vocabulary — it repeats promotional trigger words.
These observations confirm that text length and character composition are strong discriminating features that complement the TF-IDF representation.
🔤 NLP Text Preprocessing
The following preprocessing pipeline is applied to every message:
- Character filtering: Remove all non-alphabetic characters (digits, punctuation, symbols).
- Lowercasing: Normalise all tokens to lowercase.
- Stopword removal: Strip common English stopwords using NLTK's corpus (e.g. "the", "is", "at").
- Porter stemming: Reduce words to their root form — "calling" → "call", "winning" → "win".
def preprocess_text(text: str) -> str:
text = re.sub(r'[^a-zA-Z]', ' ', text).lower()
tokens = [
ps.stem(word)
for word in text.split()
if word not in stop_words and len(word) > 1
]
return ' '.join(tokens)
⚙️ Feature Engineering — TF-IDF
Term Frequency-Inverse Document Frequency (TF-IDF) converts the cleaned text into a numerical feature matrix. Key configuration choices:
- max_features=3,000: Retain the 3,000 most informative terms.
- ngram_range=(1,2): Include both unigrams and bigrams to capture phrase-level patterns such as "free entry", "cash prize", "call now".
- min_df=2: Ignore terms appearing in fewer than 2 documents (reduces noise).
- sublinear_tf=True: Apply log-normalisation to term frequencies, preventing high-frequency terms from dominating.
- Data leakage prevention: The vectoriser is fitted only on the training set and applied (transform-only) to the test set.
🤖 Models Trained
Four classifiers were trained and compared. All were subsequently tuned using GridSearchCV with 5-fold cross-validation:
| Model | Tuned Parameter | Accuracy | Precision | Recall | F1-Score |
|---|---|---|---|---|---|
| Multinomial Naive Bayes | alpha=0.1 | 0.9720 | 0.9811 | 0.7939 | 0.8776 |
| Logistic Regression | C=1.0 | 0.9662 | 1.0000 | 0.7328 | 0.8458 |
| Support Vector Classifier ★ | C=1.0 | 0.9855 | 0.9833 | 0.9008 | 0.9402 |
| Random Forest | n_estimators=100 | 0.9778 | 0.9909 | 0.8321 | 0.9046 |
📈 Key Results
- Best overall model: Support Vector Classifier (linear kernel) — achieved the highest F1-Score of 0.9402 and accuracy of 98.55%, correctly identifying 90% of spam messages while maintaining 98.3% precision.
- Logistic Regression achieved perfect precision (1.0000) — it never misclassified a legitimate message as spam — but at the cost of lower recall (0.7328), meaning it missed ~27% of spam.
- Random Forest delivered the second-best F1-Score (0.9046) with extremely high precision (0.9909), making it suitable for applications where false positives are especially costly.
- TF-IDF bigrams significantly improved spam detection by capturing multi-word trigger phrases that individual word tokens would miss.
🔁 Pipeline Workflow
- Data Loading: Read CSV with latin-1 encoding; drop unnamed metadata columns.
- Cleaning: Rename columns, remove duplicates, encode labels (ham=0, spam=1).
- Feature Engineering: Derive character count, word count, punctuation count, digit count, uppercase ratio.
- EDA: Class distribution analysis, feature distribution histograms, boxplots, correlation heatmap, top-word frequency charts.
- Text Preprocessing: Regex cleaning → lowercase → stopword removal → Porter stemming.
- Vectorisation: Stratified 80/20 train-test split; TF-IDF (3,000 features, bigrams).
- Training: Fit all four classifiers on training vectors.
- Hyperparameter Tuning: GridSearchCV with 5-fold CV to optimise each model.
- Evaluation: Accuracy, Precision, Recall, F1, ROC-AUC, confusion matrices, feature importance.
- Inference: Live prediction pipeline for new unseen messages.
🔍 Inference Pipeline
The prediction function applies the same preprocessing and vectorisation used in training, then classifies the message:
def predict_sms(message, model, vectorizer):
cleaned = preprocess_text(message)
vectorised = vectorizer.transform([cleaned]).toarray()
prediction = model.predict(vectorised)[0]
return "SPAM" if prediction == 1 else "HAM (Legitimate)"
# Example
msg = "WINNER! You have won $1000 cash. Reply CLAIM to receive."
predict_sms(msg, svc_model, tfidf)
# Output: 🚨 SPAM (Confidence: Spam=94.2%)
📸 Notebook Sections
The full Jupyter notebook contains the following visualisations and outputs. Screenshots and rendered outputs are available in the GitHub repository.
Class Distribution
Pie & Bar Charts
Feature Distribution
Histograms (6 features)
Correlation Heatmap
Engineered Features
Top 20 Words
Ham vs Spam
Model Performance
Grouped Bar Chart
Radar Chart
All 4 Models
Confusion Matrices
All 4 Models
ROC Curves
with AUC Scores