library(tidyverse)
theme_set(theme_light())
36-315: Statistical Graphics and Visualization, Summer 2026
The Office, Season 4 Episode 13: Dinner Party
Access via the schrute package
# A tibble: 6 × 3
index character text
<int> <chr> <chr>
1 16791 Stanley This is ridiculous.
2 16792 Phyllis Do you have any idea what time we'll get out of here?
3 16793 Michael Nobody likes to work late, least of all me. Do you have plans…
4 16794 Jim Nope I don't, remember when you told us not to make plans 'ca…
5 16795 Michael Yes I remember. Mmm, this is B.S. This is B.S. Why are we her…
6 16796 Dwight Thank you Michael.
Most common way to store text data: document-term matrix (DTM)
| Word 1 | Word 2 | \(\dots\) | Word \(J\) | |
|---|---|---|---|---|
| Document \(1\) | \(w_{11}\) | \(w_{12}\) | \(\dots\) | \(w_{1J}\) |
| Document \(2\) | \(w_{21}\) | \(w_{22}\) | \(\dots\) | \(w_{2J}\) |
| \(\dots\) | \(\dots\) | \(\dots\) | \(\dots\) | \(\dots\) |
| Document \(N\) | \(w_{N1}\) | \(w_{N2}\) | \(\dots\) | \(w_{NJ}\) |
To reduce number of columns:
Stop words: remove extremely common, functional words (e.g., the, and, is, in, of, etc.)
Stemming: reduce all words to their “stem”
Convert raw text into long, tidy table with one-token-per-document-per-row
Easy to convert text into DTM format using tidytext package
tidytext provides stop_words dataSnowballC package and wordStem() function to perform stemming# A tibble: 6 × 4
index character word stem
<int> <chr> <chr> <chr>
1 16791 Stanley ridiculous ridicul
2 16792 Phyllis idea idea
3 16792 Phyllis time time
4 16793 Michael likes like
5 16793 Michael late late
6 16793 Michael plans plan
# A tibble: 6 × 3
index character bigram
<int> <chr> <chr>
1 16791 Stanley this is
2 16791 Stanley is ridiculous
3 16792 Phyllis do you
4 16792 Phyllis you have
5 16792 Phyllis have any
6 16792 Phyllis any idea
separate() into two words, then filter(), then unite()# A tibble: 6 × 3
index character bigram
<int> <chr> <chr>
1 16793 Michael plans tonight
2 16795 Michael remember mmm
3 16795 Michael call corporate
4 16795 Michael michael scott
5 16795 Michael scott scranton
6 16795 Michael overtime assignment
A word cloud displays all words mentioned across documents, where more common words are larger
First, compute the word counts \[w_{\cdot 1} = \sum_{i=1}^N w_{i1} \hspace{0.1in} \dots \hspace{0.1in} w_{\cdot J} = \sum_{i=1}^N w_{iJ}\]
Then, the size of word \(j\) is proportional to \(w_{\cdot j}\)
Create word clouds in R using wordcloud package
wordcloud() function takes in two main inputs: words(vector of unique words) and freq (vector of frequencies)
Construct vectors of total words for each collection
\(\mathbf{w}^{\mathcal{A}} = (w_{\cdot 1}^{\mathcal{A}}, \dots, w_{\cdot J}^{\mathcal{A}})\)
\(\mathbf{w}^{\mathcal{B}} = (w_{\cdot 1}^{\mathcal{B}}, \dots, w_{\cdot J}^{\mathcal{B}})\)
Consider the \(j\)th word (say “dinner”)
If \(w_{\cdot j}^{\mathcal{A}}\) is large, then “dinner” is large in the word cloud for \(\mathcal{A}\)
If \(w_{\cdot j}^{\mathcal{B}}\) is large, then “dinner” is large in the word cloud for \(\mathcal{B}\)
But if both are large, this does not indicate whether \(w_{\cdot j}^{\mathcal{A}}\) or \(w_{\cdot j}^{\mathcal{B}}\) is bigger
michael_jan_summary <- transcripts_tokens |>
count(character, stem) |>
filter(character %in% c("Michael", "Jan"))
library(reshape2)
michael_jan_summary |>
acast(stem ~ character, value.var = "n", fill = 0) |>
comparison.cloud(colors = c("red", "blue"), max.words = 100, scale = c(2, 0.1), title.size = 2)Goal: compare text across documents (e.g., The Office characters)
Determine the most unique words for each document using tf-idf
Approach: downweight words that occur frequently across all documents
\[\textsf{idf}_j = \log \frac{N}{N_j},\] where \(N\) is number of documents and \(N_j\) is number of documents containing word \(j\)
\[\textsf{tf-idf}_j = \textsf{tf}_j \times \textsf{idf}_j = w_{ij} \times \textsf{idf}_j\]
Use bind_tf_idf() from tidytext package
# A tibble: 6 × 6
character stem n tf idf tf_idf
<chr> <chr> <int> <dbl> <dbl> <dbl>
1 All cheer 1 1 2.77 2.77
2 Andy anim 1 0.0476 2.77 0.132
3 Andy bet 1 0.0476 2.08 0.0990
4 Andy capit 1 0.0476 2.77 0.132
5 Andy dinner 1 0.0476 0.981 0.0467
6 Andy flower 2 0.0952 2.77 0.264
transcripts_tfidf |>
filter(character %in% c("Michael", "Jan", "Jim", "Pam")) |>
group_by(character) |>
slice_max(tf_idf, n = 7, with_ties = FALSE) |>
mutate(stem = reorder_within(stem, tf_idf, character)) |>
ggplot(aes(x = tf_idf, y = stem)) +
geom_col() +
scale_y_reordered() +
facet_wrap(~ character, ncol = 2, scales = "free") +
labs(x = "tf-idf", y = NULL)Lexicon-based approach
Find a sentiment lexicon/dictionary
Compute summary (e.g., average sentiment)
Different types of sentiment lexicons
Positive or negative
Numerical score (e.g., from −5 to +5)
Specific emotions (e.g., sadness, anger, joy, etc.)
# A tibble: 6 × 5
index character word stem sentiment
<int> <chr> <chr> <chr> <chr>
1 16791 Stanley ridiculous ridicul negative
2 16793 Michael likes like positive
3 16795 Michael mad mad negative
4 16795 Michael fair fair positive
5 16795 Michael enjoy enjoi positive
6 16797 Michael happy happi positive
See this post