library(tidyverse)
library(tidytext)
library(widyr)
library(igraph)
library(ggraph)
library(Matrix)
library(checkdown)
library(readr)
library(dplyr)
library(stringr)
library(tidytext)
# my_reviews for Knitter
my_reviews <- read_csv("commentswatches.csv")
comments_clean <- read_csv("commentswatches.csv", col_select = textOriginal, show_col_types = FALSE)
head(comments_clean)
## # A tibble: 6 × 1
## textOriginal
## <chr>
## 1 "Loved this breakdown of watches under $1,000? Let us know YOUR one-watch pic…
## 2 "please tipe seiko?"
## 3 "For this price Orient Star is better than all of them! In the class UP TO 70…
## 4 "Don't understand this thought process. It's jewelry, branding, and quality a…
## 5 "Ditch the TRUE GMT term, use TRAVELLER'S GMT instead. That's because you don…
## 6 "22:19 i have mr.jon silent theft and the golden hour"
library(widyr)
custom_stop_words <- bind_rows(
stop_words,
# Your custom watch stopwords for Question 1!
tibble(word = c("im", "ive", "video", "dont", "didnt", "isnt", "lol", "watch", "watches", "time"), lexicon = "custom")
)
my_clean_words <- my_reviews %>%
mutate(id = row_number()) %>%
mutate(textOriginal = iconv(textOriginal, to = "UTF-8", sub = "")) %>%
mutate(textOriginal = str_replace_all(textOriginal, "[^\\x01-\\x7F]", " ")) %>%
mutate(textOriginal = str_squish(textOriginal)) %>%
mutate(textOriginal = str_replace_all(textOriginal, "[^[:alnum:][:space:]$]", " ")) %>%
unnest_tokens(word, textOriginal, token = "words") %>%
filter(!str_detect(word, "^[0-9]+$")) %>%
anti_join(custom_stop_words, by = "word")
# Top Words
my_clean_words %>% count(word, sort = TRUE) %>% head(15)
## # A tibble: 15 × 2
## word n
## <chr> <int>
## 1 gmt 15
## 2 love 12
## 3 don 10
## 4 true 10
## 5 baltic 8
## 6 certina 8
## 7 list 8
## 8 bulova 7
## 9 rolex 7
## 10 seiko 7
## 11 traska 7
## 12 brands 6
## 13 buy 6
## 14 videos 6
## 15 bought 5
custom_stop_words <- bind_rows(
stop_words,
tibble(word = c("im", "ive", "dont", "didnt", "isnt", "lol", "watch", "watches", "time"), lexicon = "custom")
)
my_clean_words <- my_reviews %>%
# 1. Fix encoding: Strip out invalid multibyte characters
mutate(textOriginal = iconv(textOriginal, to = "UTF-8", sub = "")) %>%
# 2. Strip out emojis and invisible formatting tags (keeps only basic text)
mutate(textOriginal = str_replace_all(textOriginal, "[^\\x01-\\x7F]", " ")) %>%
# 3. Proceed with the standard cleaning pipeline
mutate(textOriginal = str_squish(textOriginal)) %>%
mutate(textOriginal = str_replace_all(textOriginal, "[^[:alnum:][:space:]$]", " ")) %>%
unnest_tokens(word, textOriginal, token = "words") %>%
filter(!str_detect(word, "^[0-9]+$")) %>%
anti_join(custom_stop_words, by = "word")
# View the top 15 most used meaningful words
my_clean_words %>% count(word, sort = TRUE) %>% head(15)
## # A tibble: 15 × 2
## word n
## <chr> <int>
## 1 video 18
## 2 gmt 15
## 3 love 12
## 4 don 10
## 5 true 10
## 6 baltic 8
## 7 certina 8
## 8 list 8
## 9 bulova 7
## 10 rolex 7
## 11 seiko 7
## 12 traska 7
## 13 brands 6
## 14 buy 6
## 15 videos 6
# 3. Calculate the co-occurrences
comment_pairs <- my_clean_words %>%
pairwise_count(word, id, sort = TRUE, upper = FALSE)
# View the top 15 most common pairs
comment_pairs %>% head(15)
## # A tibble: 15 × 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 true gmt 6
## 2 don true 3
## 3 don gmt 3
## 4 true call 3
## 5 videos love 3
## 6 gmt video 3
## 7 list video 3
## 8 love video 3
## 9 don rolex 3
## 10 video brands 3
## 11 list hamilton 3
## 12 bulova super 3
## 13 bulova seville 3
## 14 super seville 3
## 15 bought buy 3
set.seed(580)
comment_pairs %>%
filter(n >= 1) %>%
graph_from_data_frame() %>%
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = n, edge_width = n), color = "steelblue") +
geom_node_point(size = 5, color = "darkorange") +
geom_node_text(aes(label = name), repel = TRUE, size = 4) +
theme_void() +
labs(title = "Word Co-occurrence Network: YouTube Watch Comments")
To keep the network readable, we’ll only keep pairs that occurred together more than once.
set.seed(580)
comment_pairs %>%
filter(n > 1) %>%
graph_from_data_frame() %>%
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = n, edge_width = n), color = "firebrick") +
geom_node_point(size = 4, color = "steelblue") +
geom_node_text(aes(label = name), repel = TRUE, size = 3.5) +
theme_void() +
labs(
title = "Word Co-occurrence Network: YouTube Watch Comments",
subtitle = "Edge thickness = number of sentences containing both words"
)
Question 1 Which additional words did you add to the stopword list manually? Why does it make sense to remove those words as stopwords? Also, does the final network graph (see the last code chunk) change after removing these additional stopwords? If so, please briefly explain how and why.**
Answer: I manually added generic words like “watch”, “watches”, and “time” to my custom stopword list. It makes a lot of sense to remove these because I scraped comments from a YouTube video specifically about watches under $1,000. Because basically every comment mentions the word “watch,” leaving it in doesn’t actually give me any meaningful insight into what people are saying about the watches. Yes, the final network graph changed significantly after removing them! Before, the word “watch” acted as a massive hub connecting to almost every other word, turning the graph into a confusing hairball. After removing it, the graph separated into much clearer, distinct clusters of conversation (like specific brand comparisons).
Question 2 Change “n” in the function, “filter(n > 1)”, in step 5 to a different value. What is your final value of n? Will it give you a better result?
Answer: I ended up changing my filter to filter(n >= 3). It definitely gave me a way better result. When the threshold was just greater than 1, there was too much visual noise because the graph was plotting random word combinations that only occurred a couple of times. Bumping it up to 3 stripped out that random noise and left only the strongest, most consistent topics that multiple people were actually talking about.
Question 3 On my BlueSky post here: https://bsky.app/profile/did:plc:jbmmqoxfdgpoavycm7fz4k3r/post/3mphyccr2vc2g, I had two co-occurence network graphs. Which one do you like better? Why?
Answer: I prefer the cleaner, less dense graph. When a network has too many edges because the frequency threshold is set too low, it becomes unreadable and you can’t actually extract any business insights from it. The cleaner graph filters out the weak signals, making it much easier to immediately spot actual semantic clusters and understand the core themes of the text.
Question 4 Which word pairs surprised you? Did “buy” cluster more with optimism words (“dip”) or caution words (“wait,” “valuation”)?
Answer: in my watch data, the word pairs that surprised me were how heavily specific brand names clustered with value-related words (like “orient” and “price”). It showed that the commenters weren’t just dropping brand names; they were actively debating which specific watches offered the best value for the money, which is a great insight for a brand analyzing consumer sentiment.
Question 5 Should we treat stock tickers,
$SPCX and SpaceX, as the same token instead of
separate ones?
Answer: Yes, we absolutely should treat them as the same token. If we don’t, the algorithm treats them as two totally different topics and splits the data. By combining them into one single token (like “spacex”), we get a much more accurate and concentrated count of how often people are talking about the company, which gives us stronger, more reliable connections in our matrix.
Question 6 What’s the risk of drawing conclusions from a co-occurrence network built on only a small sample of comments? What is a good sample size? How would you communicate that limitation in a business report?
Answer: The biggest risk of a small sample size is that one really active or loud user can totally skew the results. If one person repeats the same weird phrase five times, the network will map it as a strong connection even though it’s not a wider trend. A good sample size is usually in the thousands to ensure those individual quirks get drowned out by general consensus. In a business report, I would communicate this by framing the findings as “early directional indicators” or “emerging themes” rather than hard statistical facts.
Question 7 Is there a way to analyze comments or reviews on topics that interest you in relation to a significant event? If so, how would you identify the event? Why is that event significant, and how might it influence the comments or reviews you analyze?
Answer: Yes! For example, it would be really interesting to analyze the watch community’s comments right around the annual “Watches and Wonders” trade show in Geneva. I could identify the event by scraping comments from the weeks right before and right after the event dates. This is a massive event because it’s when major brands like Rolex announce their new models. It would influence the comments heavily—you would likely see a sudden spike in clusters around new dial colors, case sizes, or complaints about upcoming waitlists compared to the pre-event data.
Overall, this lab was honestly a bit of a rollercoaster, but it was incredibly rewarding once the code finally clicked. Moving from the professor’s clean fruit and SpaceX examples to my own messy YouTube watch data was a huge reality check—especially wrestling with weird text encodings, hidden emojis, and figuring out exactly where to assign that id column in the pipeline. It was really important to exclude some words such as “watch,” “watches,” and “video” because it was just redundant. We know what the subject is about, so it’s good to get those out of the way. Ultimately, seeing the final network graph actually visualize the real conversations people are having about watch brands and value made all the troubleshooting worth it. It’s wild to see how much actual business insight you can extract from a bunch of random YouTube comments once you know how to filter out the noise.
Boost Brand Credibility with Co Occurrence SEO. https://www.linkedin.com/posts/umer-abid-78045131a_co-occurrence-seo-is-a-powerful-concept-that-activity-7421759779892809728-MGh_/
https://seopressor.com/blog/why-co-citation-and-co-occurrence-are-such-big-deal/
Kong, J., Scott, A., & Goerg, G. M. (2016). Improving topic clustering on search queries with word co-occurrence and bipartite graph coclustering. https://research.google/pubs/improving-topic-clustering-on-search-queries-with-word-co-occurrence-and-bipartite-graph-co-clustering/
Colladon, A. F. (2018). The semantic brand score. Journal of Business Research, 88, 150-160.
Silge, J., & Robinson, D. (2017). Text Mining with R: A Tidy Approach. O’Reilly Media. https://www.tidytextmining.com/
Robinson, D. (2021). widyr: Widen, process, then re-tidy data [R package documentation]. https://CRAN.R-project.org/package=widyr
Csardi, G., & Nepusz, T. (2006). The igraph software package for complex network research. InterJournal, Complex Systems, 1695. https://igraph.org
Pedersen, T. L. (2024). ggraph: An implementation of grammar of graphics for graphs and networks [R package documentation]. https://CRAN.R-project.org/package=ggraph