0% found this document useful (0 votes)
7 views20 pages

Text Analysis of Positive and Negative Reviews

The document outlines a process for analyzing a labeled text dataset containing reviews, focusing on text preprocessing, word frequency analysis, and visualization techniques such as word clouds. It includes code snippets for loading necessary R packages, cleaning the text data, and generating visual representations of the most frequent words in positive and negative reviews. The analysis aims to highlight the vocabulary characteristic of each sentiment class and provide insights into the data.

Uploaded by

hristov.nikola08
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views20 pages

Text Analysis of Positive and Negative Reviews

The document outlines a process for analyzing a labeled text dataset containing reviews, focusing on text preprocessing, word frequency analysis, and visualization techniques such as word clouds. It includes code snippets for loading necessary R packages, cleaning the text data, and generating visual representations of the most frequent words in positive and negative reviews. The analysis aims to highlight the vocabulary characteristic of each sentiment class and provide insights into the data.

Uploaded by

hristov.nikola08
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Untitled

Maksim Brusilovskii_Aleksandar Rajic_Nikola Hristov

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)

## Warning: ����� 'tm' ��� ������ ��� R ������ 4.4.3


## �������� ���������� ������: NLP
library(textstem)

## Warning: ����� 'textstem' ��� ������ ��� R ������ 4.4.3


## �������� ���������� ������: [Link]
## Warning: ����� '[Link]' ��� ������ ��� R ������ 4.4.3
## �������� ���������� ������: koRpus
## Warning: ����� 'koRpus' ��� ������ ��� R ������ 4.4.3
## �������� ���������� ������: sylly
## Warning: ����� 'sylly' ��� ������ ��� R ������ 4.4.3
## For information on available language packages for 'koRpus', run
##
## [Link]()
##
## and see ?[Link]()
##
## ����������� �����: 'koRpus'
## ��������� ������ ����� �� 'package:tm':
##
## readTagged
library(stringr)
library(tidyverse)

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)

## Warning: ����� 'tidytext' ��� ������ ��� R ������ 4.4.3


library(topicmodels)

## Warning: ����� 'topicmodels' ��� ������ ��� R ������ 4.4.3


library(slam)
library(MASS)

##
## ����������� �����: 'MASS'
##
## ��������� ������ ����� �� 'package:dplyr':
##
## select
library(ggplot2)
library(ggforce)

## Warning: ����� 'ggforce' ��� ������ ��� R ������ 4.4.3


library(wordcloud)

## Warning: ����� 'wordcloud' ��� ������ ��� R ������ 4.4.3


## �������� ���������� ������: RColorBrewer
library(RColorBrewer)
library(igraph)

## Warning: ����� 'igraph' ��� ������ ��� R ������ 4.4.3


##
## ����������� �����: 'igraph'
##
## ��������� ������� ������ �� 'package:lubridate':
##

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)

## Warning: ����� 'ggraph' ��� ������ ��� R ������ 4.4.3


library(text2vec)

## Warning: ����� 'text2vec' ��� ������ ��� R ������ 4.4.3


##
## ����������� �����: 'text2vec'
##
## ��������� ������ ����� �� 'package:igraph':
##
## normalize
##
## ��������� ������ ����� �� 'package:topicmodels':
##
## perplexity
library(Rtsne)

## Warning: ����� 'Rtsne' ��� ������ ��� R ������ 4.4.3


library([Link])

## Warning: ����� '[Link]' ��� ������ ��� R ������ 4.4.3


##
## ����������� �����: '[Link]'
##
## ��������� ������ ����� �� 'package:slam':
##

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)

# Extract labels and text


labels <- gsub(" .*", "", text_data)
labels <- ifelse(labels == "__label__2", "Positive",
ifelse(labels == "__label__1", "Negative", NA))
text_only <- gsub("^\\S+\\s+", "", text_data)

# Build dataframe
data <- [Link](label = labels, text = text_only, stringsAsFactors = FALSE)
data <- data %>% filter(![Link](label))

#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)
}

top_positive <- get_top_words("Positive")


top_negative <- get_top_words("Negative")

# Prepare dataframe for plotting


pos_df <- [Link](word = names(top_positive), frequency = [Link](top_positive), class = "Positive
neg_df <- [Link](word = names(top_negative), frequency = [Link](top_negative), class = "Negative
freq_plot_data <- rbind(pos_df, neg_df)

# 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

0 500 1000 1500


Frequency
#We identify the most frequent words in each class to understand the dominant vocabulary. This helps
highlight which words are characteristic of positive versus negative reviews. The results are visualized with
bar plots to allow easy comparison between the classes.

============================================
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)

# Store plot as a function to call


wordcloud(names(freq), freq, [Link] = max_words,
colors = [Link](8, "Dark2"),
scale = c(4, 0.5), [Link] = FALSE)
}

# Positive & Negative Word Clouds


make_wordcloud("Positive")
title("Word Cloud — Positive Reviews")

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

clean_text <- function(x) {


x <- tolower(x)
x <- gsub("[^a-z ]", " ", x) # remove punctuation, digits, symbols
x <- removeWords(x, stopwords("en"))
x <- stripWhitespace(x)
return(x)
}

make_commonality_wordcloud <- function(max_words = 150, min_freq = 20) {


pos_clean <- clean_text(data$text[data$label == "Positive"])
neg_clean <- clean_text(data$text[data$label == "Negative"])

pos_words <- unlist(strsplit(pos_clean, "\\s+"))


neg_words <- unlist(strsplit(neg_clean, "\\s+"))

pos_freq <- table(pos_words)


neg_freq <- table(neg_words)

# Find words appearing frequently in BOTH classes


common <- intersect(names(pos_freq), names(neg_freq))

# Use average frequency to rank


freq <- (pos_freq[common] + neg_freq[common]) / 2
freq <- sort(freq[freq >= min_freq], decreasing = TRUE)

wordcloud(

8
names(freq), freq,
[Link] = max_words,
colors = [Link](8, "Dark2"),
scale = c(4, 0.7),
[Link] = FALSE
)

title("Commonality Word Cloud — Shared Words")


}

make_commonality_wordcloud()

## Warning in wordcloud(names(freq), freq, [Link] = max_words, colors =


## [Link](8, : understand could not be fit on page. It will not be plotted.

almost classic novel disappoint


Commonality keep Cloud —favorite
Word enjoy dayShared Words
game bore waste since wonderful
order long product let leave original
easy
character may another without
bite star album bad
year now cant right hope
version

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

watch every help


different

two come
author recommendplay worth problem
though
point doesnt interest purchase
world something anyone excellent

# Generate the commonality word cloud


make_commonality_wordcloud()

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

also ever real


lot just try start
recommend

song
child
cd
still make like
one get classic
buy listen
old part
tell

play work time lifeseem


seefun
amazon

put give go great can waste

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)

create_cooc_network <- function(class_label, min_freq = 10) {


class_data <- [Link](text = data$text[data$label == class_label])

# Generate bigrams
bigrams <- class_data %>% unnest_tokens(bigram, text, token = "ngrams", n = 2)
bigram_sep <- bigrams %>% separate(bigram, into = c("word1","word2"), sep = " ")

# Remove stopwords and NA


data("stop_words")
bigram_filt <- bigram_sep %>% filter(!word1 %in% stop_words$word,
!word2 %in% stop_words$word,
word1 != word2,
![Link](word1),
![Link](word2))

# Count co-occurrences

10
bigram_counts <- bigram_filt %>% count(word1, word2, sort = TRUE) %>% filter(n >= min_freq)
if(nrow(bigram_counts) == 0) return(NULL)

# Create graph object


graph <- graph_from_data_frame(bigram_counts)

# Store plot as object


g <- ggraph(graph, layout = "fr") +
geom_edge_link(aes(width = n), alpha = 0.4) +
geom_node_point(color = "darkred", size = 5) +
geom_node_text(aes(label = name), repel = TRUE, size = 4) +
labs(title = paste("Word Co-occurrence Network —", class_label),
edge_width = "Frequency") +
theme_minimal()

return(g)
}

# Generate plots
pos_net <- create_cooc_network("Positive", min_freq = 15)
neg_net <- create_cooc_network("Negative", min_freq = 15)

# Print plots sequentially in current plotting device


if(![Link](pos_net)) print(pos_net)

## Warning: The `trans` argument of `continuous_scale()` is deprecated as of ggplot2 3.5.0.


## i Please use the `transform` argument instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

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

recommend book album


−2 readive
clay
aiken
start easy
0 2 4
x

if(![Link](neg_net)) print(neg_net)

12
Word Co−occurrence Network — Negative

rachel weisz main

2 baby spin dry character


line
poor player
quality dvd story Frequency
0 service
20
40
y

special customer time 60


−2 effect save waste 80

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()

## Warning in inner_join(., sentiments, by = "word"): Detected an unexpected many-to-many relationship b


## i Row 3 of `x` matches multiple rows in `y`.
## i Row 5621 of `y` matches multiple rows in `x`.
## i If a many-to-many relationship is expected, set `relationship =
## "many-to-many"` to silence this warning.
## `summarise()` has grouped output by 'label'. You can override using the
## `.groups` argument.

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)

Sentiment Scores by Class


trust

surprise

sadness

positive
sentiment

label
negative
Negative
joy
Positive
fear

disgust

anticipation

anger

0 4000 8000 12000


n
#Sentiment analysis assigns emotion categories to words using a lexicon (like NRC). We count sentiment
occurrences for each class to quantify the emotional tone of Positive and Negative reviews. Visualizing these
counts helps compare sentiment distribution across classes.

============================================
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)))
}

classes <- unique(data$label)


k <- 4
seed <- 123
gamma_list <- list()
docid_list <- list()

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))]

# MASS LDA projection


gamma_scaled <- scale(gamma_all)
lda_fit <- MASS::lda(x=gamma_scaled, grouping=grouping_all)

## Warning in [Link](x, grouping, ...): variables are collinear


scores <- predict(lda_fit)$x
num_axes <- ncol(scores)

plot_df <- tibble(LD1 = scores[,1],


LD2 = if(num_axes>=2) scores[,2] else 0,
class = grouping_all)

# Subsample for plotting


max_points_per_class <- 200
plot_df_sub <- plot_df %>% group_by(class) %>%
slice(sample(1:n(), size=min(n(), max_points_per_class))) %>% ungroup()

ggplot(plot_df_sub, aes(x=LD1, y=LD2, color=class)) +


geom_point(size=3, alpha=0.7, position=position_jitter(width=0.1, height=0.1)) +
geom_mark_ellipse(aes(fill=class), alpha=0.15, [Link]=FALSE) +
theme_minimal(base_size=15) +
labs(title=if(num_axes==1) "LDA Projection" else "LDA Projection",
x="LD1", y=if(num_axes>=2) "LD2" else "") +
scale_color_manual(values=c("Positive"="#4A90E2","Negative"="#E94E77")) +
scale_fill_manual(values=c("Positive"="#4A90E2","Negative"="#E94E77"))

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)

## Warning: ����� 'word2vec' ��� ������ ��� R ������ 4.4.3


library(Rtsne)
library(dplyr)
library(ggplot2)

# -------------------------
# Prepare token lists
pos_tokens <- tokens[data$label == "Positive"]
neg_tokens <- tokens[data$label == "Negative"]

17
# -------------------------
# Train Word2Vec embeddings (skip-gram)
vector_size <- 50

w2v_pos <- word2vec::word2vec(x = pos_tokens, type = "skip-gram",


dim = vector_size, window = 5,
min_count = 5, iter = 50)

w2v_neg <- word2vec::word2vec(x = neg_tokens, type = "skip-gram",


dim = vector_size, window = 5,
min_count = 5, iter = 50)

emb_pos <- [Link](w2v_pos)


emb_neg <- [Link](w2v_neg)

# -------------------------
# 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)
}

# Example: similar words to 'good'


similar_pos <- find_similar(emb_pos, "good", 10)
similar_neg <- find_similar(emb_neg, "good", 10)

cat("Top words similar to 'good' — Positive:\n")

## Top words similar to 'good' — Positive:


print(similar_pos)

## good great think make get one do like


## 1.0000000 0.7151615 0.6636990 0.6479684 0.6401674 0.6361348 0.6288666 0.6258024
## still ever
## 0.6097494 0.5952540
cat("\nTop words similar to 'good' — Negative:\n")

##
## Top words similar to 'good' — Negative:
print(similar_neg)

## good think just like do much really even


## 1.0000000 0.6916076 0.6764503 0.6589747 0.6585311 0.6575936 0.6480872 0.6462429
## may great
## 0.6348171 0.6335983
# -------------------------
# t-SNE Visualization

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)

t−SNE Visualization of Word Embeddings


soul former
length pair
20 prefer customer
suffer g
smell pump
printer
recommend
show
whether buy sign deal alot notice
10 installment say toy edit
evidence unable careful jazz
recent true parent
praise bob tora recently compilation
t−SNE 2

pride resolve class


tv shes
plain conclusion mary
reference fault block intrigue public
0 drama detail floor a Negative
boleyn rich
politic lover common emotion
fight quick unusual a Positive
structure desert teach stephen numerous clear sink brief
likely
field table green require unique
encounter
−10 chance parent
heavy pleaseone
noise neither
nearly still often
week
usb normally
poor
−20 package upper
cut literally plate
wet toy

−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

You might also like