Text Analysis of Positive and Negative Reviews
Text Analysis of Positive and Negative Reviews
2025-11-23
————————-
0. Install & load required packages
packages <- c("tm", "textstem", "stringr", "tidyverse", "tidytext",
"topicmodels", "slam", "MASS", "ggplot2", "ggforce",
"wordcloud", "RColorBrewer", "igraph", "ggraph",
"text2vec", "Rtsne", "[Link]")
[Link](setdiff(packages, rownames([Link]())))
library(tm)
1
## Warning: ����� 'tidyverse' ��� ������ ��� R ������ 4.4.3
## Warning: ����� 'ggplot2' ��� ������ ��� R ������ 4.4.3
## Warning: ����� 'readr' ��� ������ ��� R ������ 4.4.3
## Warning: ����� 'forcats' ��� ������ ��� R ������ 4.4.3
## Warning: ����� 'lubridate' ��� ������ ��� R ������ 4.4.3
## -- Attaching core tidyverse packages ------------------------ tidyverse 2.0.0 --
## v dplyr 1.1.4 v purrr 1.0.4
## v forcats 1.0.1 v readr 2.1.5
## v ggplot2 4.0.0 v tibble 3.2.1
## v lubridate 1.9.4 v tidyr 1.3.1
## -- Conflicts ------------------------------------------ tidyverse_conflicts() --
## x ggplot2::annotate() masks NLP::annotate()
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
## x readr::tokenize() masks koRpus::tokenize()
## i Use the conflicted package (<[Link] to force all conflicts to become errors
library(tidytext)
##
## ����������� �����: 'MASS'
##
## ��������� ������ ����� �� 'package:dplyr':
##
## select
library(ggplot2)
library(ggforce)
2
## %--%, union
##
## ��������� ������� ������ �� 'package:dplyr':
##
## as_data_frame, groups, union
##
## ��������� ������� ������ �� 'package:purrr':
##
## compose, simplify
##
## ��������� ������ ����� �� 'package:tidyr':
##
## crossing
##
## ��������� ������ ����� �� 'package:tibble':
##
## as_data_frame
##
## ��������� ������� ������ �� 'package:stats':
##
## decompose, spectrum
##
## ��������� ������ ����� �� 'package:base':
##
## union
library(ggraph)
3
## rollup
##
## ��������� ������� ������ �� 'package:lubridate':
##
## hour, isoweek, mday, minute, month, quarter, second, wday, week,
## yday, year
##
## ��������� ������� ������ �� 'package:dplyr':
##
## between, first, last
##
## ��������� ������ ����� �� 'package:purrr':
##
## transpose
============================================
1. Dataset Selection
============================================
file_path <- "[Link]" # Update with your file path
text_data <- readLines(file_path, n = 4000, warn = FALSE)
# Build dataframe
data <- [Link](label = labels, text = text_only, stringsAsFactors = FALSE)
data <- data %>% filter()
#We load a labeled text dataset containing reviews and extract the class labels (Positive or Negative). The
dataset is structured so that each line contains a label followed by text. Any rows with missing or invalid
labels are removed to ensure clean data for analysis. This step sets the foundation for all subsequent text
processing and modeling.
============================================
2. Text Preprocessing
============================================
clean_text <- data$text %>%
tolower() %>%
str_replace_all("[0-9]+", " ") %>%
removePunctuation() %>%
stripWhitespace()
4
tokens <- strsplit(clean_text, "\\s+")
stopwords_list <- stopwords("en")
tokens <- lapply(tokens, function(x) x[!x %in% stopwords_list])
tokens <- lapply(tokens, lemmatize_words)
data$text <- sapply(tokens, paste, collapse = " ")
#Text is cleaned by converting to lowercase, removing numbers, punctuation, and extra whitespace. The
text is then tokenized into individual words, stopwords are removed, and words are lemmatized to reduce
them to their base forms. This normalization improves the quality of downstream analyses like frequency
counting, topic modeling, and embeddings.
============================================
3. Word Frequency Analysis
============================================
get_top_words <- function(class_label, n = 10) {
words <- unlist(strsplit(data$text[data$label == class_label], "\\s+"))
words <- words[words != ""]
if(length(words) == 0) return(NULL)
head(sort(table(words), decreasing = TRUE), n)
}
# Bar plot
ggplot(freq_plot_data, aes(x = reorder(word, frequency), y = frequency, fill = class)) +
geom_bar(stat = "identity", position = "dodge") +
coord_flip() +
labs(title = "Top 10 Words — Positive vs Negative Reviews", x = "Words", y = "Frequency") +
theme_minimal(base_size = 14) +
scale_fill_manual(values = c("Positive" = "#1b9e77", "Negative" = "#d95f02"))
5
Top 10 Words — Positive vs Negative Reviews
book
good
great
one
movie
class
Words
like
Negative
get
Positive
read
love
do
buy
just
============================================
4. Word Clouds (no new windows)
============================================
make_wordcloud <- function(class_label, max_words = 100) {
words <- unlist(strsplit(data$text[data$label == class_label], "\\s+"))
words <- words[words != ""]
freq <- sort(table(words), decreasing = TRUE)
6
Word Cloud — Positive Reviews
dvd cant wonderful
series
old review
long
excellent
recommend
price
film really find write two
play much think song now
fan
never
may
get onetime say anyone
year
also
seem
must
people go ever
worth
read great
little
live
take
lot
im ive cd want
even can
good
feel
story workwatch
enjoy
will know
back
see easy
thinguse
make man
way do
book likebuy
just need
favorite
new
try
life
product
hear classic
fun movie love
give album
firstsound
still world
end music show
character look many come day
listen put every
start interest keep
make_wordcloud("Negative")
title("Word Cloud — Negative Reviews")
7
Word Cloud — Negative Reviews
long plot poor
keep song
nothing part music
page need
good
product
sound
back character
didnt really knowshow
day
big new write film cant
dvd read even another
time
bore will
want
work may
feel cd
ever
find
one
order
still make
play
come go look
book
just
version
waste
story
thing
start take use old
review star
watch buy movie do never
try end
little
get love
see
say
way
like bad
two also
year people
give many
think
great
interest much
purchase
lot
now first can im disappoint
quality money
author album
seem doesnt
however recommend
something
wordcloud(
8
names(freq), freq,
[Link] = max_words,
colors = [Link](8, "Dark2"),
scale = c(4, 0.7),
[Link] = FALSE
)
make_commonality_wordcloud()
money
price
way
still much want go find
even didntpage fun
time
review fan
amazon
live
song great movie film quality
feel
big
first sound
set make like one think old small
really
far im
ever hard
part
buy
love
kid
real
life
book just littleback
people
dvd
last hear
tell try cd
goodget must
use
start music
plot give new show
ive
however
listen
series take will knowneed
work read story also put
video never
seem write can child lot end
man many say see look thing nothing
enough
expect
two come
author recommendplay worth problem
though
point doesnt interest purchase
world something anyone excellent
9
hope novel kid anyone
favorite help
Commonality disappoint
Word
version Cloud wonderfulWords
— Shared
interest quality though
without
purchase never watchpeople something
series
let day dvd know year im need right
big two
nothing
much think say newkeep
expect
must come
readreallything
bite
didnt
plot fan want will
album use
good
doesnt
bad feel hard
worth
far
review
first even
movie
last
take find may
set
live
many way order
price
book love
another
song
child
cd
still make like
one get classic
buy listen
old part
tell
long
cant now video
back look story music world
point manproduct film ive enjoy excellent
different little write
since
game small character end show sound
page easy
however money star every bore
understand enoughauthor hear problem almost
original
leave
#Word clouds visually represent the frequency of words within each class, with larger words appearing more
often. Separate clouds are generated for Positive and Negative reviews, and a commonality cloud shows words
shared between the two classes. This provides a quick, intuitive understanding of prominent terms in the
dataset. # ============================================================
# 5. Word Networks # =========================================================
library(igraph)
library(ggraph)
library(tidytext)
library(dplyr)
# Generate bigrams
bigrams <- class_data %>% unnest_tokens(bigram, text, token = "ngrams", n = 2)
bigram_sep <- bigrams %>% separate(bigram, into = c("word1","word2"), sep = " ")
# Count co-occurrences
10
bigram_counts <- bigram_filt %>% count(word1, word2, sort = TRUE) %>% filter(n >= min_freq)
if(nrow(bigram_counts) == 0) return(NULL)
return(g)
}
# Generate plots
pos_net <- create_cooc_network("Positive", min_freq = 15)
neg_net <- create_cooc_network("Negative", min_freq = 15)
11
Word Co−occurrence Network — Positive
special
cd
buy
funny effect
2
watch movie
Frequency
spin dry 25
0 love 50
y
75
enjoy
highly song 100
if() print(neg_net)
12
Word Co−occurrence Network — Negative
money
watch buy
book spend
−4 movie bad read
review
write
−2.5 0.0 2.5
x
#Co-occurrence networks are created to show relationships between words that frequently appear together.
Nodes represent words and edges indicate co-occurrence frequency, revealing clusters of related terms. This
helps identify patterns and associations in the vocabulary beyond individual word counts.
============================================
6. Sentiment Analysis
============================================
sentiments <- get_sentiments("nrc")
sentiment_scores <- data %>%
unnest_tokens(word, text) %>%
inner_join(sentiments, by = "word") %>%
count(label, sentiment) %>%
group_by(label, sentiment) %>%
summarise(n = sum(n)) %>% ungroup()
13
ggplot(sentiment_scores, aes(x = sentiment, y = n, fill = label)) +
geom_bar(stat = "identity", position = "dodge") +
coord_flip() +
labs(title = "Sentiment Scores by Class") +
scale_fill_manual(values = c("Positive" = "#1b9e77", "Negative" = "#d95f02")) +
theme_minimal(base_size = 14)
surprise
sadness
positive
sentiment
label
negative
Negative
joy
Positive
fear
disgust
anticipation
anger
============================================
7. Topic Modeling (LDA)
============================================
make_ngram_dtm <- function(text_vector, doc_ids, min_term_freq = 5) {
base <- tibble(doc_id = doc_ids, text = text_vector)
unigrams <- base %>% unnest_tokens(term, text, token = "words")
bigrams <- base %>% unnest_tokens(term, text, token = "ngrams", n = 2)
tokens <- bind_rows(unigrams, bigrams)
stopw <- stop_words$word
tokens <- tokens %>% filter(!term %in% stopw, !str_detect(term, "^\\s*$"))
tbl <- tokens %>% count(doc_id, term)
14
if(nrow(tbl)==0) return(list(dtm=NULL, doc_ids=integer(0)))
dtm <- cast_dtm(tbl, document=doc_id, term=term, value=n)
term_totals <- slam::col_sums(dtm)
keep_terms <- names(term_totals[term_totals >= min_term_freq])
dtm <- dtm[, keep_terms, drop = FALSE]
dtm <- dtm[slam::row_sums(dtm)>0, , drop = FALSE]
if(nrow(dtm)==0||ncol(dtm)==0) return(list(dtm=NULL, doc_ids=integer(0)))
list(dtm=dtm, doc_ids=[Link](rownames(dtm)))
}
for(cl in classes){
subset <- data %>% filter(label == cl)
result <- make_ngram_dtm(subset$text, doc_ids = 1:nrow(subset))
dtm <- result$dtm; ids <- result$doc_ids
if([Link](dtm)) next
lda_model <- LDA(dtm, k=k, method="Gibbs", control=list(seed=seed))
gamma_list[[cl]] <- posterior(lda_model)$topics
docid_list[[cl]] <- ids
top_terms <- tidy(lda_model, matrix="beta") %>% group_by(topic) %>% slice_max(beta, n=10)
print(top_terms)
}
## # A tibble: 41 x 3
## # Groups: topic [4]
## topic term beta
## <int> <chr> <dbl>
## 1 1 movie 0.0499
## 2 1 time 0.0217
## 3 1 film 0.0172
## 4 1 watch 0.0157
## 5 1 enjoy 0.0148
## 6 1 play 0.0114
## 7 1 love 0.0110
## 8 1 wonderful 0.0104
## 9 1 fun 0.00966
## 10 1 dvd 0.00935
## # i 31 more rows
## # A tibble: 40 x 3
## # Groups: topic [4]
## topic term beta
## <int> <chr> <dbl>
## 1 1 book 0.106
## 2 1 read 0.0424
## 3 1 write 0.0186
## 4 1 story 0.0131
## 5 1 character 0.0123
## 6 1 author 0.0113
## 7 1 bore 0.0110
15
## 8 1 recommend 0.0102
## 9 1 page 0.0101
## 10 1 life 0.00780
## # i 30 more rows
gamma_all <- [Link](rbind, gamma_list)
doc_ids_all <- unlist(docid_list)
grouping_all <- data$label[match(doc_ids_all, 1:nrow(data))]
16
LDA Projection
0.10
0.05
class
0.00 Negative
Positive
−0.05
−0.10
−2 0 2
LD1
#Latent Dirichlet Allocation (LDA) is applied to uncover hidden topics in the reviews. Documents are
represented as distributions over topics, and topics are characterized by the most probable words. Projecting
the topic distributions using LDA or discriminant analysis allows us to visualize how documents cluster by
class and topic.
============================================
8. Text Embeddings (Word2Vec + t-SNE)
============================================
# Load required libraries
library(text2vec)
library(word2vec)
# -------------------------
# Prepare token lists
pos_tokens <- tokens[data$label == "Positive"]
neg_tokens <- tokens[data$label == "Negative"]
17
# -------------------------
# Train Word2Vec embeddings (skip-gram)
vector_size <- 50
# -------------------------
# Function: find top N similar words
find_similar <- function(embeddings, target_word, top_n = 10){
if(!(target_word %in% rownames(embeddings))){
warning(paste("Word", target_word, "not in vocabulary!"))
return(NULL)
}
target_vec <- embeddings[target_word,, drop = FALSE]
cos_sim <- sim2(x = embeddings, y = target_vec, method = "cosine", norm = "l2")
head(sort(cos_sim[,1], decreasing = TRUE), top_n)
}
##
## Top words similar to 'good' — Negative:
print(similar_neg)
18
# Keep only words common to both classes
common_words <- intersect(rownames(emb_pos), rownames(emb_neg))
combined_emb <- rbind(emb_pos[common_words, ], emb_neg[common_words, ])
labels_tsne <- c(rep("Positive", length(common_words)), rep("Negative", length(common_words)))
[Link](42)
tsne_out <- Rtsne(combined_emb, dims = 2, perplexity = 30, verbose = TRUE, check_duplicates = FALSE)
## Performing PCA
## Read the 3306 x 50 data matrix successfully!
## OpenMP is working. 1 threads.
## Using no_dims = 2, perplexity = 30.000000, and theta = 0.500000
## Computing input similarities...
## Building tree...
## Done in 0.68 seconds (sparsity = 0.031099)!
## Learning embedding...
## Iteration 50: error is 85.156798 (50 iterations in 0.21 seconds)
## Iteration 100: error is 85.057679 (50 iterations in 0.21 seconds)
## Iteration 150: error is 82.814289 (50 iterations in 0.20 seconds)
## Iteration 200: error is 82.810909 (50 iterations in 0.15 seconds)
## Iteration 250: error is 82.810909 (50 iterations in 0.15 seconds)
## Iteration 300: error is 3.554473 (50 iterations in 0.19 seconds)
## Iteration 350: error is 3.372881 (50 iterations in 0.18 seconds)
## Iteration 400: error is 3.273188 (50 iterations in 0.18 seconds)
## Iteration 450: error is 3.211465 (50 iterations in 0.18 seconds)
## Iteration 500: error is 3.168804 (50 iterations in 0.19 seconds)
## Iteration 550: error is 3.139269 (50 iterations in 0.19 seconds)
## Iteration 600: error is 3.126256 (50 iterations in 0.18 seconds)
## Iteration 650: error is 3.116577 (50 iterations in 0.18 seconds)
## Iteration 700: error is 3.105911 (50 iterations in 0.18 seconds)
## Iteration 750: error is 3.095658 (50 iterations in 0.18 seconds)
## Iteration 800: error is 3.086469 (50 iterations in 0.17 seconds)
## Iteration 850: error is 3.079761 (50 iterations in 0.18 seconds)
## Iteration 900: error is 3.073838 (50 iterations in 0.18 seconds)
## Iteration 950: error is 3.068533 (50 iterations in 0.17 seconds)
## Iteration 1000: error is 3.063449 (50 iterations in 0.17 seconds)
## Fitting performed in 3.63 seconds.
tsne_df <- [Link](
X = tsne_out$Y[,1],
Y = tsne_out$Y[,2],
word = rep(common_words, 2),
class = labels_tsne
)
# -------------------------
# Subsample points per class to avoid clutter
max_points <- 300
if(nrow(tsne_df) > max_points){
tsne_df <- tsne_df %>%
group_by(class) %>%
group_modify(~ {
n_rows <- nrow(.x)
n_sample <- min(floor(max_points/2), n_rows)
19
.x[sample(n_rows, n_sample), ]
}) %>%
ungroup()
}
# -------------------------
# Plot t-SNE
ggplot(tsne_df, aes(x = X, y = Y, color = class, label = word)) +
geom_point(alpha = 0.7) +
geom_text(aes(label = word), check_overlap = TRUE, size = 3) +
scale_color_manual(values = c("Positive" = "#1b9e77", "Negative" = "#d95f02")) +
labs(title = "t-SNE Visualization of Word Embeddings",
x = "t-SNE 1", y = "t-SNE 2") +
theme_minimal(base_size = 14)
−20 −10 0 10 20
t−SNE 1
#Word2Vec embeddings convert words into numerical vectors that capture semantic similarity. We train
separate embeddings for Positive and Negative reviews, then find words with similar meanings within each
class. t-SNE projects high-dimensional embeddings into 2D for visualization, revealing clusters of related
words and class-specific patterns.
20