Machine Learning · NLP · Data Science · Python

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.

Dataset: UCI SMS Spam Collection
Messages: 5,169 (after deduplication)
Best Model: SVC (linear kernel)
Best F1-Score: 0.9402
View Project on GitHub

🎯 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:

  1. Character filtering: Remove all non-alphabetic characters (digits, punctuation, symbols).
  2. Lowercasing: Normalise all tokens to lowercase.
  3. Stopword removal: Strip common English stopwords using NLTK's corpus (e.g. "the", "is", "at").
  4. 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

  1. Data Loading: Read CSV with latin-1 encoding; drop unnamed metadata columns.
  2. Cleaning: Rename columns, remove duplicates, encode labels (ham=0, spam=1).
  3. Feature Engineering: Derive character count, word count, punctuation count, digit count, uppercase ratio.
  4. EDA: Class distribution analysis, feature distribution histograms, boxplots, correlation heatmap, top-word frequency charts.
  5. Text Preprocessing: Regex cleaning → lowercase → stopword removal → Porter stemming.
  6. Vectorisation: Stratified 80/20 train-test split; TF-IDF (3,000 features, bigrams).
  7. Training: Fit all four classifiers on training vectors.
  8. Hyperparameter Tuning: GridSearchCV with 5-fold CV to optimise each model.
  9. Evaluation: Accuracy, Precision, Recall, F1, ROC-AUC, confusion matrices, feature importance.
  10. 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
View Full Project on GitHub