Tipe data NumPy menempatkan memori tetap: int16 selalu dua byte, float32 selalu empat byte. Keputusan desain itu membuat array ringkas dan cepat, tetapi membawa dua konsekuensi yang bersembunyi dari perhitungan di atas kertas. Pertama, bilangan bulat yang melampaui batas tipe tidak berhenti di nilai maksimum — ia membungkus ke nilai paling kecil. Kedua, bilangan pecahan hampir selalu disimpan dengan pembulatan, dan pembulatan itu menumpuk sepanjang perhitungan. Chapter Tipe Data Numerik memperkenalkan tipe-tipe ini; sekarang kita lihat apa yang terjadi ketika batasnya dilampaui.
Agenda bab:
- Overflow bilangan bulat pada array: membungkus tanpa suara.
- Perbedaan perilaku pada scalar NumPy yang memberi
RuntimeWarning. - Presisi float:
0.1 + 0.2, perbedaanfloat32danfloat64, serta cara mitigasi.
Overflow bilangan bulat: membungkus tanpa suara #
int16 menampung nilai -32768 sampai 32767. Ketika hasil operasi keluar dari rentang itu, NumPy membungkusnya menurut representasi two’s complement — tidak ada error, tidak ada peringatan:
import numpy as np
maks = np.array([32767], dtype=np.int16)
print(maks + 1)
kecil = np.array([127, 100], dtype=np.int8)
print(kecil + 5)Output yang Dihasilkan:
[-32768]
[-124 105]- Baris 4 (
maks + 1):32767 + 1seharusnya32768, tetapi nilai itu tidak muat diint16. Hasilnya justru-32768— pola bit yang sama dibaca ulang sebagai bilangan negatif. - Baris 6 (
kecil + 5): padaint8rentangnya -128 sampai 127, sehingga127 + 5yang seharusnya 132 menjadi-124; elemen kedua100 + 5 = 105masih dalam rentang dan tampil normal. - Kedua baris berjalan tanpa satu pun pesan — inilah sifat paling menyesatkan dari overflow array.
Scalar NumPy memberi RuntimeWarning #
Perilaku overflow berbeda ketika kedua operannya adalah scalar NumPy, bukan array. NumPy 2.x mengirim RuntimeWarning — di script biasa peringatan itu tampil di stderr tanpa menghentikan program — namun nilai yang dikembalikan tetap hasil pembungkusan. Blok berikut menangkap peringatan itu agar keluaran deterministik:
import numpy as np
import warnings
with warnings.catch_warnings(record=True) as tercatat:
warnings.simplefilter("always")
hasil = np.int16(32767) + np.int16(1)
print(hasil)
print(tercatat[0].category.__name__)
print(tercatat[0].message)Output yang Dihasilkan:
-32768
RuntimeWarning
overflow encountered in scalar add- Baris 4-6:
warnings.catch_warnings(record=True)mencegat peringatan yang dikirim ke dalam daftartercatat, sehingga stdout tetap bersih dan urutan keluaran pasti. - Baris 6 (
np.int16(32767) + np.int16(1)): scalar overflow. Nilai yang dikembalikan tetap-32768, sama seperti kasus array. - Baris 8-9: kategori peringatannya
RuntimeWarningdan pesannyaoverflow encountered in scalar add. Pada script biasa, pesan ini muncul di stderr sementara stdout tetap bersih.
Presisi float: 0.1 + 0.2 #
Bilangan pecahan disimpan dalam biner, dan angka sederhana seperti 0,1 sebenarnya tidak dapat disimpan persis. Akibatnya, hasil perhitungan bisa sedikit menyimpang dari harapan:
import numpy as np
print(0.1 + 0.2)
print(f"{np.float32(0.1):.9f}")
print(f"{np.float64(0.1):.20f}")Output yang Dihasilkan:
0.30000000000000004
0.100000001
0.10000000000000000555- Baris 3 (
0.1 + 0.2): hasilnya0.30000000000000004, bukan0.3— galat pembulatan biner padafloat64. - Baris 4:
float32hanya menyimpan sekitar 7 digit berarti; nilai tersimpannya0.100000001. - Baris 5:
float64menyimpan sekitar 15-16 digit berarti; nilai tersimpannya0.10000000000000000555— lebih dekat ke 0,1 tetapi tetap bukan persis.
Karena hasil float jarang persis, bandingkan float dengan toleransi (np.isclose, np.allclose) sebagaimana dibahas pada Chapter Floating Point Comparison, bukan dengan ==.
Akumulasi float32 versus float64 #
Galat pembulatan yang kecil per operasi menumpuk seiring banyaknya penjumlahan. Satu juta penambahan 0.1 memberi gambaran yang jelas:
import numpy as np
import math
besar = np.full(1000000, 0.1, dtype=np.float32)
print(np.sum(besar))
print(np.sum(besar, dtype=np.float64))
print(math.fsum([0.1] * 10))Output yang Dihasilkan:
100000.086
100000.00149011612
1.0- Baris 4:
np.full(1000000, 0.1, dtype=np.float32)membuat sejuta salinan0.1versifloat32; nilai idealnya100000. - Baris 5 (
np.sum(besar)): akumulator mengikuti dtype array, yaitufloat32. Hasilnya100000.086— melenceng sekitar 0,086 dari nilai ideal. - Baris 6 (
dtype=np.float64): penjumlahan dipaksa memakai akumulatorfloat64. Hasilnya100000.00149011612, yang memperlihatkan galat sebenarnya berasal dari penyimpanan0.1versifloat32, bukan dari proses penjumlahannya. - Baris 7 (
math.fsum): fungsi dari pustaka baku Python menjumlahkan dengan presisi penuh — sepuluh salinan0.1kembali tepat1.0.
| Aspek | float32 | float64 |
|---|---|---|
| Ukuran per elemen | 4 byte | 8 byte |
| Digit berarti | sekitar 7 | sekitar 15-16 |
np.sum atas 1.000.000 × 0.1 | 100000.086 | 100000.00149011612 |
| Posisi yang disarankan | array besar yang menuntut hemat memori | pilihan bawaan untuk perhitungan umum |
Strategi mitigasi yang bisa kita terapkan:
- Gunakan
float64(dtype bawaan NumPy) untuk perhitungan umum. - Paksa akumulator yang lebih besar:
np.sum(a, dtype=np.float64). - Butuh penjumlahan presisi penuh pada list kecil: pakai
math.fsum. - Bandingkan hasil float dengan toleransi, bukan kesetaraan persis.