ICS 582Lecture 04Part 05
Weighting with tf-idf
Why raw frequency is a poor representation, how log-scaled term frequency and inverse document frequency combine into tf-idf, and a full worked tf-idf table for the Shakespeare example.
- Concepts
- 5
- Slides
- 45-52
- Reading
- 30 min
Why this part matters
Part 04 built count vectors and compared them with cosine. Those vectors have a flaw you can see the moment you look at real counts: the words with the biggest numbers are the, it and good, words that sit near everything and therefore say nothing. Any similarity computed from raw counts is dominated by them.
This part fixes the term-document side of that problem with tf-idf, the weighting that has been the baseline of information retrieval since the 1970s and still ships as the default sparse retriever and text feature extractor. Its descendant BM25 is the keyword half of most retrieval-augmented generation pipelines you will meet in research. Exams like this material because it is computable by hand: the difference between document frequency and collection frequency, and a tf-idf cell worked out from a count, are classic questions. The next part does the same job for term-term matrices with PPMI.
By the end you can
- Explain why raw co-occurrence counts over-weight frequent, uninformative words.
- Compute log-scaled term frequency and contrast the log10(count + 1) and 1 + log10(count) variants.
- Distinguish document frequency from collection frequency using Romeo and action.
- Compute idf with N = 37 and explain why a word in every document gets weight zero.
- Reproduce any cell of the slide 52 tf-idf table and explain how the choice of document changes the weights.
Build a Term-context matrix from a large corpus and look at the row for apricot. The context sugar has a healthy count there, and that is genuinely useful: apricots are sweet things you cook with sugar, and a word whose row also has a high sugar count, such as peach, is probably similar. Now look further along the same row. The contexts the, it and they have counts many times larger than sugar. They have similarly huge counts in the row for digital, for information, for every word in the vocabulary.
That is the paradox. Frequency is clearly informative, since co-occurring often is exactly how sugar earns its place. But frequency is not proportional to information. A Dot product sums products dimension by dimension, so a dimension where both vectors carry a count in the thousands swamps a dimension where both carry a count of twenty. Even after normalising by length, Cosine similarity on raw counts mostly measures how much two words share the ubiquitous dimensions, and every word shares those. What we want instead is a weight with two pressures in it: reward a term that is frequent here, and penalise a term that is frequent everywhere.
Two classic reweightings
Which reweighting you pick depends on which matrix you have. For a Term-document matrix, where columns are documents, the standard answer is tf-idf. Jurafsky and Martin describe it as "the product of two terms, the term frequency tf and the inverse document frequency idf", and note that the "-" is a hyphen, not a minus sign. The first factor rewards local frequency; the second penalises spread across the collection.
For a term-term matrix, where both rows and columns are words, the standard answer is Pointwise mutual information. It compares how often two words actually appear together with how often they would if they were independent. Words like good and great earn a high PMI only if they meet more often than their individual frequencies predict, so a context like the that meets everything at the expected rate scores near zero. Part 06 develops PMI in full.
| Scheme | Matrix it suits | Question it answers |
|---|---|---|
| tf-idf | Term-document | Is this term distinctive for this document? |
| PMI | Term-term (word-context) | Do these two words co-occur more than chance would predict? |
The idea behind the idf half is older than vector semantics. Karen Sparck Jones argued in 1972 that "matches on less frequent, more specific, terms are of greater value than matches on frequent terms", and proposed weighting each term by how few documents it appears in. Every term-weighting scheme in this lecture, PPMI included, is a variation on her insight.
Recall
Why does a cosine between two raw count vectors tend to be high for almost any pair of words?
Suppose a word appears 0, 1, 9, 99 or 999 times in a document. Under the slides' formula those counts become a term frequency of 0, 0.301, 1, 2 and 3. A hundredfold increase in occurrences, from 9 to 999, adds only 2 to the weight.
Raw count to log-scaled term frequency, tf = log10(count + 1)
- count 0
- tf = log10(1) = 0
- count 1
- tf = log10(2) = 0.301
- count 9
- tf = log10(10) = 1
- count 99
- tf = log10(100) = 2
- count 999
- tf = log10(1000) = 3
The simplest term frequency would be the raw count itself, tf = count(t, d). The trouble is that significance does not grow linearly with repetition. Manning, Raghavan and Schütze put it plainly: "It seems unlikely that twenty occurrences of a term in a document truly carry twenty times the significance of a single occurrence." The second mention of battle in a play tells you a lot (this play has a battle in it); the hundredth mention adds far less. So we squash the count with a logarithm.
The +1 is there because log 0 is undefined. Adding one before taking the log sends a count of zero to log10(1) = 0, which is exactly the weight an absent term should have, while barely changing large counts.
The other formula you will meet
The current SLP3 draft (now Chapter 11, on retrieval and RAG) and the IR book both use a slightly different squash. SLP3 notes in a footnote that log10(count + 1) is "this alternative formulation" used in its earlier editions, which is where the slides come from. Neither is an error; they are two members of the same sublinear family.
The two agree on the shape but not the numbers. Under the variant, a count of 1 gives 1 + log10 1 = 1 rather than 0.301, a count of 10 gives 2 rather than 1.041, and a count of 7 gives 1.845 rather than 0.903. scikit-learn offers a third version through sublinear_tf=True, which uses 1 + ln(tf) with the natural log.
Recall
Compute tf = log10(count + 1) for counts 0, 9 and 99.
Across the complete works of Shakespeare, the words Romeo and action each occur exactly 113 times. By raw volume they are identical. Yet every occurrence of Romeo sits in one play, Romeo and Juliet, while action is spread across 31 different plays. If you are handed a document and told it contains Romeo, you know which play it is. If you are told it contains action, you have learned almost nothing.
The two numbers in that story have names. The total number of times a term occurs across the whole collection is its Collection frequency, cf. The number of documents that contain the term at least once is its Document frequency, df_t. The IR book defines them exactly that way and concludes that for discriminating between documents it is "better to use a document-level statistic ... than to use a collection-wide statistic". SLP3 gives the reason: "Terms that occur in only a few documents are useful for discriminating those documents from the rest of the collection."
| Word | cf | df | idf |
|---|---|---|---|
| Romeo | 113 | 1 | log10(37 / 1) = 1.57 |
| action | 113 | 31 | log10(37 / 31) = 0.077 |
The Shakespeare pair is not a curiosity. The IR book finds the same pattern in the Reuters newswire collection, where try and insurance have almost the same collection frequency but very different document frequencies. Insurance clusters in the articles that are actually about insurance; try is sprinkled through everything.
| Word | cf | df |
|---|---|---|
| try | 10,422 | 8,760 |
| insurance | 10,440 | 3,997 |
Recall
Romeo and action both have collection frequency 113. Give their df values and say which gets the larger idf with N = 37.
Quick check
Romeo and action both occur 113 times across Shakespeare. Why does Romeo receive a much higher idf?
Treat each of Shakespeare's 37 plays as a document and walk down a ladder of words ordered by how many plays they appear in. Romeo is in one play, salad in two, Falstaff in four, forest in twelve, battle in twenty-one, wit in thirty-four, fool in thirty-six, and good and sweet in all thirty-seven. We want a weight that is large at the top of the ladder and vanishes at the bottom.
The ratio N / df_t of collection size to Document frequency does that: it is 37 for Romeo and 1 for good. Raw ratios grow too fast, though (a word in one document out of a million would get a ratio of a million), so we take the logarithm, exactly as we did for tf. The result is the inverse document frequency.
| Word | df | idf |
|---|---|---|
| Romeo | 1 | 1.57 |
| salad | 2 | 1.27 |
| Falstaff | 4 | 0.966 (slide prints 0.967) |
| forest | 12 | 0.489 |
| battle | 21 | 0.246 |
| wit | 34 | 0.037 |
| fool | 36 | 0.012 |
| good, sweet | 37 | 0 |
Two boundary values are worth memorising. The largest possible idf is log10 N, reached when a term occurs in a single document: log10 37 = 1.57 here. The smallest is 0, reached when a term occurs in every document, since log10(N / N) = log10 1 = 0. SLP3 says it directly: the lowest weight, 0, is "assigned to terms that occur in every document", words "like good or sweet". Notice that idf is not linear in df. Going from df = 1 to df = 2 costs 0.30, the same as going from df = 12 to df = 24: halving the spread always buys the same amount of weight.
Worked example
Checking the two ends of the ladder
Romeo, df = 1
idf = log10(37 / 1) = log10 37 = 1.568, which rounds to the slide's 1.57.fool, df = 36
37 / 36 = 1.0278, so idf = log10 1.0278 = 0.0119, which rounds to 0.012.good, df = 37
idf = log10(37 / 37) = log10 1 = 0.Result
A word in one play is worth 1.57; a word missing from just one play is worth about 0.012, over a hundred times less; a word in every play is worth nothing.
What counts as a document?
Nothing in the formula says a document must be a play. SLP3 defines a document as "whatever unit of text the system indexes and retrieves (web pages, scientific papers, news articles, or even shorter passages like paragraphs)". It could be a Wikipedia article, a tweet, a paragraph, or a 300-token chunk in a RAG index. The choice is a modelling decision, and it changes everything downstream: N changes, every df changes, and so every idf changes.
Switch Shakespeare from plays to paragraphs and N grows into the thousands. good is no longer in every document, because most paragraphs do not contain it, so it gets a positive idf. A word like battle that clusters in a few scenes now looks rarer relative to the collection, and its weight rises. The right unit is the one that matches what you retrieve or compare: if your system returns passages, compute df over passages.
Recall
What happens to the idf values if you treat each paragraph as a document instead of each play?
Take battle in Julius Caesar. The play uses the word 7 times, so its term frequency is log10(7 + 1) = log10 8 = 0.903. battle appears in 21 of the 37 plays, so its idf is 0.246. Multiply: 0.903 × 0.246 = 0.222, which the slide prints as 0.22. That is one cell of the tf-idf matrix, and every other cell is computed the same way.
The rule is just the definition applied cell by cell: squash the count into Term frequency, look up the word's Inverse document frequency, multiply. The table below combines the raw counts on slide 52 with the idf column of slide 50 and shows the intermediate tf values the slide skips. All sixteen weights agree with the slide.
| Word | Raw counts | tf = log10(count + 1) | idf | tf-idf |
|---|---|---|---|---|
| battle | 1, 0, 7, 13 | 0.301, 0, 0.903, 1.146 | 0.246 | 0.074, 0, 0.22, 0.28 |
| good | 114, 80, 62, 89 | 2.061, 1.908, 1.799, 1.954 | 0 | 0, 0, 0, 0 |
| fool | 36, 58, 1, 4 | 1.568, 1.771, 0.301, 0.699 | 0.0119 | 0.019, 0.021, 0.0036, 0.0083 |
| wit | 20, 15, 2, 3 | 1.322, 1.204, 0.477, 0.602 | 0.0367 | 0.049, 0.044, 0.018, 0.022 |
Worked example
Two cells from the slide 52 table
battle in Julius Caesar
tf = log10 8 = 0.903, idf = log10(37 / 21) = 0.246, so w = 0.903 × 0.246 = 0.222 ≈ 0.22.fool in Henry V
The count is 4, so tf = log10 5 = 0.699. fool is in 36 plays, so idf = 0.0119, and w = 0.699 × 0.0119 = 0.0083.Result
battle in Julius Caesar outweighs fool in Henry V by a factor of about 27, although the raw counts differ only by 7 against 4. The difference comes almost entirely from idf.
What the table teaches
Look at the good row. It has the largest counts in the table, from 62 to 114, and its tf values are all around 2. Yet every tf-idf cell is 0, because good occurs in all 37 plays and its idf is log10 1 = 0. The word that dominated the raw vectors has been removed from the comparison without anyone writing a stop list.
Now look at battle. In raw counts it was a minor dimension, dwarfed by good in every play and by fool in the comedies. After weighting it is the largest value in every column where it appears, and it cleanly separates Julius Caesar (0.22) and Henry V (0.28), plays full of war, from the comedies As You Like It (0.074) and Twelfth Night (0). The IR book summarises the pattern: a weight is highest when a term occurs many times in a small number of documents, lower when it occurs fewer times or in many documents, and lowest when it occurs in virtually all documents.
The resulting document vectors are still sparse vectors of vocabulary length, mostly zeros, and you compare them with Cosine similarity exactly as in part 04. The difference is that the dimensions now carry information in proportion to how distinctive they are.
tf-idf in real systems
The formula is the same everywhere, but the details are not, and the differences matter when you reproduce a paper or debug a pipeline. scikit-learn's TfidfVectorizer by default uses raw counts for tf, a smoothed natural-log idf, ln((1 + n) / (1 + df)) + 1, and then L2-normalises each document vector. Because of the +1, a term in every document gets idf 1, not 0, so good would survive. The documentation's own example turns the vector [3, 0, 1.8473] into [0.8515, 0, 0.5243] after normalisation.
| Source | tf | idf | idf of a term in every document |
|---|---|---|---|
| Slides (SLP3 earlier editions) | log10(count + 1) | log10(N / df) | 0 |
| SLP3 current draft, IIR | 1 + log10(count), or 0 | log10(N / df) | 0 |
| scikit-learn default | raw count (sublinear_tf=False) | ln((1 + n) / (1 + df)) + 1 | 1 |
BM25 is the modern member of the family and the standard keyword retriever in search engines and hybrid RAG systems. It keeps idf but replaces log tf with a saturating function controlled by a parameter k, usually between 1.2 and 2, and normalises for document length with b = 0.75. Salton and Buckley's 1988 comparison of term-weighting schemes is the classic study of why these choices matter.
Recall
Compute the tf-idf of battle in Henry V (count 13, df 21, N 37).
Recall
Why does every cell in the good row of slide 52 become 0?
Quick check
With N = 37 and tf = log10(count + 1), what is the tf-idf of battle in Julius Caesar (count 7, df 21)?
Quick check
Why is every entry in the good row of the slide 52 tf-idf table equal to zero?
Quick check
In scikit-learn's default TfidfVectorizer, what idf does a term that appears in every document receive?
Recap
If you remember nothing else
- Raw frequency is informative but dominated by ubiquitous words. tf-idf (term-document) and PMI (term-term) reweight counts.
- The slides use tf = log10(count + 1), so 0 → 0, 9 → 1, 99 → 2. The current SLP3 draft and IIR use 1 + log10(count) for count > 0.
- df counts documents, cf counts tokens. Romeo and action share cf 113 but have df 1 versus 31.
- idf = log10(N / df). With N = 37 plays it ranges from 1.57 (Romeo) to 0 (good, sweet). The slides never state N.
- w = tf × idf. Battle in Julius Caesar is log10 8 × 0.246 = 0.22, and the whole good row becomes 0.
- A document is any unit you choose. Changing it changes N, df and every weight.
- Libraries differ: scikit-learn uses ln((1 + n) / (1 + df)) + 1 with L2 normalization, so a term in every document keeps idf 1 instead of 0. BM25 adds tf saturation and length normalization.
Sources
- Speech and Language Processing, 3rd ed. draft, Chapter 11: Information Retrieval and Retrieval-Augmented GenerationBookJurafsky and Martin, draft of August 2026Section 11.1.2 on tf-idf: the log tf formula and footnote on the slides' variant, the Shakespeare idf table, the definition of a document, stop words and BM25 parameters(opens in a new tab)
- Introduction to Information Retrieval, section 6.2.1: Inverse document frequencyBookManning, Raghavan and Schütze, Cambridge University PressCollection frequency versus document frequency, and the Reuters try and insurance example(opens in a new tab)
- Introduction to Information Retrieval, section 6.2.2: Tf-idf weightingBookManning, Raghavan and Schütze, Cambridge University PressWhen a tf-idf weight is high, lower and lowest(opens in a new tab)
- Introduction to Information Retrieval, section 6.4.1: Sublinear tf scalingBookManning, Raghavan and Schütze, Cambridge University PressWhy twenty occurrences are not twenty times as significant, and the 1 + log tf variant(opens in a new tab)
- A Statistical Interpretation of Term Specificity and Its Application in RetrievalPaperSparck Jones, Journal of Documentation 28(1):11-21, 1972The original proposal of inverse document frequency(opens in a new tab)
- Term-weighting approaches in automatic text retrievalPaperSalton and Buckley, Information Processing and Management 24(5):513-523, 1988The classic comparison of tf, idf and normalisation choices(opens in a new tab)
- Understanding inverse document frequency: on theoretical arguments for IDFPaperRobertson, Journal of Documentation 60(5), 2004Why the probabilistic retrieval model, not information theory, grounds idf(opens in a new tab)
- Feature extraction: Tf-idf term weightingDocsscikit-learn documentationsmooth_idf formula with the natural log, sublinear_tf, and default L2 normalisation(opens in a new tab)