Ana içeriğe geç
Kesit Python
İleri · Veri analizi ve bilimi

Keşifsel veri analizi (EDA): sorudan bulguya

Hipotez defteriyle başlayan yöntemli EDA: profil fonksiyonu, eksikliğin nedeni, çarpık dağılım ve aykırı değer, iki değişkenli analiz (Spearman, crosstab, grup özeti), zaman boyutu, veri hatası ile sinyali ayırma (raw_signups, kirli_veri) ve Kanat Havayolları rötar mini projesiyle bulgu yazımı.

Uygulamada aç, 20 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

EDA neden ayrı bir beceri?

Keşifsel veri analizi (exploratory data analysis, EDA), modele, panoya ya da rapora geçmeden önce veriyi tanıma işidir: veri ne kadar, neyi temsil ediyor, nerede eksik, nerede yanlış, hangi örüntüler var? pandas'ı, gruplamayı ve grafikleri önceki derslerde gördün. EDA bunları bir yöntem içinde birleştirir. Yöntemsiz EDA, yüzlerce grafiğin içinde kaybolmaktır; yöntemli EDA ise üç beş net bulguyla biter.

Bu derste Kanat Havayolları'nın Eylül–Aralık 2024 uçuşlarını (hv_flights, 3.173 uçuş) ve rötar nedenlerini (hv_delays) inceleyeceğiz. Veri hatası ile gerçek sinyali ayırmayı ise Anadolu Market'in form kayıtları (raw_signups) ve dağınık bayi raporu (kirli_veri) üzerinde göreceğiz.

1. Önce sorular, sonra kod: hipotez defteri

EDA'ya bir soru listesiyle başla. Operasyon müdürü "rötarlar kötüleşiyor mu, neden?" diye soruyorsa defterin şöyle görünebilir:

#Soru / hipotezNasıl bakarım?Durum
S1Tipik bir uçuş ne kadar rötar yapıyor?Kalkış rötarının dağılımı, medyan, %90'lık dilimaçık
S2Akşam uçuşları daha mı geç kalkıyor?Saat dilimine göre medyan ve 15 dk üstü oranıaçık
S3Aralık'ta kötüleşme hava koşulundan mı?Ay × neden çapraz tablosu (dakika payı)açık
S4Rötar uçaktan uçağa yayılıyor mu?Aynı uçağın önceki varış rötarı ile sonraki kalkış rötarıaçık
S5Dolu uçaklar daha mı geç kalkıyor?Doluluk (pax/seats) ile rötar arasında Spearman korelasyonuaçık

Her sorunun yanına sonradan kanıtı ve sonucu yazarsın. Böylece hem neyi neden incelediğini hatırlarsın hem de "her şeye baktım, bir şey bulamadım" tuzağına düşmezsin.

2. Veri profili: ilk 10 dakika

Her yeni tabloda aynı kontrolleri yap: şekil, türler, eksiklik, benzersizlik, aralıklar. Bunu bir fonksiyona dökersen her projede tekrar kullanırsın.

import veri
import pandas as pd

f = veri.oku("kanat_havayollari", "hv_flights",
             parse_dates=["sched_dep", "sched_arr", "actual_dep", "actual_arr"])

def profil(df):
    return pd.DataFrame({
        "eksik": df.isna().sum(),
        "eksik_yuzde": (df.isna().mean() * 100).round(2),
        "benzersiz": df.nunique(),
        "ornek": df.iloc[0],
    }).sort_values("eksik", ascending=False)

print(f.shape)
print(profil(f))
print(f["status"].value_counts())
print(f["sched_dep"].min(), "→", f["sched_dep"].max())

Profilden çıkan ilk sorular: actual_dep 62, actual_arr 71 satırda boş. 62 iptal uçuşu var; peki fazladan 9 boş varış nereden geliyor? 9 uçuş başka meydana yönlendirilmiş (diverted): kalkış var, planlanan varış yok. Eksikliğin bir nedeni var; rastgele değil. Bu ayrım, eksik değeri nasıl ele alacağını belirler.

Aralık ve anahtar kontrolleriProfile şu üç soruyu da ekle: (1) Birincil anahtar gerçekten tekil mi (df["flight_id"].is_unique)? (2) Sayılar mantıklı aralıkta mı (yolcu sayısı koltuk sayısını aşıyor mu)? (3) Tarihler beklenen dönemde mi? Bu kontroller veri hatalarının çoğunu ilk dakikada yakalar.

3. Tek değişkenli analiz: dağılımı tanı

Kalkış rötarını dakika olarak türetelim ve dağılımına bakalım. Rötar gibi "çoğu küçük, birkaçı çok büyük" değişkenler sağa çarpıktır (right-skewed): ortalama, uzun kuyruk tarafından yukarı çekilir.

import veri
import pandas as pd

f = veri.oku("kanat_havayollari", "hv_flights", parse_dates=["sched_dep", "actual_dep"])
f["rotar"] = (f["actual_dep"] - f["sched_dep"]).dt.total_seconds() / 60
ucan = f[f["status"] != "cancelled"]

r = ucan["rotar"]
print("ortalama:", round(r.mean(), 1), "| medyan:", r.median(), "| çarpıklık:", round(r.skew(), 2))
print(r.quantile([0.5, 0.75, 0.9, 0.99]))
print("15 dk üstü oranı:", round((r > 15).mean(), 3))

# IQR kuralıyla aykırı değer sınırları
q1, q3 = r.quantile([0.25, 0.75])
ust = q3 + 1.5 * (q3 - q1)
print("IQR üst sınırı:", ust, "| aykırı sayısı:", int((r > ust).sum()))

Ortalama yaklaşık 25 dakika, medyan 7 dakika. "Tipik uçuş 25 dakika gecikiyor" demek yanıltıcıdır; tipik uçuş 7 dakika gecikir, uçuşların yaklaşık %10'u ise 77 dakikayı aşar. Raporlarda çarpık değişkenler için medyan + yüzdelik dilimler ver.

Aykırı değer silinecek bir şey değildirIQR kuralı 374 uçuşu "aykırı" işaretler; bunlar gerçek, uzun rötarlardır ve operasyonun asıl sorunu tam da onlardır. Aykırı değeri yalnız imkânsızsa (negatif yaş, koltuktan fazla yolcu) ya da ölçüm hatası olduğu kanıtlanırsa ayıkla. Model için kırpma (winsorize) ya da log dönüşümü ayrı bir karardır.

Kategorik değişkenlerde frekans tablosu ve uzun kuyruk sorusu gelir: "Rötar nedenlerinin kaçı toplam dakikanın %80'ini oluşturuyor?"

import veri

d = veri.oku("kanat_havayollari", "hv_delays")
dakika = d.groupby("reason")["minutes"].sum().sort_values(ascending=False)
pay = (dakika / dakika.sum()).round(3)
print(pay)
print("kümülatif:", pay.cumsum().round(3).tolist())

4. İki değişkenli analiz

Değişken çiftiSayısal özetGrafik
Sayısal – sayısalPearson (doğrusal), Spearman (sıralı, aykırıya dayanıklı)Saçılım (scatter), hexbin
Kategorik – sayısalgroupby ile medyan, yüzdelik, oranKutu grafiği (box), violin
Kategorik – kategorikpd.crosstab(..., normalize="index")Yığılmış çubuk, ısı haritası
Zaman – sayısalresample / ay bazında medyanÇizgi grafik
import veri
import numpy as np
import pandas as pd

f = veri.oku("kanat_havayollari", "hv_flights", parse_dates=["sched_dep", "actual_dep"])
f = f[f["status"] != "cancelled"].copy()
f["rotar"] = (f["actual_dep"] - f["sched_dep"]).dt.total_seconds() / 60
saat = f["sched_dep"].dt.hour
f["dilim"] = np.select([saat < 12, saat < 18], ["sabah", "öğle"], "akşam")

# kategorik – sayısal
print(f.groupby("dilim")["rotar"].agg(medyan="median", gec_orani=lambda x: (x > 15).mean()).round(3))

# zaman boyutu
print(f.groupby(f["sched_dep"].dt.strftime("%Y-%m"))["rotar"].median())

# sayısal – sayısal: doluluk ile rötar
u = veri.oku("kanat_havayollari", "hv_aircraft")
f = f.merge(u[["tail_no", "seats"]], on="tail_no", how="left", validate="many_to_one")
f["doluluk"] = f["pax"] / f["seats"]
# Spearman = sıralar (rank) üzerinde Pearson; Series.corr(method="spearman") scipy ister
print("Spearman:", round(f["doluluk"].rank().corr(f["rotar"].rank()), 3))

Üç bulgu çıktı: akşam dilimi (18:00 sonrası) belirgin biçimde kötü; Aralık'ta medyan rötar iki katına çıkıyor; doluluk ile rötar arasında ilişki yok (Spearman ≈ 0). "İlişki yok" da bir bulgudur: S5 kapanır, ekip boşuna kapasite kısıtı aramaz.

import veri
import pandas as pd
import matplotlib.pyplot as plt

f = veri.oku("kanat_havayollari", "hv_flights", parse_dates=["sched_dep", "actual_dep"])
f["rotar"] = (f["actual_dep"] - f["sched_dep"]).dt.total_seconds() / 60
f = f.dropna(subset=["rotar"])
f["saat"] = f["sched_dep"].dt.hour

fig, ax = plt.subplots(1, 2, figsize=(10, 3.5))
ax[0].hist(f["rotar"].clip(upper=180), bins=36)
ax[0].set_title("Kalkış rötarı (dk, 180'de kırpıldı)")
f.boxplot(column="rotar", by="saat", ax=ax[1], showfliers=False)
ax[1].set_title("Saate göre rötar")
plt.suptitle("")
plt.tight_layout()
plt.show()
print("grafik çizildi:", len(f), "uçuş")
Korelasyon nedensellik değildirAkşam uçuşları daha geç kalkıyor; ama neden saat değil, gün boyunca biriken gecikme ya da akşam trafiği olabilir. EDA bulgusu bir hipotez üretir; nedeni kanıtlamak için deney, ek veri ya da daha dikkatli analiz gerekir (istatistik dersi).

5. Veri hatası mı, gerçek sinyal mi?

EDA'nın en değerli anı, "tuhaf" bir şey gördüğün andır. Önce sor: bu bir veri hatası mı, yoksa işin kendisi mi?

GözlemHata mı?Nasıl anladık?
62 uçuşta pax = 0Hayır: hepsi iptalpd.crosstab(f["pax"] == 0, f["status"])
raw_signups'ta 52 satır, 40 kimlikEvet: form iki kez gönderilmişE-posta küçük harfe çevrilince satırlar aynı
Aynı e-posta, iki farklı kimlik ve tarihBelirsiz: aynı kişi yeniden kayıt olmuş olabilirİş birimine sorulur, kural yazılır
Bayi raporunda Tutar "1.430"Biçim sorunu: Türkçe binlik ayırıcıDiğer satırlarda "1.250,50" var
Müşteri kaydında 2024-02-30Evet: imkânsız tarihpd.to_datetime(..., errors="coerce") NaT verir
import veri
import pandas as pd

r = veri.oku("anadolu_market", "raw_signups")
print(len(r), "satır,", r["id"].nunique(), "kimlik,", int(r.duplicated().sum()), "birebir kopya")

r["email"] = r["email"].str.strip().str.lower()
print("e-posta normalleşince kopya:", int(r.duplicated().sum()))

def sehir_anahtar(s):
    return s.strip().replace("İ", "i").lower() if isinstance(s, str) else s

print(r["city"].value_counts(dropna=False).head(8))       # 'ISTANBUL', 'istanbul', 'İstanbul'...
print(r["city"].map(sehir_anahtar).value_counts(dropna=False))

# iki tarih biçimi: önce ISO, olmazsa gün.ay.yıl
t = pd.to_datetime(r["signup_date"], format="%Y-%m-%d", errors="coerce")
t = t.fillna(pd.to_datetime(r["signup_date"], format="%d.%m.%Y", errors="coerce"))
print("çözülemeyen tarih:", int(t.isna().sum()))

ayni = r.drop_duplicates().groupby("email")["id"].nunique()
print("aynı e-posta, farklı kimlik:", ayni[ayni > 1].to_dict())
Temizlik kararlarını kaydet"Kopyaları sildim" yetmez: hangi kurala göre (birebir mi, e-posta mı?), kaç satır gitti, belirsiz durumda ne yaptın? Bu notlar bulguların güvenilirliğini belirler ve aynı temizliğin yarın tekrar çalışabilmesini sağlar.

6. Bulgu yazımı: gözlem → kanıt → olası açıklama → sonraki adım

EDA'nın çıktısı grafik yığını değil, karar verdiren kısa bir metindir. Her bulguyu dört parçayla yaz:

  1. Gözlem: Aralık'ta rötar belirgin biçimde arttı.
  2. Kanıt: Medyan kalkış rötarı Eylül'de 5, Aralık'ta 16 dakika; hava kaynaklı dakikaların payı %25'ten %38'e çıktı.
  3. Olası açıklama: Kış koşulları; ancak geç gelen uçak payı da yüksek, zincirleme etki olabilir.
  4. Sonraki adım: Havalimanı bazında hava kaynaklı rötarı ayır; kış tarifesinde akşam bankosuna tampon süre ekleme senaryosunu hesapla.
Gerçek işteAnalist işe alım vakalarında (take-home) değerlendiriciler grafiğin güzelliğine değil, bulguların yapısına bakar: soru net mi, veri kalitesi sorunları fark edilmiş ve belgelenmiş mi, sonuç sayıyla desteklenmiş mi, "bilmiyorum, şu veriyle anlarız" diyebilmiş mi? İş hayatında da EDA raporu çoğunlukla bir sayfadır: 3–5 bulgu, her biri bir grafik ve bir sonraki adım. Notebook'un geri kalanı ekte durur.
Uzmanlaş

Otomatik profil araçları

ydata-profiling ve sweetviz tek satırda yüzlerce istatistik ve grafik içeren HTML rapor üretir. İlk bakış için hızlıdır; ama soruyu ve yorumu senin yerine yapmaz. Tarayıcıda çalışmazlar.

pip install ydata-profiling
from ydata_profiling import ProfileReport
import pandas as pd
df = pd.read_csv("hv_flights.csv")
ProfileReport(df, title="Uçuşlar", minimal=True).to_file("profil.html")

Kendi bilgisayarında dene: yukarıdaki komutla kur, raporu tarayıcıda aç ve kendi profil() fonksiyonunun çıktısıyla karşılaştır.

Çatallanan yollar ve sızıntı

Yeterince çok kırılıma bakarsan bazıları şans eseri "anlamlı" görünür (çoklu karşılaştırma). EDA'da bulduğun bir örüntüyü kesin sonuç gibi sunma; ya yeni veride doğrula ya da hipotez olarak etiketle. Model kuracaksan EDA'yı yalnız eğitim verisinde yap; test verisine bakıp karar vermek veri sızıntısıdır (data leakage).

Simpson ve kompozisyon etkisi

Toplam ortalama değişirken her alt grup aynı kalabilir: örneğin Aralık'ta akşam uçuşlarının payı artarsa toplam rötar, hiçbir dilim kötüleşmese de yükselir. Zaman karşılaştırmalarında daima "karma (mix) değişti mi?" sorusunu sor. Bu konu iş metrikleri dersinde köprü analiziyle derinleşiyor.

Alıştırmalara geç (20)