Etiket yokken veriden anlam çıkarmak
Anadolu Market'in pazarlama ekibi 40 bin müşteriye aynı e-postayı gönderiyor ve dönüş oranı düşük. Elinde "bu müşteri sadık, bu müşteri kaçmak üzere" diye bir etiket yok; yalnızca her müşterinin ne zaman, ne sıklıkla ve ne kadar alışveriş yaptığı var. Makine öğrenmesine giriş ve Regresyon ve sınıflandırma modelleri derslerinde gördüğün denetimli öğrenmede (supervised learning) modele doğru cevabı (y) verirdin. Bu derste doğru cevap yok: algoritma benzer satırları kendisi gruplayacak. Buna denetimsiz öğrenme (unsupervised learning) denir.
Denetimsiz öğrenmenin üç ana işi bu derste:
- Kümeleme (clustering): benzer müşterileri, ürünleri, şubeleri gruplara ayırmak.
- Boyut indirgeme (dimensionality reduction): 20 sütunu bilginin çoğunu koruyarak 2-3 sütuna sıkıştırmak (PCA).
- Anomali tespiti (anomaly detection): Ilgaz Bank'ta "bu işlem diğerlerine hiç benzemiyor" diyebilmek.
k-means nasıl çalışır?
k-means en çok kullanılan kümeleme algoritmasıdır. Sen küme sayısını (k) verirsin; algoritma her kümenin bir merkezini (centroid) bulur ve her noktayı en yakın merkeze bağlar.
- k tane başlangıç merkezi seç (scikit-learn akıllı bir yöntem kullanır: k-means++).
- Her noktayı Öklid uzaklığına göre en yakın merkeze ata.
- Her kümenin merkezini, o kümedeki noktaların ortalamasına taşı.
- Atamalar değişmeyene kadar 2. ve 3. adımı tekrarla.
Alıştırma için gerçek müşteri verisi yerine make_blobs ile sentetik veri üreteceğiz. random_state verdiğin sürece her çalıştırmada aynı veri gelir.
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
X, _ = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42)
print("veri:", X.shape)
km = KMeans(n_clusters=3, n_init=10, random_state=0)
etiketler = km.fit_predict(X)
print("küme boyutları:", sorted(np.bincount(etiketler).tolist()))
print("merkezler:")
print(np.round(km.cluster_centers_, 2))
print("inertia:", round(km.inertia_, 1))
make_blobs ikinci değer olarak "gerçek" küme numaralarını da döndürür; gerçek hayatta bu bilgi olmadığı için _ ile yok sayıyoruz. n_init=10, algoritmayı 10 farklı başlangıçla çalıştırıp en iyisini seçer; kütüphane sürümleri arasında varsayılan değişebildiği için açıkça yazmak iyi alışkanlıktır.
Inertia, her noktanın kendi merkezine uzaklığının karelerinin toplamıdır (within-cluster sum of squares). Küçük olması kümelerin sıkı olduğunu gösterir.
Ölçekleme neden şart?
k-means uzaklık ile çalışır. Bir sütun yaş (20-70), diğeri aylık gelir (20.000-30.000 TL) ise gelirdeki 1.000 TL'lik fark, yaştaki 40 yıllık farkı ezer. Algoritma aslında anlamsız olan gelir gürültüsüne göre küme kurar. Çözüm, Ön işleme, pipeline ve model değerlendirme dersinde gördüğün StandardScaler: her sütunu ortalaması 0, standart sapması 1 olacak şekilde dönüştürür.
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# yaş iki net grup (25 ve 60 civarı), gelir ise yapısız gürültü
yas = np.array([25, 25, 26, 25, 24, 26, 27, 26, 24, 23, 59, 60, 57, 60, 58, 59, 59, 60, 61, 62])
gelir = np.array([20283, 21243, 26706, 26472, 26154, 23837, 29972, 29808, 26855, 26505,
26884, 23889, 21351, 27215, 25254, 23102, 24858, 28895, 29340, 23578])
X = np.column_stack([yas, gelir])
ham = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(X)
olcekli = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(StandardScaler().fit_transform(X))
def gencler_ayni_kumede_mi(etiket):
return len(set(etiket[:10])) == 1 and etiket[0] != etiket[10]
print("ölçeksiz yaşa göre ayırdı mı?", gencler_ayni_kumede_mi(ham))
print("ölçekli yaşa göre ayırdı mı?", gencler_ayni_kumede_mi(olcekli))
make_pipeline(StandardScaler(), KMeans(...)) yazarsan ölçekleme her zaman kümelemeden önce ve aynı ayarlarla yapılır. Yeni müşteriyi kümeye atarken (predict) de aynı ölçekleyici kullanılır.Kaç küme? Dirsek yöntemi ve silhouette
k-means k'yi senden ister; ama doğru k'yi bilmiyorsun. İki yaygın araç var:
- Dirsek yöntemi (elbow method): k'yi 1'den 8'e kadar dene, inertia'yı çiz. Inertia k arttıkça hep azalır (k = nokta sayısı olursa 0 olur), bu yüzden en küçüğü seçilmez. Eğrinin keskin düşüşten yataya döndüğü "dirsek" noktası aranır.
- Silhouette skoru: her nokta için "kendi kümeme ne kadar yakınım, en yakın komşu kümeye ne kadar uzağım" ölçüsü. -1 ile 1 arasıdır; 1'e yakın iyi ayrılmış kümeler demektir. En yüksek ortalama skoru veren k iyi bir adaydır.
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
X, _ = make_blobs(n_samples=400, centers=4, cluster_std=0.9, random_state=7)
print(" k inertia silhouette")
for k in range(2, 8):
km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
s = silhouette_score(X, km.labels_)
print(f"{k:>2} {km.inertia_:>9.1f} {s:.3f}")
Çıktıda inertia'nın k=4'e kadar hızla düşüp sonra yavaşladığını, silhouette'in de k=4'te zirve yaptığını görürsün. Gerçek veride bu kadar net olmaz; o zaman iş tarafının kullanabileceği küme sayısı da (pazarlama 3-6 segmenti yönetebilir, 15'i yönetemez) karara girer.
Başka kümeleme algoritmaları
| Algoritma | k ister mi? | Küme şekli | Gürültü/aykırı | Ne zaman? |
|---|---|---|---|---|
| KMeans | Evet | Yuvarlak, benzer boyutlu | Her noktayı bir kümeye atar | Büyük veri, hızlı segmentasyon |
| AgglomerativeClustering (hiyerarşik) | Evet ya da mesafe eşiği | Bağlantı türüne göre esnek | Atar | Küçük-orta veri, ağaç (dendrogram) yorumu |
| DBSCAN | Hayır (eps, min_samples) | Her şekil, yoğunluk temelli | Gürültüyü -1 etiketiyle ayırır | Coğrafi noktalar, düzensiz şekiller |
| GaussianMixture | Evet | Elips, olasılıklı üyelik | Atar (olasılıkla) | "Bu müşteri %70 A, %30 B" demek gerektiğinde |
Hiyerarşik kümeleme (hierarchical clustering) her noktayı ayrı küme sayarak başlar ve en yakın iki kümeyi adım adım birleştirir; ortaya bir soy ağacı çıkar. DBSCAN ise "etrafında eps yarıçapında en az min_samples komşusu olan nokta yoğun bölgededir" der; yoğun bölgeleri büyütür, hiçbir yoğun bölgeye girmeyen noktaları gürültü sayar.
import numpy as np
from sklearn.cluster import AgglomerativeClustering, DBSCAN
# Anadolu Lojistik: teslimat noktalarının (x, y) km koordinatları
noktalar = np.array([[1, 1], [1.2, 0.8], [0.9, 1.1], [5, 5], [5.1, 4.8], [4.9, 5.2], [12, 1]])
hiy = AgglomerativeClustering(n_clusters=2).fit_predict(noktalar)
print("hiyerarşik:", hiy.tolist())
db = DBSCAN(eps=0.6, min_samples=2).fit_predict(noktalar)
print("DBSCAN :", db.tolist())
print("gürültü sayısı:", int((db == -1).sum()))
Hiyerarşik yöntem 7. noktayı da bir kümeye zorla koydu; DBSCAN ise onu -1 (gürültü) olarak işaretledi. Uzak bir teslimat adresi ayrı planlanmalıdır; bu bilgi DBSCAN'de bedavaya gelir.
PCA: boyut indirgeme
Bir müşteri için 10-20 özellik olabilir ve bunların çoğu birbiriyle ilişkilidir (sipariş sayısı ile toplam harcama birlikte artar). Temel bileşen analizi (Principal Component Analysis, PCA) veriyi, en çok değişkenliği (varyansı) taşıyan yeni eksenlere döndürür. İlk bileşen en fazla varyansı, ikincisi kalanın en fazlasını taşır.
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
X, _ = make_blobs(n_samples=200, n_features=6, centers=3, random_state=1)
X = StandardScaler().fit_transform(X)
pca = PCA().fit(X)
oran = pca.explained_variance_ratio_
print("bileşen başına açıklanan varyans:", np.round(oran, 3))
print("kümülatif:", np.round(np.cumsum(oran), 3))
X2 = PCA(n_components=2).fit_transform(X)
print("6 sütun -> 2 sütun:", X.shape, "->", X2.shape)
explained_variance_ratio_ her bileşenin toplam varyansın ne kadarını taşıdığını söyler. Kümülatif toplam %90'ı geçtiği yerde durmak yaygın bir kuraldır. Kümeleri görmek için ise 2 bileşen alıp matplotlib ile görselleştirme dersindeki gibi plt.scatter(X2[:, 0], X2[:, 1], c=etiketler) ile çizersin. PCA'dan önce ölçekleme yine şarttır; yoksa büyük birimli sütun ilk bileşeni tek başına ele geçirir.
pca.components_ içindeki ağırlıklara bakarak "bu bileşen çoğunlukla sıklık ve harcamadan oluşuyor" gibi yorum yapabilirsin, ama raporda asıl sütun adlarıyla konuş.Müşteri segmentasyonu: RFM + k-means
Bitirme projesi: Anadolu Market müşteri analitiği paketi projesinde RFM'yi çeyreklik skorlarla kurmuştun. Burada aynı üç ölçütü k-means'e veriyoruz: Recency (son alışverişten bu yana gün), Frequency (sipariş sayısı), Monetary (toplam harcama). Harcama çok çarpık dağılır (birkaç müşteri çok harcar); np.log1p ile sıkıştırıp sonra ölçeklemek kümeleri dengeler.
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# her satır bir müşteri: recency (gün), frequency (sipariş), monetary (TL)
rfm = np.array([
[5, 24, 18500], [8, 30, 22000], [3, 18, 15400], # sık ve çok harcayan
[40, 6, 2100], [35, 5, 1800], [50, 7, 2600], # orta
[180, 1, 250], [210, 2, 400], [160, 1, 180], # uzun süredir yok
])
X = rfm.astype(float)
X[:, 2] = np.log1p(X[:, 2])
X = StandardScaler().fit_transform(X)
km = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X)
for k in range(3):
grup = rfm[km.labels_ == k]
r, f, m = grup.mean(axis=0)
print(f"{len(grup)} müşteri | recency {r:.0f} gün | {f:.1f} sipariş | {m:,.0f} TL")
Kümeleri iş diline çevirmek
Model "küme 0, küme 1, küme 2" der; yönetici "kime ne kampanyası yapalım" diye sorar. Köprüyü sen kurarsın:
- Profil çıkar: her küme için özelliklerin ortalamasını (ya da medyanını) ve küme büyüklüğünü hesapla; genel ortalamayla karşılaştır.
- İsim ver: "Şampiyonlar" (düşük recency, yüksek sıklık ve harcama), "Kaybedilmek üzere" (yüksek recency), "Gelişmekte olanlar" gibi.
- Aksiyon bağla: şampiyonlara sadakat programı, kaybedilmek üzere olanlara geri kazanma indirimi.
- Ölç: kampanya sonrası segment bazında dönüş oranını takip et; segmentler zamanla değişir, model düzenli yeniden kurulur.
| Küme profili | İş adı | Önerilen aksiyon |
|---|---|---|
| Recency düşük, frequency ve monetary yüksek | Şampiyonlar | Erken erişim, sadakat puanı; indirimle marjı yakma |
| Recency orta, frequency orta | Potansiyel sadıklar | Çapraz satış önerileri |
| Recency yüksek, geçmişte iyi harcamış | Kaybedilmek üzere | Kişisel geri kazanma kuponu |
| Recency yüksek, frequency 1 | Tek seferlikler | Düşük maliyetli hatırlatma |
Anomali tespiti: z-skoru ve IsolationForest
Ilgaz Bank'ta bir müşterinin işlemleri genelde 200-800 TL arasındayken birden 45.000 TL'lik bir işlem görünürse incelenmelidir. En basit yöntem z-skoru: değerin ortalamadan kaç standart sapma uzakta olduğu, z = (x - ortalama) / std. |z| > 3 genelde aykırı sayılır. Tek değişkende hızlıdır ama aykırı değer ortalamayı ve std'yi kendisi de büyüttüğü için küçük örneklerde eşiği düşürmen gerekebilir.
Birden çok özellik birlikte önemliyse (tutar, saat, işlem sıklığı) IsolationForest kullanılır: rastgele bölmelerle her noktayı yalnız bırakmaya çalışır; aykırı noktalar çok az bölmeyle yalnız kalır. predict normal için 1, anomali için -1 döndürür.
import numpy as np
from sklearn.ensemble import IsolationForest
tutarlar = np.array([320, 450, 280, 610, 390, 505, 45000, 350, 470, 300, 520, 12500])
z = (tutarlar - tutarlar.mean()) / tutarlar.std()
print("z-skoru > 2 olanlar:", tutarlar[np.abs(z) > 2].tolist())
# saat bilgisini de ekleyelim: gece 3'teki işlem de şüpheli
saat = np.array([10, 14, 12, 18, 11, 16, 3, 13, 15, 9, 17, 2])
X = np.column_stack([np.log1p(tutarlar), saat])
iso = IsolationForest(contamination=0.15, random_state=0).fit(X)
tahmin = iso.predict(X)
print("IsolationForest anomaliler:", tutarlar[tahmin == -1].tolist())
Derinleş: gerçek müşterilerle RFM kümeleme
Dersteki RFM ve k-means örnekleri birkaç elle yazılmış işlemle çalışıyordu. Şimdi aynı akışı Anadolu Market'in gerçek verisiyle kuracağız: anadolu_market setindeki customers (150 müşteri), orders (910 sipariş) ve tutarı hesaplamak için order_items. Gerçek veride üç yeni sorunla karşılaşacaksın: hiç teslim edilmiş siparişi olmayan müşteriler, çok çarpık harcama dağılımı ve kümeleri iş ekibinin zaten kullandığı segmentlerle (Bireysel, Kurumsal, Premium) karşılaştırma ihtiyacı.
1. RFM tablosu: müşteri başına bir satır
Yalnızca teslim edilmiş (delivered) siparişleri say; iptal ve iadeler müşterinin değerini şişirmesin. Sipariş tutarı kalemlerden gelir, bu yüzden önce kalemleri sipariş düzeyine toplayıp siparişlere bağlarız. Referans tarihi (bugün) veri bitişinden sonraki gün seçilir: 1 Ocak 2025.
import veri
import pandas as pd
def rfm_tablosu(bugun="2025-01-01"):
o = veri.oku("anadolu_market", "orders", parse_dates=["order_date"])
oi = veri.oku("anadolu_market", "order_items")
oi["ciro"] = oi["quantity"] * oi["unit_price"] * (1 - oi["discount"])
tutar = oi.groupby("order_id")["ciro"].sum()
o = o[o["status"] == "delivered"].join(tutar, on="order_id")
r = o.groupby("customer_id").agg(son=("order_date", "max"), frequency=("order_id", "size"),
monetary=("ciro", "sum"))
r["recency"] = (pd.Timestamp(bugun) - r["son"]).dt.days
return r[["recency", "frequency", "monetary"]]
r = rfm_tablosu()
print(len(r), "müşteri")
print(r.describe().loc[["mean", "50%", "max"]].round(1))
150 müşteriden 135'inin teslim edilmiş siparişi var. Kalan 15 müşteri kümelemeye girmez; onlar ayrı bir "henüz alışveriş yapmamış" grubudur ve pazarlama ekibine ayrıca raporlanır. Harcamada ortalama (7.854 TL) medyanın (5.620 TL) belirgin üstünde: birkaç çok değerli müşteri dağılımı sağa çekiyor.
2. Ölçekleme ve k seçimi
monetary binlerle, frequency birlerle ölçülür. Ölçeklemeden k-means'e verirsen uzaklığı neredeyse yalnızca harcama belirler. StandardScaler sonrası farklı k değerleri için inertia ve silhouette skoruna bakalım:
import veri
import pandas as pd
def rfm_tablosu(bugun="2025-01-01"):
o = veri.oku("anadolu_market", "orders", parse_dates=["order_date"])
oi = veri.oku("anadolu_market", "order_items")
oi["ciro"] = oi["quantity"] * oi["unit_price"] * (1 - oi["discount"])
tutar = oi.groupby("order_id")["ciro"].sum()
o = o[o["status"] == "delivered"].join(tutar, on="order_id")
r = o.groupby("customer_id").agg(son=("order_date", "max"), frequency=("order_id", "size"),
monetary=("ciro", "sum"))
r["recency"] = (pd.Timestamp(bugun) - r["son"]).dt.days
return r[["recency", "frequency", "monetary"]]
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
r = rfm_tablosu()
X = StandardScaler().fit_transform(r)
for k in range(2, 7):
km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
print(k, round(km.inertia_, 1), round(silhouette_score(X, km.labels_), 3))
Silhouette en yüksek değerine k = 3'te ulaşıyor (yaklaşık 0,53) ve inertia eğrisi de k = 3–4 civarında bükülüyor. Matematik k = 3 diyor; son karar yine iş ekibinin kaç farklı kampanyayı yönetebileceğine bağlıdır.
3. Kümeleri iş diline çevirmek ve mevcut segmentlerle kıyaslamak
import veri
import pandas as pd
def rfm_tablosu(bugun="2025-01-01"):
o = veri.oku("anadolu_market", "orders", parse_dates=["order_date"])
oi = veri.oku("anadolu_market", "order_items")
oi["ciro"] = oi["quantity"] * oi["unit_price"] * (1 - oi["discount"])
tutar = oi.groupby("order_id")["ciro"].sum()
o = o[o["status"] == "delivered"].join(tutar, on="order_id")
r = o.groupby("customer_id").agg(son=("order_date", "max"), frequency=("order_id", "size"),
monetary=("ciro", "sum"))
r["recency"] = (pd.Timestamp(bugun) - r["son"]).dt.days
return r[["recency", "frequency", "monetary"]]
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
r = rfm_tablosu()
X = StandardScaler().fit_transform(r)
r["kume"] = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(X)
profil = r.groupby("kume").agg(musteri=("recency", "size"), recency=("recency", "mean"),
frequency=("frequency", "mean"), monetary=("monetary", "mean"))
print(profil.round(0).sort_values("monetary"))
seg = veri.oku("anadolu_market", "customers").set_index("customer_id")["segment"]
print(pd.crosstab(r["kume"], r.join(seg)["segment"]))
| Küme profili | Ortalama | İş adı | Eylem |
|---|---|---|---|
| ~32 müşteri | son alışveriş ~53 gün, ~11 sipariş, ~18.000 TL | Şampiyonlar | Sadakat programı, erken erişim |
| ~84 müşteri | ~77 gün, ~3 sipariş, ~4.500 TL | Potansiyel sadıklar | Çapraz satış, ikinci kategori önerisi |
| ~19 müşteri | ~460 gün, ~4 sipariş, ~5.600 TL | Kayıp riski | Geri kazanım kampanyası |
Çapraz tablo ilginç bir şey söylüyor: CRM'deki "Premium" etiketli müşterilerin bir kısmı davranış olarak "Kayıp riski" kümesinde. Etiket kayıt anındaki sözleşmeyi, küme ise bugünkü davranışı anlatır. Bu farkı bulmak, kümelemenin iş değerinin ta kendisidir.
kesit_bi.RFMSegmentleri tablosu, kural tabanlı RFM'in Power BI tarafındaki karşılığıdır; bitirme projesinde (proje-ileri) ikisini karşılaştıracaksın.Derinleş: market sepeti analizi ve basit öneri
Kümeleme müşterileri gruplar; sepet analizi (market basket analysis) ise ürünleri birlikte satın alınma davranışına göre ilişkilendirir. "Kitap alan kupa da alıyor mu?", "Güneş kremi sepetine şampuan önerelim mi?" gibi soruların cevabı buradadır. Bu da etiketsiz (denetimsiz) bir öğrenme problemidir: hedef sütun yok, yalnızca birlikte görülme sayıları var. Bu bölümde Anadolu Market'in gerçek order_items tablosunu (910 sipariş, 1620 satır) kullanacaksın.
Üç ölçü: destek, güven, lift
N sipariş içinde A ürününü içeren sepet sayısı n(A), hem A hem B içeren sepet sayısı n(A, B) olsun. "A alan B de alır" kuralı (A → B) için:
| Ölçü | Formül | Ne söyler? |
|---|---|---|
| Destek (support) | n(A, B) / N | Bu ikili ne kadar yaygın? Küçükse kural az müşteriyi etkiler. |
| Güven (confidence) | n(A, B) / n(A) | A alanların yüzde kaçı B de alıyor? Yöne bağlıdır: A → B ile B → A farklıdır. |
| Lift | güven(A → B) / destek(B) | Birliktelik tesadüften kaç kat fazla? 1 bağımsızlık, 1'in üstü çekim, altı itme demektir. Simetriktir. |
Güven tek başına yanıltır: B zaten çok popülerse (her sepette su varsa) "A alan su da alır" kuralının güveni yüksek çıkar ama hiçbir şey öğretmez. Lift, B'nin kendi yaygınlığına bölerek bu etkiyi ayıklar.
import veri
import pandas as pd
kalemler = veri.oku("anadolu_market", "order_items")
urunler = veri.oku("anadolu_market", "products").set_index("product_id")
# sipariş × ürün ikili matris: hücre "bu sepette bu ürün var mı?"
sepet = pd.crosstab(kalemler["order_id"], kalemler["product_id"]) > 0
print(sepet.shape) # (sipariş sayısı, ürün sayısı)
def kural(m, a, b):
destek = (m[a] & m[b]).mean() # bool ortalaması = oran
guven = destek / m[a].mean()
lift = guven / m[b].mean()
return round(destek, 4), round(guven, 4), round(lift, 2)
a, b = 10, 13
print(urunler.loc[a, "name"], "->", urunler.loc[b, "name"])
print(kural(sepet, a, b))Kürk Mantolu Madonna alanların yaklaşık %10'u Saatleri Ayarlama Enstitüsü'nü de almış; bu, ikinci kitabın genel yaygınlığının neredeyse iki katı (lift ≈ 1.87). Ama destek yalnızca 7 sepet! Küçük sayılarla kurulan kurallar gürültüye çok açıktır; bu yüzden her kural listesine önce bir en az destek (min support) eşiği uygulanır.
Sık ikilileri saymak: Apriori fikri
40 ürünle 780 olası ikili, 9880 olası üçlü var; binlerce ürünle bu sayı patlar. Apriori algoritmasının gözlemi basittir: bir küme sık değilse, onu içeren hiçbir büyük küme sık olamaz (anti-monotonluk). O hâlde önce sık tekil ürünleri bul, yalnız onlardan ikili aday üret, sık ikililerden üçlü aday üret ve her adımda budayarak ilerle. Küçük veride itertools.combinations ile doğrudan saymak yeterlidir:
from collections import Counter
from itertools import combinations
ciftler = Counter()
for _, g in kalemler.groupby("order_id"):
for x, y in combinations(sorted(set(g["product_id"])), 2):
ciftler[(x, y)] += 1
print("farklı ikili:", len(ciftler))
print("en az 5 sepette görülen:", sum(1 for v in ciftler.values() if v >= 5))
for (x, y), n in ciftler.most_common(3):
print(n, urunler.loc[x, "name"], "+", urunler.loc[y, "name"])mlxtend (apriori, association_rules) ya da FP-Growth kullanılır; mantık aynıdır.Öğe-öğe benzerliğiyle öneri
Sepet matrisinin her sütunu bir ürünün "hangi sepetlerde göründüğü" vektörüdür. İki sütun arasındaki kosinüs benzerliği (cosine similarity), ikili vektörlerde n(A, B) / √(n(A)·n(B)) olur. "Bu ürünü alanlar şunları da aldı" kutusu tam olarak budur: ürünün sütununa en benzer k sütun.
import numpy as np
A = sepet.to_numpy().astype(float)
ortak = A.T @ A # ürün × ürün birlikte görülme sayıları
norm = np.sqrt(np.diag(ortak))
benzerlik = pd.DataFrame(ortak / np.outer(norm, norm),
index=sepet.columns, columns=sepet.columns)
hedef = 15 # İstatistiğe Giriş
enler = benzerlik[hedef].drop(hedef).sort_values(ascending=False).head(3)
for pid, s in enler.items():
print(f"{urunler.loc[pid, 'name']:<24} {s:.3f}")Matris çarpımı A.T @ A köşegeninde her ürünün kaç sepette geçtiğini, köşegen dışında ise ikililerin ortak sayısını verir; tek satırda tüm ikili sayımı yapmış olursun. Lift de bu matristen hesaplanabilir: ortak * N / np.outer(n, n).
Özetle iş akışı şudur: sepet matrisini kur → en az destek eşiğiyle sık öğe kümelerini bul → güven ve lift ile kuralları sırala → kuralları iş diline çevir ("Kitap alanlara kupa seti kampanyası") → etkisini deneyle ölç.
k-means'in varsayımları
k-means kümeleri yuvarlak ve benzer büyüklükte varsayar; hilal ya da halka şeklindeki kümeleri bölemez, çok farklı yoğunluklarda da büyük kümeyi ikiye bölüp küçükleri birleştirebilir. Bu durumda DBSCAN, HDBSCAN (scikit-learn 1.3+) ya da GaussianMixture dene. Kategorik sütunlar (şehir, kanal) için Öklid uzaklığı anlamsızdır; one-hot kodlama ile eklenebilir ama k-prototypes gibi karma uzaklık kullanan yöntemler daha uygundur.
Büyük veri ve kararlılık
Milyonlarca satırda MiniBatchKMeans veriyi parça parça işler. Kümelerin güvenilir olup olmadığını görmek için farklı random_state ya da veri alt örnekleriyle kümelemeyi tekrarla; segmentler her seferinde başka çıkıyorsa veride gerçek bir yapı zayıftır. Yüksek boyutta (50+ sütun) uzaklıklar birbirine benzer hâle gelir ("boyut laneti"); kümelemeden önce PCA ile 10-20 bileşene inmek hem hızlandırır hem gürültüyü azaltır.
Değerlendirme
Silhouette dışında davies_bouldin_score (küçük iyi) ve calinski_harabasz_score (büyük iyi) de var. Ama hiçbir iç ölçüt "bu segmentler işe yarar mı" sorusunu cevaplamaz; onu kampanya deneyi cevaplar.