Introdução

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.

Informações sobre o dataset

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

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

Transformação das variáveis

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

Normalização dos dados

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]

Criação do Modelo

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)

Algoritmo KNN (K-Nearest-Neighbor)

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

Interpretando o resultado do modelo preditivo

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.

Classificação sem knn

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?

Importância das variáveis

É 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:

Spore.print.color - Esporada

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

Odor - Odor

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
 

Gill.size - Tamanho das brânquias

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
 

Ring.number - Número de anéis

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

Conclusão final

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