Ana içeriğe geç
Kesit Python
İleri · Veri analizi ve bilimi

İş metrikleri ve analitik kalıplar: KPI, huni, kohort, LTV

KPI ağacı ve metrik sözleşmesi, web_events ile huni, çeyreklik kohort tutundurma ve churn penceresi, basit ve kohort bazlı LTV ile LTV/CAC, MoM/YoY/YTD ve aynı dönem kıyası, fiyat-hacim-karma köprüsü, Pareto (ABC) ve metrik tuzakları: oranların ortalaması, Simpson paradoksu, payda kayması.

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

Metrik, analistin ürünüdür

Yönetim "ciro neden düştü?", pazarlama "kampanya işe yaradı mı?", finans "bir müşteri bize ne kadar kazandırıyor?" diye sorar. Bu soruların hepsi bir metrik (metric) ve onun doğru hesaplanmış, doğru karşılaştırılmış bir değeri ister. pandas ile gruplamayı, istatistik dersinde A/B testini gördün. Bu ders analitik ekiplerin her gün kullandığı kalıpları toplar: KPI ağacı, huni, kohort ve tutundurma, LTV, dönem karşılaştırmaları, köprü analizi, Pareto ve en sık metrik tuzakları.

Veri: Anadolu Market orders, order_items, products ve web_events (Ekim–Kasım 2024 site olayları); ayrıca Kesit Power BI'daki kesit_bi yıldız şemasının Satislar tablosu. Ciro tanımı bu derste de aynı: net = quantity × unit_price × (1 − discount), yalnız delivered ve shipped siparişler.

1. KPI ağacı ve metrik sözleşmesi

Ciro tek başına "neden" söylemez. Onu çarpanlarına ayırınca hangi kolun hareket ettiği görünür:

ciro = aktif müşteri × müşteri başına sipariş (frekans) × ortalama sepet (AOV)
AOV  = sipariş başına ürün adedi × ortalama birim fiyat
web cirosu = ziyaretçi × dönüşüm oranı × AOV
import veri
import pandas as pd

o = veri.oku("anadolu_market", "orders", parse_dates=["order_date"])
i = veri.oku("anadolu_market", "order_items")
i["net"] = i["quantity"] * i["unit_price"] * (1 - i["discount"])
x = o[o["status"].isin(["delivered", "shipped"])].merge(i, on="order_id", validate="one_to_many")

yil = x.groupby(x["order_date"].dt.year).agg(
    ciro=("net", "sum"), siparis=("order_id", "nunique"), musteri=("customer_id", "nunique"))
yil["frekans"] = yil["siparis"] / yil["musteri"]
yil["aov"] = yil["ciro"] / yil["siparis"]
print(yil.round(2))

# 2023 → 2024 büyümesinin kaynakları (çarpanların oranları)
b = yil.loc[2024] / yil.loc[2023]
print("ciro x", round(b["ciro"], 3), "= müşteri x", round(b["musteri"], 3),
      "· frekans x", round(b["frekans"], 3), "· AOV x", round(b["aov"], 3))

2024'te ciro 1,88 katına çıkmış; bunun büyük kısmı aktif müşteri sayısındaki artıştan (×1,62), daha azı frekans (×1,08) ve AOV'den (×1,07) geliyor. Yönetime "büyüme yeni müşteriden geldi" demek artık bir sayıya dayanıyor.

Her metrik için yazılı bir sözleşme (metric definition) tut: ad, formül, pay ve payda, dahil/hariç durumlar (iptal, iade, test kullanıcıları), zaman damgası (sipariş mi teslim tarihi mi?), sahibi. "Dönüşüm oranı" iki ekipte iki farklı şey demeye başladığı an, toplantılar sayı tartışmasına döner.

2. Huni analizi

Huni (funnel), kullanıcıların adım adım nerede döküldüğünü gösterir. Her adımda o adımı yapan tekil kullanıcı sayılır; olay sayısı değil. Bu veride fark büyüktür: kullanıcılar geri gelir ve adımları tekrar eder, 40 olay da iki kez gönderilmiştir; 2.819 satır 600 kullanıcıdan gelir.

import veri
import pandas as pd

w = veri.oku("anadolu_market", "web_events")
adimlar = ["visit", "product_view", "add_to_cart", "checkout", "purchase"]

huni = w.groupby("event_type")["user_id"].nunique().reindex(adimlar).to_frame("kullanici")
huni["adim_donusum"] = (huni["kullanici"] / huni["kullanici"].shift(1)).round(3)
huni["toplam_donusum"] = (huni["kullanici"] / huni["kullanici"].iloc[0]).round(3)
print(huni)

cihaz = w.pivot_table(index="event_type", columns="device", values="user_id", aggfunc="nunique").reindex(adimlar)
print((cihaz.loc["purchase"] / cihaz.loc["visit"]).round(3))

En büyük kayıp ürün görüntülemeden sepete geçişte (%40); mobilde ziyaretten satın almaya dönüşüm masaüstünden düşük. Huniyi bir kırılımla (cihaz, kaynak, yeni/eski kullanıcı) okumak "nerede" sorusunu "kimde" sorusuna çevirir.

Katı huni mi, gevşek huni mi?Gevşek huni her adımı bağımsız sayar. Katı (strict) huni ise bir adımı yalnız önceki adımları da yapmış kullanıcılar için sayar; sırayı ve zaman penceresini (ör. 7 gün içinde) de şart koşabilir. Bu veride tekil kullanıcıda ikisi aynı sonucu veriyor; sıra şartı eklersen farklılaşır, çünkü 24 kullanıcıda iki adımın zamanı yer değiştirmiştir. Gerçek veride sepete doğrudan bağlantıyla gelenler yüzünden de genellikle farklıdır. Hangisini kullandığını sözleşmeye yaz.

3. Kohort, tutundurma ve churn

Kohort (cohort), aynı dönemde başlayan müşteri grubudur. Tutundurma (retention) tablosu her kohortun sonraki dönemlerde ne kadarının geri geldiğini gösterir. SQL sürümünü Python'dan SQL ile analiz dersinde yazdın; burada pandas ile çeyreklik kohort kuruyoruz.

import veri
import pandas as pd

o = veri.oku("anadolu_market", "orders", parse_dates=["order_date"])
o["donem"] = o["order_date"].dt.to_period("Q")
o["kohort"] = o.groupby("customer_id")["donem"].transform("min")
o["fark"] = (o["donem"] - o["kohort"]).apply(lambda d: d.n)

sayim = o.groupby(["kohort", "fark"])["customer_id"].nunique().unstack()   # gelecekteki hücreler NaN kalır
oran = sayim.div(sayim[0], axis=0).round(2)
print(oran.iloc[-6:, :5])
Gelecek hücreleri 0 ile doldurmaunstack(fill_value=0) yazarsan 2024Q4 kohortunun 1. çeyreği "%0 tutundurma" görünür; oysa o çeyrek henüz yaşanmadı. Ortalama tutundurma eğrisini hesaplarken bu sıfırlar eğriyi aşağı çeker. Gözlemlenmemiş dönemi NaN bırak; ortalamayı yalnız gözlemlenen hücrelerden al.

Churn (müşteri kaybı) sözleşme gerektiren bir metriktir. Abonelik işinde iptal tarihi bellidir; e-ticarette "kaybedildi" demek için bir hareketsizlik penceresi seçersin: "son siparişin üzerinden 180 gün geçti". Pencere kısalırsa churn yükselir; pencereyi satın alma döngüsüne göre seç (bu veride iki sipariş arası ortalama süre 86 gün).

4. LTV ve CAC

Müşteri yaşam boyu değeri (customer lifetime value, LTV), bir müşterinin ilişki boyunca bıraktığı katkı payıdır (ciro değil, marj). İki yaygın hesap:

YöntemFormülNe zaman
BasitAOV × yıllık frekans × marj oranı × beklenen ömür (yıl)Hızlı tahmin, yönetim sunumu
Kohort bazlıKohort başına kümülatif marj / kohort büyüklüğü, dönem dönemGerçekleşen değeri görmek, kanalları karşılaştırmak
OlasılıksalBG/NBD + Gamma-Gamma (lifetimes kütüphanesi)Bireysel müşteri tahmini

Müşteri edinme maliyeti (customer acquisition cost, CAC) = pazarlama harcaması / yeni müşteri. LTV/CAC oranı yaygın bir sağlık göstergesidir; 3 civarı sağlıklı kabul edilir, 1'in altı her müşteride para kaybetmek demektir. Oranı yorumlarken geri ödeme süresine (CAC'nin kaç ayda geri kazanıldığı) de bak.

5. Dönem karşılaştırmaları: MoM, YoY, YTD

MoM (month over month) kısa vadeli ivmeyi, YoY (year over year) mevsimsellikten arındırılmış büyümeyi, YTD (year to date) yılbaşından bugüne birikimi gösterir. Kural: karşılaştırdığın iki dönem aynı uzunlukta ve aynı takvim yapısında olmalı.

import veri
import pandas as pd

o = veri.oku("anadolu_market", "orders", parse_dates=["order_date"])
i = veri.oku("anadolu_market", "order_items")
i["net"] = i["quantity"] * i["unit_price"] * (1 - i["discount"])
x = o[o["status"].isin(["delivered", "shipped"])].merge(i, on="order_id")

def donem(bas, son):
    m = (x["order_date"] >= bas) & (x["order_date"] <= son)
    return round(x.loc[m, "net"].sum(), 2)

bu = donem("2024-01-01", "2024-06-15")
gecen = donem("2023-01-01", "2023-06-15")          # aynı gün: 15 Haziran
print("YTD 2024:", bu, "| aynı dönem 2023:", gecen, "| YoY:", round(bu / gecen - 1, 3))
print("YANLIŞ kıyas (tüm 2023):", donem("2023-01-01", "2023-12-31"))

Takvim etkisi: Bir ayda 5 cumartesi, diğerinde 4 olabilir; bayramlar her yıl başka aya düşer. Günlük ciroda haftanın günü etkisi güçlüyse "aynı gün" karşılaştırması (bu yılın 2. pazartesisi ile geçen yılın 2. pazartesisi) ya da gün başına ortalama kullanılır.

6. Köprü analizi: fiyat, hacim ve karma etkisi

"Ciro 329 bin TL arttı; ne kadarı daha çok satmaktan, ne kadarı fiyattan, ne kadarı ürün karmasından?" Köprü (bridge, price-volume-mix) analizi değişimi parçalara ayırır. Kategori düzeyinde, A adet ve P ortalama birim fiyat olmak üzere:

EtkiFormülAnlamı
Hacim(ΣA₁ − ΣA₀) × P̄₀Karma ve fiyat sabitken toplam adedin artışı
KarmaΣ (payᵢ₁ − payᵢ₀) × ΣA₁ × Pᵢ₀Satışın pahalı/ucuz kategorilere kayması
FiyatΣ (Pᵢ₁ − Pᵢ₀) × Aᵢ₁Aynı kategoride birim fiyat değişimi

Üçünün toplamı ciro farkına birebir eşittir; köprü grafiği (waterfall) bu üç çubuğu başlangıç ve bitiş cirosu arasına dizer. Uygulamasını alıştırmalarda yapacaksın.

7. Pareto (ABC) analizi

Ürünleri ciroya göre sırala, kümülatif payı hesapla: ilk %80'i oluşturanlar A, sonraki %15 B, kalan C. Stok, kampanya ve raf kararları bu sınıflara göre verilir. "80/20" bir yasa değil, kontrol edilmesi gereken bir hipotezdir: Anadolu Market'te ürünlerin ilk %20'si cironun yalnız yaklaşık %37'sini yapıyor; ciro geniş bir ürün yelpazesine yayılmış. Müşteri tarafında aynı fikrin çok boyutlu hâli RFM segmentasyonudur (bitirme projesi dersinde).

8. Metrik tuzakları

Oranların ortalaması ≠ ortalama oran

import veri
import pandas as pd

w = veri.oku("anadolu_market", "web_events", parse_dates=["event_time"])
w["gun"] = w["event_time"].dt.date
gunluk = w.pivot_table(index="gun", columns="event_type", values="user_id", aggfunc="nunique", fill_value=0)
gunluk = gunluk[gunluk["visit"] > 0]

print("günlük oranların ortalaması:", round((gunluk["purchase"] / gunluk["visit"]).mean(), 4))
print("toplam / toplam (doğru):   ", round(gunluk["purchase"].sum() / gunluk["visit"].sum(), 4))

Günlük oranların ortalaması her güne eşit ağırlık verir: 3 ziyaretçili bir gün ile 30 ziyaretçili bir gün aynı etkiyi yapar. Oran metriklerini daima pay toplamı / payda toplamı olarak hesapla.

Simpson paradoksu ve payda kayması

Bir karşılaştırma her alt grupta bir yönü gösterirken toplamda ters yönü gösterebilir. Aşağıdaki küçük örnekte A kampanyası hem mobilde hem masaüstünde B'den iyidir, ama trafiğinin çoğu düşük dönüşümlü mobilden geldiği için toplamda kötü görünür:

import pandas as pd

df = pd.DataFrame({
    "kampanya": ["A", "A", "B", "B"],
    "cihaz": ["mobil", "masaüstü", "mobil", "masaüstü"],
    "ziyaret": [1000, 100, 100, 1000],
    "satis": [100, 30, 8, 250],
})
df["oran"] = df["satis"] / df["ziyaret"]
print(df.pivot(index="cihaz", columns="kampanya", values="oran"))
t = df.groupby("kampanya")[["satis", "ziyaret"]].sum()
print((t["satis"] / t["ziyaret"]).round(3))

Gerçek veride paradoks nadirdir ama karma (mix) etkisi her yerdedir: toplam AOV düştüğünde önce "her kanalda mı düştü, yoksa ucuz kanalın payı mı arttı?" diye sor. Payda kayması da benzer bir tuzaktır: dönüşüm oranı yükseldi, çünkü paydadaki "ziyaret" tanımı bot trafiği filtrelenerek değişti. Metrik sözleşmesi tam da bu yüzden vardır.

import veri

s = veri.oku("kesit_bi", "Satislar")
s = s[s["Durum"] != "İptal"]
s["marj"] = s["Tutar"] - s["Maliyet"]
k = s.groupby("Kanal")[["marj", "Tutar"]].sum()
print("ağırlıklı marj oranı (doğru):", (k["marj"] / k["Tutar"]).round(4).to_dict())
print("satır oranlarının ortalaması:", (s["marj"] / s["Tutar"]).groupby(s["Kanal"]).mean().round(4).to_dict())
Gerçek işteHaftalık iş incelemesinde (weekly business review) en sık duyulan cümle "bu sayı geçen haftaki rapordakiyle tutmuyor"dur. Olgun ekipler bunu bir semantik katmanla (dbt metrics, Power BI ölçüleri, LookML) çözer: metrik bir kez tanımlanır, her rapor oradan okur. Analistin işi de sayıyı üretmekle bitmez; "ciro %10 düştü" dediğinde hemen ardından KPI ağacının hangi kolunun, hangi segmentte, karma mı oran mı yüzünden düştüğünü söyleyebilmektir.
Uzmanlaş

Kuzey yıldızı metriği ve koruyucu metrikler

Ürün ekipleri tek bir kuzey yıldızı metriğine (north star metric) odaklanır: ör. "haftalık en az bir sipariş veren aktif müşteri". Onu yükseltirken bozulmaması gerekenler koruyucu metriklerdir (guardrail): iade oranı, ortalama teslim süresi, müşteri şikâyeti. A/B testlerinde de karar yalnız hedef metrikle değil, koruyucularla birlikte verilir.

Göreli ve mutlak değişim

%2'den %3'e çıkan dönüşüm "yüzde 50 arttı" (göreli) ya da "1 puan arttı" (mutlak, yüzde puan) diye anlatılabilir. Sunumda ikisini birlikte ver; küçük tabanlı göreli değişimler abartılı görünür.

Olasılıksal LTV

pip install lifetimes
from lifetimes import BetaGeoFitter, GammaGammaFitter
from lifetimes.utils import summary_data_from_transaction_data
ozet = summary_data_from_transaction_data(df, "customer_id", "order_date", "net", observation_period_end="2024-12-31")
bgf = BetaGeoFitter(penalizer_coef=0.01).fit(ozet["frequency"], ozet["recency"], ozet["T"])

Kendi bilgisayarında dene: bu model her müşterinin gelecekteki sipariş sayısını ve harcamasını tahmin eder; tarayıcıda çalışmaz.

Alıştırmalara geç (20)