R ile Sınıflandırma: Lojistik Regresyon, SVM, CART ve Rastgele Orman

Lojistik regresyon, SVM, CART ve rastgele ormanı aynı veriyle eğitip aynı test setinde karşılaştırıyorum. Sonuç beklenenin tersi çıkıyor.

  • Makine Öğrenmesi
  • İstatistik

Dört farklı sınıflandırma algoritması, tek bir veri seti, tek bir test kümesi. Bu yazının amacı algoritmaları tek tek anlatmak değil — her birini sitede ayrı yazılarda ele aldım. Buradaki soru şu: aynı problemde hangisi ne yapıyor ve neden?

Serinin üçüncü yazısında Carseats verisi üzerinde lojistik regresyon, destek vektör makineleri, karar ağacı ve rastgele ormanı kuruyor; dördünü de aynı test setinde, aynı metriklerle karşılaştırıyorum.

“Modelleri karşılaştırmak için hepsinin aynı veriyle eğitilmesi yetmez; hepsinin aynı veriyle ölçülmesi de gerekir.”

1. Problem ve Taban Oran

Carseats'te Sales sürekli bir değişken. Sınıflandırma problemine çevirmek için 8 bin adet eşiğinden ikiye ayırıyoruz: 8'in üzerindeki mağazalar High, altındakiler Low.

Hedefi ikiliye çevirmek ve tabakalı bölme
df <- Carseats
df$Sales <- factor(ifelse(df$Sales <= 8, "Low", "High"),
                   levels = c("Low", "High"))

table(df$Sales)        # Low 236  High 164

set.seed(5364)
idx <- unlist(lapply(levels(df$Sales), function(lv) {
  satirlar <- which(df$Sales == lv)
  sample(satirlar, size = floor(0.8 * length(satirlar)))
}))

train <- df[idx, ]    # 319 gozlem: Low 188, High 131
test  <- df[-idx, ]   # 81 gozlem:  Low  48, High  33
Önce taban oranı hesapla: Test setinde 48 Low ve 33 High var. Hiçbir şey öğrenmeyip her mağazaya "Low" diyen bir model %59,3 doğruluk elde eder. Bu, tüm modellerin aşması gereken eşik. Bir modelin "%60 doğruluk" üretmesi, bu veride tam anlamıyla hiçbir şey öğrenmemiş olmak demek.
Hedef

İkili sınıf

High (164) / Low (236) — hafif dengesiz ama kritik düzeyde değil.

Pozitif sınıf

High

Bütün duyarlılık ve kesinlik değerleri "High" sınıfı için hesaplanıyor.

Taban oran

%59,3

Çoğunluk sınıfını tahmin eden saf modelin doğruluğu.

Bölme

Tabakalı %80/%20

Sınıf oranları iki sette de korunuyor.

2. Lojistik Regresyon

En yorumlanabilir seçenekle başlıyoruz. Lojistik regresyon, doğrusal bir birleşimi sigmoid fonksiyonundan geçirerek olasılık üretir.

Model ve test tahmini
set.seed(86451)
glm_fit <- glm(Sales ~ ., data = train, family = "binomial")

# "High" ikinci seviye oldugu icin model P(High) tahmin ediyor
olasilik <- predict(glm_fit, newdata = test, type = "response")
tahmin   <- factor(ifelse(olasilik > 0.5, "High", "Low"),
                   levels = c("Low", "High"))

table(Tahmin = tahmin, Gercek = test$Sales)
Sık yapılan hata: predict() çağrısına newdata = test yazmayı unutmak. Bu durumda R sessizce eğitim verisi üzerinde tahmin üretir, sen de modelin test performansını ölçtüğünü sanırsın. Faktör seviyelerinin sırası da önemli: modelin hangi sınıfı modellediği levels() ile kontrol edilmeli.
Lojistik regresyon — model özeti (kısaltılmış)
Coefficients:
                 Estimate Std. Error z value Pr(>|z|)
(Intercept)     -5.282068   2.933818  -1.800   0.0718 .
CompPrice        0.192240   0.029544   6.507 7.68e-11 ***
Income           0.030561   0.009737   3.139   0.0017 **
Advertising      0.343855   0.064202   5.356 8.52e-08 ***
Price           -0.188692   0.026197  -7.203 5.90e-13 ***
ShelveLocGood    9.076140   1.282961   7.074 1.50e-12 ***
ShelveLocMedium  4.229255   0.805378   5.251 1.51e-07 ***
Age             -0.088403   0.018244  -4.846 1.26e-06 ***

    Null deviance: 431.99  on 318  degrees of freedom
Residual deviance: 126.34  on 307  degrees of freedom
AIC: 150.34

Sapma (deviance) 431,99'dan 126,34'e düşüyor: model boş modele göre ciddi bir açıklama gücü kazanmış. Katsayılar da regresyon yazısındakiyle aynı hikâyeyi anlatıyor — raf konumu Good olduğunda yüksek satış log-odds'u 9,08 artıyor.

Gerçek
LowHigh
TahminLow425
High628

Accuracy 0,864 · Duyarlılık 0,849 · Özgüllük 0,875 · F1 0,836 · AUC 0,929

0,5 eşiği nereden geliyor?

Yukarıdaki karışıklık matrisi tek bir karardan doğuyor: olasılık 0,5'i geçerse "High". Bu eşik matematikten gelmiyor — bir varsayım. Değiştirdiğinde model değişmez, sadece modelin ürettiği olasılıkları nasıl karara çevirdiğin değişir.

Farklı eşiklerde aynı model
p <- predict(glm_fit, newdata = test, type = "response")

for (esik in c(0.3, 0.4, 0.5, 0.6, 0.7)) {
  tahmin <- factor(ifelse(p > esik, "High", "Low"),
                   levels = c("Low", "High"))
  print(table(tahmin, test$Sales))
}

# Youden J ile en iyi esik:
coords(roc(test$Sales, p), "best", best.method = "youden")
# threshold = 0.5924
EşikAccuracyDuyarlılıkÖzgüllükKesinlikF1
0,30,8270,8790,7920,7440,806
0,40,8520,8490,8540,8000,824
0,50,8640,8490,8750,8240,836
0,60,8890,8490,9170,8750,862
0,70,8400,7270,9170,8570,787

0,6 eşiği doğruluğu 0,864'ten 0,889'a çıkarıyor — üç mağaza daha doğru sınıflanıyor ve duyarlılıktan hiçbir şey kaybedilmiyor. ROC eğrisi üzerinde Youden J ölçütünün bulduğu optimum eşik de 0,592, yani 0,5 değil.

Ama eşiği test setinde seçme. Yukarıdaki tablo bir ders niteliğinde, bir yöntem değil: eşiği test verisine bakarak ayarlarsan artık o test seti "görülmemiş veri" olmaktan çıkar ve 0,889 iyimser bir sayı hâline gelir. Doğrusu, eşiği eğitim setinden ayrılan bir doğrulama kümesinde veya çapraz doğrulamayla seçmek. Eşik bir hiperparametredir ve diğerleri gibi muamele görmelidir.

Eşiği nereye koyacağın aslında bir maliyet sorusu. Yüksek satış potansiyelli bir mağazayı kaçırmanın maliyeti, boşuna yatırım yapmanın maliyetinden büyükse eşik aşağı çekilir; tersiyse yukarı.

3. Destek Vektör Makineleri (SVM)

SVM, iki sınıfı ayıran ve marjı en geniş tutan sınırı arar. Doğrusal çekirdekle başlıyoruz.

SVM modeli
library(e1071)

set.seed(144)
svm_fit <- svm(Sales ~ ., data = train,
               kernel = "linear", cost = 1,
               scale = TRUE, type = "C-classification")

tahmin_svm <- predict(svm_fit, newdata = test)
table(Tahmin = tahmin_svm, Gercek = test$Sales)

svm_fit$tot.nSV   # 89 destek vektoru
scale = TRUE şart: SVM mesafe tabanlı bir yöntem. Population (10–509) ile Advertising (0–29) aynı ölçekte değilken, büyük ölçekli değişken sınırı domine eder. Orijinal çalışmada scale = FALSE kullanılmıştı; ölçekleme açıldığında model belirgin biçimde iyileşiyor.
Gerçek
LowHigh
TahminLow435
High528

Accuracy 0,877 · Duyarlılık 0,849 · Özgüllük 0,896 · F1 0,849 — dört model arasında en yüksek doğruluk.

cost ve çekirdek: varsayılanlarla mı kalalım?

Yukarıdaki model cost = 1 ve doğrusal çekirdekle kuruldu; ikisi de varsayılan tercih. cost, SVM'in marj ihlallerine ne kadar tolerans göstereceğini belirler: küçük değer geniş ama hoşgörülü bir marj, büyük değer dar ve katı bir sınır demek. Doğru değer veriye bağlı, dolayısıyla aranması gerekir.

Hiperparametre taraması (eğitim setinde 10 katlı CV)
set.seed(144)
t1 <- tune(svm, Sales ~ ., data = train, kernel = "linear",
          ranges = list(cost = c(0.01, 0.1, 1, 5, 10)))
t1$performances

t2 <- tune(svm, Sales ~ ., data = train, kernel = "radial",
          ranges = list(cost = c(0.1, 1, 5, 10),
                        gamma = c(0.01, 0.05, 0.1)))
t2$best.parameters   # cost = 5, gamma = 0.05
costCV hatasıDağılım (sd)
0,010,16270,0774
0,100,10650,0573
1,000,11280,0446
5,000,11910,0459
10,000,11280,0513

En iyi cost değeri 1 değil 0,1 — ama fark yalnızca 0,006 ve katlar arası standart sapma 0,057. Yani bu tarama "cost'u değiştir" demiyor; "bu veride cost'un geniş bir aralıkta önemi yok" diyor. Tek istisna 0,01: fazla gevşek bir marj modeli belirgin biçimde bozuyor.

ÇekirdekEn iyi parametrelerCV hatasıDestek vektörüTest doğruluğu
Doğrusalcost = 0,10,1065890,877
Radyal (RBF)cost = 5, gamma = 0,050,10661280,827

Radyal çekirdek doğrusal olmayan sınırlar çizebiliyor ve genellikle "daha güçlü" sayılıyor. Burada çapraz doğrulama hatası neredeyse birebir aynı (0,1066'ya karşı 0,1065), test doğruluğu ise beş puan daha düşük. Üstelik 89 yerine 128 destek vektörü kullanıyor — yani aynı işi daha karmaşık bir modelle yapıyor.

Daha esnek model, daha iyi model değildir. Radyal çekirdek doğrusal sınırı da öğrenebilir; ama bunu öğrenmek için veriden daha fazla kanıt ister. Sınır zaten doğrusalken ek esneklik yalnızca varyans ekler. Bu, serinin en tekrar eden dersi: model karmaşıklığı veriye uymalı, beklentiye değil.

4. Karar Ağacı (CART)

CART, veriyi ardışık ikili bölünmelerle ayırır. En büyük avantajı okunabilirliği: modeli bir dizi "eğer–ise" kuralı olarak görebilirsin.

Ağacı kurmak ve budamak
library(rpart)
library(rpart.plot)

set.seed(144)
cart_fit <- rpart(Sales ~ ., data = train, method = "class")

# Capraz dogrulama hatasini en kucuk yapan cp
printcp(cart_fit)
en_iyi_cp <- cart_fit$cptable[which.min(cart_fit$cptable[, "xerror"]), "CP"]

cart_budanmis <- prune(cart_fit, cp = en_iyi_cp)
rpart.plot(cart_budanmis)
cp tablosu — budama kararı
          CP nsplit rel error    xerror       xstd
1 0.32061069      0 1.0000000 1.0000000 0.06707303
2 0.07633588      1 0.6793893 0.6793893 0.06114941
3 0.05343511      2 0.6030534 0.7557252 0.06307573
4 0.03816794      4 0.4961832 0.6335878 0.05981752
5 0.02798982      6 0.4198473 0.5877863 0.05834268
6 0.02290076      9 0.3358779 0.5648855 0.05754827   <- en dusuk xerror
7 0.01000000     11 0.2900763 0.5725191 0.05781745

Tablodaki iki sütun farkı anlatıyor: rel error eğitim hatası, ağaç büyüdükçe sürekli düşüyor (1,00 → 0,29). xerror ise çapraz doğrulama hatası ve 9 bölünmeden sonra yükselmeye başlıyor. İşte aşırı öğrenmenin başladığı nokta burası. En düşük xerror'u veren cp = 0,0229 seçiliyor ve ağaç 10 yaprağa budanıyor.

Gerçek
LowHigh
TahminLow3811
High1022

Accuracy 0,741 · Duyarlılık 0,667 · Özgüllük 0,792 · F1 0,677 · AUC 0,769 — dört model arasında en düşük performans.

5. Rastgele Orman

Rastgele orman, yüzlerce karar ağacını farklı örneklem ve değişken alt kümeleriyle eğitip oylarını birleştirir. Tek ağacın kararsızlığını büyük ölçüde giderir.

Rastgele orman
library(randomForest)

set.seed(144)
rf_fit <- randomForest(Sales ~ ., data = train, importance = TRUE)
rf_fit

# OOB estimate of error rate: 17.55%
# Confusion matrix (OOB):
#      Low High class.error
# Low  171   17   0.0904
# High  39   92   0.2977

tahmin_rf <- predict(rf_fit, newdata = test)
importance(rf_fit)[, "MeanDecreaseGini"]

OOB (out-of-bag) hatası, rastgele ormanın bedava gelen çapraz doğrulaması: her ağaç eğitilirken dışarıda kalan gözlemler üzerinde test edilir. Buradaki %17,55'lik OOB hatası, ayrı bir doğrulama seti ayırmaya gerek kalmadan elde edilen bir tahmin.

DeğişkenMeanDecreaseGini
Price34,04
ShelveLoc27,46
Age18,69
Advertising18,44
CompPrice16,82
Income13,52

Değişken önemi sıralaması regresyon modelinin anlattığıyla örtüşüyor: fiyat ve raf konumu açık ara önde. Üç farklı yöntemin aynı iki değişkeni öne çıkarması, bulgunun yönteme değil veriye ait olduğunu gösteriyor.

Gerçek
LowHigh
TahminLow4313
High520

Accuracy 0,778 · Duyarlılık 0,606 · Özgüllük 0,896 · F1 0,690 · AUC 0,878

6. Dört Model, Tek Tablo

Bütün modeller aynı eğitim setiyle kuruldu ve aynı 81 gözlemlik test setinde ölçüldü. Pozitif sınıf "High".

ModelAccuracyDuyarlılıkÖzgüllükF1AUC
SVM (doğrusal)0,8770,8490,8960,8490,926
Lojistik regresyon0,8640,8490,8750,8360,929
Rastgele orman0,7780,6060,8960,6900,878
CART0,7410,6670,7920,6770,769
Taban oran0,5930,0001,000—0,500
Accuracy karşılaştırması
  • SVMAUC 0,9260,877
  • Lojistik regresyonAUC 0,9290,864
  • Rastgele orman500 ağaç0,778
  • CART10 yaprak0,741
  • Taban orançoğunluk sınıfı0,593

7. Neden Basit Modeller Kazandı?

Beklenti genellikle tersidir: rastgele ormanın lojistik regresyonu geçmesi beklenir. Burada tam tersi oldu ve sebebi veride.

01

İlişki zaten doğrusal

Regresyon yazısında gördük: aynı değişkenler satışı doğrusal bir modelle %87 açıklıyor. Doğrusal bir sınır bu problemi çözmeye yetiyor.

02

Eşik yapay bir sınır

High/Low ayrımı 8 bin adette keyfî olarak çizildi. Sınıra yakın mağazalar ağaç tabanlı yöntemleri zorluyor.

03

Veri küçük

319 eğitim gözlemi, topluluk yöntemlerinin avantajını göstermesi için az. Rastgele orman veri büyüdükçe fark yaratır.

04

Ağaçlar eşiği kesemiyor

CART sürekli bir değişkeni basamaklarla böler; sürekli bir eşiği yakalamak doğrusal modelin işi.

Rastgele ormanın profili ayrıca dikkat çekici: özgüllüğü 0,896 ile SVM ile aynı, ama duyarlılığı 0,606. Yani düşük satışlı mağazaları iyi buluyor, yüksek satışlıların üçte birini kaçırıyor. Eğer amaç "yüksek potansiyelli mağazaları tespit etmek" ise bu model, doğruluk sıralamasında ikinci olmasına rağmen iş açısından en kötü seçenek olabilir.

Metrik seçimi iş sorusudur: Doğruluk sıralamasına göre SVM kazanıyor. Ama "kaç yüksek satışlı mağazayı yakaladık?" diye sorarsan SVM ve lojistik berabere (0,849), rastgele orman belirgin biçimde geride. "Yanlış alarm maliyeti yüksek mi?" diye sorarsan özgüllükte SVM ve rastgele orman eşit. Tek bir sayıya bakıp model seçmek, sorunun ne olduğunu bilmeden cevap vermektir.

8. Model Karşılaştırmasında Üç Tuzak

TuzakNasıl oluşur?Sonucu
Farklı veri üzerinde ölçmekBir model eğitim, diğeri test setinde değerlendirilir.Karşılaştırma tamamen geçersizdir; eğitimde ölçülen model haksız kazanır.
Taban oranı atlamakDoğruluk tek başına raporlanır.%60 doğruluk başarı sanılır; oysa hiçbir şey öğrenmemiş olabilir.
Tek bölünmeye güvenmekSonuç tek bir rastgele eğitim-test ayrımından gelir.Tohum değişince sıralama değişebilir; fark gürültü olabilir.

Üçüncüsü bu çalışma için de geçerli: SVM ile lojistik regresyon arasındaki 0,013'lük fark, 81 gözlemlik bir test setinde tek bir mağaza demek. Bu farka dayanarak "SVM daha iyi" demek doğru olmaz. Gerçek bir karşılaştırma için tekrarlı çapraz doğrulama gerekir.

Daha sağlam karşılaştırma: tekrarlı çapraz doğrulama
library(caret)

ctrl <- trainControl(method = "repeatedcv", number = 10, repeats = 5,
                     classProbs = TRUE, summaryFunction = twoClassSummary)

modeller <- lapply(c("glm", "svmLinear", "rpart", "rf"), function(m) {
  set.seed(144)
  train(Sales ~ ., data = train, method = m,
        metric = "ROC", trControl = ctrl)
})

summary(resamples(setNames(modeller, c("glm", "svm", "cart", "rf"))))

9. Fark Gerçek mi? Tekrarlı Çapraz Doğrulama

Önceki bölüm bir soruyla kapandı: SVM ile lojistik regresyon arasındaki 0,013'lük fark gerçek mi, gürültü mü? Bu sorunun iki cevabı var ve ikisi de aynı yöne işaret ediyor.

Birinci cevap: McNemar testi

İki model aynı test setinde ölçüldüğünde, doğruluk oranlarını bağımsız örneklermiş gibi karşılaştırmak yanlış olur — ikisi de aynı 81 mağazayı tahmin ediyor. Doğru araç eşleştirilmiş veriler için tasarlanmış McNemar testi. Sorduğu şey şu: bir modelin doğru, diğerinin yanlış bildiği mağazalar dengeli mi dağılmış?

McNemar testi
dogru_svm <- pred_svm == test$Sales
dogru_glm <- pred_glm == test$Sales

mcnemar.test(table(dogru_svm, dogru_glm))
# McNemar's chi-squared = 0, df = 1, p-value = 1
KarşılaştırmaYalnız 1. doğruYalnız 2. doğruχ²pSonuç
SVM – Lojistik100,0001,000Fark yok
SVM – Rastgele orman1243,0630,080Sınırda
SVM – CART1654,7620,029Anlamlı fark
Lojistik – Rastgele orman1142,4000,121Fark yok

Sayılar tabloyu bitiriyor: SVM ile lojistik regresyon 81 mağazadan yalnızca birinde farklı karar veriyor. Doğruluk tablosunda SVM'i birinci sıraya koyan şey tek bir mağaza. "SVM daha iyi" cümlesinin arkasında duracak hiçbir kanıt yok. Buna karşılık SVM ile CART arasındaki fark (p = 0,029) gerçek.

İkinci cevap: 5 × 10 katlı çapraz doğrulama

Tek bir bölünme tek bir fotoğraf. Veriyi 10 kata bölüp bunu 5 kez farklı tohumlarla tekrarlarsak elimizde her model için 50 ölçüm olur — ve sıralamanın ne kadar kararlı olduğunu görebiliriz.

Dört model, 50 kat
# her tekrar icin tabakali 10 kat; 4 model x 50 kat = 200 egitim
for (tekrar in 1:5) {
  set.seed(1000 + tekrar)
  # ... sinif oranlarini koruyarak 10 kata bol ...
  for (k in 1:10) {
    egitim <- df[kat != k, ]; dogrulama <- df[kat == k, ]
    # glm / svm / rpart / randomForest egitilir ve olculur
  }
}
ModelOrtalama doğrulukStandart sapmaEn kötü katEn iyi katKazandığı kat
Lojistik regresyon0,88860,05220,7561,00040 / 50
SVM (doğrusal)0,88150,05150,7561,0006 / 50
Rastgele orman0,81070,07600,5750,9494 / 50
CART0,74700,08110,5750,8780 / 50

Sıralama değişti. Tek bölünmede SVM birinciydi; 50 katın ortalamasında lojistik regresyon önde ve 50 katın 40'ında kazanıyor. Eşleştirilmiş t-testi farkı anlamlı buluyor: ortalama fark 0,0071, p = 0,024. Küçük bir fark ama tutarlı.

Alt sıralarda tablo daha net: SVM ile rastgele orman arasındaki 0,071'lik fark p < 0,00001 ile anlamlı. Yani "doğrusal modeller bu veride topluluk yöntemlerini geçiyor" iddiası tek bir bölünmenin kaprisi değil — 50 katta da aynı yönde duruyor.

Standart sapmalara da bak: CART ve rastgele ormanın kat-kat oynaklığı (0,081 ve 0,076), doğrusal modellerinkinin (0,052) yaklaşık 1,5 katı. Ağaç tabanlı yöntemler burada yalnızca daha az doğru değil, aynı zamanda daha kararsız. Küçük veride bu iki sorun genellikle birlikte gelir.

Sonuç

Dört algoritma aynı veriyle eğitildi, aynı test setinde ölçüldü ve sonuç beklenenin tersi çıktı: doğrusal modeller topluluk yöntemlerini geçti. Bu, "rastgele orman her zaman daha iyidir" varsayımının neden test edilmesi gerektiğini gösteriyor.

Ama asıl ders tek bir tablonun içinde değil, o tabloyu sorgulamakta. Test setindeki sıralama SVM'i birinci gösteriyordu; McNemar testi bu üstünlüğün tek bir mağazadan ibaret olduğunu, 50 katlı çapraz doğrulama ise ortalamada lojistik regresyonun önde olduğunu söyledi. İlk tablo yanlış değildi — yalnızca tek bir bölünmenin fotoğrafıydı.

Bütün modellerin ortak noktası ise daha değerli: fiyat ve raf konumu, hangi algoritmayı kullanırsan kullan en belirleyici iki değişken. Bir bulgu üç farklı yöntemde de ortaya çıkıyorsa, o bulgu modele değil veriye aittir.

“En iyi model, doğruluk tablosunda ilk sırada olan değil; iş probleminin sorduğu soruya en iyi cevabı verendir.”

Teorik arka plan: Lojistik Regresyon, Karar Ağacı / CART ve Confusion Matrix yazılarında yöntemlerin ayrıntısını ele aldım. Kaynak kod: github.com/ozgurnrttn/Machine-Learning

Four classification algorithms, one data set, one test set. The aim of this post is not to explain each algorithm — I covered them individually elsewhere on the site. The question here is: on the same problem, what does each one do, and why?

In this third post of the series I fit logistic regression, a support vector machine, a decision tree and a random forest on the Carseats data, and compare all four on the same test set with the same metrics.

“Comparing models takes more than training them on the same data; they also have to be measured on the same data.”

1. The Problem and the Base Rate

Sales in Carseats is continuous. To turn this into a classification problem we split it at 8 thousand units: stores above 8 are High, the rest are Low.

Binarizing the target and a stratified split
df <- Carseats
df$Sales <- factor(ifelse(df$Sales <= 8, "Low", "High"),
                   levels = c("Low", "High"))

table(df$Sales)        # Low 236  High 164

set.seed(5364)
idx <- unlist(lapply(levels(df$Sales), function(lv) {
  rows <- which(df$Sales == lv)
  sample(rows, size = floor(0.8 * length(rows)))
}))

train <- df[idx, ]    # 319 rows: Low 188, High 131
test  <- df[-idx, ]   # 81 rows:  Low  48, High  33
Compute the base rate first: the test set holds 48 Low and 33 High. A model that learns nothing and labels every store "Low" reaches 59.3% accuracy. That is the bar every model has to clear. A model reporting "60% accuracy" on this data has learned essentially nothing.
Target

Binary class

High (164) / Low (236) — slightly imbalanced but not critically so.

Positive class

High

All sensitivity and precision figures are computed for the "High" class.

Base rate

59.3%

The accuracy of a naive model predicting the majority class.

Split

Stratified 80/20

Class ratios preserved in both sets.

2. Logistic Regression

We start with the most interpretable option. Logistic regression passes a linear combination through the sigmoid function to produce a probability.

Model and test prediction
set.seed(86451)
glm_fit <- glm(Sales ~ ., data = train, family = "binomial")

# "High" is the second level, so the model predicts P(High)
prob <- predict(glm_fit, newdata = test, type = "response")
pred <- factor(ifelse(prob > 0.5, "High", "Low"),
               levels = c("Low", "High"))

table(Prediction = pred, Actual = test$Sales)
A common mistake: forgetting newdata = test in the predict() call. R then silently predicts on the training data while you believe you are measuring test performance. The order of the factor levels matters too: check with levels() which class the model is modelling.
Logistic regression — model summary (abridged)
Coefficients:
                 Estimate Std. Error z value Pr(>|z|)
(Intercept)     -5.282068   2.933818  -1.800   0.0718 .
CompPrice        0.192240   0.029544   6.507 7.68e-11 ***
Income           0.030561   0.009737   3.139   0.0017 **
Advertising      0.343855   0.064202   5.356 8.52e-08 ***
Price           -0.188692   0.026197  -7.203 5.90e-13 ***
ShelveLocGood    9.076140   1.282961   7.074 1.50e-12 ***
ShelveLocMedium  4.229255   0.805378   5.251 1.51e-07 ***
Age             -0.088403   0.018244  -4.846 1.26e-06 ***

    Null deviance: 431.99  on 318  degrees of freedom
Residual deviance: 126.34  on 307  degrees of freedom
AIC: 150.34

Deviance falls from 431.99 to 126.34: the model gains substantial explanatory power over the null model. The coefficients tell the same story as the regression post — a Good shelving location raises the log-odds of high sales by 9.08.

Actual
LowHigh
PredictedLow425
High628

Accuracy 0.864 · Sensitivity 0.849 · Specificity 0.875 · F1 0.836 · AUC 0.929

Where does the 0.5 threshold come from?

The confusion matrix above rests on a single decision: call it "High" when the probability passes 0.5. That threshold does not come from the mathematics — it is an assumption. Changing it does not change the model, only how you turn the model's probabilities into decisions.

The same model at different thresholds
p <- predict(glm_fit, newdata = test, type = "response")

for (cut in c(0.3, 0.4, 0.5, 0.6, 0.7)) {
  pred <- factor(ifelse(p > cut, "High", "Low"),
                 levels = c("Low", "High"))
  print(table(pred, test$Sales))
}

# Best threshold by Youden's J:
coords(roc(test$Sales, p), "best", best.method = "youden")
# threshold = 0.5924
ThresholdAccuracySensitivitySpecificityPrecisionF1
0.30.8270.8790.7920.7440.806
0.40.8520.8490.8540.8000.824
0.50.8640.8490.8750.8240.836
0.60.8890.8490.9170.8750.862
0.70.8400.7270.9170.8570.787

A threshold of 0.6 lifts accuracy from 0.864 to 0.889 — three more stores classified correctly, with no loss in sensitivity. Youden's J on the ROC curve also puts the optimum at 0.592 rather than 0.5.

But do not pick the threshold on the test set. The table above is a lesson, not a method: tune the threshold by looking at the test data and that test set stops being unseen, which makes 0.889 an optimistic number. The right move is to choose the threshold on a validation split carved out of the training data, or by cross-validation. A threshold is a hyperparameter and deserves the same treatment as any other.

Where you put it is ultimately a question of cost. If missing a high-potential store costs more than investing in the wrong one, the threshold comes down; if the reverse holds, it goes up.

3. Support Vector Machines

An SVM looks for the boundary that separates the two classes with the widest margin. We start with a linear kernel.

The SVM model
library(e1071)

set.seed(144)
svm_fit <- svm(Sales ~ ., data = train,
               kernel = "linear", cost = 1,
               scale = TRUE, type = "C-classification")

pred_svm <- predict(svm_fit, newdata = test)
table(Prediction = pred_svm, Actual = test$Sales)

svm_fit$tot.nSV   # 89 support vectors
scale = TRUE is essential: SVM is a distance-based method. With Population (10–509) and Advertising (0–29) on different scales, the large-scale variable dominates the boundary. The original study used scale = FALSE; turning scaling on improves the model noticeably.
Actual
LowHigh
PredictedLow435
High528

Accuracy 0.877 · Sensitivity 0.849 · Specificity 0.896 · F1 0.849 — the highest accuracy of the four.

cost and kernel: should we keep the defaults?

The model above was fitted with cost = 1 and a linear kernel — both default choices. cost controls how much margin violation the SVM tolerates: a small value buys a wide but forgiving margin, a large value a narrow and strict boundary. The right value depends on the data, so it has to be searched for.

Hyperparameter search (10-fold CV on the training set)
set.seed(144)
t1 <- tune(svm, Sales ~ ., data = train, kernel = "linear",
          ranges = list(cost = c(0.01, 0.1, 1, 5, 10)))
t1$performances

t2 <- tune(svm, Sales ~ ., data = train, kernel = "radial",
          ranges = list(cost = c(0.1, 1, 5, 10),
                        gamma = c(0.01, 0.05, 0.1)))
t2$best.parameters   # cost = 5, gamma = 0.05
costCV errorDispersion (sd)
0.010.16270.0774
0.100.10650.0573
1.000.11280.0446
5.000.11910.0459
10.000.11280.0513

The best cost is 0.1 rather than 1 — but the gap is only 0.006 against a fold-to-fold standard deviation of 0.057. So the search is not telling us to change cost; it is telling us that cost barely matters across a wide range in this dataset. The one exception is 0.01: too loose a margin clearly degrades the model.

KernelBest parametersCV errorSupport vectorsTest accuracy
Linearcost = 0.10.1065890.877
Radial (RBF)cost = 5, gamma = 0.050.10661280.827

The radial kernel can draw non-linear boundaries and is usually treated as the "stronger" option. Here its cross-validated error is all but identical (0.1066 against 0.1065) and its test accuracy is five points lower. It also leans on 128 support vectors instead of 89 — doing the same job with a more complicated model.

A more flexible model is not a better model. The radial kernel can learn a linear boundary too; it just needs more evidence from the data to do so. When the boundary is already linear, the extra flexibility only adds variance. This is the most repeated lesson of the series: model complexity should match the data, not expectations.

4. Decision Tree (CART)

CART splits the data through successive binary partitions. Its biggest advantage is readability: you can see the model as a series of if–then rules.

Growing and pruning the tree
library(rpart)
library(rpart.plot)

set.seed(144)
cart_fit <- rpart(Sales ~ ., data = train, method = "class")

# The cp that minimizes cross-validated error
printcp(cart_fit)
best_cp <- cart_fit$cptable[which.min(cart_fit$cptable[, "xerror"]), "CP"]

cart_pruned <- prune(cart_fit, cp = best_cp)
rpart.plot(cart_pruned)
The cp table — the pruning decision
          CP nsplit rel error    xerror       xstd
1 0.32061069      0 1.0000000 1.0000000 0.06707303
2 0.07633588      1 0.6793893 0.6793893 0.06114941
3 0.05343511      2 0.6030534 0.7557252 0.06307573
4 0.03816794      4 0.4961832 0.6335878 0.05981752
5 0.02798982      6 0.4198473 0.5877863 0.05834268
6 0.02290076      9 0.3358779 0.5648855 0.05754827   <- lowest xerror
7 0.01000000     11 0.2900763 0.5725191 0.05781745

Two columns tell the story: rel error is the training error and keeps falling as the tree grows (1.00 → 0.29). xerror is the cross-validated error, and after nine splits it starts rising again. That is where overfitting begins. We take the cp = 0.0229 with the lowest xerror and prune the tree to 10 leaves.

Actual
LowHigh
PredictedLow3811
High1022

Accuracy 0.741 · Sensitivity 0.667 · Specificity 0.792 · F1 0.677 · AUC 0.769 — the weakest of the four.

5. Random Forest

A random forest trains hundreds of decision trees on different samples and variable subsets, then combines their votes. It largely removes the instability of a single tree.

Random forest
library(randomForest)

set.seed(144)
rf_fit <- randomForest(Sales ~ ., data = train, importance = TRUE)
rf_fit

# OOB estimate of error rate: 17.55%
# Confusion matrix (OOB):
#      Low High class.error
# Low  171   17   0.0904
# High  39   92   0.2977

pred_rf <- predict(rf_fit, newdata = test)
importance(rf_fit)[, "MeanDecreaseGini"]

The out-of-bag (OOB) error is a random forest's built-in cross-validation: each tree is tested on the observations left out while it was trained. The 17.55% OOB error here is an estimate obtained without setting aside a separate validation set.

VariableMeanDecreaseGini
Price34.04
ShelveLoc27.46
Age18.69
Advertising18.44
CompPrice16.82
Income13.52

The importance ranking matches what the regression model said: price and shelving location are far ahead. Three different methods highlighting the same two variables suggests the finding belongs to the data, not to the method.

Actual
LowHigh
PredictedLow4313
High520

Accuracy 0.778 · Sensitivity 0.606 · Specificity 0.896 · F1 0.690 · AUC 0.878

6. Four Models, One Table

Every model was trained on the same training set and measured on the same 81-observation test set. The positive class is "High".

ModelAccuracySensitivitySpecificityF1AUC
SVM (linear)0.8770.8490.8960.8490.926
Logistic regression0.8640.8490.8750.8360.929
Random forest0.7780.6060.8960.6900.878
CART0.7410.6670.7920.6770.769
Base rate0.5930.0001.000—0.500
Accuracy comparison
  • SVMAUC 0.9260.877
  • Logistic regressionAUC 0.9290.864
  • Random forest500 trees0.778
  • CART10 leaves0.741
  • Base ratemajority class0.593

7. Why Did the Simple Models Win?

The expectation usually runs the other way: a random forest is supposed to beat logistic regression. Here the opposite happened, and the reason lies in the data.

01

The relationship is already linear

We saw it in the regression post: the same variables explain 87% of sales with a linear model. A linear boundary is enough for this problem.

02

The threshold is artificial

The High/Low split was drawn arbitrarily at 8 thousand units. Stores near the boundary are hard for tree-based methods.

03

The data is small

319 training rows is little for ensembles to show their advantage. Random forests pull ahead as data grows.

04

Trees cannot cut a smooth threshold

CART splits a continuous variable into steps; catching a smooth threshold is a linear model's job.

The random forest's profile is also notable: its specificity of 0.896 matches the SVM, but its sensitivity is 0.606. It finds low-selling stores well and misses a third of the high-selling ones. If the goal is "identify high-potential stores", this model may be the worst business choice despite ranking second on accuracy.

Choosing a metric is a business question: by accuracy, the SVM wins. But ask "how many high-selling stores did we catch?" and the SVM and logistic regression tie (0.849) while the random forest falls clearly behind. Ask "is a false alarm expensive?" and the SVM and random forest are level on specificity. Picking a model from a single number is answering before you know the question.

8. Three Pitfalls in Model Comparison

PitfallHow it happensThe consequence
Measuring on different dataOne model is evaluated on training, another on test.The comparison is void; the one measured on training wins unfairly.
Ignoring the base rateAccuracy is reported on its own.60% accuracy looks like success when nothing was learned.
Trusting a single splitThe result comes from one random train/test partition.The ranking can change with the seed; the gap may be noise.

The third applies to this study too: the 0.013 gap between the SVM and logistic regression amounts to a single store in an 81-observation test set. Declaring "the SVM is better" on that basis would be wrong. A real comparison needs repeated cross-validation.

A sounder comparison: repeated cross-validation
library(caret)

ctrl <- trainControl(method = "repeatedcv", number = 10, repeats = 5,
                     classProbs = TRUE, summaryFunction = twoClassSummary)

models <- lapply(c("glm", "svmLinear", "rpart", "rf"), function(m) {
  set.seed(144)
  train(Sales ~ ., data = train, method = m,
        metric = "ROC", trControl = ctrl)
})

summary(resamples(setNames(models, c("glm", "svm", "cart", "rf"))))

9. Is the Difference Real? Repeated Cross-Validation

The previous section closed on a question: is the 0.013 gap between SVM and logistic regression real, or noise? There are two ways to answer it, and both point the same way.

First answer: McNemar's test

When two models are measured on the same test set, comparing their accuracies as if they were independent samples is wrong — both are predicting the same 81 stores. The right tool is McNemar's test, built for paired data. The question it asks is: are the stores one model gets right and the other gets wrong balanced between them?

McNemar's test
right_svm <- pred_svm == test$Sales
right_glm <- pred_glm == test$Sales

mcnemar.test(table(right_svm, right_glm))
# McNemar's chi-squared = 0, df = 1, p-value = 1
ComparisonOnly 1st rightOnly 2nd rightχ²pVerdict
SVM – Logistic100.0001.000No difference
SVM – Random forest1243.0630.080Borderline
SVM – CART1654.7620.029Real difference
Logistic – Random forest1142.4000.121No difference

The numbers settle the table: SVM and logistic regression disagree on exactly one of the 81 stores. What puts SVM at the top of the accuracy ranking is a single store. There is no evidence to stand behind the sentence "SVM is better". The gap between SVM and CART (p = 0.029), by contrast, is real.

Second answer: 5 × 10-fold cross-validation

A single split is a single photograph. Splitting the data into 10 folds and repeating that 5 times with different seeds gives 50 measurements per model — enough to see how stable the ranking actually is.

Four models, 50 folds
# stratified 10 folds per repeat; 4 models x 50 folds = 200 fits
for (rep in 1:5) {
  set.seed(1000 + rep)
  # ... split into 10 folds preserving class ratios ...
  for (k in 1:10) {
    tr <- df[fold != k, ]; va <- df[fold == k, ]
    # glm / svm / rpart / randomForest are fitted and scored
  }
}
ModelMean accuracyStd. deviationWorst foldBest foldFolds won
Logistic regression0.88860.05220.7561.00040 / 50
SVM (linear)0.88150.05150.7561.0006 / 50
Random forest0.81070.07600.5750.9494 / 50
CART0.74700.08110.5750.8780 / 50

The ranking flips. SVM was first on the single split; averaged over 50 folds, logistic regression leads and wins 40 of them. A paired t-test finds the difference significant: mean gap 0.0071, p = 0.024. Small, but consistent.

Further down the table the picture is sharper: the 0.071 gap between SVM and random forest is significant at p < 0.00001. So the claim that linear models beat the ensembles on this data is not the whim of one split — it holds across 50 folds.

Read the standard deviations too: the fold-to-fold variability of CART and random forest (0.081 and 0.076) is roughly 1.5 times that of the linear models (0.052). The tree-based methods here are not just less accurate, they are less stable. On small data those two problems usually arrive together.

Conclusion

Four algorithms were trained on the same data and measured on the same test set, and the result ran against expectation: the linear models beat the ensembles. That is why the assumption "a random forest is always better" has to be tested rather than believed.

But the real lesson is not inside a single table — it is in questioning that table. The test-set ranking put SVM first; McNemar's test showed that lead amounted to one store, and 50-fold cross-validation put logistic regression ahead on average. The first table was not wrong. It was a photograph of one split.

What all the models share is more valuable still: price and shelving location are the two decisive variables whichever algorithm you use. When a finding shows up in three different methods, it belongs to the data rather than the model.

“The best model is not the one at the top of the accuracy table; it is the one that best answers the question the business is asking.”

Theoretical background: I covered the methods in detail in Logistic Regression, Decision Tree / CART and Confusion Matrix. Source code: github.com/ozgurnrttn/Machine-Learning