STA 279 Lab 8
Complete all Questions.
Goal
Today we are going to practice linear discriminant analysis, or LDA.
We are going to work with our Federalist data from previous labs. As a reminder, in this data set we have three authors: Jay, Madison, and Hamilton. To load the data, put the following into a chunk in Markdown and press play.
train <- read.csv("https://www.dropbox.com/scl/fi/9q980le0xizsvsmzeknmt/train_Federalist.csv?rlkey=vem4v5yft4d5ufbkvbwd6kn3g&st=bycfifdu&dl=1")
train <- train[,1:80]Today, the data set has 85 rows and 80 columns. There is a response
variable, author, and 79 features from LIWC. Our goal is to
describe how these 79 features can be used to tell the papers of the
different authors apart.
Starting Small
Before we use all 79 features, let’s start with just a few. This will help us dig a little deeper into what LDA is doing. We are going to start with just 5 features.
To create a data set called trainSmall with just 5
features, run the following code:
Question 1
Which features are we currently using?
The goal of LDA is to take these 5 features and use them to create 2 new features called discriminants. Each discriminant will be a weighted mixture of the 5 original features. For example, linear discriminant 1 (LD1) will be:
\[LD1_i = \beta_1 x_{1i} + \beta_2 x_{2i} + \beta_3 x_{3i} + \beta_4 x_{4i} + \beta_5 x_{5i}\]
Here, \(x_{1i}\) is the value of the first feature for row \(i\), and so on.
Question 2
Suppose for Row 4 in the data, \(x_{1} = 2\), \(x_{2} = .5\), \(x_{3} = -.2\), \(x_{4} = -2.7\) and \(x_{5} = .02\). If all the \(\beta\) terms are 1, what is \(LD1_{4}\)?
Using the same set up as LD1, linear discriminant 2 (LD2) will be:
\[LD2_i = \gamma_1 x_{1i} + \gamma_2 x_{2i} + \gamma_3 x_{3i} + \gamma_4 x_{4i} + \gamma_5 x_{5i}\]
Question 3
Again suppose Row 4 in the data has \(x_{1} = 2\), \(x_{2} = .5\), \(x_{3} = -.2\), \(x_{4} = -2.7\) and \(x_{5} = .02\). If all the \(\gamma\) terms are -1, what is \(LD1_{4}\)?
As we can see from Questions 2 and 3, the value of LD1 and LD2 can be different even with the same values of the features! It all depends on the values of the coefficients that are used.
This means that in order to use LDA, we have to estimate the coefficients, meaning the \(\beta\) and \(\gamma\) terms. If this sounds a lot like what we do in regression models, it should!! The difference in LDA versus other regression models is how we choose the coefficients.
In LDA, we choose the coefficients to:
- Make the LD1 and LD2 scores of the papers by the same author be close to one another
- Make the average LD1 and LD2 score of the papers of different authors be far from one another.
In other words, when we convert from the features to LD1 and LD2, we want to do this in such a way that papers by the same authors are clustered together, and papers by different authors are kept far apart.
The math is to find the coefficients with these properties is beyond the scope of this course, but luckily R can help us do it!
Coding LDA
We start by loading the libraries we need.
# Load the libraries we need
require("tidyverse")
require("caret")
require("MASS")
require("ggthemes")
require("rsample")
library(dplyr)The next step is something we haven’t done before in this course. We have to standardize the features. This basically means we convert each feature from the raw data form to a z-score. Recall that a z-score is:
\[z-score_i = \frac{x_i - \text{Mean of X}}{\text{Standard Deviation of X}}\]
This means that when you convert to a z-score, we take the feature from whatever scale it is originally on and convert it to “how many standard deviations is this value of this feature for this paper \(i\) above or below the mean for the feature?”
For instance, the first paper has 1628 words, so \(x_{1i} = 1628\). The average number of
words in all papers can be found using mean(trainSmall$WC),
and the standard deviation can be found using
sd(trainSmall$WC).
Question 4
What is the standardized value of \(x_{1i}\)? In other words, what is \(z_{1i}\)?
This process of converting the features from \(x_i\) to \(z_i\) can be done quickly using R.
# We skip column 1 because it is Y, which is categorical!
# This means the features are in columns 2 -6, so we use 2:6
for(i in 2:6){
trainSmall[,i] <- scale(trainSmall[,i])
trainSmall[,i] <- as.numeric(trainSmall[,i])
}Once we have standardized the features, we estimate the coefficients for LDA using:
When you run the code above, it will look like nothing has happened, but it has! To see the coefficients, run:
Question 5
What is the estimate for \(\beta_2\)?
Question 6
What is the estimate for \(\gamma_5\)?
Question 7
What feature has the most impact (the most weight) in LD1?
Question 8
What feature has the most impact (the most weight) in LD2?
If we were working with regression models, we would start interpreting at this point. With LDA, we don’t do any interpretations yet. First, we need to use our coefficients to assign values of LD1 and LD2 to each row in the data set!
Question 9
The first row in trainSmall has these standardized
features. What is the value of \(LD1_{1}\)?
- WC: -.83
- BigWords: -.38
- Positive Tone: 1.74
- Negative Tine: .97
- Politic: -1.18
Question 10
The value of \(LD2_{1} \approx 1.96\). This means that if we make a plot with axes LD1 and LD2, at which coordinates should the dot for row 1 be placed?
Plotting LDA
So far, we have only found the LD1 and LD2 coordinates for one row of
data, but we can do it for all the rows in trainSmall. Once
we do this, we can create a plot!
trainSmall|>
cbind(predict(lda_mod)$x) |>
ggplot(aes(x = LD1, y = LD2, color = author, shape = author)) +
geom_point(size = 1, alpha = 0.8) +
labs(x = "LD1 (NUMBER%)", y = "LD2 (NUMER%)", color = "author") +
scale_color_colorblind() +
guides(color = guide_legend(override.aes = list(alpha = 1, size = 4))) +
theme_bw() +
theme(legend.title = element_text(face = "bold"),
axis.title = element_text(face = "bold"),
axis.text = element_text(color = "black"))You should notice one odd thing on the plot, and that is that we have
NUMBER% on the x and y axis labels. There should be actual
numbers there!
To get those values, we use
tibble(discriminant = c("LD1", "LD2"),
svd = lda_mod$svd) |>
mutate(sigma_squared = svd^2,
pct_rounded = round(sigma_squared/sum(sigma_squared)*100, 1)) |>
dplyr::select(discriminant, pct_rounded)
Question 11
Adapt the plot to replace NUMBER% with an actual
percent.
Question 12
Right now, which discriminant is able to capture more of the variability (patterns) in the features?
If we look at the plot right now, it’s not very exciting. We expected to see clusters where the papers of each author were grouped together, but that is not what is happening here. Why??
Well, it is because we only used a few features! When we do LDA, one of the strengths is that we can use all of them. Now that we know how the method works, let’s add all the features back in!
LDA with all the features
To use all the features in LDA, we just switch from
trainSmall to train.
# We skip column 1 because it is Y, which is categorical!
# This means the features are in columns 2 -80, so we use 2:80
for(i in 2:80){
train[,i] <- scale(train[,i])
train[,i] <- as.numeric(train[,i])
}
set.seed(279)
lda_mod <- lda(author ~ ., data = train)Question 13
Update your plot from Question 11 to reflect the new LDA, and show the plot.
Question 14
Are the authors more separated than they were when we only used 5 features?
At this point, we are ready to start interpreting. Remember that in LDA, we generally look at the top 10 largest (in absolute value) coefficients in LDA1 and LD2. To see those, we use the code below, but we will need to adapt it!!
features <- list(colnames(train)[-1])
top_features <- data.frame(lda_mod$scaling) |>
rownames_to_column("features") |>
pivot_longer(starts_with("LD"),
names_to = "discriminant",
values_to = "coefficient") |>
group_by(discriminant) |>
arrange(desc(abs(coefficient))) |>
mutate(rank = row_number()) |>
pivot_wider(id_cols = features,
names_from = discriminant,
values_from = c(coefficient, rank)) |>
mutate(association = case_when(
# ADAPT THIS
coefficient_LD1 & coefficient_LD2 ~ "Hamilton",
# ADAPT THIS
coefficient_LD1 & coefficient_LD2 ~ "Madison",
# ADAPT THIS
coefficient_LD1 & coefficient_LD2 ~ "Jay",
TRUE ~ NA
)) |>
filter(rank_LD2 <= 10 | rank_LD1 <= 10) |>
arrange(rank_LD1)
knitr::kable(top_features)Question 15
In the code above, you will see three spaces to
ADAPT THIS code. We are going to fill these in based on the
plot to associate the authors with the features.
Which of the following is the correct adaptation for the Hamilton
part of the code? hint: & means “and”.
coefficient_LD1 < 0 & coefficient_LD2 < 0 ~ "Hamilton"
coefficient_LD1 < 0 & coefficient_LD2 > 0 ~ "Hamilton"
coefficient_LD1 > 0 & coefficient_LD2 < 0 ~ "Hamilton"
coefficient_LD1 > 0 & coefficient_LD2 < 0 ~ "Hamilton"
coefficient_LD2 < 0 ~ "Hamilton"
coefficient_LD2 > 0 ~ "Hamilton"
coefficient_LD1 < 0 ~ "Hamilton"
coefficient_LD1 > 0 ~ "Hamilton"
Question 16
How should we adapt the Madison part of the code?
Question 17
How should we adapt the Jay part of the code?
Question 18
Based on your answers to Question 15 - 17, adapt the code above Question 15 and show the top features.
At this point, we are ready to interpret! It takes more work to get to this point then it does in linear regression, but we can work with a lot more features at once.
Question 19
Describe the traits that define Hamilton’s speeches. Don’t use the LIWC lingo - look it up (google is fine) to see what the terms mean.
Question 20
Describe the traits that define Jay’s speeches. Don’t use the LIWC lingo - look it up (google is fine) to see what the terms mean.
Question 21
Describe the traits that define Madison’s speeches. Don’t use the LIWC lingo - look it up (google is fine) to see what the terms mean.
Prediction
The last thing we can do with LDA is prediction. This is especially important in LDA because there is really no other way to see how the model is performing. We don’t have an R-squared or anything like that, so getting something like a confusion matrix can help us see how well LDA is reflecting the patterns in the data!
Question 22
Suppose I have a new paper, and the LD1 score is 9 and the LD2 score is 9. Which author would you predict wrote the paper? Hint: No math is needed!
We do this for all the papers in the training data, and we can create the following confusion matrix using this code (no adaptation needed).
preds <- factor(predict(lda_mod, newdata = train)$class)
actual <- factor(train$author)
overall_accuracy <- tibble(preds, actual) |>
mutate(match = if_else(preds == actual, TRUE, FALSE)) |>
summarize(overall_accuracy = mean(match) * 100)
conf_matrix <- confusionMatrix(data = preds, reference = actual)
as_tibble(conf_matrix$table) |>
group_by(Reference) |>
mutate(freq = n/sum(n)) |>
ungroup() |>
ggplot(aes(y = Prediction, x = Reference, fill = freq)) +
geom_tile(color = "white") +
geom_text(aes(label = paste0(round(freq * 100, 2),"%\n(",n,")")),
color = "white", size = 3) +
scale_fill_gradient(low = "#9ecae1",high = "#08306b",
labels = scales::percent, limits = c(0, 1)) +
labs(fill = "Percent\n", x = "True Author",
y = "Predicted Author") +
coord_equal() +
theme_linedraw() +
theme(panel.grid.major = element_blank(),
axis.ticks = element_line(color = "black"),
axis.text.x = element_text(angle = 45, hjust = 1),
axis.text = element_text(color = "black"),
axis.title = element_text(color = "black", face = "bold"),
legend.title = element_text(color = "black", face = "bold"),
legend.text = element_text(color = "black"))Question 23
Because we made predictions on the training data, we can use our confusion matrix to assess model fit. In other words, how well does LDA seem to correctly separate the papers of the authors? Explain.