0Pricing
R Academy · Lesson

Topic Modeling with LDA

Discover latent topics in document corpora using the topicmodels package.

Topic Modeling with LDA is a free R Academy lesson on CoddyKit — lesson 4 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 Topic Modelling?

Topic modelling is an unsupervised machine learning technique that discovers latent thematic structure in a corpus. Latent Dirichlet Allocation (LDA) assumes each document is a mixture of K topics, and each topic is a probability distribution over words.

# LDA key assumptions:
# 1. Each document is a mixture of K topics
# 2. Each topic is a distribution over all vocabulary words
# 3. Words are generated by first picking a topic, then a word from that topic

# Example: K=2 topics in a corpus about tech and cooking
# Topic 1 (Tech):    neural(0.15) python(0.12) algorithm(0.10) ...
# Topic 2 (Cooking): recipe(0.14) flour(0.11) bake(0.10) ...

# A document about 'AI-generated recipes' might be:
# 70% Topic 1 (Tech) + 30% Topic 2 (Cooking)

cat('LDA parameters:\n')
cat('K = number of topics (you choose)\n')
cat('alpha = document-topic sparsity prior\n')
cat('beta  = topic-word sparsity prior\n')

DocumentTermMatrix from tm

LDA requires a Document-Term Matrix (DTM): rows are documents, columns are words, cells are word counts. The tm package's DocumentTermMatrix() or tidytext's cast_dtm() both produce this format.

library(tm)

# Build a corpus from raw text
docs <- c(
  'machine learning neural networks training algorithms',
  'deep learning gradient backpropagation optimizer',
  'python scikit numpy pandas machine learning',
  'database sql tables indexes queries joins',
  'relational schema normalization foreign primary',
  'nosql mongodb document store redis queries'
)

corpus <- Corpus(VectorSource(docs))
corpus <- tm_map(corpus, removePunctuation)
corpus <- tm_map(corpus, removeWords, stopwords('english'))
corpus <- tm_map(corpus, stripWhitespace)

dtm <- DocumentTermMatrix(corpus)
cat('DTM shape:', nrow(dtm), 'docs x', ncol(dtm), 'terms\n')
cat('Sparsity: ', round(tm::sparsity(dtm) * 100, 1), '%\n')

cast_dtm from tidytext

cast_dtm(document, term, value) converts a tidy word-count tibble directly into a DocumentTermMatrix compatible with the topicmodels package. This keeps your pipeline in the tidyverse style.

library(tidytext)
library(dplyr)

# Tidy corpus
corpus <- tibble::tibble(
  doc_id = c(rep(1, 3), rep(2, 3), rep(3, 3), rep(4, 3)),
  text   = c(
    'neural networks deep learning training',
    'gradient descent backpropagation optimizer',
    'machine learning algorithms classification',
    'database sql tables queries indexes',
    'relational schema normalization joins',
    'foreign primary key constraints transactions',
    'recipe bake flour butter oven',
    'cooking temperature simmer saute ingredients',
    'kitchen knife herbs spices garnish'
  )
)

dtm <- corpus |>
  unnest_tokens(word, text) |>
  anti_join(tidytext::stop_words, by = 'word') |>
  count(doc_id, word) |>
  cast_dtm(doc_id, word, n)

cat('DTM:', nrow(dtm), 'docs x', ncol(dtm), 'terms\n')

LDA(dtm, k = 5): Fitting the Model

LDA(dtm, k = K, control = list(seed = 42)) fits an LDA model with K topics using Gibbs sampling or VEM. Always set a seed for reproducibility — LDA is stochastic and results vary between runs without a fixed seed.

library(tidytext)
library(dplyr)
library(topicmodels)

# Build DTM
corpus <- tibble::tibble(
  doc_id = c(rep(1,3), rep(2,3), rep(3,3), rep(4,3), rep(5,3), rep(6,3)),
  text = c(
    'neural networks deep gradient','backpropagation training optimizer','learning model layers',
    'database sql queries indexes','relational tables joins foreign','schema normalization constraints',
    'recipe flour bake butter','cooking temperature oven simmer','kitchen knife herbs spices'
  )[rep(c(1,2,3,4,5,6), each=1)]
)

dtm <- corpus |>
  unnest_tokens(word, text) |>
  count(doc_id, word) |>
  cast_dtm(doc_id, word, n)

# Fit LDA with k=3 topics
lda_model <- LDA(dtm, k = 3, control = list(seed = 42))
cat('LDA model fitted: k=3 topics\n')
cat('Class:', class(lda_model), '\n')

tidy(lda, matrix = 'beta'): Per-Word Topic Probabilities

tidy(lda_model, matrix = 'beta') extracts the per-word topic probabilities (the beta matrix). Each row gives the probability that a specific word was generated by a specific topic. High-probability words define what each topic is "about".

library(topicmodels)
library(tidytext)
library(dplyr)

# Using the built-in AssociatedPress dataset
data('AssociatedPress', package = 'topicmodels')

lda <- LDA(AssociatedPress[1:50, ], k = 4, control = list(seed = 1234))

# Per-word topic probabilities (beta)
beta_tbl <- tidy(lda, matrix = 'beta')
cat('Beta matrix rows:', nrow(beta_tbl), '\n')
cat('Columns:', names(beta_tbl), '\n')

# Top words per topic
top_terms <- beta_tbl |>
  group_by(topic) |>
  slice_max(beta, n = 5) |>
  ungroup()

cat('\nTop 5 words per topic:\n')
print(top_terms)

Visualising Topic Words

Plot the top-N words per topic as a faceted bar chart. Sort within each topic by beta value using reorder_within() and scale_x_reordered() from tidytext for correct independent ordering per facet.

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

data('AssociatedPress', package = 'topicmodels')
lda <- LDA(AssociatedPress[1:100, ], k = 4, control = list(seed = 42))

tidy(lda, matrix = 'beta') |>
  group_by(topic) |>
  slice_max(beta, n = 8) |>
  ungroup() |>
  mutate(term = reorder_within(term, beta, topic)) |>
  ggplot(aes(term, beta, fill = factor(topic))) +
    geom_col(show.legend = FALSE) +
    facet_wrap(~topic, scales = 'free') +
    scale_x_reordered() +
    coord_flip() +
    labs(x = NULL, y = 'Beta', title = 'Top Words per LDA Topic') +
    theme_minimal(base_size = 10)

tidy(lda, matrix = 'gamma'): Per-Document Topic Probabilities

tidy(lda_model, matrix = 'gamma') extracts per-document topic probabilities (the gamma matrix). Each row gives the proportion of a document estimated to come from a specific topic. Documents with high gamma for topic 2 are "about" topic 2.

library(topicmodels)
library(tidytext)
library(dplyr)

data('AssociatedPress', package = 'topicmodels')
lda <- LDA(AssociatedPress[1:30, ], k = 3, control = list(seed = 99))

# Per-document topic proportions (gamma)
gamma_tbl <- tidy(lda, matrix = 'gamma')
cat('Gamma matrix rows:', nrow(gamma_tbl), '\n')

# Dominant topic per document
dominant_topic <- gamma_tbl |>
  group_by(document) |>
  slice_max(gamma, n = 1) |>
  ungroup()

cat('\nDominant topic per document (first 8):\n')
print(head(dominant_topic, 8))

Choosing K: Number of Topics

LDA requires you to pre-specify K. Common approaches: (1) perplexity — lower is better but decreases monotonically with K; (2) coherence score (ldatuning package); (3) domain knowledge; (4) try K = 5, 10, 20 and inspect word lists manually.

library(topicmodels)
library(tidytext)

data('AssociatedPress', package = 'topicmodels')

# Evaluate perplexity for K = 2, 3, 4, 5
k_values <- 2:5
perplexities <- vapply(k_values, function(k) {
  model <- LDA(AssociatedPress[1:50, ], k = k,
               control = list(seed = 42))
  perplexity(model)
}, numeric(1))

results <- data.frame(k = k_values, perplexity = round(perplexities, 1))
cat('Perplexity by K:\n')
print(results)
cat('\nNote: lower perplexity = better fit to training data\n')

Topic Labelling

LDA produces anonymous topics (1, 2, 3…). The human analyst must label each topic by inspecting its top words. Create a lookup table mapping topic numbers to descriptive names and join it onto your results.

library(topicmodels)
library(tidytext)
library(dplyr)

data('AssociatedPress', package = 'topicmodels')
lda <- LDA(AssociatedPress[1:100, ], k = 4, control = list(seed = 7))

# Top 5 words per topic for manual labelling
tidy(lda, matrix = 'beta') |>
  group_by(topic) |>
  slice_max(beta, n = 5) |>
  summarise(top_words = paste(term, collapse = ', ')) |>
  print()

# After inspecting top words, assign human-readable labels
topic_labels <- tibble::tibble(
  topic = 1:4,
  label = c('Politics', 'Economy', 'Sports', 'Science')  # your interpretation
)

cat('\nTopic labels assigned (example):\n')
print(topic_labels)

LDA on Custom Corpus

Here is a complete end-to-end LDA pipeline on a custom corpus: tokenise, build DTM, fit LDA, extract beta matrix, and display the top words per topic.

library(tidytext)
library(dplyr)
library(topicmodels)

# 9 documents across 3 latent topics
docs <- tibble::tibble(
  id   = 1:9,
  text = c(
    'machine learning neural deep training',
    'algorithm gradient backpropagation network',
    'python tensorflow keras model layers',
    'database sql relational tables queries',
    'joins indexes schema foreign primary',
    'transactions normalization constraints',
    'recipe ingredients bake flour butter',
    'cooking temperature simmer oven saute',
    'kitchen knife herbs spices garnish'
  )
)

dtm <- docs |>
  unnest_tokens(word, text) |>
  count(id, word) |>
  cast_dtm(id, word, n)

lda <- LDA(dtm, k = 3, control = list(seed = 123))

tidy(lda, 'beta') |>
  group_by(topic) |>
  slice_max(beta, n = 4) |>
  summarise(words = paste(term, collapse = ', ')) |>
  print()

LDA Limitations

LDA assumes a bag-of-words model (word order doesn't matter), topics are static across the corpus, and K must be chosen in advance. For dynamic or hierarchical topics, consider STM (Structural Topic Model) or CTM (Correlated Topic Model) available in the stm package.

# LDA assumptions and limitations
limitations <- data.frame(
  Assumption = c(
    'Bag of words', 'Fixed K', 'Topic independence',
    'Static topics', 'No metadata'
  ),
  Alternative = c(
    'word2vec / BERT',
    'ldatuning for K selection',
    'CTM (Correlated Topic Model)',
    'DTM (Dynamic Topic Model)',
    'STM (Structural Topic Model)'
  )
)

print(limitations, row.names = FALSE)

Quick Check

In LDA, what does the beta matrix represent?

Recap: Topic Modelling with LDA

Key takeaways:

  • LDA discovers K latent topics as word distributions from a document-term matrix
  • Build DTM with cast_dtm(document, word, n) (tidytext) or DocumentTermMatrix() (tm)
  • LDA(dtm, k = K, control = list(seed = 42)) fits the model
  • tidy(lda, 'beta') = per-word topic probabilities (what defines each topic)
  • tidy(lda, 'gamma') = per-document topic proportions (what each doc is about)
  • Use perplexity or coherence scores to guide choice of K
  • Always manually inspect top words to label topics
library(topicmodels)
library(tidytext)
library(dplyr)

data('AssociatedPress', package = 'topicmodels')
lda <- LDA(AssociatedPress[1:20, ], k = 2, control = list(seed = 1))

tidy(lda, 'beta') |>
  group_by(topic) |>
  slice_max(beta, n = 3) |>
  summarise(top_words = paste(term, collapse = ', ')) |>
  print()

Frequently asked questions

Is the “Topic Modeling with LDA” lesson free?

Yes — the full text of “Topic Modeling with LDA” 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 “Topic Modeling with LDA”?

Discover latent topics in document corpora using the topicmodels package. 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 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Topic Modeling with LDA” 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