Model, ona verdiğin özellik kadar iyidir
Ilgaz Bank pazarlama ekibi şunu soruyor: "Hangi müşteriler yılın ikinci yarısında kartıyla çok harcayacak? Onlara özel taksit kampanyası sunalım." Elinde müşteri tablosu (yaş, segment, risk puanı) ve 42 bin satırlık işlem tablosu var. Yalnız demografik sütunlarla kurulan model neredeyse yazı tura atmak kadar başarılı (ROC-AUC 0.53). Aynı işlem tablosundan üretilen birkaç akıllı sütunla skor 0.81'e çıkıyor. Modeli değiştirmedik; özellik mühendisliği (feature engineering) yaptık: ham veriyi, modelin öğrenebileceği biçime dönüştürdük.
Bu derste Ön işleme, pipeline ve model değerlendirme dersinde gördüğün ölçekleme ve one-hot kodlamanın ötesine geçeceksin: tarih, oran, grup istatistiği, gecikme (lag), hedef kodlama, binning, eksiklik göstergesi ve özellik seçimi. Hepsinin ortak kuralı tek: tahmin anında bilinmeyen hiçbir bilgi özelliğe sızmamalı.
| Özellik ailesi | Örnek (Ilgaz Bank) | Ne zaman işe yarar? | Tuzak |
|---|---|---|---|
| Tarih ve saat | işlem saati, hafta sonu, ay | Davranış zamana bağlıysa | Saat 23 ile 0'ı uzak sanmak |
| Oran, fark, etkileşim | kart harcaması / maaş | İlişki göreliyse | Sıfıra bölme |
| Dönüşüm (log, Box-Cox) | log(1 + kart toplamı) | Çarpık, uzun kuyruklu dağılım | Negatif değerin logu |
| Grup istatistiği | hesabın geçmiş ortalama tutarı | Kişiye göre "normal" değişiyorsa | Geleceği içeren ortalama |
| Lag ve rolling | geçen ayın harcaması | Zaman sıralı veri | shift unutmak |
| Kategori kodlama | şehir frekansı, hedef ortalaması | Çok değerli kategoriler | Hedef kodlamada sızıntı |
| Binning, eksiklik bayrağı | kıdem grubu, limit_eksik | Eşik etkisi, anlamlı boşluk | Kova sınırını testten seçmek |
Tarih ve saat özellikleri
Ham zaman damgası (2024-01-01 07:05:28) modele doğrudan girmez. Onu parçalarına ayırırsın: saat, haftanın günü, ay, hafta sonu bayrağı, resmî tatil bayrağı. Saat gibi döngüsel değerlerde bir sorun var: sayı olarak 23 ile 0 arasındaki fark 23, gerçekte bir saat. Çözüm saati bir çemberin üzerine yerleştirmek: sin(2π·saat/24) ve cos(2π·saat/24). Böylece gece yarısının iki yanı birbirine yakın düşer.
import numpy as np
import veri
islem = veri.oku("ilgaz_bank", "bank_txns",
usecols=["account_id", "txn_time", "amount", "txn_type"],
parse_dates=["txn_time"])
df = islem.copy()
df["saat"] = df["txn_time"].dt.hour
df["gun"] = df["txn_time"].dt.dayofweek # 0 = Pazartesi
df["haftasonu"] = (df["gun"] >= 5).astype(int)
df["saat_sin"] = np.sin(2 * np.pi * df["saat"] / 24)
df["saat_cos"] = np.cos(2 * np.pi * df["saat"] / 24)
print(df[["txn_time", "saat", "haftasonu", "saat_sin", "saat_cos"]].head(3).round(3))
print("Hafta sonu payı:", round(df["haftasonu"].mean(), 3))
df["tarih"].dt.date.isin(tatiller) ile bayrak üret. Ağaç modelleri sin/cos'a ihtiyaç duymaz (saat 22'den büyük mü diye bölebilir); doğrusal modeller ve sinir ağları için döngüsel kodlama fark yaratır.Oran, fark, etkileşim ve dönüşümler
Bir müşterinin 30 bin TL kart harcaması, maaşı 20 bin ise başka, 200 bin ise başka anlam taşır. Oran özelliği (harcama / maaş) bu göreliliği modele hazır verir. Fark (bu ay − geçen ay) değişimi, etkileşim (risk puanı × limit) iki etkinin birlikteliğini yakalar. Doğrusal model bunları kendi başına öğrenemez; ağaç modeller öğrenebilir ama çok daha fazla veri ister.
Para tutarları genellikle sağa çarpıktır: çoğu müşteri az, birkaçı çok harcar. np.log1p(x) (yani log(1 + x)) bu kuyruğu toplar, sıfırı da güvenle işler. Hem negatif hem pozitif olabilen tutarlar için işaretli log kullan: np.sign(x) * np.log1p(np.abs(x)). İstatistiksel olarak en uygun üssü veriden seçen dönüşümler de var: Box-Cox (yalnız pozitif değer) ve Yeo-Johnson (negatife de uygun); ikisi de PowerTransformer içinde.
import pandas as pd
import numpy as np
import veri
hesap = veri.oku("ilgaz_bank", "bank_accounts")
islem = veri.oku("ilgaz_bank", "bank_txns",
usecols=["account_id", "txn_time", "amount", "txn_type"],
parse_dates=["txn_time"])
d = islem.merge(hesap[["account_id", "customer_id"]], on="account_id")
ozet = pd.DataFrame({
"kart": d[d["txn_type"] == "kart_harcama"].groupby("customer_id")["amount"].sum().abs(),
"maas": d[d["txn_type"] == "maas"].groupby("customer_id")["amount"].sum(),
}).fillna(0)
ozet["kart_maas_orani"] = ozet["kart"] / (ozet["maas"] + 1) # +1: sıfıra bölme yok
ozet["log_kart"] = np.log1p(ozet["kart"])
print(ozet.round(2).head(3))
print("Çarpıklık ham:", round(ozet["kart"].skew(), 2), " log:", round(ozet["log_kart"].skew(), 2))
Grup istatistikleri ve sızıntısız hesap
"Bu işlem, bu hesabın normal tutarından ne kadar sapıyor?" sorusu dolandırıcılık ve churn modellerinin bel kemiğidir. Hesabın ortalama tutarını groupby("account_id")["amount"].transform("mean") ile hesaplamak kolay ama yanlış: o ortalama, işlemden sonraki işlemleri de içerir. Tahmin anında geleceği bilemezsin. Doğrusu yalnız geçmişi kullanmak: önce shift(1) ile bir satır geri kay, sonra expanding().mean() ile o ana kadarki ortalamayı al.
import pandas as pd
ornek = pd.DataFrame({"hesap": ["A", "A", "A", "B", "B"],
"tutar": [100, 300, 200, 50, 150]})
ornek["sizdiran_ort"] = ornek.groupby("hesap")["tutar"].transform("mean")
ornek["gecmis_ort"] = ornek.groupby("hesap")["tutar"].transform(
lambda s: s.shift(1).expanding().mean())
ornek["son2_ort"] = ornek.groupby("hesap")["tutar"].transform(
lambda s: s.shift(1).rolling(2, min_periods=1).mean())
print(ornek)
A hesabının ilk satırında gecmis_ort boş (NaN): o an henüz geçmiş yok. Bu boşluk gerçektir; "ilk işlem" bilgisini taşır. Modelde ya ayrı bir bayrakla ya da genel ortalamayla doldurarak kullanırsın.
Lag ve rolling: zaman sıralı veride özellik
Aylık tahminde en güçlü özellik çoğu zaman geçen ayın değeridir (lag 1). Hesap × ay tablosunu geniş biçime getirmek (satır hesap, sütun ay) iki işi birden çözer: eksik ayları 0 ile doldurursun (o ay harcama yoksa gerçekten 0'dır) ve "Haziran'ı tahmin ederken yalnız Mayıs ve öncesine bak" kuralını sütun seçerek uygularsın.
kart = islem[islem["txn_type"] == "kart_harcama"]
genis = (kart.assign(ay=kart["txn_time"].dt.month)
.groupby(["account_id", "ay"])["amount"].sum().abs()
.unstack(fill_value=0).round(2))
hedef_ay = 6
tablo = pd.DataFrame({
"lag1": genis[hedef_ay - 1],
"lag2": genis[hedef_ay - 2],
"ort3": genis[[hedef_ay - 3, hedef_ay - 2, hedef_ay - 1]].mean(axis=1).round(2),
"hedef": genis[hedef_ay],
})
print(genis.shape)
print(tablo.head(3))
print("lag1 ile hedef korelasyonu:", round(tablo["lag1"].corr(tablo["hedef"]), 3))
groupby().shift(1) uzun biçimde "bir önceki satırı" verir; arada boş ay varsa bu iki ay öncesi olabilir. Önce takvimi tamamla (geniş tablo ya da reindex), sonra kaydır. Bunun pandas ayrıntısını Zaman serisi analizi dersinde gördün; tahmin modelleri Zaman serisi tahmini: temel çizgiden ETS, ARIMA ve ML'ye dersinde.Yüksek kardinaliteli kategoriler: frekans ve hedef kodlama
Şehir (8 değer) için one-hot sorunsuz. Ama işyeri kimliği, ürün kodu, posta kodu gibi binlerce değerli sütunda one-hot binlerce seyrek sütun üretir, nadir değerler gürültüye dönüşür ve canlıda yeni değerler sürekli gelir. İki pratik alternatif:
- Frekans kodlama: her değeri eğitimdeki görülme oranıyla değiştir. Hedefi kullanmadığı için sızıntı riski yok. Eğitimde görülmeyen değer 0 alır.
- Hedef kodlama (target encoding): her değeri o değerdeki hedef ortalamasıyla değiştir. Çok güçlüdür ama tehlikelidir: satırın kendi hedefi kendi özelliğine girer. Çözüm iki parçalı: (1) katlar dışı (out-of-fold) hesap: her satırın kodu, o satırı görmeyen katlardan gelir; (2) yumuşatma (smoothing): az gözlemli değerleri genel ortalamaya çek:
(toplam + m·genel_ort) / (adet + m).
from sklearn.model_selection import KFold
import numpy as np
musteri = veri.oku("ilgaz_bank", "bank_customers")
y = (musteri["risk_score"] >= 8).astype(int) # örnek hedef: yüksek risk
sehir = musteri["city"]
def oof_hedef_kodla(kat, y, m=10):
sonuc = np.zeros(len(kat))
for egt, dog in KFold(5, shuffle=True, random_state=0).split(kat):
genel = y.iloc[egt].mean()
istat = y.iloc[egt].groupby(kat.iloc[egt]).agg(["sum", "count"])
kod = (istat["sum"] + m * genel) / (istat["count"] + m)
sonuc[dog] = kat.iloc[dog].map(kod).fillna(genel).to_numpy()
return sonuc
kod = oof_hedef_kodla(sehir, y)
print(pd.DataFrame({"sehir": sehir, "kod": kod.round(3)}).drop_duplicates("sehir").head(4))
sklearn.preprocessing.TargetEncoder aynı fikri uygular: fit_transform eğitimde katlar dışı kodlar, transform testte tüm eğitim istatistiğini kullanır. Pipeline içine koyarsan çapraz doğrulamada (cross-validation) da doğru çalışır. Elle yazmayı bilmek, mülakatta "neden sızdırır?" sorusunu cevaplamanı sağlar.Binning ve eksiklik göstergesi
Bazen ilişki eşiklidir: "hesabı 1 yıldan yeni olanlar farklı davranır." pd.cut sabit sınırlarla, pd.qcut eşit dolu kovalarla gruplar. Kova sınırlarını eğitim verisinden ya da iş kuralından seç; testten değil. Boşluk da bilgi taşır: Ilgaz Bank'ta credit_limit yalnız kredi kartında doludur; vadesiz hesapta boş olması "kart değil" demektir. Boşluğu doldurmadan önce limit_eksik bayrağını üret; SimpleImputer(add_indicator=True) bunu Pipeline içinde otomatik yapar.
hesap = veri.oku("ilgaz_bank", "bank_accounts", parse_dates=["opened_date"])
h = hesap.copy()
h["limit_eksik"] = h["credit_limit"].isna().astype(int)
h["limit"] = h["credit_limit"].fillna(0)
yil = (pd.Timestamp("2025-01-01") - h["opened_date"]).dt.days / 365
h["kidem"] = pd.cut(yil, bins=[0, 1, 3, 5, 100], labels=["0-1", "1-3", "3-5", "5+"], right=False)
print(h["kidem"].value_counts().sort_index().to_dict())
print(h.groupby("account_type")["limit_eksik"].mean().round(2).to_dict())
Özellik seçimi
Her yeni sütun bir maliyettir: canlıda hesaplanması, izlenmesi, bozulduğunda hata ayıklanması gerekir. Seçim için dört katman:
- Varyans: hep aynı değeri alan sütun bilgi taşımaz (
VarianceThreshold). - Korelasyon: |r| > 0.95 olan iki sütundan birini at; doğrusal modelde katsayıları kararsızlaştırır.
- Model tabanlı: L1 (Lasso) katsayısı sıfır olanlar, ağaç önemleri (
SelectFromModel). - Permutation importance: doğrulama setinde bir sütunu karıştırınca skor ne kadar düşüyor? Model bağımsızdır ve en güvenilir ölçüdür; ama birbirine çok bağlı iki sütun birbirinin önemini "saklar".
Pipeline'a gömmek: FunctionTransformer ve özel dönüştürücü
Özellik kodun not defterinde kalırsa canlıda başka türlü yazılır ve eğitim-servis çarpıklığı (training-serving skew) doğar. Çözüm, dönüşümü Pipeline'ın parçası yapmak. Durumsuz (veriden bir şey öğrenmeyen) dönüşümler için FunctionTransformer yeter. Eğitimden bir şey öğrenen dönüşüm (ör. kırpma sınırları) için BaseEstimator ve TransformerMixin'den türeyen bir sınıf yazarsın: fit öğrenir ve self döndürür, transform uygular.
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import FunctionTransformer, StandardScaler
from sklearn.linear_model import LogisticRegression
class Kirpici(BaseEstimator, TransformerMixin):
def __init__(self, ust=0.99):
self.ust = ust
def fit(self, X, y=None):
self.sinir_ = np.quantile(np.asarray(X, dtype=float), self.ust, axis=0)
return self
def transform(self, X):
return np.minimum(np.asarray(X, dtype=float), self.sinir_)
model = make_pipeline(Kirpici(0.99), FunctionTransformer(np.log1p), StandardScaler(),
LogisticRegression(max_iter=1000))
X = ozet[["kart", "maas"]]
y = (ozet["kart"] > ozet["kart"].median()).astype(int)
model.fit(X, y)
print("öğrenilen sınırlar:", model[0].sinir_.round(0))
Kesim tarihi (cutoff) disiplini
Müşteri düzeyindeki her özellik bir kesim tarihine göre hesaplanır: "1 Temmuz'a kadar olan işlemler." Hedef ise kesimden sonraki pencereden gelir: "Temmuz–Aralık kart harcaması." Bu derste mini projede tam olarak bunu yapacaksın. Birden fazla kesim tarihi (her ay başı) üretip satırları üst üste koymak veri setini büyütür ve modeli zamana karşı sağlamlaştırır; bu durumda doğrulama da zamana göre yapılmalı.
Otomatik özellik üretimi ne zaman?
featuretools gibi kütüphaneler ilişkili tablolardan yüzlerce toplam özelliği otomatik üretir. Hızlı bir taban çizgi için iyidir ama çoğu özellik gürültüdür ve canlı bakımı pahalıdır. Kıdemli bir veri bilimci önce iş hipotezinden (ör. "maaşı yatmayan müşteri ayrılmaya yakındır") özellik türetir, sonra seçimle sadeleştirir.
Mülakat cümlesi
"Özellik mühendisliğinde ilk sorum şudur: bu bilgi tahmin anında elimde olacak mı? Hedef kodlamayı katlar dışında, grup ortalamalarını yalnız geçmişle, ölçek ve kodlamaları Pipeline içinde fit ederim."