A/B testini doğru yapmak: hipotez, örneklem büyüklüğü ve erken bakma hatası
"Yeni tasarımı yarı kullanıcıya gösterdik, üç günde dönüşüm yüzde 12 arttı, herkese açalım." Bu cümleyi duyduğumda ilk sorduğum şey kaç kullanıcıya gösterildiği ve testin başta ne kadar süreceğine karar verilip verilmediği oluyor. Çoğu zaman cevap "bir bakalım dedik" çıkıyor.
A/B testi kurmak kolay; iki varyant, rastgele bir atama, bir dönüşüm sayacı. Zor olan, çıkan sayının gerçekten bir şey söyleyip söylemediğini bilmek. Bu yazıda portföyümdeki yemek sipariş uygulaması çalışmasında kurduğum deney altyapısı üzerinden adımları anlatıyorum.
Yemek sipariş uygulamasındaki deney: neyi test ediyoruz
Uygulamanın odaklandığı sorun sepet terki. Kullanıcı sepete ürün ekliyor ama siparişi tamamlamıyor. Araştırma bulguları arasında teslimat ücretinin ödemede sürpriz olması, adres girişindeki hatalar ve 9 adımlı bir ödeme süreci vardı.
Uygulamada iki ödeme akışı aynı kod tabanında yaşıyor:
- A (kontrol): eski akışı taklit eden klasik 4 ekran. İl, ilçe, mahalle ve posta kodu zorunlu, ücret yalnızca son adımda görünüyor.
- B (yeni): tek sayfa. Adres, ödeme, bahşiş, not, özet ve toplam aynı ekranda, tek onay düğmesi.
Buradan bir hipotez yazılıyor ve bu, test başlamadan önce yapılıyor:
Ödeme akışını tek sayfaya indirmek, sepete ürün ekleyen oturumlarda sepet terk oranını düşürür.
İyi bir hipotezde üç şey var: ne değişiyor (tek sayfa ödeme), kim etkileniyor (sepete ekleyen oturumlar) ve hangi metrik hangi yöne gidecek (terk oranı düşecek). "Yeni tasarım daha iyi olur" bir hipotez değil; neyin ölçüleceğini söylemiyor.
Atama: kullanıcı her seferinde aynı varyantı görmeli
Rastgele atama derken her açılışta zar atmak kastedilmiyor. Kullanıcı bugün A'yı, yarın B'yi görürse iki deneyimin karışımını ölçersiniz. Bu projede atama, kullanıcı kimliği ile deney adının özetinden çıkıyor:
export function assign(userId: string, experiment: string, split = 0.5): Variant {
return hash32(`${experiment}:${userId}`) / 0xffffffff < split ? "A" : "B";
}
hash32 FNV-1a algoritması. Aynı kullanıcı aynı deneyde hep aynı kovaya düşüyor, farklı deneylerde ise birbirinden bağımsız kovalara dağılıyor. Dağılımın dengeli olduğu bir testle doğrulanıyor: 2.000 kullanıcıda iki varyantın payı yüzde 5'lik bant içinde kalıyor.
Canlı bir deneyde bunu sürekli izlemek de gerekir. Yüzde 50/50 bölmesi planlandığı hâlde veride 54/46 görüyorsanız (buna örneklem oranı uyumsuzluğu deniyor), bir varyantın olayları kayboluyor ya da bir kullanıcı grubu hiç atanmıyor demektir. Böyle bir testin sonucuna güvenmeyin; önce kaçağı bulun.
Metrik seçimi: bir birincil, birkaç koruyucu
Uygulamada kaydedilen olaylar şunlar: restaurant_view, add_to_cart, cart_view, checkout_start, checkout_step, checkout_error, order_complete. Bunlardan hesaplanan huni şunları veriyor: sepet terk oranı, dönüşüm, ortalama ödeme adımı ve form hatası sayısı.
Birincil metrik tek olmalı ve hipotezde yazan metrik olmalı: sepete ekleyen oturumlar içinde siparişi tamamlamayanların oranı. Paydayı "sepete ekleyen oturumlar" seçmek önemli. Tüm oturumları payda alırsanız, ödeme akışını hiç görmeyen, restoranlara bakıp çıkan kullanıcıların gürültüsü etkiyi sulandırır.
Diğer metrikler koruyucu (guardrail) rolünde. Terk oranı düşerken form hatası artıyorsa ya da ortalama sipariş tutarı düşüyorsa kazanç göründüğü kadar temiz değildir. Ama on metriğe bakıp "hangisi anlamlı çıktıysa onu kazanç sayalım" demek yanlış. Yirmi bağımsız metriğe yüzde 5 eşikle bakarsanız, hiçbir fark yokken bile ortalama bir tanesi tesadüfen anlamlı görünür.
Örneklem büyüklüğü: teste başlamadan hesaplayın
Testin ne kadar süreceğini sonuçlara bakarak değil, baştan hesaplayarak belirlersiniz. Bunun için üç şey gerekiyor: mevcut oran, yakalamak istediğiniz en küçük fark ve kabul ettiğiniz hata payları. Yemek sipariş uygulamasındaki fonksiyon standart değerleri kullanıyor: yanlış alarm olasılığı (α) 0,05 ve gerçek bir etkiyi yakalama gücü 0,8.
export function sampleSize(p: number, d: number): number {
const za = 1.96, zb = 0.8416;
const p2 = p + d, pbar = (p + p2) / 2;
const n = Math.pow(
za * Math.sqrt(2 * pbar * (1 - pbar)) +
zb * Math.sqrt(p * (1 - p) + p2 * (1 - p2)), 2) / (d * d);
return Math.ceil(n);
}
Bu formülle birkaç örnek hesapladım. Sayılar varyant başına gereken kullanıcı sayısı:
| Mevcut oran | Yakalanacak mutlak fark | Varyant başına |
|---|---|---|
| %5 | 0,5 puan (%5 → %5,5) | 31.234 |
| %5 | 1 puan | 8.158 |
| %10 | 1 puan | 14.751 |
| %30 | 3 puan | 3.763 |
| %30 | 5 puan | 1.377 |
Tablodan çıkan ders şu: küçük bir farkı düşük bir oranda yakalamak çok büyük trafik istiyor. Günde birkaç yüz sipariş alan bir işletme yüzde 5'lik bir dönüşümde yarım puanlık farkı haftalarca, belki aylarca bekler. Bu durumda ya daha büyük bir değişiklik test edin ya da A/B testi yerine kullanıcı görüşmeleri ve oturum kayıtlarıyla karar verin.
Süreyi hesaplarken bir kural daha eklemek gerekiyor: en az bir tam hafta, mümkünse iki. Yemek siparişinde hafta içi öğle ile cumartesi akşamı bambaşka kitleler. Salı başlayıp perşembe biten bir test, haftanın yalnızca bir kesitini ölçer.
Anlamlılık ve erken bakma hatası
Bu çalışmada iki oranı karşılaştırmak için z-testi kullanıyorum. Fonksiyon z değerini, iki yönlü p-değerini, anlamlılığı (p < 0,05) ve kaldıracı, yani yüzde farkı döndürüyor. Deney panelinde her iki varyantın hunisi ve bu değerler yan yana görünüyor.
p-değerinin ne dediğine dikkat: "Varyantlar arasında hiçbir fark olmasaydı, bu kadar büyük ya da daha büyük bir farkı görme olasılığı." B'nin daha iyi olma olasılığı değil. p = 0,03, B'nin yüzde 97 ihtimalle daha iyi olduğu anlamına gelmez.
Asıl tuzak ise erken bakma, İngilizcesiyle peeking. Test sürerken her gün sonuca bakıp p 0,05'in altına ilk indiği gün testi durdurursanız, yanlış alarm olasılığınız artık yüzde 5 değildir. Bunu görmek için küçük bir simülasyon yaptım: iki varyant da birebir aynı (yüzde 30 dönüşüm, yani gerçekte hiçbir fark yok), her varyanta günde 500 kullanıcı, test 20 gün sürüyor. 20.000 tekrar sonunda:
- Yalnızca 20. günün sonunda bakınca testlerin yaklaşık yüzde 5'i yanlışlıkla anlamlı çıktı. Beklenen buydu.
- Her gün bakıp ilk anlamlı günde durunca bu oran yaklaşık yüzde 25'e çıktı.
Yani her gün bakan biri, hiçbir farkın olmadığı dört testten birinde "kazandık" diyor. Sonuçlar gün içinde bir yukarı bir aşağı dalgalanır ve yeterince sık bakarsanız bir noktada eşiğin altına denk gelirsiniz.
Çözüm yolları:
- Örneklemi baştan hesaplayın ve o sayıya ulaşana kadar karar vermeyin. Paneli izleyebilirsiniz ama durdurma kararını yalnızca planlanan örneklemde verin.
- Ara bakış gerçekten gerekiyorsa, bunun için tasarlanmış sıralı test yöntemlerini kullanın. Bunlar her bakış için eşiği sıkılaştırır.
- Testi yalnızca bir şey bozulduğunda erken durdurun; örneğin B'de ödeme hataları patladıysa.
Sentetik veriyle gerçeği karıştırmayın
Yemek sipariş uygulamasının deney panelinde, panelin nasıl çalıştığını göstermek için tohumlu bir üreteçle oluşturulmuş 1.200 sentetik oturum var. Bu oturumlar arayüzde "sentetik" olarak ayrı sayılıyor ve gerçek olaylarla asla birleştirilmiyor. Çalışma konsept olduğu için panelde gördüğünüz farklar gerçek kullanıcı davranışı değil, yazılmış bir davranış modelinin çıktısı.
Bunu özellikle vurguluyorum çünkü aynı hata gerçek projelerde de görülüyor: test ortamındaki olaylar, iç ekibin tıklamaları ve bot trafiği canlı veriye karışıyor. Deney verisini kimin ürettiğini etiketleyin ve analizde süzün.
Test başlatmadan önce kontrol listesi
- Hipotez yazılı mı: ne değişiyor, kim etkileniyor, hangi metrik hangi yöne gidecek?
- Birincil metrik tek mi, paydası doğru kitle mi?
- Koruyucu metrikler belirlendi mi?
- Atama kalıcı mı; aynı kullanıcı hep aynı varyantı görüyor mu?
- Örneklem büyüklüğü hesaplandı mı, bu trafikle kaç gün sürüyor?
- Süre en az bir tam haftayı kapsıyor mu?
- Varyant oranları planlananla uyuşuyor mu?
- Test, bot ve iç ekip trafiği analizden ayrılıyor mu?
- Durdurma kuralı baştan yazıldı mı?