HardNLP

Implement TF-IDF Vectorizer

NLP · Linear Algebra · Feature Engineering

Hard

Problem

Build a TF-IDF representation from text documents. Convert text to lowercase, split on whitespace, and sort the unique vocabulary alphabetically.

\operatorname{tf}(t,d) = \frac{\operatorname{count}(t,d)}{|d|}

\operatorname{idf}(t) = \log\left(\frac{N}{\operatorname{df}(t)}\right)

\operatorname{tfidf}(t,d) = \operatorname{tf}(t,d)\operatorname{idf}(t)

Here, t is a term, d is a document, |d| is its token count, N is the number of documents, and \operatorname{df}(t) is the number of documents containing t. Return a dictionary with tfidf_matrix, a NumPy array of shape (N,V), and vocabulary, the sorted list of V terms.

Theory

TF-IDF (Term Frequency-Inverse Document Frequency) is a numerical statistic that reflects how important a word is to a document within a collection. It combines two factors: how frequently a term appears in a document (TF) and how rare the term is across all documents (IDF). Words that are frequent in a document but rare overall receive high scores.


The Intuition Behind TF-IDF

Problem with raw counts: Common words like "the", "is", "and" appear frequently in all documents. Using raw counts gives them high importance despite carrying little meaning.

TF component: Captures local importance - a word mentioned many times in a document is likely important to that document.

IDF component: Captures global rarity - a word appearing in few documents is more discriminative than one appearing everywhere.

Combined effect: Balances frequency with distinctiveness.


Term Frequency (TF)

Several variants exist:

Raw count:

\text{tf}(t, d) = f_{t,d}

Where f_{t,d} is the number of times term t appears in document d.

Boolean:

\text{tf}(t, d) = \begin{cases} 1 & \text{if } t \in d \\ 0 & \text{otherwise} \end{cases}

Log normalization:

\text{tf}(t, d) = 1 + \log(f_{t,d}) \quad \text{if } f_{t,d} > 0

Augmented frequency (prevents bias toward long documents):

\text{tf}(t, d) = 0.5 + 0.5 \cdot \frac{f_{t,d}}{\max\{f_{t',d} : t' \in d\}}


Inverse Document Frequency (IDF)

Measures how common or rare a term is across documents:

\text{idf}(t) = \log\left(\frac{N}{n_t}\right)

Where:

Properties:

Smoothed variant (avoids division by zero):

\text{idf}(t) = \log\left(\frac{N + 1}{n_t + 1}\right) + 1


The TF-IDF Formula

Combining TF and IDF:

\text{tf-idf}(t, d) = \text{tf}(t, d) \times \text{idf}(t)

Interpretation: High TF-IDF means the term is frequent in this document but rare across the corpus - likely a key term for this document.


Worked Example

Corpus (3 documents):

Vocabulary: [cat, dog, mat, park, play, ran, sat]

Document Frequencies:

IDF calculations (using standard formula):

\text{idf(cat)} = \log(3/2) \approx 0.405

\text{idf(mat)} = \log(3/1) \approx 1.099

TF-IDF for D1:

Observation: "mat" and "sat" have higher TF-IDF than "cat" because they are unique to D1.


The TF-IDF Matrix

For N documents and vocabulary of size V:

Matrix shape: (N, V)

Each row: TF-IDF vector for one document

Each column: TF-IDF values for one term across documents

Sparsity: Most entries are zero (documents do not contain most vocabulary terms)


Vocabulary Building

Step 1: Tokenize all documents into terms

Step 2: Build vocabulary (unique terms)

Step 3: Optionally filter vocabulary:

Result: Mapping from term to column index


Fitting and Transforming

Fit (learn from training corpus):

Transform (apply to documents):

Fit-Transform: Combines both on training data

Important: Use same vocabulary and IDF values for training and test data.


L2 Normalization

Often applied after TF-IDF:

\text{normalized}(d) = \frac{\text{tf-idf}(d)}{||\text{tf-idf}(d)||_2}

Benefits:


N-gram Extension

Instead of single words, consider sequences:

Unigrams: "the", "cat", "sat"

Bigrams: "the cat", "cat sat"

Trigrams: "the cat sat"

Combined: Often use (1,2) or (1,3) range to capture both words and phrases

Tradeoff: More features, potentially better representation, but higher dimensionality


TF-IDF vs Word Embeddings

TF-IDF:

Word embeddings (Word2Vec, GloVe):

Modern approach: Often combine both or use transformer embeddings


Handling Out-of-Vocabulary Terms

Terms in test documents but not in training vocabulary:

Standard approach: Ignore them (they contribute nothing to the vector)

Implication: New documents with mostly OOV terms get sparse representations

Solutions:


Where TF-IDF Shows Up

Examples

Example 1

Input
documents = ["the cat sat", "the cat ran", "the dog sat"]
Output
{"tfidf_matrix": [[0.135155, 0.0, 0.0, 0.135155, 0.0], [0.135155, 0.0, 0.366204, 0.0, 0.0], [0.0, 0.366204, 0.0, 0.135155, 0.0]], "vocabulary": ["cat", "dog", "ran", "sat", "the"]}
Explanation
The vocabulary fixes the column order, then every document receives one TF-IDF weight per vocabulary term.

Example 2

Input
documents = ["machine learning is great", "cooking pasta is fun"]
Output
{"tfidf_matrix": [[0.0, 0.0, 0.173287, 0.0, 0.173287, 0.173287, 0.0], [0.173287, 0.173287, 0.0, 0.0, 0.0, 0.0, 0.173287]], "vocabulary": ["cooking", "fun", "great", "is", "learning", "machine", "pasta"]}

Hints

  1. Use Counter(tokens) for term counts and Counter.update(set(tokens)) for document frequencies.
  2. Create a token-to-column dictionary with enumerate(vocabulary).
  3. Initialize the output with np.zeros((len(documents), len(vocabulary))).

Requirements

Constraints

Starter Code

import math
from collections import Counter
import numpy as np

def tfidf_vectorizer(documents: list[str]) -> dict:
    """
    Returns a dictionary with tfidf_matrix and vocabulary.
    """
    # Write code here
    pass

Test Cases

CaseMatches
Basic 3 docspublic
Two distinct docspublic