Pengujian yang hari ini lulus dan besok tidak lulus tanpa perubahan kode adalah pengujian yang tidak berguna. Pada program numerik, sumber ketidakstabilan itu hampir selalu sama: data uji dibuat dari generator acak yang keadaannya berjalan terus, sehingga hasil bergantung pada urutan eksekusi dan jumlah pemanggilan sebelumnya. Kabar baiknya, API modern np.random.default_rng memberi kita kendali penuh atas masalah ini.
Bab ini menetapkan pola yang benar — seed dibuat di dalam fungsi uji — lalu membongkar jebakan generator bersama, memperkenalkan SeedSequence.spawn untuk membagi seed ke banyak test, dan menutup dengan pengujian statistik menggunakan seed tetap beserta nilai harapannya. Konsep Generator dan keadaan internalnya telah dibedah pada Chapter default_rng dan Generator.
Seed di Dalam Fungsi, Bukan di Modul #
Aturan utamanya satu kalimat: setiap fungsi uji membuat generatornya sendiri. Dengan begitu, dua pemanggilan fungsi yang sama menghasilkan data yang sama persis, apa pun urutan eksekusinya:
import numpy as np
def buat_sampel(n):
rng = np.random.default_rng(2024)
return rng.normal(170.0, 8.0, size=n)
def test_dua_pemanggilan_identik():
a = buat_sampel(4)
b = buat_sampel(4)
np.testing.assert_array_equal(a, b)
test_dua_pemanggilan_identik()
print(buat_sampel(4))
print("Seed di dalam fungsi -> identik")Output yang Dihasilkan:
[178.23085499 183.13536033 179.17375624 162.21456388]
Seed di dalam fungsi -> identikBedah baris demi baris:
- Baris 3-6: generator ber-seed
2024dibuat di dalambuat_sampel, sehingga setiap pemanggilan memulai dari titik awal yang sama — bukan melanjutkan urutan yang sudah berjalan. - Baris 8-12: pengujian memanggil
buat_sampeldua kali dan menuntut hasilnya identik lewatassert_array_equal; asersi ketat ini layak karena kedua array seharusnya benar-benar sama bit demi bit. - Baris 14-16: nilai sampel yang tercetak —
178.23, 183.13, 179.17, 162.21— akan sama di komputer mana pun, sehingga bisa dituliskan sebagai nilai harapan pada pengujian lain.
Jebakan Generator Bersama Antar Uji #
Pola yang keliru — satu generator di level modul, dipakai bersama semua fungsi uji — menghasilkan kegagalan yang muncul-hilang tergantung urutan eksekusi. Mari kita buktikan dengan dua fungsi uji yang berharap nilai harapan yang sama:
import numpy as np
rng_bersama = np.random.default_rng(7)
def tarik_sampel():
return rng_bersama.integers(0, 10, size=3)
def test_pertama():
np.testing.assert_array_equal(tarik_sampel(), np.array([9, 6, 6]))
def test_kedua():
np.testing.assert_array_equal(tarik_sampel(), np.array([9, 6, 6]))
for uji in (test_pertama, test_kedua):
try:
uji()
print(uji.__name__, "-> lulus")
except AssertionError:
print(uji.__name__, "-> gagal")Output yang Dihasilkan:
test_pertama -> lulus
test_kedua -> gagalMari kita telaah penyebabnya:
- Baris 3 (
rng_bersama = ...): generator dibuat sekali di level modul; seluruh fungsi uji menarik dari urutan yang sama dan menggeser keadaannya bergantian. test_pertama: tarikan pertama dari seed7adalah[9 6 6], sesuai nilai harapan, sehingga lulus.test_kedua: tarikan kedua adalah[8 5 7]— bukan karena data salah, melainkan karena urutan generator sudah bergeser oleh pemanggilan pertama. Perilaku stateful ini terbukti pada Chapter default_rng dan Generator.
Perbaikannya persis pola bab ini: pindahkan pembuatan generator ke dalam fungsi, dan kedua pengujian langsung lulus dengan nilai harapan yang sama:
import numpy as np
def tarik_sampel():
rng = np.random.default_rng(7)
return rng.integers(0, 10, size=3)
def test_pertama():
np.testing.assert_array_equal(tarik_sampel(), np.array([9, 6, 6]))
def test_kedua():
np.testing.assert_array_equal(tarik_sampel(), np.array([9, 6, 6]))
for uji in (test_pertama, test_kedua):
try:
uji()
print(uji.__name__, "-> lulus")
except AssertionError:
print(uji.__name__, "-> gagal")Output yang Dihasilkan:
test_pertama -> lulus
test_kedua -> lulusMembagi Seed dengan SeedSequence.spawn #
Kadang kita memang butuh beberapa urutan acak yang berbeda namun tetap dapat direproduksi — misalnya data masukan dan data pembanding yang tidak boleh berasal dari urutan yang sama. SeedSequence.spawn memecah satu seed menjadi anak-anak seed yang saling independen:
import numpy as np
ss = np.random.SeedSequence(2024)
anak1, anak2 = ss.spawn(2)
rng1 = np.random.default_rng(anak1)
rng2 = np.random.default_rng(anak2)
print("urutan anak 1:", rng1.integers(0, 10, size=5))
print("urutan anak 2:", rng2.integers(0, 10, size=5))
ss_ulang = np.random.SeedSequence(2024)
b1, b2 = ss_ulang.spawn(2)
print("reproduksi :", np.random.default_rng(b1).integers(0, 10, size=5))Output yang Dihasilkan:
urutan anak 1: [6 6 1 5 1]
urutan anak 2: [2 1 3 4 8]
reproduksi : [6 6 1 5 1]Penjelasan tiap bagian:
- Baris 3-4: satu
SeedSequence(2024)dipecah menjadi dua anak; keduanya menghasilkan aliran bilangan yang tidak tumpang tindih meski berasal dari satu angka yang mudah diingat. - Baris 5-8: dua generator dibangun dari anak yang berbeda dan menghasilkan urutan berbeda —
6 6 1 5 1melawan2 1 3 4 8. - Baris 10-12: menjalankan
SeedSequence(2024)lagi dan melakukan spawn ulang menghasilkan anak yang identik; urutan anak pertama kembali persis6 6 1 5 1. Reproduksibilitas tidak ikut terbagi.
Cara lain yang sah adalah menjalankan spawn dari np.random.default_rng(...) langsung: rng.bit_generator.seed_seq.spawn(2). Keduanya memenuhi tujuan yang sama — satu seed utama mengendalikan seluruh keluarga urutan.
Statistik dengan Seed Tetap dan Nilai Harapan #
Pengujian statistik punya tantangan tambahan: hasilnya adalah besaran agregat yang memang tidak persis. Strateginya adalah mengunci seed, lalu menuntut statistik sampel besar berada dekat nilai teorinya dalam margin yang longgar:
import numpy as np
def hitung_statistik(seed, n):
rng = np.random.default_rng(seed)
data = rng.normal(loc=170.0, scale=8.0, size=n)
return data.mean(), data.std()
def test_statistik_sampel_besar():
rata, std = hitung_statistik(10, 10_000)
assert abs(rata - 170.0) < 0.5
assert abs(std - 8.0) < 0.2
test_statistik_sampel_besar()
rata, std = hitung_statistik(10, 10_000)
print(f"rata-rata = {rata:.6f}")
print(f"simpangan = {std:.6f}")
print("Semua pengujian lulus")Output yang Dihasilkan:
rata-rata = 169.821059
simpangan = 7.960738
Semua pengujian lulusBedah baris demi baris:
- Baris 3-7:
hitung_statistikmenarik 10.000 sampel dari distribusi normal dengan rata-rata teori170.0dan simpangan teori8.0; seed10dikunci agar sampel yang diuji selalu sama. - Baris 9-13: pengujian tidak menuntut kesamaan persis, melainkan kedekatan: rata-rata sampel dalam
±0.5dari teori dan simpangan dalam±0.2. Margin ini longgar terhadap perubahan versi NumPy, tetapi tetap ketat terhadap bug logika yang menggeser skala data. - Baris 15-18: memanggil fungsi yang sama menghasilkan
169.821059dan7.960738di setiap eksekusi — angka yang boleh dituliskan kembali sebagai nilai harapan.
Tabel berikut merangkum tiga pola seed yang kita uji sepanjang bab ini:
| Pola | Cara | Cocok untuk | Risiko |
|---|---|---|---|
| Seed di dalam fungsi uji | default_rng(seed) per fungsi | mayoritas pengujian | hampir tidak ada |
| Generator bersama modul | default_rng(seed) di level modul | tidak disarankan | urutan eksekusi mengubah hasil |
SeedSequence.spawn | pecah satu seed jadi anak independen | banyak urutan dari satu seed utama | sedikit lebih banyak kode |
Dengan seed yang terkendali, seluruh rangkaian pengujian numerik pada section ini kini bersifat deterministik: hasil yang sama di komputer mana pun, kapan pun dijalankan. Kombinasi asersi numpy.testing, kasus batas yang disadari, dan seed yang terisolasi adalah paket lengkap pengujian program NumPy yang sehat.