| Name | M_Nr |
|---|---|
| Malte Gunnar Grube | 221200075 |
| Maximilian Runge | 220200831 |
| Pascal Pischel | 221201473 |
| Truong An Nguyen | 221200154 |
Mushrooms are widely consumed in various countries due to their culinary appeal. However, the presence of poisonous mushrooms poses a significant risk to human health and safety. Therefore, it is particularly important to be able to distinguish between edible and poisonous mushrooms. This task can be very challenging, as there are many different types of mushrooms species. In this project, our objective is to address this challenge by using a comprehensive dataset to train and employ a machine learning model capable of accurately distinguish between edible and poisonous mushrooms.
For this project, we aim to enhance the reliability and efficiency of mushroom classification, ultimately promoting public health and minimizing the risks associated with mushroom consumption. To accomplish this, we will make use of a dataset that contains relevant information about mushrooms. We will ultimately train a model, to effectively analyze and interpret the provided mushroom data, enabling it to classify mushrooms into edible and poisonous.
The dataset we are going to deal with is the Mushroom Classification dataset from Kaggle. It contains information about mushrooms and whether they are edible or poisonous on 8124 samples. The dataset contains 23 columns, of which 22 are features and 1 is the target variable (edible or poisonous).
Introducing, we will take a closer look at the columns of the dataset.
Let’s take a look at how to read the dataset. The dataset is available as a csv file. We can read it with the following command:
| Attribute | Classes |
|---|---|
| cap-shape | bell=b, conical=c, convex=x, flat=f, knobbed=k, sunken=s |
| cap-surface | fibrous=f, grooves=g, scaly=y, smooth=s |
| cap-color | brown=n, buff=b, cinnamon=c, gray=g, green=r, pink=p, purple=u, red=e, white=w, yellow=y |
| bruises | bruises=t, no=f |
| odor | almond=a, anise=l, creosote=c, fishy=y, foul=f, musty=m, none=n, pungent=p, spicy=s |
| gill-attachment | attached=a, descending=d, free=f, notched=n |
| gill-spacing | close=c, crowded=w, distant=d |
| gill-size | broad=b, narrow=n |
| gill-color | black=k, brown=n, buff=b, chocolate=h, gray=g, green=r, orange=o, pink=p, purple=u, red=e, white=w, yellow=y |
| stalk-shape | enlarging=e, tapering=t |
| stalk-root | bulbous=b, club=c, cup=u, equal=e, rhizomorphs=z, rooted=r, missing=? |
| stalk-surface-above-ring | fibrous=f, scaly=y, silky=k, smooth=s |
| stalk-surface-below-ring | fibrous=f, scaly=y, silky=k, smooth=s |
| stalk-color-above-ring | brown=n, buff=b, cinnamon=c, gray=g, orange=o, pink=p, red=e, white=w, yellow=y |
| stalk-color-below-ring | brown=n, buff=b, cinnamon=c, gray=g, orange=o, pink=p, red=e, white=w, yellow=y |
| veil-type | partial=p, universal=u |
| veil-color | brown=n, orange=o, white=w, yellow=y |
| ring-number | none=n, one=o, two=t |
| ring-type | cobwebby=c, evanescent=e, flaring=f, large=l, none=n, pendant=p, sheathing=s, zone=z |
| spore-print-color | black=k, brown=n, buff=b, chocolate=h, green=r, orange=o, purple=u, white=w, yellow=y |
| population | abundant=a, clustered=c, numerous=n, scattered=s, several=v, solitary=y |
| habitat | grasses=g, leaves=l, meadows=m, paths=p, urban=u, waste=w, woods=d |
Now we will check if there are any null columns in the dataset:
class cap.shape cap.surface
0 0 0
cap.color bruises odor
0 0 0
gill.attachment gill.spacing gill.size
0 0 0
gill.color stalk.shape stalk.root
0 0 0
stalk.surface.above.ring stalk.surface.below.ring stalk.color.above.ring
0 0 0
stalk.color.below.ring veil.type veil.color
0 0 0
ring.number ring.type spore.print.color
0 0 0
population habitat
0 0
Let’s check the structure of the dataset:
'data.frame': 8124 obs. of 23 variables:
$ class : chr "p" "e" "e" "p" ...
$ cap.shape : chr "x" "x" "b" "x" ...
$ cap.surface : chr "s" "s" "s" "y" ...
$ cap.color : chr "n" "y" "w" "w" ...
$ bruises : chr "t" "t" "t" "t" ...
$ odor : chr "p" "a" "l" "p" ...
$ gill.attachment : chr "f" "f" "f" "f" ...
$ gill.spacing : chr "c" "c" "c" "c" ...
$ gill.size : chr "n" "b" "b" "n" ...
$ gill.color : chr "k" "k" "n" "n" ...
$ stalk.shape : chr "e" "e" "e" "e" ...
$ stalk.root : chr "e" "c" "c" "e" ...
$ stalk.surface.above.ring: chr "s" "s" "s" "s" ...
$ stalk.surface.below.ring: chr "s" "s" "s" "s" ...
$ stalk.color.above.ring : chr "w" "w" "w" "w" ...
$ stalk.color.below.ring : chr "w" "w" "w" "w" ...
$ veil.type : chr "p" "p" "p" "p" ...
$ veil.color : chr "w" "w" "w" "w" ...
$ ring.number : chr "o" "o" "o" "o" ...
$ ring.type : chr "p" "p" "p" "p" ...
$ spore.print.color : chr "k" "n" "n" "k" ...
$ population : chr "s" "n" "n" "s" ...
$ habitat : chr "u" "g" "m" "u" ...
In this section, we will prepare the data for the model.
After this, we will remove the column veil-type, as it
only contains one value called p:
Finally, we will convert the variables into factors – because all features are categorical. Here is a preview of the converted variables:
| class | cap.shape | cap.surface | cap.color | bruises | odor | gill.attachment | gill.spacing | gill.size | gill.color | stalk.shape | stalk.root | stalk.surface.above.ring | stalk.surface.below.ring | stalk.color.above.ring | stalk.color.below.ring | veil.color | ring.number | ring.type | spore.print.color | population | habitat |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| p | x | s | n | t | p | f | c | n | k | e | e | s | s | w | w | w | o | p | k | s | u |
| e | x | s | y | t | a | f | c | b | k | e | c | s | s | w | w | w | o | p | n | n | g |
| e | b | s | w | t | l | f | c | b | n | e | c | s | s | w | w | w | o | p | n | n | m |
| p | x | y | w | t | p | f | c | n | n | e | e | s | s | w | w | w | o | p | k | s | u |
| e | x | s | g | f | n | f | w | b | k | t | e | s | s | w | w | w | o | e | n | a | g |
| e | x | y | y | t | a | f | c | b | n | e | c | s | s | w | w | w | o | p | k | n | g |
Here you can see the distribution of the both classes:
ggplot(data = mush, aes(x = class, fill = class)) +
geom_bar(alpha = 0.7) +
labs(title = "class", x = NULL, y = NULL) +
scale_fill_manual(values = c("#33cc33", "#4b0092")) +
theme_bw() +
theme(
plot.title = element_text(hjust = 0.5, size = 11, face = "bold"),
legend.position = "none"
)| class | n |
|---|---|
| edible | 4208 |
| poisonous | 3916 |
Let’s take a look at the distributions of all the features:
feature_names <- names(mush)[-1]
feature_plots <- lapply(feature_names, function(feature) {
ggplot(data = mush, aes(x = get(feature), fill = class)) +
geom_bar(alpha = 0.7) +
labs(title = paste(feature), x = NULL, y = NULL) +
scale_fill_manual(
values = c("#33cc33", "#4b0092"),
labels = c("edible", "poisonous")
) +
theme_bw() +
theme(
plot.title = element_text(hjust = 0.5, size = 11, face = "bold"),
legend.position = "top"
) +
guides(fill = guide_legend(title = "class", nrow = 2))
})
legend <- get_legend(feature_plots[[1]])
for (i in seq_along(feature_plots)) {
feature_plots[[i]] <- feature_plots[[i]] + theme(legend.position = "none")
}
plot_grid(plotlist = feature_plots, ncol = 2, nrow = 11, legend = legend)In these plots, we can see that certain features are important to
classify a mushroom as edible.
If a test mushroom has one of these features, it is more likely to be
this class:
| Attribute | Description |
|---|---|
| odor | almond=a, anise=l, none=n |
| stalk.color.below.ring | red=e, orange=o, gray=g |
| stalk.color.above.ring | red=e, orange=o, gray=g |
| spore.print.color | yellow=y, purple=u, orange=o, buff=b |
| population | abundant=a, numerous=n |
| veil.color | orange=o, brown=n |
| cap.color | red=e, purple=u |
| gill.color | gray=g, orange=o |
| ring.type | flaring=f |
| cap.shape | sunken=s |
In these plots, we can see that certain features are important to
classify a mushroom as poisonous.
If a test mushroom has one of these features, it is more likely to be
this class:
| Attribute | Description |
|---|---|
| odor | pungent=p, foul=f, creosote=c, fishy=y, spicy=s, musty=m |
| stalk.color.below.ring | buff=b, yellow=y, cinnamon=c |
| ring.number | none=n |
| gill.color | buff=b, green=r |
| spore.print.color | green=r |
| cap.shape | conical=c |
| veil.color | yellow=y |
| stalk.color.above.ring | buff=b, yellow=y, cinnamon=c |
| ring.type | large=l |
| cap.surface | grooves=g |
Now we will split the data into training and test data. We will use the training data to train the model and the test data to test the model.
In this section, we will train several models and compare them – sped up via parallel processing.
Let’s take a look at the Logistic Regression (glm) model:
cl <- makeCluster(detectCores() - 1)
registerDoParallel(cl)
start_time <- Sys.time()
model_glm <- train(class ~ ., data = train, method = "glm")
stopCluster(cl)
end_time <- Sys.time()
end_time - start_timeTime difference of 17.80068 secs
Let’s take a look at the Support Vector Machines (linear) model:
cl <- makeCluster(detectCores() - 1)
registerDoParallel(cl)
start_time <- Sys.time()
model_svm <- train(class ~ ., data = train, method = "svmLinear")
stopCluster(cl)
end_time <- Sys.time()
end_time - start_timeTime difference of 8.636677 secs
Let’s take a look at the Random Forest model:
cl <- makeCluster(detectCores() - 1)
registerDoParallel(cl)
start_time <- Sys.time()
model_rf <- train(class ~ ., data = train, method = "rf")
stopCluster(cl)
end_time <- Sys.time()
end_time - start_timeTime difference of 2.940914 mins
Let’s take a look at the K-Nearest Neighbors model:
cl <- makeCluster(detectCores() - 1)
registerDoParallel(cl)
start_time <- Sys.time()
model_knn <- train(class ~ ., data = train, method = "knn")
stopCluster(cl)
end_time <- Sys.time()
end_time - start_timeTime difference of 49.82261 secs
Let’s take a look at the Gradient Boosting Machine model:
cl <- makeCluster(detectCores() - 1)
registerDoParallel(cl)
start_time <- Sys.time()
model_gbm <- train(class ~ ., data = train, method = "gbm", verbose = FALSE)
stopCluster(cl)
end_time <- Sys.time()
end_time - start_timeTime difference of 24.34671 secs
Let’s take a look at the confusion matrix of the Logistic Regression (glm) model:
Confusion Matrix and Statistics
Reference
Prediction e p
e 841 0
p 0 783
Accuracy : 1
95% CI : (0.9977, 1)
No Information Rate : 0.5179
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 1
Mcnemar's Test P-Value : NA
Sensitivity : 1.0000
Specificity : 1.0000
Pos Pred Value : 1.0000
Neg Pred Value : 1.0000
Prevalence : 0.5179
Detection Rate : 0.5179
Detection Prevalence : 0.5179
Balanced Accuracy : 1.0000
'Positive' Class : e
Let’s take a look at the confusion matrix of the Support Vector Machines (linear) model:
Confusion Matrix and Statistics
Reference
Prediction e p
e 841 0
p 0 783
Accuracy : 1
95% CI : (0.9977, 1)
No Information Rate : 0.5179
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 1
Mcnemar's Test P-Value : NA
Sensitivity : 1.0000
Specificity : 1.0000
Pos Pred Value : 1.0000
Neg Pred Value : 1.0000
Prevalence : 0.5179
Detection Rate : 0.5179
Detection Prevalence : 0.5179
Balanced Accuracy : 1.0000
'Positive' Class : e
Let’s take a look at the confusion matrix of the Random Forest model:
Confusion Matrix and Statistics
Reference
Prediction e p
e 841 0
p 0 783
Accuracy : 1
95% CI : (0.9977, 1)
No Information Rate : 0.5179
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 1
Mcnemar's Test P-Value : NA
Sensitivity : 1.0000
Specificity : 1.0000
Pos Pred Value : 1.0000
Neg Pred Value : 1.0000
Prevalence : 0.5179
Detection Rate : 0.5179
Detection Prevalence : 0.5179
Balanced Accuracy : 1.0000
'Positive' Class : e
Let’s take a look at the confusion matrix of the K-Nearest Neighbors model:
Confusion Matrix and Statistics
Reference
Prediction e p
e 841 0
p 0 783
Accuracy : 1
95% CI : (0.9977, 1)
No Information Rate : 0.5179
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 1
Mcnemar's Test P-Value : NA
Sensitivity : 1.0000
Specificity : 1.0000
Pos Pred Value : 1.0000
Neg Pred Value : 1.0000
Prevalence : 0.5179
Detection Rate : 0.5179
Detection Prevalence : 0.5179
Balanced Accuracy : 1.0000
'Positive' Class : e
Let’s take a look at the confusion matrix of the Gradient Boosting Machine model:
Confusion Matrix and Statistics
Reference
Prediction e p
e 841 0
p 0 783
Accuracy : 1
95% CI : (0.9977, 1)
No Information Rate : 0.5179
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 1
Mcnemar's Test P-Value : NA
Sensitivity : 1.0000
Specificity : 1.0000
Pos Pred Value : 1.0000
Neg Pred Value : 1.0000
Prevalence : 0.5179
Detection Rate : 0.5179
Detection Prevalence : 0.5179
Balanced Accuracy : 1.0000
'Positive' Class : e
One of the most accurate models is RandomForest. With
RandomForest the function varImp() shows the
permutation importance. As shown in the importance plot of the model,
the odor none is the most significant indicator for an
edible mushroom.
var_importance <- varImp(model_rf$finalModel)
var_df <- as.data.frame(var_importance)
sorted_vars <- var_df %>%
arrange(desc(Overall)) %>%
head(20)
total <- sum(sorted_vars$Overall)
sorted_vars$Overall <- (sorted_vars$Overall / total) * 100
x_labels <- str_sub(row.names(sorted_vars), end = -2) %>%
paste0("_", str_sub(row.names(sorted_vars), -1))
ggplot(sorted_vars, aes(
x = reorder(row.names(sorted_vars), -Overall),
y = Overall
)) +
geom_bar(stat = "identity", alpha = 0.6, fill = "#00303F") +
labs(x = NULL, y = NULL, title = "Variable vs. Importance (in %)") +
coord_flip() +
theme_bw() +
theme(
legend.position = "none",
plot.title = element_text(hjust = 0.5, face = "bold")
) +
scale_x_discrete(labels = x_labels)Here are some plots that show the relationship between some of the variables and the class of the mushrooms.
ggplot(mush, aes(x = gill.size, y = odor)) +
geom_jitter(aes(color = class, alpha = class),
position = position_jitter(width = 0.2)
) +
scale_color_manual(values = c("e" = "#33cc33", "p" = "#4b0092")) +
scale_alpha_manual(values = c("e" = 0.4, "p" = 0.6)) +
scale_size(guide = "none") +
labs(title = "Gill size vs. Odor") +
theme_bw() +
theme(
plot.title = element_text(
hjust = 0.5, face = "bold",
margin = ggplot2::margin(0, 0, 5, 0)
),
axis.title.x = element_text(margin = ggplot2::margin(7, 0, 0, 0)),
axis.title.y = element_text(margin = ggplot2::margin(0, 15, 0, 0))
)ggplot(mush, aes(x = spore.print.color, y = gill.color)) +
geom_jitter(aes(color = class, alpha = class),
position = position_jitter(width = 0.2)
) +
scale_color_manual(values = c("e" = "#33cc33", "p" = "#4b0092")) +
scale_alpha_manual(values = c("e" = 0.4, "p" = 0.6)) +
scale_size(guide = "none") +
labs(title = "Spore print color vs. Gill color") +
theme_bw() +
theme(
plot.title = element_text(
hjust = 0.5, face = "bold",
margin = ggplot2::margin(0, 0, 5, 0)
),
axis.title.x = element_text(margin = ggplot2::margin(7, 0, 0, 0)),
axis.title.y = element_text(margin = ggplot2::margin(0, 15, 0, 0))
)ggplot(mush, aes(x = habitat, y = population)) +
geom_jitter(aes(color = class, alpha = class),
position = position_jitter(width = 0.2)
) +
scale_color_manual(values = c("e" = "#33cc33", "p" = "#4b0092")) +
scale_alpha_manual(values = c("e" = 0.4, "p" = 0.6)) +
scale_size(guide = "none") +
labs(title = "Habitat vs. Population") +
theme_bw() +
theme(
plot.title = element_text(
hjust = 0.5, face = "bold",
margin = ggplot2::margin(0, 0, 5, 0)
),
axis.title.x = element_text(margin = ggplot2::margin(7, 0, 0, 0)),
axis.title.y = element_text(margin = ggplot2::margin(0, 15, 0, 0))
)ggplot(mush, aes(x = cap.shape, y = stalk.color.below.ring)) +
geom_jitter(aes(color = class, alpha = class),
position = position_jitter(width = 0.2)
) +
scale_color_manual(values = c("e" = "#33cc33", "p" = "#4b0092")) +
scale_alpha_manual(values = c("e" = 0.4, "p" = 0.6)) +
scale_size(guide = "none") +
labs(title = "Cap shape vs. Stalk color below ring") +
theme_bw() +
theme(
plot.title = element_text(
hjust = 0.5, face = "bold",
margin = ggplot2::margin(0, 0, 5, 0)
),
axis.title.x = element_text(margin = ggplot2::margin(7, 0, 0, 0)),
axis.title.y = element_text(margin = ggplot2::margin(0, 15, 0, 0))
)Finishing the document, we are illustrating various decision trees.
The first one is a general tree. Then, we are going to remove the
odor variable and see how the tree changes. The next tree
does not provide properties that are hard to tell. The last one just
provides properties according to the top view of a mushroom.
mush_fac <- as.data.frame(lapply(mush, as.factor))
loss_matrix <- matrix(c(0, 1, 4, 0), byrow = TRUE, nrow = 2)tree1 <- rpart(class ~ .,
minbucket = 1,
method = "class",
parms = list(loss = loss_matrix),
data = mush_fac
)
fancyRpartPlot(tree1,
palettes = c("Greens", "Purples"),
caption = NULL, cex = 0.8,
main = "General tree"
)mush_fac_no_odor <- mush_fac
mush_fac_no_odor$odor <- NULL
tree2 <- rpart(class ~ .,
minbucket = 2,
method = "class",
parms = list(loss = loss_matrix),
data = mush_fac_no_odor
)
fancyRpartPlot(tree2,
palettes = c("Greens", "Purples"),
caption = NULL, cex = 0.8,
main = "Without Odor"
)mush_fac_hard <- mush_fac %>%
select(
-odor, -spore.print.color, -gill.size, -bruises, -ring.type,
-stalk.surface.above.ring, -stalk.surface.below.ring, -stalk.root
) %>%
mutate_at(vars(
gill.color, cap.color, stalk.color.above.ring,
stalk.color.below.ring
), ~ if_else(. %in% c("b", "h", "c"), "n", .))
tree3 <- rpart(class ~ .,
minbucket = 200,
method = "class",
parms = list(loss = loss_matrix),
data = mush_fac_hard
)
fancyRpartPlot(tree3,
palettes = c("Greens", "Purples"),
caption = NULL,
main = "Without hard to tell properties"
)mush_fac_top <- subset(mush_fac, select = -c(
gill.size,
gill.color, gill.attachment, gill.spacing,
stalk.shape, stalk.root, stalk.surface.above.ring,
stalk.surface.below.ring, stalk.color.above.ring,
stalk.color.below.ring, spore.print.color
))
tree4 <- rpart(class ~ .,
minbucket = 1,
method = "class",
parms = list(loss = loss_matrix),
data = mush_fac_top
)
fancyRpartPlot(tree4,
palettes = c("Greens", "Purples"),
caption = NULL, cex = 0.8,
main = "Top view only"
)In this section, we are going to show some real mushrooms. In addition to that, we are going to provide trees for classfying them.
As we delved into the analysis of the mushroom dataset, exploring its various features and their importance, we were able to train several models and compare them. With that we gained valuable insights into the characteristics that differentiate edible and poisonous mushrooms. Through our efforts, we successfully developed decision trees that proved effective in classifying real mushrooms based on the knowledge obtained from the dataset. In conclusion, this project serves as a foundation for future mushroom classification and related studies. By utilizing the power of machine learning, we can continue to make strides in understanding and utilizing the world of mushrooms for the betterment of society.
[1] iNaturalist: Shield Dapperling. URL: https://www.inaturalist.org/observations/156623219.
Accessed: July 20, 2023
[2] iNaturalist: Deadly Dapperling. URL: https://www.inaturalist.org/observations/70759242.
Accessed: July 20, 2023.
[3] iNaturalist: Medusa Mushroom. URL: https://www.inaturalist.org/observations/91868779.
Accessed: July 20, 2023.
[4] iNaturalist: Inky Mushroom. URL: https://www.inaturalist.org/observations/170844438.
Accessed: July 20, 2023.