Assignment10A | Sentiment Analysis

Author

Yeimi Perez

Published

November 2, 2026

Approach

For this assignment, I will reproduce the sentiment analysis example from Chapter 2 of Text Mining with R using Jane Austen’s novels. I will use the tidytext package in R to tokenize the text and apply the NRC, Bing, and AFINN sentiment lexicons to examine patterns in positive and negative language. To extend the analysis, I will use a different text corpus consisting of U.S. presidential inaugural addresses. I will also introduce the Syuzhet sentiment lexicon to compare how different dictionaries evaluate the same text.

Introduction

This assignment reproduces the sentiment analysis examples from Chapter 2 of Text Mining with R by Silge and Robinson (2017), using Jane Austen’s novels and the NRC, Bing, and AFINN sentiment lexicons.To extend the analysis, I will examine U.S. presidential inaugural addresses and introduce the Syuzhet sentiment lexicon to compare sentiment patterns across different texts and methods.

Research Question: How does sentiment vary across U.S. presidential inaugural addresses, and how do different sentiment lexicons influence the results?

The original example code is adapted from Silge and Robinson’s Chapter 2.

library(dplyr)
library(tidyr)
library(stringr)
library(ggplot2)
library(tidytext)
library(janeaustenr)
library(quanteda)
library(syuzhet)
library(knitr)

Reproduce Chapter 2

Jane Austen - Make the novels tidy

The authors tokenize Jane Austen’s novels into one word per row, retaining each word’s book and line number. This is the central preparation step in Chapter 2.

tidy_books <- janeaustenr::austen_books() %>%
  group_by(book) %>%
  mutate(
    linenumber = row_number(),
    chapter = cumsum(
      str_detect(text, regex("^chapter [\\divxlc]", ignore_case = TRUE))
    )
  ) %>%
  ungroup() %>%
  unnest_tokens(word, text)

print(head(tidy_books, 8))
# A tibble: 8 × 4
  book                linenumber chapter word       
  <fct>                    <int>   <int> <chr>      
1 Sense & Sensibility          1       0 sense      
2 Sense & Sensibility          1       0 and        
3 Sense & Sensibility          1       0 sensibility
4 Sense & Sensibility          3       0 by         
5 Sense & Sensibility          3       0 jane       
6 Sense & Sensibility          3       0 austen     
7 Sense & Sensibility          5       0 1811       
8 Sense & Sensibility         10       1 chapter    
cat("Number of novels:", n_distinct(tidy_books$book), "\n")
Number of novels: 6 

Sentiment analysis across Austen novels

I will retrieve Jane Austen’s six completed novels and convert the text into individual word tokens and prepared the original corpus for sentiment analysis.

tidy_books <- janeaustenr::austen_books() %>%
  group_by(book) %>%
  mutate(
    linenumber = row_number(),
    chapter = cumsum(
      stringr::str_detect(
        text,
        stringr::regex(
          "^chapter [\\divxlc]",
          ignore_case = TRUE
        )
      )
    )
  ) %>%
  ungroup() %>%
  tidytext::unnest_tokens(word, text)

# Display the first 10 rows
print(head(tidy_books, 10))
# A tibble: 10 × 4
   book                linenumber chapter word       
   <fct>                    <int>   <int> <chr>      
 1 Sense & Sensibility          1       0 sense      
 2 Sense & Sensibility          1       0 and        
 3 Sense & Sensibility          1       0 sensibility
 4 Sense & Sensibility          3       0 by         
 5 Sense & Sensibility          3       0 jane       
 6 Sense & Sensibility          3       0 austen     
 7 Sense & Sensibility          5       0 1811       
 8 Sense & Sensibility         10       1 chapter    
 9 Sense & Sensibility         10       1 1          
10 Sense & Sensibility         13       1 the        
# Confirm the number of novels
print(dplyr::n_distinct(tidy_books$book))
[1] 6
# Examine the dataset structure
dplyr::glimpse(tidy_books)
Rows: 725,055
Columns: 4
$ book       <fct> Sense & Sensibility, Sense & Sensibility, Sense & Sensibili…
$ linenumber <int> 1, 1, 1, 3, 3, 3, 5, 10, 10, 13, 13, 13, 13, 13, 13, 13, 13…
$ chapter    <int> 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
$ word       <chr> "sense", "and", "sensibility", "by", "jane", "austen", "181…

NRC Sentiment Analysis: Identifying Joy-Associated Words in Emma

The NRC Word-Emotion Association Lexicon categorizes words according to different emotions, including joy, trust, fear, anger, and sadness. In Chapter 2 of Text Mining with R, Silge and Robinson (2017) use this lexicon to identify words associated with joy in Jane Austen’s Emma.

To reproduce this example, I will filter the NRC lexicon to include only joy-associated words, match those words with the text of Emma, and calculate their frequencies. This analysis will help identify which joy-related terms occur most frequently in the novel.

nrc_joy <- get_sentiments("nrc") %>%
  filter(sentiment == "joy")

# Match NRC joy words with Emma
emma_joy <- tidy_books %>%
  filter(book == "Emma") %>%
  inner_join(nrc_joy, by = "word") %>%
  count(word, sort = TRUE)

# Display the 10 most frequent joy words
print(head(emma_joy, 10))
# A tibble: 10 × 2
   word          n
   <chr>     <int>
 1 good        359
 2 friend      166
 3 hope        143
 4 happy       125
 5 love        117
 6 deal         92
 7 found        92
 8 present      89
 9 kind         82
10 happiness    76

Interpretation: The NRC sentiment analysis identified frequently occurring words associated with joy in Jane Austen’s Emma. By matching the novel’s text with the NRC lexicon, I was able to examine which joy-related terms appeared most often.

This example demonstrates how sentiment dictionaries can transform written language into measurable word-frequency patterns. However, the presence of a joy-associated word does not necessarily mean that the entire sentence expresses happiness, since the surrounding context may influence its meaning.

library(tidytext)
library(dplyr)

nrc_joy <- get_sentiments("nrc") %>%
  filter(sentiment == "joy")

print(head(nrc_joy, 10))
# A tibble: 10 × 2
   word          sentiment
   <chr>         <chr>    
 1 absolution    joy      
 2 abundance     joy      
 3 abundant      joy      
 4 accolade      joy      
 5 accompaniment joy      
 6 accomplish    joy      
 7 accomplished  joy      
 8 achieve       joy      
 9 achievement   joy      
10 acrobat       joy      

Bing Sentiment Analysis: Measuring Positive and Negative Sentiment Across Jane Austen’s Novels

I will reproduce the Bing sentiment analysis from Chapter 2 of Text Mining with R. Unlike the NRC analysis, which focused on joy-associated words in Emma, the Bing lexicon classifies words as either positive or negative.

I will divide Jane Austen’s novels into sections of approximately 80 lines, count the positive and negative words in each section, and calculate the difference to measure net sentiment. I will then visualize how sentiment changes throughout the six novels.

# Calculate positive and negative sentiment across novels
jane_sentiment <- tidy_books %>%
  inner_join(
    get_sentiments("bing"),
    by = "word"
  ) %>%
  count(
    book,
    index = linenumber %/% 80,
    sentiment
  ) %>%
  pivot_wider(
    names_from = sentiment,
    values_from = n,
    values_fill = 0
  ) %>%
  mutate(
    net_sentiment = positive - negative
  )

# Display the first 10 observations
print(head(jane_sentiment, 10))
# A tibble: 10 × 5
   book                index negative positive net_sentiment
   <fct>               <dbl>    <int>    <int>         <int>
 1 Sense & Sensibility     0       16       32            16
 2 Sense & Sensibility     1       19       53            34
 3 Sense & Sensibility     2       12       31            19
 4 Sense & Sensibility     3       15       31            16
 5 Sense & Sensibility     4       16       34            18
 6 Sense & Sensibility     5       16       51            35
 7 Sense & Sensibility     6       24       40            16
 8 Sense & Sensibility     7       23       51            28
 9 Sense & Sensibility     8       30       40            10
10 Sense & Sensibility     9       15       19             4
# Verify that all six novels are represented
print(n_distinct(jane_sentiment$book))
[1] 6

Visualizing Sentiment Patterns Across the Novels

ggplot(
  jane_sentiment,
  aes(
    x = index,
    y = net_sentiment,
    fill = book
  )
) +
  geom_col(show.legend = FALSE) +
  facet_wrap(
    ~ book,
    ncol = 2,
    scales = "free_x"
  ) +
  labs(
    title = "Sentiment Throughout Jane Austen's Novels",
    subtitle = "Bing Positive and Negative Word Analysis",
    x = "Narrative Section (80-Line Groups)",
    y = "Net Sentiment Score"
  ) +
  theme_minimal()

  • Positive fluctuations: Sections with more positive than negative word matches.

  • Negative fluctuations: Sections with more negative than positive word matches.

  • Variation across novels: Differences in the frequency and magnitude of these fluctuations.

Interpretation: The Bing sentiment analysis demonstrates how positive and negative language fluctuates throughout Jane Austen’s novels. By dividing the text into approximately 80-line sections, I was able to calculate net sentiment scores and visualize how the balance of positive and negative words changes across the narratives.

The visualizations show that sentiment is not constant throughout a novel. Instead, the frequency of positive and negative language varies across different sections. This analysis also highlights an important limitation of dictionary-based sentiment analysis: individual word classifications may not fully capture the intended meaning or emotional context of a passage.

Comparing AFINN, Bing, and NRC in Pride & Prejudice

I will reproduce the sentiment lexicon comparison from Chapter 2 of Text Mining with R by applying AFINN, Bing, and NRC to Jane Austen’s Pride & Prejudice.

Although all three lexicons measure sentiment, they use different scoring methods. AFINN assigns numerical values to words, while Bing and NRC classify words as positive or negative.

By calculating sentiment across 80-line sections of the novel, I will compare whether these methods identify similar patterns throughout the narrative.

pride_prejudice <- tidy_books %>%
  filter(book == "Pride & Prejudice")

# Preview the selected novel
print(head(pride_prejudice, 10))
# A tibble: 10 × 4
   book              linenumber chapter word     
   <fct>                  <int>   <int> <chr>    
 1 Pride & Prejudice          1       0 pride    
 2 Pride & Prejudice          1       0 and      
 3 Pride & Prejudice          1       0 prejudice
 4 Pride & Prejudice          3       0 by       
 5 Pride & Prejudice          3       0 jane     
 6 Pride & Prejudice          3       0 austen   
 7 Pride & Prejudice          7       1 chapter  
 8 Pride & Prejudice          7       1 1        
 9 Pride & Prejudice         10       1 it       
10 Pride & Prejudice         10       1 is       
# Verify the number of word tokens
print(nrow(pride_prejudice))
[1] 122204

AFINN Sentiment Analysis

afinn_results <- pride_prejudice %>%
  inner_join(
    get_sentiments("afinn"),
    by = "word"
  ) %>%
  group_by(
    index = linenumber %/% 80
  ) %>%
  summarise(
    sentiment = sum(value),
    .groups = "drop"
  ) %>%
  mutate(
    method = "AFINN"
  )

# Display the first 10 results
print(head(afinn_results, 10))
# A tibble: 10 × 3
   index sentiment method
   <dbl>     <dbl> <chr> 
 1     0        29 AFINN 
 2     1         0 AFINN 
 3     2        20 AFINN 
 4     3        30 AFINN 
 5     4        62 AFINN 
 6     5        66 AFINN 
 7     6        60 AFINN 
 8     7        18 AFINN 
 9     8        84 AFINN 
10     9        26 AFINN 

Bing and NRC Sentiment Analysis

bing_nrc_results <- bind_rows(
  
  # Bing sentiment
  pride_prejudice %>%
    inner_join(
      get_sentiments("bing"),
      by = "word"
    ) %>%
    mutate(method = "Bing"),
  
  # NRC positive and negative sentiment
  pride_prejudice %>%
    inner_join(
      get_sentiments("nrc") %>%
        filter(
          sentiment %in% c("positive", "negative")
        ),
      by = "word"
    ) %>%
    mutate(method = "NRC")
  
) %>%
  count(
    method,
    index = linenumber %/% 80,
    sentiment
  ) %>%
  pivot_wider(
    names_from = sentiment,
    values_from = n,
    values_fill = 0
  ) %>%
  mutate(
    sentiment = positive - negative
  )

# Display the first 10 observations
print(head(bing_nrc_results, 10))
# A tibble: 10 × 5
   method index negative positive sentiment
   <chr>  <dbl>    <int>    <int>     <int>
 1 Bing       0        7       21        14
 2 Bing       1       20       19        -1
 3 Bing       2       16       20         4
 4 Bing       3       19       31        12
 5 Bing       4       23       47        24
 6 Bing       5       15       49        34
 7 Bing       6       18       46        28
 8 Bing       7       23       33        10
 9 Bing       8       17       48        31
10 Bing       9       22       40        18

Combining the Sentiment Results

lexicon_comparison <- bind_rows(
  
  afinn_results,
  
  bing_nrc_results %>%
    select(index, sentiment, method)
  
)

# Display the combined results
print(head(lexicon_comparison, 10))
# A tibble: 10 × 3
   index sentiment method
   <dbl>     <dbl> <chr> 
 1     0        29 AFINN 
 2     1         0 AFINN 
 3     2        20 AFINN 
 4     3        30 AFINN 
 5     4        62 AFINN 
 6     5        66 AFINN 
 7     6        60 AFINN 
 8     7        18 AFINN 
 9     8        84 AFINN 
10     9        26 AFINN 
# Verify all three lexicons are present
print(unique(lexicon_comparison$method))
[1] "AFINN" "Bing"  "NRC"  

Visualizing the Three Sentiment Lexicons

ggplot(
  lexicon_comparison,
  aes(
    x = index,
    y = sentiment,
    fill = method
  )
) +
  geom_col(
    show.legend = FALSE
  ) +
  facet_wrap(
    ~ method,
    ncol = 1,
    scales = "free_y"
  ) +
  labs(
    title = "Comparing Sentiment Lexicons",
    subtitle = "Pride & Prejudice by Jane Austen",
    x = "Narrative Section (80-Line Groups)",
    y = "Sentiment Score"
  ) +
  theme_minimal()

Interpretation: The comparison of AFINN, Bing, and NRC demonstrates how different sentiment dictionaries can produce different numerical results when analyzing the same text. In Pride & Prejudice, the three methods identify broadly similar positive and negative fluctuations throughout the narrative, although the magnitude of the sentiment scores differs.

AFINN uses weighted numerical values, while Bing and NRC calculate net sentiment from positive and negative word classifications. As discussed by Silge and Robinson (2017), these methodological differences influence the resulting scores. This comparison highlights the importance of considering the sentiment lexicon when interpreting text-mining results.

Extended Sentiment Analysis

Introducing a New Text Corpus: U.S. Presidential Inaugural Addresses

To extend the original Chapter 2 sentiment analysis, I selected U.S. presidential inaugural addresses as a different text corpus. Unlike Jane Austen’s novels, which follow fictional narratives, inaugural addresses are speeches that communicate national priorities, challenges, and future goals. I will retrieve presidential inaugural addresses from 1961 onward and prepare them for sentiment analysis. I selected this corpus to investigate how positive and negative language varies across speeches and to compare how different sentiment lexicons evaluate the same text.

# Loading the corpus from the quanteda package
data("data_corpus_inaugural", package = "quanteda")

# Convert the corpus into a tidy data frame
speeches <- tibble(
  year = as.integer(
    quanteda::docvars(
      data_corpus_inaugural, "Year"
    )
  ),
  president = as.character(
    quanteda::docvars(
      data_corpus_inaugural, "President"
    )
  ),
  text = as.character(data_corpus_inaugural)
) %>%
  filter(year >= 1961) %>%
  mutate(
    speech = paste(year, president)
  ) %>%
  arrange(year)

# Display the first six observations
print(
  speeches %>%
    select(year, president, speech) %>%
    head(6)
)
# A tibble: 6 × 3
   year president speech      
  <int> <chr>     <chr>       
1  1961 Kennedy   1961 Kennedy
2  1965 Johnson   1965 Johnson
3  1969 Nixon     1969 Nixon  
4  1973 Nixon     1973 Nixon  
5  1977 Carter    1977 Carter 
6  1981 Reagan    1981 Reagan 
# Display the total number of speeches
print(
  paste("Number of speeches:", nrow(speeches))
)
[1] "Number of speeches: 17"
# Examine the structure of the dataset
glimpse(speeches)
Rows: 17
Columns: 4
$ year      <int> 1961, 1965, 1969, 1973, 1977, 1981, 1985, 1989, 1993, 1997, …
$ president <chr> "Kennedy", "Johnson", "Nixon", "Nixon", "Carter", "Reagan", …
$ text      <chr> "Vice President Johnson, Mr. Speaker, Mr. Chief Justice, Pre…
$ speech    <chr> "1961 Kennedy", "1965 Johnson", "1969 Nixon", "1973 Nixon", …

Interpretation: The quanteda package provided a collection of U.S. presidential inaugural addresses, including the year, president, and full speech text. I filtered the dataset to include speeches delivered from 1961 onward and created a unique identifier for each address.

This dataset will allow me to apply sentiment analysis to a different type of text while maintaining the same general methodology used in the original Jane Austen example. The next stage will involve tokenizing the speeches so that individual words can be matched with sentiment dictionaries.

Preparing Presidential Inaugural Addresses for Sentiment Analysis

Before applying sentiment lexicons, I will convert the presidential inaugural addresses into tidy text using the unnest_tokens() function. This process separates each speech into individual words while retaining the year, president, and speech identifier.

I will also calculate the total number of word tokens in each speech. Because inaugural addresses vary in length, these counts will allow me to normalize sentiment scores and compare the speeches on a more consistent basis.

# Convert each speech into individual word tokens
speech_words <- speeches %>%
  select(
    year,
    president,
    speech,
    text
  ) %>%
  unnest_tokens(word, text)

# Display the first 10 word observations
print(head(speech_words, 10))
# A tibble: 10 × 4
    year president speech       word      
   <int> <chr>     <chr>        <chr>     
 1  1961 Kennedy   1961 Kennedy vice      
 2  1961 Kennedy   1961 Kennedy president 
 3  1961 Kennedy   1961 Kennedy johnson   
 4  1961 Kennedy   1961 Kennedy mr        
 5  1961 Kennedy   1961 Kennedy speaker   
 6  1961 Kennedy   1961 Kennedy mr        
 7  1961 Kennedy   1961 Kennedy chief     
 8  1961 Kennedy   1961 Kennedy justice   
 9  1961 Kennedy   1961 Kennedy president 
10  1961 Kennedy   1961 Kennedy eisenhower
# Count the total number of word tokens in each speech
speech_lengths <- speech_words %>%
  count(
    speech,
    year,
    president,
    name = "total_tokens"
  ) %>%
  arrange(year)

# Display the number of tokens in each speech
print(speech_lengths)
# A tibble: 17 × 4
   speech        year president total_tokens
   <chr>        <int> <chr>            <int>
 1 1961 Kennedy  1961 Kennedy           1367
 2 1965 Johnson  1965 Johnson           1490
 3 1969 Nixon    1969 Nixon             2124
 4 1973 Nixon    1973 Nixon             1805
 5 1977 Carter   1977 Carter            1226
 6 1981 Reagan   1981 Reagan            2436
 7 1985 Reagan   1985 Reagan            2571
 8 1989 Bush     1989 Bush              2318
 9 1993 Clinton  1993 Clinton           1600
10 1997 Clinton  1997 Clinton           2158
11 2001 Bush     2001 Bush              1585
12 2005 Bush     2005 Bush              2073
13 2009 Obama    2009 Obama             2399
14 2013 Obama    2013 Obama             2106
15 2017 Trump    2017 Trump             1446
16 2021 Biden    2021 Biden             2377
17 2025 Trump    2025 Trump             2918
# Verify the total number of speeches
print(
  paste(
    "Number of speeches:",
    nrow(speech_lengths)
  )
)
[1] "Number of speeches: 17"
# Verify that the token count matches the dataset
print(
  paste(
    "Total word tokens:",
    sum(speech_lengths$total_tokens)
  )
)
[1] "Total word tokens: 33999"

Interpretation: The presidential inaugural addresses were successfully converted into tidy text using unnest_tokens(), resulting in one word per row. Each word remains associated with its original speech, year, and president.

I also calculated the total number of word tokens for each address. This step is important because inaugural speeches vary in length, and comparing raw sentiment counts alone could be misleading. By retaining the speech lengths, I will be able to calculate normalized sentiment scores per 1,000 tokens in the next stage of the analysis.

Bing Sentiment Analysis of Presidential Inaugural Addresses

In this section, I will apply the Bing sentiment lexicon to U.S. presidential inaugural addresses. This extends the original Chapter 2 analysis by using the same sentiment classification method on a different text corpus.

The Bing lexicon identifies words as positive or negative. I will calculate the frequency of each category within every speech and determine the net sentiment by subtracting negative word counts from positive word counts.

Because the inaugural addresses differ in length, I will normalize the sentiment scores per 1,000 word tokens to make comparisons across speeches more meaningful.

# Match speech words with Bing sentiment classifications
bing_speeches <- speech_words %>%
  inner_join(
    get_sentiments("bing"),
    by = "word"
  ) %>%
  count(
    speech,
    year,
    president,
    sentiment
  ) %>%
  pivot_wider(
    names_from = sentiment,
    values_from = n,
    values_fill = 0
  ) %>%
  right_join(
    speech_lengths,
    by = c("speech", "year", "president")
  ) %>%
  mutate(
    positive = replace_na(positive, 0L),
    negative = replace_na(negative, 0L),
    net_sentiment = positive - negative,
    bing_per_1000 =
      net_sentiment / total_tokens * 1000
  ) %>%
  arrange(desc(bing_per_1000))

# Display results from highest to lowest
print(
  bing_speeches %>%
    select(
      year,
      president,
      positive,
      negative,
      total_tokens,
      net_sentiment,
      bing_per_1000
    ) %>%
    mutate(
      bing_per_1000 = round(bing_per_1000, 2)
    )
)
# A tibble: 17 × 7
    year president positive negative total_tokens net_sentiment bing_per_1000
   <int> <chr>        <int>    <int>        <int>         <int>         <dbl>
 1  1989 Bush           157       35         2318           122          52.6
 2  1977 Carter          96       36         1226            60          48.9
 3  1973 Nixon          120       41         1805            79          43.8
 4  2001 Bush           110       49         1585            61          38.5
 5  2017 Trump           80       26         1446            54          37.3
 6  2025 Trump          184       78         2918           106          36.3
 7  1997 Clinton        123       47         2158            76          35.2
 8  2005 Bush           145       74         2073            71          34.2
 9  1985 Reagan         162       75         2571            87          33.8
10  1969 Nixon          123       59         2124            64          30.1
11  2013 Obama          104       50         2106            54          25.6
12  1993 Clinton         76       35         1600            41          25.6
13  1965 Johnson         82       45         1490            37          24.8
14  1981 Reagan         117       70         2436            47          19.3
15  2021 Biden          122       77         2377            45          18.9
16  2009 Obama          124       81         2399            43          17.9
17  1961 Kennedy         76       61         1367            15          11.0

Visualizing Bing Sentiment Across Presidential Inaugural Addresses

ggplot(
  bing_speeches,
  aes(
    x = reorder(speech, bing_per_1000),
    y = bing_per_1000
  )
) +
  geom_col(
    fill = "steelblue",
    width = 0.75
  ) +
  coord_flip() +
  labs(
    title = "Bing Sentiment Across Presidential Inaugural Addresses",
    subtitle = "Net sentiment normalized per 1,000 word tokens",
    x = "Presidential Inaugural Address",
    y = "Net Bing Sentiment per 1,000 Tokens"
  ) +
  theme_minimal()

Interpretation: The Bing sentiment analysis allowed me to compare positive and negative language across U.S. presidential inaugural addresses. By calculating net sentiment and normalizing the scores per 1,000 word tokens, I was able to account for differences in speech length.

The visualization shows how sentiment varies across the speeches, with higher scores representing a greater frequency of positive relative to negative dictionary matches. Unlike the original Jane Austen analysis, which examined sentiment changes throughout fictional narratives, this extension compares overall sentiment across individual political speeches.

Although the scores provide an useful measure of sentiment-associated word usage, they do not fully capture the context or intended meaning of each speech.

Additional Sentiment Lexicon: Syuzhet

To further extend the original sentiment analysis, I will introduce the Syuzhet sentiment lexicon using the syuzhet R package. Unlike the Bing lexicon, which classifies words as positive or negative, Syuzhet assigns weighted numerical sentiment scores.

I will apply Syuzhet to the same presidential inaugural addresses analyzed in the previous section. By calculating and normalizing the sentiment scores per 1,000 tokens, I will be able to compare how different sentiment dictionaries evaluate the same collection of speeches.

# Create a lookup table of unique words and sentiment values
syuzhet_words <- speech_words %>%
  distinct(word) %>%
  mutate(
    value = syuzhet::get_sentiment(
      word,
      method = "syuzhet"
    )
  )

# Calculate the weighted sentiment for each speech
syuzhet_speeches <- speech_words %>%
  left_join(
    syuzhet_words,
    by = "word"
  ) %>%
  group_by(
    speech,
    year,
    president
  ) %>%
  summarise(
    total_score = sum(value, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  left_join(
    speech_lengths,
    by = c("speech", "year", "president")
  ) %>%
  mutate(
    syuzhet_per_1000 =
      total_score / total_tokens * 1000
  ) %>%
  arrange(desc(syuzhet_per_1000))

# Display the results
print(
  syuzhet_speeches %>%
    select(
      year,
      president,
      total_tokens,
      total_score,
      syuzhet_per_1000
    ) %>%
    mutate(
      total_score = round(total_score, 2),
      syuzhet_per_1000 = round(syuzhet_per_1000, 2)
    )
)
# A tibble: 17 × 5
    year president total_tokens total_score syuzhet_per_1000
   <int> <chr>            <int>       <dbl>            <dbl>
 1  1977 Carter            1226        73.9             60.3
 2  1989 Bush              2318       123.              52.9
 3  1997 Clinton           2158       110.              51.1
 4  1973 Nixon             1805        87.5             48.5
 5  2017 Trump             1446        66.1             45.7
 6  2001 Bush              1585        72.4             45.7
 7  2005 Bush              2073        93.2             45.0
 8  1985 Reagan            2571       105.              40.7
 9  1969 Nixon             2124        76               35.8
10  1993 Clinton           1600        54.0             33.7
11  1981 Reagan            2436        80.6             33.1
12  2009 Obama             2399        76.3             31.8
13  2013 Obama             2106        66.8             31.7
14  2025 Trump             2918        87.4             29.9
15  1965 Johnson           1490        43.0             28.8
16  2021 Biden             2377        52.8             22.2
17  1961 Kennedy           1367        29.1             21.3
# Verify that all speeches are represented
print(
  paste(
    "Number of speeches analyzed:",
    nrow(syuzhet_speeches)
  )
)
[1] "Number of speeches analyzed: 17"

Visualizing Syuzhet Sentiment Across Presidential Inaugural Addresses

ggplot(
  syuzhet_speeches,
  aes(
    x = reorder(speech, syuzhet_per_1000),
    y = syuzhet_per_1000
  )
) +
  geom_col(
    fill = "darkorange",
    width = 0.75
  ) +
  coord_flip() +
  labs(
    title = "Syuzhet Sentiment Across Inaugural Addresses",
    subtitle = "Weighted sentiment per 1,000 word tokens",
    x = "Presidential Inaugural Address",
    y = "Syuzhet Score per 1,000 Tokens"
  ) +
  theme_minimal()

Interpretation: The Syuzhet sentiment analysis provided an additional method for evaluating positive and negative language across presidential inaugural addresses. Unlike Bing, which calculates sentiment using positive and negative word counts, Syuzhet assigns weighted numerical values to individual words.

After normalizing the scores per 1,000 tokens, I was able to compare the relative sentiment of speeches of different lengths. The resulting visualization provides an alternative perspective on sentiment patterns within the inaugural-address corpus.

Because the two lexicons use different scoring systems, their numerical scores should not be interpreted as directly equivalent. The next stage will compare the rankings produced by Bing and Syuzhet to evaluate the consistency of their results.

Comparison of Bing and Syuzhet Sentiment Results

To evaluate how sentiment lexicon selection influences the results, I will compare the Bing and Syuzhet sentiment scores calculated for the presidential inaugural addresses.

Both methods were applied to the same speeches, and their scores were normalized per 1,000 word tokens. However, Bing calculates sentiment using positive and negative word frequencies, while Syuzhet assigns weighted numerical values.

I will combine the results, create a comparative visualization, and calculate Spearman rank correlation to determine whether the two lexicons produce similar rankings across speeches.

sentiment_comparison <- bing_speeches %>%
  select(
    speech,
    year,
    president,
    bing_per_1000
  ) %>%
  inner_join(
    syuzhet_speeches %>%
      select(
        speech,
        syuzhet_per_1000
      ),
    by = "speech"
  ) %>%
  arrange(year)

# Display a formatted comparison table
sentiment_comparison %>%
  mutate(
    bing_per_1000 = round(bing_per_1000, 2),
    syuzhet_per_1000 = round(syuzhet_per_1000, 2)
  ) %>%
  knitr::kable(
    caption = "Comparison of Bing and Syuzhet Sentiment Scores"
  )
Comparison of Bing and Syuzhet Sentiment Scores
speech year president bing_per_1000 syuzhet_per_1000
1961 Kennedy 1961 Kennedy 10.97 21.29
1965 Johnson 1965 Johnson 24.83 28.83
1969 Nixon 1969 Nixon 30.13 35.78
1973 Nixon 1973 Nixon 43.77 48.48
1977 Carter 1977 Carter 48.94 60.28
1981 Reagan 1981 Reagan 19.29 33.09
1985 Reagan 1985 Reagan 33.84 40.74
1989 Bush 1989 Bush 52.63 52.87
1993 Clinton 1993 Clinton 25.62 33.72
1997 Clinton 1997 Clinton 35.22 51.14
2001 Bush 2001 Bush 38.49 45.68
2005 Bush 2005 Bush 34.25 44.96
2009 Obama 2009 Obama 17.92 31.80
2013 Obama 2013 Obama 25.64 31.74
2017 Trump 2017 Trump 37.34 45.71
2021 Biden 2021 Biden 18.93 22.23
2025 Trump 2025 Trump 36.33 29.93
# Verify that all speeches are represented
print(
  paste(
    "Speeches in comparison:",
    nrow(sentiment_comparison)
  )
)
[1] "Speeches in comparison: 17"

Visualizing the Relationship Between Bing and Syuzhet

ggplot(
  sentiment_comparison,
  aes(
    x = bing_per_1000,
    y = syuzhet_per_1000
  )
) +
  geom_point(
    color = "steelblue",
    size = 3,
    alpha = 0.8
  ) +
  geom_smooth(
    method = "lm",
    se = FALSE,
    color = "darkorange"
  ) +
  labs(
    title = "Bing vs. Syuzhet Sentiment Scores",
    subtitle = "U.S. Presidential Inaugural Addresses, 1961 Onward",
    x = "Bing Net Sentiment per 1,000 Tokens",
    y = "Syuzhet Weighted Sentiment per 1,000 Tokens"
  ) +
  theme_minimal()

Spearman Rank Correlation

spearman_result <- cor(
  sentiment_comparison$bing_per_1000,
  sentiment_comparison$syuzhet_per_1000,
  method = "spearman",
  use = "complete.obs"
)

# Display the correlation coefficient
print(
  paste(
    "Spearman correlation:",
    round(spearman_result, 3)
  )
)
[1] "Spearman correlation: 0.843"

Highest-Scoring Inaugural Addresses

# Highest normalized Bing sentiment
top_bing <- sentiment_comparison %>%
  slice_max(
    order_by = bing_per_1000,
    n = 1,
    with_ties = TRUE
  )

# Highest normalized Syuzhet sentiment
top_syuzhet <- sentiment_comparison %>%
  slice_max(
    order_by = syuzhet_per_1000,
    n = 1,
    with_ties = TRUE
  )

# Display the results
print("Highest Bing Sentiment:")
[1] "Highest Bing Sentiment:"
print(top_bing)
# A tibble: 1 × 5
  speech     year president bing_per_1000 syuzhet_per_1000
  <chr>     <int> <chr>             <dbl>            <dbl>
1 1989 Bush  1989 Bush               52.6             52.9
print("Highest Syuzhet Sentiment:")
[1] "Highest Syuzhet Sentiment:"
print(top_syuzhet)
# A tibble: 1 × 5
  speech       year president bing_per_1000 syuzhet_per_1000
  <chr>       <int> <chr>             <dbl>            <dbl>
1 1977 Carter  1977 Carter             48.9             60.3

Interpretation: Comparing Bing and Syuzhet allowed me to examine how two different sentiment-scoring methods evaluate the same presidential inaugural addresses. Although both methods were normalized per 1,000 tokens, they use different approaches to assigning sentiment values.

The comparison scatterplot provides a visual representation of the relationship between the two methods, while Spearman rank correlation measures how similarly the speeches are ranked.

These results are important because they demonstrate that sentiment analysis can be influenced by the choice of lexicon. The findings will be discussed alongside the original Jane Austen example to evaluate how sentiment-analysis methods behave across different text corpora.

Conclusion

By analyzing Jane Austen’s novels with the NRC, Bing, and AFINN lexicons, I learned how to tokenize text, calculate sentiment scores, and visualize changes in positive and negative language throughout a narrative.

For my extended analysis, I applied the same text-mining techniques to U.S. presidential inaugural addresses and introduced Syuzhet as an additional sentiment lexicon. Comparing Bing and Syuzhet allowed me to examine how different scoring methods evaluate the same text and whether they produce similar sentiment rankings.

This analysis highlights the importance of selecting appropriate sentiment lexicons and interpreting results within the context of the documents being studied. Although lexicon-based sentiment analysis provides a systematic way to identify language patterns, it does not fully capture the complexity of human language, including context, negation, and intended meaning.