Model kodunun %80'i model değildir
Önceki iki derste veriler hep tertemiz sayılardı. Gerçekte Ilgaz Bank'ın müşteri tablosunda yaş bazen boş, şehir metin, segment kategorik, bakiye 100 TL ile 2 milyon TL arasında değişiyor. Modeller metni anlamaz, boş hücrede durur, ölçek farkından etkilenir. Bu yüzden her projede bir ön işleme (preprocessing) katmanı vardır: eksik doldurma, ölçekleme, kategorik kodlama. Bu derste bu adımları scikit-learn'ün Pipeline ve ColumnTransformer yapılarıyla tek bir nesneye bağlamayı, modeli çapraz doğrulamayla dürüstçe ölçmeyi, hiperparametreleri sistematik aramayı, modeli kaydetmeyi ve sonuçları bir model kartıyla raporlamayı öğreneceksin.
Senaryo: Ilgaz Bank, önümüzdeki üç ayda hesabını kapatacak (churn) müşterileri önceden bulup elde tutma kampanyası yapmak istiyor. Veri kurgusaldır ve tohumla üretilir.
import numpy as np
import pandas as pd
def churn_verisi(n=600, seed=1):
"""Ilgaz Bank kurgusal müşteri kaybı (churn) verisi; eksik değerler içerir."""
rng = np.random.default_rng(seed)
yas = rng.integers(18, 75, n).astype(float)
bakiye = rng.gamma(2, 15000, n)
urun = rng.integers(1, 5, n)
sikayet = rng.poisson(0.5, n)
sehir = rng.choice(["İstanbul", "Ankara", "İzmir", "Bursa"], n)
segment = rng.choice(["Bireysel", "Premium", "KOBİ"], n, p=[0.6, 0.25, 0.15])
logit = -1.2 + 0.9 * sikayet - 0.6 * (urun - 2) + 0.03 * (yas - 45) - 0.8 * (segment == "Premium")
y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
yas[rng.random(n) < 0.08] = np.nan
bakiye[rng.random(n) < 0.05] = np.nan
X = pd.DataFrame({"yas": yas, "bakiye": bakiye.round(2), "urun": urun, "sikayet": sikayet,
"sehir": sehir, "segment": segment})
return X, y
X, y = churn_verisi()
print(X.shape, "kayıp oranı:", round(y.mean(), 3))
print(X.head(3))
print(X.isna().sum().to_dict())
Dönüştürücüler: ölçek, kodlama, doldurma
Dönüştürücüler (transformer) fit ile veriden bir şey öğrenir (ortalama, medyan, kategori listesi) ve transform ile bunu uygular. fit_transform ikisini birden yapar. Kritik kural: fit yalnızca eğitim verisinde yapılır; test verisine yalnızca transform uygulanır.
| Dönüştürücü | Ne öğrenir | Ne yapar | Ne zaman |
|---|---|---|---|
StandardScaler | Ortalama ve standart sapma | (x − ort) / std; ortalama 0, std 1 | Doğrusal modeller, KNN, SVM |
MinMaxScaler | Min ve max | 0-1 aralığına sıkıştırır | Sınırlı aralık gereken durumlar, sinir ağları |
OneHotEncoder | Kategori listesi | Her kategoriye 0/1 sütun | Sırasız kategoriler (şehir, segment) |
OrdinalEncoder | Kategori listesi | Kategoriyi tam sayıya çevirir | Sıralı kategoriler, ağaç modelleri |
SimpleImputer | Ortalama/medyan/en sık değer | Eksikleri doldurur | Eksik değer içeren her sütun |
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder, StandardScaler
bakiye = np.array([[1000.0], [5000.0], [9000.0]])
sc = StandardScaler().fit(bakiye)
print(sc.mean_, sc.scale_.round(1))
print(sc.transform([[5000.0], [13000.0]]).ravel().round(3))
print(MinMaxScaler().fit_transform(bakiye).ravel())
sehir = pd.DataFrame({"sehir": ["İzmir", "Ankara", "İzmir"]})
enc = OneHotEncoder(handle_unknown="ignore", sparse_output=False).fit(sehir)
print(enc.get_feature_names_out().tolist())
print(enc.transform(pd.DataFrame({"sehir": ["Ankara", "Van"]})).tolist()) # Van hiç görülmedi
yas = np.array([[30.0], [np.nan], [50.0], [41.0]])
print(SimpleImputer(strategy="median").fit_transform(yas).ravel())
ValueError verir ve tahmin servisi çöker. handle_unknown="ignore" bilinmeyen kategoriyi tüm sütunları 0 olan bir satırla temsil eder. sparse_output=False ise sonucu seyrek matris yerine normal numpy dizisi yapar (sklearn 1.2 öncesinde adı sparse idi).ColumnTransformer ve Pipeline
ColumnTransformer farklı sütun gruplarına farklı işlem uygular: sayısallara doldur + ölçekle, kategoriklere doldur + kodla. Pipeline ise adımları sıraya dizer ve son adım olarak modeli ekler. Ortaya çıkan nesne bir model gibi davranır: fit, predict, score. Her adıma bir ad verirsin; bu adlar hiperparametre ararken adim__parametre biçiminde kullanılır.
import numpy as np
import pandas as pd
def churn_verisi(n=600, seed=1):
"""Ilgaz Bank kurgusal müşteri kaybı (churn) verisi; eksik değerler içerir."""
rng = np.random.default_rng(seed)
yas = rng.integers(18, 75, n).astype(float)
bakiye = rng.gamma(2, 15000, n)
urun = rng.integers(1, 5, n)
sikayet = rng.poisson(0.5, n)
sehir = rng.choice(["İstanbul", "Ankara", "İzmir", "Bursa"], n)
segment = rng.choice(["Bireysel", "Premium", "KOBİ"], n, p=[0.6, 0.25, 0.15])
logit = -1.2 + 0.9 * sikayet - 0.6 * (urun - 2) + 0.03 * (yas - 45) - 0.8 * (segment == "Premium")
y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
yas[rng.random(n) < 0.08] = np.nan
bakiye[rng.random(n) < 0.05] = np.nan
X = pd.DataFrame({"yas": yas, "bakiye": bakiye.round(2), "urun": urun, "sikayet": sikayet,
"sehir": sehir, "segment": segment})
return X, y
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
def on_isleyici(sayisal, kategorik):
sayi_hatti = Pipeline([
("doldur", SimpleImputer(strategy="median")),
("olcek", StandardScaler()),
])
kat_hatti = Pipeline([
("doldur", SimpleImputer(strategy="most_frequent")),
("kodla", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
return ColumnTransformer([
("sayisal", sayi_hatti, sayisal),
("kategorik", kat_hatti, kategorik),
])
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
X, y = churn_verisi()
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=0, stratify=y)
model = Pipeline([
("on_isleme", on_isleyici(["yas", "bakiye", "urun", "sikayet"], ["sehir", "segment"])),
("model", LogisticRegression(max_iter=1000)),
])
model.fit(X_tr, y_tr) # tüm fit'ler yalnızca eğitim verisinde
risk = model.predict_proba(X_te)[:, 1] # test yalnızca transform edilir
print("Test ROC-AUC:", round(roc_auc_score(y_te, risk), 3))
print(model.named_steps["on_isleme"].get_feature_names_out()[:6].tolist())
print(model.get_params()["model__C"])
Pipeline'ın üç büyük kazancı var: (1) sızıntıyı önler, çünkü çapraz doğrulamada her katta dönüştürücüler yalnızca o katın eğitim kısmında fit edilir; (2) canlıya tek nesne gider, ön işlemeyi ayrıca kopyalamazsın; (3) ön işleme ayarları da hiperparametre gibi aranabilir (on_isleme__sayisal__doldur__strategy).
Çapraz doğrulama
Tek bir eğitim/test bölmesi şansa bağlıdır: başka bir random_state ile skor ±0.03 oynayabilir. k-katlı çapraz doğrulama (k-fold cross-validation) veriyi k parçaya böler; her seferinde bir parçayı doğrulama, kalanını eğitim yapar ve k skor üretir. Ortalaması daha güvenilir bir tahmin, standart sapması ise belirsizlik ölçüsüdür.
| Bölücü | Ne yapar | Ne zaman |
|---|---|---|
KFold(n_splits, shuffle, random_state) | Düz k parça | Regresyon |
StratifiedKFold | Her katta sınıf oranını korur | Sınıflandırma (varsayılan seçim) |
GroupKFold | Aynı gruptaki (müşteri) satırları aynı katta tutar | Bir müşterinin birden fazla satırı varsa |
TimeSeriesSplit | Hep geçmişle eğitir, gelecekle doğrular | Zaman serisi |
import numpy as np
import pandas as pd
def churn_verisi(n=600, seed=1):
"""Ilgaz Bank kurgusal müşteri kaybı (churn) verisi; eksik değerler içerir."""
rng = np.random.default_rng(seed)
yas = rng.integers(18, 75, n).astype(float)
bakiye = rng.gamma(2, 15000, n)
urun = rng.integers(1, 5, n)
sikayet = rng.poisson(0.5, n)
sehir = rng.choice(["İstanbul", "Ankara", "İzmir", "Bursa"], n)
segment = rng.choice(["Bireysel", "Premium", "KOBİ"], n, p=[0.6, 0.25, 0.15])
logit = -1.2 + 0.9 * sikayet - 0.6 * (urun - 2) + 0.03 * (yas - 45) - 0.8 * (segment == "Premium")
y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
yas[rng.random(n) < 0.08] = np.nan
bakiye[rng.random(n) < 0.05] = np.nan
X = pd.DataFrame({"yas": yas, "bakiye": bakiye.round(2), "urun": urun, "sikayet": sikayet,
"sehir": sehir, "segment": segment})
return X, y
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
def on_isleyici(sayisal, kategorik):
sayi_hatti = Pipeline([
("doldur", SimpleImputer(strategy="median")),
("olcek", StandardScaler()),
])
kat_hatti = Pipeline([
("doldur", SimpleImputer(strategy="most_frequent")),
("kodla", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
return ColumnTransformer([
("sayisal", sayi_hatti, sayisal),
("kategorik", kat_hatti, kategorik),
])
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score, cross_validate
X, y = churn_verisi()
pipe = Pipeline([
("on_isleme", on_isleyici(["yas", "bakiye", "urun", "sikayet"], ["sehir", "segment"])),
("model", LogisticRegression(max_iter=1000)),
])
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
skorlar = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc")
print(skorlar.round(3), "ort:", round(skorlar.mean(), 3), "std:", round(skorlar.std(), 3))
detay = cross_validate(pipe, X, y, cv=cv, scoring=["roc_auc", "recall"], return_train_score=True)
print("eğitim AUC:", detay["train_roc_auc"].mean().round(3), "doğrulama AUC:", detay["test_roc_auc"].mean().round(3))
X_olcekli = StandardScaler().fit_transform(X) deyip sonra cross_val_score(model, X_olcekli, y) çağırmak. Ölçekleyici tüm veriyi, yani doğrulama katlarını da görmüş olur. Doğru: ölçekleyiciyi pipeline'a koy ve pipeline'ı çapraz doğrula. Kural: veriden bir şey öğrenen her adım pipeline'ın içinde olmalı.Hiperparametre arama
GridSearchCV verdiğin tüm kombinasyonları dener, her biri için çapraz doğrulama yapar ve en iyisini seçer; sonunda en iyi ayarla tüm eğitim verisinde yeniden fit eder (refit=True). Kombinasyon sayısı hızla patlar (4 × 4 × 3 = 48 ayar × 5 kat = 240 eğitim). RandomizedSearchCV ise dağılımlardan n_iter kadar rastgele ayar dener; geniş arama uzaylarında aynı bütçeyle genellikle daha iyi sonuç bulur.
import numpy as np
import pandas as pd
def churn_verisi(n=600, seed=1):
"""Ilgaz Bank kurgusal müşteri kaybı (churn) verisi; eksik değerler içerir."""
rng = np.random.default_rng(seed)
yas = rng.integers(18, 75, n).astype(float)
bakiye = rng.gamma(2, 15000, n)
urun = rng.integers(1, 5, n)
sikayet = rng.poisson(0.5, n)
sehir = rng.choice(["İstanbul", "Ankara", "İzmir", "Bursa"], n)
segment = rng.choice(["Bireysel", "Premium", "KOBİ"], n, p=[0.6, 0.25, 0.15])
logit = -1.2 + 0.9 * sikayet - 0.6 * (urun - 2) + 0.03 * (yas - 45) - 0.8 * (segment == "Premium")
y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
yas[rng.random(n) < 0.08] = np.nan
bakiye[rng.random(n) < 0.05] = np.nan
X = pd.DataFrame({"yas": yas, "bakiye": bakiye.round(2), "urun": urun, "sikayet": sikayet,
"sehir": sehir, "segment": segment})
return X, y
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
def on_isleyici(sayisal, kategorik):
sayi_hatti = Pipeline([
("doldur", SimpleImputer(strategy="median")),
("olcek", StandardScaler()),
])
kat_hatti = Pipeline([
("doldur", SimpleImputer(strategy="most_frequent")),
("kodla", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
return ColumnTransformer([
("sayisal", sayi_hatti, sayisal),
("kategorik", kat_hatti, kategorik),
])
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
X, y = churn_verisi()
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=0, stratify=y)
pipe = Pipeline([
("on_isleme", on_isleyici(["yas", "bakiye", "urun", "sikayet"], ["sehir", "segment"])),
("model", LogisticRegression(max_iter=1000)),
])
izgara = {
"model__C": [0.01, 0.1, 1, 10],
"model__class_weight": [None, "balanced"],
}
arama = GridSearchCV(pipe, izgara, cv=StratifiedKFold(5, shuffle=True, random_state=0), scoring="roc_auc")
arama.fit(X_tr, y_tr)
print("en iyi:", arama.best_params_)
print("CV AUC:", round(arama.best_score_, 3))
print("Test AUC:", round(arama.score(X_te, y_te), 3)) # scoring="roc_auc" ile ölçülür
best_score_ çapraz doğrulama ortalamasıdır, test skoru değildir. Seçim süreci bu skorları kullandığı için hafif iyimserdir; asıl raporlayacağın sayı, aramaya hiç girmemiş test setindeki skordur.
Öğrenme eğrisi mantığı
"Daha fazla veri toplamaya değer mi?" sorusunun cevabı öğrenme eğrisidir (learning curve): eğitim verisi miktarını artırarak eğitim ve doğrulama skorlarını çizersin. İki eğri birbirine yaklaşıp düz bir platoya oturduysa daha çok veri pek yardım etmez; daha zengin özellik ya da daha güçlü model gerekir. Doğrulama skoru hâlâ yükseliyor ve eğitim skoruyla arasında büyük boşluk varsa (yüksek varyans), daha fazla veri işe yarar. sklearn.model_selection.learning_curve bunu çapraz doğrulamayla hesaplar.
import numpy as np
import pandas as pd
def churn_verisi(n=600, seed=1):
"""Ilgaz Bank kurgusal müşteri kaybı (churn) verisi; eksik değerler içerir."""
rng = np.random.default_rng(seed)
yas = rng.integers(18, 75, n).astype(float)
bakiye = rng.gamma(2, 15000, n)
urun = rng.integers(1, 5, n)
sikayet = rng.poisson(0.5, n)
sehir = rng.choice(["İstanbul", "Ankara", "İzmir", "Bursa"], n)
segment = rng.choice(["Bireysel", "Premium", "KOBİ"], n, p=[0.6, 0.25, 0.15])
logit = -1.2 + 0.9 * sikayet - 0.6 * (urun - 2) + 0.03 * (yas - 45) - 0.8 * (segment == "Premium")
y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
yas[rng.random(n) < 0.08] = np.nan
bakiye[rng.random(n) < 0.05] = np.nan
X = pd.DataFrame({"yas": yas, "bakiye": bakiye.round(2), "urun": urun, "sikayet": sikayet,
"sehir": sehir, "segment": segment})
return X, y
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
def on_isleyici(sayisal, kategorik):
sayi_hatti = Pipeline([
("doldur", SimpleImputer(strategy="median")),
("olcek", StandardScaler()),
])
kat_hatti = Pipeline([
("doldur", SimpleImputer(strategy="most_frequent")),
("kodla", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
return ColumnTransformer([
("sayisal", sayi_hatti, sayisal),
("kategorik", kat_hatti, kategorik),
])
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, learning_curve
X, y = churn_verisi()
pipe = Pipeline([("on_isleme", on_isleyici(["yas", "bakiye", "urun", "sikayet"], ["sehir", "segment"])),
("model", LogisticRegression(max_iter=1000))])
boyutlar, egitim, dogrulama = learning_curve(
pipe, X, y, train_sizes=[0.1, 0.3, 0.6, 1.0],
cv=StratifiedKFold(5, shuffle=True, random_state=0), scoring="roc_auc",
)
for n, e, d in zip(boyutlar, egitim.mean(axis=1), dogrulama.mean(axis=1)):
print(f"{n:>4} örnek eğitim={e:.3f} doğrulama={d:.3f}")
Modeli kaydetmek
Eğitilmiş pipeline'ı diske yazıp tahmin servisinde yüklersin. scikit-learn için önerilen araç joblib'dir (büyük numpy dizilerinde pickle'dan verimli). Aşağıda dosya yerine bellek içi io.BytesIO kullanıyoruz; gerçek projede joblib.dump(model, "churn_v3.joblib") yazarsın.
import io
import joblib
import pickle
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_iris(return_X_y=True)
pipe = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)).fit(X, y)
tampon = io.BytesIO()
joblib.dump(pipe, tampon)
print("boyut > 0:", tampon.tell() > 0)
tampon.seek(0) # okumadan önce başa sar
yuklenen = joblib.load(tampon)
print((yuklenen.predict(X[:5]) == pipe.predict(X[:5])).all())
veri = pickle.dumps(pipe) # pickle da çalışır
print(type(pickle.loads(veri)).__name__)
Model kartı ve raporlama
Model kartı (model card), bir modelin kimlik belgesidir: ne için yapıldı, hangi veriyle eğitildi, nasıl ölçüldü, nerede kullanılmamalı. Denetçinin, ürün yöneticisinin ve altı ay sonraki senin ilk okuyacağın belgedir.
| Bölüm | İçerik örneği |
|---|---|
| Amaç ve kapsam | 3 ay içinde hesap kapatma riskini tahmin; yalnız bireysel ve KOBİ müşterileri |
| Veri | Kaynak tablolar, tarih aralığı, satır sayısı, hedef tanımı, eksik oranları |
| Model | Pipeline adımları, hiperparametreler, sklearn sürümü, eğitim tarihi |
| Değerlendirme | CV ortalama ± std, test metrikleri, taban çizgi, seçilen eşik ve gerekçesi |
| Sınırlar ve riskler | Yeni şubelerde güven düşük; segment bazında performans farkları; izleme planı |
Derinleş: modeli açıklamak, adil olup olmadığını ölçmek, aramayı hızlandırmak
Skor tablosu "model ne kadar iyi?" sorusunu cevaplar; iş ekibi ise "neden bu kişiyi riskli buldu, kimde yanılıyor, herkese eşit davranıyor mu?" diye sorar. Bu bölümde Pusula İK'nın gerçek çalışan verisiyle (pusula_ik, 2199 çalışan) gönüllü ayrılma modeli kuracak, sonra onu açıklayacak ve denetleyeceksin. Etiket: term_type == "gönüllü" (yaklaşık %27). Cinsiyet modele özellik olarak verilmiyor; yalnızca adalet denetiminde kullanılıyor.
import veri
import pandas as pd
def ik_verisi():
e = veri.oku("pusula_ik", "ik_employees")
p = veri.oku("pusula_ik", "ik_positions")
d = e.merge(p[["position_id", "level"]], on="position_id")
d["ise_giris_yasi"] = (pd.to_datetime(d["hire_date"]).dt.year - d["birth_year"]).astype(float)
d["level"] = d["level"].astype(float)
y = (d["term_type"] == "gönüllü").astype(int)
X = d[["ise_giris_yasi", "level", "education", "city", "work_mode", "hire_source"]]
return X, y, d["gender"]
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
X, y, cinsiyet = ik_verisi()
X_tr, X_te, y_tr, y_te, g_tr, g_te = train_test_split(
X, y, cinsiyet, test_size=0.3, random_state=0, stratify=y)
kategorik = ["education", "city", "work_mode", "hire_source"]
on_isleme = ColumnTransformer(
[("kat", OneHotEncoder(handle_unknown="ignore"), kategorik)], remainder="passthrough")
model = Pipeline([("on", on_isleme), ("model", LogisticRegression(max_iter=1000))])
model.fit(X_tr, y_tr)
from sklearn.metrics import roc_auc_score
print("test ROC-AUC:", round(roc_auc_score(y_te, model.predict_proba(X_te)[:, 1]), 3))AUC'nin 0.6 civarında olması gerçekçi: işe girişte bilinen birkaç özellik, kimin ayrılacağını ancak kısmen açıklar. Böyle bir modeli canlıya almadan önce aşağıdaki dört soruyu sormak zorundasın.
1) Hangi özellik önemli? Permutation importance
Fikir: bir sütunu karıştır (satırlar arasında rastgele yer değiştir), modeli yeniden eğitmeden skoru ölç. Skor ne kadar düşerse model o sütuna o kadar dayanıyordur. Ağaçların feature_importances_ değerinden farkı: test verisinde ve seçtiğin metrikle ölçülür, her model türünde çalışır ve çok değerli (yüksek kardinaliteli) sütunlara karşı yanlı değildir. Pipeline'a ham sütunları verdiğin için önem de ham sütun düzeyinde çıkar.
from sklearn.inspection import permutation_importance r = permutation_importance(model, X_te, y_te, scoring="roc_auc", n_repeats=5, random_state=0) onem = pd.Series(r.importances_mean, index=X_te.columns).sort_values(ascending=False) print(onem.round(4))
2) Etki hangi yönde? PDP ve ICE
Kısmi bağımlılık (partial dependence, PDP) tek bir özelliği sırayla her değere sabitler, tüm satırlar için tahmin alır ve ortalamasını çizer: "diğer her şey veri gibiyken seviye 1'den 6'ya çıkınca ortalama ayrılma olasılığı nasıl değişir?" ICE (individual conditional expectation) aynı hesabı ortalama almadan, her satır için ayrı çizgi olarak gösterir; çizgiler birbirine paralel değilse etkileşim vardır.
from sklearn.inspection import partial_dependence
pdp = partial_dependence(model, X_tr, ["level"], kind="both")
print("ızgara:", pdp["grid_values"][0])
print("PDP :", pdp["average"][0].round(3))
print("ICE boyutu:", pdp["individual"].shape) # (çıktı, satır, ızgara)Grafiği PartialDependenceDisplay.from_estimator(model, X_tr, ["level"], kind="both") ile çizersin. Tamsayı sütunlar için scikit-learn uyarı verir; sütunu önceden float yapmamızın nedeni bu. SHAP değerleri ise her tahmini özellik katkılarına böler ("bu çalışanın riskini en çok seviyesi artırdı"); pip install shap ile kendi bilgisayarında dene:
import shap ozellik = model[:-1].transform(X_te) aciklayici = shap.LinearExplainer(model[-1], ozellik) degerler = aciklayici(ozellik) shap.plots.beeswarm(degerler) # genel tablo shap.plots.waterfall(degerler[0]) # tek çalışan için açıklama
3) Kimde yanılıyor? Hata dilimi analizi
Ortalama hata, küçük bir segmentte çok kötü çalışmayı gizler. Test tahminlerini segmentlere (şehir, çalışma biçimi, işe alım kaynağı) göre grupla; her segment için satır sayısı ve hata oranı çıkar, en kötüden iyiye sırala. Çok küçük segmentleri (ör. 30 satırdan az) ayrı işaretle; orada gördüğün fark gürültü olabilir.
tahmin = (model.predict_proba(X_te)[:, 1] >= 0.3).astype(int)
dilim = X_te.assign(hata=(tahmin != y_te.to_numpy()))
tablo = dilim.groupby("city")["hata"].agg(n="size", hata_orani="mean")
print(tablo.sort_values("hata_orani", ascending=False).round(3))4) Herkese eşit mi? Grup bazında metrikler
| Metrik | Grup başına hesap | İK örneğinde soru |
|---|---|---|
| Seçim oranı (demographic parity) | tahmin = 1 oranı | Kadınların ne kadarı "riskli" işaretleniyor? |
| FPR (yanlış alarm) | FP / (FP + TN) | Kalacak çalışanların ne kadarına gereksiz müdahale ediliyor? |
| FNR (kaçırma, equal opportunity) | FN / (FN + TP) | Ayrılacak çalışanların ne kadarı gözden kaçıyor? |
def oranlar(s):
fp = ((s.tahmin == 1) & (s.y == 0)).sum(); tn = ((s.tahmin == 0) & (s.y == 0)).sum()
fn = ((s.tahmin == 0) & (s.y == 1)).sum(); tp = ((s.tahmin == 1) & (s.y == 1)).sum()
return pd.Series({"secim": s.tahmin.mean(), "fpr": fp / (fp + tn), "fnr": fn / (fn + tp)})
grup = pd.DataFrame({"y": y_te.to_numpy(), "tahmin": tahmin, "cinsiyet": g_te.to_numpy()})
print(grup.groupby("cinsiyet")[["y", "tahmin"]].apply(oranlar).round(3))Cinsiyeti modelden çıkarmak adaleti garanti etmez: şehir, eğitim ya da pozisyon seviyesi cinsiyetle ilişkiliyse (vekil değişken, proxy) model farkı yine öğrenir. Bu yüzden ölçmeden "adil" denmez. Farkı gördüğünde seçeneklerin: eşiği gruplara göre değil süreç düzeyinde gözden geçirmek, veriyi incelemek, sonucu model kartına yazmak ve kararı insan incelemesine bağlamak. Bu bulguları dersin önceki bölümündeki model kartına "Açıklanabilirlik" ve "Adalet denetimi" başlıklarıyla ekle.
MetricFrame) bu tabloyu tek satırda üretir.Arama bütçesi: Halving, Optuna ve erken durdurma
GridSearchCV her adayı tüm veriyle eğitir. HalvingGridSearchCV ardışık yarılama (successive halving) yapar: ilk turda tüm adaylar az kaynakla (az satırla) yarışır, en iyi 1/factor kadarı bir sonraki tura daha çok kaynakla geçer. Kötü adaylara bütçe harcanmaz.
from sklearn.experimental import enable_halving_search_cv # deneysel API'yi açar
from sklearn.model_selection import HalvingGridSearchCV
izgara = {"model__C": [0.1, 1.0], "model__class_weight": [None, "balanced"]}
arama = HalvingGridSearchCV(model, izgara, factor=2, scoring="roc_auc", cv=3, random_state=0)
arama.fit(X_tr, y_tr)
print("aday sayıları :", list(arama.n_candidates_))
print("kaynak (satır):", list(arama.n_resources_))
print("en iyi :", arama.best_params_)Izgara büyüdükçe rastgele ya da Bayesçi arama daha verimlidir. Optuna önceki denemelerin sonucuna bakarak umut verici bölgeden yeni değer önerir ve kötü giden denemeleri erkenden budar (pip install optuna):
import optuna
from sklearn.model_selection import cross_val_score
def amac(trial):
C = trial.suggest_float("C", 1e-3, 100, log=True)
agirlik = trial.suggest_categorical("class_weight", [None, "balanced"])
model.set_params(model__C=C, model__class_weight=agirlik)
return cross_val_score(model, X_tr, y_tr, cv=3, scoring="roc_auc").mean()
calisma = optuna.create_study(direction="maximize")
calisma.optimize(amac, n_trials=40, timeout=120) # bütçe: deneme sayısı ve süre
print(calisma.best_params)Erken durdurma (early stopping) ise tek bir modelin içinde bütçe tasarrufudur: boosting ya da sinir ağı her turda doğrulama kaybını ölçer, n_iter_no_change tur boyunca iyileşme yoksa durur (HistGradientBoosting'de early_stopping=True). Pratik kural: arama bütçesini (deneme sayısı × kat sayısı × eğitim süresi) önceden yaz, kaba aramayla başla, iyi bölgeyi daralt.
İç içe çapraz doğrulama
Hem model seçip hem de seçimin performansını yansız tahmin etmek istiyorsan iç içe CV (nested cross-validation) kullan: dış döngü performansı ölçer, her dış katın içinde ayrı bir GridSearchCV hiperparametre seçer. Kod olarak tek satırdır: cross_val_score(GridSearchCV(pipe, izgara, cv=ic_cv), X, y, cv=dis_cv). Küçük veri setlerinde raporlanan skorun güvenilirliğini ciddi artırır.
pandas çıktısı ve özellik adları
pipe.set_output(transform="pandas") ile dönüştürücüler numpy yerine sütun adlı DataFrame döndürür (OneHotEncoder'da sparse_output=False gerekir). Hata ayıklarken ve katsayıları özellik adlarıyla eşlerken çok işe yarar: pipe.named_steps["on_isleme"].get_feature_names_out() ile pipe.named_steps["model"].coef_ yan yana konunca hangi şehrin ya da segmentin kayıp riskini artırdığı tek tabloda görünür.
Kendi dönüştürücün
İşe özgü bir özellik (ör. bakiye / ürün sayısı) gerekiyorsa FunctionTransformer ile bir fonksiyonu pipeline adımına çevirebilir ya da BaseEstimator ve TransformerMixin'den türeyen bir sınıf yazabilirsin. Kural aynı: veriden öğrenilen her şey fit'te, uygulanan her şey transform'da.