Class 607, Assignment 2b

Author

Troy Tournat

Published

September 10, 2026

Introduction

This dataset is from Professor Catlin’s public repository on GitHub called “data” located here

Tackling the problem: My plan is to review some of the concepts such as null error rates and harmonic mean that tripped me up in the reading Performance Metrics for Classification Problems in Machine Learning. Then, I will ingest the dataset “penguin_predictions” and go through the calculation exercises. I will build the matrices manually.

Anticipated data challenges: The biggest hurdle calculating these tasks in R rather than pen and paper. Second, is making sure the performance metrics are correctly done.

Citations:

  • Anthropic. (2025). Claude Opus 4.5 [Large language model]. (https://claude.ai/) Accessed January 8, 2026. Link to chat.

Analysis

#Not shown: Location on computer where I am grabbing the data and creating a saved location

#Install packages needed 
pacman::p_load(
  # Package Install and Management
  pacman,           # package install/load
  janitor,          #clean up data names 
  
  # Project and File Management
  readr,            # import data
  httr,             #github passkey 

  # General Data Management
  dplyr,            # data management
  tidyr,            # data management
  lubridate,        # work with dates
  zoo,              # work with dates
  tidyverse,        # work with dates
  gmodels           # freq tables SAS 
)


#Load data
#desktop
penguins <- read.csv(paste0(csv_location, "penguin_predictions.csv"), header=TRUE, stringsAsFactors = FALSE)


#Review and prepare data for 
#looking at dataframe 
ls(penguins)
[1] "sex"
#cleaning names slightly 
clean <- penguins %>% clean_names
ls(clean)
[1] "pred_class"  "pred_female" "sex"        
#change notation to easier format 
options(scipen = 100) # take out sci notation 
clean$pred_female <- round(clean$pred_female, 2) #round 

#sex = actual
#pred_female = model predicted probability that sex is female 
#pred_class =  
  #1 = if the model probability is greater than .5
  #0 = if not 

#change pred_class in 1/0 binary 
clean <- clean%>% 
  mutate( 
    pred_class_50edit = case_when( 
      pred_female > .5 ~ 1, 
      pred_female <= .5 ~ 2), 
    true_class_edit = case_when( 
      sex == "female" ~ 1, 
      TRUE ~ 2
      ))

Task #1: Null Error Rate

A. Calculate the null error rate (majority-class error rate).

#what is majority class (higher count of actual data) 
majority <- table(clean$true_class_edit)
addmargins(majority)

  1   2 Sum 
 39  54  93 
prop.table(majority)

        1         2 
0.4193548 0.5806452 
#calc null error rate = 1 - (majority class proportion) = lower count of actual data 
# 
1 - (54/93)
[1] 0.4193548

B. Create a plot showing the distribution of the actual class (sex).

#sum up true counts (sex) and percents 
clean_graph<- clean%>% 
  count(sex)%>% 
  mutate( 
    percent = n / sum(n))


ggplot(clean_graph, aes(x = sex, y = percent)) +
  geom_col(fill = "darkblue") +
  scale_y_continuous(labels = scales::percent) +
  labs(
    title = "True Class Percent",
    x = "Class",
    y = "Percent"
  ) +
  theme_minimal()

C. Explain why knowing the null error rate is important when evaluating models.

It’s important to understand the proportion of the actual distribution of positive and negative values (in this case (sex of penguin) before evaluating how the model does. This of course will be helpful once we examine the confusion matrix and other performance metrics.

Task #2: Confusion Matrices at Multiple Thresholds

#adding in thresholds 
clean <- clean%>% 
  mutate( 
    #first threshold : .2 
    pred_class_20edit = case_when( 
      pred_female > .2 ~ 1, 
      pred_female <= .2 ~ 2), 
    #Second threshold: .5 (already done)
    #Third threshold: .8 
    pred_class_80edit = case_when( 
      pred_female > .8 ~ 1, 
      pred_female <= .8 ~ 2
      ))


#confusion matrix 
#looking at it in SAS style library(gmodels)
#In order for it to work with out messing up the formatting 1 = TRUE 2 = FALSE 

#20% threshold 
CrossTable(clean$pred_class_20edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = FALSE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_20edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        37 |         6 |        43 | 
                        |    19.952 |    14.410 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         2 |        48 |        50 | 
                        |    17.159 |    12.392 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
------------------------|-----------|-----------|-----------|

 
#True Positives: 37
#False Positives: 6
#True Negatives: 2 
#False Negatives: 48

#50% threshold 
CrossTable(clean$pred_class_50edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = FALSE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_50edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         3 |        39 | 
                        |    23.597 |    17.043 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        51 |        54 | 
                        |    17.043 |    12.309 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
------------------------|-----------|-----------|-----------|

 
#True Positives: 36
#False Positives: 3
#True Negatives: 51 
#False Negatives: 3

#80% threshold 
CrossTable(clean$pred_class_80edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = FALSE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_80edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         2 |        38 | 
                        |    25.263 |    18.246 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        52 |        55 | 
                        |    17.455 |    12.606 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
------------------------|-----------|-----------|-----------|

 
#True Positives: 36
#False Positives: 2
#True Negatives: 52 
#False Negatives: 3

Task #3: Performance Metrics

##Accuracy##
#meaning: Overall % correctly classified
#equation: Accuracy = (TP + TN) / total

#addmargins(table(clean$pred_class_20edit, clean$true_class_edit))

#20% threshold 
sum(diag(table(clean$pred_class_20edit, clean$true_class_edit))) / nrow(clean) 
[1] 0.9139785
#double check
(37+48)/93 
[1] 0.9139785
#50% threshold 
sum(diag(table(clean$pred_class_50edit, clean$true_class_edit))) / nrow(clean) 
[1] 0.9354839
#double check
(36+51)/93
[1] 0.9354839
#80% threshold 
sum(diag(table(clean$pred_class_80edit, clean$true_class_edit))) / nrow(clean) 
[1] 0.9462366
#double check
(36+52)/93
[1] 0.9462366
##Precision## 
#meaning: Estimated outcome or Positive Predictive Value (PPV) 
#“Among those the model called positive, how many were actually positive?”
#equation: Precision = TP / (TP + FP)   <- aka row total
addmargins(table(clean$pred_class_20edit, clean$true_class_edit))
     
       1  2 Sum
  1   37  6  43
  2    2 48  50
  Sum 39 54  93
#20% threshold 
CrossTable(clean$pred_class_20edit, clean$true_class_edit, expected = FALSE, prop.r = TRUE, prop.c = FALSE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Row Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_20edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        37 |         6 |        43 | 
                        |    19.952 |    14.410 |           | 
                        |     0.860 |     0.140 |     0.462 | 
------------------------|-----------|-----------|-----------|
                      2 |         2 |        48 |        50 | 
                        |    17.159 |    12.392 |           | 
                        |     0.040 |     0.960 |     0.538 | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
------------------------|-----------|-----------|-----------|

 
#.86 
#double check
37/(37+6)  
[1] 0.8604651
#50% threshold 
CrossTable(clean$pred_class_50edit, clean$true_class_edit, expected = FALSE, prop.r = TRUE, prop.c = FALSE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Row Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_50edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         3 |        39 | 
                        |    23.597 |    17.043 |           | 
                        |     0.923 |     0.077 |     0.419 | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        51 |        54 | 
                        |    17.043 |    12.309 |           | 
                        |     0.056 |     0.944 |     0.581 | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
------------------------|-----------|-----------|-----------|

 
#.92 
#double check
36/39 
[1] 0.9230769
#80% threshold 
CrossTable(clean$pred_class_80edit, clean$true_class_edit, expected = FALSE, prop.r = TRUE, prop.c = FALSE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Row Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_80edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         2 |        38 | 
                        |    25.263 |    18.246 |           | 
                        |     0.947 |     0.053 |     0.409 | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        52 |        55 | 
                        |    17.455 |    12.606 |           | 
                        |     0.055 |     0.945 |     0.591 | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
------------------------|-----------|-----------|-----------|

 
#.947
#double check
36/38
[1] 0.9473684
##Recall## 
#meaning: Sensitivity or True Positive Rate 
#“Among those who were actually positive, how many did the model identify?”
#equation: Recall = TP / (TP + FN)   <- aka column total

#20% threshold 
CrossTable(clean$pred_class_20edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = TRUE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_20edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        37 |         6 |        43 | 
                        |    19.952 |    14.410 |           | 
                        |     0.949 |     0.111 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         2 |        48 |        50 | 
                        |    17.159 |    12.392 |           | 
                        |     0.051 |     0.889 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
                        |     0.419 |     0.581 |           | 
------------------------|-----------|-----------|-----------|

 
#.95
#double check
37/(37+2)  
[1] 0.9487179
#50% threshold 
CrossTable(clean$pred_class_50edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = TRUE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_50edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         3 |        39 | 
                        |    23.597 |    17.043 |           | 
                        |     0.923 |     0.056 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        51 |        54 | 
                        |    17.043 |    12.309 |           | 
                        |     0.077 |     0.944 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
                        |     0.419 |     0.581 |           | 
------------------------|-----------|-----------|-----------|

 
#.92 
#double check
36/39 
[1] 0.9230769
#80% threshold 
CrossTable(clean$pred_class_80edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = TRUE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_80edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         2 |        38 | 
                        |    25.263 |    18.246 |           | 
                        |     0.923 |     0.037 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        52 |        55 | 
                        |    17.455 |    12.606 |           | 
                        |     0.077 |     0.963 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
                        |     0.419 |     0.581 |           | 
------------------------|-----------|-----------|-----------|

 
#.92
#double check
36/39
[1] 0.9230769
##F-1 Score##
#meanin: It's the harmonic mean of precision (PPV) and recall (sensitivity):
#equation: 2( Precision * Recall ) / Precision + Recall

#20% threshold 
CrossTable(clean$pred_class_20edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = TRUE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_20edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        37 |         6 |        43 | 
                        |    19.952 |    14.410 |           | 
                        |     0.949 |     0.111 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         2 |        48 |        50 | 
                        |    17.159 |    12.392 |           | 
                        |     0.051 |     0.889 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
                        |     0.419 |     0.581 |           | 
------------------------|-----------|-----------|-----------|

 
#Recall = .95
#Precision = .86
#equation
2*(.95*.86)/(.95+.86)  
[1] 0.9027624
#50% threshold 
CrossTable(clean$pred_class_50edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = TRUE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_50edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         3 |        39 | 
                        |    23.597 |    17.043 |           | 
                        |     0.923 |     0.056 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        51 |        54 | 
                        |    17.043 |    12.309 |           | 
                        |     0.077 |     0.944 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
                        |     0.419 |     0.581 |           | 
------------------------|-----------|-----------|-----------|

 
#Recall = .92
#Precision = .92
#equation
2*(.92*.92)/(.92+.92)  
[1] 0.92
#80% threshold 
CrossTable(clean$pred_class_80edit, clean$true_class_edit, expected = FALSE, prop.r = FALSE, prop.c = TRUE, prop.t = FALSE)

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  93 

 
                        | clean$true_class_edit 
clean$pred_class_80edit |         1 |         2 | Row Total | 
------------------------|-----------|-----------|-----------|
                      1 |        36 |         2 |        38 | 
                        |    25.263 |    18.246 |           | 
                        |     0.923 |     0.037 |           | 
------------------------|-----------|-----------|-----------|
                      2 |         3 |        52 |        55 | 
                        |    17.455 |    12.606 |           | 
                        |     0.077 |     0.963 |           | 
------------------------|-----------|-----------|-----------|
           Column Total |        39 |        54 |        93 | 
                        |     0.419 |     0.581 |           | 
------------------------|-----------|-----------|-----------|

 
#Recall = .92
#Precision = .947
2*(.92*.947)/(.92+.947)  
[1] 0.9333048

Task #4: Threshold Use Cases

A .2 threshold would be preferable when

A .8 threshold would be preferable when

Conclusion

to write