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:
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:
0.1 + 0.2 = 0.30000000000000004
== 0.3 -> False
a == b -> [False True]
(a == b).all() -> FalseMari kita bedah:
- Baris 3-4:
0.1dan0.2tidak punya representasi biner yang persis; penjumlahannya menghasilkan0.30000000000000004, satu langkah di atas0.3, sehingga perbandingan==bernilaiFalse. - Baris 6-7: pada array, perbandingan
==bekerja per elemen dan menghasilkan array boolean — elemen pertamaFalsekarena selisih satu bit, elemen keduaTruekarena0.5memang dapat direpresentasikan persis. - Baris 8: merangkai hasilnya dengan
.all()mewarisi keputusasaan yang sama: satu elemen saja berbeda membuat keseluruhan perbandinganFalse.
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):
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:
True
False
True
False
TruePenjelasan tiap baris:
- Baris 3: selisih
0.30000000000000004dengan0.3jauh di bawah toleransi, sehinggaisclosebernilaiTrue— di sinilah masalah blok sebelumnya selesai. - Baris 4-5: membandingkan
1e-7dengan0.0gagal pada pengaturan bawaan; toleransi relatif tidak menolong karenartol * |0.0|selalu nol, danatolbawaan1e-08lebih kecil dari selisih1e-7. - Baris 6: menaikkan
atol=1e-6membuat perbandingan terhadap nol berjalan — aturan praktisnya, bandingkan sesuatu dengan nol membutuhkanatol, bukanrtol. - Baris 7-8: sebaliknya, untuk bilangan besar seperti
100.01melawan100.0, toleransi relatifrtol=1e-3yang memutuskan, bukanatol.
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.
| Parameter | Nilai bawaan np.isclose | Peran |
|---|---|---|
rtol | 1e-05 | longgar relatif terhadap ` |
atol | 1e-08 | batas absolut, wajib saat membandingkan dengan nol |
equal_nan | False | bila 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:
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:
[ True True False]
False
TrueMari kita baca hasilnya:
- Baris 4:
iscloseper elemen menghasilkanTrueuntuk dua elemen pertama; elemen ketiga bernilaiFalsekarenanantidak dekat dengan apa pun — bahkan dengan dirinya sendiri, sesuai sifatnanpada Chapter NaN dan Infinity. - Baris 5:
allclosemereduksi hasil itu menjadi satu nilaiFalse; satunansaja menahan lulusnya seluruh perbandingan. - Baris 6: parameter
equal_nan=Truemengubah makna perbandingan:nandianggap berdekatan dengannanpada posisi yang sama, sehingga hasilnyaTrue.
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:
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:
0.9999999999999999 False
np.sum: 1.0Bedah baris demi baris:
- Baris 4-7: penjumlahan berurutan sepuluh kali
0.1menghasilkan0.9999999999999999— satu langkah di bawah1.0— sehinggatotal == 1.0bernilaiFalse. - Baris 8-9:
np.set_printoptions(precision=17)membuka digit-digit yang selama ini disembunyikan; inilah cara memastikan nilai sebenarnya sebelum memutuskan toleransi. - Baris 10:
tagihan.sum()mengembalikan persis1.0karena 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.