Lecture 11
September 30, 2026
For NOAA Storm Events, a document might be:
The choice determines what “frequent” and “distinctive” mean.
# A tibble: 8 × 3
event_id event_type word
<dbl> <chr> <chr>
1 1150291 Drought drought
2 1150291 Drought severity
3 1150291 Drought decreased
4 1150291 Drought severe
5 1150291 Drought moderate
6 1150291 Drought late
7 1150291 Drought january
8 1150292 Drought drought
Now one row is one token occurrence, not one storm event.
A document-term matrix has one row per document, one column per term, and a count or weight in each cell.
# A tibble: 3 × 14
event_type flooding road water reported due hail size report quarter
<chr> <int> <int> <int> <int> <int> <int> <int> <int> <int>
1 Flash Flood 2616 2321 2080 1783 1134 0 0 0 0
2 Hail 0 0 0 2155 0 4991 2194 1569 1547
3 Tornado 0 2760 0 0 0 0 0 0 0
# ℹ 4 more variables: tornado <int>, damage <int>, county <int>, trees <int>
The representation is useful for comparison but cannot recover word order or syntax.
Common function words often overwhelm a frequency table, so analysts remove a stop-word list. But domain-specific meaning can be lost:
Inspect the removed terms rather than treating the list as universally correct.
Stemming reduces related forms to a common root, such as damage, damaged, and damaging. This can improve counting, but the stem may not be a real word and unrelated terms can be merged.
Lemmatization uses vocabulary and grammar to return dictionary forms, but requires more language modeling.
After converting narratives to individual words, list three kinds of information that have been discarded. Which loss matters most for interpreting warnings or impacts?
Frequent terms can be:
Counts describe the corpus; interpretation requires context.
Word clouds make exact frequency and group comparison difficult. Font size is imprecise, placement is arbitrary, and long words attract attention.
A sorted bar chart is usually easier to audit.
if (requireNamespace("wordcloud", quietly = TRUE)) {
cloud_terms <- tokens |>
count(word, sort = TRUE) |>
slice_head(n = 100)
wordcloud::wordcloud(cloud_terms$word, cloud_terms$n, max.words = 100,
colors = c(deep_gold, blue_gray, charcoal), random.order = FALSE)
} else {
plot.new()
text(0.5, 0.5, "Install wordcloud to render this example")
}Use it as an exploratory overview, not as the primary evidence for a ranked comparison.
tokens |>
filter(event_type %in% c("Tornado", "Flash Flood")) |>
count(event_type, word) |>
group_by(event_type) |>
mutate(rate = n / sum(n)) |>
slice_max(rate, n = 10, with_ties = FALSE) |>
ungroup() |>
mutate(word = reorder_within(word, rate, event_type)) |>
ggplot(aes(rate, word, fill = event_type)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ event_type, scales = "free_y") +
scale_y_reordered() +
scale_x_continuous(labels = percent) +
labs(title = "Within-group rates support a direct vocabulary comparison", x = "Share of tokens", y = NULL)For term \(t\) and document/group \(d\):
\[ \mathrm{tfidf}(t,d)=\mathrm{tf}(t,d)\log\left(\frac{N}{\mathrm{df}(t)}\right) \]
Terms score highly when common in one group but uncommon across groups.
tfidf_terms <- tokens |>
filter(event_type %in% c("Flash Flood", "Tornado", "Hail")) |>
count(event_type, word) |>
bind_tf_idf(word, event_type, n) |>
group_by(event_type) |>
slice_max(tf_idf, n = 8, with_ties = FALSE) |>
ungroup()
tfidf_terms |>
mutate(word = reorder_within(word, tf_idf, event_type)) |>
ggplot(aes(tf_idf, word, fill = event_type)) +
geom_col(show.legend = FALSE) +
facet_wrap(~event_type, scales = "free") +
scale_y_reordered() +
labs(title = "TF-IDF surfaces language distinctive to each event type", x = "TF-IDF", y = NULL)Choose one high-TF-IDF word. Read three source narratives containing it. Does the word mean what the chart seems to imply? Report one context the token display hid.
A lexicon maps words to labels or scores. It does not understand:
“Fatal” may be informative, but a sentiment score is not a measure of harm.
sentiment_terms <- tokens |>
filter(event_type %in% c("Tornado", "Flash Flood", "Hail")) |>
inner_join(get_sentiments("bing"), by = "word") |>
count(event_type, sentiment)
sentiment_terms |>
group_by(event_type) |>
mutate(share = n / sum(n)) |>
ggplot(aes(event_type, share, fill = sentiment)) +
geom_col(position = "dodge") +
scale_y_continuous(labels = percent) +
labs(title = "Lexicon matches describe words, not event severity", x = NULL, y = "Share of matched tokens", fill = NULL)# A tibble: 10 × 3
word1 word2 n
<chr> <chr> <int>
1 wind gust 5679
2 wind gusts 3183
3 power lines 2004
4 heat index 1721
5 size hail 1638
6 60 mph 1613
7 mesonet station 1582
8 peak wind 1562
9 thunderstorm winds 1488
10 tree damage 1434
Bigrams preserve adjacent word pairs, helping distinguish phrases such as “flash flood” or “power lines” that single tokens separate.
Instead of “Are narratives positive or negative?”, ask:
Choose one text-based claim. Define the document, token/feature, comparison group, and a manual validation sample that would make the claim credible.
Next: animation, interaction, and choosing the right medium.
MaDS Data Visualization · Fall 2026