Homework 6: Word Relationships & Topic Models
Introduction
In this assignment you will apply the n-gram, word-correlation, and topic modeling skills to learn more about why students chose to enter the criminal justice major.
Note: While many of these steps could be done together into one code chunk, please keep your code for a particular step right under that step.
Setup
Load the tidyverse, tidytext, widyr, igraph, ggraph and topicmodels packages in this code chunk, along with your data.
A little bit of data preparation steps…
Remove the #| eval: false from the top of each provided code chunk in your template as you get to it.
cj_long <- cj_data |>
# add a unique respondent ID to each row
mutate(respondent_id = as.character(row_number())) |>
# rename the questions to shorter variable names
select(respondent_id, cj = why_cj, pols = why_pols_legal, para = why_paralegal_cert) |>
# reshape so it's one row per response-question combination
pivot_longer(cj:para, names_to = "program", values_to = "response", values_drop_na = TRUE)1. Describe what each row is now.
Review the output below and describe in your own words what each row represents now. How many responses are there in total?
Part A — Relationships between words
2. Bigrams
Let’s look at which pairs of words show up together.
- tokenize
responseinto bigrams. save ascj_bigrams - split each bigram into two words. Save as
bigrams_separated - remove stop words from both positions. Save as
bigrams_filtered - Count the pairs
In 2-3 sentences, describe the top 10 bigrams. Which seem meaningful for understanding students’ reasons, and which just reflect the wording of the survey question?
3. Assess impact of valence shifting.
Explore how much negation, or other valence shifting, is in these responses. Could it change how we interpret their sentiment? Here is a vector of potential valence shifters to consider.
shifters <- c("not", "no", "never", "don't", "didn't", "very", "really", "but", "however")Using the
filterandcountfunctions, count how often each word inshiftersstarts a bigram. How many responses have one or more of these shifters? What is this percent out of the total number of responses? Is this a large percent?Pick one of the
shifterwords other than “not”. Join the words that follow it (word2) to the AFINN lexicon and plot what it changes. Note: if an empty plot shows up, what does that tell you about the data? (my code is wrong is not what i’m looking for.))The word “not” is a common negator. Below I have pulled out and printed the first 5 responses that use this word. Based on your reading, how much negation of sentiments/emotions is happening?
4. Custom stop words and word correlations
Create a custom stop-word list called
custom_stop. You can reuse or adapt the list you made for Homework 3, and add any words you noticed in questions 2 and 3 that echo the survey wording or don’t tell us much. Then create the data framecj_wordsby tokenizingresponseinto single words and removing all stop words (orginal and custom).Fill in the blanks below to keep words in
cj_wordsthat appear at least 10 times, then calculate the pairwise correlations betweenwords across people (respondent_id). Save the data frame asword_cors. Look at the top few rows of this data set usinghead()and comment on what you notice.
_______ <- _______ |>
group_by(word) |>
filter(n() >= 10) |>
pairwise_cor(_______, _______, sort = TRUE)5. Draw the word network
Turn word_cors into a network graph. Filter on correlation to hide the weakly correlated pairs. Choose a cutoff that leaves a graph you can read, and tell us what cutoff you used. Run set.seed(1234) before you draw it so the layout doesn’t change every time you render.
Part B — Topic Modeling
6. Build the document-term matrix
Finish the code below to count how often each word appears in each response, then cast the counts into a document-term matrix. How many documents are being analyzed, and what is the sparsity?
Code to combine respondent_id and program into one new column called unique_id is provided. For example “11_cj” is respondent 11’s response to why they chose the cj major, “. This allows for each response to have it’s own unique ID, which cast_dtm() needs.
(cj_dtm <- _______ |>
unite(unique_id, respondent_id, program) |>
count(_______, _______) |>
cast_dtm(_______, _______, _______))7. Fit and choose a topic model
Fit
LDA()at two different values of k, with the sameseed = 8675309. For each model, tidy the \(\beta\) values, calculate the top terms per topic, and then plot the top 8 terms in each topic.Try to name the topics. Then choose which model you think makes more sense (e.g. choose which \(k\) to use). Save your chosen model as
cj_lda.
cj_lda <- _______8. Analyzing topic patterns
When we tidy a LDA object, it renames the unique identifier variable document. So let’s rename that back so it makes sense to us.
- Do student response align cleanly into a single topic? The code below filters
gamma.probsto pull out a few students by theirunique_id, then reshapes it to wide by topic to make it easier to read. Examine and interpret their topic probabilities. Remember what we’re analyzing. A response is a student’s reason for choosing that specific educational program. Write your answer in this context.
gamma.probs |>
filter(str_starts(unique_id, "11_|12_|151_")) |>
arrange(unique_id, topic) |>
pivot_wider(names_from = topic, names_prefix = "Topic",
values_from = gamma)- How do these topics differ across the three programs? Fill in the blank below to plot the topic each response most belongs to (its largest \(\gamma\)), by program. Since each program has a different number of responses, we need to compare proportions, not direct counts.
gamma.probs |>
group_by(_______) |>
slice_max(_______) |>
ungroup() |>
separate(unique_id, c("respondent_id", "program"), sep = "_") |>
count(program, topic) |>
group_by(program) |>
mutate(prop = n / sum(n), topic = factor(topic)) |>
ggplot(aes(x=_______, y=_______, fill = topic)) +
geom_col() +
facet_wrap(~ _______)9. Reflection
Name one benefit of LDA over manual open coding.
Challenge question
Redraw your network from question 5 with the LDA topics drawn over it as shaded hulls. Each word is placed in the topic where it is most likely to appear (\(\beta\)). Fill in the same cutoff you used in question 5. This code uses the ggforce package, so you may need to install.packages("ggforce") first.
Keep changing the seed before graph to redraw the network map and hulls until you find something that looks reasonably good.
In 3-4 sentences: did the hulls tell you something you didn’t already know from the network alone, or lead you to notice or wonder about something new?
word_topic <- tidy(cj_lda, matrix = "beta") |>
group_by(term) |>
slice_max(beta, n = 1) |>
ungroup() |>
transmute(name = term, topic = factor(topic))
cors <- word_cors |>
filter(correlation > ______)
g <- graph_from_data_frame(
cors,
vertices = filter(word_topic, name %in% c(cors$item1, cors$item2))
)
set.seed(123)
ggraph(g, layout = "fr") +
ggforce::geom_mark_hull(aes(x, y, group = topic, fill = topic),
alpha = .2, concavity = 4, expand = unit(2, "mm")) +
geom_edge_link(aes(edge_alpha = correlation), show.legend = FALSE) +
geom_node_point() +
geom_node_text(aes(label = name), repel = TRUE) +
theme_void()