一覧CS-01-3 · 数の表現

小数の精度と浮動小数点数

小数を有限の桁数で保存する方法と、計算結果に誤差が生じる理由を学びます。

説明

小数を保存する方法には、整数に一定の倍率を掛けて扱う固定小数点と、仮数・指数を使う浮動小数点があります。どちらも有限の保存領域を使うため、扱える細かさや大きさに制限があります。

このモジュールでは、価格を整数の最小単位で保存する例から始めます。次に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

1
price = 1234; fee = 66; total = price + fee; total, divmod(total, 100)
(1300, (13, 0))
2
from decimal import Decimal; amount = Decimal('12.345'); [(scale, int(amount * scale)) for scale in (100, 1000)]
[(100, 1234), (1000, 12345)]
3
import struct; raw = struct.pack('>d', 1.5); raw.hex(' '), len(raw)
('3f f8 00 00 00 00 00 00', 8)
4
bits = format(int.from_bytes(raw, 'big'), '064b'); bits[0], bits[1:12], bits[12:]
('0', '01111111111', '1000000000000000000000000000000000000000000000000000')
5
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)
6
x = 0.1; x.as_integer_ratio(), format(x, '.20f')
((3602879701896397, 36028797018963968), '0.10000000000000000555')
7
result = x + x + x; result, result == 0.3, result - 0.3
(0.30000000000000004, False, 5.551115123125783e-17)
8
import math; math.isclose(result, 0.3, rel_tol=0.0, abs_tol=1e-12)
True
9
decimal_result = Decimal('0.1') * 3; decimal_result, decimal_result == Decimal('0.3')
(Decimal('0.3'), True)
10
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')]

演習完了