Nosso objetivo é criar um modelo preditivo que classifique cogumelos, dizendo se ele é comestível ou venenoso, utilizando aprendizagem supervisionada e o algoritmo KNN (K-Nearest-Neighbor). O projeto foi realizado com a linguagem R e alguns de seus pacotes para análise de dados, para a apresentação do trabalho foi utilizado o RMarkdown.
O conjunto de dados nos fornece informações sobre diferentes características dos cogumelos. Ao longo do trabalho iremos criar um modelo preditivo de classificão e depois vamos explorar e gerar insights sobre algumas características dos cogumelos.
Os dados foram obtidos no site “Kaggle”, e para maiores informações sobre o dataset, visite o link:https://www.kaggle.com/uciml/mushroom-classification
#Coletando os dados
df <- read.csv('mushrooms.csv')
#Informações sobre o dataset
str(df)
## 'data.frame': 8124 obs. of 23 variables:
## $ class : Factor w/ 2 levels "e","p": 2 1 1 2 1 1 1 1 2 1 ...
## $ cap.shape : Factor w/ 6 levels "b","c","f","k",..: 6 6 1 6 6 6 1 1 6 1 ...
## $ cap.surface : Factor w/ 4 levels "f","g","s","y": 3 3 3 4 3 4 3 4 4 3 ...
## $ cap.color : Factor w/ 10 levels "b","c","e","g",..: 5 10 9 9 4 10 9 9 9 10 ...
## $ bruises : Factor w/ 2 levels "f","t": 2 2 2 2 1 2 2 2 2 2 ...
## $ odor : Factor w/ 9 levels "a","c","f","l",..: 7 1 4 7 6 1 1 4 7 1 ...
## $ gill.attachment : Factor w/ 2 levels "a","f": 2 2 2 2 2 2 2 2 2 2 ...
## $ gill.spacing : Factor w/ 2 levels "c","w": 1 1 1 1 2 1 1 1 1 1 ...
## $ gill.size : Factor w/ 2 levels "b","n": 2 1 1 2 1 1 1 1 2 1 ...
## $ gill.color : Factor w/ 12 levels "b","e","g","h",..: 5 5 6 6 5 6 3 6 8 3 ...
## $ stalk.shape : Factor w/ 2 levels "e","t": 1 1 1 1 2 1 1 1 1 1 ...
## $ stalk.root : Factor w/ 5 levels "?","b","c","e",..: 4 3 3 4 4 3 3 3 4 3 ...
## $ stalk.surface.above.ring: Factor w/ 4 levels "f","k","s","y": 3 3 3 3 3 3 3 3 3 3 ...
## $ stalk.surface.below.ring: Factor w/ 4 levels "f","k","s","y": 3 3 3 3 3 3 3 3 3 3 ...
## $ stalk.color.above.ring : Factor w/ 9 levels "b","c","e","g",..: 8 8 8 8 8 8 8 8 8 8 ...
## $ stalk.color.below.ring : Factor w/ 9 levels "b","c","e","g",..: 8 8 8 8 8 8 8 8 8 8 ...
## $ veil.type : Factor w/ 1 level "p": 1 1 1 1 1 1 1 1 1 1 ...
## $ veil.color : Factor w/ 4 levels "n","o","w","y": 3 3 3 3 3 3 3 3 3 3 ...
## $ ring.number : Factor w/ 3 levels "n","o","t": 2 2 2 2 2 2 2 2 2 2 ...
## $ ring.type : Factor w/ 5 levels "e","f","l","n",..: 5 5 5 5 1 5 5 5 5 5 ...
## $ spore.print.color : Factor w/ 9 levels "b","h","k","n",..: 3 4 4 3 4 3 3 4 3 3 ...
## $ population : Factor w/ 6 levels "a","c","n","s",..: 4 3 3 4 1 3 3 4 5 4 ...
## $ habitat : Factor w/ 7 levels "d","g","l","m",..: 6 2 4 6 2 2 4 4 2 4 ...
#A coluna stalk.root possui algumas observações com valores "?", inicialmente irei manter essas observações, e se essa variável stalk.root for uma das mais relevantes para a classificação do modelo, trataremos esses valores.
str(df$stalk.root)
## Factor w/ 5 levels "?","b","c","e",..: 4 3 3 4 4 3 3 3 4 3 ...
#Verificando valores nulos
any(is.na(df))
## [1] FALSE
Precisamos transformar as variáveis em numéricas para o algoritmo.
#Sapply para transformar variáveis factor em numéricas
dfnum <- as.data.frame(lapply(df, as.numeric))
str(dfnum)
## 'data.frame': 8124 obs. of 23 variables:
## $ class : num 2 1 1 2 1 1 1 1 2 1 ...
## $ cap.shape : num 6 6 1 6 6 6 1 1 6 1 ...
## $ cap.surface : num 3 3 3 4 3 4 3 4 4 3 ...
## $ cap.color : num 5 10 9 9 4 10 9 9 9 10 ...
## $ bruises : num 2 2 2 2 1 2 2 2 2 2 ...
## $ odor : num 7 1 4 7 6 1 1 4 7 1 ...
## $ gill.attachment : num 2 2 2 2 2 2 2 2 2 2 ...
## $ gill.spacing : num 1 1 1 1 2 1 1 1 1 1 ...
## $ gill.size : num 2 1 1 2 1 1 1 1 2 1 ...
## $ gill.color : num 5 5 6 6 5 6 3 6 8 3 ...
## $ stalk.shape : num 1 1 1 1 2 1 1 1 1 1 ...
## $ stalk.root : num 4 3 3 4 4 3 3 3 4 3 ...
## $ stalk.surface.above.ring: num 3 3 3 3 3 3 3 3 3 3 ...
## $ stalk.surface.below.ring: num 3 3 3 3 3 3 3 3 3 3 ...
## $ stalk.color.above.ring : num 8 8 8 8 8 8 8 8 8 8 ...
## $ stalk.color.below.ring : num 8 8 8 8 8 8 8 8 8 8 ...
## $ veil.type : num 1 1 1 1 1 1 1 1 1 1 ...
## $ veil.color : num 3 3 3 3 3 3 3 3 3 3 ...
## $ ring.number : num 2 2 2 2 2 2 2 2 2 2 ...
## $ ring.type : num 5 5 5 5 1 5 5 5 5 5 ...
## $ spore.print.color : num 3 4 4 3 4 3 3 4 3 3 ...
## $ population : num 4 3 3 4 1 3 3 4 5 4 ...
## $ habitat : num 6 2 4 6 2 2 4 4 2 4 ...
#Mudando valores e tipo da variável prevista para fator
dfnum$class <- factor(dfnum$class, levels = c(1, 2), labels = c("Comestível", "Venenoso"))
Muitos algoritmos de classificação exigem que os dados estejam na mesma escala, criamos uma função para normalizar os dados
#Função para normalização
dadosnorm <- function(x) {return ((x - min(x)) / (max(x) - min(x)))}
#Aplicando a normalização
dfnorm <- as.data.frame(lapply(dfnum[2:23], dadosnorm))
#Verificando a normalização
#Escala original
summary(dfnum[c('cap.surface','bruises','odor','stalk.shape')])
## cap.surface bruises odor stalk.shape
## Min. :1.000 Min. :1.000 Min. :1.000 Min. :1.000
## 1st Qu.:1.000 1st Qu.:1.000 1st Qu.:3.000 1st Qu.:1.000
## Median :3.000 Median :1.000 Median :6.000 Median :2.000
## Mean :2.828 Mean :1.416 Mean :5.145 Mean :1.567
## 3rd Qu.:4.000 3rd Qu.:2.000 3rd Qu.:6.000 3rd Qu.:2.000
## Max. :4.000 Max. :2.000 Max. :9.000 Max. :2.000
#Dataset normalizado
summary(dfnorm[c('cap.surface','bruises','odor','stalk.shape')])
## cap.surface bruises odor stalk.shape
## Min. :0.0000 Min. :0.0000 Min. :0.0000 Min. :0.0000
## 1st Qu.:0.0000 1st Qu.:0.0000 1st Qu.:0.2500 1st Qu.:0.0000
## Median :0.6667 Median :0.0000 Median :0.6250 Median :1.0000
## Mean :0.6092 Mean :0.4156 Mean :0.5181 Mean :0.5672
## 3rd Qu.:1.0000 3rd Qu.:1.0000 3rd Qu.:0.6250 3rd Qu.:1.0000
## Max. :1.0000 Max. :1.0000 Max. :1.0000 Max. :1.0000
#Verificando dados normalizados, observamos que agora existem dados nulos no nosso dataset
any(is.na(dfnorm))
## [1] TRUE
str(dfnorm)
## 'data.frame': 8124 obs. of 22 variables:
## $ cap.shape : num 1 1 0 1 1 1 0 0 1 0 ...
## $ cap.surface : num 0.667 0.667 0.667 1 0.667 ...
## $ cap.color : num 0.444 1 0.889 0.889 0.333 ...
## $ bruises : num 1 1 1 1 0 1 1 1 1 1 ...
## $ odor : num 0.75 0 0.375 0.75 0.625 0 0 0.375 0.75 0 ...
## $ gill.attachment : num 1 1 1 1 1 1 1 1 1 1 ...
## $ gill.spacing : num 0 0 0 0 1 0 0 0 0 0 ...
## $ gill.size : num 1 0 0 1 0 0 0 0 1 0 ...
## $ gill.color : num 0.364 0.364 0.455 0.455 0.364 ...
## $ stalk.shape : num 0 0 0 0 1 0 0 0 0 0 ...
## $ stalk.root : num 0.75 0.5 0.5 0.75 0.75 0.5 0.5 0.5 0.75 0.5 ...
## $ stalk.surface.above.ring: num 0.667 0.667 0.667 0.667 0.667 ...
## $ stalk.surface.below.ring: num 0.667 0.667 0.667 0.667 0.667 ...
## $ stalk.color.above.ring : num 0.875 0.875 0.875 0.875 0.875 0.875 0.875 0.875 0.875 0.875 ...
## $ stalk.color.below.ring : num 0.875 0.875 0.875 0.875 0.875 0.875 0.875 0.875 0.875 0.875 ...
## $ veil.type : num NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ...
## $ veil.color : num 0.667 0.667 0.667 0.667 0.667 ...
## $ ring.number : num 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 ...
## $ ring.type : num 1 1 1 1 0 1 1 1 1 1 ...
## $ spore.print.color : num 0.25 0.375 0.375 0.25 0.375 0.25 0.25 0.375 0.25 0.25 ...
## $ population : num 0.6 0.4 0.4 0.6 0 0.4 0.4 0.6 0.8 0.6 ...
## $ habitat : num 0.833 0.167 0.5 0.833 0.167 ...
#Todos os valores da coluna "veil.type" são o mesmo, fazendo com que além de se tornar um valor "nan" (valor nulo) após a normalização, não será relevante para a criação do modelo preditivo.
table(df$veil.type)
##
## p
## 8124
#Eliminando a coluna "veil.type"
dfnum <- dfnum[-17]
dfnorm <- dfnorm[-16]
Separando dados de treino e de teste:
#Criando amostras randômicas para dados de treino e de teste (função do pacote caTools)
set.seed(101)
amostra <- sample.split(dfnorm, SplitRatio = 0.70)
# 70% dos dados para treino
dados_treino = subset(dfnorm, amostra == TRUE)
dim(dados_treino)
## [1] 5416 21
# 30% dos dados para teste
dados_teste = subset(dfnorm, amostra == FALSE)
dim(dados_teste)
## [1] 2708 21
#Criando labels que serão utilizados para avaliação do modelo
dados_treino_labels <- subset(dfnum[,1], amostra == TRUE)
dados_teste_labels <- subset(dfnum[,1], amostra == FALSE)
#Criando o modelo (função do pacote "class")
modelo <- knn(train = dados_treino,
test = dados_teste,
cl = dados_treino_labels,
k = 10)
# Gerando Confusion Matrix (pacote caret)
confusionMatrix(dados_teste_labels, modelo)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Comestível Venenoso
## Comestível 1414 0
## Venenoso 3 1291
##
## Accuracy : 0.9989
## 95% CI : (0.9968, 0.9998)
## No Information Rate : 0.5233
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.9978
## Mcnemar's Test P-Value : 0.2482
##
## Sensitivity : 0.9979
## Specificity : 1.0000
## Pos Pred Value : 1.0000
## Neg Pred Value : 0.9977
## Prevalence : 0.5233
## Detection Rate : 0.5222
## Detection Prevalence : 0.5222
## Balanced Accuracy : 0.9989
##
## 'Positive' Class : Comestível
##
A Confusion Matrix gerada nos diz que:
Label Positivo = 1 = Comestível
Label Negativo = 2 = Venenoso
-Verdadeiro Negativo: 1291
-Falso Negativo: 3
-Falso Positivo: 0
-Verdadeiro Positivo: 1414
Accuracy mede a exatidão do modelo de classificação, é a proporção de resultados verdadeiros em relação ao total de casos analisados. Podemos ver que nosso modelo possuí uma exatidão de 0,9989 (ele acerta 99,89% das previsões), poderíamos realizar tarefas de otimização, criando novas variáveis, coletando mais dados, filtrando valores, alterando o parâmetro k no modelo Knn, mas a exatidão encontrada inicialmente está de bom tamanho para o nosso trabalho.
Vamos explorar um pouco mais os dados e imaginar a seguinte situação: Se estivéssemos procurando cogumelos numa floresta para nossa próxima refeição sem o auxílio do algoritmo classificador knn, o que fazer?
É natural que em um conjunto de dados algumas características sejam mais relevantes, menos relevantes ou até redundantes para o modelo preditivo. É tarefa importante do analista de dados identificar quais variáveis devem ser consideradas, desconsideradas ou alteradas para alcançar o resultado desejado.
#Importância das variáveis para a classificação do modelo (quanto mais para a direita no gráfico, maior a importância da variável)
importancia<- randomForest( class ~ .,
data = dfnum,
ntree = 100, nodesize = 10, importance = T)
varImpPlot(importancia)
Vamos explorar as 4 variáveis mais relevantes para o modelo:
ggplot(df, aes(`spore.print.color`, fill = `class`)) + geom_bar() + labs(title = 'Gráfico de Barras Empilhadas e = Comestível, p = Venenoso', x = 'spore.print.color', y = 'Contagem de spore.print.color')
A esporada mostra a cor dos esporos do cogumelo, e é normalmente obtida espalmando a superfície produtora de esporos numa folha de plástico transparente e rígido ou folha de papel. Quando o cogumelo é retirado, a cor dos esporos deverá ser visível.
Podemos observar que a maior parte dos cogumelos venenosos possuem esporada de cor:
h(chocolate) - Cor chocolate
w(white) - Cor branca
ggplot(df, aes(`odor`, fill = `class`)) + geom_bar() + labs(title = "Gráfico de Barras Empilhadas e = Comestível, p = Venenoso'", x = "odor" , y = "Contagem de odor" )
Outra característica importante na classificação do cogumelo é o odor que o fungo possuí.
Podemos observar que a maior parte dos cogumelos venenosos possuem odor:
f(foul) - Odor desagradável
ggplot(df, aes(`gill.size`, fill = `class`)) + geom_bar() + labs(title = 'Gráfico de Barras Empilhadas e = Comestível, p = Venenoso', x = 'gill.size', y = 'Contagem de gill.size')
O tamanho das brânquias do cogumelos também devem ser observados.
Podemos observar que proporcionalmente a maior parte dos cogumelos venenosos possuem brânquias:
n(narrow) - Tamanho estreito
ggplot(df, aes(`ring.number`, fill = `class`)) + geom_bar() + labs(title = 'Gráfico de Barras Empilhadas e = Comestível, p = Venenoso', x = 'ring.number', y = 'Contagem de ring.number')
O número de anéis no cogumelo também é uma característica relevante para a nossa classificação.
Podemos observar que a maior parte dos cogumelos venenosos possuem:
o(one) - Um anel
Resumindo: Se você estiver procurando cogumelos numa floresta para a sua próxima refeição e não possuir o algoritmo knn para te auxiliar nesta tarefa, as características dos cogumelos venenosos que você tem maior probabilidade de encontrar, e que deverá evitar são:
-Spore = Esporada de cor chocolate ou branca
-Odor = Odor desagradável
-Gill.size = Brânquias estreitas
-Ring.number = Um anel