Class 607, Assignment 2b

Author

Troy Tournat

Published

September 13, 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. Since I am familiar with confusion matrices (2x2 tables) I likely will use SAS knowledge to re create them in R.

Anticipated data challenges: The biggest hurdle may be calculating in R rather than manually. 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 here.

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
  scales,           # ggplot 
  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 = 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

##################
# Importants note:
##In order for the confusion matrix to work with out flipping the table, I needed to change the formatting, so now:  1 = TRUE 2 = FALSE 
##################


#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)

#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: 48 
#False Negatives: 2

#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 we want to maximize the predicted positives regardless of they are actually positive. An example of this is 911 call screenings. Regardless of whether there is actually an emergency, it’s important that filtered like it is one.

A .8 threshold would be preferable when we want to minimize false positives. An example of this could be making sure a donor’s blood type matches with a patient’s blood type. We would want to make sure there are no patients receiving the wrong blood match.

Conclusion

I did a mixture of manual calculations, and I would like to understand how to code these calculations better. It would be interesting to use modeling techniques to play out which thresholds work better.