R ile Machine Learning: Lojistik Regresyon ile Mantarları Sınıflandırma

Bu yazımızda, Kaggle.com içerisindeki mantar sınıflandırma “mushroom classification” problemi için lojistik regresyon ile analiz yapacağız. Mantarların yenilebilir mi yoksa ölümcül zehirli mi olduklarının modelini oluşturacağız.

Gerekli Kütüphaneler ve Tanımlamalar

Kullanacağımız tüm kütüphaneler ile ilgili detaylar şu şekildedir:
dplyr, magrittr: Data manipülasyon
caTools: Datayı traning/test setlerine ayırma
rPSI: Population stability index (PSI)
RWeka: Gain ratio
caret: Confusion matrix
pROC: ROC curve
car: Variance inflation factor (vif)

Tüm yazılarımızda olduğu gibi öncelikle sisteminizde eksik olan kütüphanelerin kurulumunu yapalım.

install.packages(c("dplyr","magrittr","caTools",
                   "RWeka","caret","pROC","car"))

devtools::install_github("siyuany/rpsi")

Ana kütüphaneleri aktif hale getirelim.

library(dplyr)
library(magrittr)

Çalışacağımız datayı direkt olarak kaynağından alalım.

url       <- "https://raw.githubusercontent.com/toygur/REgitimMerkezi/master/dataset/mushrooms.csv"
mushrooms <- readr::read_csv(url)

Mantarlar verisetini çalışma ortamımıza (environment) aldık. Amacımız, veriseti içerisindeki class değişkenini (p:poisonous, e:edible) yani mantarın zehirli mi yoksa yenilebilir mi olduğunun modelini oluşturmak. Data içerisinde kullanılan değişkenleri ve aldıkları değerleri (veri sözlüğü) yazının sonunda detaylı olarak sunulmuştur.

Datanın özelliklerini incelediğimizde str(mushrooms) tüm sütunların character tipinde olduğu görülmektedir. Analize başlamadan önce sütunları kategorik (factor) değişkene çevirmeliyiz. dplyr kütüphanesi fonksiyonları ile bunu kolaylıkla yapabiliriz. Daha önceki yazılarımızda %>% pipeline ifadesini sıklıkla kullanmıştık. Pipeline’ı ilk kez %<>% biçiminde kullandığımıza dikkat ediniz. Bu kullanım ile işlemin sonucu tekrar sol tarafta yer alan değişkene atanmaktadır.

mutate_all fonksiyonu ile as.factor fonksiyonunu tüm sütunlara uygulanmıştır. Öte yandan as.factor fonksiyonu içerisine yazdığımız . (nokta) sütunları temsil etmektedir.

Bir sonraki adımda sütun isimleri değiştirilecektir. Örnek olarak cap-shape biçiminde olan sütun başlığını cap_shape biçimine dönüştürmek için gsub fonksiyonu kullanılmıştır.

mushrooms %<>% mutate_all(funs(as.factor(.)))

colnames(mushrooms) %<>% gsub(pattern = "-", 
                              replacement = "_", 
                              x = .)
mushrooms
## # A tibble: 8,124 x 23
##    class cap_~ cap_~ cap_~ brui~ odor  gill~ gill~ gill~ gill~ stal~ stal~
##    <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct> <fct>
##  1 p     x     s     n     t     p     f     c     n     k     e     e    
##  2 e     x     s     y     t     a     f     c     b     k     e     c    
##  3 e     b     s     w     t     l     f     c     b     n     e     c    
##  4 p     x     y     w     t     p     f     c     n     n     e     e    
##  5 e     x     s     g     f     n     f     w     b     k     t     e    
##  6 e     x     y     y     t     a     f     c     b     n     e     c    
##  7 e     b     s     w     t     a     f     c     b     g     e     c    
##  8 e     b     y     w     t     l     f     c     b     n     e     c    
##  9 p     x     y     w     t     p     f     c     n     p     e     e    
## 10 e     b     s     y     t     a     f     c     b     g     e     c    
## # ... with 8,114 more rows, and 11 more variables:
## #   stalk_surface_above_ring <fctr>, stalk_surface_below_ring <fctr>,
## #   stalk_color_above_ring <fctr>, stalk_color_below_ring <fctr>,
## #   veil_type <fctr>, veil_color <fctr>, ring_number <fctr>, ring_type
## #   <fctr>, spore_print_color <fctr>, population <fctr>, habitat <fctr>

Ön Hazırlık

Modelleme yapılırken verisetleri traning ve test olarak bağımlı değişkene göre belirli bir oranda ikiye ayrılmaktadır. Model, traning sete göre oluşturulur ve test sete göre sınanır. caTools kütüphanesi içerisindeki sample.split fonksiyonu ile kolaylıkla yapılabilmektedir. Bu örnek için verisetimizi %80 – %20 oranında ayırdık.

set.seed(123)
split        <- caTools::sample.split(mushrooms$class, SplitRatio = 0.8)

trainingData <- mushrooms %>% filter(split)
testData     <- mushrooms %>% filter(!split)

Population Stability Index (PSI)

Bağımlı değişkene göre iki parçaya ayırdığımız verisetinin aynı özellikleri taşıması gereklidir. Bunu sınamak için literatürde kullanılan yöntemlerden biri Population Stability Index olup rPSI kütüphanesi içerisindedir. Sonuçlar incelendiğinde traning ve test setleri başarılı şekilde ayrıştırıldığı görülmektedir.

PSI Sonuç Açıklama
PSI < 0.10 Değiliklik yok Ayırım başarılı
0.10 < PSI < 0.25 Küçük değişiklikler Kontrol için Farklı metotlara başvur
PSI > 0.25 Büyük önemli değişiklik Ayırım başarısız
p <- rPSI::psi(original = trainingData$class,
               current  = testData$class)

summary(p)
## PSI: 0 
## 
##   Levels OrgCnt CurCnt OrgPct CurPct Index
## 1      e   3366    842 0.5179 0.5182     0
## 2      p   3133    783 0.4821 0.4818     0
## 3  Total   6499   1625 1.0000 1.0000     0

Gain Ratio

Weka, Java ile yazılan, veri madenciliğinde (data mining) ön işleme (pre-processing), sınıflandırma (classification), regresyon, kümeleme (clustering), ilişkilendirme kuralları (association rules) ve görselleştirme (visualization) için araçlar içeren bir makine öğrenme algoritmaları (ML, machine learning) topluluğudur. Gain Ratio, Weka’nın attribute selection fonksiyonudur.

gainRatio <- RWeka::GainRatioAttributeEval(class ~ .,
                                           data = trainingData)

broom::tidy(sort(gainRatio, decreasing = TRUE)) 
## # A tibble: 22 x 2
##    names                        x
##    <chr>                    <dbl>
##  1 odor                     0.389
##  2 gill_size                0.255
##  3 stalk_surface_above_ring 0.232
##  4 spore_print_color        0.216
##  5 ring_type                0.206
##  6 bruises                  0.197
##  7 stalk_surface_below_ring 0.191
##  8 gill_spacing             0.157
##  9 gill_color               0.138
## 10 stalk_color_above_ring   0.131
## # ... with 12 more rows

Confusion Matrisi

Confusion matris, modele göre tahmin edilen sonuçlar ile gerçek sonuçlar arasındaki ilişkilerin izlendiği matristir. caret kütüphanesi içerisindeki confusionMatrix fonksiyonu ile bulunmaktadır. Kullanımını kolaylaştırmak için kendi fonksiyonumuzu oluşturalım ve içerisinde hesaplamaları gerçekleştirelim.

Burada kütüphanenin fonksiyon içerisindeki kullanım biçimine değinmeliyiz. caret kütüphanesinin R dosyasının genelinde aktif hale getirilmediğini aksine fonksiyonun içerisinde sadece kullanıldığı satırda aktif hale getirildiğine dikkat ediniz.

confusionMatrix fonksiyonu, model sonuçları ile ilgili bazı rasyolar da hesaplamaktadır. Detaylara kütüphane aktif iken ?confusionMatrix kodunu çalıştırarak ulaşabilirsiniz. Bunlardan biri olan accuracy rate modelin başarı ölçütüdür ancak tek başına model performansı için yeterli değildir.

getConfusionMatrix <- function(prediction, threshold = 0.5) {

  predictions <- ifelse(prediction > threshold, "p", "e")
  res         <- caret::confusionMatrix(data      = testData$class, 
                                        reference = predictions)
  return(res)
}

ROC Eğrisi (Curve)

ROC eğrisi, model sonuçlarının başarısını test etmektedir. Köşegen üzerindeki doğruya yaklaştıkça model sonuçlarının rassallaştığı söylenebilir. ROC curve üretmek için R’da bir çok kütüphane bulunmaktadır. Bu yazımızda pROC kütüphanesi içerisideki roc fonksiyonu kullanılacaktır. Bir önceki bölümde olduğu gibi kullanımı kolaylaştırmak için kendi fonksiyonumuzu oluşturalım.

plotROCCurve <- function(prediction, title, color = "red") {
  plot(pROC::roc(testData$class, prediction, direction="<"), 
       print.auc = TRUE, 
       col       = color, 
       lwd       = 2, 
       main      = title )
}

Lojistik (Logistic) Regresyon

Model 1: class ~ odor

GainRatio sonuçlarına göre ilk sırada çıkan oder değişkenini göz önüne alarak ilk modelimizi oluşturalım.

model1   <- glm(class ~ odor, 
                data = trainingData, 
                family = "binomial" )
predict1 <- predict(object  = model1, 
                    newdata = testData, 
                    type    = "response")

getConfusionMatrix(prediction = predict1)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   e   p
##          e 842   0
##          p  17 766
##                                           
##                Accuracy : 0.9895          
##                  95% CI : (0.9833, 0.9939)
##     No Information Rate : 0.5286          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.979           
##  Mcnemar's Test P-Value : 0.0001042       
##                                           
##             Sensitivity : 0.9802          
##             Specificity : 1.0000          
##          Pos Pred Value : 1.0000          
##          Neg Pred Value : 0.9783          
##              Prevalence : 0.5286          
##          Detection Rate : 0.5182          
##    Detection Prevalence : 0.5182          
##       Balanced Accuracy : 0.9901          
##                                           
##        'Positive' Class : e               
## 

Confusion matris sonuçlarına göre, test veriseti içerisindeki yenebilen (edible, e) 842 mantarın tamamı doğru tahmin edilmiştir. Öte yandan test veriseti içerisindeki zehirli 783 mantarın 766 tanesi doğru tahmin edilmiştir, 17 tanesi ise zehirli olmasına rağmen yemelik olarak tahminlenmiştir. Model1’in model performansı 0.9895 olarak gözükmektedir.

plotROCCurve(prediction = predict1, 
             title      = "Model 1: ROC Curve")

Model 2: class ~ odor + gill_size + stalk_surface_above_ring

Model1’de bulduğumuz sonuçları biraz daha iyileştirebilmek için bu sefer GainRatio sonuçlarındaki ilk üç sıradaki değişkeni seçelim ve modeli bu değişkenlere göre oluşturalım.

Elimizde birden fazla değişken olduğu için artık değişkenler arasında multicollinearity problemi olup olmadığını da incelememiz gereklidir. Bunun için car kütüphanesi içerisindeki vif (variance inflation factor) fonksiyonu kullanılacaktır. Literatürde farklı eşik değerleri olmasına rağmen genellikle VIF değerinin 10‘dan büyük olması, değişkenler arasında problem yaratabilecek multicollinearity olduğunun habercisidir.

model2   <- glm(class ~ odor + gill_size + stalk_surface_above_ring, 
                data = trainingData, 
                family = "binomial" )

car::vif(model2)
##                              GVIF Df GVIF^(1/(2*Df))
## odor                     1.951775  8        1.042682
## gill_size                1.006947  1        1.003468
## stalk_surface_above_ring 1.965334  3        1.119196
predict2 <- predict(object  = model2, 
                    newdata = testData, 
                    type    = "response")

getConfusionMatrix(prediction = predict2)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   e   p
##          e 842   0
##          p  10 773
##                                          
##                Accuracy : 0.9938         
##                  95% CI : (0.9887, 0.997)
##     No Information Rate : 0.5243         
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.9877         
##  Mcnemar's Test P-Value : 0.004427       
##                                          
##             Sensitivity : 0.9883         
##             Specificity : 1.0000         
##          Pos Pred Value : 1.0000         
##          Neg Pred Value : 0.9872         
##              Prevalence : 0.5243         
##          Detection Rate : 0.5182         
##    Detection Prevalence : 0.5182         
##       Balanced Accuracy : 0.9941         
##                                          
##        'Positive' Class : e              
## 

Confusion matris sonuçlarına göre, test veriseti içerisindeki yenebilen (edible,e) 842 mantarın tamamı yine Model1’de olduğu gibi doğru tahmin edilmiştir. Öte yandan test veriseti içerisindeki zehirli 783 mantarın 773 tanesi doğru tahmin edilmiş, 10 tanesi ise zehirli olmasına rağmen yemelik olarak tahminlenmiştir. Model2’in model performansı 0.9938 olarak gözükmektedir. Model1’den Model2’ye iyileşme oldukça iyi gözükmektedir.

plotROCCurve(prediction = predict2, 
             title      = "Model 2: ROC Curve")

Veri Sözlüğü

Değişken Değerler
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