AISeptember 01, 20254 min read

Spam Message Detector using Python and Streamlit

Detect spam messages with Python and Streamlit — train a Naive Bayes classifier on real SMS data and test any message with a spam probability.

Galvan

Galvan

Founder & Creator

Introduction

Spam filtering is the perfect first machine-learning project: the data is real, the problem is binary, and the classic algorithm — Naive Bayes — is simple enough to understand completely. This app trains a classifier on the famous SMS Spam Collection (5,574 real messages), reports honest accuracy metrics, and lets you test any message live with a spam probability.

It is the trained-model counterpart to the rule-based sentiment analyzer: VADER looks words up in a lexicon; this model *learns* which words mean spam from data.

Features

  • Real training data — the SMS Spam Collection, downloaded automatically.
  • TF-IDF features — words weighted by distinctiveness, not just counts.
  • Live testing — type any message, get a spam probability.
  • Word-level explanations — see which words pushed the verdict.
  • Honest metrics — precision, recall, and confusion matrix on held-out data.
  • Prerequisites

  • Python 3.9+ — from python.org.
  • Dependencies:
  • code
    pip install streamlit scikit-learn pandas numpy

    Step 1: Create the Script

    Save as spam_detector.py:

    code
    import streamlit as st
    import pandas as pd
    import numpy as np
    import re
    import zipfile
    import urllib.request
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.naive_bayes import MultinomialNB
    from sklearn.model_selection import train_test_split
    from sklearn.metrics import precision_score, recall_score
    
    st.set_page_config(page_title="Spam Detector", page_icon="🚫")
    st.title("🚫 Spam Message Detector")
    
    
    @st.cache_data
    def load_data():
        url = "https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip"
        path = "sms_spam.zip"
        urllib.request.urlretrieve(url, path)
        with zipfile.ZipFile(path) as z:
            with z.open("SMSSpamCollection") as f:
                df = pd.read_csv(f, sep="\t", names=["label", "message"], encoding="latin-1")
        return df
    
    
    def clean(text):
        return re.sub(r"[^a-z0-9 ]", " ", text.lower())
    
    
    df = load_data()
    st.caption(f"Trained on {len(df):,} messages ({(df.label == 'spam').sum()} spam)")
    
    vec = TfidfVectorizer(stop_words="english", ngram_range=(1, 2))
    X = vec.fit_transform(df["message"].map(clean))
    y = (df["label"] == "spam").astype(int)
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
    model = MultinomialNB()
    model.fit(X_train, y_train)
    
    pred = model.predict(X_test)
    c1, c2, c3 = st.columns(3)
    c1.metric("Precision", f"{precision_score(y_test, pred):.0%}")
    c2.metric("Recall", f"{recall_score(y_test, pred):.0%}")
    c3.metric("Spam rate", f"{y.mean():.1%}")
    
    st.divider()
    st.subheader("Test a message")
    msg = st.text_area("Message", height=90, placeholder="CONGRATULATIONS! You've won a free prize...")
    
    if st.button("🔎 Analyze", type="primary") and msg.strip():
        prob = model.predict_proba(vec.transform([clean(msg)]))[0][1]
        verdict = "🚫 SPAM" if prob > 0.5 else "✅ Not spam"
        st.markdown(f"### {verdict} — {prob:.0%} spam probability")
        st.progress(float(prob))
    
        words = clean(msg).split()
        spam_words = ["free", "win", "winner", "prize", "claim", "urgent", "congratulations",
                      "offer", "cash", "bonus", "limited", "click", "txt", "mobile"]
        hits = [w for w in words if w in spam_words]
        if hits:
            st.caption(f"Spammy words spotted: {', '.join(hits)}")

    Step 2: Run the App

    code
    streamlit run spam_detector.py

    Test a genuine message ("hey, running late, be there in 10") versus a classic scam — watch the probability swing.

    How It Works

    TF-IDF converts messages to numbers: term frequency (how often a word appears) times inverse document frequency (how rare it is across all messages). "Free" appearing everywhere gets a low score; "ringtone" appearing mostly in spam gets a high one. The ngram_range=(1, 2) adds word *pairs*, capturing phrases like "call now" that single words miss.

    Multinomial Naive Bayes then applies Bayes' theorem with a deliberate simplification — it treats words as independent. That's wrong about language and right about spam: the evidence piles up so strongly that the naive assumption barely hurts. It's also blazingly fast, which is why it powered production spam filters for a decade.

    The metrics matter more than accuracy: spam is only ~13% of the data, so a model saying "never spam" scores 87% accuracy while being useless. Precision (when we say spam, are we right?) and recall (of all spam, how much did we catch?) are the honest numbers — the same lesson the forecasting app's backtest teaches.

    The word-spotting footer is a transparency bonus: it lists known spam markers in your message, giving users an intuitive hook for why the model decided what it did.

    Common Errors & Fixes

  • Download fails (403/timeout) — UCI occasionally rate-limits; host the zip yourself or load a local copy in load_data.
  • `empty vocabulary` error — your message cleaned down to nothing (only punctuation/stopwords); guard with if not clean(msg).strip().
  • Everything classifies as ham — check that labels mapped correctly (spam → 1); a flipped mapping inverts every verdict.
  • Slow on every rerun — training reruns each time; move the vectorizer+fit into @st.cache_resource keyed on nothing, since the dataset is static.
  • Key Concepts

  • TF-IDF — frequency weighted by rarity; the classic text feature.
  • Naive Bayes — independent-evidence classification, fast and strong.
  • Precision vs recall — the two ways to be wrong, and why accuracy lies on imbalanced data.
  • N-grams — word pairs capture phrase-level signals.
  • What to Try Next

  • Swap in LogisticRegression and compare precision/recall — one line change.
  • Show top spammy features by coefficient — the model's own explanation.
  • Add a threshold slider trading recall against precision live.
  • Classify your real inbox exports — the pipeline handles email text unchanged, and pairs with the email automation tutorial.
  • FAQ

    Why Naive Bayes over deep learning?

    For short-text spam with ~5K examples, NB matches complex models while training in milliseconds on CPU. Deep learning earns its complexity on bigger, messier problems.

    What does the probability actually mean?

    The model's calibrated confidence given its training data — not a guarantee. Treat 0.7 as "likely spam" and review borderline cases manually.

    Can spammers defeat it?

    Yes — deliberate misspellings ('fr3e') evade word features. Production filters fight back with character n-grams and continuous retraining; that arms race is the field.