Activity 3.2 - PC concepts

SUBMISSION INSTRUCTIONS

  1. Render to html
  2. Publish your html to Posit Connect
  3. Submit a link to your published solutions

Question 1

The data set we will analyze for this question are on the 10 events in the Men’s 2024 Olympic Decathlon in Paris.

decathlon <- read.csv('Data/mens_decathlon_paris2024.csv')
head(decathlon)
         Athlete  Medal      Nation Overall X100m LongJump ShotPut HighJump
1   Markus Rooth   Gold      Norway    8796 10.71     7.80   15.25     1.99
2 Leo Neugebauer Silver     Germany    8748 10.67     7.98   16.55     2.05
3  Lindon Victor Bronze     Grenada    8711 10.56     7.48   15.71     2.02
4    Sven Roosen   None Netherlands    8607 10.52     7.56   15.10     1.87
5  Janek Õiglane   None     Estonia    8572 10.89     7.25   14.58     1.99
6   Johannes Erm   None     Estonia    8569 10.64     7.66   14.61     2.08
  X400m X110mHurdle Discus PoleVault Javelin X1500m
1 47.69       14.25  49.80       5.3   66.87  279.6
2 47.70       14.51  53.33       5.0   56.64  284.7
3 47.84       14.62  53.91       4.9   68.22  283.5
4 46.40       13.99  46.88       4.7   63.72  258.5
5 48.02       14.45  43.49       5.3   71.89  265.6
6 47.19       14.35  46.29       4.6   59.58  259.7

For the purposes of this question, assume we have 10-dimensional data - that is, ignore the Overall column.

A)

Explain why we need to scale this data set before performing PCA.

The 10 events are measured in different units and on very different scales, from times like the 100m and 1500m to distances like the high jump and javelin. PCA maximizes explained variance, so without scaling, the events with large values and spreads would dominate the PCs because of their units. Scaling gives every event mean 0 and SD 1, so each one contributes equally.

B)

Use svd() to find the first 2 principal component scores and their loadings. Full credit will only be granted if you use the svd() ingredients u, d, and v. What percent of the overall variability do the first two PCs explain?

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
# Step 1: mean-center and scale (numeric event columns only, no Overall)
decathlon_scaled <- decathlon %>% 
  dplyr::select(where(is.numeric), -Overall) %>% 
  scale()

# Step 2: SVD
decathlon_svd <- svd(decathlon_scaled)
U <- decathlon_svd$u
D <- diag(decathlon_svd$d)
V <- decathlon_svd$v

# Step 3: first 2 scores
first2scores <- U[, 1:2] %*% D[1:2, 1:2]
head(first2scores)
           [,1]        [,2]
[1,] -0.9696335  0.90753969
[2,] -2.1338950  2.24014162
[3,] -0.7323012  2.05468273
[4,] -1.4369409 -0.05634227
[5,]  1.0587944 -0.11932168
[6,] -1.1274914 -0.28469476
# First 2 loadings
V[, 1:2]
              [,1]        [,2]
 [1,]  0.446612410  0.01678592
 [2,] -0.493037935 -0.06565628
 [3,] -0.309431542  0.53134100
 [4,] -0.150386291 -0.05665757
 [5,]  0.485973869  0.14847674
 [6,]  0.345270647  0.31685991
 [7,] -0.139145881  0.59570522
 [8,]  0.019829831  0.48083261
 [9,]  0.252821246  0.03137029
[10,]  0.005588625 -0.01948870
# Percent of variability explained (total = 10)
all_PCs <- U %*% D

pct_var <- (all_PCs
            %>% data.frame()
            %>% summarize(across(everything(), var))
            %>% mutate(across(everything(), \(x) x/10))
)
pct_var %>% round(4)
      X1     X2     X3     X4     X5     X6     X7    X8     X9   X10
1 0.2913 0.1928 0.1465 0.1404 0.0952 0.0554 0.0481 0.014 0.0122 0.004
sum(pct_var[1:2])   
[1] 0.48415

48.415% is the overall variabliity that the first two PCS explain.

C)

Find and print the loadings. Based on the loadings alone, if the first two PCs are plotted in a 2D plane as shown below, which of the four quadrants will the medalists be in? Explain your reasoning.

loadings <- V[, 1:2]
rownames(loadings) <- colnames(decathlon_scaled)
colnames(loadings) <- c("PC1", "PC2")
round(loadings, 3)
               PC1    PC2
X100m        0.447  0.017
LongJump    -0.493 -0.066
ShotPut     -0.309  0.531
HighJump    -0.150 -0.057
X400m        0.486  0.148
X110mHurdle  0.345  0.317
Discus      -0.139  0.596
PoleVault    0.020  0.481
Javelin      0.253  0.031
X1500m       0.006 -0.019
# order: 100m, LJ, SP, HJ, 400m, 110H, DT, PV, JT, 1500m
better_dir <- c(-1, 1, 1, 1, -1, -1, 1, 1, 1, -1)

colSums(loadings * better_dir)
      PC1       PC2 
-2.102796  1.054301 

Quadrant II because PC1s loadings are running events and field events are opposite, and weighting the loadings this way gives −2.10, so better athletes got lower PC1 scores. The same weighting gives +1.05 for PC2, so better performance makes PC2 higher, putting the medalists at negative PC1 and positive PC2.

D)

Add the PCs to the decathlon data set and create a scatterplot of these PCs, with the points labeled by the athletes’ names. Color-code the points on whether or not the athlete was a medalist. Use the ggrepel package for better labeling. Verify that your intuition from C) is correct.

library(ggrepel)

# Name column = first text column in the data
name_col <- names(decathlon)[sapply(decathlon, is.character)][1]

decathlon <- decathlon %>% 
  mutate(PC1 = first2scores[, 1],
         PC2 = first2scores[, 2],
         Medalist = if_else(rank(-Overall, ties.method = "min") <= 3,
                            "Medalist", "Non-Medalist"))

pc_plot <- ggplot(decathlon, aes(x = PC1, y = PC2)) + 
  geom_hline(yintercept = 0, color = "gray60") +
  geom_vline(xintercept = 0, color = "gray60") +
  geom_point(aes(color = Medalist), size = 3) + 
  geom_text_repel(aes(label = .data[[name_col]], color = Medalist), 
                  size = 3, max.overlaps = 20, show.legend = FALSE) +
  scale_color_manual(values = c("Medalist" = "darkgoldenrod2",
                                "Non-Medalist" = "gray30")) +
  labs(x = "PC1", y = "PC2", color = NULL,
       title = "Medalists Vs. Non-Medalists, 2024 Olympic Decathlon") +
  theme_classic(base_size = 14)

pc_plot

E)

Canadian Damian Warner won the gold medal in the decathlon in the 2020 Tokyo games. He began the 2024 decathlon but bowed out after three straight missed pole vault attempts.

These are his results in the 10 events in 2020:

warner <- c(10.12, 8.24, 14.8, 2.02, 47.48, 13.46, 48.67, 4.9, 63.44, 271.08)

Would this have won a medal if it had happened in 2024? To answer this, we will compute his PCs with respect to the 2024 athletes and add it to the plot to see where his 2020 gold-medal performance compares to the 2024 athletes. To do this:

  • Find the mean vector from the 2024 athletes. Call it mean_vec_24.
  • Find the standard deviation vector from the 2024 athletes. Call it sd_vec_24.
  • Standardize Warner’s 2020 results with respect to the 2024 athletes: (warner-mean_vec_24)/sd_vec_24
  • Find Warner’s PC coordinates using the 2024 loadings.
  • Add his point to the scatterplot.

Do you think his 2020 performance would have won a medal if it had happened in 2024?

warner <- c(10.12, 8.24, 14.8, 2.02, 47.48, 13.46, 48.67, 4.9, 63.44, 271.08)

# Mean and SD of the 2024 athletes (saved by scale() in 1B)
mean_vec_24 <- attr(decathlon_scaled, "scaled:center")
sd_vec_24   <- attr(decathlon_scaled, "scaled:scale")

warner_scaled <- (warner - mean_vec_24) / sd_vec_24

# Warner's PCs using the 2024 loadings
warner_PCs <- warner_scaled %*% V[, 1:2]
warner_PCs
          [,1]       [,2]
[1,] -3.422699 -0.5092531
warner_df <- data.frame(PC1 = warner_PCs[1, 1], PC2 = warner_PCs[1, 2],
                        label = "Damian Warner (2020)")

pc_plot + 
  geom_point(data = warner_df, aes(x = PC1, y = PC2),
             color = "red", size = 4, shape = 17) +
  geom_label_repel(data = warner_df, aes(x = PC1, y = PC2, label = label),
                   color = "red", size = 3.5)

No because he didnt get into the upper left quadrant and his PC2 score was too low.

Question 2

Below is a screenshot of a conversation between me and chatbot Claude:

After looking at the graphs, I grew skeptical. So I said:

Behold, Claude’s three data sets which I’ve called claudeA, claudeB, and claudeC:

claudeA <- read.csv('Data/claude_dataA.csv')
claudeB <- read.csv('Data/claude_dataB.csv')
claudeC <- read.csv('Data/claude_dataC.csv')

Each data set has an X and a Y column which represent 2-dimensional variables that we need to rotate.

A)

Scale each data set and plot them side-by-side using the patchwork package. Make sure the aspect ratio of each graph is 1 (i.e., make the height and width of each graph equal). At this point, explain why you think I was skeptical. Specifically, do you think the percent variability explained by the first PC of each data set appears to exceed or fall short of the variability I asked it to?

library(patchwork)

claudeA <- read.csv('Data/claude_dataA.csv')
claudeB <- read.csv('Data/claude_dataB.csv')
claudeC <- read.csv('Data/claude_dataC.csv')

claudeA_scaled <- scale(claudeA %>% dplyr::select(X, Y))
claudeB_scaled <- scale(claudeB %>% dplyr::select(X, Y))
claudeC_scaled <- scale(claudeC %>% dplyr::select(X, Y))

plot_scaled <- function(dat, mytitle) {
  ggplot(data.frame(dat)) + 
    geom_point(aes(x = X, y = Y), alpha = 0.6) + 
    xlim(c(-4, 4)) + ylim(c(-4, 4)) +
    labs(x = "Scaled X", y = "Scaled Y", title = mytitle) +
    theme_classic(base_size = 14) +
    theme(aspect.ratio = 1)
}

plot_scaled(claudeA_scaled, "claudeA") + 
  plot_scaled(claudeB_scaled, "claudeB") + 
  plot_scaled(claudeC_scaled, "claudeC")

I think you were skeptical because all three scaled data sets look almost the same. Very thin, tight lines. That shape means PC1 explains nearly all of the variability in each data set.

B)

Use SVD to find the first PC for each data set, and find the actual percent of total variability explained by each PC using aggregation methods.

svdA <- svd(claudeA_scaled)
svdB <- svd(claudeB_scaled)
svdC <- svd(claudeC_scaled)

# PC scores = U D (first column = first PC)
PCs_A <- svdA$u %*% diag(svdA$d)
PCs_B <- svdB$u %*% diag(svdB$d)
PCs_C <- svdC$u %*% diag(svdC$d)

head(PCs_A[, 1]); head(PCs_B[, 1]); head(PCs_C[, 1])
[1] -2.2900182 -1.9202720 -1.9400319 -1.4404504 -1.4602102 -0.9606287
[1] -2.0192225 -1.7047641 -1.8808246 -1.2802303 -1.4562907 -0.8556964
[1] -2.270557 -1.933711 -1.914344 -1.486791 -1.467423 -1.039870
# Percent of total variability (p = 2), using aggregation
bind_rows(
  PCs_A %>% data.frame() %>% mutate(dataset = "claudeA"),
  PCs_B %>% data.frame() %>% mutate(dataset = "claudeB"),
  PCs_C %>% data.frame() %>% mutate(dataset = "claudeC")
) %>% 
  group_by(dataset) %>% 
  summarize(across(c(X1, X2), var)) %>% 
  mutate(across(c(X1, X2), \(x) 100 * x / 2)) %>% 
  rename(`% explained by PC1` = X1, `% explained by PC2` = X2)
# A tibble: 3 × 3
  dataset `% explained by PC1` `% explained by PC2`
  <chr>                  <dbl>                <dbl>
1 claudeA                 97.6                2.44 
2 claudeB                 95.9                4.08 
3 claudeC                 99.5                0.509