小数の精度と浮動小数点数
小数を有限の桁数で保存する方法と、計算結果に誤差が生じる理由を学びます。
説明
小数を保存する方法には、整数に一定の倍率を掛けて扱う固定小数点と、仮数・指数を使う浮動小数点があります。どちらも有限の保存領域を使うため、扱える細かさや大きさに制限があります。
このモジュールでは、価格を整数の最小単位で保存する例から始めます。次に1.5の浮動小数点表現を符号・指数・仮数に分け、0.1のように2進数では割り切れない値を調べます。最後に、小数の一致判定と丸め方の指定を扱います。
1小数点以下2桁を整数で保存
価格12.34を、0.01を1単位とする整数1234で保存する例です。同じ単位の代金を加算します。
price = 1234; fee = 660.01単位の整数; total = price + fee同じ単位で加算; total, divmod(total, 100)総単位数と整数部・小数部合計は1300単位です。100で割った商13と余り0から、13.00と表せます。小数点の位置をあらかじめ決めているので、整数の加算で金額を扱えます。
2倍率を変えると表せる細かさが変わる
12.345を、小数点以下2桁と3桁の固定小数点にします。ここでは単位に満たない部分を切り捨てます。
from decimal import Decimal10進数の計算機能; amount = Decimal('12.345')10進小数を文字列から作る; [(scale, int(amount * scale)) for scale in (100, 1000)]倍率を変え整数単位にする倍率100では1234なので12.34まで、倍率1000では12345なので12.345まで保存できます。倍率を上げると細かい値を表せますが、同じビット幅なら表せる最大値は小さくなります。
31.5の64ビット浮動小数点表現
浮動小数点は、数を有効数字に当たる仮数と、小数点の位置を決める指数で表します。ここではIEEE 754のbinary64形式へ1.5を変換します。
import struct数値とバイト列の変換機能; raw = struct.pack('>d', 1.5)binary64へ変換; raw.hex(' '), len(raw)保存バイトと長さ3F F8 00 00 00 00 00 00の8バイト、64ビットです。>dは上位バイトから並べる64ビット浮動小数点の指定です。
4符号・指数・仮数の領域
binary64は符号1ビット、指数11ビット、小数部52ビットに分かれます。保存したバイト列をビット列に直して切り分けます。
bits = format(int.from_bytes(raw, 'big'), '064b')64桁の2進数へ変換; bits[0], bits[1:12], bits[12:]符号・指数・小数部符号は0、指数は01111111111、小数部は1の後に0が51個です。符号0は正数を表します。
5格納された指数から値を求める
通常の正規化数では、指数の格納値から1023を引き、仮数の整数部分に1を補います。1.5の例で元の値を復元します。
exponent = int(bits[1:12], 2) - 1023指数の補正値1023を引く; fraction = int(bits[12:], 2) / 2**52小数部の値; exponent, fraction, (1 + fraction) * 2**exponent各要素と復元した値指数0、小数部分0.5なので、(1+0.5)×2⁰=1.5です。ここでの式は正規化数に対するもので、ゼロや非正規化数、無限大、NaNでは別の規則を使います。
60.1として保存される値
10進数0.1は、2進数では有限の桁数で表せません。保存されたfloatの正確な分数を求め、10進数でも細かく表示します。
x = 0.1浮動小数点数; x.as_integer_ratio()保存値を正確な分数で取得, format(x, '.20f')小数点以下20桁で表示保存値は3602879701896397/36028797018963968で、表示すると0.10000000000000000555です。通常の表示が0.1でも、内部の値は正確な1/10とは少し異なります。
7加算後に完全一致しない例
0.1を3回加えた値を0.3と比較します。どちらも近似値を使った計算です。
result = x + x + x0.1を3回加算; result, result == 0.3, result - 0.3結果・完全一致・差結果は0.30000000000000004で、完全一致はFalseです。差は約5.55×10⁻¹⁷と小さくても、==は差が0かどうかで判定します。
8許容する誤差を指定した比較
測定値などを比較するときは、用途に応じた誤差を許容する場合があります。ここでは絶対誤差10⁻¹²以内を一致とします。
import math数学関数を読み込む; math.isclose(result, 0.3, rel_tol=0.0, abs_tol=1e-12)絶対差10⁻¹²以内か判定Trueです。この指定では値の大きさに比例する相対誤差は使わず、絶対差だけを判定しています。許容値は扱う量や必要な精度から決めます。
910進小数での計算
Decimalを使って、0.1を10進数として作り直します。floatを渡すのでなく文字列から作ることで、意図した10進数を指定できます。
decimal_result = Decimal('0.1') * 310進数の0.1を3倍; decimal_result, decimal_result == Decimal('0.3')結果と完全一致Decimalでは0.3となり、完全一致もTrueです。ただしDecimalも計算精度の設定を持ち、1/3などの無限小数を常に正確に保存できるわけではありません。
10中間値の丸め方による違い
2.345を小数点以下2桁に丸めます。末尾がちょうど5のとき、偶数側へ丸める方式と、絶対値が大きい側へ丸める方式では結果が変わります。
from decimal import ROUND_HALF_EVEN, ROUND_HALF_UP二つの丸め方式; n = Decimal('2.345')ちょうど中間になる値; [n.quantize(Decimal('0.01'), rounding=mode) for mode in (ROUND_HALF_EVEN, ROUND_HALF_UP)]各方式で小数点以下2桁に丸める偶数側への丸めは2.34、四捨五入に相当するROUND_HALF_UPでは2.35です。丸める桁だけでなく、中間値をどちらへ寄せるかも仕様として決める必要があります。
まとめ
固定小数点では、最小単位と整数の対応を先に決めます。浮動小数点では、指数によって扱える大きさを広げる代わりに、保存できる有効桁数が限られます。
計算結果の比較や丸めには、用途に応じた規則が必要です。表示が同じに見えることと、内部の値が等しいことは一致するとは限りません。
操作履歴と解説を振り返れます。
Python 3
price = 1234; fee = 66; total = price + fee; total, divmod(total, 100)(1300, (13, 0))
from decimal import Decimal; amount = Decimal('12.345'); [(scale, int(amount * scale)) for scale in (100, 1000)][(100, 1234), (1000, 12345)]
import struct; raw = struct.pack('>d', 1.5); raw.hex(' '), len(raw)('3f f8 00 00 00 00 00 00', 8)bits = format(int.from_bytes(raw, 'big'), '064b'); bits[0], bits[1:12], bits[12:]('0', '01111111111', '1000000000000000000000000000000000000000000000000000')exponent = int(bits[1:12], 2) - 1023; fraction = int(bits[12:], 2) / 2**52; exponent, fraction, (1 + fraction) * 2**exponent(0, 0.5, 1.5)
x = 0.1; x.as_integer_ratio(), format(x, '.20f')((3602879701896397, 36028797018963968), '0.10000000000000000555')
result = x + x + x; result, result == 0.3, result - 0.3(0.30000000000000004, False, 5.551115123125783e-17)
import math; math.isclose(result, 0.3, rel_tol=0.0, abs_tol=1e-12)True
decimal_result = Decimal('0.1') * 3; decimal_result, decimal_result == Decimal('0.3')(Decimal('0.3'), True)from decimal import ROUND_HALF_EVEN, ROUND_HALF_UP; n = Decimal('2.345'); [n.quantize(Decimal('0.01'), rounding=mode) for mode in (ROUND_HALF_EVEN, ROUND_HALF_UP)][Decimal('2.34'), Decimal('2.35')]演習完了