Term Frequency › Code Implementation
Language AI HandbookAll chapters Term Frequency › Code ImplementationOne section · Sections in this chapter9 / 17 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement all five variants from scratch, then compare them to scikit-learn's output to verify our understanding.
First, set up our sample corpus and compute raw counts.
from collections import Counter
import numpy as np
# Sample corpus - three documents of different lengths
corpus = [
"the cat sat on the mat the cat sat", # 9 tokens
"the dog chased the cat across the yard", # 8 tokens
"a neural network learns patterns in data patterns patterns patterns", # 10 tokens
]
# Tokenize and build vocabulary
tokenized = [doc.lower().split() for doc in corpus]
vocab = sorted(set(word for doc in tokenized for word in doc))
word_to_idx = {word: i for i, word in enumerate(vocab)}
# Build raw count matrix
def build_count_matrix(tokenized_docs, vocab, word_to_idx):
"""Build term-document count matrix."""
n_docs = len(tokenized_docs)
n_terms = len(vocab)
matrix = np.zeros((n_docs, n_terms), dtype=float)
for doc_idx, tokens in enumerate(tokenized_docs):
counts = Counter(tokens)
for term, count in counts.items():
if term in word_to_idx:
matrix[doc_idx, word_to_idx[term]] = count
return matrix
count_matrix = build_count_matrix(tokenized, vocab, word_to_idx)Vocabulary (16 terms): ['a', 'across', 'cat', 'chased', 'data', 'dog', 'in', 'learns', 'mat', 'network', 'neural', 'on', 'patterns', 'sat', 'the', 'yard'] Raw count matrix shape: (3, 16) (docs x terms) Raw count matrix: Term a acros cat chase data dog in learn mat netwo neura on patte sat the yard Doc A 0 0 2 0 0 0 0 0 1 0 0 1 0 2 3 0 Doc B 0 1 1 1 0 1 0 0 0 0 0 0 0 0 3 1 Doc C 1 0 0 0 1 0 1 1 0 1 1 0 4 0 0 0
Now let's implement each weighting scheme.
def tf_raw(count_matrix):
"""Raw term frequency: just the counts."""
return count_matrix.copy()
def tf_log(count_matrix):
"""Log-scaled term frequency: 1 + log(count) for count > 0."""
tf = np.zeros_like(count_matrix, dtype=float)
nonzero = count_matrix > 0
tf[nonzero] = 1 + np.log(count_matrix[nonzero])
return tf
def tf_boolean(count_matrix):
"""Boolean term frequency: 1 if present, 0 otherwise."""
return (count_matrix > 0).astype(float)
def tf_augmented(count_matrix, K=0.5):
"""Augmented term frequency: K + (1-K) * count / max_count."""
tf = np.zeros_like(count_matrix, dtype=float)
for i in range(count_matrix.shape[0]):
row = count_matrix[i]
max_count = row.max()
if max_count > 0:
nonzero = row > 0
tf[i, nonzero] = K + (1 - K) * row[nonzero] / max_count
return tf
def tf_l2_normalized(count_matrix):
"""L2-normalized term frequency."""
norms = np.linalg.norm(count_matrix, axis=1, keepdims=True)
# Avoid division by zero for all-zero documents
norms[norms == 0] = 1.0
return count_matrix / normsTerm frequency weights for selected terms: Term: 'the' Raw counts: Doc A=3 Doc B=3 Doc C=0 Raw Doc A=3.000 Doc B=3.000 Doc C=0.000 Log Doc A=2.099 Doc B=2.099 Doc C=0.000 Boolean Doc A=1.000 Doc B=1.000 Doc C=0.000 Augmented Doc A=1.000 Doc B=1.000 Doc C=0.000 L2-Norm Doc A=0.688 Doc B=0.802 Doc C=0.000 Term: 'cat' Raw counts: Doc A=2 Doc B=1 Doc C=0 Raw Doc A=2.000 Doc B=1.000 Doc C=0.000 Log Doc A=1.693 Doc B=1.000 Doc C=0.000 Boolean Doc A=1.000 Doc B=1.000 Doc C=0.000 Augmented Doc A=0.833 Doc B=0.667 Doc C=0.000 L2-Norm Doc A=0.459 Doc B=0.267 Doc C=0.000 Term: 'patterns' Raw counts: Doc A=0 Doc B=0 Doc C=4 Raw Doc A=0.000 Doc B=0.000 Doc C=4.000 Log Doc A=0.000 Doc B=0.000 Doc C=2.386 Boolean Doc A=0.000 Doc B=0.000 Doc C=1.000 Augmented Doc A=0.000 Doc B=0.000 Doc C=1.000 L2-Norm Doc A=0.000 Doc B=0.000 Doc C=0.853 Term: 'data' Raw counts: Doc A=0 Doc B=0 Doc C=1 Raw Doc A=0.000 Doc B=0.000 Doc C=1.000 Log Doc A=0.000 Doc B=0.000 Doc C=1.000 Boolean Doc A=0.000 Doc B=0.000 Doc C=1.000 Augmented Doc A=0.000 Doc B=0.000 Doc C=0.625 L2-Norm Doc A=0.000 Doc B=0.000 Doc C=0.213
Notice how "patterns" appears 4 times in Doc C, while "cat" appears twice in Doc A. With raw counts, "patterns" gets double the weight of "cat." With log TF, the gap shrinks considerably. With augmented TF, the weights depend on what the dominant term is in each document, so "patterns" and "cat" are compared relative to their own document contexts.
Let's also verify that L2 normalization creates unit-length vectors.
l2_matrix = tf_l2_normalized(count_matrix)
norms_after = np.linalg.norm(l2_matrix, axis=1)L2 norms after normalization: Doc A: ||v||_2 = 1.000000 Doc B: ||v||_2 = 1.000000 Doc C: ||v||_2 = 1.000000 All vectors have unit length (norm = 1.0)
Now let's compare with scikit-learn's implementation.
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
# CountVectorizer gives raw counts
count_vec = CountVectorizer()
sk_counts = count_vec.fit_transform(corpus).toarray().astype(float)
sk_vocab = count_vec.get_feature_names_out()
# TfidfVectorizer with use_idf=False gives log TF with L2 normalization
# sublinear_tf=True applies 1+log(tf), norm='l2' applies L2 normalization
tfidf_log_l2 = TfidfVectorizer(use_idf=False, sublinear_tf=True, norm="l2")
sk_log_l2 = tfidf_log_l2.fit_transform(corpus).toarray()Comparing our log TF + L2 vs sklearn TfidfVectorizer(use_idf=False): (for term 'cat' in each document) Doc A: sklearn=0.4860 ours=0.4860 match=yes Doc B: sklearn=0.3261 ours=0.3261 match=yes Doc C: sklearn=0.0000 ours=0.0000 match=yes
The implementations agree, which confirms that scikit-learn's TfidfVectorizer(use_idf=False, sublinear_tf=True, norm='l2') computes exactly what we've described as log TF followed by L2 normalization.