0Pricing
R Academy · Lesson

TF-IDF and Term Frequency Analysis

Score term importance across documents with bind_tf_idf().

TF-IDF and Term Frequency Analysis is a free R Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is TF-IDF?

TF-IDF (Term Frequency–Inverse Document Frequency) measures how distinctive a word is to a specific document within a corpus. A high TF-IDF score means the word is frequent in that document but rare across all documents — a good indicator of the document's key topics.

# TF-IDF formula
# TF(w, d)  = count of w in document d / total words in d
# IDF(w)    = log(N / number of docs containing w)
# TF-IDF(w, d) = TF(w, d) * IDF(w)

# Manual example
N <- 4  # total documents
docs_with_word <- 1  # 'neural' appears in only 1 doc
tf <- 5 / 100  # word appears 5 times in 100-word doc
idf <- log(N / docs_with_word)
tfidf <- tf * idf

cat('TF:     ', round(tf, 4), '\n')
cat('IDF:    ', round(idf, 4), '\n')
cat('TF-IDF: ', round(tfidf, 4), '\n')

Preparing the Token Count

Before computing TF-IDF, you need a data frame with columns: document (document identifier), word (token), and n (count). Use unnest_tokens() followed by count(document, word).

library(tidytext)
library(dplyr)

# Simulate a 4-document corpus on tech topics
docs <- tibble::tibble(
  document = c(rep('ML', 3), rep('Stats', 3), rep('DB', 3), rep('Web', 3)),
  text = c(
    'machine learning neural networks training',
    'deep learning models gradient descent',
    'reinforcement learning reward policy agent',
    'probability distributions hypothesis testing',
    'bayesian inference regression analysis variance',
    'statistics sampling confidence intervals normal',
    'database sql joins indexes queries transactions',
    'relational tables primary keys foreign constraints',
    'nosql document store mongodb redis cassandra',
    'html css javascript frontend react components',
    'dom events browser api fetch requests',
    'responsive design flexbox grid layout'
  )
)

word_counts <- docs |>
  unnest_tokens(word, text) |>
  count(document, word, sort = TRUE)

cat('Document-word pairs:', nrow(word_counts), '\n')
print(head(word_counts, 8))

bind_tf_idf: Computing TF-IDF

bind_tf_idf(word, document, n) takes a tidy count data frame and appends three columns: tf (term frequency), idf (inverse document frequency), and tf_idf (their product). It computes IDF from the documents present in the data frame.

library(tidytext)
library(dplyr)

docs <- tibble::tibble(
  document = c(rep('ML', 2), rep('Stats', 2), rep('DB', 2)),
  text = c(
    'machine learning neural gradient',
    'deep networks training epochs',
    'probability distributions variance covariance',
    'bayesian inference prior posterior',
    'sql database tables joins indexes',
    'queries transactions foreign primary'
  )
)

tfidf <- docs |>
  unnest_tokens(word, text) |>
  count(document, word) |>
  bind_tf_idf(word, document, n)

cat('Columns added:', names(tfidf), '\n')
print(head(arrange(tfidf, desc(tf_idf)), 8))

arrange(desc(tf_idf)): Top Terms

Sort by descending tf_idf to see which words best characterise each document. Words with IDF of zero (present in every document) will have TF-IDF of zero regardless of their frequency — they cannot distinguish documents.

library(tidytext)
library(dplyr)

docs <- tibble::tibble(
  document = c(rep('Python', 3), rep('R', 3), rep('Julia', 3)),
  text = c(
    'python pandas numpy scipy programming',
    'machine learning scikit tensorflow keras',
    'jupyter notebooks scripts debugging modules',
    'ggplot2 dplyr tidyverse statistics vectors',
    'shiny rmarkdown knitr cran bioconductor',
    'linear models factors dataframes packages',
    'julia multiple dispatch type system macros',
    'package ecosystem flux diffeq parallel',
    'scientific computing performance benchmarks'
  )
)

top_terms <- docs |>
  unnest_tokens(word, text) |>
  count(document, word) |>
  bind_tf_idf(word, document, n) |>
  arrange(desc(tf_idf))

cat('Top distinctive terms per language:\n')
print(head(top_terms[, c('document', 'word', 'tf_idf')], 9))

Top TF-IDF Terms per Document

Use group_by(document) |> slice_max(tf_idf, n = 5) to extract the top N most distinctive terms per document. This is a powerful summary of a corpus — each document's distinctive vocabulary appears clearly.

library(tidytext)
library(dplyr)

docs <- tibble::tibble(
  document = c(rep('Cooking', 3), rep('Finance', 3), rep('Sports', 3)),
  text = c(
    'recipe ingredients oven bake flour butter',
    'cooking temperature boil simmer saute',
    'kitchen knife chopping herbs spices garnish',
    'investment portfolio returns dividends stocks bonds',
    'market equity risk hedge fund derivatives',
    'inflation interest rate treasury balance sheet',
    'goal tackle pass dribble goalkeeper penalty',
    'tournament league championship trophy season',
    'athlete training stamina sprint endurance'
  )
)

top5 <- docs |>
  unnest_tokens(word, text) |>
  count(document, word) |>
  bind_tf_idf(word, document, n) |>
  group_by(document) |>
  slice_max(tf_idf, n = 3) |>
  ungroup()

print(top5[, c('document', 'word', 'tf_idf')])

Visualising TF-IDF per Document

Bar charts of TF-IDF scores, faceted by document, reveal each document's distinctive vocabulary at a glance. Use facet_wrap(~document, scales = 'free_y') so each panel shows its own top terms independently.

library(tidytext)
library(dplyr)
library(ggplot2)

docs <- tibble::tibble(
  document = c(rep('AI', 3), rep('DB', 3), rep('Web', 3)),
  text = c(
    'neural networks gradient backpropagation',
    'deep learning transformer attention bert',
    'reinforcement policy reward exploration',
    'sql database indexes joins transactions',
    'relational schema normalization queries',
    'nosql mongodb redis cassandra document',
    'html css javascript react dom',
    'frontend api fetch async components',
    'responsive flexbox grid webpack bundle'
  )
)

plot_data <- docs |>
  unnest_tokens(word, text) |>
  count(document, word) |>
  bind_tf_idf(word, document, n) |>
  group_by(document) |>
  slice_max(tf_idf, n = 4) |>
  ungroup()

ggplot(plot_data, aes(reorder(word, tf_idf), tf_idf, fill = document)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~document, scales = 'free_y') +
  coord_flip() +
  labs(x = NULL, y = 'TF-IDF', title = 'Top TF-IDF Terms per Topic') +
  theme_minimal()

IDF: Words Appearing in All Docs

Words appearing in every document get IDF = log(N/N) = 0, and therefore TF-IDF = 0. This automatically down-weights corpus-wide common words without needing a stop word list — though stop word removal is still useful for very small corpora.

library(tidytext)
library(dplyr)

docs <- tibble::tibble(
  document = c('A', 'B', 'C'),
  text = c(
    'data analysis statistics regression probability',
    'data engineering pipelines etl transformation',
    'data visualisation ggplot2 charts dashboards'
  )
)

tfidf <- docs |>
  unnest_tokens(word, text) |>
  count(document, word) |>
  bind_tf_idf(word, document, n)

# 'data' appears in all 3 docs: tf_idf should be 0
data_rows <- filter(tfidf, word == 'data')
cat('TF-IDF for "data" across documents:\n')
print(data_rows[, c('document', 'word', 'idf', 'tf_idf')])

Comparing Two Documents

TF-IDF makes document comparison easy: find terms with high TF-IDF in one document but low or zero in another to understand what makes each document unique. Inner join the TF-IDF tables on word and compare scores.

library(tidytext)
library(dplyr)

docs <- tibble::tibble(
  document = c(rep('Doc1', 3), rep('Doc2', 3)),
  text = c(
    'bayesian statistics prior posterior mcmc',
    'markov chain monte carlo sampling',
    'probability distributions conjugate inference',
    'convolutional neural network image classification',
    'pooling activation relu softmax batch',
    'convolution filter feature map stride padding'
  )
)

tfidf <- docs |>
  unnest_tokens(word, text) |>
  count(document, word) |>
  bind_tf_idf(word, document, n)

# Top 3 unique terms per document
tfidf |>
  group_by(document) |>
  slice_max(tf_idf, n = 3) |>
  select(document, word, tf_idf) |>
  print()

TF-IDF with Jane Austen Novels

The janeaustenr package provides the full text of six Austen novels. This is a classic TF-IDF benchmark: Austen fans can identify each novel's distinctive vocabulary — the word "wentworth" is almost unique to Persuasion.

library(tidytext)
library(dplyr)

# Requires janeaustenr package
# library(janeaustenr)
# austen_books() returns: book, text

# Simulated mini-Austen corpus
mini_austen <- tibble::tibble(
  book = c(rep('Sense', 3), rep('Pride', 3), rep('Emma', 3)),
  text = c(
    'elinor marianne dashwood willoughby colonel',
    'edward ferrars barton cottage sister sense',
    'brandon feelings attachment sensibility heart',
    'darcy bennet bingley netherfield wickham',
    'jane lizzy lydia longbourn pemberley',
    'pride prejudice proposal marriage happiness',
    'emma woodhouse knightley harriet weston',
    'highbury match social governess niece',
    'frank jane fairfax box hill picnic'
  )
)

top <- mini_austen |>
  unnest_tokens(word, text) |>
  count(book, word) |>
  bind_tf_idf(word, book, n) |>
  group_by(book) |>
  slice_max(tf_idf, n = 3) |>
  select(book, word, tf_idf)

print(top)

TF-IDF for Feature Engineering

TF-IDF scores can be used as features in machine learning classifiers. Convert the tidy TF-IDF output to a document-term matrix using cast_dtm() or cast_sparse() and feed it to glmnet, xgboost, or any other classifier.

library(tidytext)
library(dplyr)

docs <- tibble::tibble(
  document = c(rep('Positive', 4), rep('Negative', 4)),
  text = c(
    'excellent product love recommend quality',
    'amazing fast delivery five star perfect',
    'great value money satisfied happy return',
    'wonderful experience purchase outstanding best',
    'terrible waste money broken arrived damaged',
    'horrible quality slow delivery disappointed awful',
    'poor value cheap plastic flimsy broken',
    'worst experience refund needed useless junk'
  )
)

# TF-IDF as features
tfidf_wide <- docs |>
  unnest_tokens(word, text) |>
  count(document, word) |>
  bind_tf_idf(word, document, n) |>
  cast_dtm(document, word, tf_idf)

cat('DTM dimensions:', dim(tfidf_wide), '\n')
cat('(rows=documents, cols=unique words)\n')

Limitations and Alternatives

TF-IDF has limitations: it treats words as independent, ignores word order and context, and can be dominated by rare misspellings. Modern alternatives include word embeddings (word2vec, GloVe) and contextual embeddings (BERT), but TF-IDF remains excellent for document retrieval and classification baselines.

# TF-IDF strengths and weaknesses summary
criteria <- data.frame(
  Criterion = c('Speed', 'Interpretability', 'Context', 'Word order',
                'Rare words', 'Scalability'),
  TF_IDF    = c('Fast', 'High', 'None', 'Ignored',
                'High IDF (inflated)', 'Excellent'),
  BERT      = c('Slow', 'Low', 'Rich', 'Captured',
                'Handled', 'Moderate')
)

print(criteria, row.names = FALSE)

Quick Check

A word appears in all documents in your corpus. What will its TF-IDF score be?

Recap: TF-IDF Analysis

Key takeaways:

  • TF-IDF = term frequency × inverse document frequency; high score = distinctive word
  • Workflow: unnest_tokens()count(document, word)bind_tf_idf(word, document, n)
  • bind_tf_idf() appends tf, idf, and tf_idf columns
  • arrange(desc(tf_idf)) / slice_max(tf_idf, n = 5) surface top terms per document
  • Words in all documents get IDF = 0 and TF-IDF = 0 automatically
  • cast_dtm(document, word, tf_idf) converts to a document-term matrix for ML
  • Faceted bar charts are the standard visualisation for TF-IDF results
library(tidytext)
library(dplyr)

# Minimal TF-IDF pipeline
tibble::tibble(
  doc = c('A', 'A', 'B', 'B'),
  text = c('neural networks deep', 'learning training', 'sql database query', 'joins indexes')
) |>
  unnest_tokens(word, text) |>
  count(doc, word) |>
  bind_tf_idf(word, doc, n) |>
  arrange(desc(tf_idf)) |>
  print()

Frequently asked questions

Is the “TF-IDF and Term Frequency Analysis” lesson free?

Yes — the full text of “TF-IDF and Term Frequency Analysis” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.

What will I learn in “TF-IDF and Term Frequency Analysis”?

Score term importance across documents with bind_tf_idf(). You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “TF-IDF and Term Frequency Analysis” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Tokenization and Stop Word Removal
  2. TF-IDF and Term Frequency Analysis
  3. Sentiment Analysis in R
  4. Topic Modeling with LDA
← Back to R Academy