15. Aritmatika Pecahan *Floating Point*: Masalah dan Keterbatasan
*****************************************************************

Angka pecahan *floating point* diwakili dalam perangkat keras komputer
sebagai pecahan basis 2 (biner). Misalnya, pecahan desimal:

   0.125

memiliki nilai 1/10 + 2/100 + 5/1000, dan dengan cara yang sama
pecahan biner

   0.001

memiliki nilai 0/2 + 0/4 + 1/8. Dua pecahan ini memiliki nilai yang
identik, satu-satunya perbedaan nyata adalah bahwa yang pertama
ditulis dalam notasi fraksi basis 10, dan yang kedua dalam basis 2.

Sayangnya, sebagian besar pecahan desimal tidak dapat
direpresentasikan persis dengan pecahan biner. Konsekuensinya adalah
bahwa, secara umum, angka pecahan *floating-point* desimal yang Anda
masukkan hanya didekati oleh angka-angka pecahan *floating-point*
biner yang sebenarnya disimpan dalam mesin.

Masalahnya lebih mudah dipahami pada awalnya di basis 10.
Pertimbangkan fraksi 1/3. Anda dapat memperkirakannya sebagai pecahan
basis 10:

   0.3

atau, lebih baik,

   0.33

atau, lebih baik,

   0.333

dan seterusnya. Tidak peduli berapa banyak digit yang Anda ingin
tulis, hasilnya tidak akan pernah benar-benar 1/3, tetapi akan menjadi
perkiraan yang semakin baik dari 1/3.

Dengan cara yang sama, tidak peduli berapa banyak digit basis 2 yang
ingin Anda gunakan, nilai desimal 0.1 tidak dapat direpresentasikan
persis sebagai fraksi basis 2. Dalam basis 2, 1/10 adalah percahan
berulang yang tak terhingga

   0.0001100110011001100110011001100110011001100110011...

Berhenti pada jumlah bit yang terbatas, dan Anda mendapatkan
perkiraan. Pada kebanyakan mesin saat ini, *float* diperkirakan
menggunakan percahan biner dengan pembilang menggunakan 53 bit pertama
dimulai dengan bit paling signifikan dan dengan penyebut sebagai
pangkat dua. Dalam kasus 1/10, fraksi biner adalah "3602879701896397 /
2 ** 55" yang dekat dengan tetapi tidak persis sama dengan nilai
sebenarnya dari 1/10.

Banyak pengguna tidak menyadari pendekatan tentang bagaimana cara
nilai ditampilkan. Python hanya mencetak perkiraan desimal ke nilai
desimal sebenarnya dari perkiraan biner yang disimpan oleh mesin. Pada
kebanyakan mesin, jika Python mencetak nilai desimal sebenarnya dari
perkiraan biner yang disimpan untuk 0.1, ia harus menampilkan

   >>> 0.1
   0.1000000000000000055511151231257827021181583404541015625

Itu lebih banyak angka daripada yang dianggap berguna oleh kebanyakan
orang, jadi Python menjaga jumlah angka tetap dapat dikelola dengan
menampilkan nilai bulat sebagai gantinya

   >>> 1 / 10
   0.1

Hanya ingat, meskipun hasil cetakannya terlihat seperti nilai tepat
1/10, nilai sebenarnya yang disimpan adalah pecahan biner terdekat
yang dapat direpresentasikan.

Menariknya, ada banyak angka desimal berbeda yang memiliki pecahan
biner perkiraan terdekat yang sama. Misalnya, angka "0.1" dan
"0.10000000000000001" dan
"0.1000000000000000055511151231257827021181583404541015625" semuanya
didekati oleh "3602879701896397 / 2 ** 55". Karena semua nilai desimal
ini memiliki perkiraan yang sama, salah satu dari nilai tersebut dapat
ditampilkan sambil tetap mempertahankan invarian lainnya
"eval(repr(x)) == x".

Secara historis, Python prompt dan fungsi bawaan "repr()" akan memilih
satu dengan 17 digit signifikan, "0.10000000000000001". Dimulai dengan
Python 3.1, Python (pada kebanyakan sistem) sekarang dapat memilih
yang paling pendek dan hanya menampilkan "0.1".

Perhatikan bahwa ini adalah sifat dasar dari pecahan *floating-point*
biner: ini bukan bug di Python, dan ini juga bukan bug dalam kode
Anda. Anda akan melihat hal yang sama dalam semua bahasa yang
mendukung aritmatika pecahan *floating-point* perangkat keras Anda
(meskipun beberapa bahasa mungkin tidak *display* perbedaan secara
default, atau dalam semua mode keluaran).

Untuk hasil yang lebih menyenangkan, Anda mungkin ingin menggunakan
pemformatan string untuk menghasilkan jumlah digit signifikan yang
terbatas:

   >>> format(math.pi, '.12g')  # give 12 significant digits
   '3.14159265359'

   >>> format(math.pi, '.2f')   # give 2 digits after the point
   '3.14'

   >>> repr(math.pi)
   '3.141592653589793'

Sangat penting untuk menyadari bahwa ini adalah, dalam arti
sebenarnya, sebuah ilusi: Anda hanya membulatkan *display* dari nilai
mesin yang sebenarnya.

Satu ilusi mungkin melahirkan yang lain. Misalnya, karena 0.1 tidak
tepat 1/10, menjumlahkan tiga nilai 0.1 mungkin tidak menghasilkan
tepat 0.3, baik:

   >>> .1 + .1 + .1 == .3
   False

Juga, karena 0.1 tidak bisa mendekati nilai tepat 1/10 dan 0.3 tidak
bisa mendekati nilai tepat 3/10, maka pra-pembulatan dengan fungsi
"round()" tidak dapat membantu:

   >>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
   False

Meskipun angka tidak dapat dibuat lebih dekat dengan nilai pastinya,
fungsi "round()" dapat berguna untuk *post-rounding* sehingga hasil
dengan nilai yang tidak tepat menjadi sebanding satu sama lain:

   >>> round(.1 + .1 + .1, 10) == round(.3, 10)
   True

Aritmatika pecahan *floating-point* biner memiliki banyak kejutan
seperti ini. Masalah dengan "0.1" dijelaskan secara rinci di bawah
ini, di bagian "Representation Error". Lihat Perils of Floating Point
untuk penjelasan lebih lengkap tentang kejutan umum lainnya.

Seperti yang dikatakan menjelang akhir, "tidak ada jawaban yang
mudah." Namun, jangan terlalu waspada terhadap pecahan *floating
point*! Kesalahan dalam operasi float Python diwarisi dari pecahan
*floating point* perangkat keras, dan pada kebanyakan mesin ada di
urutan tidak lebih dari 1 bagian dalam 2**53 per operasi. Itu lebih
dari cukup untuk sebagian besar tugas, tetapi Anda perlu ingat bahwa
itu bukan aritmatika desimal dan bahwa setiap operasi *float* dapat
mengalami kesalahan pembulatan baru.

Sementara kasus patologis memang ada, untuk sebagian besar penggunaan
aritmatika floating-point yang santai Anda akan melihat hasil yang
Anda harapkan pada akhirnya jika Anda hanya membulatkan tampilan hasil
akhir Anda ke jumlah angka desimal yang Anda harapkan. "str()"
biasanya mencukupi, dan untuk kontrol yang lebih baik lihat format
"str.format()" penentu format di Format String Syntax.

Untuk kasus penggunaan yang memerlukan representasi desimal yang
tepat, coba gunakan modul "decimal" yang mengimplementasikan
aritmatika desimal yang cocok untuk aplikasi akuntansi dan aplikasi
presisi tinggi.

Bentuk lain dari aritmatika yang tepat didukung oleh modul "fractions"
yang mengimplementasikan aritmatika berdasarkan bilangan rasional
(sehingga angka seperti 1/3 dapat direpresentasikan secara tepat).

Jika Anda adalah pengguna berat operasi floating point, Anda harus
melihat pada paket *Numerical Python* dan banyak paket lainnya untuk
operasi matematika dan statistik yang disediakan oleh proyek SciPy.
Lihat <https://scipy.org>.

Python menyediakan alat yang dapat membantu pada saat-saat langka
ketika Anda benar-benar *do* ingin tahu nilai pasti float. Metode
"float.as_integer_ratio()" menyatakan nilai *float* sebagai pecahan:

   >>> x = 3.14159
   >>> x.as_integer_ratio()
   (3537115888337719, 1125899906842624)

Karena rasio ini tepat, dapat digunakan untuk membuat ulang nilai asli
tanpa berkurang *lossless*:

   >>> x == 3537115888337719 / 1125899906842624
   True

Metode "float.hex()" mengekspresikan float dalam heksadesimal (basis
16), sekali lagi memberikan nilai tepat yang disimpan oleh komputer
Anda:

   >>> x.hex()
   '0x1.921f9f01b866ep+1'

Representasi heksadesimal yang tepat ini dapat digunakan untuk
merekonstruksi nilai *float* dengan tepat:

   >>> x == float.fromhex('0x1.921f9f01b866ep+1')
   True

Karena representasinya tepat, maka berguna untuk porting nilai secara
andal di berbagai versi Python (platform independensi) dan pertukaran
data dengan bahasa lain yang mendukung format yang sama (seperti Java
dan C99).

Alat lain yang bermanfaat adalah fungsi "math.fsum()" yang membantu
mengurangi kehilangan presisi selama penjumlahan. Ini melacak "lost
digits" karena nilai ditambahkan ke total yang sedang berlangsung. Itu
dapat membuat perbedaan dalam akurasi keseluruhan sehingga kesalahan
tidak terakumulasi ke titik di mana mereka mempengaruhi total akhir:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True


15.1. Kesalahan Representasi
============================

Bagian ini menjelaskan contoh "0.1" secara terperinci, dan menunjukkan
bagaimana Anda dapat melakukan analisis yang tepat atas kasus-kasus
seperti ini sendiri. Diasumsikan terbiasa secara mendasar dengan
representasi pecahan *floating point* biner.

*Representation error* mengacu pada fakta bahwa beberapa pecahan
desimal (sebagian besar, sebenarnya) tidak dapat direpresentasikan
persis sebagai pecahan biner (basis 2). Ini adalah alasan utama
mengapa Python (atau Perl, C, C++, Java, Fortran, dan banyak lainnya)
sering tidak akan menampilkan angka desimal tepat yang Anda harapkan.

Mengapa demikian? 1/10 tidak tepat direpresentasikan sebagai pecahan
biner. Hampir semua mesin saat ini (November 2000) menggunakan
aritmetika pecahan *floating point* IEEE-754, dan hampir semua
platform memetakan *float* Python ke IEEE-754 "double precision". 754
*double* mengandung 53 bit presisi, sehingga pada input komputer
berusaha untuk mengkonversi 0.1 ke fraksi terdekat dari bentuk
*J*/2***N* di mana *J* adalah bilangan bulat yang mengandung persis 53
bit. Menulis ulang

   1 / 10 ~= J / (2**N)

sebagai

   J ~= 2**N / 10

dan mengingat bahwa *J* memiliki tepat 53 bit (adalah ">= 2**52"
tetapi "< 2**53"), nilai terbaik untuk *N* adalah 56:

   >>> 2**52 <=  2**56 // 10  < 2**53
   True

Artinya, 56 adalah satu-satunya nilai untuk *N* yang meninggalkan *J*
dengan tepat 53 bit. Nilai terbaik untuk *J* adalah bahwa hasil bagi
dibulatkan:

   >>> q, r = divmod(2**56, 10)
   >>> r
   6

Karena sisanya lebih dari setengah dari 10, perkiraan terbaik
diperoleh dengan membulatkan ke atas:

   >>> q+1
   7205759403792794

Oleh karena itu perkiraan terbaik untuk 1/10 dalam 754 presisi
*double* adalah

   7205759403792794 / 2 ** 56

Membagi pembilang dan penyebut dengan dua mengurangi pecahan menjadi:

   3602879701896397 / 2 ** 55

Perhatikan bahwa sejak kami mengumpulkan, ini sebenarnya sedikit lebih
besar dari 1/10; jika kita belum mengumpulkan, hasil bagi akan sedikit
lebih kecil dari 1/10. Tetapi tidak dapatkah hal itu *exactly* 1/10!

Jadi komputer tidak pernah "sees" 1/10: apa yang dilihatnya adalah
pecahan tepat yang diberikan di atas, perkiraan 754 *double* terbaik
yang bisa didapatnya:

   >>> 0.1 * 2 ** 55
   3602879701896397.0

Jika kita mengalikan pecahan itu dengan 10**55, kita bisa melihat
nilainya menjadi 55 angka desimal:

   >>> 3602879701896397 * 10 ** 55 // 2 ** 55
   1000000000000000055511151231257827021181583404541015625

artinya angka persis yang disimpan di komputer sama dengan nilai
desimal 0.1000000000000000055511151231257827021181583404541015625.
Alih-alih menampilkan nilai desimal penuh, banyak bahasa (termasuk
versi Python yang lebih lama), bulatkan hasilnya menjadi 17 digit
signifikan

   >>> format(0.1, '.17f')
   '0.10000000000000001'

Modul "fractions" dan "desimal" membuat perhitungan ini mudah:

   >>> from decimal import Decimal
   >>> from fractions import Fraction

   >>> Fraction.from_float(0.1)
   Fraction(3602879701896397, 36028797018963968)

   >>> (0.1).as_integer_ratio()
   (3602879701896397, 36028797018963968)

   >>> Decimal.from_float(0.1)
   Decimal('0.1000000000000000055511151231257827021181583404541015625')

   >>> format(Decimal.from_float(0.1), '.17')
   '0.10000000000000001'
