Skip to content
R for the Humanities

When a topic model in R produces nonsense, the cause is almost always upstream of the model: dirty text, empty documents, a badly chosen K, or a missing seed. Fix preprocessing first, align metadata to the documents that actually survive prepDocuments(), then tune K with searchK(). This guide walks the failures I hit most often with the stm package on historical corpora and the fixes that hold.

Why are all my topics just stopwords and noise? ​

This is a preprocessing failure, full stop. If you feed raw text in, the highest-probability words in every topic will be "the", "and", "of" — or, on OCR'd material, garbage tokens like "tbe" and "wbich".

r
library(stm)

processed <- textProcessor(
  documents = corpus$text,
  metadata  = corpus,
  lowercase = TRUE,
  removestopwords = TRUE,
  removenumbers = TRUE,
  stem = FALSE,                      # stemming hurts interpretability on historical text
  customstopwords = c("hath", "thy", "thou", "tbe", "wbich")
)
out <- prepDocuments(processed$documents, processed$vocab,
                     processed$meta, lower.thresh = 2)

Set lower.thresh = 2 so terms in only one document are dropped — those single-document terms inflate the vocabulary and produce incoherent, junk-only topics.

How do I fix "contains missing values" from prepDocuments? ​

After cleaning, some documents become empty and are removed. If you fit stm() against your original metadata frame, the covariate rows no longer line up with the documents.

r
out <- prepDocuments(processed$documents, processed$vocab, processed$meta,
                     lower.thresh = 2)
# out$meta is already re-aligned — USE IT, not the original frame
fit <- stm(out$documents, out$vocab, K = 30,
           prevalence = ~ decade, data = out$meta,
           init.type = "Spectral", seed = 1851)

Always pass out$meta to stm(). The processed$docs.removed and out$docs.removed indices tell you exactly which rows vanished if you need to debug a mismatch.

How do I choose K without guessing? ​

Run searchK() over a range and read two curves: held-out likelihood (predictive fit) and semantic coherence (do top words co-occur?). They trade off against exclusivity.

r
k_search <- searchK(out$documents, out$vocab,
                    K = c(10, 20, 30, 40, 50),
                    prevalence = ~ decade, data = out$meta)
plot(k_search)
SymptomLikely causeFix
Topics merge distinct themesK too lowIncrease K, re-read coherence
Many near-duplicate topicsK too highLower K or raise lower.thresh
Coherence high, sense lowBoilerplate co-occurrenceStrip headers/OCR junk, refit
Different topics each runRandom init, no seedinit.type = "Spectral", set seed

Why do my topics change every time I run the model? ​

Topic models are stochastic. With random initialisation, each run lands in a different local optimum. Use spectral initialisation, which is deterministic, and still set a seed so the whole pipeline is reproducible. Spectral starts also tend to converge faster and to more stable solutions on medium corpora.

How do I read a model once it fits? ​

Do not trust the top-words list alone. Use labelTopics() for FREX words (frequent and exclusive), then read real documents with findThoughts().

r
labelTopics(fit, n = 8)
findThoughts(fit, texts = out$meta$title, topics = 7, n = 3)

Reading three actual documents per topic catches the "coherent but meaningless" trap that pure metrics miss.

Key Takeaways ​

  • Most bad topic models are bad preprocessing — clean before you blame the model.
  • Set lower.thresh = 2 or higher to drop single-document junk terms.
  • Always fit against out$meta, the re-aligned metadata from prepDocuments().
  • Choose K with searchK(), balancing coherence, exclusivity and interpretability.
  • Use init.type = "Spectral" plus a seed for reproducible, stable topics.
  • Validate by reading real documents with findThoughts(), not just top-word lists.
  • On OCR'd corpora, custom stopwords for scanning artefacts dramatically improve results.

Frequently Asked Questions ​

Why does my stm model produce topics that are all stopwords? ​

Your preprocessing did not remove function words or low-frequency junk. Run textProcessor() with a stopword list and then prepDocuments() with a lower.thresh of at least 2 to drop terms appearing in only one document.

How many topics (K) should I choose? ​

There is no single correct K. Use searchK() across a range like 10 to 60, read the held-out likelihood and semantic coherence curves, then pick a value where coherence is high and the topics are humanly interpretable.

Why do I get "Error: contains missing values" from prepDocuments? ​

Empty documents survive preprocessing and become zero-length. prepDocuments() returns a docs.removed index — re-align your metadata to the kept documents before fitting or the covariate rows will not match.

Should I use stm, topicmodels, or text2vec? ​

Use stm when you have document metadata (date, genre, author) you want as covariates; topicmodels (LDA/CTM) for a classic baseline; text2vec when speed on a large corpus matters more than covariate effects.

Why are my topics unstable between runs? ​

Topic models are stochastic. Set a seed in stm() and use spectral initialisation (init.type = "Spectral") which is deterministic and usually converges to better, more reproducible solutions than random starts.

My coherence is high but the topics make no sense — what now? ​

High coherence with low usefulness usually means residual boilerplate (headers, OCR garbage, catalogue text) co-occurs predictably. Clean those artefacts and consider raising lower.thresh, then refit.