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, perbedaan float32 dan float64, 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:

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

text
[-32768]
[-124  105]
  1. Baris 4 (maks + 1): 32767 + 1 seharusnya 32768, tetapi nilai itu tidak muat di int16. Hasilnya justru -32768 — pola bit yang sama dibaca ulang sebagai bilangan negatif.
  2. Baris 6 (kecil + 5): pada int8 rentangnya -128 sampai 127, sehingga 127 + 5 yang seharusnya 132 menjadi -124; elemen kedua 100 + 5 = 105 masih dalam rentang dan tampil normal.
  3. 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:

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

text
-32768
RuntimeWarning
overflow encountered in scalar add
  1. Baris 4-6: warnings.catch_warnings(record=True) mencegat peringatan yang dikirim ke dalam daftar tercatat, sehingga stdout tetap bersih dan urutan keluaran pasti.
  2. Baris 6 (np.int16(32767) + np.int16(1)): scalar overflow. Nilai yang dikembalikan tetap -32768, sama seperti kasus array.
  3. Baris 8-9: kategori peringatannya RuntimeWarning dan pesannya overflow 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:

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

text
0.30000000000000004
0.100000001
0.10000000000000000555
  1. Baris 3 (0.1 + 0.2): hasilnya 0.30000000000000004, bukan 0.3 — galat pembulatan biner pada float64.
  2. Baris 4: float32 hanya menyimpan sekitar 7 digit berarti; nilai tersimpannya 0.100000001.
  3. Baris 5: float64 menyimpan sekitar 15-16 digit berarti; nilai tersimpannya 0.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:

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

text
100000.086
100000.00149011612
1.0
  1. Baris 4: np.full(1000000, 0.1, dtype=np.float32) membuat sejuta salinan 0.1 versi float32; nilai idealnya 100000.
  2. Baris 5 (np.sum(besar)): akumulator mengikuti dtype array, yaitu float32. Hasilnya 100000.086 — melenceng sekitar 0,086 dari nilai ideal.
  3. Baris 6 (dtype=np.float64): penjumlahan dipaksa memakai akumulator float64. Hasilnya 100000.00149011612, yang memperlihatkan galat sebenarnya berasal dari penyimpanan 0.1 versi float32, bukan dari proses penjumlahannya.
  4. Baris 7 (math.fsum): fungsi dari pustaka baku Python menjumlahkan dengan presisi penuh — sepuluh salinan 0.1 kembali tepat 1.0.
Aspekfloat32float64
Ukuran per elemen4 byte8 byte
Digit berartisekitar 7sekitar 15-16
np.sum atas 1.000.000 × 0.1100000.086100000.00149011612
Posisi yang disarankanarray besar yang menuntut hemat memoripilihan bawaan untuk perhitungan umum

Strategi mitigasi yang bisa kita terapkan:

  1. Gunakan float64 (dtype bawaan NumPy) untuk perhitungan umum.
  2. Paksa akumulator yang lebih besar: np.sum(a, dtype=np.float64).
  3. Butuh penjumlahan presisi penuh pada list kecil: pakai math.fsum.
  4. Bandingkan hasil float dengan toleransi, bukan kesetaraan persis.