Bir satır = bir örnek
Makine öğrenmesi modelleri genellikle varlık başına bir satır ister: her müşteri için özellikler (features) ve bir hedef (label). Bu tabloyu SQL ile üretmek, veri bilimcinin en sık yaptığı işlerden biridir.
Zaman sızıntısına dikkat
Özellikler kesim tarihinden önceki veriden, etiket sonraki veriden hesaplanmalı. Aksi halde model geleceği görür (data leakage).
WITH params AS (SELECT '2024-07-01' AS kesim),
gecmis AS (
SELECT o.* FROM orders o, params WHERE o.order_date < params.kesim AND o.status = 'delivered'
),
ozellik AS (
SELECT g.customer_id,
COUNT(DISTINCT g.order_id) AS siparis_sayisi,
ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount)), 2) AS toplam_harcama,
ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount)) / COUNT(DISTINCT g.order_id), 2) AS ort_sepet,
CAST(julianday((SELECT kesim FROM params)) - julianday(MAX(g.order_date)) AS INTEGER) AS son_siparisten_gun,
COUNT(DISTINCT p.category) AS kategori_cesidi,
ROUND(AVG(oi.discount > 0), 2) AS indirimli_oran
FROM gecmis g
JOIN order_items oi ON oi.order_id = g.order_id
JOIN products p ON p.product_id = oi.product_id
GROUP BY g.customer_id
),
etiket AS (
SELECT f.customer_id,
CASE WHEN EXISTS (SELECT 1 FROM orders o, params
WHERE o.customer_id = f.customer_id AND o.order_date >= params.kesim
AND o.order_date < date(params.kesim, '+6 months')) THEN 0 ELSE 1 END AS churn
FROM ozellik f
)
SELECT f.*, c.segment, 2024 - c.birth_year AS yas, e.churn
FROM ozellik f
JOIN customers c ON c.customer_id = f.customer_id
JOIN etiket e ON e.customer_id = f.customer_id
LIMIT 15;
CASE WHEN segment = 'Premium' THEN 1 ELSE 0 END AS seg_premium.Derinleş: veri sızıntısını yakalamak
Veri sızıntısı (data leakage), modelin eğitim sırasında tahmin anında elinde olmayacak bir bilgiyi görmesidir. Sızıntılı bir model test skorunda parlar, canlıda çöker. En sık türleri:
- Gelecek bilgisi: Özellik, kesim tarihinden sonraki veriden hesaplanır (tüm zamanların son sipariş tarihi gibi).
- Hedef sızıntısı: Özellik, hedefin kendisinden türer (churn etiketi için “hesap kapatma tarihi dolu mu”).
- Küme sızıntısı: Ölçekleme ortalaması veya kategori frekansı test verisi de katılarak hesaplanır.
Anadolu Market'te kesim tarihi 2024-07-01 ve hedef “kesimden sonra (veri 2024 sonunda bitiyor) sipariş verdi mi?” olsun. “Son sipariş tarihi” özelliğini yanlışlıkla tüm siparişlerden hesaplarsak:
WITH m AS (
SELECT customer_id,
MAX(order_date) AS son_tum,
MAX(CASE WHEN order_date < '2024-07-01' THEN order_date END) AS son_gecmis,
MAX(CASE WHEN order_date >= '2024-07-01' THEN 1 ELSE 0 END) AS geri_geldi
FROM orders
GROUP BY customer_id
)
SELECT CASE WHEN son_tum >= '2024-07-01' THEN 'son sipariş kesimden sonra'
ELSE 'son sipariş kesimden önce' END AS sizan_ozellik,
COUNT(*) AS musteri, SUM(geri_geldi) AS geri_gelen
FROM m
WHERE son_gecmis IS NOT NULL
GROUP BY CASE WHEN son_tum >= '2024-07-01' THEN 'son sipariş kesimden sonra'
ELSE 'son sipariş kesimden önce' END;
Sonuç kusursuz: 91 müşterinin 91'i geri gelmiş, 23 müşterinin hiçbiri gelmemiş. Bu kadar temiz bir ayrım neredeyse her zaman sızıntı alarmıdır; özellik etiketi “önceden biliyor”. Doğru (zaman noktasına uygun) özellik, yalnızca kesim öncesi son siparişten geçen gün sayısıdır; onunla geri gelme oranı 0–90 gün grubunda %93,8, 91–180 günde %75, 180 günden eskide %0 çıkar. Gerçekçi ve hâlâ çok bilgilendirici.
WHERE tarih < kesim, etiket CTE'sinde WHERE tarih >= kesim bulunsun ve kesim tek bir parametre CTE'sinden gelsin.Özellik tablosu kontrol listesi
| Kontrol | SQL ile nasıl? |
|---|---|
| Bir satır = bir varlık | COUNT(*) = COUNT(DISTINCT customer_id) |
| Eksik değer politikası | COALESCE(siparis_sayisi, 0); ama “hiç veri yok” bilgisi için ayrıca bayrak sütunu |
| Kategorik kodlama | CASE WHEN segment = 'Premium' THEN 1 ELSE 0 END |
| Oran özellikleri | Payda sıfırsa NULLIF; tam sayı bölmesinden kaçın |
| Sızıntı | Etiketle mükemmel ayrışan özellik var mı? Grupla ve etiket oranına bak |