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
Prerequisites
pip install streamlit scikit-learn pandas numpyStep 1: Create the Script
Save as spam_detector.py:
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
streamlit run spam_detector.pyTest 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
load_data.if not clean(msg).strip().spam → 1); a flipped mapping inverts every verdict.@st.cache_resource keyed on nothing, since the dataset is static.Key Concepts
What to Try Next
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.