The previous chapter built the vocabulary of text representation. Now we use it. Every project here follows the same skeleton: load text data, clean it, convert it to numbers with a representation, train a classifier, and measure the result. Once you can repeat that skeleton, the specific dataset hardly matters.
The classic starter dataset is a collection of SMS or email messages labelled spam (unwanted) or ham (legitimate). Each message is a short document, which makes it perfect for trying representations quickly. Load the data and inspect the class balance first:
import pandas as pd
df = pd.read_csv("messages.csv")
print(df["label"].value_counts())
print(df.head())
An imbalanced set needs handling, so always check the distribution before training.
The first model uses a bag of words and a Naive Bayes or logistic regression classifier. The plan:
CountVectorizer on the training text only.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, confusion_matrix
X_train, X_test, y_train, y_test = train_test_split(
df["message"], df["label"], test_size=0.2, random_state=42)
vectorizer = CountVectorizer(stop_words="english")
X_train_vec = vectorizer.fit_transform(X_train)
X_test_vec = vectorizer.transform(X_test)
model = MultinomialNB()
model.fit(X_train_vec, y_train)
preds = model.predict(X_test_vec)
print(accuracy_score(y_test, preds))
print(confusion_matrix(y_test, preds))
BOW plus Naive Bayes is a classic pair for spam because spam messages share heavy word overlap.
The same pipeline with TfidfVectorizer often performs better, because it de-emphasises words like "the" and "you" that appear everywhere and rewards the distinctive terms spammers use:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
tfidf = TfidfVectorizer(stop_words="english", ngram_range=(1, 2))
X_train_vec = tfidf.fit_transform(X_train)
X_test_vec = tfidf.transform(X_test)
model = LogisticRegression(max_iter=1000)
model.fit(X_train_vec, y_train)
preds = model.predict(X_test_vec)
print(accuracy_score(y_test, preds))
Adding bigrams lets the model catch phrases, and comparing the two representations on the same split tells you which suits your data.
fit on the test text. The vectorizer must be fitted on the training set alone and only transformed on test data, otherwise the model's score is misleadingly optimistic.
Across these projects a handful of habits separate reliable results from lucky ones:
Counting representations work, but word embeddings capture meaning. This project classifies messages by converting them to averaged Word2Vec vectors.
Train or load a Word2Vec model, then write a helper that averages the vectors of every word in a message. Messages with no known words become a zero vector:
import numpy as np
from gensim.models import Word2Vec
sentences = [msg.split() for msg in df["message"]]
w2v = Word2Vec(sentences, vector_size=100, window=5,
min_count=1, sg=1, epochs=10)
def avg_vector(text):
tokens = text.split()
vectors = [w2v.wv[w] for w in tokens if w in w2v.wv]
if not vectors:
return np.zeros(w2v.vector_size)
return np.mean(vectors, axis=0)
X = np.array([avg_vector(msg) for msg in df["message"]])
Once each message is a fixed-size vector, any normal classifier applies:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, df["label"], test_size=0.2, random_state=42)
model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train)
print(model.score(X_test, y_test))
Averaging loses word order, but it is fast and often competitive with TF-IDF, while giving you vectors you can reuse for clustering and similarity.
The Kindle review dataset contains book reviews with star ratings. Converting ratings into positive and negative sentiment gives a natural classification task: reviews with four or five stars are positive, one or two stars are negative, and three stars are dropped to keep the signal clear.
import pandas as pd
reviews = pd.read_csv("kindle_reviews.csv")
reviews["label"] = reviews["rating"].apply(
lambda r: "pos" if r >= 4 else ("neg" if r <= 2 else None)
)
reviews = reviews.dropna(subset=["label"])
print(reviews["label"].value_counts())
print(reviews["review"][0])
Exploration should also look at review lengths and a few samples per class, so you know what the model is actually reading.
Reuse the pipeline: split, vectorize with TF-IDF (bigrams help sentiment), train, and report a confusion matrix alongside accuracy:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
reviews["review"], reviews["label"],
test_size=0.2, random_state=42)
vec = TfidfVectorizer(stop_words="english", ngram_range=(1, 2))
X_tr = vec.fit_transform(X_train)
X_te = vec.transform(X_test)
model = LogisticRegression(max_iter=1000)
model.fit(X_tr, y_train)
print(classification_report(y_test, model.predict(X_te)))
Use classification_report so you see precision and recall for both classes. A model that says "positive" for everything would score decently on accuracy but uselessly on negative reviews, and the report exposes exactly that.
You now have three representations and can reason about when to reach for each:
None is universally best; the winning choice depends on your dataset. That is why every project in this chapter evaluates the same split with each representation and lets the scores decide.