R ile Makine Öğrenmesi: Veri Hazırlığı ve Keşif

Model kurmadan önce ne yapılır? Eksik gözlem analizi, dağılım kontrolü, normallik testleri ve tabakalı eğitim-test ayrımını gerçek bir veri seti üzerinde ele alıyorum.

  • Makine Öğrenmesi
  • Araçlar

Makine öğrenmesi yazılarının çoğu modelden başlar. Oysa bir projede harcanan zamanın büyük bölümü model kurmadan önce geçer: veriyi tanımak, eksikleri bulmak, değişkenlerin nasıl dağıldığını görmek ve veriyi eğitim ile test olarak ayırmak.

Bu yazı dört bölümlük bir serinin ilki. Seri boyunca iki gerçek veri seti üzerinde çalışacağız: Carseats (400 mağazanın çocuk oto koltuğu satışları) ve World Happiness Report 2021 (149 ülkenin mutluluk skorları). Bu ilk yazıda modele hiç girmeden önce yapılması gerekenleri ele alıyorum.

“Model kurmadan önce verinin ne olduğunu bilmiyorsan, kurduğun şey bir model değil; bir tahmindir.”

Serinin diğer yazıları: 2) R ile çoklu doğrusal regresyon — modelden tahmine, 3) R ile sınıflandırma — lojistik regresyon, SVM, CART ve rastgele orman karşılaştırması, 4) R ile denetimsiz öğrenme — K-means, hiyerarşik kümeleme ve PCA.

1. İki Veri Seti

Seri boyunca iki veri seti kullanıyorum. İkisi de açık, ikisi de küçük; yani makinende aynı sonuçları üretebilirsin.

Denetimli

Carseats

400 mağaza, 11 değişken. An Introduction to Statistical Learning kitabının ISLR paketinden geliyor. Regresyonda hedef Sales, sınıflandırmada aynı değişkenin ikili hali.

Denetimsiz

World Happiness 2021

149 ülke, 13 değişken. Mutluluk skoru, kişi başı GSYİH, sosyal destek, sağlıklı yaşam beklentisi gibi göstergeler. Kümeleme ve PCA için kullanacağız.

DeğişkenTürAnlamı
SalesSayısalMağaza başına satış (bin adet) — regresyonun hedefi
CompPriceSayısalRakibin aynı lokasyondaki fiyatı
IncomeSayısalBölgenin gelir düzeyi (bin $)
AdvertisingSayısalYerel reklam bütçesi (bin $)
PopulationSayısalBölge nüfusu (bin)
PriceSayısalÜrünün mağazadaki fiyatı
ShelveLocKategorikRaf konumu: Bad / Medium / Good
AgeSayısalBölgedeki ortalama nüfus yaşı
EducationSayısalBölgedeki eğitim düzeyi
Urban, USKategorikMağaza kentsel mi, ABD'de mi
Veriyi okumak ve ilk bakış
library(tidyverse)
library(ISLR)

data(Carseats)
dim(Carseats)      # 400  11
str(Carseats)
summary(Carseats$Sales)
Sales değişkeninin özeti
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  0.000   5.390   7.490   7.496   9.320  16.270

Ortalama ile medyanın birbirine bu kadar yakın olması (7,496 ve 7,49) ilk ipucunu veriyor: Sales simetriğe yakın dağılıyor. Bunu birazdan sayıyla doğrulayacağız.

2. Eksik Gözlem Analizi

Eksik veri kontrolü, modelleme öncesi atlanmaması gereken ilk adım. R'da bunun için birkaç pratik yol var.

Eksik gözlem kontrolü
# En hizli yol: toplam ve sutun bazinda
sum(is.na(Carseats))                 # 0
colSums(is.na(Carseats))

# Gorsel olarak
library(VIM)
aggr(Carseats, numbers = TRUE, sortVars = TRUE)

library(Amelia)
missmap(Carseats)

Carseats'te hiç eksik gözlem yok — ders kitabından gelen temiz bir veri seti olduğu için beklenen bir sonuç. Gerçek projelerde durum böyle olmaz; o yüzden eksikliğin nasıl ele alınacağı kadar neden oluştuğu da önemli.

Eksiklik türüNe demek?Yaklaşım
MCAR — tamamen rastgeleEksiklik hiçbir değişkenle ilişkili değil.Silmek görece güvenli; veri kaybı tek maliyet.
MAR — gözlenene bağlı rastgeleEksiklik başka bir değişkenle açıklanabiliyor.Model tabanlı doldurma (örn. mice) uygun olabilir.
MNAR — rastgele değilEksiklik, eksik olan değerin kendisine bağlı.En zoru; eksikliği ayrı bir bilgi olarak modellemek gerekebilir.
Kritik sıra: Doldurma (imputation) işlemi veriyi böldükten sonra ve yalnızca eğitim setinden öğrenilen değerlerle yapılmalı. Medyanı tüm veri üzerinden hesaplayıp eksikleri doldurursan, test setinin bilgisi modele sızar ve performans olduğundan iyi görünür.

3. Veriyi Keşfetmek

Sayısal özet kadar kategorik değişkenlerin dağılımı da önemli. Dengesiz bir kategori, hem modelin öğrenmesini hem de sonradan yapacağın tabakalı bölmeyi etkiler.

Kategorik değişkenlerin dağılımı
table(Carseats$ShelveLoc)
table(Carseats$Urban)
table(Carseats$US)
DeğişkenDağılımNot
ShelveLocBad 96 · Medium 219 · Good 85Orta kategori baskın; uç kategoriler benzer büyüklükte.
UrbanNo 118 · Yes 282Mağazaların %70,5'i kentsel.
USNo 142 · Yes 258%64,5 ABD içinde.

Değişkenler birbiriyle nasıl ilişkili?

Tek tek dağılımlar kadar önemli olan bir şey daha var: değişkenlerin birbirine göre nasıl durduğu. Korelasyon matrisi hem hedefe en yakın değişkenleri gösterir hem de modelde sorun çıkarabilecek bağımlılıkları önceden haber verir.

Korelasyonlar
sayisal <- Carseats[sapply(Carseats, is.numeric)]
ko <- cor(sayisal)

round(sort(ko[, "Sales"], decreasing = TRUE), 3)

# corrplot ile gorsellestirme:
library(corrplot)
corrplot(ko, method = "color", type = "upper", addCoef.col = "black")
Sales ile korelasyonlar (mutlak büyüklüğe göre)
  • Price−0,445
  • Advertising0,270
  • Age−0,232
  • Income0,152
  • CompPrice0,064
  • Education−0,052
  • Population0,051

Tek bir değişken bile 0,5'i geçmiyor; en güçlü ilişki Price ile ve o bile −0,445. Bu, tek değişkenli bakışın yetersiz olduğunun ilk işareti. Regresyon yazısında aynı değişkenlerin birlikte satışın %87'sini açıkladığını göreceğiz — yani asıl bilgi değişkenlerin tek başına değil, bir arada taşıdığı şeyde.

Bağımsız değişkenler arasındaki ilişkilere de bakmak gerekiyor:

Değişken çiftiKorelasyonYorum
CompPrice – Price0,585Beklenen: rakip fiyatını artırınca mağaza da artırıyor. Tek dikkat çeken ilişki.
Advertising – Population0,266Büyük bölgelere daha çok reklam bütçesi ayrılıyor.
Population – Education−0,106İhmal edilebilir.
Price – Age−0,102İhmal edilebilir.
0,585 sorun mu? Hayır, ama akılda tutulması gereken bir sayı. Çoklu doğrusal bağlantı asıl olarak VIF ile ölçülür ve iki değişkenin 0,585 korelasyonu VIF'i yaklaşık 1,5 düzeyine taşır — eşiklerin çok altında. Yine de modelde Price ve CompPrice katsayılarının birlikte hareket etmesini beklemek doğru olur.

Aykırı değerler: var mı, silinmeli mi?

Aykırı değer analizi genellikle "bulduğunu sil" olarak uygulanıyor ve bu çoğu zaman yanlış. Önce ne kadar aykırı değer olduğunu görmek gerekiyor.

IQR kuralıyla aykırı değer sayımı
for (v in names(sayisal)) {
  x <- sayisal[[v]]
  q <- quantile(x, c(.25, .75)); i <- IQR(x)
  cat(v, sum(x < q[1] - 1.5*i | x > q[2] + 1.5*i), "\n")
}
DeğişkenAlt sınırÜst sınırAykırı sayısı
Price53,5177,55 (3 düşük, 2 yüksek)
Sales−0,5015,212 (15,63 ve 16,27)
CompPrice85,0165,02
Income, Advertising, Population, Age, Education—0

400 gözlemde toplam dokuz aykırı değer, yani %2,25. Üstelik hiçbiri fiziksel olarak imkânsız değil: 16,27 bin adet satan bir mağaza mümkün, 177'nin üzerinde fiyatlanmış bir ürün de. Bunlar veri hatası değil, dağılımın uçları.

Aykırı değeri silmeden önce üç soru: (1) Fiziksel olarak mümkün mü? Yaş 150 ise hata, satış 16,27 ise değil. (2) Hangi mekanizmadan geldi? Ölçüm hatası mı, gerçek bir uç durum mu? (3) Modeli gerçekten etkiliyor mu? Üçüncüsünü Cook uzaklığıyla ölçebilirsin — regresyon yazısında bu veride en büyük Cook değerinin 0,038 olduğunu, yani hiçbir gözlemin modeli sürüklemediğini göreceğiz. Bu durumda silmek yalnızca veri kaybı demek.

Hangi grafiğin hangi soruya cevap verdiği konusunu ayrı bir yazıda ele almıştım; burada tekrar etmek yerine kısa bir eşleme bırakıyorum.

Dağılım

Histogram

Tek bir sayısal değişkenin hangi aralıklarda yoğunlaştığını gösterir.

Dağılım

Kutu grafiği

Medyan, çeyrekler ve aykırı değerler; gruplar arası karşılaştırma için ideal.

İlişki

Saçılım

İki sayısal değişken arasındaki ilişkinin yönü ve biçimi.

İlişki

Korelasyon matrisi

Bütün sayısal değişkenlerin ikili ilişkilerine tek bakışta görmek.

4. Normallik, Çarpıklık ve Basıklık

Doğrusal regresyonun varsayımı değişkenlerin kendisinin değil, hata teriminin normal dağılmasıdır. Yine de değişkenlerin dağılımını baştan bilmek, hangi dönüşümün gerekebileceğini ve hangi grafiğin yanıltabileceğini önceden gösterir.

Shapiro-Wilk ve moment ölçüleri
carpiklik <- function(x) mean((x - mean(x))^3) / sd(x)^3
basiklik  <- function(x) mean((x - mean(x))^4) / sd(x)^4

sayisal <- Carseats[sapply(Carseats, is.numeric)]

for (v in names(sayisal)) {
  sw <- shapiro.test(sayisal[[v]])
  cat(sprintf("%-12s carpiklik=%6.3f basiklik=%6.3f W=%.4f p=%.4g\n",
              v, carpiklik(sayisal[[v]]), basiklik(sayisal[[v]]),
              sw$statistic, sw$p.value))
}
DeğişkenÇarpıklıkBasıklıkShapiro-Wilk WpSonuç
Sales0,1842,8910,99520,254Normallik reddedilemez
CompPrice−0,0423,0110,99840,977Normallik reddedilemez
Price−0,1243,4140,99590,390Normallik reddedilemez
Income0,0491,9040,96118,4e−09Normal değil (basık)
Advertising0,6352,4340,87351,5e−17Normal değil (sağa çarpık)
Population−0,0511,7890,95204,1e−10Normal değil (basık)
Age−0,0771,8550,95671,9e−09Normal değil (basık)
Education0,0441,6940,92422,4e−13Normal değil (basık)

Tabloda dikkat çeken şey şu: çarpıklık değerleri sıfıra çok yakın olduğu hâlde beş değişken için normallik reddediliyor. Sebep basıklıkta: Income, Population, Age ve Education için basıklık 1,7–1,9 bandında, yani normal dağılımın 3 değerinin belirgin biçimde altında. Bunlar simetrik ama fazla basık, neredeyse düzgün (uniform) dağılıma yakın değişkenler. Simetri tek başına normallik demek değil.

Ayrıntı için: Normallik varsayımı, çarpıklık ve basıklık ölçüleri ile Shapiro-Wilk ve Kolmogorov-Smirnov testlerinin ayrıntısını Normal Dağılım yazısında ele aldım. Burada yalnızca modelleme kararını etkileyen kısmı kullanıyorum.

5. Veriyi Eğitim ve Test Olarak Ayırmak

Bir modelin başarısını onu eğittiğin veriyle ölçersen, sonuç her zaman olduğundan iyi çıkar. Model o satırları zaten görmüştür. Gerçek performans, modelin hiç görmediği veriyle ölçülür.

Eğitim ve test ayrımının mantığı
  1. Tüm veri400 gözlem
  2. Böl%80 / %20
  3. EğitModel yalnızca eğitimi görür
  4. Test etGörülmemiş veri
  5. KararGerçekçi performans

Sınıflandırmada basit rastgele bölme yeterli olmayabilir. Sınıfların oranı eğitim ve test setinde korunmazsa, karşılaştırmalar bozulur. Bunun çözümü tabakalı (stratified) bölme: her sınıftan aynı oranda örnek almak.

Tabakalı %80/%20 bölme
df <- Carseats
df$Sales <- factor(ifelse(df$Sales <= 8, "Low", "High"),
                   levels = c("Low", "High"))

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, ]
test  <- df[-idx, ]

table(train$Sales)   # Low 188  High 131
table(test$Sales)    # Low  48  High  33

Aynı işi caret paketindeki createDataPartition() de yapar; yukarıdaki açık yazım, arkada ne olduğunu göstermek için. Sonuç: 319 eğitim, 81 test gözlemi ve her iki sette de yaklaşık %59 Low / %41 High oranı.

Eşiği 8'e koymak masum bir karar mı?

Sınıflandırma için Sales değişkenini 8 bin adette ikiye böldük. Bu sayı veriden gelmiyor, bizden geliyor — ve taban oranı doğrudan belirliyor.

EşikLowHighTaban oranModelin aşması gereken çıta
7,01842160,540Düşük — model kolayca geçer
7,52021980,505En dengeli bölünme
8,0 (seçilen)2361640,590Orta
8,52571430,643Yüksek
9,02871130,718Çok yüksek — dengesiz sınıf sorunu başlar

Eşiği 7,5'e çekseydik taban oran %50,5 olurdu ve "%60 doğruluk" gerçekten bir başarı sayılırdı. 9'a çekseydik taban oran %71,8'e çıkar, aynı doğruluk değeri başarısızlık anlamına gelirdi. Yani doğruluk rakamı tek başına değil, hangi eşikle üretildiğiyle birlikte okunmalı.

İkinci bir mesele daha var: eşiğe yakın mağazalar. Sales değeri 7,5 ile 8,5 arasında olan 56 mağaza var — veri setinin %14'ü. Bu mağazalar "High" ile "Low" arasında anlamlı bir fark taşımıyor; yalnızca çizginin hangi tarafına düştükleri farklı.

Sürekli bir değişkeni ikiye bölmenin maliyeti: 7,99 satan mağaza ile 8,01 satan mağaza modele göre tamamen farklı iki sınıfta; oysa aralarındaki gerçek fark 20 adet. Bu bilgi kaybı sınıflandırmanın doğasında var. Mümkün olduğunda sürekli hedefi sürekli bırakmak (yani regresyon kurmak) daha fazla bilgi kullanır. İkiye bölmek yalnızca karar gerçekten ikili olduğunda — "bu mağazaya yatırım yapalım mı?" — anlamlıdır.
set.seed() neden şart? Bölme rastgele; tohum sabitlenmezse her çalıştırmada farklı bir eğitim-test ayrımı ve farklı bir doğruluk elde edersin. Sonuçların tekrar üretilebilir olması için tohum mutlaka sabitlenmeli ve raporda belirtilmeli.

6. Veri Sızıntısından Kaçınmak

Veri sızıntısı (data leakage), test setine ait bilginin modele farkında olmadan taşınmasıdır. Sonuç her zaman aynı: model raporda harika, gerçek hayatta vasat görünür.

SızıntıNasıl oluşur?Doğrusu
ÖlçeklemeOrtalama ve standart sapma tüm veriden hesaplanır.Parametreler yalnızca eğitimden öğrenilir, teste uygulanır.
Eksik doldurmaMedyan tüm veri üzerinden alınır.Eğitim medyanı hesaplanır, test aynı değerle doldurulur.
Değişken seçimiEn iyi değişkenler tüm veriye bakılarak seçilir.Seçim yalnızca eğitim verisiyle yapılır.
Hedeften türetmeHedefi içeren bir değişken modelde kalır.Hedeften türeyen alanlar modelden çıkarılır.
Zaman serisiGelecek tarihli satırlar eğitime girer.Bölme zamana göre yapılır, rastgele değil.

Bu serideki sınıflandırma örneğinde gizli bir sızıntı riski vardı: Sales değişkeni ikiliye çevrilirken orijinal sayısal sütunun üzerine yazılması gerekiyor. Sayısal Sales veri setinde kalsaydı model hedefi doğrudan okur ve %100 doğruluk üretirdi.

7. Modele Geçmeden Önce Kontrol Listesi

01

Boyut ve tipler

Kaç satır, kaç sütun? Kategorik alanlar faktör olarak mı okundu?

02

Eksik gözlem

Nerede, ne kadar ve neden eksik? Doldurma bölmeden sonra mı yapılıyor?

03

Dağılımlar

Çarpıklık, basıklık ve aykırı değerler incelendi mi?

04

Hedef değişken

Sınıflar dengeli mi? Taban oran ne? Doğruluk bunu geçebilecek mi?

05

Bölme

Tabakalı mı? Tohum sabit mi? Oranlar iki sette de korunuyor mu?

06

Sızıntı

Ölçekleme, doldurma ve değişken seçimi yalnızca eğitimden mi öğreniliyor?

Sonuç

Bu yazıda tek bir model kurmadık ama bir sonraki adımın bütün zeminini hazırladık: veriyi tanıdık, eksik olmadığını doğruladık, değişkenlerin dağılımını ölçtük ve veriyi tekrar üretilebilir biçimde eğitim-test olarak ayırdık.

Burada elde ettiğimiz iki bilgi sonraki yazılarda doğrudan işimize yarayacak: Sales, CompPrice ve Price normal dağılıma uygun; sınıflandırmada taban oran yaklaşık %59. İkinci sayı özellikle önemli — çünkü bir modelin %60 doğruluk üretmesi, hiçbir şey öğrenmediği anlamına gelebilir.

“İyi bir model, iyi hazırlanmış veriyle başlar; iyi bir değerlendirme ise veriyi doğru böldüğün anda.”

Kaynak kod: Bu serinin dayandığı R Markdown çalışması GitHub'da: github.com/ozgurnrttn/Machine-Learning. Veri setleri ve rapor aynı depoda.

Most machine learning articles start with the model. Yet in a real project most of the time is spent before any model exists: getting to know the data, finding what is missing, seeing how the variables are distributed, and splitting the data into training and test sets.

This is the first of a four-part series. Throughout the series we work with two real data sets: Carseats (child car seat sales at 400 stores) and the World Happiness Report 2021 (happiness scores for 149 countries). In this first post I cover everything that has to happen before a single model is fitted.

“If you do not know what the data is before you fit a model, what you built is not a model — it is a guess.”

The rest of the series: 2) Multiple linear regression in R — from model to prediction, 3) Classification in R — comparing logistic regression, SVM, CART and random forest, 4) Unsupervised learning in R — K-means, hierarchical clustering and PCA.

1. Two Data Sets

Two data sets run through the whole series. Both are open and both are small, so you can reproduce every result on your own machine.

Supervised

Carseats

400 stores, 11 variables, from the ISLR package that accompanies An Introduction to Statistical Learning. The regression target is Sales; for classification we use a binary version of the same variable.

Unsupervised

World Happiness 2021

149 countries, 13 variables: happiness score, GDP per capita, social support, healthy life expectancy and more. We use it for clustering and PCA.

VariableTypeMeaning
SalesNumericUnit sales per store (thousands) — the regression target
CompPriceNumericCompetitor's price at the same location
IncomeNumericCommunity income level (thousand $)
AdvertisingNumericLocal advertising budget (thousand $)
PopulationNumericRegional population (thousands)
PriceNumericPrice charged at the store
ShelveLocCategoricalShelving location: Bad / Medium / Good
AgeNumericAverage age of the local population
EducationNumericEducation level at the location
Urban, USCategoricalWhether the store is urban, and whether it is in the US
Reading the data and a first look
library(tidyverse)
library(ISLR)

data(Carseats)
dim(Carseats)      # 400  11
str(Carseats)
summary(Carseats$Sales)
Summary of the Sales variable
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  0.000   5.390   7.490   7.496   9.320  16.270

The mean and the median being this close (7.496 and 7.49) is the first clue: Sales is close to symmetric. We will confirm that with a number shortly.

2. Missing Data Analysis

Checking for missing data is the first step that must not be skipped before modelling. R offers several practical ways to do it.

Checking for missing values
# Quickest: total and per column
sum(is.na(Carseats))                 # 0
colSums(is.na(Carseats))

# Visually
library(VIM)
aggr(Carseats, numbers = TRUE, sortVars = TRUE)

library(Amelia)
missmap(Carseats)

Carseats has no missing values at all — expected for a clean textbook data set. Real projects are rarely like this, which is why why a value is missing matters as much as how you handle it.

Type of missingnessWhat it meansApproach
MCAR — completely at randomMissingness is unrelated to any variable.Deleting rows is relatively safe; losing data is the only cost.
MAR — at random given observed dataMissingness can be explained by another variable.Model-based imputation (e.g. mice) may be appropriate.
MNAR — not at randomMissingness depends on the missing value itself.The hardest case; missingness may need to be modelled as information.
The order is critical: imputation must happen after the split, using values learned from the training set only. If you compute the median over the whole data set and fill with it, the test set leaks into the model and performance looks better than it is.

3. Exploring the Data

The distribution of the categorical variables matters as much as the numeric summary. An imbalanced category affects both what the model can learn and the stratified split you will do later.

Distribution of the categorical variables
table(Carseats$ShelveLoc)
table(Carseats$Urban)
table(Carseats$US)
VariableDistributionNote
ShelveLocBad 96 · Medium 219 · Good 85The middle category dominates; the extremes are similar in size.
UrbanNo 118 · Yes 28270.5% of stores are urban.
USNo 142 · Yes 25864.5% are inside the US.

How are the variables related to each other?

Individual distributions matter, but so does how the variables sit relative to one another. A correlation matrix shows which variables are closest to the target and warns early about dependencies that could cause trouble in the model.

Correlations
numeric_cols <- Carseats[sapply(Carseats, is.numeric)]
co <- cor(numeric_cols)

round(sort(co[, "Sales"], decreasing = TRUE), 3)

# visualised with corrplot:
library(corrplot)
corrplot(co, method = "color", type = "upper", addCoef.col = "black")
Correlation with Sales (by absolute size)
  • Price−0.445
  • Advertising0.270
  • Age−0.232
  • Income0.152
  • CompPrice0.064
  • Education−0.052
  • Population0.051

Not one variable clears 0.5; the strongest relationship is with Price and even that is −0.445. This is the first sign that a univariate view is not enough. In the regression post we will see these same variables explain 87% of sales together — the information lives in the combination, not in any single column.

The relationships among the predictors deserve a look as well:

Variable pairCorrelationReading
CompPrice – Price0.585As expected: when the competitor raises prices, the store follows. The only notable relationship.
Advertising – Population0.266Larger regions get larger advertising budgets.
Population – Education−0.106Negligible.
Price – Age−0.102Negligible.
Is 0.585 a problem? No, but it is a number worth remembering. Multicollinearity is properly measured with VIF, and a correlation of 0.585 between two variables lifts VIF to roughly 1.5 — well under any threshold. Still, expect the Price and CompPrice coefficients to move together in the model.

Outliers: are there any, and should they go?

Outlier analysis is often applied as "find them and delete them", which is usually wrong. The first step is simply to see how many there are.

Counting outliers with the IQR rule
for (v in names(numeric_cols)) {
  x <- numeric_cols[[v]]
  q <- quantile(x, c(.25, .75)); i <- IQR(x)
  cat(v, sum(x < q[1] - 1.5*i | x > q[2] + 1.5*i), "\n")
}
VariableLower boundUpper boundOutliers
Price53.5177.55 (3 low, 2 high)
Sales−0.5015.212 (15.63 and 16.27)
CompPrice85.0165.02
Income, Advertising, Population, Age, Education—0

Nine outliers across 400 observations — 2.25%. None of them is physically impossible either: a store selling 16,270 units is plausible, and so is a product priced above 177. These are not data errors, they are the tails of the distribution.

Three questions before deleting an outlier: (1) Is it physically possible? An age of 150 is an error; sales of 16.27 are not. (2) What mechanism produced it — measurement error, or a genuine extreme case? (3) Does it actually affect the model? Cook's distance answers the third; in the regression post we will see the largest Cook's distance on this data is 0.038, meaning no observation drives the fit. In that case deleting is just losing data.

Which chart answers which question is a topic I covered in a separate post; instead of repeating it, here is a short mapping.

Distribution

Histogram

Shows where a single numeric variable concentrates.

Distribution

Box plot

Median, quartiles and outliers; ideal for comparing groups.

Relationship

Scatter plot

The direction and shape of the relationship between two numeric variables.

Relationship

Correlation matrix

All pairwise relationships among the numeric variables at a glance.

4. Normality, Skewness and Kurtosis

The assumption in linear regression is that the error term is normally distributed, not the variables themselves. Still, knowing the distributions up front tells you which transformation might be needed and which chart could mislead you.

Shapiro-Wilk and the moment measures
skewness <- function(x) mean((x - mean(x))^3) / sd(x)^3
kurtosis <- function(x) mean((x - mean(x))^4) / sd(x)^4

numeric_vars <- Carseats[sapply(Carseats, is.numeric)]

for (v in names(numeric_vars)) {
  sw <- shapiro.test(numeric_vars[[v]])
  cat(sprintf("%-12s skew=%6.3f kurt=%6.3f W=%.4f p=%.4g\n",
              v, skewness(numeric_vars[[v]]), kurtosis(numeric_vars[[v]]),
              sw$statistic, sw$p.value))
}
VariableSkewnessKurtosisShapiro-Wilk WpConclusion
Sales0.1842.8910.99520.254Normality not rejected
CompPrice−0.0423.0110.99840.977Normality not rejected
Price−0.1243.4140.99590.390Normality not rejected
Income0.0491.9040.96118.4e−09Not normal (flat)
Advertising0.6352.4340.87351.5e−17Not normal (right-skewed)
Population−0.0511.7890.95204.1e−10Not normal (flat)
Age−0.0771.8550.95671.9e−09Not normal (flat)
Education0.0441.6940.92422.4e−13Not normal (flat)

One thing stands out: although the skewness values are very close to zero, normality is rejected for five variables. The reason is kurtosis. For Income, Population, Age and Education it sits between 1.7 and 1.9, clearly below the value of 3 for a normal distribution. These variables are symmetric but far too flat — close to a uniform distribution. Symmetry alone does not mean normality.

For the details: I covered the normality assumption, skewness and kurtosis, and the Shapiro-Wilk and Kolmogorov-Smirnov tests in the Normal Distribution post. Here I only use the part that affects the modelling decision.

5. Splitting into Training and Test Sets

If you measure a model with the data it was trained on, the result always looks better than it is — the model has already seen those rows. Real performance is measured on data the model has never seen.

The logic of a train/test split
  1. All data400 observations
  2. Split80% / 20%
  3. TrainModel sees training only
  4. TestUnseen data
  5. DecideRealistic performance

For classification a plain random split may not be enough. If the class ratio is not preserved in both sets, comparisons break down. The fix is a stratified split: sampling the same proportion from each class.

A stratified 80/20 split
df <- Carseats
df$Sales <- factor(ifelse(df$Sales <= 8, "Low", "High"),
                   levels = c("Low", "High"))

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, ]
test  <- df[-idx, ]

table(train$Sales)   # Low 188  High 131
table(test$Sales)    # Low  48  High  33

createDataPartition() from the caret package does the same job; the explicit version above shows what happens underneath. The result: 319 training and 81 test observations, with roughly 59% Low / 41% High in both.

Is putting the threshold at 8 an innocent decision?

For classification we split Sales in two at 8,000 units. That number does not come from the data, it comes from us — and it sets the base rate directly.

ThresholdLowHighBase rateBar the model must clear
7.01842160.540Low — easily cleared
7.52021980.505The most balanced split
8.0 (chosen)2361640.590Moderate
8.52571430.643High
9.02871130.718Very high — class imbalance starts to bite

Had we cut at 7.5, the base rate would be 50.5% and "60% accuracy" would count as a genuine result. Cut at 9 and the base rate rises to 71.8%, where the same accuracy means failure. An accuracy figure has to be read together with the threshold that produced it.

There is a second issue: the stores near the line. 56 stores have Sales between 7.5 and 8.5 — 14% of the dataset. Nothing meaningful separates them into "High" and "Low"; only which side of the line they happen to land on.

The cost of dichotomising a continuous variable: a store selling 7.99 and one selling 8.01 belong to entirely different classes as far as the model is concerned, while the real gap between them is 20 units. That information loss is inherent to classification. Where possible, leaving a continuous target continuous — fitting a regression — uses more of the data. Splitting in two makes sense only when the decision itself is binary: "do we invest in this store or not?"
Why set.seed() is essential: the split is random, so without a fixed seed every run produces a different train/test partition and a different accuracy. For results to be reproducible, the seed must be fixed and stated in the report.

6. Avoiding Data Leakage

Data leakage is information from the test set reaching the model unnoticed. The outcome is always the same: the model looks excellent in the report and mediocre in production.

LeakHow it happensThe right way
ScalingMean and standard deviation computed on all data.Parameters learned from training only, then applied to test.
ImputationThe median is taken over the whole data set.Compute the training median and fill the test set with it.
Feature selectionBest variables chosen by looking at all the data.Selection is done on the training set alone.
Target-derived featuresA variable containing the target stays in the model.Fields derived from the target are removed.
Time seriesFuture-dated rows end up in training.Split by time, not at random.

The classification example in this series carried a hidden leakage risk: when Sales is turned into a binary variable, the original numeric column must be overwritten. Had numeric Sales stayed in the data, the model would read the target directly and report 100% accuracy.

7. A Checklist Before Modelling

01

Shape and types

How many rows and columns? Were categorical fields read as factors?

02

Missing data

Where, how much, and why? Does imputation happen after the split?

03

Distributions

Have skewness, kurtosis and outliers been examined?

04

The target

Are the classes balanced? What is the base rate? Can accuracy beat it?

05

The split

Is it stratified? Is the seed fixed? Are the ratios preserved in both sets?

06

Leakage

Are scaling, imputation and selection learned from training only?

Conclusion

We did not fit a single model in this post, but we laid the whole foundation for the next step: we got to know the data, confirmed there are no missing values, measured how the variables are distributed, and split the data reproducibly into training and test sets.

Two facts from here will matter directly in the next posts: Sales, CompPrice and Price are consistent with a normal distribution, and the base rate in classification is roughly 59%. That second number is especially important — because a model reporting 60% accuracy may have learned nothing at all.

“A good model starts with well-prepared data; a good evaluation starts the moment you split it correctly.”

Source code: the R Markdown study behind this series is on GitHub: github.com/ozgurnrttn/Machine-Learning. The data sets and the report are in the same repository.