STA 279 Lab 3

Complete all Questions.

The Goal

In our last class, we talked about TF-IDF, which is a tool we can use if we want to figure out what words characterize certain types of text. Today, we are going to see an application of how we can use this in practice.

The Data Set

The Federalist papers are a famous series of 97 articles written in the 1700s in the early days of the United States. They debated and proposed ideas for the new government. Three famous authors of these papers were Alexander Hamilton, James Madison, and John Jay.

Most of the Federalist papers (85 of them) were known to have been written by either Hamilton, Madison, or Jay, but for 12 papers, the papers were submitted anonymously. After Madison’s death, Hamilton declared that he had written the articles. Since then, many scholars have worked to determine which of the three authors had written the anonymous papers using text analysis.

Our goal for today is to see if we can use the text analysis skills we have learned so far to determine who wrote the mystery papers.

To read in the data on the \(n = 97\) Federalist papers, use the following code:

# Load the data
Federalist <- read.csv("https://www.dropbox.com/scl/fi/lb7n6eoc094epytqqkuc2/FederalistAll.csv?rlkey=nhrr5m90iyy7bueggffslxm0e&st=5jvps3wz&dl=1")

# Convert to a data frame
Federalist <- data.frame(Federalist)

# Make sure author is treated as categorical
Federalist[, "author"] <- as.factor(Federalist[, "author"])

The columns are:

  • paper: the number given to the paper; think of this like an identifier for the article.
  • text: the text of the entire paper.
  • author: either Hamilton or Madison or Jay or mystery (for the 12 papers with unknown author)

Once you have loaded the data, load the packages you will need for this lab:

library(tidytext)
library(tidyr)
library(dplyr)
library(ggplot2)
library(tm)
library(naivebayes)

# NEW 
library(stringr)
library(forcats)

Feature Engineering

Our goal for today is to determine which of the three authors (Hamilton, Madison, or Jay) likely wrote each of the mystery papers. This means we need to figure out which words characterize or distinguish each author’s writing, and then see if those words are present in the mystery papers!

It may seem natural to use frequency as a way to determine which words we should use as features. In other words, we could the number of times each word occurs in a text (in this case a paper) and we choose the words that are most frequent for each author as our features. However, it turns out that this is not the best approach. Let’s see why.

Suppose we want to find the top 5 words in Madison’s articles after removing stop words. I’ve given you a skeleton of the code you need below:

top5_Madison <- Federalist |>
  filter(author == ...) |>
  unnest_tokens(word,... ) |>
  anti_join(stop_words, by = join_by(word)) |>
  count( ...   ) |>
  slice_max(n , n = ... )

Question 1

Complete the code above by filling in the … and annotate the code.

Note: You will note I used anti_join(stop_words, by = join_by(word)) rather than anti_join(stop_words). The codes do exactly the same thing. The only difference is that some of you have noticed you get annoying warnings about Join by: by = join_by(word) when you run anti_join(stop_words). The adaptation I have in the code above just removes that warning.

Question 2

Create and show a plot to show the top 5 words in Madison’s texts. Make sure your plot is well formatted and well labelled.

Hint: Here is a skeleton code to get you started:

ggplot( ... , aes(n, fct_reorder( ... , ... ) )) +
  geom_...() +
  labs( x = ... , y = ..., title = ...)

This shows us potential words to use as features for Madison, but we also need words for Hamilton and Jay and the mystery author. We could repeat the process 3 times, but it turns out we actually don’t have to. As we saw in class, we can use grouping in R to find the top 5 words for each author in one code, without having to repeat! A skeleton code for this is included below.

top5_all <- Federalist |>
  unnest_tokens(word,... ) |>
  anti_join(stop_words, by = join_by(word)) |>
  group_by( ... ) |>
  count( ... ) |>
  slice_max(n , n = ... )

Question 3

Complete the code above. As the answer to this question, state the top most frequent word in (a) Hamilton’s papers and (b) Jay’s papers and (c) the mystery author’s papers..

We can also create a plot to compare the authors using the following code.

ggplot( top5_all , aes(n, reorder_within( word , n, author),fill =author)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~author,ncol = 2, scales = "free_y") +
  scale_y_reordered() + 
  labs( x = ... , y = ..., title = ...)

You will note two new things in this plotting code.

  • reorder_within: We use fct_reorder when we want to make sure that the words appear in order on our bar graph. However, because we now have three different authors, we actually need to make sure that the words appear in order for all authors. reorder_within allows us to include author in our ordering so that happens!

  • facet_wrap(): This is what allows us to make one plot for each group. In this case, facet_wrap(~author) allows to make one plot for each author.

Question 4

Finish the code above to create the plot to compare the top 5 words in Madison’s, Hamilton’s, Jay’s, and the mystery author’s texts. Show the plot.

Question 5

Are there any words that show up in the top 5 list for more than one author? In other words, are there words that show up in more than one of the plots in Question 4?

The goal is for us to find words we can use as features in a model to predict author. For this purpose, it is helpful if we can find words that help us separate the writing of the three authors, meaning finding words that tend to be used commonly by one author but not the others. A word that is used often by multiple authors is not useful in determining among these authors for prediction.

All of this means that frequency alone is not enough. Instead, we need something that will help us find words that are commonly used by an author, but not commonly used by other authors. This is exactly the set up that motivates TF-IDF.

TF-IDF

The TF-IDF is a number that is high if a word is commonly used by an author but not commonly used by other authors. In other words, words with high TF-IDF scores fulfill the properties that we want in features for our Naive Bayes model to predict author.

As we learned in class, the TF-IDF score has two components: the TF score and IDF score.

Question 6

What does the TF score measure? In other words, briefly explain what having a high TF score tells us about a word.

Recall that the TF of word \(i\) in text group \(j\) is defined as:

\[TF_{i,j} = \frac{\text{Number of times word i appears in group j}}{\text{Total Number of words in group j}}\]

Question 7

How many groups of text are we working with today?

Note: This should not be a large number, and the number of groups is always the number of things we are trying to tell apart in the data set!

Question 8

The word “will” occurs 703 times in Hamilton’s papers, and there are 114321 words in total in Hamilton’s papers. Based on this, state and interpret the TF of the word “will” in Hamilton’s papers.

Once we have the TF score, the next step is to compute the IDF score.

Question 9

What does the IDF score measure? In other words, briefly explain what having a high IDF score tells us about a word.

Question 10

For these data, there are only four possible values for the IDF. State what those numeric values are and in what situations we would use each. In other words, in what situation would we get each of the 4 different values of the IDF?

Question 11

In addition to appearing 703 times in Hamilton’s papers, the word “will” occurs 247 times in Madison’s papers, 105 times in Jay’s papers, and 228 times in the mystery author’s papers. Based on this, what is the IDF of “will”?

The TF-IDF is computed by multiplying the TF and the IDF together.

Question 12

What is the TF-IDF for the word “will” in Hamilton’s papers? Based on this, is “will” going to be a useful word in distinguishing Hamilton’s papers from Madison’s or Jay’s? Explain.

Question 13

The word “representatives” occurs 59 times in Hamilton’s papers, 141 times in Madison’s papers, 112 times in the mystery author’s papers, and not at all in Jay’s papers. There are 114321 words in total in Hamilton’s papers. What is the TF-IDF of “representatives” in Hamilton’s papers?

Computing TF-IDF in R

Now that we know the TF-IDF will be useful for finding words we can use as features in our model, and we have reviewed how the TF-IDF is computed, the next step is to compute the TF-IDF for every word across the 85 Federalist papers. We then choose the words with the highest TF-IDF for each author as our features.

Given that there are over 14000 unique words in the papers, this would take a while. Luckily, R has one nice function that we can use to get the TF-IDF score for all words at once. .

tfidf_all <- Federalist |>

  unnest_tokens(word, text)|>
  
  group_by(author) |>
  
  count(word) |>
  
  # NEW!! Create the TF IDF Score
  bind_tf_idf(word, author, n)

Question 14

Annotate the code above!

Hint: I know the last line of code is new. To see what it does, I recommend running the code with and without this final line so you can see what it does! The structure of the bind_tf_idf code requires (1) each word, (2) the groups, and (3) the number of times each word appears in each group (n).

When you are done running the code above, you have a data set called tfidf_all with 14505 rows and 6 columns. Each row is a unique word in the Federalist papers, and for each author we are given (1) the frequency of that word, (2) the TF score, (3) the IDF score, and (4) the TF-IDF score of that word.

Question 15

Open up tfidf_all. What do you notice is unusual about the first few words that are listed?

This data set, like many in text, requires some cleaning before we proceed. To handle the issue in Question 15, we add a line of a code we have seen before:

tfidf_all <- Federalist |>

  unnest_tokens(word, text)|>
  
  filter(!grepl('[0-9]', word)) |>
  
  group_by(author) |>
  
  count(word) |>
  
  bind_tf_idf(word, author, n)

Question 16

Open up tfidf_all again. What do you notice is unusual now about the first few words that are listed?

This is a new one for us - weird punctuation. We have to deal with this a lot in text data, but luckily it can be handled with one additional line of code:

tfidf_all <- Federalist |>

  # Tokenize 
  unnest_tokens(word, text)|>
  
  # Remove the numbers 
  filter(!grepl('[0-9]', word)) |>
  
  # Remove the punctuation 
  filter(!grepl('[[:punct:]]', word)) |>
  
  # Group by author
  group_by(author) |>
  
  # Count the number of times each word appears
  # for each author
  count(word) |>
  
  # Compute the tf-idf 
  bind_tf_idf(word, author, n)

Question 17

Open up tfidf_all again. Do the first few words listed look okay now, meaning they are words without numbers or symbols?

Most of the time when we have cleaning issues in text, we discover them just as we did today, meaning during the course of an analysis. Text can be very long, and it is difficult to anticipate all the cleaning that might need to be done. This means our job is to look at the data as we go and keep our eyes out for anything that might look odd so we can handle it. There is no short cut for this - we just have to be vigilant.

The Top Few Words

At this point, we have the TF-IDF score for every unique word in the Federalist papers. However, we do not want to use every single word as a feature. Instead, we want to choose words with high TF-IDF scores. These are the words that help distinguish the different authors.

If we only want the to find the top 5 words in terms of TF-IDF for each author, we use a very similar code as we did for counting the top 5 words in terms of frequency:

tfidf_top5 <- Federalist |>

  unnest_tokens(word, text)|>
  
  filter(!grepl('[0-9]', word)) |>
  
  filter(!grepl('[[:punct:]]', word)) |>
  
  group_by(author) |>
  
  count(word) |>

  bind_tf_idf(word, author, n) |>
  
  slice_max( tf_idf, n = 5)

The only change we have made to this code is in the slice_max part. Usually, we have slice_max( n , n = 5). This is because the column n in the data set holds the counts, meaning the number of times each word occurs in a group. If we want the top 5 words in terms of count, we want the top 5 (n=5) in the column n. However, now we want the top 10 words in terms of TF-IDF. If we want the top 5 words in terms of TF-IDF, we want the top 5 (n=5) in the column tf_idf.

Question 18

Look at the top words 5 in terms of TF-IDF for Madison. There should be one word in that is surprising. What is it?

Another cleaning issue!! Each paper has the author’s name in it, listed as the author. The mystery papers were written anonymously, so the author’s names are not useful features.

Question 19

So far in our code, we have seem that filter(!grepl'something',word) means to remove all instances of 'something' from the word column in the data set.

Right now, we want to remove madison, jay, and hamilton from the words in our data set. Adapt the code above Question 18 to do this, and state the top 10 words for each author in terms of TF=IDF.

Wow this code is getting long!! As a note, you can store pieces of this code as you go, and then you don’t have to run the whole long thing every time. However, because we keep tweaking the code as we find new cleaning issues, for me it’s sometimes easier to just work with the whole code so I know where to make adjustments as needed.

Question 20

After all our cleaning steps, create a plot showing the top 5 words for each author in terms of the TF-IDF using tfidf_top5 above.

Hint: You can use the code you already used to create a plot in Questions 3 and 4 for this! The only difference is that no longer want words with the highest count (n); we want words with the highest tf_idf. Let me know if you get stuck!

Question 21

Recall that Hamilton claimed to have written all 12 test papers. Based on your current results, our historian client wants us to say whether or not we think this claim is true. State and justify your conclusion for your client.

So far, we have only looked at the top 5 words. We could do this for the top 10, 15, 20, 50, and so on! At that point, though, it becomes tedious to look down the list. SO, we are going to have R count for us! Copy and paste the following into an R chunk and press play.

check_tfidf_match <- function( howmany , whichauthor){
  # Find the tf=idf
  tfidf_top <- Federalist |>

  unnest_tokens(word, text)|>
  
  filter(!grepl('[0-9]', word)) |>
  
  filter(!grepl('[[:punct:]]', word)) |>
  
  filter(!grepl('madison', word)) |>
  
  filter(!grepl('hamilton', word)) |>
  
  filter(!grepl('jay', word)) |>
  
  group_by(author) |>
  
  count(word) |>

  bind_tf_idf(word, author, n) |>
  
  slice_max( tf_idf, n = howmany)
  
  whichauthor  = subset(tfidf_top, author == whichauthor)
  mystery  = subset(tfidf_top, author == "mystery")
  sum(whichauthor$word %in% mystery$word)/nrow(mystery)
}

I just wrote a function for you that (1) find the top howmany words in terms of TF-IDF for each author and then (2) tells you what proportion of the top howmany TF-IDF words for the mystery author are also in the list for whichauthor. For example,

check_tfidf_match( 10, "Madison")

returns .3, means 30% of the mystery’s authors top 10 words in terms of TF-IDF are also on Madison’s list of top 10 words.

Question 22

Use the check_tfidf_match function to find what percent of words on the mystery author’s list of top 100 words in terms of TF-IDF are also on (a) Hamilton’s, (b) Jay’s, and (c) Madison’s top 100 TF-IDF lists.

Question 23

Recall that Hamilton claimed to have written all 12 test papers. Based on your current results, our historian client wants us to say whether or not we think this claim is true. State and justify your conclusion for your client.

References

Data

The data come from https://github.com/nicholasjhorton/FederalistPapers, the GitHub repository of Dr. Nicholas J Horton. Citation: Horton, Nicholas J. Federalist Papers, Retrieved July 20, 2024 from https://github.com/nicholasjhorton/FederalistPapers.

Code

The code was adapted from Chapter 3 of “Text Mining with R: A Tidy Approach”, written by Julia Silge and David Robinson. The book was last built on 2024-06-20.

Activity

Creative Commons License
This work was created by Nicole Dalzell is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. Last updated 2026 August 22.