Write text and code here.

imdb_tidy <- imdb_data %>% 
  unnest_tokens(input = review, output = word, drop = F) %>%
  filter(!word %in% c("br")) %>% 
  anti_join(stop_words)
## Joining with `by = join_by(word)`
sentiment_words <- imdb_tidy %>% 
  inner_join(get_sentiments("bing"), by = "word") 
## Warning in inner_join(., get_sentiments("bing"), by = "word"): Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 1212127 of `x` matches multiple rows in `y`.
## ℹ Row 5781 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
##   "many-to-many"` to silence this warning.
sentiment_counts <- sentiment_words %>%
  count(sentiment, word, sort = TRUE) %>%
  group_by(sentiment) %>%
  slice_max(n, n = 15) %>%
  ungroup() %>%
  mutate(word = reorder_within(word, n, sentiment))
sentiment_counts
## # A tibble: 30 × 3
##    sentiment word                  n
##    <chr>     <fct>             <int>
##  1 negative  bad___negative    18448
##  2 negative  plot___negative   12940
##  3 negative  funny___negative   8737
##  4 negative  worst___negative   5330
##  5 negative  hard___negative    5269
##  6 negative  death___negative   3921
##  7 negative  poor___negative    3837
##  8 negative  dead___negative    3685
##  9 negative  boring___negative  3626
## 10 negative  wrong___negative   3576
## # ℹ 20 more rows
library(wordcloud)
## Loading required package: RColorBrewer
word_freq <- sentiment_words %>%
  count(word, sentiment, sort = TRUE)
positive_words <- word_freq %>% filter(sentiment == "positive")
negative_words <- word_freq %>% filter(sentiment == "negative")

par(mfrow = c(1, 2))
wordcloud(words = positive_words$word, freq = positive_words$n, 
          max.words = 50, colors = brewer.pal(8, "Greens"), scale = c(2, 0.8))
wordcloud(words = negative_words$word, freq = negative_words$n, 
          max.words = 50, colors = brewer.pal(8, "Reds"), scale = c(2, 0.8))

ggplot(sentiment_counts, aes(x = word, y = n, fill = sentiment)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~ sentiment, scales = "free_y") +
  coord_flip() +
  scale_x_reordered() +
  labs(title = "Top 10 Words in Positive and Negative Reviews",
       x = "Words", y = "Frequency")

imdb_3000 <- imdb_data %>%
  slice(1:3000) %>%
  mutate(id = row_number()) %>%
  unnest_tokens(word, review) %>%
  filter(!word %in% c("br")) %>%
  anti_join(stop_words, by = "word")

pair_3000 <- imdb_3000 %>%
  pairwise_count(item = word, feature = id, sort = TRUE, upper = FALSE)
pair_3000
## # A tibble: 8,424,079 × 3
##    item1 item2      n
##    <chr> <chr>  <dbl>
##  1 movie film     948
##  2 time  movie    658
##  3 time  film     592
##  4 film  story    578
##  5 movie story    565
##  6 movie movies   544
##  7 movie bad      503
##  8 film  films    481
##  9 movie watch    457
## 10 movie people   449
## # ℹ 8,424,069 more rows
library(tidygraph)
## 
## Attaching package: 'tidygraph'
## The following object is masked from 'package:stats':
## 
##     filter
graph_review <- pair_3000 %>%
  filter(n >= 300) %>%
  as_tbl_graph()

library(ggraph)
set.seed(1234)
ggraph(graph_review, layout = "fr") +       
  geom_edge_link(color = "gray50", alpha = 0.5) +             
  geom_node_point(color = "lightcoral", size = 5) +              
  geom_node_text(aes(label = name), repel = T, size = 5) +  
  theme_graph()

imdb_3000 <- imdb_data %>%
  slice(1:3000) %>%
  mutate(id = row_number())

imdb_bigrams <- imdb_3000 %>%
  unnest_tokens(bigram, review, token = "ngrams", n = 2) %>% 
  filter(!is.na(bigram))

bigrams_separated <- imdb_bigrams %>%
  separate(bigram, into = c("word1", "word2"), sep = " ") %>%
  filter(!word1 %in% c("br", "oz", stop_words$word),
         !word2 %in% c("br", "oz", stop_words$word),
         str_detect(word1, "^[a-zA-Z]+$"),   
         str_detect(word2, "^[a-zA-Z]+$"))

bigrams_united <- bigrams_separated %>%
  unite(bigram, word1, word2, sep = " ")

bigram_tf_idf <- bigrams_united %>%
  count(review_sentiment, bigram) %>%
  bind_tf_idf(bigram, review_sentiment, n) %>%
  arrange(desc(tf_idf))
bigram_tf_idf
## # A tibble: 70,182 × 6
##    review_sentiment bigram              n       tf   idf   tf_idf
##    <chr>            <chr>           <int>    <dbl> <dbl>    <dbl>
##  1 negative         worst movies       21 0.000528 0.693 0.000366
##  2 negative         worst film         20 0.000503 0.693 0.000348
##  3 negative         complete waste     12 0.000302 0.693 0.000209
##  4 negative         god awful          12 0.000302 0.693 0.000209
##  5 negative         worst films        12 0.000302 0.693 0.000209
##  6 positive         wonderful film     13 0.000300 0.693 0.000208
##  7 negative         fell asleep        11 0.000277 0.693 0.000192
##  8 negative         terrible movie     11 0.000277 0.693 0.000192
##  9 positive         excellent movie    10 0.000230 0.693 0.000160
## 10 positive         john ford          10 0.000230 0.693 0.000160
## # ℹ 70,172 more rows
bigram_tf_idf %>%
  group_by(review_sentiment) %>%
  slice_max(tf_idf, n = 10) %>%
  ungroup() %>%
  mutate(bigram = reorder_within(bigram, tf_idf, review_sentiment)) %>%
  ggplot(aes(tf_idf, bigram, fill = review_sentiment)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~review_sentiment, scales = "free") +
  scale_y_reordered() +
  labs(title = "Top 10 TF-IDF Bigrams by Sentiment",
       x = "TF-IDF",
       y = "Bigram")

library(igraph)
## 
## Attaching package: 'igraph'
## The following object is masked from 'package:tidygraph':
## 
##     groups
## The following object is masked from 'package:text2vec':
## 
##     normalize
## The following objects are masked from 'package:lubridate':
## 
##     %--%, union
## The following objects are masked from 'package:dplyr':
## 
##     as_data_frame, groups, union
## The following objects are masked from 'package:purrr':
## 
##     compose, simplify
## The following object is masked from 'package:tidyr':
## 
##     crossing
## The following object is masked from 'package:tibble':
## 
##     as_data_frame
## The following objects are masked from 'package:stats':
## 
##     decompose, spectrum
## The following object is masked from 'package:base':
## 
##     union
positive_bigram_counts <- bigrams_separated %>% 
  filter(review_sentiment == "positive") %>%
  count(word1, word2, sort = TRUE)

positive_bigram_graph <- positive_bigram_counts %>%
  filter(n > 10) %>%  
  graph_from_data_frame()

set.seed(1234)
ggraph(positive_bigram_graph, layout = "fr") +
  geom_edge_link(edge_alpha = 0.4) +
  geom_node_point(color = "darkgreen", size = 4) +
  geom_node_text(aes(label = name), repel = TRUE, size = 4) +
  labs(title = "Frequent Bigrams in Positive Reviews") +
  theme_void()

library(igraph)

negative_bigram_counts <- bigrams_separated %>% 
  filter(review_sentiment == "negative") %>%
  count(word1, word2, sort = TRUE)

negative_bigram_graph <- negative_bigram_counts %>%
  filter(n > 10) %>%  
  graph_from_data_frame()

set.seed(1234)
ggraph(negative_bigram_graph, layout = "fr") +
  geom_edge_link(edge_alpha = 0.4) +
  geom_node_point(color = "darkred", size = 4) +
  geom_node_text(aes(label = name), repel = TRUE, size = 4) +
  labs(title = "Frequent Bigrams in Negative Reviews") +
  theme_void()

Executive summary

What is (are) your main question(s)? What is your story? What does the final graphic show?

The main research question of this project is: What different keywords and linguistic patterns do audiences use in positive and negative movie reviews, and how do these differences reflect their emotional tendencies and evaluation focus?

By conducting text analysis on the IMDb movie review dataset, focusing on reviews labeled as either positive or negative, the project explores the distinct patterns in word frequency, bigram combinations, and word co-occurrence network structures between the two sentiment categories.

The final visualizations reveal several key findings: First, positive reviews frequently contain emotional words expressing affection, value, and personal feeling, such as love, fun, and worth, while negative reviews often focus on words criticizing plot, acting, or structural issues, such as bad, plot, and boring. Second, the TF-IDF extracted bigrams reveal representative language patterns for each sentiment category, such as “worst movies” and “wonderful film”, highlighting emotional intensity and evaluative stance. Finally, the network graphs show that “movie” acts as a semantic core in both types of reviews, connecting with key dimensions like acting, story, and characters, which reflect common points of audience concern.

Data background

Explain where the data came from, what agency or company made it, how it is structured, what it shows, etc.

The dataset used in this study is the IMDb Dataset of 50K Movie Reviews, published by Kaggle user @lakshmi25npathi. It contains 50,000 English movie review texts, evenly split into two sentiment categories: 25,000 positive and 25,000 negative reviews. All reviews were originally collected from the IMDb website and are pre-labeled for sentiment analysis tasks.

Data loading, cleaning and preprocessing

Describe and show how you cleaned and reshaped the data

began by importing the IMDb movie review dataset using read_csv(), and renamed the sentiment column to review_sentiment for better clarity. The review text was then tokenized into individual words using unnest_tokens(), which transforms the text data into a tidy format where each row represents a single word from a review. To clean the dataset, we removed HTML tags such as “br” and applied anti_join() with a standard stop_words dictionary to eliminate common English stop words that do not contribute meaningful information. Next, we extracted the sentiment words by performing an inner join with the Bing sentiment lexicon, retaining only words with known positive or negative sentiment labels. This cleaned and reshaped dataset provides a solid foundation for further text mining tasks such as word frequency analysis, TF-IDF scoring, and network-based co-occurrence analysis.

# Load required libraries
library(tidyverse)
library(tidytext)
library(readr)
library(dplyr)
library(text2vec)
library(stringr)
library(widyr)

# Load the IMDb dataset and rename sentiment column
imdb_data <- read_csv("IMDB Dataset.csv") %>%
  rename(review_sentiment = sentiment)
## Rows: 50000 Columns: 2
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): review, sentiment
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Tokenize text into words, remove HTML tags and stop words
imdb_tidy <- imdb_data %>% 
  unnest_tokens(input = review, output = word, drop = F) %>%
  filter(!word %in% c("br")) %>% 
  anti_join(stop_words)
## Joining with `by = join_by(word)`
# Extract sentiment-related words using Bing lexicon
sentiment_words <- imdb_tidy %>% 
  inner_join(get_sentiments("bing"), by = "word")
## Warning in inner_join(., get_sentiments("bing"), by = "word"): Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 1212127 of `x` matches multiple rows in `y`.
## ℹ Row 5781 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
##   "many-to-many"` to silence this warning.

Text data analysis

The text analysis in this project was conducted on three levels: 1. Unigram Frequency Analysis and Sentiment Visualization High-frequency keywords in positive and negative reviews were identified through frequency statistics and visualized with word clouds. A bar chart of the top 20 most frequent words further analyzed the core vocabulary, revealing linguistic differences between positive and negative sentiments. 2. Word Co-occurrence Network Analysis A word co-occurrence matrix was constructed and visualized as a network graph to identify words frequently mentioned together. This analysis revealed the core semantic structure of the movie reviews. 3. TF-IDF Bigram Analysis and Network Visualization The TF-IDF algorithm was used to extract representative bigrams from both positive and negative reviews. These bigrams were then visualized using network graphs to show structural relationships between phrase pairs. This layer of analysis further highlights distinctive linguistic patterns in positive versus negative reviews.

Together, these three layers of analysis complement each other by offering both an intuitive overview of word distribution and a deeper understanding of how language reflects emotion in movie reviews.

Individual analysis and figures

Anaysis and Figure 1

Describe and show how you created the first figure. Why did you choose this figure type?

The word cloud is an intuitive way to visualize word frequency distribution. It highlights the most frequently appearing words in the text at a glance and is particularly effective for showcasing high-frequency emotional terms in positive and negative reviews, thereby revealing differences in word usage across sentiment categories.

I processed the movie review data using the tidytext package and applied the Bing sentiment lexicon to extract words and their frequencies separately from the positive and negative reviews. Then, using the wordcloud package, I created two word clouds to visualize the frequency of terms by sentiment.

The size of each word reflects its frequency in the dataset, making it visually easy to identify the most prominent terms. Green represents positive sentiment, and red indicates negative sentiment, enhancing the visual polarity of emotions. Words like love, fun, and pretty appear frequently in positive reviews, while bad, plot, and funny are common in negative ones.

It’s worth noting that some words may be categorized counterintuitively in the sentiment lexicon. For instance, the word “funny” is classified as negative. In the bar chart of the top 10 high-frequency words in negative reviews, “funny” appears frequently. This suggests that in many negative reviews, “funny” is used sarcastically or ironically, as a form of criticism rather than praise. This usage helps explain why the Bing sentiment lexicon categorizes “funny” as a negative term in this dataset.

Anaysis and Figure 2

Building on the keyword “movie”, a co-occurrence network graph was constructed to explore the key themes closely associated with the concept of film within the review texts.

The network visualization reveals that “movie” is strongly linked with several core keywords such as “plot”, “acting”, “characters”, “scenes”, “life”, and “watch”, which largely represent the main aspects audiences focus on when evaluating a film. Certain clusters also demonstrate semantic groupings—for instance, “story-time-plot” reflects the narrative structure, while “acting-characters” highlights the connection between performance and character development.

This figure employs a radial layout, placing “movie” at the center, with edge thickness and node size indicating co-occurrence frequency. This effectively illustrates the multidimensional aspects of how viewers perceive and describe movies.

Compared to traditional frequency plots, the network graph offers a more relational and structured perspective, uncovering the latent thematic organization embedded in the language of the reviews.

Anaysis and Figure 3

In showing the figures that you created, describe why you designed it the way you did. Why did you choose those colors, fonts, and other design elements? Does it convey truth?

To understand which word pairs define sentiment polarity, Figure 3 presents a dual analysis using TF-IDF-ranked bigrams and network diagrams of frequent bigrams.

In the TF-IDF chart, the top bigrams in negative reviews include “worst movies,” “worst film,” “god awful,” and “complete waste,” reflecting harsh, evaluative language. On the other hand, positive reviews favor expressive bigrams such as “wonderful film,” “sean connery,” “love key,” and “excellent movie.” These terms either describe quality or highlight memorable elements (e.g., actors, emotions).

The frequent bigram networks visualize recurring word pairings in each sentiment. In the positive network, phrases like “worth watching,” “true story,” “short film,” “independent movie,” and “emotional moments” show a focus on recommendation, emotional impact, and narrative. Meanwhile, the negative network shows clusters like “terrible movie,” “fell asleep,” “plot holes,” and “slow pace,” emphasizing disappointment with pacing, coherence, or production.

The combination of bar plots and networks reveals not just frequent collocations, but also how bigram patterns express sentiment themes differently.

Colors were chosen to reflect emotional polarity (green for positive, red for negative), and the force-directed layout enhances visual readability. Overall, these visualizations effectively illustrate how multi-word expressions carry sentiment intensity and evaluative stance, extending beyond individual words.

You can also include images like this: