Bab Simulasi Monte Carlo memakai ribuan percobaan ber-seed untuk menghitung nilai yang sulit. Pola serupa menjawab pertanyaan statistik yang lebih umum: seberapa jauh satu angka ringkasan bisa dipercaya? Kita mengumpulkan 30 nota pengeluaran harian konsumen sebuah warung kopi; rata-ratanya sekitar Rp28.117 — tetapi bila survei diulang dengan 30 konsumen lain, angka itu pasti bergeser. Yang kita butuhkan bukan sekadar satu angka, melainkan interval kepercayaan (confidence interval): rentang yang diperkirakan memuat nilai populasi dengan tingkat kepercayaan tertentu, misalnya 95%.

Populasi tidak pernah kita miliki, sehingga survei tidak bisa diulang berkali-kali. Bootstrap menawarkan jalan keluar: perlakukan sampel sebagai populasi mini, tarik ulang (resampling) dengan penggantian dari sampel itu sendiri, lalu hitung statistik pada tiap resample. Distribusi statistik tidak diturunkan dari rumus teori, melainkan diaproksimasi dari ribuan resample. Alur standarnya empat langkah:

  1. Bentuk sampel ber-seed (di sini 30 pengeluaran harian).
  2. rng.choice(sampel, size=(B, n), replace=True) menghasilkan B = 10_000 resample sekaligus.
  3. Hitung mean per baris resample lewat axis=1.
  4. Ambil np.percentile di 2.5 dan 97.5 sebagai batas CI 95%.

Alur Standar: Resample, Mean, Percentile #

Satu resample adalah tarikan n elemen dari sampel dengan penggantian: elemen yang sama boleh terpilih beberapa kali, dan ukuran resample disamakan dengan ukuran sampel asli (n = 30). Resample diambil dari sampel, bukan dari populasi — sampel-lah satu-satunya data yang kita pegang. Kita mulai dari sampelnya:

python
import numpy as np

rng_sampel = np.random.default_rng(42)
pengeluaran = rng_sampel.normal(28_000, 7_000, size=30).round(0)
print(pengeluaran[:5])
print("mean sampel :", pengeluaran.mean().round(2))
print("std sampel  :", pengeluaran.std(ddof=1).round(2))

Output yang Dihasilkan:

text
[30133. 20720. 33253. 34584. 14343.]
mean sampel : 28117.73
std sampel  : 5437.25

Bedah baris demi baris:

  1. Baris 3 (default_rng(42)): Generator pembentuk sampel dengan seed 42 — sampel itu sendiri harus dapat direproduksi.
  2. Baris 4 (normal(28_000, 7_000, size=30)): 30 pengeluaran harian di sekitar Rp28.000 dengan sebaran Rp7.000, dibulatkan ke rupiah utuh.
  3. Baris 5-7: lima nota pertama, mean sampel Rp28.117,73, dan std sampel Rp5.437,25.

Berikut inti bootstrap — ribuan resample dalam satu panggilan:

python
import numpy as np

rng_sampel = np.random.default_rng(42)
pengeluaran = rng_sampel.normal(28_000, 7_000, size=30).round(0)

B = 10_000
rng_boot = np.random.default_rng(2024)
resample = rng_boot.choice(pengeluaran, size=(B, pengeluaran.size), replace=True)
bootstrap_mean = resample.mean(axis=1)
print(resample.shape)
print(bootstrap_mean[:5].round(2))

Output yang Dihasilkan:

text
(10000, 30)
[27785.3  27726.5  29191.17 29487.33 26667.27]

Bedah baris demi baris:

  1. Baris 7 (default_rng(2024)): Generator resampling — seed-nya sengaja berbeda dari seed sampel; alasannya dibahas di bagian berikutnya.
  2. Baris 8 (rng_boot.choice(..., size=(B, n), replace=True)): satu panggilan menghasilkan matriks (10.000, 30); tiap baris satu resample utuh berisi 30 tarikan dengan penggantian.
  3. Baris 9 (resample.mean(axis=1)): mean dihitung sepanjang axis=1 (di dalam baris), sehingga hasilnya 10.000 nilai — satu mean per resample. Matriks resample tidak dicetak; yang menarik adalah statistik per barisnya.

Sepuluh ribu mean bootstrap itu adalah “ulangan survei” versi murah. CI 95% dibaca langsung dari sebarannya:

python
import numpy as np

rng_sampel = np.random.default_rng(42)
pengeluaran = rng_sampel.normal(28_000, 7_000, size=30).round(0)
rng_boot = np.random.default_rng(2024)
resample = rng_boot.choice(pengeluaran, size=(10_000, pengeluaran.size), replace=True)
bootstrap_mean = resample.mean(axis=1)

ci = np.percentile(bootstrap_mean, [2.5, 97.5])
print("CI 95%      :", ci.round(2))
print("mean sampel :", pengeluaran.mean().round(2))
print("median boot :", np.median(bootstrap_mean).round(2))
print("se boot     :", bootstrap_mean.std(ddof=1).round(2))
print("se teori    :", (pengeluaran.std(ddof=1) / np.sqrt(30)).round(2))

Output yang Dihasilkan:

text
CI 95%      : [26110.72 29970.04]
mean sampel : 28117.73
median boot : 28131.57
se boot     : 980.85
se teori    : 992.7

Bedah baris demi baris:

  1. Baris 9 (np.percentile(..., [2.5, 97.5])): 2.5% mean bootstrap terendah dan 2.5% tertinggi dipotong; 95% sisanya berada di antara [26110.72, 29970.04] — itu CI 95% untuk rata-rata populasi. Cara membaca percentile diulas pada quantile dan percentile.
  2. Baris 10-12: mean sampel 28117.73 berada di dalam CI, dan median bootstrap 28131.57 nyaris menimpanya — sebaran resample memusat di sekitar statistik sampel.
  3. Baris 13-14: standar error bootstrap (980.85) hampir sama dengan standar error teori std/√n (992.7); bootstrap mereproduksi rumus klasik tanpa rumus itu ditulis.

Dua Seed: Reproduksibel tapi Independen #

Ada dua keacakan di alur ini: pembentukan sampel (seed 42) dan resampling (seed 2024). Keduanya wajib tetap agar hasil dapat direproduksi, dan wajib berbeda agar keputusan acak resampling independen dari keputusan acak pembentukan sampel. Fungsi kecil berikut membungkus alurnya agar mudah diuji:

python
import numpy as np

rng_sampel = np.random.default_rng(42)
pengeluaran = rng_sampel.normal(28_000, 7_000, size=30).round(0)

def bootstrap_ci(data, seed):
    rng = np.random.default_rng(seed)
    resample = rng.choice(data, size=(10_000, data.size), replace=True)
    return np.percentile(resample.mean(axis=1), [2.5, 97.5])

print(bootstrap_ci(pengeluaran, 2024).round(2))
print(bootstrap_ci(pengeluaran, 2024).round(2))
print(bootstrap_ci(pengeluaran, 7).round(2))

Output yang Dihasilkan:

text
[26110.72 29970.04]
[26110.72 29970.04]
[26175.59 29976.01]

Bedah baris demi baris:

  1. Baris 11-12 (seed 2024 dua kali): kedua panggilan menghasilkan CI yang identik bit-per-bit — seed tetap berarti reproduksibel di komputer mana pun.
  2. Baris 13 (seed 7): aliran acak lain menggeser CI hanya sekitar Rp65 — sinyal bahwa B = 10_000 sudah cukup besar sehingga hasil stabil terhadap pilihan seed.

Jebakan dan Batas Praktis #

Kesalahan paling umum adalah lupa replace=True, atau sengaja memakai replace=False supaya “adil”. Tanpa penggantian, menarik 30 dari 30 hanya menghasilkan permutasi sampel asli, sehingga mean semua resample identik dan CI-nya rata nol. NumPy bahkan menolak lebih dulu, karena size=(10_000, 30) meminta 300.000 undian dari populasi 30 elemen:

python
import numpy as np

rng_sampel = np.random.default_rng(42)
pengeluaran = rng_sampel.normal(28_000, 7_000, size=30).round(0)
rng_boot = np.random.default_rng(2024)
try:
    rng_boot.choice(pengeluaran, size=(10_000, 30), replace=False)
except ValueError as e:
    print("ValueError:", e)

Output yang Dihasilkan:

text
ValueError: Cannot take a larger sample than population when replace is False

Perhatian berikutnya adalah memori: matriks resample berukuran B x n elemen float64 — 10.000 x 30 hanya sekitar 2,4 MB, tetapi n = 100.000 dengan B tetap butuh sekitar 8 GB. Saat sampel besar, kerjakan bertahap: resample per potongan (misalnya 500 baris sekali jalan), hitung mean potongan, buang matriksnya, lanjut potongan berikut; hanya array mean yang disimpan.

Tabel berikut merangkum keputusan-keputusan alur bootstrap:

KeputusanRekomendasiAlasan
Ukuran resamplesama dengan n (di sini 30)meniru ulangan survei sesungguhnya
Argumen replaceselalu Truetanpa penggantian, resample hanya permutasi sampel
Jumlah resample B1.000 untuk eksplorasi, 10.000 untuk laporanCI stabil terhadap seed pada B besar
Seed sampel vs resampletetap dan berbedareproduksibel sekaligus independen
Statistik lain (median, proporsi)alur sama, ganti mean(axis=1)bootstrap tidak peduli bentuk statistiknya

Dengan alur ini, ketidakpastian statistik apa pun dapat diukur langsung dari sampel — lengkap dengan jaminan reproduksibilitas dari pasangan seed yang tetap.