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:

python
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:

text
[178.23085499 183.13536033 179.17375624 162.21456388]
Seed di dalam fungsi -> identik

Bedah baris demi baris:

  1. Baris 3-6: generator ber-seed 2024 dibuat di dalam buat_sampel, sehingga setiap pemanggilan memulai dari titik awal yang sama — bukan melanjutkan urutan yang sudah berjalan.
  2. Baris 8-12: pengujian memanggil buat_sampel dua kali dan menuntut hasilnya identik lewat assert_array_equal; asersi ketat ini layak karena kedua array seharusnya benar-benar sama bit demi bit.
  3. 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:

python
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:

text
test_pertama -> lulus
test_kedua -> gagal

Mari kita telaah penyebabnya:

  1. Baris 3 (rng_bersama = ...): generator dibuat sekali di level modul; seluruh fungsi uji menarik dari urutan yang sama dan menggeser keadaannya bergantian.
  2. test_pertama: tarikan pertama dari seed 7 adalah [9 6 6], sesuai nilai harapan, sehingga lulus.
  3. 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:

python
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:

text
test_pertama -> lulus
test_kedua -> lulus

Membagi 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:

python
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:

text
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:

  1. 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.
  2. Baris 5-8: dua generator dibangun dari anak yang berbeda dan menghasilkan urutan berbeda — 6 6 1 5 1 melawan 2 1 3 4 8.
  3. Baris 10-12: menjalankan SeedSequence(2024) lagi dan melakukan spawn ulang menghasilkan anak yang identik; urutan anak pertama kembali persis 6 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:

python
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:

text
rata-rata = 169.821059
simpangan = 7.960738
Semua pengujian lulus

Bedah baris demi baris:

  1. Baris 3-7: hitung_statistik menarik 10.000 sampel dari distribusi normal dengan rata-rata teori 170.0 dan simpangan teori 8.0; seed 10 dikunci agar sampel yang diuji selalu sama.
  2. Baris 9-13: pengujian tidak menuntut kesamaan persis, melainkan kedekatan: rata-rata sampel dalam ±0.5 dari teori dan simpangan dalam ±0.2. Margin ini longgar terhadap perubahan versi NumPy, tetapi tetap ketat terhadap bug logika yang menggeser skala data.
  3. Baris 15-18: memanggil fungsi yang sama menghasilkan 169.821059 dan 7.960738 di setiap eksekusi — angka yang boleh dituliskan kembali sebagai nilai harapan.

Tabel berikut merangkum tiga pola seed yang kita uji sepanjang bab ini:

PolaCaraCocok untukRisiko
Seed di dalam fungsi ujidefault_rng(seed) per fungsimayoritas pengujianhampir tidak ada
Generator bersama moduldefault_rng(seed) di level modultidak disarankanurutan eksekusi mengubah hasil
SeedSequence.spawnpecah satu seed jadi anak independenbanyak urutan dari satu seed utamasedikit 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.