Term Frequency › Code Implementation

Michael BrenndoerferMarch 30, 20255 min read
Language AI HandbookAll chapters
Part 2 of 67Classical Text Representations
Chapter 11Term Frequency › Code Implementation
Reading section 9 of 17 · Code Implementation · 5 minSection 9 of 17 · 5 min

Book contents

Section 9 of 17 · 5 min read

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.

In[3]:
Code
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)
Out[4]:
Console
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.

In[5]:
Code
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 / norms
Out[6]:
Console
Term 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.

In[7]:
Code
l2_matrix = tf_l2_normalized(count_matrix)
norms_after = np.linalg.norm(l2_matrix, axis=1)
Out[8]:
Console
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.

In[9]:
Code
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()
Out[10]:
Console
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.