Inverse Document Frequency › Implementing IDF Efficiently

Michael BrenndoerferMarch 28, 20255 min read
Language AI HandbookAll chapters
Part 2 of 67Classical Text Representations
Chapter 12Inverse Document Frequency › Implementing IDF Efficiently
Reading section 7 of 14 · Implementing IDF Efficiently · 5 minSection 7 of 14 · 5 min

Book contents

Section 7 of 14 · 5 min read

Inverse Document Frequency › Implementing IDF EfficientlyOne section · Sections in this chapter7 / 14 ↓
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

For large corpora, efficient IDF computation matters. Let's compare a naive implementation with an optimized approach:

In[21]:
Code
import time
from collections import defaultdict


# Naive implementation: iterate through all documents for each term
def compute_idf_naive(tokenized_corpus):
    """Naive O(V * D) implementation."""
    all_terms = set()
    for doc in tokenized_corpus:
        all_terms.update(doc)

    num_docs = len(tokenized_corpus)
    idf = {}

    for term in all_terms:
        df = sum(1 for doc in tokenized_corpus if term in set(doc))
        idf[term] = np.log(num_docs / df)

    return idf


# Optimized implementation: single pass through corpus
def compute_idf_optimized(tokenized_corpus):
    """Optimized O(total_tokens) implementation."""
    doc_freq = defaultdict(int)

    for doc in tokenized_corpus:
        for term in sorted(set(doc)):  # Stable order across theme renders
            doc_freq[term] += 1

    num_docs = len(tokenized_corpus)
    idf = {term: np.log(num_docs / df) for term, df in doc_freq.items()}

    return idf


# Benchmark on repeated corpus
benchmark_corpus = tokenized_corpus * 100  # 500 documents

start = time.time()
for _ in range(10):
    _ = compute_idf_naive(benchmark_corpus)
naive_time = time.time() - start

start = time.time()
for _ in range(10):
    _ = compute_idf_optimized(benchmark_corpus)
optimized_time = time.time() - start
Out[22]:
Console
IDF Computation Benchmark:
==================================================
Corpus size: 500 documents
Iterations: 10
--------------------------------------------------
Naive implementation:     0.031s
Optimized implementation: 0.005s
Speedup: 6.5x

The speedup demonstrates why algorithm choice matters. The optimized version makes a single pass through the corpus, using a set to count each term once per document. The naive version iterates through all documents for each vocabulary term, making it O(V×D)O(V \times D) instead of O(total tokens)O(\text{total tokens}). For production systems with millions of documents and large vocabularies, this difference can mean hours versus seconds of computation time.

The key insight in the optimized version is to invert the loop structure. Instead of asking "for each term, how many documents contain it?", ask "for each document, which terms does it contain?" The latter question requires only a single pass through the data, building up document frequency counts incrementally.

Out[23]:
Visualization
Log-scale line plot showing maximum IDF growing logarithmically from 2.3 to 13.8 as corpus size increases from 10 to 1 million.
Maximum IDF grows logarithmically with corpus size. A term appearing in only 1 of 10 documents achieves IDF=2.3, while the same uniqueness in a million-document corpus yields IDF=13.8. The logarithmic growth means that larger corpora don't cause IDF values to explode, but they do produce wider ranges.
Horizontal lines at constant IDF values for different document frequency fractions, independent of corpus size.
IDF values at fixed document frequency fractions are independent of corpus size. A term appearing in 50% of documents always has IDF=0.69, whether the corpus has 10 or 1 million documents. This scale-invariance property makes IDF values interpretable regardless of how large your corpus is.

The second plot reveals an important property: IDF values for terms at fixed fractions of the corpus are completely corpus-size independent. A term appearing in 10% of documents always has IDF =log⁡(10)≈2.3= \log(10) \approx 2.3, whether you have 100 documents or 100 million. This scale-invariance is one of IDF's practical strengths: you can compare IDF weights across corpora as long as you think in terms of fractions rather than absolute document counts.

Using scikit-learn for Production

For production systems, use scikit-learn's TfidfVectorizer, which computes IDF efficiently and handles all edge cases:

In[24]:
Code
from sklearn.feature_extraction.text import TfidfVectorizer

# Create vectorizer (use_idf=True is default)
vectorizer = TfidfVectorizer(use_idf=True, smooth_idf=True, norm=None)

# Fit on corpus to learn vocabulary and IDF weights
vectorizer.fit(corpus)

# Access IDF values
sklearn_idf = dict(zip(vectorizer.get_feature_names_out(), vectorizer.idf_))
Out[25]:
Console
scikit-learn IDF Values (smooth_idf=True):
=======================================================
Term                     sklearn IDF     Our sklearn
-------------------------------------------------------
  agent                         2.0986          2.0986
  agents                        2.0986          2.0986
  algorithms                    2.0986          2.0986
  and                           2.0986          2.0986
  computer                      2.0986          2.0986
  convolutional                 2.0986          2.0986
  core                          2.0986          2.0986
  cumulative                    2.0986          2.0986
  error                         2.0986          2.0986
  for                           2.0986          2.0986
  hierarchical                  2.0986          2.0986
  image                         2.0986          2.0986

The values match exactly between scikit-learn's implementation and our manual calculation of the sklearn variant formula. This confirms that TfidfVectorizer with smooth_idf=True uses log⁡(1+N1+df(t))+1\log\left(\frac{1 + N}{1 + \text{df}(t)}\right) + 1. For production applications, always use scikit-learn rather than implementing IDF manually, as it handles edge cases, optimizes memory usage, and integrates directly with machine learning pipelines.