Pengujian A/B

Merancang pengujian A/B: penentuan ukuran sampel, durasi, dan jebakan yang sering menyesatkan.

Daftar isi

Pengujian A/B (A/B testing) mengukur efek satu perubahan dengan membagi pengguna secara acak ke dalam dua versi dan membandingkan satu metrik utama. Contoh yang digunakan di sini adalah dua versi halaman informasi perpustakaan, yaitu tata letak lama dan tata letak baru yang menonjolkan tautan jadwal layanan, dengan proporsi pengunjung yang membuka tautan itu sebagai metrik utama. Topik ini membahas desain eksperimennya, perhitungan ukuran sampel, penentuan durasi, jebakan yang sering menyesatkan, dan analisis akhirnya dengan scipy.

Desain eksperimen dan randomisasi#

Randomisasi (randomization) adalah inti pengujian A/B. Pengunjung dibagi ke versi A atau versi B dengan peluang yang sama dan tanpa pola, sehingga faktor lain seperti jam kunjung, perangkat, dan kebiasaan membaca menyebar merata ke kedua versi. Perbedaan hasil yang teramati kemudian berasal dari dua sumber saja, yaitu efek versinya dan kebetulan, dan kebetulan itulah yang ditakar oleh p-value.

Satu metrik utama ditetapkan sebelum eksperimen berjalan. Pada contoh ini metriknya adalah proporsi pengunjung yang membuka tautan jadwal layanan, dan data historis menempatkan proporsi dasarnya di 0,30. Hipotesis kerjanya menyatakan tata letak baru menaikkan proporsi itu, sedangkan hipotesis nihilnya menyatakan kedua versi sama, yaitu H0:pA=pBH_0: p_A = p_B. Metrik lain yang ikut terukur diperlakukan sebagai pendamping dan tidak digunakan untuk menyatakan kemenangan.

Tanpa randomisasi, perbandingan sebelum dan sesudah perubahan menjadi pilihan yang tersisa, dan pilihan itu bermasalah. Kunjungan pada dua periode berbeda membawa komposisi pengguna yang berbeda, misalnya periode ujian akhir yang menaikkan pencarian jadwal, sehingga efek perubahan bercampur dengan efek musim. Pembagian acak yang berjalan serentak membuat kedua varian menanggung campuran hari dan pengguna yang sama, sehingga perbedaan yang tersisa menyisakan efek versi dan kebetulan yang ditakar.

Ukuran sampel dan durasi#

Ukuran sampel ditentukan oleh tiga keputusan, yaitu tingkat signifikansi α\alpha, daya uji (power), dan selisih terkecil yang masih layak dideteksi. Daya uji menyatakan peluang menolak H0H_0 apabila efek yang dicari benar-benar ada, dan nilai yang lazim adalah 0,80. Selisih yang ditargetkan pada contoh ini adalah kenaikan dari 0,30 menjadi 0,36, karena kenaikan sebesar itu dinilai berarti bagi layanan, sedangkan kenaikan yang lebih kecil tidak mengubah apa pun.

Ukuran sampel untuk perbandingan dua proporsi dihitung dari ambang α\alpha, daya uji, dan lebar efek yang dicari:

n=(zα/22pˉ(1−pˉ)+zβpˉ1(1−pˉ1)+pˉ2(1−pˉ2))2(pˉ1−pˉ2)2 n = \frac{\left(z_{\alpha/2}\sqrt{2\bar{p}(1-\bar{p})} + z_{\beta}\sqrt{\bar{p}_1(1-\bar{p}_1) + \bar{p}_2(1-\bar{p}_2)}\right)^2}{(\bar{p}_1 - \bar{p}_2)^2}

dengan pˉ\bar{p} sebagai rata-rata kedua proporsi, zα/2z_{\alpha/2} sebagai nilai kritis normal untuk α\alpha dua arah, dan zβz_{\beta} sebagai nilai kritis untuk daya uji. Kode berikut menghitungnya dengan scipy untuk rancangan pada contoh ini.

python ukuran-sampel.py
import numpy as np
from scipy import stats

# perencanaan: proporsi dasar 0.30, selisih yang dideteksi 0.36, alpha 0.05, power 0.80
p1 = 0.30
p2 = 0.36
alpha = 0.05
power = 0.80

z_alpha = stats.norm.ppf(1 - alpha / 2)
z_beta = stats.norm.ppf(power)

p_rata = (p1 + p2) / 2
pembilang = (z_alpha * np.sqrt(2 * p_rata * (1 - p_rata))
             + z_beta * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2
n = np.ceil(pembilang / (p2 - p1) ** 2)

print(f"z untuk alpha 0.05 (dua arah) : {z_alpha:.3f}")
print(f"z untuk power 0.80            : {z_beta:.3f}")
print(f"ukuran sampel per varian      : {n:.0f} pengunjung")
text Keluaran
z untuk alpha 0.05 (dua arah) : 1.960
z untuk power 0.80            : 0.842
ukuran sampel per varian      : 963 pengunjung

Nilai zα/2z_{\alpha/2} sebesar 1,960 dan zβz_{\beta} sebesar 0,842 menghasilkan ukuran sampel 963 pengunjung per varian. Eksperimen yang dihentikan sebelum ukuran ini tercapai memiliki daya yang lebih rendah daripada yang direncanakan. Durasi mengikuti pembagian ukuran sampel dengan kunjungan harian, misalnya halaman yang dikunjungi 150 kali per hari membutuhkan sekitar tiga belas hari, dan durasi tersebut dibulatkan menjadi dua minggu penuh agar satu siklus mingguan ikut tercakup.

Durasi yang terlalu pendek menyisakan risiko di luar daya. Kunjungan akhir pekan, hari jadwal baru dirilis, dan kejadian khusus di sekolah memengaruhi proporsi pembukaan tautan, sehingga rentang yang utuh per minggu menjaga kedua varian menanggung campuran hari yang sama. Perubahan lain pada halaman selama eksperimen berjalan dihindari, karena perubahan itu bercampur dengan efek yang sedang diukur.

Analisis akhir dengan uji z dua proporsi#

Analisis akhir membandingkan proporsi kedua varian dengan uji z dua proporsi. Statistik ujinya membagi selisih proporsi dengan standard error gabungan kedua kelompok, yaitu sebaran selisih yang wajar muncul karena kebetulan:

z=p^1−p^2p^(1−p^)(1n1+1n2) z = \frac{\hat{p}_1 - \hat{p}_2}{\sqrt{\hat{p}(1-\hat{p})\left(\dfrac{1}{n_1} + \dfrac{1}{n_2}\right)}}

dengan p^\hat{p} sebagai proporsi gabungan kedua sampel. Kode berikut menjalankan analisis pada data simulasi berseed tetap yang dibangkitkan sesuai rancangan, yaitu 963 pengunjung per varian dengan proporsi dasar 0,30 dan efek yang ditanam berupa kenaikan ke 0,36. Data simulasi digunakan agar prosedurnya dapat direproduksi, sedangkan langkah-langkahnya sama persis dengan analisis data nyata.

python analisis-akhir.py
import numpy as np
from scipy import stats

rng = np.random.default_rng(42)

# data simulasi sesuai rancangan: n = 963 per varian
n = 963
a = rng.binomial(1, 0.30, n)  # varian A, tata letak lama
b = rng.binomial(1, 0.36, n)  # varian B, tata letak baru

x1, x2 = a.sum(), b.sum()
p1, p2 = x1 / n, x2 / n

# uji z dua proporsi, selisih dinyatakan sebagai B dikurangi A
p_gabung = (x1 + x2) / (2 * n)
se = np.sqrt(p_gabung * (1 - p_gabung) * (2 / n))
selisih = p2 - p1
z = selisih / se
p_value = 2 * stats.norm.sf(abs(z))

# interval kepercayaan 95% untuk selisih proporsi
se_selisih = np.sqrt(p1 * (1 - p1) / n + p2 * (1 - p2) / n)
bawah = selisih - 1.96 * se_selisih
atas = selisih + 1.96 * se_selisih

print(f"klik varian A        : {x1} dari {n} ({p1:.3f})")
print(f"klik varian B        : {x2} dari {n} ({p2:.3f})")
print(f"selisih proporsi     : {selisih:.3f}")
print(f"statistik z          : {z:.3f}")
print(f"p-value              : {p_value:.4f}")
print(f"interval selisih 95% : ({bawah:.3f}, {atas:.3f})")
text Keluaran
klik varian A        : 292 dari 963 (0.303)
klik varian B        : 347 dari 963 (0.360)
selisih proporsi     : 0.057
statistik z          : 2.662
p-value              : 0.0078
interval selisih 95% : (0.015, 0.099)

Varian A memberi 292 pembukaan dari 963 pengunjung, sedangkan varian B 347 dari 963, sehingga proporsinya 0,303 berbanding 0,360. Statistik z sebesar 2,662 menghasilkan p-value 0,0078, jauh di bawah ambang 0,05, sehingga H0H_0 ditolak. Interval kepercayaan 95% untuk selisih proporsi berada pada 0,015 sampai 0,099, yang berarti kenaikannya diperkirakan antara 1,5 dan 9,9 poin persentase. Selisih, p-value, dan interval dilaporkan bersama karena saling melengkapi. Nilai selisih 0,057 juga berdekatan dengan efek yang ditanam sebesar 0,06, sehingga kewajaran prosedur pengukurannya dapat diperiksa.

Jebakan yang menyesatkan#

Jebakan pertama adalah menghentikan eksperimen begitu p-value menyentuh ambang. Kebiasaan mengintip berkala (peeking) berupa memeriksa hasil setiap beberapa hari dan menghentikan eksperimen pada pemeriksaan pertama yang signifikan. Setiap pemeriksaan adalah undangan kesalahan tipe I yang baru, sehingga peluang menyatakan kemenangan palsu jauh melampaui 5%.

Simulasi berikut membandingkan dua aturan keputusan pada eksperimen tanpa efek nyata, yaitu satu uji tunggal di akhir dan pengintipan berkala setiap dua ratus pengunjung hingga batas 2.000. Eksperimennya diulang seribu kali dengan seed tetap.

python peeking-simulasi.py
import numpy as np
from scipy import stats

def p_value_dua_proporsi(x1, x2, n1, n2):
    p1 = x1 / n1
    p2 = x2 / n2
    p = (x1 + x2) / (n1 + n2)
    se = np.sqrt(p * (1 - p) * (1 / n1 + 1 / n2))
    z = (p1 - p2) / se
    return 2 * stats.norm.sf(np.abs(z))

rng = np.random.default_rng(42)
banyak_eksperimen = 1000
n_maks = 2000
titik_cek = np.arange(200, n_maks + 1, 200)

berhenti_dini = 0
signifikan_final = 0

for _ in range(banyak_eksperimen):
    # kedua varian berasal dari proporsi yang sama, yaitu 0.30 tanpa efek nyata
    a = rng.binomial(1, 0.30, n_maks)
    b = rng.binomial(1, 0.30, n_maks)
    x1 = np.cumsum(a)[titik_cek - 1]
    x2 = np.cumsum(b)[titik_cek - 1]
    p_cek = p_value_dua_proporsi(x1, x2, titik_cek, titik_cek)

    # kebiasaan mengintip: berhenti pada pemeriksaan pertama yang signifikan
    if np.any(p_cek < 0.05):
        berhenti_dini += 1

    # aturan tetap: satu uji tunggal hanya pada akhir eksperimen
    if p_cek[-1] < 0.05:
        signifikan_final += 1

print(f"eksperimen tanpa efek nyata         : {banyak_eksperimen}")
print(f"signifikan dengan satu uji final    : {signifikan_final} dari {banyak_eksperimen}")
print(f"signifikan karena mengintip berkala : {berhenti_dini} dari {banyak_eksperimen}")
text Keluaran
eksperimen tanpa efek nyata         : 1000
signifikan dengan satu uji final    : 45 dari 1000
signifikan karena mengintip berkala : 191 dari 1000

Kedua varian pada simulasi berasal dari proporsi yang sama persis, sehingga kemenangan yang dilaporkan pasti palsu. Dengan satu uji final, 45 dari 1.000 eksperimen dinyatakan signifikan, atau 4,5% sesuai janji ambangnya. Dengan pengintipan berkala pada sepuluh titik pemeriksaan, angkanya melonjak menjadi 191 dari 1.000, atau 19,1%. Ukuran sampel yang tetap dan satu pemeriksaan di akhir bukan formalitas, melainkan syarat yang menjaga janji 5% tetap berlaku.

Jebakan kedua adalah menguji banyak metrik sekaligus tanpa koreksi, dan satu di antaranya hampir pasti signifikan pada eksperimen yang cukup panjang, sesuai perilaku uji berulang yang membengkakkan peluang kebetulan. Jebakan ketiga adalah efek kebaruan (novelty effect), yaitu kenaikan sesaat karena tampilan baru terasa berbeda, bukan karena lebih baik, dan durasi yang memuat satu siklus penuh membantu menguranginya. Jebakan keempat adalah membaca hasil pada subkelompok setelah eksperimen selesai, misalnya kenaikan yang hanya muncul pada pengunjung sore, karena pembagian subkelompok sesudah hasil dilihat mengembalikan masalah p-hacking.

Ringkasan#

  • Pengujian A/B mengandalkan randomisasi untuk menyebar faktor lain secara merata, dan satu metrik utama ditetapkan sebelum eksperimen berjalan.
  • Ukuran sampel dihitung dari α\alpha, daya uji, dan selisih terkecil yang layak dideteksi, lalu durasinya memuat satu siklus mingguan penuh.
  • Analisis akhir menggunakan uji z dua proporsi dan melaporkan selisih, p-value, serta interval kepercayaannya bersama-sama.
  • Menghentikan dini, mengintip berkala, menguji banyak metrik, dan efek kebaruan adalah jebakan yang melaporkan kemenangan palsu.

Referensi#