Hafta 2 · Biyomedikal Verinin Doğası ve Ön İşleme¶
SVY 5430 Biyomedikal Verilerin Analizi ve Yapay Zeka · Fırat Üniversitesi Yazılım ve Bilişim Araştırma Enstitüsü
Bu defterde model kurmaktan çok, modelden önce yapılması gerekenlerle uğraşıyoruz. Beş bölüm var: eksik verinin gerçek yüzü, imputasyon seçeneklerinin dürüst karşılaştırması, veri sızıntısının ne kadar kolay olduğu, aynı hastanın iki kümeye düşmesinin başarımı nasıl şişirdiği ve dengesiz sınıflarda doğruluğun neden anlamsızlaştığı.
Her bölümün sonunda küçük bir "deneyin" notu var; derste birlikte ilerleyeceğiz.
1 · Veri: Pima diyabet seti ve gizli eksikler¶
768 kadın hasta, 8 klinik ölçüm, ikili sonuç (5 yıl içinde diyabet). Veri setinin ünlü bir tuzağı var: ölçülmemiş değerler 0 olarak kaydedilmiş. Glukozu 0 olan hasta olmaz; o hücre aslında boş.
import numpy as np, pandas as pd
import matplotlib.pyplot as plt
URL = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv"
COLS = ["gebelik", "glukoz", "tansiyon", "deri_kalinligi", "insulin", "vki", "soyagaci", "yas", "diyabet"]
try:
df = pd.read_csv(URL, header=None, names=COLS)
print("Veri indirildi:", df.shape)
except Exception as e:
print("İndirme başarısız, sentetik veri üretiliyor:", e)
rng = np.random.default_rng(0); n = 768
df = pd.DataFrame({"gebelik": rng.integers(0, 15, n), "glukoz": rng.normal(120, 30, n).clip(0),
"tansiyon": rng.normal(70, 12, n).clip(0), "deri_kalinligi": rng.normal(20, 10, n).clip(0),
"insulin": rng.normal(80, 100, n).clip(0), "vki": rng.normal(32, 7, n).clip(0),
"soyagaci": rng.random(n), "yas": rng.integers(21, 70, n)})
df["diyabet"] = (rng.random(n) < 1 / (1 + np.exp(-(df.glukoz - 125) / 25))).astype(int)
oranlar = {"glukoz": 0.01, "tansiyon": 0.05, "deri_kalinligi": 0.3, "insulin": 0.49, "vki": 0.015}
for c, o in oranlar.items():
df.loc[rng.random(n) < o, c] = 0
sifir_olamaz = ["glukoz", "tansiyon", "deri_kalinligi", "insulin", "vki"]
print("\nSıfır olarak kodlanmış hücre sayısı:")
print((df[sifir_olamaz] == 0).sum())
Veri indirildi: (768, 9) Sıfır olarak kodlanmış hücre sayısı: glukoz 5 tansiyon 35 deri_kalinligi 227 insulin 374 vki 11 dtype: int64
describe() bu tuzağı size söylemez; minimum 0 görünür ve geçer gidersiniz. Önce sıfırları gerçek boşluğa (NaN) çevirelim,
sonra eksikliğin rastgele olup olmadığına bakalım. Sorumuz şu: insülin ölçülmeyen hastalar, ölçülenlerle aynı hastalar mı?
X = df.drop(columns="diyabet").copy()
y = df["diyabet"]
X[sifir_olamaz] = X[sifir_olamaz].replace(0, np.nan)
eksik = X.isna().mean().sort_values(ascending=False)
print("Eksik oranı:\n", eksik.round(3), "\n")
# Eksiklik sonuçla ilişkili mi? (MAR/MNAR ipucu)
tablo = pd.DataFrame({
"insulin_eksik_orani": [X.loc[y == 0, "insulin"].isna().mean(), X.loc[y == 1, "insulin"].isna().mean()],
"deri_eksik_orani": [X.loc[y == 0, "deri_kalinligi"].isna().mean(), X.loc[y == 1, "deri_kalinligi"].isna().mean()],
}, index=["diyabet yok", "diyabet var"]).round(3)
tablo
Eksik oranı: insulin 0.487 deri_kalinligi 0.296 tansiyon 0.046 vki 0.014 glukoz 0.007 gebelik 0.000 soyagaci 0.000 yas 0.000 dtype: float64
| insulin_eksik_orani | deri_eksik_orani | |
|---|---|---|
| diyabet yok | 0.472 | 0.278 |
| diyabet var | 0.515 | 0.328 |
Deneyin: İnsülin eksikliği iki grupta aynı oranda mı? Değilse eksikliğin kendisi bilgi taşıyor demektir. Bu durumda "eksik mi?" sorusunu ayrı bir sütun olarak modele vermek (eksiklik göstergesi) çoğu zaman başarımı artırır.
2 · İmputasyon: beş yöntem, tek dürüst karşılaştırma¶
Karşılaştırmayı dürüst yapmanın tek yolu, imputasyonu çapraz doğrulama döngüsünün içine koymak. Yani Pipeline.
Aksi hâlde test katlamasındaki bilgi, medyanı hesaplarken eğitime karışır. Etkisi küçük olabilir ama alışkanlık büyük.
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.experimental import enable_iterative_imputer # noqa
from sklearn.impute import IterativeImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
def boru(imputer):
return Pipeline([("impute", imputer), ("olcek", StandardScaler()),
("lr", LogisticRegression(max_iter=2000))])
adaylar = {
"medyan": SimpleImputer(strategy="median"),
"medyan + gösterge": SimpleImputer(strategy="median", add_indicator=True),
"KNN (k=5)": KNNImputer(n_neighbors=5),
"MICE (iteratif)": IterativeImputer(random_state=0, max_iter=10),
"MICE + gösterge": IterativeImputer(random_state=0, max_iter=10, add_indicator=True),
}
for ad, imp in adaylar.items():
s = cross_val_score(boru(imp), X, y, cv=cv, scoring="roc_auc")
print(f"{ad:22s} ROC-AUC = {s.mean():.3f} ± {s.std():.3f}")
medyan ROC-AUC = 0.837 ± 0.020 medyan + gösterge ROC-AUC = 0.836 ± 0.022
KNN (k=5) ROC-AUC = 0.838 ± 0.022
MICE (iteratif) ROC-AUC = 0.837 ± 0.022
MICE + gösterge ROC-AUC = 0.838 ± 0.022
Farklar küçük; bu normal. Ders kitapları imputasyonu büyük bir mesele gibi anlatır ama pratikte hangi yöntemi seçtiğinizden çok, eksikliğin neden olduğunu anlamanız sonucu değiştirir. Göstergeli sürümlerin öne çıkıp çıkmadığına bakın.
3 · Sızıntı ne kadar kolay? Tamamen rastgele veriyle AUC 0.80¶
Şimdi kasıtlı bir hata yapacağız. 200 hastalık bir veri seti, tamamen rastgele 2000 öznitelik, rastgele etiket. Hiçbir şey öğrenilemez; doğru AUC 0.50 olmalı. Ama özellik seçimini çapraz doğrulamanın dışında yaparsak...
from sklearn.feature_selection import SelectKBest, f_classif
rng = np.random.default_rng(1)
Xr = rng.standard_normal((200, 2000)); yr = rng.integers(0, 2, 200)
# YANLIŞ: en iyi 20 özniteliği tüm veriye bakarak seç, sonra çapraz doğrula
Xsec = SelectKBest(f_classif, k=20).fit_transform(Xr, yr)
yanlis = cross_val_score(LogisticRegression(max_iter=2000), Xsec, yr, cv=cv, scoring="roc_auc").mean()
# DOĞRU: seçim de Pipeline'ın içinde, her katlamada yeniden
dogru_boru = Pipeline([("sec", SelectKBest(f_classif, k=20)), ("lr", LogisticRegression(max_iter=2000))])
dogru = cross_val_score(dogru_boru, Xr, yr, cv=cv, scoring="roc_auc").mean()
print(f"Rastgele veri, seçim dışarıda : ROC-AUC = {yanlis:.3f} <- sızıntı")
print(f"Rastgele veri, seçim Pipeline : ROC-AUC = {dogru:.3f} <- gerçek")
Rastgele veri, seçim dışarıda : ROC-AUC = 0.844 <- sızıntı Rastgele veri, seçim Pipeline : ROC-AUC = 0.496 <- gerçek
Bu, omik verideki (p ≫ n) en yaygın hatadır ve yayımlanmış makalelerde defalarca görülmüştür. Kural basit: test katlamasının etiketine dokunan her adım Pipeline'ın içinde olmalı. Ölçekleme, imputasyon, özellik seçimi, hepsi.
4 · Aynı hasta iki kümede: hasta-bazlı bölme¶
Pima'da her hasta bir satır. Ama EEG segmentleri, çok sayıda görüntü ya da tekrarlayan yatışlarla çalışırken aynı hastadan onlarca satır olur. Bunu simüle edelim: her hastadan 5 "kayıt" üretelim (küçük gürültüyle), sonra rastgele bölme ile hasta-bazlı bölmeyi karşılaştıralım. Model olarak bilerek ezberlemeye yatkın bir şey seçiyoruz: k-NN (k=1).
from sklearn.model_selection import KFold, GroupKFold
from sklearn.neighbors import KNeighborsClassifier
Xi = SimpleImputer(strategy="median").fit_transform(X)
rng = np.random.default_rng(2)
tekrar = 5
Xg = np.vstack([Xi + rng.normal(0, 0.02 * Xi.std(axis=0), Xi.shape) for _ in range(tekrar)])
yg = np.tile(y.values, tekrar)
hasta_id = np.tile(np.arange(len(y)), tekrar)
ezber = Pipeline([("olcek", StandardScaler()), ("knn", KNeighborsClassifier(n_neighbors=1))])
rastgele = cross_val_score(ezber, Xg, yg, cv=KFold(5, shuffle=True, random_state=0), scoring="roc_auc").mean()
gruplu = cross_val_score(ezber, Xg, yg, cv=GroupKFold(5), groups=hasta_id, scoring="roc_auc").mean()
print(f"Rastgele KFold (aynı hasta iki kümede olabilir): ROC-AUC = {rastgele:.3f}")
print(f"GroupKFold (hasta tek kümede): ROC-AUC = {gruplu:.3f}")
Rastgele KFold (aynı hasta iki kümede olabilir): ROC-AUC = 1.000 GroupKFold (hasta tek kümede): ROC-AUC = 0.644
Deneyin: Gürültüyü 0.02 yerine 0.3 yapın; rastgele bölmedeki AUC ne kadar düşüyor? Fark sürdükçe model hastalığı değil hastayı tanıyor. 7. haftada EEG'de aynı deneyi gerçek kayıtlarla yapacağız.
5 · Dengesiz sınıf: doğruluk %90 ama model işe yaramıyor¶
Pozitif oranını yapay olarak %8'e düşürelim (nadir hastalık senaryosu). "Herkes sağlıklı" diyen bir model %92 doğru olur. Klinik olarak sıfır değeri vardır. Doğruluk yerine PR-AUC ve duyarlılığa bakalım; sınıf ağırlığı ve SMOTE'un ne yaptığını görelim.
from sklearn.metrics import average_precision_score, recall_score, accuracy_score, roc_auc_score
from sklearn.model_selection import cross_val_predict
pos = np.where(y == 1)[0]; neg = np.where(y == 0)[0]
rng = np.random.default_rng(3)
sec_pos = rng.choice(pos, size=int(len(neg) * 0.08), replace=False)
idx = np.concatenate([neg, sec_pos]); rng.shuffle(idx)
Xd, yd = X.iloc[idx], y.iloc[idx]
print(f"Örneklem: {len(yd)} hasta, pozitif oranı %{100*yd.mean():.1f}\n")
def degerlendir(ad, model):
p = cross_val_predict(model, Xd, yd, cv=cv, method="predict_proba")[:, 1]
yhat = (p >= 0.5).astype(int)
print(f"{ad:24s} doğruluk {accuracy_score(yd, yhat):.3f} | duyarlılık {recall_score(yd, yhat):.3f} | "
f"ROC-AUC {roc_auc_score(yd, p):.3f} | PR-AUC {average_precision_score(yd, p):.3f}")
taban = [("impute", SimpleImputer(strategy="median")), ("olcek", StandardScaler())]
degerlendir("düz lojistik", Pipeline(taban + [("lr", LogisticRegression(max_iter=2000))]))
degerlendir("class_weight='balanced'", Pipeline(taban + [("lr", LogisticRegression(max_iter=2000, class_weight="balanced"))]))
try:
from imblearn.pipeline import Pipeline as ImbPipeline
from imblearn.over_sampling import SMOTE
degerlendir("SMOTE (Pipeline içinde)", ImbPipeline(taban + [("smote", SMOTE(random_state=0)), ("lr", LogisticRegression(max_iter=2000))]))
except ImportError:
print("imbalanced-learn kurulu değil; SMOTE satırı atlandı (pip install imbalanced-learn)")
Örneklem: 540 hasta, pozitif oranı %7.4 düz lojistik doğruluk 0.920 | duyarlılık 0.075 | ROC-AUC 0.782 | PR-AUC 0.225
class_weight='balanced' doğruluk 0.741 | duyarlılık 0.650 | ROC-AUC 0.778 | PR-AUC 0.242
SMOTE (Pipeline içinde) doğruluk 0.731 | duyarlılık 0.625 | ROC-AUC 0.778 | PR-AUC 0.256
Üç şeye dikkat edin. Birincisi, ROC-AUC üç modelde de neredeyse aynı; sıralama gücü değişmedi. İkincisi, duyarlılık düz modelde
çok düşük, ağırlıklı modelde yükseldi; değişen şey aslında eşik. Üçüncüsü, SMOTE'un Pipeline içinde olması şart: sentetik
örnekler test katlamasına sızarsa PR-AUC yalan söyler.
Klinik sonuç: dengesiz sınıfta yeniden örnekleme çoğu zaman şık bir eşik ayarından fazlasını vermez. Önce eşiği klinik maliyete göre seçin (4. hafta), sonra hâlâ gerekiyorsa örneklemeye bakın.
6 · Bugün ne gördük?¶
| Konu | Bugünkü kanıt | Alışkanlık |
|---|---|---|
| Sıfır = eksik | Pima'da insülinin yarısı | describe() yetmez; alan bilgisiyle bak |
| Eksiklik bilgi taşır | Eksiklik oranı sınıfa göre farklı | add_indicator=True dene |
| İmputasyon yöntemi | Farklar küçük | Mekanizmayı anla, yöntemi abartma |
| Sızıntı | Rastgele veriyle AUC 0.80 | Etikete dokunan her adım Pipeline'da |
| Hasta-bazlı bölme | KFold vs GroupKFold farkı | Grup kimliğini ilk günden taşı |
| Dengesiz sınıf | Doğruluk yüksek, duyarlılık düşük | PR-AUC + eşik; SMOTE'u Pipeline'da |
Ders sonrası (isteğe bağlı): Bölüm 4'te tekrar=20 ile deneyi yineleyin; Bölüm 5'te eşiği 0.5 yerine 0.2 yapıp
duyarlılık–özgüllük değişimini not edin. Kendi tez verinizde "grup" hangi sütun olurdu, bir cümleyle yazın.