E-ticaret sepet terki: neden ayrılıyorlar, nasıl geri döndürürsün?
Dönüşüm hunisi · 22 dakika okuma · Python + XGBoost + interaktif simülasyon
Türkiye'de e-ticaret pazarının büyüklüğü 2024'te 700 milyar TL'yi aştı. Bu pazarda ortalama sepet terk oranı %70'in üzerinde — yani her 10 kişiden 7'si ödeme adımına geçmeden ayrılıyor.
Sepet terki soyut bir kayıp değil, somut bir optimizasyon problemi. Hangi kullanıcı neden ayrılıyor, kim ne zaman geri dönebilir? Bu soruların cevabı hem teknik hem de iş stratejisi gerektiriyor.
Sepet terkinin anatomisi
Yukarıdaki huniye bakıldığında en büyük kayıp sepetten ödeme sayfasına geçişte yaşanıyor: 3.180 kişi sepete ürün ekliyor ama sadece 1.620'si ödemeye geçiyor. %49 terk oranı. Neden?
- Zorunlu üyelik: "Hesap oluştur" adımı ile karşılaşmak conversion'ı %25 düşürüyor
- Beklenmedik kargo ücreti: Kargo maliyeti ödemede ortaya çıkınca %35 terk
- Güven eksikliği: Tanınmayan markada ödeme bilgisi girme direnci
- Fiyat karşılaştırma: "Şimdi değil, biraz düşüneyim" — özellikle mobilde
Veri seti ve EDA
Olist Brazilian E-Commerce Dataset: 100K+ sipariş, müşteri davranışı ve ödeme verileri. Kaggle'da açık olarak erişilebilir.
import pandas as pd
import numpy as np
# Temel metrikler
df = pd.read_csv("olist_orders_dataset.csv")
print(df["order_status"].value_counts())
# delivered 96478
# shipped 1107
# canceled 625
# ...
# Sepet terk oranı (ödeme başlatılmış ama tamamlanmamış)
terk_orani = df[df["order_status"] == "canceled"].shape[0] / len(df)
print(f"İptal oranı: {terk_orani:.2%}") # ~0.63%
# Olist'te asıl sepet terki: session log'larından türetme
# Gerçek e-ticarette: payment_attempt başlayıp order oluşmayan kayıtlar# Cihaz tipine göre dönüşüm (simüle veri)
cihaz_df = pd.DataFrame({
"cihaz": ["Masaüstü", "Tablet", "Mobil"],
"sepet_olusturma": [0.32, 0.28, 0.24],
"tamamlama": [0.38, 0.28, 0.19],
})
cihaz_df["genel_donusum"] = (
cihaz_df["sepet_olusturma"] * cihaz_df["tamamlama"]
)
print(cihaz_df.round(3))
# cihaz sepet_olusturma tamamlama genel_donusum
# Masaüstü 0.320 0.380 0.122
# Tablet 0.280 0.280 0.078
# Mobil 0.240 0.190 0.046 ← %60 fark!Masaüstü kullanıcısının genel dönüşüm oranı mobil kullanıcının 2.6 katı. Mobil öncelikli pazarda bu fark doğrudan kayıp anlamına geliyor. UX iyileştirmesinin ROI'si en yüksek kanallardan biri mobil checkout akışı.
Feature engineering
# Oturum bazlı özellikler
features = [
"device_type", # masaustu / tablet / mobil
"session_duration_min", # oturumda geçirilen süre
"pages_viewed", # incelenen sayfa sayısı
"cart_value", # sepet tutarı
"n_items", # ürün sayısı
"traffic_source", # organik / ücretli / email / sosyal
"hour_of_day", # günün saati
"is_weekend", # hafta sonu mu?
"is_returning_user", # daha önce alışveriş yapmış mı?
"discount_applied", # indirim kodu uygulandı mı?
"free_shipping", # ücretsiz kargo var mı?
]
# Yeni türetilen özellikler
df["val_per_item"] = df["cart_value"] / df["n_items"]
df["engagement_rate"] = df["pages_viewed"] / df["session_duration_min"]
df["late_night"] = ((df["hour_of_day"] >= 22) |
(df["hour_of_day"] <= 6)).astype(int)
X = df[features + ["val_per_item","engagement_rate","late_night"]]
y = df["completed_purchase"] # 1 = tamamladı, 0 = terk ettiXGBoost modeli
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OrdinalEncoder
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
import xgboost as xgb
from sklearn.metrics import roc_auc_score, average_precision_score
cat_cols = ["device_type", "traffic_source"]
num_cols = [c for c in X.columns if c not in cat_cols]
preprocessor = ColumnTransformer([
("cat", OrdinalEncoder(handle_unknown="use_encoded_value",
unknown_value=-1), cat_cols),
("num", "passthrough", num_cols),
])
model = Pipeline([
("prep", preprocessor),
("clf", xgb.XGBClassifier(
n_estimators=400,
learning_rate=0.04,
max_depth=5,
scale_pos_weight=3, # ~%70 terk = dengesizlik
subsample=0.8,
colsample_bytree=0.8,
random_state=42,
verbosity=0,
))
])
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
model.fit(X_train, y_train)
y_prob = model.predict_proba(X_test)[:, 1]
print(f"ROC-AUC : {roc_auc_score(y_test, y_prob):.4f}") # ~0.871
print(f"PR-AUC : {average_precision_score(y_test, y_prob):.4f}") # ~0.761Interaktif: Kullanıcı profili simülatörü
Model özellikleri değiştirdikçe tahmin nasıl değişiyor? Aşağıda bir kullanıcının profilini oluştur ve satın alma olasılığını gör.
olasılığı
Özellik önemi
import shap
explainer = shap.TreeExplainer(model["clf"])
X_test_prep = model["prep"].transform(X_test)
shap_values = explainer.shap_values(X_test_prep)
shap.summary_plot(shap_values, X_test_prep,
feature_names=cat_cols + num_cols,
max_display=10, show=False)
# En etkili özellikler (ortalama |SHAP|):
# 1. traffic_source — email trafiği en yüksek dönüşüm
# 2. is_returning_user — geri dönen kullanıcılar 3x daha iyi tamamlıyor
# 3. session_duration — uzun oturum = ciddi alıcı
# 4. device_type — masaüstü dominansı
# 5. free_shipping — ücretsiz kargo toggleı ciddi etkili
# 6. cart_value — yüksek tutar = tereddüt
# 7. discount_applied — indirim kodu tamamlamayı artırıyorSegment bazlı aksiyon planı
# Kullanıcıları risk segmentlerine ayır
y_prob_train = model.predict_proba(X_train)[:, 1]
df_train = X_train.copy()
df_train["tamamlama_olasiligi"] = y_prob_train
df_train["segment"] = pd.cut(
df_train["tamamlama_olasiligi"],
bins=[0, 0.30, 0.55, 1.0],
labels=["Yüksek Risk", "Orta Risk", "Düşük Risk"]
)
# Her segment için farklı müdahale
MUDAHALELER = {
"Yüksek Risk": [
"Exit-intent popup: %10 anlık indirim",
"Güven sinyalleri göster: SSL, iade garantisi, yorumlar",
"Ücretsiz kargo eşiği: sepet tutarını görünür yap",
],
"Orta Risk": [
"Terk sonrası email: 1 saat içinde sepet hatırlatma",
"Sosyal kanıt: 'X kişi şu an inceliyor'",
"Stok uyarısı: 'Son 3 ürün kaldı'",
],
"Düşük Risk": [
"Upsell/cross-sell önerisi ekle",
"Sadakat puanı hatırlatması",
"Express kargo seçeneği sun",
],
}
for segment, mudahaleler in MUDAHALELER.items():
n = (df_train["segment"] == segment).sum()
print(f"
{segment}: {n} kullanıcı")
for m in mudahaleler:
print(f" • {m}")Interaktif: Gelir kurtarma hesaplayıcı
Dönüşüm oranını az da olsa artırmak büyük gelir farkı yaratır. Kendi metriklerinle hesapla:
A/B testi tasarımı
from scipy import stats
import numpy as np
# Kontrol: mevcut tamamlama oranı
p_kontrol = 0.22
# Deney: exit-intent popup ile hedef
p_deney = 0.25 # +3 puan iyileştirme hedefi
# Gerekli örneklem büyüklüğü
alpha = 0.05
guc = 0.80
effect = p_deney - p_kontrol
pooled = (p_kontrol + p_deney) / 2
n = (
2 * pooled * (1 - pooled) *
(stats.norm.ppf(1 - alpha/2) + stats.norm.ppf(guc)) ** 2
) / effect ** 2
print(f"Her grup için gereken örneklem: {int(n):,}")
# ~3.200 kullanıcı / grup → ~6-7 gün trafik ile test edilebilir
# Kural: testin çalışma süresi
haftalik_trafik = 5000
print(f"Tahmini test süresi: {2*n/haftalik_trafik:.1f} hafta")Sonuç
Sepet terk optimizasyonu tek bir çözümle bitmez. En etkili yaklaşım:
- Ölç: Hangi adımda en çok kayıp var? Segment bazında fark nedir?
- Tahmin et: Kim yüksek risk taşıyor? Müdahaleyi önceliklendir.
- Dene: Her müdahale için A/B testi — sezgi yeterli değil.
- Kişiselleştir: Herkese aynı popup yerine profile özel müdahale.
Gelir kurtarma hesaplayıcıdan görüldüğü gibi, dönüşümde küçük bir yüzdelik iyileştirme bile yıllık büyük gelir farkı yaratıyor. Bu yüzden sepet terki, veri ekibinin en yüksek ROI'li çalışma alanlarından biri.
Kaynaklar: Olist E-Commerce Dataset (Kaggle) · XGBoost dokümantasyonu · SHAP dokümantasyonu