library(tidyverse)
library(tidytext)
library(widyr)
library(igraph)
library(ggraph)
library(Matrix)
library(atrrr)
auth(
user = "chinstigator.bsky.social",
password = "3fvw-qplv-g53i-km3d"
)
## A token already exists on disk. Do you want to overwrite it? (yes/No/cancel)
This project applies word co-occurrence analysis to Bluesky posts about the Nintendo Switch 2. A simple word count can show which words appear most often, but co-occurrence analysis gives more detail because it shows which words appear together in the same post. This helps explain how topics are connected in the conversation.
For example, if price often appears with expensive, that may suggest users are concerned about cost. If preorder appears with stock, sold, or available, that may suggest concerns about availability. If Mario, Zelda, or launch appear together, that may suggest excitement about Nintendo first-party games.
This matters from a business analytics perspective because companies can use social media text to understand what customers are paying attention to. For Nintendo, retailers, accessory companies, and game publishers, these patterns could help identify customer expectations, concerns, and marketing opportunities around the Nintendo Switch 2.
Word co-occurrence analysis looks at which words tend to appear together within the same unit of text. In this project, each Bluesky post is treated as one document. If two words appear in the same post, they are counted as a co-occurring pair.
The basic workflow is:
This analysis uses Bluesky posts collected with the search term “Nintendo Switch 2”. The goal is to identify which words appear together most often in public discussion around the console. I am especially interested in whether the conversation clusters around price, preorders, games, launch availability, hardware, accessories, or comparisons to other gaming products.
posts <- search_post("Nintendo Switch 2", limit = 100)
posts_df <- posts %>%
as_tibble() %>%
filter(!is.na(text)) %>%
distinct(text, .keep_all = TRUE)
cat("Total unique posts collected:", nrow(posts_df), "\n")
## Total unique posts collected: 99
posts_df %>%
select(text) %>%
head(5)
## # A tibble: 5 × 1
## text
## <chr>
## 1 "Nintendo Switch 2 Final Fantasy VII Rebirth #Mediamarkt\n\n👍🏻👍🏻 Precio Ahora: …
## 2 "🔴 MARIO TENNIS FEVER para Nintendo Switch 2...\n\n⭐️ ¡55 €!\n\ntinyurl.com/mn…
## 3 "For the time being, I'm going to play some of my Nintendo Switch and Nintend…
## 4 "🔴 YOSHI Y EL LIBRO MISTERIOSO para Nintendo Switch 2 (y viene en el cartucho…
## 5 "BREAKING NEWS:\n\nFormer PlayStation spokesperson and gamer, Maggie, is now …
Each post is treated as its own document. This means the analysis looks for words that appear together inside the same Bluesky post.
Real social media text contains URLs, usernames, hashtags, punctuation, numbers, emojis, and repeated search terms. These need to be cleaned so the final network focuses on meaningful words.
I removed common stop words and custom stop words such as nintendo, switch, switch2, https, www, and com. The words nintendo and switch are removed because they are part of the search topic itself. If they stayed in the dataset, they would dominate the graph and connect to almost everything.
data(stop_words)
custom_stop_words <- bind_rows(
stop_words,
tibble(
word = c(
"nintendo", "switch", "switch2", "2",
"https", "http", "www", "com", "amp", "rt",
"bsky", "social",
"im", "ive", "dont", "didnt", "isnt", "cant", "wont",
"people", "person", "thing", "things", "time", "day", "today",
"just", "really", "like", "know", "think", "make", "made", "got",
"get", "getting", "gonna", "wanna", "yeah", "lol"
),
lexicon = "custom"
)
)
switch_text <- posts_df %>%
mutate(
post_id = row_number(),
text = str_to_lower(text),
text = str_replace_all(text, "https?://\\S+", " "),
text = str_replace_all(text, "@[\\w\\.]+", " "),
text = str_replace_all(text, "#", " "),
text = str_replace_all(text, "[^[:alnum:][:space:]]", " ")
)
switch_words <- switch_text %>%
unnest_tokens(word, text, token = "words") %>%
mutate(
word = case_when(
word %in% c("preorder", "preorders", "preordered", "preordering") ~ "preorder",
word %in% c("games", "gaming", "gameplay", "gamer", "gamers") ~ "game",
word %in% c("prices", "priced", "pricing") ~ "price",
word %in% c("launches", "launched", "launching") ~ "launch",
word %in% c("zelda", "totk", "botw") ~ "zelda",
word %in% c("mario", "kart") ~ "mario",
word %in% c("joycon", "joycons", "joy", "con") ~ "joycon",
word %in% c("controllers", "controller") ~ "controller",
word %in% c("consoles", "console") ~ "console",
word %in% c("stocks", "stock", "restock", "restocks") ~ "stock",
word %in% c("expensive", "cost", "costs") ~ "expensive",
TRUE ~ word
)
) %>%
filter(!str_detect(word, "^[0-9]+$")) %>%
filter(str_detect(word, "^[a-z]+$")) %>%
anti_join(custom_stop_words, by = "word")
switch_words %>%
count(word, sort = TRUE) %>%
head(20)
## # A tibble: 20 × 2
## word n
## <chr> <int>
## 1 game 38
## 2 de 23
## 3 amazon 18
## 4 joycon 15
## 5 final 13
## 6 fantasy 12
## 7 para 12
## 8 em 11
## 9 link 11
## 10 cupom 9
## 11 edition 9
## 12 eshop 9
## 13 impossible 9
## 14 ad 8
## 15 el 8
## 16 en 8
## 17 frete 8
## 18 news 8
## 19 tinyurl 8
## 20 jogobara 7
The graph below keeps the broader cleaned Bluesky vocabulary before applying the Nintendo-related word filter. I included this version as a comparison because it shows why extra filtering is helpful. Without the Nintendo-related filter, the network can become very crowded and may include random words, usernames, store names, non-English words, or web artifacts.
This graph is still useful because it shows the raw conversation pattern, but it is harder to interpret for a business audience. The filtered graphs later in the report are cleaner and more focused on Nintendo Switch 2 topics.
switch_pairs_unfiltered <- switch_words %>%
pairwise_count(word, post_id, sort = TRUE, upper = FALSE)
switch_pairs_unfiltered %>%
head(20)
## # A tibble: 20 × 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 de amazon 9
## 2 final fantasy 7
## 3 amazon em 7
## 4 de joycon 7
## 5 news game 6
## 6 amazon cupom 6
## 7 final impossible 6
## 8 fantasy impossible 6
## 9 de en 5
## 10 de em 5
## 11 amazon frete 5
## 12 em frete 5
## 13 amazon joycon 5
## 14 final square 5
## 15 fantasy square 5
## 16 impossible square 5
## 17 final enix 5
## 18 fantasy enix 5
## 19 impossible enix 5
## 20 square enix 5
set.seed(123)
switch_pairs_unfiltered %>%
filter(n >= 2) %>%
graph_from_data_frame() %>%
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = n), show.legend = FALSE) +
geom_node_point(size = 3) +
geom_node_text(aes(label = name), repel = TRUE, size = 3) +
theme_void() +
labs(
title = "Unfiltered Word Co-occurrence Network: Nintendo Switch 2 on Bluesky",
subtitle = "This broader version shows why additional Nintendo-related filtering is useful"
)
The pairwise_count() function counts how often two words
appear in the same post. Each pair represents a connection between two
words.
switch_pairs <- switch_words_filtered %>%
pairwise_count(word, post_id, sort = TRUE, upper = FALSE)
switch_pairs %>%
head(20)
## # A tibble: 20 × 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 amazon joycon 5
## 2 game console 4
## 3 target link 3
## 4 game amazon 3
## 5 game physical 3
## 6 game launch 2
## 7 link amazon 2
## 8 amazon zelda 2
## 9 link console 2
## 10 amazon console 2
## 11 game link 1
## 12 game exclusive 1
## 13 mario amazon 1
## 14 link zelda 1
## 15 amazon digital 1
## 16 zelda digital 1
## 17 game graphics 1
## 18 launch graphics 1
## 19 game donkey 1
## 20 amazon donkey 1
A co-occurrence matrix shows how often the most common words appear together. To keep the matrix readable, I limited it to the top 10 most frequent Nintendo-related words.
top_words <- switch_words_filtered %>%
count(word, sort = TRUE) %>%
slice_max(n, n = 10) %>%
pull(word)
switch_matrix <- switch_pairs %>%
filter(item1 %in% top_words, item2 %in% top_words) %>%
bind_rows(
switch_pairs %>%
rename(item1 = item2, item2 = item1) %>%
filter(item1 %in% top_words, item2 %in% top_words)
) %>%
cast_sparse(item1, item2, n) %>%
as.matrix()
switch_matrix
## joycon console link amazon physical launch zelda pokemon controller
## amazon 5 2 2 0 0 0 2 1 1
## game 0 4 1 3 3 2 0 1 0
## target 0 0 3 0 0 0 0 0 0
## link 1 2 0 2 0 0 1 0 1
## mario 1 0 0 1 0 0 0 0 0
## console 1 0 2 2 0 0 0 0 0
## pokemon 0 0 0 1 0 0 0 0 1
## joycon 0 1 1 5 0 0 0 0 0
## physical 0 0 0 0 0 0 0 0 0
## launch 0 0 0 0 0 0 0 0 0
## zelda 0 0 1 2 0 0 0 0 0
## controller 0 0 1 1 0 0 0 1 0
## game target mario
## amazon 3 0 1
## game 0 0 0
## target 0 0 0
## link 1 3 0
## mario 0 0 0
## console 4 0 0
## pokemon 1 0 0
## joycon 0 0 1
## physical 3 0 0
## launch 2 0 0
## zelda 0 0 0
## controller 0 0 0
The graph below shows word pairs that appeared together at least twice. The thicker the edge, the more often the two words appeared together in the same Bluesky posts.
set.seed(123)
switch_pairs %>%
filter(n >= 2) %>%
graph_from_data_frame() %>%
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = n, edge_width = n), show.legend = FALSE) +
geom_node_point(size = 4) +
geom_node_text(aes(label = name), repel = TRUE, size = 3.5) +
theme_void() +
labs(
title = "Word Co-occurrence Network: Nintendo Switch 2 on Bluesky",
subtitle = "Filtered to Nintendo-related words appearing together at least twice"
)
A higher threshold makes the graph less crowded and highlights only the strongest relationships. This graph only shows word pairs that appeared together more than five times.
set.seed(123)
switch_pairs %>%
filter(n > 5) %>%
graph_from_data_frame() %>%
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = n, edge_width = n), show.legend = FALSE) +
geom_node_point(size = 4) +
geom_node_text(aes(label = name), repel = TRUE, size = 3.5) +
theme_void() +
labs(
title = "Strongest Word Co-occurrences: Nintendo Switch 2 on Bluesky",
subtitle = "Filtered to word pairs appearing together more than five times"
)
# Interpretation No network graph was produced using the threshold of n
> 5 because none of the Nintendo-related word pairs appeared together
more than five times in the sample of 100 Bluesky posts.
This suggests that the conversation was spread across a variety of Nintendo Switch 2 topics rather than being dominated by a few highly repeated word pairs. Because the dataset is relatively small, increasing the threshold removed all remaining connections from the network.
The graph created using the lower threshold (n ≥ 2) provides a more meaningful visualization because it captures the strongest relationships that actually exist within this sample. If a much larger collection of Bluesky posts were analyzed, stronger and more frequent co-occurrence patterns would likely appear, making a higher threshold more useful.
The most common keywords are the Nintendo-related words that appeared most often in the Bluesky posts. These may include words related to games, launch timing, price, preorders, availability, hardware, and Nintendo franchises.
The exact results depend on the 100 posts collected at the time the code is run. This is important because social media data changes over time. If the code is run on a different day, the most common words may change.
Changing the filter threshold affects how crowded the network graph looks. A low threshold includes more word pairs, including weaker relationships that may have only appeared a few times. This can make the graph dense and harder to interpret.
A higher threshold removes less frequent word pairs and keeps only the strongest repeated patterns. For this project, the higher threshold is usually better for communication because it makes the Nintendo Switch 2 discussion easier to understand. A business audience would likely prefer the cleaner graph because the main relationships are easier to see.
The network graph shows which Nintendo Switch 2 topics are discussed together most often on Bluesky. Instead of only showing which words are common, it shows how ideas are connected.
For example, the graph can show whether users connect the console with price, preorders, stock, launch games, hardware features, or Nintendo franchises. These connections can reveal what people care about most in the conversation.
Some word pairs may be surprising if they reveal unexpected customer concerns. For example, if price appears often with expensive, that suggests affordability is an important issue. If preorder appears with stock or sold, then availability may be a major concern. If mario or zelda appears with launch, that suggests users are connecting the console to expected first-party games.
These relationships are useful because they show not just what words are common, but what ideas users connect together.
Token normalization is important because social media users often refer to the same idea in different ways. For example, preorder, preorders, preordered, and preordering all refer to the same concept. Similarly, games, gaming, and gameplay are closely related.
If these are left separate, the co-occurrence signal gets split across multiple nodes. Normalizing them makes the final graph cleaner and more accurate because related versions of the same term are grouped together.
This analysis has several limitations. First, only 100 Bluesky posts were collected, which is a small sample. Second, the sample is query-seeded, meaning it only includes posts that matched the search phrase “Nintendo Switch 2”. Third, Bluesky users are not necessarily representative of all Nintendo customers or all social media users.
Another limitation is that the Nintendo-related word filter focuses the graph but may also remove some useful words that were not included in the list. Because of this, the findings should be described as exploratory rather than definitive.
Businesses can use this type of analysis to identify popular topics, customer interests, and emerging concerns. Nintendo could monitor whether people are focused on price, games, launch timing, hardware features, or availability. Retailers could use preorder and stock discussion to understand demand. Accessory companies could look for discussion around controllers, docks, storage, and other hardware needs.
This type of analysis can also help businesses compare public reaction before and after major events, such as a Nintendo Direct, launch date announcement, preorder opening, or major game reveal.
Word co-occurrence analysis helps turn messy Bluesky posts into clearer business insight. For Nintendo Switch 2 discussion, this method shows not only which words are common, but also how ideas are connected.
By filtering for Nintendo-related terms, the final graph becomes more focused and easier to interpret. The results can help show whether Bluesky users are mainly discussing games, price, preorders, availability, hardware, or Nintendo franchises. While the analysis is limited by sample size and platform, it still provides a useful example of how text analysis can be applied to real social media data.
Silge, J., & Robinson, D. (2017). Text Mining with R: A Tidy Approach. O’Reilly Media. https://www.tidytextmining.com/
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
Robinson, D. (2021). widyr: Widen, process, then re-tidy data [R package documentation]. https://CRAN.R-project.org/package=widyr