Reading a word cloud carefully

Compare the picture with the count table that made it

information visualization
word clouds
inaugural addresses
Learn how word clouds encode word counts, why the layout can mislead, and how to check the same words in a sorted table.

Twenty-two words can fill Iris’s slide about the inaugural addresses. The table behind the picture matters more than the shape of the picture itself.

The tempting part is also the risk. A large word can mean a high count, a long spelling, or a lucky position in the packing algorithm.

Word clouds draw words from a count table with larger type for larger counts. They are useful for a quick vocabulary preview. They are weak for exact comparisons, rank order, meaning, and change over time.

TipWhat you will learn

This lesson shows how to:

  • build one corpus-wide word-count table from inaugural paragraphs;
  • use a named stop-word lexicon and inspect what it removes;
  • draw a sorted bar chart and a word cloud from the same keyed counts;
  • keep word-cloud size, layout, and colour claims honest;
  • test a word-length trap with real words from the data; and
  • provide a two-part text alternative for the cloud.

Build one count table

The corpus helper returns one row per paragraph. This lesson asks one corpus-wide question: after a stated cleanup, which words occur most often across all inaugural paragraphs? That flattening weights long speeches more than short speeches, so the output is a vocabulary preview, not a claim about importance.

library(dplyr)
library(tidyr)
library(tibble)
library(stringr)
library(tidytext)
library(ggplot2)
library(ggwordcloud)
library(purrr)
library(knitr)

source("R/inaugural-corpus.R")

paragraphs <- inaugural_paragraphs()

all_word_tokens <- paragraphs |>
  select(paragraph_id, speech_id, paragraph) |>
  unnest_tokens(word, paragraph)

dropped_by_letters_filter <- all_word_tokens |>
  filter(!str_detect(word, "^[a-z]+$"))

word_tokens <- all_word_tokens |>
  filter(str_detect(word, "^[a-z]+$"))

onix_stop_words <- stop_words |>
  filter(lexicon == "onix") |>
  distinct(word)

inaugural_word_counts <- word_tokens |>
  anti_join(onix_stop_words, by = join_by(word)) |>
  count(word, sort = TRUE, name = "count") |>
  mutate(
    word_key = word,
    letters = str_length(word)
  )

cloud_words <- inaugural_word_counts |>
  slice_head(n = 22) |>
  mutate(word_for_plot = reorder(word_key, count))

count_table <- cloud_words |>
  transmute(
    word_key,
    count,
    letters
  )

top_five_bars <- count_table |>
  slice_head(n = 5)

bar_alt_text <- sprintf(
  paste(
    "Horizontal bar chart of 22 words.",
    "The first five bars are %s (%s), %s (%s), %s (%s), %s (%s), and %s (%s).",
    "Every bar comes from the same count table used for the cloud."
  ),
  top_five_bars$word_key[1],
  top_five_bars$count[1],
  top_five_bars$word_key[2],
  top_five_bars$count[2],
  top_five_bars$word_key[3],
  top_five_bars$count[3],
  top_five_bars$word_key[4],
  top_five_bars$count[4],
  top_five_bars$word_key[5],
  top_five_bars$count[5]
)

cloud_alt_seed_7701 <- paste(
  "Word cloud of the same 22 count-table words.",
  "The largest words by count are government with 585 and people with 561;",
  "the exact positions are not data.",
  "The full count table above is the long text alternative."
)

stopword_note <- tibble(
  lexicon = "onix",
  removes_states = "states" %in% onix_stop_words$word,
  removes_united = "united" %in% onix_stop_words$word
)

kable(
  count_table,
  col.names = c("Word key", "Count", "Letters"),
  caption = "Top words after removing the onix stop-word lexicon from inaugural paragraphs",
  row.names = FALSE
)
Top words after removing the onix stop-word lexicon from inaugural paragraphs
Word key Count Letters
government 585 10
people 561 6
so 384 2
country 306 7
world 305 5
nation 299 6
citizens 242 8
own 242 3
peace 240 5
power 240 5
public 224 6
time 214 4
constitution 207 12
united 198 6
nations 195 7
union 188 5
free 179 4
freedom 175 7
war 174 3
america 166 7
american 160 8
national 157 8
kable(
  stopword_note,
  col.names = c("Stop-word lexicon", "Removes `states`", "Removes `united`"),
  caption = "The named stop-word choice used for the count table",
  row.names = FALSE
)
The named stop-word choice used for the count table
Stop-word lexicon Removes states Removes united
onix TRUE FALSE

The onix lexicon removes states but keeps united, so a phrase such as United States can leave united by itself. A different stop-word list would produce a different table.

unnest_tokens() lowercases the paragraphs and splits punctuation away from words. The ^[a-z]+$ filter keeps only tokens made entirely of letters, so it drops whole tokens such as america's; in this corpus it drops 367 tokens. After that filter and the onix stop-word list, so survives 384 times because so is not in the onix list.

Show the counts before the cloud

A sorted bar chart puts the same words on a common horizontal scale. Position along a common scale is easier to compare than text area, so the chart is the check Iris should read before the cloud. Cleveland and McGill placed area below position in their hypothesized ranking of graphical perception tasks; they did not run the word-cloud experiment on this page.

bar_plot <- ggplot(cloud_words, aes(x = count, y = word_for_plot)) +
  geom_col(fill = "#3B5F8A") +
  labs(
    x = "Count in inaugural paragraphs",
    y = NULL
  ) +
  theme_minimal()

bar_plot
Horizontal bar chart of 22 words. The first five bars are government (585), people (561), so (384), country (306), and world (305). Every bar comes from the same count table used for the cloud.
Figure 1: The 22 words used in the word cloud, sorted by count after onix stop-word removal.

The bar chart and the table use the same word_key and count values. No position in the later cloud is evidence of a relationship between words.

Draw the same counts as a cloud

The cloud below uses geom_text_wordcloud() with one colour and no rotation. scale_size_area() maps the numeric count through an area scale for the size aesthetic: a count of zero would map to zero, and the largest count maps to the largest font size in this plot. Font size grows with the square root of the count, so each letter’s area follows the count, and a word’s whole footprint also grows with its number of letters. It does not divide by word length. A longer word can still cover more horizontal space than a shorter word with a similar or even larger count.

set.seed(7701)
cloud_plot_seed_7701 <- ggplot(
  cloud_words,
  aes(label = word_key, size = count)
) +
  geom_text_wordcloud(
    seed = 7701,
    color = "#3B5F8A",
    angle = 0,
    rm_outside = FALSE
  ) +
  scale_size_area(max_size = 9) +
  labs(size = "Count") +
  theme_minimal()

cloud_plot_seed_7701
Word cloud of the same 22 count-table words. The largest words by count are government with 585 and people with 561; the exact positions are not data. The full count table above is the long text alternative.
Figure 2: A word cloud drawn from the same 22 counts as the bar chart, using seed 7701.

The middle word is the first row drawn, not the topic of the speeches. Because the table is sorted by count, government is drawn first. If the table were alphabetical, a different word could start in the centre. The packing layout also depends on the seed, device, and fonts, so the page does not claim that any word belongs at a particular coordinate.

The area-corrected ggwordcloud option, area_corr, is a different choice. It shrinks or grows labels so each whole text box better follows the count, which trades the length bias for a letter-height bias because short words get taller type. Cleveland and McGill did not test word clouds; Felix and colleagues later found that bar length beat font size for judging values.

The short part of the text alternative is the cloud’s alt text: “Word cloud of the same 22 count-table words. The largest words by count are government with 585 and people with 561; the exact positions are not data. The full count table above is the long text alternative.” The long part is the sorted count table above, which lists every word and count used in the cloud.

Change the seed, not the data

The next cloud uses the identical keyed counts and a different seed. It is still the same input table.

set.seed(7702)
cloud_plot_seed_7702 <- ggplot(
  cloud_words,
  aes(label = word_key, size = count)
) +
  geom_text_wordcloud(
    seed = 7702,
    color = "#3B5F8A",
    angle = 0,
    rm_outside = FALSE
  ) +
  scale_size_area(max_size = 9) +
  labs(size = "Count") +
  theme_minimal()

cloud_plot_seed_7702
Second word cloud with the same words and counts as the first cloud but a different random layout seed. Government still has 585 counts and people has 561; position changes are layout artifacts.
Figure 3: The same 22 count-table words drawn again with seed 7702.

If the two pictures feel different, that feeling is the warning. The counts did not change.

Check preprocessing sensitivity

Stop-word lists are editorial choices. The table below recomputes the top words with three named choices: onix only, snowball only, and all three tidytext lexicons together.

stopword_choices <- list(
  "onix only" = stop_words |>
    filter(lexicon == "onix") |>
    distinct(word),
  "snowball only" = stop_words |>
    filter(lexicon == "snowball") |>
    distinct(word),
  "all tidytext lexicons" = stop_words |>
    distinct(word)
)

sensitivity_table <- imap_dfr(
  stopword_choices,
  \(stop_table, stop_choice) {
    word_tokens |>
      anti_join(stop_table, by = join_by(word)) |>
      count(word, sort = TRUE, name = "count") |>
      slice_head(n = 10) |>
      mutate(stop_choice = stop_choice, rank = row_number(), .before = 1)
  }
)

kable(
  sensitivity_table,
  col.names = c("Stop-word choice", "Rank", "Word", "Count"),
  caption = "Top words change when the stop-word lexicon changes",
  row.names = FALSE
)
Top words change when the stop-word lexicon changes
Stop-word choice Rank Word Count
onix only 1 government 585
onix only 2 people 561
onix only 3 so 384
onix only 4 country 306
onix only 5 world 305
onix only 6 nation 299
onix only 7 citizens 242
onix only 8 own 242
onix only 9 peace 240
onix only 10 power 240
snowball only 1 will 940
snowball only 2 government 585
snowball only 3 people 561
snowball only 4 us 468
snowball only 5 can 456
snowball only 6 upon 369
snowball only 7 must 359
snowball only 8 great 342
snowball only 9 states 331
snowball only 10 may 326
all tidytext lexicons 1 government 585
all tidytext lexicons 2 people 561
all tidytext lexicons 3 country 306
all tidytext lexicons 4 world 305
all tidytext lexicons 5 nation 299
all tidytext lexicons 6 citizens 242
all tidytext lexicons 7 peace 240
all tidytext lexicons 8 power 240
all tidytext lexicons 9 public 224
all tidytext lexicons 10 time 214

With snowball only, common words such as will, us, and states remain. With onix, states is removed while united remains. A cloud without this preprocessing note would look more objective than it is.

Check the word-length trap

The next table compares real words from the count table. The rough space score is letters * sqrt(count). It is not a perception model; it only shows why a long word can occupy more horizontal room even when its count is lower. Alexander and coauthors tested word-length effects in word clouds; Felix, Franconeri, and Bertini tested magnitude judgments for several visual encodings, not word length.

length_pairs <- inaugural_word_counts |>
  filter(word_key %in% c("constitution", "time", "peace", "government", "people")) |>
  transmute(
    word_key,
    count,
    letters,
    rough_horizontal_space = round(letters * sqrt(count), 1)
  ) |>
  arrange(desc(rough_horizontal_space))

kable(
  length_pairs,
  col.names = c("Word", "Count", "Letters", "Rough space score"),
  caption = "Long words can cover more space than shorter words with higher counts",
  row.names = FALSE
)
Long words can cover more space than shorter words with higher counts
Word Count Letters Rough space score
government 585 10 241.9
constitution 207 12 172.6
people 561 6 142.1
peace 240 5 77.5
time 214 4 58.5

constitution appears 207 times and time appears 214 times. The longer word can still take about 3 times the horizontal space under this simple geometry check. That is why Iris should not ask readers to compare close counts from font size.

What this cloud does not show

The cloud does not show word order, phrases, relationships, distinctiveness, change over time, or importance. It also does not show that united and states were part of a phrase before preprocessing split and filtered them.

Use a cloud as a doorway to the table, not as the table’s replacement.

What to remember

  • A word cloud starts from a count table; keep that table visible.
  • The onix stop-word lexicon removes states but keeps united in this corpus.
  • The bar chart and both clouds here use the same 22 keyed counts.
  • Word-cloud placement, adjacency, centre position, and colour carry no meaning.
  • Long words can look stronger than shorter words with similar counts.
  • Sorted counts are the long text alternative for the cloud.

Iris keeps the cloud, but the slide title points to the count table: “Common words after one stated cleanup.”

Sources