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 / hipotez | Nasıl bakarım? | Durum |
|---|---|---|---|
| S1 | Tipik bir uçuş ne kadar rötar yapıyor? | Kalkış rötarının dağılımı, medyan, %90'lık dilim | açık |
| S2 | Akşam uçuşları daha mı geç kalkıyor? | Saat dilimine göre medyan ve 15 dk üstü oranı | açık |
| S3 | Aralık'ta kötüleşme hava koşulundan mı? | Ay × neden çapraz tablosu (dakika payı) | açık |
| S4 | Rö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 |
| S5 | Dolu uçaklar daha mı geç kalkıyor? | Doluluk (pax/seats) ile rötar arasında Spearman korelasyonu | açı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.
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.
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 çifti | Sayısal özet | Grafik |
|---|---|---|
| Sayısal – sayısal | Pearson (doğrusal), Spearman (sıralı, aykırıya dayanıklı) | Saçılım (scatter), hexbin |
| Kategorik – sayısal | groupby ile medyan, yüzdelik, oran | Kutu grafiği (box), violin |
| Kategorik – kategorik | pd.crosstab(..., normalize="index") | Yığılmış çubuk, ısı haritası |
| Zaman – sayısal | resample / 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ş")
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özlem | Hata mı? | Nasıl anladık? |
|---|---|---|
62 uçuşta pax = 0 | Hayır: hepsi iptal | pd.crosstab(f["pax"] == 0, f["status"]) |
raw_signups'ta 52 satır, 40 kimlik | Evet: form iki kez gönderilmiş | E-posta küçük harfe çevrilince satırlar aynı |
| Aynı e-posta, iki farklı kimlik ve tarih | Belirsiz: 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-30 | Evet: imkânsız tarih | pd.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())
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:
- Gözlem: Aralık'ta rötar belirgin biçimde arttı.
- 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ı.
- Olası açıklama: Kış koşulları; ancak geç gelen uçak payı da yüksek, zincirleme etki olabilir.
- Sonraki adım: Havalimanı bazında hava kaynaklı rötarı ayır; kış tarifesinde akşam bankosuna tampon süre ekleme senaryosunu hesapla.
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.