Sekilas perbandingan floating point terlihat sepele — tinggal memakai ==. Namun bilangan float disimpan dalam representasi biner berketepatan terbatas, sehingga hasil perhitungan yang secara matematis sama bisa berbeda satu bit di belakang koma. Pada program numerik, kesalahan satu bit inilah yang membuat pengujian == bersikap menyesatkan: kadang lulus, kadang tidak, tergantung urutan operasinya.

Bab ini menunjukkan dari mana selisih kecil itu berasal, lalu memperkenalkan np.isclose dan np.allclose sebagai cara membandingkan yang benar — lengkap dengan parameter rtol dan atol yang mengatur longgarnya. Fondasi representasi float telah dibahas pada Chapter Perhitungan dengan Presisi Terbatas.

Mengapa == Menyesatkan #

Contoh klasiknya sangat pendek, tetapi akibatnya panjang: 0.1 + 0.2 tidak pernah sama persis dengan 0.3 dalam float64:

python
import numpy as np

print("0.1 + 0.2      =", 0.1 + 0.2)
print("== 0.3         ->", 0.1 + 0.2 == 0.3)

a = np.array([0.1 + 0.2, 0.5])
b = np.array([0.3, 0.5])
print("a == b         ->", a == b)
print("(a == b).all() ->", (a == b).all())

Output yang Dihasilkan:

text
0.1 + 0.2      = 0.30000000000000004
== 0.3         -> False
a == b         -> [False  True]
(a == b).all() -> False

Mari kita bedah:

  1. Baris 3-4: 0.1 dan 0.2 tidak punya representasi biner yang persis; penjumlahannya menghasilkan 0.30000000000000004, satu langkah di atas 0.3, sehingga perbandingan == bernilai False.
  2. Baris 6-7: pada array, perbandingan == bekerja per elemen dan menghasilkan array boolean — elemen pertama False karena selisih satu bit, elemen kedua True karena 0.5 memang dapat direpresentasikan persis.
  3. Baris 8: merangkai hasilnya dengan .all() mewarisi keputusasaan yang sama: satu elemen saja berbeda membuat keseluruhan perbandingan False.

Selisih seperti ini bukan bug NumPy — ia sifat dasar aritmetika floating point IEEE 754. Yang bisa kita kendalikan adalah cara membandingkannya.

np.isclose: Toleransi Relatif dan Absolut #

np.isclose(a, b) menjawab pertanyaan yang lebih berguna: apakah a dan b berdekatan dalam batas toleransi? Secara bawaan toleransinya rtol=1e-05 (relatif terhadap |b|) dan atol=1e-08 (absolut):

python
import numpy as np

print(np.isclose(0.1 + 0.2, 0.3))
print(np.isclose(1e-7, 0.0))
print(np.isclose(1e-7, 0.0, atol=1e-6))
print(np.isclose(100.01, 100.0))
print(np.isclose(100.01, 100.0, rtol=1e-3))

Output yang Dihasilkan:

text
True
False
True
False
True

Penjelasan tiap baris:

  1. Baris 3: selisih 0.30000000000000004 dengan 0.3 jauh di bawah toleransi, sehingga isclose bernilai True — di sinilah masalah blok sebelumnya selesai.
  2. Baris 4-5: membandingkan 1e-7 dengan 0.0 gagal pada pengaturan bawaan; toleransi relatif tidak menolong karena rtol * |0.0| selalu nol, dan atol bawaan 1e-08 lebih kecil dari selisih 1e-7.
  3. Baris 6: menaikkan atol=1e-6 membuat perbandingan terhadap nol berjalan — aturan praktisnya, bandingkan sesuatu dengan nol membutuhkan atol, bukan rtol.
  4. Baris 7-8: sebaliknya, untuk bilangan besar seperti 100.01 melawan 100.0, toleransi relatif rtol=1e-3 yang memutuskan, bukan atol.

Rumus resminya: isclose bernilai True ketika |a - b| <= atol + rtol * |b|. Dua parameter itu bekerja bersamaan — rtol menangani rentang nilai besar, atol menangani wilayah dekat nol.

ParameterNilai bawaan np.isclosePeran
rtol1e-05longgar relatif terhadap `
atol1e-08batas absolut, wajib saat membandingkan dengan nol
equal_nanFalsebila True, nan dianggap berdekatan dengan nan

np.allclose untuk Array dan equal_nan #

Untuk membandingkan seluruh array sekaligus, np.allclose menggabungkan isclose per elemen dengan reduksi .all() — bernilai True hanya jika semua elemen berdekatan:

python
import numpy as np

a = np.array([0.3, 0.1 + 0.2, np.nan])
b = np.array([0.3, 0.3, np.nan])
print(np.isclose(a, b))
print(np.allclose(a, b))
print(np.allclose(a, b, equal_nan=True))

Output yang Dihasilkan:

text
[ True  True False]
False
True

Mari kita baca hasilnya:

  1. Baris 4: isclose per elemen menghasilkan True untuk dua elemen pertama; elemen ketiga bernilai False karena nan tidak dekat dengan apa pun — bahkan dengan dirinya sendiri, sesuai sifat nan pada Chapter NaN dan Infinity.
  2. Baris 5: allclose mereduksi hasil itu menjadi satu nilai False; satu nan saja menahan lulusnya seluruh perbandingan.
  3. Baris 6: parameter equal_nan=True mengubah makna perbandingan: nan dianggap berdekatan dengan nan pada posisi yang sama, sehingga hasilnya True.

Galat yang Tersembunyi di Balik Print #

Perangkap terakhir bersifat tampilan: print dengan format bawaan membulatkan angka, sehingga selisih kecil tidak terlihat di layar. Akumulasi berulang adalah sumbernya:

python
import numpy as np

tagihan = np.full(10, 0.1)
total = 0.0
for t in tagihan:
    total += t
print(total, total == 1.0)
np.set_printoptions(precision=17)
print("np.sum:", tagihan.sum())
np.set_printoptions(precision=8)

Output yang Dihasilkan:

text
0.9999999999999999 False
np.sum: 1.0

Bedah baris demi baris:

  1. Baris 4-7: penjumlahan berurutan sepuluh kali 0.1 menghasilkan 0.9999999999999999 — satu langkah di bawah 1.0 — sehingga total == 1.0 bernilai False.
  2. Baris 8-9: np.set_printoptions(precision=17) membuka digit-digit yang selama ini disembunyikan; inilah cara memastikan nilai sebenarnya sebelum memutuskan toleransi.
  3. Baris 10: tagihan.sum() mengembalikan persis 1.0 karena NumPy menjumlahkan dengan algoritme pairwise yang mengurutkan ulang operasi — bukti bahwa hasil perhitungan float bergantung pada urutan penjumlahan.

Kesimpulan bab ini berlaku luas: jangan pernah membandingkan hasil perhitungan float dengan ==, baik pada scalar maupun array. Untuk data ilmiah, toleransi biasanya diturunkan dari galat pengukuran; untuk keuangan, biasanya berupa nilai rupiah absolut. Cara menetapkan dua besaran itu di dalam pengujian otomatis menjadi inti Chapter assert_allclose, dan dampak urutan operasi yang lebih dalam dibahas pada Chapter Stabilitas Numerik.