Ana içeriğe geç
Kesit Python
İleri · Makine öğrenmesi

Özellik mühendisliği: ham veriden modele hazır özelliğe

Tarih ve döngüsel saat, oran ve log dönüşümleri, sızıntısız grup istatistikleri, lag/rolling, frekans ve katlar dışı hedef kodlama, binning, eksiklik bayrağı, özellik seçimi ve özel dönüştürücüyle Pipeline; Ilgaz Bank verisiyle mini proje.

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ç.

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 saatişlem saati, hafta sonu, ayDavranış zamana bağlıysaSaat 23 ile 0'ı uzak sanmak
Oran, fark, etkileşimkart harcaması / maaşİlişki göreliyseSıfıra bölme
Dönüşüm (log, Box-Cox)log(1 + kart toplamı)Çarpık, uzun kuyruklu dağılımNegatif değerin logu
Grup istatistiğihesabın geçmiş ortalama tutarıKişiye göre "normal" değişiyorsaGeleceği içeren ortalama
Lag ve rollinggeçen ayın harcamasıZaman sıralı verishift unutmak
Kategori kodlamaşehir frekansı, hedef ortalamasıÇok değerli kategorilerHedef kodlamada sızıntı
Binning, eksiklik bayrağıkıdem grubu, limit_eksikEşik etkisi, anlamlı boşlukKova 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))
Tatil takvimiResmî tatil, ramazan ve kurban bayramı gibi günler Türkiye'de harcama davranışını ciddi biçimde değiştirir. Tatilleri bir küme olarak tutup 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))
Satır mı, zaman mı?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:

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))
Hazır araçscikit-learn 1.3 ile gelen 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:

  1. Varyans: hep aynı değeri alan sütun bilgi taşımaz (VarianceThreshold).
  2. Korelasyon: |r| > 0.95 olan iki sütundan birini at; doğrusal modelde katsayıları kararsızlaştırır.
  3. Model tabanlı: L1 (Lasso) katsayısı sıfır olanlar, ağaç önemleri (SelectFromModel).
  4. 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))
Gerçek işteBankalarda model ekipleri genellikle "özellik sözlüğü" tutar: her özelliğin adı, tanımı, hangi tablodan, hangi zaman penceresiyle ve hangi kesim tarihine göre hesaplandığı. Denetim (model risk yönetimi) bir skoru sorguladığında bu sözlüğe bakılır. Büyük ekiplerde aynı tanımlar bir özellik deposunda (feature store: Feast, Tecton, Databricks) saklanır ki eğitim ve canlı aynı kodu kullansın.
Uzmanlaş

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."

Alıştırmalara geç (20)