Sentiment Analysis in R
Join tokens with sentiment lexicons to measure positive/negative tone.
Sentiment Analysis in R is a free R Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Sentiment Analysis Overview
Sentiment analysis assigns emotional valence to text. The tidytext approach matches individual words against a sentiment lexicon and aggregates the scores. Three built-in lexicons are available: AFINN (numeric scores), Bing (positive/negative), and NRC (emotion categories).
library(tidytext)
# Available sentiment lexicons
# AFINN: -5 to +5 numeric score
# Bing: binary positive/negative
# NRC: emotion categories (joy, fear, anger, ...)
# Preview AFINN
afinn_sample <- get_sentiments('afinn')
cat('AFINN rows:', nrow(afinn_sample), '\n')
cat('Score range:', range(afinn_sample$value), '\n')
print(head(afinn_sample, 5))get_sentiments('afinn')
The AFINN lexicon assigns integer scores from −5 (very negative) to +5 (very positive) to 2,477 English words. It was compiled by Finn Årup Nielsen and works well for social media and review text.
library(tidytext)
library(dplyr)
afinn <- get_sentiments('afinn')
# Most positive and most negative words
cat('Most positive words:\n')
print(afinn |> slice_max(value, n = 5))
cat('\nMost negative words:\n')
print(afinn |> slice_min(value, n = 5))
# Score distribution
cat('\nScore distribution:\n')
print(table(afinn$value))get_sentiments('bing')
The Bing lexicon (Bing Liu et al.) classifies 6,786 words as either positive or negative. It is larger than AFINN and well-suited for product reviews and customer feedback where simple polarity is sufficient.
library(tidytext)
library(dplyr)
bing <- get_sentiments('bing')
cat('Bing lexicon size:', nrow(bing), '\n')
cat('Positive words:', sum(bing$sentiment == 'positive'), '\n')
cat('Negative words:', sum(bing$sentiment == 'negative'), '\n')
# Sample positive and negative words
cat('\nSample positive:', head(bing$word[bing$sentiment == 'positive'], 8), '\n')
cat('Sample negative:', head(bing$word[bing$sentiment == 'negative'], 8), '\n')inner_join: Scoring Words
inner_join(tokens, afinn, by = 'word') keeps only tokens that appear in the AFINN lexicon, appending their scores. Words not in the lexicon are silently dropped — this is both a strength (focuses on known sentiment words) and a limitation.
library(tidytext)
library(dplyr)
reviews <- tibble::tibble(
review_id = 1:3,
text = c(
'The product is excellent and outstanding quality',
'Terrible experience, broken and disappointing',
'Good value but slow delivery, acceptable overall'
)
)
scored <- reviews |>
unnest_tokens(word, text) |>
inner_join(get_sentiments('afinn'), by = 'word')
cat('Scored words:\n')
print(scored[, c('review_id', 'word', 'value')])Net Sentiment per Document
Sum AFINN scores per document with group_by(document) |> summarise(sentiment = sum(value)) to get a net sentiment score. Positive sums indicate overall positive sentiment; negative sums indicate negative sentiment.
library(tidytext)
library(dplyr)
reviews <- tibble::tibble(
review_id = 1:5,
text = c(
'excellent outstanding perfect love best amazing',
'terrible broken horrible awful waste money',
'good acceptable okay decent average',
'brilliant fantastic outstanding love recommend',
'poor bad disappointing slow useless'
)
)
net_sentiment <- reviews |>
unnest_tokens(word, text) |>
inner_join(get_sentiments('afinn'), by = 'word') |>
group_by(review_id) |>
summarise(
sentiment = sum(value),
word_count = n()
) |>
mutate(polarity = ifelse(sentiment > 0, 'positive', 'negative'))
print(net_sentiment)Bing Sentiment: Positive vs Negative Counts
With the Bing lexicon, compare positive and negative word counts per document. Compute sentiment = positive_n - negative_n for a net polarity score, or plot both counts as grouped bars to see the composition.
library(tidytext)
library(dplyr)
corpus <- tibble::tibble(
chapter = 1:4,
text = c(
'the hero was brave courageous strong and victorious in battle',
'disaster struck terrible losses failure defeat mourning grief',
'love joy happiness beautiful peaceful wonderful morning',
'evil darkness corrupt wicked terrible pain suffering fear'
)
)
bing_sentiment <- corpus |>
unnest_tokens(word, text) |>
inner_join(get_sentiments('bing'), by = 'word') |>
count(chapter, sentiment) |>
tidyr::pivot_wider(names_from = sentiment, values_from = n, values_fill = 0) |>
mutate(net = positive - negative)
print(bing_sentiment)Sentiment by Chapter or Section
To track sentiment across a narrative arc, compute net sentiment per chapter or rolling window. This reveals emotional structure: rising action, climax, resolution. Use ggplot2 with geom_line() or geom_bar() for the arc visualisation.
library(tidytext)
library(dplyr)
library(ggplot2)
# Simulate a 10-chapter story arc
set.seed(42)
chapters <- tibble::tibble(
chapter = 1:10,
text = c(
'peaceful beautiful joy love happy wonderful',
'good friends happy carefree enjoyable fun',
'worried anxious trouble fear uncertain dark',
'danger terrible threat awful pain suffering',
'fear horror disaster terrible devastation loss',
'fight battle struggle hard difficult challenge',
'hope courage determination brave strong resist',
'triumph victory success celebrate joy love',
'relief peace gratitude wonderful blessed happy',
'love joy peace beautiful grateful wonderful'
)
)
arc <- chapters |>
unnest_tokens(word, text) |>
inner_join(get_sentiments('afinn'), by = 'word') |>
group_by(chapter) |>
summarise(net_sentiment = sum(value))
ggplot(arc, aes(chapter, net_sentiment)) +
geom_line(color = 'steelblue', linewidth = 1.2) +
geom_hline(yintercept = 0, linetype = 'dashed') +
labs(x = 'Chapter', y = 'Net Sentiment', title = 'Story Sentiment Arc') +
theme_minimal()NRC Lexicon: Emotion Categories
The NRC lexicon (Saif Mohammad & Peter Turney) categorises 13,901 words into eight emotions (anger, anticipation, disgust, fear, joy, sadness, surprise, trust) plus positive/negative. Filter by emotion with filter(sentiment == 'joy').
library(tidytext)
library(dplyr)
nrc <- get_sentiments('nrc')
cat('NRC size:', nrow(nrc), '\n')
cat('Emotions:', paste(unique(nrc$sentiment), collapse = ', '), '\n')
# Words associated with 'joy'
joy_words <- nrc |> filter(sentiment == 'joy')
cat('\nJoy words:', nrow(joy_words), '\n')
cat('Sample:', head(joy_words$word, 10), '\n')
# Count words per emotion
nrc |>
count(sentiment, sort = TRUE) |>
print()Applying NRC to a Corpus
Join tokenised text with NRC to score each emotion separately. Aggregate per document and emotion using count(doc_id, sentiment) to see which emotions dominate each document.
library(tidytext)
library(dplyr)
docs <- tibble::tibble(
doc_id = c(1, 1, 2, 2, 3, 3),
text = c(
'wonderful joyful happy love peace',
'excited surprise anticipation trust',
'fear anger terrible hate disgust',
'dark horrible awful suffering pain',
'curious wonder discovery learning',
'hope trust anticipation future growth'
)
)
nrc_scores <- docs |>
unnest_tokens(word, text) |>
inner_join(get_sentiments('nrc'), by = 'word') |>
filter(!sentiment %in% c('positive', 'negative')) |> # keep only emotions
count(doc_id, sentiment, sort = TRUE)
cat('Emotion counts per document:\n')
print(nrc_scores)Wordcloud Concept
A wordcloud visualises word frequencies as text sized proportionally to count. The wordcloud or wordcloud2 packages generate them from a word/frequency data frame. Colour words by sentiment using the Bing lexicon for a sentiment-coloured cloud.
library(tidytext)
library(dplyr)
# Prepare word frequencies coloured by Bing sentiment
corpus_text <- tibble::tibble(
text = c(
'excellent performance amazing results love beautiful',
'terrible failure poor broken horrible waste',
'brilliant outstanding success happy wonderful joy',
'awful disappointing bad slow useless frustrating'
)
)
word_freq <- corpus_text |>
unnest_tokens(word, text) |>
count(word, sort = TRUE) |>
left_join(get_sentiments('bing'), by = 'word') |>
mutate(
sentiment = replace(sentiment, is.na(sentiment), 'neutral'),
colour = case_when(
sentiment == 'positive' ~ '#2196F3',
sentiment == 'negative' ~ '#F44336',
TRUE ~ '#9E9E9E'
)
)
print(word_freq)
# In practice: wordcloud2(word_freq[, c('word','n')], color = word_freq$colour)Limitations of Lexicon-Based Sentiment
Lexicon-based sentiment analysis has known failure modes: negation ("not good" scores as positive), sarcasm ("oh great, another bug"), domain mismatch ("sick" is positive in slang), and out-of-vocabulary words. Consider these limitations when interpreting results.
library(tidytext)
library(dplyr)
# Negation problem
neg_examples <- tibble::tibble(
text = c(
'not good at all', # negative, but 'good' scores +3
'not bad', # positive, but 'bad' scores -3
'sick beats bro' # slang positive, 'sick' is negative in AFINN
)
)
scored <- neg_examples |>
unnest_tokens(word, text) |>
inner_join(get_sentiments('afinn'), by = 'word')
cat('Lexicon scores (naive - ignores negation):\n')
print(scored[, c('word', 'value')])
cat('\nNote: "not" is a stop word - the negation is lost!\n')Quick Check
You use inner_join(tokens, afinn, by = 'word') for sentiment scoring. What happens to words in your tokens that are NOT in the AFINN lexicon?
Recap: Sentiment Analysis
Key takeaways:
- Three main lexicons: AFINN (scores −5 to +5), Bing (positive/negative), NRC (8 emotions)
get_sentiments('afinn')/'bing'/'nrc'retrieves the lexicon as a tibbleinner_join(tokens, lexicon, by = 'word')scores only matched words (unmatched dropped)- Net sentiment =
group_by(doc) |> summarise(sentiment = sum(value)) - Bing: compare
positive - negativeword counts per document or section - NRC: filter by emotion name to isolate fear, joy, anger, etc.
- Key limitation: lexicon-based methods don't handle negation, sarcasm, or domain slang
library(tidytext)
library(dplyr)
tibble::tibble(text = 'excellent amazing love joy beautiful happy') |>
unnest_tokens(word, text) |>
inner_join(get_sentiments('afinn'), by = 'word') |>
summarise(net_sentiment = sum(value)) |>
print()Frequently asked questions
Is the “Sentiment Analysis in R” lesson free?
Yes — the full text of “Sentiment Analysis in R” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.
What will I learn in “Sentiment Analysis in R”?
Join tokens with sentiment lexicons to measure positive/negative tone. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start R Academy?
No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Sentiment Analysis in R” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this R Academy lesson?
Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Tokenization and Stop Word Removal
- TF-IDF and Term Frequency Analysis
- Sentiment Analysis in R
- Topic Modeling with LDA