From 5,169 messages to a working model, step by step
Introduction to Data Science and Machine Learning · Md Mahbub E Noor · University of Barishal
model = MultinomialNB() model.fit(X_train_counts, y_train)
The same seven steps work for most classification problems: plant disease photos, speech emotion, fraud.
"sms.tsv" instead of urlX = the messages (input), y = the labels (answer)import pandas as pd
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"])
df = df.drop_duplicates()
X = df["message"]
y = df["label"]
print(df.shape)(5169, 2)
stratify=y keeps the same spam share in bothrandom_state=42 makes the split repeatablefrom sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42,
stratify=y)
print(len(X_train), len(X_test))4135 1034
Models only understand numbers. CountVectorizer builds a table: one row per message, one column per word, each cell a count.
| Message | win | free | prize | now | lunch | class |
|---|---|---|---|---|---|---|
| Win a free prize, win now | 2 | 1 | 1 | 1 | 0 | 0 |
| Free lunch now | 0 | 1 | 0 | 1 | 1 | 0 |
| See you in class | 0 | 0 | 0 | 0 | 0 | 1 |
This is count_words() from Lecture 4, done for every message and arranged as a table. Word order is lost: it is a “bag of words”.
fit_transform on training data: learn the vocabulary, then counttransform on test data: count only, using that vocabularyfrom sklearn.feature_extraction.text import (
CountVectorizer)
vec = CountVectorizer()
X_train_counts = vec.fit_transform(X_train)
X_test_counts = vec.transform(X_test)
print(X_train_counts.shape)(4135, 7680)
fit reads the training messages and builds the vocabularytransform counts words using that vocabulary onlydemo = CountVectorizer() demo.fit(["win a free prize", "see you in class"]) print(demo.get_feature_names_out()) print(demo.transform(["free class tomorrow"]).toarray())
['class' 'free' 'in' 'prize' 'see' 'win' 'you'] [[1 1 0 0 0 0 0]]
fit = learn from examples: counts words per labelfrom sklearn.naive_bayes import MultinomialNB
model = MultinomialNB()
model.fit(X_train_counts, y_train)
print("Training done!")Training done!
predict labels all 1,034 test messagespredictions = model.predict(X_test_counts)
check = pd.DataFrame({"message": X_test.str[:28],
"truth": y_test,
"predicted": predictions})
print(check.sample(6, random_state=42))message truth predicted 2599 Okie... Thanx... ham ham 1207 As a SIM subscriber, you are spam spam 5147 Get your garden ready for su spam spam 3332 How much it will cost approx ham ham 788 Ever thought about living a spam ham 1972 Enjoy ur life. . Good night ham ham
from sklearn.metrics import accuracy_score
acc = accuracy_score(y_test, predictions)
baseline = (y_test == "ham").mean()
print(f"Model accuracy: {acc:.1%}")
print(f"Always say ham: {baseline:.1%}")Model accuracy: 98.2% Always say ham: 87.3%
Our model
Always says "ham"
from sklearn.metrics import confusion_matrix
print(confusion_matrix(y_test, predictions,
labels=["ham", "spam"]))[[898 5] [ 14 117]]
WATCH FOR
Silent video: press play, pause any time.
vec.transformpredict_proba gives [ham, spam] probabilitiestests = [
"Congratulations! You won a free prize. Call now!",
"Are we still meeting at the library tomorrow?",
"Your parcel is waiting, pay the delivery fee"]
new_counts = vec.transform(tests)
print(model.predict(new_counts))
print(model.predict_proba(new_counts).round(2))['spam' 'ham' 'ham'] [[0. 1. ] [1. 0. ] [0.83 0.17]]
import numpy as np words = vec.get_feature_names_out() lp = model.feature_log_prob_ diff = lp[1] - lp[0] top = np.argsort(diff)[-10:][::-1] print(list(words[top]))
['claim', 'prize', '150p', 'tone', 'co', 'www', '18', 'guaranteed', '500', '1000']
Mostly UK and Singapore messages from around 2011. New scam styles are unseen.
1,034 test messages, one random split. Another split gives a slightly different score.
It ignores word order, the sender and links. "FR33" tricks can still work.
A real message lost in the spam folder can matter more than one spam let through.
Everything from today in one cell. This short program is a complete, real machine learning project.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score
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"]).drop_duplicates()
X_tr, X_te, y_tr, y_te = train_test_split(
df["message"], df["label"], test_size=0.2,
random_state=42, stratify=df["label"])
vec = CountVectorizer()
model = MultinomialNB().fit(vec.fit_transform(X_tr), y_tr)
pred = model.predict(vec.transform(X_te))
print(f"Accuracy: {accuracy_score(y_te, pred):.1%}")Accuracy: 98.2%
Read the last line of an error first. It names the problem.
| What you typed | What Python says | What it means |
|---|---|---|
model.predict(["Win cash now"]) | ValueError: Expected 2D array, got 1D array instead: | The model needs numbers. Use model.predict(vec.transform([...])). |
vec2 = CountVectorizer() X_bad = vec2.fit_transform(X_test) model.predict(X_bad) | ValueError: X has 3320 features, but MultinomialNB is expecting 7680 features as input. | A new vocabulary no longer matches the model. Fit only on training data; use vec.transform for new text. |
In the lab you can
Opens in a new tab and works offline. All labs: labs/START_HERE.html
Due: [date, about two weeks from today]
Type the full pipeline in your own notebook, one step per cell, with a text cell explaining each step in your own words.
Report accuracy, the always-ham baseline, and the confusion matrix. Explain each cell of the matrix in one sentence.
Try CountVectorizer(stop_words="english") and random_state=7. What changes? Keep a small table of results.
Write 3 spam messages designed to slip through. Did they? Explain why they worked or failed.
Collect 10 SMS (5 spam, 5 normal), remove all personal details, and test them. How many are right? Why might this differ from the test score?
One paragraph: would you trust this filter on your own phone? What would you change first?
Watch it again at home to revise.