Kerangka potential outcomes pada chapter sebelumnya berakhir pada satu kesimpulan tegas: selisih dua rata-rata baru menjadi estimasi efek yang sahih apabila grup treatment dan kontrol sebanding — dan penyesuaian statistik hanya bisa menjamin kesebandingan untuk kovariat yang terukur. A/B testing menyelesaikan masalah itu dari akarnya: status treatment diserahkan kepada pengacakan, sehingga sebandingnya kedua grup dijamin oleh mekanisme, bukan oleh asumsi. Chapter ini merancang eksperimen lengkap dari nol — pengacakan, dua jenis metrik, uji signifikansi, interval kepercayaan — lalu menghadapi kenyataan yang jarang dibahas tutorial: eksperimen yang bersih pun bisa gagal menemukan efek yang benar-benar ada, karena ukuran sampelnya kurang. Kita menutup dengan dua jebakan praktis yang paling sering membuat hasil eksperimen tidak bisa dipercaya: peeking dan pengujian berganda.
Mengapa Pengacakan Menjamin Exchangeability #
Mekanismenya sederhana sampai terasa aneh. Ketika setiap pengguna diberi status treatment oleh pelempar koin digital — rng.random(n) < 0.5 — hasil pelemparan tidak berkorelasi dengan apa pun tentang pengguna: bukan dengan pendapatannya, perangkatnya, niat belanjanya, apalagi variabel yang tidak pernah kita ukur. Karena treatment tidak memilih siapa pun, grup treatment dan grup kontrol menjadi dua sampel acak dari populasi yang sama; dalam kosakata kerangka sebelumnya, exchangeability terpenuhi untuk semua kovariat, termasuk yang tak terlihat. Hukum bilangan besar kemudian bekerja: dengan cukup banyak pengguna, rata-rata kedua grup akan menyatu pada nilai populasi yang sama selama treatment tidak berpengaruh.
Pengacakan juga membangun fondasi pengujian hipotesis. Kalimat “jika treatment tidak berpengaruh, selisih yang teramati murni derau pelemparan koin” kini benar secara pembuktian, sehingga kita bisa menghitung berapa besar selisih yang wajar muncul dari kebetulan saja, lalu menyatakannya sebagai p-nilai. Seluruh inferensi pada chapter ini berdiri di atas mekanisme itu — dan seluruhnya runtuh bila penugasan tidak acak, misalnya treatment diberikan lebih dulu kepada pengguna yang sudah aktif.
Contoh Kode: Eksperimen dengan Dua Jenis Metrik #
Skenarionya klasik: Toko Ceria menguji halaman checkout baru. Sepuluh ribu pengguna diacak menjadi lengan A (halaman lama) dan lengan B (halaman baru); dua metrik diamati — laju konversi (biner) dan total belanja selama eksperimen (kontinu). Uplift sejati disuntikkan ke data: konversi +1,2 poin persentase, belanja +0,15 ratus ribu rupiah, sehingga kita bisa menilai apakah analisis menemukannya:
# Eksperimen A/B halaman checkout baru: pengacakan menjamin exchangeability,
# lalu dua jenis metrik diuji -- konversi (biner, uji proporsi z) dan total
# belanja selama eksperimen (kontinu, uji t Welch). Uplift keduanya diketahui.
import numpy as np
from scipy import stats
from statsmodels.stats.proportion import proportion_confint, proportions_ztest
from statsmodels.stats.weightstats import CompareMeans, DescrStatsW
rng = np.random.default_rng(42)
ALPHA = 0.05
n = 10000 # total pengguna, dibagi acak menjadi dua lengan
# Kovariat pra-eksperimen: total belanja bulan lalu (ratus ribu rupiah).
belanja_bulan_lalu = rng.normal(5.0, 2.0, n)
lengan_b = rng.random(n) < 0.5 # pelempar koin digital: pengacakan murni
print("=== Langkah 0: keseimbangan kovariat pra-eksperimen ===")
print(f"pengguna lengan A : {np.sum(~lengan_b):5d}")
print(f"pengguna lengan B : {np.sum(lengan_b):5d}")
print(f"belanja bulan lalu, rata-rata A : {belanja_bulan_lalu[~lengan_b].mean():.3f}")
print(f"belanja bulan lalu, rata-rata B : {belanja_bulan_lalu[lengan_b].mean():.3f}")
print("Pengacakan membuat kedua lengan sebanding bahkan untuk hal yang tak diukur.")
# --- Metrik 1: konversi (biner) ---
# Peluang sejati: A = 0.100, B = 0.112 (uplift absolut 1,2 pp, relatif 12%).
p_konversi = np.where(lengan_b, 0.112, 0.100)
konversi = (rng.random(n) < p_konversi).astype(int)
n_a = int(np.sum(~lengan_b))
n_b = int(np.sum(lengan_b))
k_a = int(konversi[~lengan_b].sum())
k_b = int(konversi[lengan_b].sum())
z, p_nilai = proportions_ztest([k_b, k_a], [n_b, n_a])
ci_a = proportion_confint(k_a, n_a, alpha=ALPHA, method="wilson")
ci_b = proportion_confint(k_b, n_b, alpha=ALPHA, method="wilson")
print("\n=== Metrik 1: konversi (uji proporsi z, dua sisi) ===")
print(f"lengan A: {k_a}/{n_a} = {k_a / n_a:.4f} "
f"CI 95% [{ci_a[0]:.4f}, {ci_a[1]:.4f}]")
print(f"lengan B: {k_b}/{n_b} = {k_b / n_b:.4f} "
f"CI 95% [{ci_b[0]:.4f}, {ci_b[1]:.4f}]")
print(f"statistik z = {z:.3f}, p-nilai = {p_nilai:.4f}")
print(f"alpha yang ditetapkan sebelum eksperimen = {ALPHA:.2f} -> "
f"{'tolak H0' if p_nilai < ALPHA else 'gagal tolak H0'} "
f"(H0: laju konversi kedua lengan sama)")
# --- Metrik 2: total belanja selama eksperimen (kontinu) ---
# Uplift sejati lengan B = +0.15 ratus ribu rupiah (Rp150 ribu).
belanja_eksperimen = rng.normal(5.0, 2.0, n) + np.where(lengan_b, 0.15, 0.0)
belanja_a = belanja_eksperimen[~lengan_b]
belanja_b_val = belanja_eksperimen[lengan_b]
t_stat, p_nilai_t = stats.ttest_ind(belanja_b_val, belanja_a, equal_var=False)
selisih = belanja_b_val.mean() - belanja_a.mean()
cm = CompareMeans(DescrStatsW(belanja_b_val), DescrStatsW(belanja_a))
ci_selisih = cm.tconfint_diff(alpha=ALPHA, usevar="unequal")
print("\n=== Metrik 2: total belanja (uji t Welch, dua sisi) ===")
print(f"rata-rata A : {belanja_a.mean():.3f}")
print(f"rata-rata B : {belanja_b_val.mean():.3f}")
print(f"selisih (B - A) = {selisih:+.3f} CI 95% [{ci_selisih[0]:+.3f}, {ci_selisih[1]:+.3f}]")
print(f"statistik t = {t_stat:.3f}, p-nilai = {p_nilai_t:.4f}")
print(f"alpha = {ALPHA:.2f} -> {'tolak H0' if p_nilai_t < ALPHA else 'gagal tolak H0'} "
f"(H0: rata-rata belanja kedua lengan sama)")
print("Uplift sejati yang disuntikkan: konversi +1,2 pp, belanja +0,15.")Output yang Dihasilkan:
=== Langkah 0: keseimbangan kovariat pra-eksperimen ===
pengguna lengan A : 5083
pengguna lengan B : 4917
belanja bulan lalu, rata-rata A : 4.970
belanja bulan lalu, rata-rata B : 4.990
Pengacakan membuat kedua lengan sebanding bahkan untuk hal yang tak diukur.
=== Metrik 1: konversi (uji proporsi z, dua sisi) ===
lengan A: 508/5083 = 0.0999 CI 95% [0.0920, 0.1085]
lengan B: 525/4917 = 0.1068 CI 95% [0.0984, 0.1157]
statistik z = 1.122, p-nilai = 0.2618
alpha yang ditetapkan sebelum eksperimen = 0.05 -> gagal tolak H0 (H0: laju konversi kedua lengan sama)
=== Metrik 2: total belanja (uji t Welch, dua sisi) ===
rata-rata A : 4.987
rata-rata B : 5.143
selisih (B - A) = +0.156 CI 95% [+0.077, +0.235]
statistik t = 3.874, p-nilai = 0.0001
alpha = 0.05 -> tolak H0 (H0: rata-rata belanja kedua lengan sama)
Uplift sejati yang disuntikkan: konversi +1,2 pp, belanja +0,15.Mari kita bedah alurnya langkah demi langkah:
- Baris 16–17 (pengacakan): kovariat pra-eksperimen dibangun lebih dulu, lalu
lengan_bditentukan murni olehrng.random(n) < 0.5— urutan ini penting agar penugasan tidak bisa “melihat” kovariat. - Baris 19–24 (Langkah 0): rata-rata belanja bulan lalu kedua lengan dicetak sebagai pemeriksaan keseimbangan — 4,970 lawan 4,990 — bukti mekanis bahwa pengacakan bekerja.
- Baris 28–29 (metrik biner): peluang konversi masing-masing lengan adalah 0,100 dan 0,112; hasil biner diundi dari peluang tersebut.
- Baris 36–38 (uji proporsi):
proportions_ztestmenguji H0 “laju konversi kedua lengan sama”, sementaraproportion_confint(method="wilson")memberi interval kepercayaan tiap lengan. - Baris 52–59 (metrik kontinu):
stats.ttest_ind(..., equal_var=False)menjalankan uji t Welch yang tidak mengandaikan varians sama;CompareMeans(...).tconfint_diff(usevar="unequal")memberi interval kepercayaan untuk selisih kedua rata-rata.
Hasilnya memuat satu keberhasilan dan satu kegagalan — dan keduanya sama berharganya. Metrik belanja berhasil: selisih teramati +0,156 (kebenaran +0,15) dengan interval kepercayaan 95% [+0,077; +0,235] yang tidak memuat nol, t = 3,874, p-nilai 0,0001; H0 ditolak pada alpha 0,05 yang ditetapkan sebelum eksperimen. Metrik konversi justru gagal: laju teramati 0,0999 lawan 0,1068 menghasilkan z = 1,122 dan p-nilai 0,2618 — padahal uplift sejati +1,2 pp memang ada di dalam data. Perhatikan juga bahwa interval kepercayaan Wilson kedua lengan saling tumpang-tindih lebar, konsisten dengan vonis “gagal tolak H0”. Eksperimen ini bukan rusak; eksperimen ini kurang daya deteksi untuk metrik konversi — dan besaran kekurangannya bisa dihitung.
Daya Deteksi: Keputusan Sebelum Eksperimen Dimulai #
Daya deteksi (statistical power) adalah peluang eksperimen menolak H0 ketika efek nyata memang sebesar yang kita asumsikan. Tiga bahan menentukannya: besar efek yang ingin ditemukan, tingkat alpha, dan jumlah pengguna per lengan. Intuisinya satu kalimat: galat standar selisih dua proporsi mengecil sebanding akar jumlah pengguna, sehingga efek yang sama makin mudah menonjol dari derau pada sampel yang besar. Skrip berikut mengukur hubungan itu secara empiris — 300 replikasi eksperimen pada tiap ukuran sampel, plus satu uji kalibrasi saat efeknya nol:
# Intuisi daya deteksi (statistical power): berapa peluang eksperimen
# menemukan uplift konversi nyata +1,2 pp pada tiap ukuran sampel, dan
# seberapa sering eksperimen "menemukan" efek padahal tidak ada satu pun.
import numpy as np
from statsmodels.stats.proportion import proportions_ztest
rng = np.random.default_rng(42)
ALPHA = 0.05
P0 = 0.100 # laju konversi lengan kontrol
UPLIFT = 0.012 # uplift absolut lengan treatment (1,2 pp)
REPLIKASI = 300
print(f"Uplift nyata yang disuntikkan : +{UPLIFT:.3f} ({UPLIFT / P0:.0%} relatif)")
print(f"Uji dua sisi, alpha = {ALPHA:.2f}, {REPLIKASI} replikasi per ukuran\n")
print(f"{'n per lengan':>12s} {'SE selisih':>11s} {'z teoretis':>11s} {'terdeteksi':>11s}")
for n_lengan in [250, 500, 1000, 2000, 5000, 10000]:
se = np.sqrt(P0 * (1 - P0) * 2 / n_lengan) # SE selisih dua proporsi
z_teoretis = UPLIFT / se
terdeteksi = 0
for _ in range(REPLIKASI):
k_b = rng.binomial(n_lengan, P0 + UPLIFT)
k_a = rng.binomial(n_lengan, P0)
_, p_nilai = proportions_ztest([k_b, k_a], [n_lengan, n_lengan])
terdeteksi += (p_nilai < ALPHA)
print(f"{n_lengan:12d} {se:11.4f} {z_teoretis:11.2f} "
f"{terdeteksi / REPLIKASI:10.0%}")
# Uji kalibrasi: tanpa efek sama sekali, peluang p < alpha harus mendekati alpha.
n_lengan = 5000
REPLIKASI_NOL = 1000
signifikan_nol = 0
for _ in range(REPLIKASI_NOL):
k_b = rng.binomial(n_lengan, P0)
k_a = rng.binomial(n_lengan, P0)
_, p_nilai = proportions_ztest([k_b, k_a], [n_lengan, n_lengan])
signifikan_nol += (p_nilai < ALPHA)
print(f"\nKalibrasi tanpa efek (n = {n_lengan} per lengan, {REPLIKASI_NOL} replikasi):")
print(f"eksperimen menyatakan 'signifikan' padahal efek nol: "
f"{signifikan_nol / REPLIKASI_NOL:.1%} (harusnya sekitar {ALPHA:.0%})")Output yang Dihasilkan:
Uplift nyata yang disuntikkan : +0.012 (12% relatif)
Uji dua sisi, alpha = 0.05, 300 replikasi per ukuran
n per lengan SE selisih z teoretis terdeteksi
250 0.0268 0.45 3%
500 0.0190 0.63 12%
1000 0.0134 0.89 13%
2000 0.0095 1.26 25%
5000 0.0060 2.00 53%
10000 0.0042 2.83 78%
Kalibrasi tanpa efek (n = 5000 per lengan, 1000 replikasi):
eksperimen menyatakan 'signifikan' padahal efek nol: 4.0% (harusnya sekitar 5%)Bedah singkat strukturnya:
- Baris 10–13 (parameter): uplift nyata +0,012 pada baseline 0,100 — angka yang sama dengan eksperimen halaman checkout, agar temuan kedua skrip bisa dibandingkan langsung.
- Baris 18–28 (sapuan ukuran): untuk tiap ukuran lengan,
rng.binomialmengundi jumlah konversi kedua lengan,proportions_ztestmenguji, dan proporsi replikasi dengan p-nilai di bawah alpha menjadi daya deteksi empiris; kolomz teoretismembandingkan besar uplift terhadap galat standar selisih. - Baris 30–38 (uji kalibrasi): seribu replikasi tanpa efek sama sekali — laju kedua lengan identik — untuk menghitung seberapa sering eksperimen keliru menyatakan “signifikan”.
Tabel hasilnya menjelaskan kegagalan eksperimen pertama dengan rapi. Pada ±5.000 pengguna per lengan — persis ukuran eksperimen checkout — daya deteksi untuk uplift +1,2 pp hanya 53%: separuh eksperimen identik akan melewatkan efek yang ada, dan eksperimen kita kebetulan (secara deterministik) berada di separuh itu. Baris terakhir tabel menunjukkan jalan keluarnya: 10.000 pengguna per lengan menaikkan daya deteksi ke 78%, karena galat standar selisih mengecil dari 0,0060 menjadi 0,0042 sehingga uplift yang sama kini 2,8 galat standar besar. Uji kalibrasi menutup lingkaran: tanpa efek, 4,0% eksperimen menyatakan signifikan — dekat dengan alpha 5% yang seharusnya, bukti bahwa pengacakan dan uji statistik bekerja sesuai desain.
Jebakan Praktis: Peeking dan Pengujian Berganda #
Dua kebiasaan sehari-hari bisa membongkar seluruh jaminan di atas tanpa terlihat dari luar. Yang pertama adalah peeking: memeriksa p-nilai setiap hari lalu menghentikan eksperimen begitu angkanya menyentuh batas signifikan. Uji yang dirancang untuk dilihat sekali pada akhir periode kehilangan maknanya bila dilihat berpuluh kali — dengan pemeriksaan harian selama sebulan, peluang menemukan “signifikan” padahal efeknya nol bisa membengkak jauh di atas 5%. Aturan yang benar: tentukan durasi dan ukuran sampel di awal (berbekal hitungan daya deteksi), dan biarkan eksperimen selesai; teknik penghentian dini yang sahih memang ada tetapi memakai ambang yang disesuaikan, bukan alpha 0,05 yang biasa.
Yang kedua adalah pengujian berganda. Eksperimen nyata memantau belasan metrik sekaligus: konversi, nilai pesanan rata-rata, retensi 7 hari, frekuensi kunjungan, dan lain-lain. Dengan 20 metrik yang saling bebas dan alpha 0,05, rata-rata satu metrik akan tampak signifikan padahal tidak ada efek apa pun — itulah mengapa dashboard eksperimen sering memuat satu-dua “pemenang” yang tidak masuk akal. Koreksi seperti Bonferroni (bagi alpha dengan jumlah uji) menahan laju galat positif; alternatif yang lebih ringkas: batasi analisis pada satu metrik utama yang ditetapkan di proposal eksperimen, dan perlakukan metrik lainnya sebagai sinyal eksplorasi yang perlu dikonfirmasi ulang.
Sayangnya, tidak semua pertanyaan kausal bisa dijawab dengan eksperimen. Menaikkan harga atau membedakan layanan pelanggan berdasarkan undian acak dapat berdampak buruk pada pengalaman pengguna atau berada di luar batas kebijakan perusahaan; sebagian program — misalnya program loyalitas — sudah berjalan bertahun-tahun dan tidak mungkin diacak ulang. Untuk data observasional semacam itu, kita membutuhkan alat yang meniru sebandingnya grup secara statistik: skor kecenderungan dan pencocokannya, materi Chapter Propensity Score Matching.
Referensi Resmi #
- statsmodels —
proportions_ztest— API resmi uji proporsi dua sampel yang dipakai untuk metrik konversi. - statsmodels —
proportion_confint— API resmi interval kepercayaan proporsi (metode Wilson) per lengan. - scipy —
stats.ttest_ind— API resmi uji t dua sampel, termasuk varian Welch lewatequal_var=False. - statsmodels —
CompareMeans.tconfint_diff— API resmi interval kepercayaan selisih dua rata-rata yang dipakai pada metrik belanja.