library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(ggplot2)This project evaluates the performance of a binary classification model using data related to penguin sex predictions. The analysis will examine how different probability thresholds affect classification results and performance metrics.
The dataset is provided by the DATA 607 course as the ‘penguin_predictions.csv’ file.
First, I will explore the dataset to examine the distribution of the actual sex classes and calculate the null error rate. Then, I will calculate predicted classes using probability thresholds of 0.2, 0.5, and 0.8 and create a confusion matrix for each threshold. Finally, I will calculate and compare accuracy, precision, recall, and F1 score to understand how different thresholds affect model performance.
An anticipated challenge is creating the confusion matrix and correctly calculating the performance metrics for each probability threshold.
library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(ggplot2)The dataset is loaded from the course GitHub repository for analysis.
penguin_predictions <- read.csv("https://raw.githubusercontent.com/acatlin/data/refs/heads/master/penguin_predictions.csv")head(penguin_predictions) .pred_female .pred_class sex
1 0.9921746 female female
2 0.9542394 female female
3 0.9847350 female female
4 0.1870206 male female
5 0.9947012 female female
6 0.9999891 female female
The structure of the dataset is examined to confirm the number of observations, variables, and their data types.
str(penguin_predictions)'data.frame': 93 obs. of 3 variables:
$ .pred_female: num 0.992 0.954 0.985 0.187 0.995 ...
$ .pred_class : chr "female" "female" "female" "male" ...
$ sex : chr "female" "female" "female" "female" ...
The distribution of the actual class variable is examined to determine the number of observations in each class.
penguin_predictions %>%
count(sex) sex n
1 female 39
2 male 54
class_counts <- penguin_predictions %>%
count(sex)
class_counts sex n
1 female 39
2 male 54
The null error rate is calculated using the majority class in the actual “sex” variable. This provides a baseline for evaluating whether the classification model performs better than a simple majority-class prediction.
majority_class_count <- max(class_counts$n)
total_count <- sum(class_counts$n)
null_error_rate <- 1 - (majority_class_count / total_count)
null_error_rate[1] 0.4193548
The distribution of the actual classes is visualized to show the balance between female and male observations. The plot also makes the majority class easy to identify.
ggplot(penguin_predictions, aes(x = sex)) +
geom_bar() +
labs(
title = "Distribution of Actual Classes",
x = "Sex",
y = "Number of Observations"
) +
theme_minimal()The null error rate provides a baseline for evaluating a classification model. In this dataset, the majority class is male, so a simple model that predicts every observation as male would have an error rate of approximately 41.94%. Therefore, the classification model should perform better than this baseline to demonstrate that it provides useful predictive information.
A probability threshold of 0.2 is applied to “.pred_female” to create predicted class labels. Observations with a predicted female probability greater than 0.2 are classified as female; otherwise, they are classified as male.
penguins_02 <- penguin_predictions %>%
mutate(
predicted_class = ifelse(.pred_female > 0.2, "female", "male")
)
head(penguins_02) .pred_female .pred_class sex predicted_class
1 0.9921746 female female female
2 0.9542394 female female female
3 0.9847350 female female female
4 0.1870206 male female male
5 0.9947012 female female female
6 0.9999891 female female female
The true positives, false positives, true negatives, and false negatives are calculated by comparing the predicted class with the actual “sex” class.
TP_02 <- sum(
penguins_02$predicted_class == "female" &
penguins_02$sex == "female"
)
FP_02 <- sum(
penguins_02$predicted_class == "female" &
penguins_02$sex == "male"
)
TN_02 <- sum(
penguins_02$predicted_class == "male" &
penguins_02$sex == "male"
)
FN_02 <- sum(
penguins_02$predicted_class == "male" &
penguins_02$sex == "female"
)
TP_02[1] 37
FP_02[1] 6
TN_02[1] 48
FN_02[1] 2
The four confusion matrix components are organized into a table to show how the model’s predictions compare with the actual classes at the 0.2 probability threshold.
confusion_02 <- matrix(
c(TN_02, FN_02,
FP_02, TP_02),
nrow = 2,
byrow = TRUE
)
rownames(confusion_02) <- c("Predicted Male", "Predicted Female")
colnames(confusion_02) <- c("Actual Male", "Actual Female")
confusion_02 Actual Male Actual Female
Predicted Male 48 2
Predicted Female 6 37
A probability threshold of 0.5 is applied to “.pred_female” to create predicted class labels. Observations with a predicted female probability greater than 0.5 are classified as female; otherwise, they are classified as male.
penguins_05 <- penguin_predictions %>%
mutate(
predicted_class = ifelse(.pred_female > 0.5, "female", "male")
)
head(penguins_05) .pred_female .pred_class sex predicted_class
1 0.9921746 female female female
2 0.9542394 female female female
3 0.9847350 female female female
4 0.1870206 male female male
5 0.9947012 female female female
6 0.9999891 female female female
The true positives, false positives, true negatives, and false negatives are calculated by comparing the predicted class with the actual “sex” class.
TP_05 <- sum(
penguins_05$predicted_class == "female" &
penguins_05$sex == "female" )
FP_05 <- sum(
penguins_05$predicted_class == "female" &
penguins_05$sex == "male" )
TN_05 <- sum(
penguins_05$predicted_class == "male" &
penguins_05$sex == "male" )
FN_05 <- sum(
penguins_05$predicted_class == "male" &
penguins_05$sex == "female" )
TP_05 [1] 36
FP_05 [1] 3
TN_05 [1] 51
FN_05[1] 3
The four confusion matrix components are organized into a table to show how the model’s predictions compare with the actual classes at the 0.5 probability threshold.
confusion_05 <- matrix(
c(TN_05, FN_05,
FP_05, TP_05),
nrow = 2,
byrow = TRUE
)
rownames(confusion_05) <- c("Predicted Male", "Predicted Female")
colnames(confusion_05) <- c("Actual Male", "Actual Female")
confusion_05 Actual Male Actual Female
Predicted Male 51 3
Predicted Female 3 36
A probability threshold of 0.8 is applied to “.pred_female” to create predicted class labels. Observations with a predicted female probability greater than 0.8 are classified as female; otherwise, they are classified as male.
penguins_08 <- penguin_predictions %>%
mutate(
predicted_class = ifelse(.pred_female > 0.8, "female", "male")
)
head(penguins_08) .pred_female .pred_class sex predicted_class
1 0.9921746 female female female
2 0.9542394 female female female
3 0.9847350 female female female
4 0.1870206 male female male
5 0.9947012 female female female
6 0.9999891 female female female
The true positives, false positives, true negatives, and false negatives are calculated by comparing the predicted class with the actual “sex” class.
TP_08 <- sum(
penguins_08$predicted_class == "female" &
penguins_08$sex == "female" )
FP_08 <- sum(
penguins_08$predicted_class == "female" &
penguins_08$sex == "male" )
TN_08 <- sum(
penguins_08$predicted_class == "male" &
penguins_08$sex == "male" )
FN_08 <- sum(
penguins_08$predicted_class == "male" &
penguins_08$sex == "female" )
TP_08 [1] 36
FP_08[1] 2
TN_08[1] 52
FN_08[1] 3
The four confusion matrix components are organized into a table to show how the model’s predictions compare with the actual classes at the 0.8 probability threshold.
confusion_08 <- matrix(
c(TN_08, FN_08,
FP_08, TP_08),
nrow = 2,
byrow = TRUE )
rownames(confusion_08) <- c("Predicted Male", "Predicted Female")
colnames(confusion_08) <- c("Actual Male", "Actual Female")
confusion_08 Actual Male Actual Female
Predicted Male 52 3
Predicted Female 2 36
Accuracy, precision, recall, and F1 score are calculated from the confusion matrix components for each probability threshold.
calculate_metrics <- function(TP, FP, TN, FN) {
accuracy <- (TP + TN) / (TP + FP + TN + FN)
precision <- TP / (TP + FP)
recall <- TP / (TP + FN)
f1 <- 2 * precision * recall / (precision + recall)
return(
c(
Accuracy = accuracy,
Precision = precision,
Recall = recall,
F1 = f1
)
)
}The classification metrics are calculated for thresholds of 0.2, 0.5, and 0.8 to compare how changing the probability threshold affects model performance.
metrics_02 <- calculate_metrics(
TP = TP_02,
FP = FP_02,
TN = TN_02,
FN = FN_02
)
metrics_05 <- calculate_metrics(
TP = TP_05,
FP = FP_05,
TN = TN_05,
FN = FN_05
)
metrics_08 <- calculate_metrics(
TP = TP_08,
FP = FP_08,
TN = TN_08,
FN = FN_08
)
metrics_02 Accuracy Precision Recall F1
0.9139785 0.8604651 0.9487179 0.9024390
metrics_05 Accuracy Precision Recall F1
0.9354839 0.9230769 0.9230769 0.9230769
metrics_08 Accuracy Precision Recall F1
0.9462366 0.9473684 0.9230769 0.9350649
The results are presented in a single table to clearly compare accuracy, precision, recall, and F1 score across the three probability thresholds.
metrics_table <- data.frame(
Threshold = c(0.2, 0.5, 0.8),
Accuracy = c(
metrics_02["Accuracy"],
metrics_05["Accuracy"],
metrics_08["Accuracy"]
),
Precision = c(
metrics_02["Precision"],
metrics_05["Precision"],
metrics_08["Precision"]
),
Recall = c(
metrics_02["Recall"],
metrics_05["Recall"],
metrics_08["Recall"]
),
F1 = c(
metrics_02["F1"],
metrics_05["F1"],
metrics_08["F1"]
)
)
metrics_table Threshold Accuracy Precision Recall F1
1 0.2 0.9139785 0.8604651 0.9487179 0.9024390
2 0.5 0.9354839 0.9230769 0.9230769 0.9230769
3 0.8 0.9462366 0.9473684 0.9230769 0.9350649
A 0.2 threshold may be preferable in a medical screening system where it is important to identify as many potentially positive cases as possible. For example, if a model is being used to identify patients who may have a serious disease, missing a positive case can be more harmful than sending some additional patients for further testing. A lower threshold increases the number of observations classified as positive and can help reduce false negatives. In this assignment, the 0.2 threshold produced the highest recall (0.9487), meaning that it identified a larger proportion of the actual female cases.
A 0.8 threshold may be preferable in a situation where false positive predictions are costly or disruptive. For example, in a system that flags transactions for fraud investigation, a higher threshold could be used when investigators have limited time and resources. The system would only classify a transaction as positive when the model has a high level of confidence. In this assignment, the 0.8 threshold produced the highest precision (0.9474) and the highest accuracy (0.9462), making it useful when more precise positive predictions are preferred.
The results show that changing the probability threshold affects the classification model’s performance. The 0.2 threshold produced the highest recall (0.9487), which means it identified the largest proportion of actual female cases. The 0.8 threshold produced the highest precision (0.9474) and accuracy (0.9462), indicating more precise positive predictions and better overall classification performance. The 0.5 threshold provided a more balanced result, with precision and recall both equal to 0.9231.
Overall, there is no single best threshold for every situation. A lower threshold can be useful when reducing false negatives is more important, while a higher threshold can be useful when reducing false positives and improving the precision of positive predictions is more important. Therefore, the appropriate threshold should be selected based on the goals and consequences of the classification problem.
I used ChatGPT (GPT-5.6 Luna), developed by OpenAI, to help me understand some of the functions I used in this assignment and to check my work.
OpenAI. (2026). ChatGPT (GPT-5.6 Luna) [Large language model]. [https://chatgpt.com/ Accessed September 10, 2026].
Link: