コンピューターで小数点以下の数値を扱うにあたっての備忘録

Python

Pythonに限らず、コンピューターを使用する上で付いて回る問題について。

小数点以下の数値を使用する場合ありがちなエラー

小数点以下の数値がある数で判定をしていた時、おかしな結果が出た。

>>> 4.45 + 4.15 == 8.6
False

あれ?
4.45 + 4.15 は間違いなく 8.6の筈なのに、この判定結果はFalseになる。

なぜこの様なエラーが発生するのか

確認する為に左右の辺それぞれを出力してみた。

>>> print(4.45 + 4.15)
8.600000000000001
>>> print(8.6)
8.6

4.45 + 4.15 の凄い小さい桁に1がある。

これはバグではなく、パソコンは二進法で計算しているから出てくる誤差。

ちなみに、4.45、4.15、8.6はprintで出力するとその通り、例えば4.45の様に表示されるが実は20桁まで表示するとこの誤差部分が見えてくる。

>>> print(f'{4.45:.20f}')
4.45000000000000017764
>>> print(f'{4.15:.20f}')
4.15000000000000035527
>>> print(f'{8.6:.20f}')
8.59999999999999964473

Python2.7/Python3.1になる前までは下記の様にprint()を使用しなければ可能な限りの桁を出力していたらしいが、Python2.7/Python3.1以降はprint()を使用しない場合も見やすさ重視の表示になった。

>>> 3.1415 * 2 # 可能な限り出力
6.2830000000000004
>>> print(3.1415 * 2) # printで出力すると、必要な桁分だけ表示して見やすく
6.283 

見やすさ重視の表示にはなっているが、表示の裏では先程確認した通り、かなり下の桁まで数字を持っている。

誤差の原因:二進法で計算するコンピューターの限界

何故この誤差が出てくるのか?

それは小数点以下の数字を二進法で表す事に限界があるから。
これはPythonに限らず世のコンピューター全てに通じる限界。

整数を二進法で表す場合

十進法の整数を二進法で表す場合は計算は簡単、2で割っていき、それぞれの余り(0か1)がその桁の数であり、並べれば二進法表記のできあがり。

例えば13を二進法で表す場合

13 ÷ 2 = 6余り1(2の0乗(1桁目))
6 ÷ 2 = 3 余り 0(2の1乗(2桁目))
3 ÷ 2 = 1 余り 1(2の2乗(3桁目))
1 ÷ 2 = 0 余り 1(2の3乗(4桁目))

であり、余りを4桁目から並べた1101が二進法における13の表記。

この計算で分かる通り、どんな整数が来ても、最後は2で割り切れて0になるか、割り切れず余り1になって計算が終わる。
よって、整数部分については十進法と二進法はお互いにピッタリ同じ数字として表現できる。

しかし、小数点以下を表示する場合には違ってくる。

小数点以下を含む数を二進法で表す場合

例えば4.45を二進法で表す場合

整数の4については上記と同様にすると100、小数点以下の部分0.45については下記で算出する。

0.45 ÷ 1(2の0乗) = 0 余り0.45 (1桁目)
0.45 ÷ 0.5(2の-1乗) = 0 余り0.45 (小数点第1位)
0.45 ÷ 0.25(2の-2乗) = 1 余り0.20 (小数点第2位)
0.20 ÷ 0.125(2の-3乗) = 1 余り0.075 (小数点第3位)
0.075 ÷ 0.0625(2の-4乗) = 1 余り0.0125 (小数点第4位)
0.0125 ÷ 0.03125(2の-5乗) = 0 余り0.0125 (小数点第5位)
0.0125 ÷ 0.015625(2の-6乗) = 0 余り0.0125 (小数点第6位)
0.0125 ÷ 0.0078125(2の-7乗) = 1 余り0.0046875 (小数点第7位)
0.0046875 ÷ 0.00390625(2の-8乗) = 1 余り0.00078125 (小数点第8位)
0.00078125 ÷ 0.001953125(2の-9乗) = 0 余り0.00078125 (小数点第9位)
0.00078125 ÷ 0.000976563(2の-10乗) = 0 余り0.00078125 (小数点第10位)
….(小数点第11位以降、11001100と続く)

上記の計算で分かると思うが、小数点以下を二進法で表す場合には、多くの数で割り切れなくなる。
結果、十進法と二進法はピッタリ同じ数字として表現できない。
割り切れない分は、きりの良い所で丸めたデータとして保持される。

例に挙げた小数点以下の数0.45の二進法表示は0.0111001100…(後は1100の繰り返し)となる。

4.45 はこの整数部4の二進法表記100、小数点以下の二進法表記0.0111001100….を合わせて
100.01110011001100…
となる。

同様に、4.15も8.6も二進法で表示場合割り切れない数字で下記になる。

4.15 = 100.0010011001100…
8.6 = 1000.10011001100…

4.45 + 4.15 == 8.6
は見えている通りの数での比較ではなく、裏では二進法での丸めた数同士の合計との比較になっている。
結果、このまま==で確認した時にはFalseとなる。

二進法での小数点以下の表示について、より詳しくはコンピュータにおける「データ表現」の基礎(第3回)小数点数を表す方法も参考になる。

2種類の対策

では、この様な場合にboolean等で比較する場合はどうするのか。
結論としてはround使用もしくは整数に直す処理が有効。

対策1 roundを使用

roundで必要な箇所で四捨五入。

下記は左右を小数点第2位で四捨五入(実質的には後に続く0を削除)しているのでイコールと判断される。

>>> round(4.45 + 4.15, 2) == round(8.6, 2)
True

ちなみにroundする場合、合計してから最後にround、と言う順番で処理したい。

今回の場合、先に合計して8.6とした後にroundすればイコールになるが、先にそれぞれをroundしてから合計すると、イコールにならない。

>>> round(4.45,2) + round(4.15, 2) == round(8.6, 2)
False

ちなみに左辺をさらにroundするとイコールになる。

>>> round(round(4.45,2) + round(4.15, 2),2) == round(8.6, 2)
True

ここで使用したそれぞれの数を20桁まで見ると下記の通り。

>>> print(f'{round(4.45,2) + round(4.15, 2):.20f}')
8.60000000000000142109
>>> print(f'{round(4.45 + 4.15, 2):.20f}')
8.59999999999999964473
>>> print(f'{round(8.6, 2):.20f}')
8.59999999999999964473
>>> print(f'{round(round(4.45,2) + round(4.15, 2), 2):.20f}')
8.59999999999999964473

ここから分かる事は小数点以下がある数値同士を比較する場合、最後のroundした数値同士での比較とすべき、と言う事。
roundした数値同士で更に計算すると、roundしない数同士で計算をしたのと同じ問題が発生する。

roundを使用する場合、計算量を抑える為にも最後に一回roundするだけ、と言う処理が良い。

対策2 両辺を整数にする

これは興味深い対応。
それぞれの数に10の何乗かを掛けて両辺を整数にすればこの不具合は出なくなる。

>>> 4.45 * 100 + 4.15 * 100 == 8.6 * 100 # それぞれの数に100(10の2乗)を掛けて整数にしている
True

これはそれぞれを整数にする事で、小数点以下ので発生する誤差を無くせる事に着目した手法。
下記を見れば分かる通り、100倍して整数にしただけで誤差がなくなり綺麗な整数になっていると分かる。

>>> print(f'{4.45:.20f}')
4.45000000000000017764
>>> print(f'{(4.45*100):.20f}')
445.00000000000000000000

因みに、この手法を取る場合には、計算をしてから整数にすると誤差が出る。

>>> (4.45 + 4.15) * 100 == 8.6 * 100
False
>>> print(f'{(8.6*100):.20f}')
860.00000000000000000000
>>> print(f'{((4.45 + 4.15)*100):.20f}')
860.00000000000011368684

これは、先に計算してしまうと誤差ある数値同士の二進法での合計値が誤差込みで確定してしまい、その誤差を含んだ二進法の合計値を100倍する為に右辺の100倍とはイコールにならずエラーになる、と言う理解。

結論:使い所を考えよう

以上、小数点以下の処理をする場合には
処理結果を最後にroundする
もしくは
個別に整数にしてから処理する
のどちらかで行いたい。

結論2:実際大きな問題ではないので「回避」がより良い解決

見てきた様に、出ているとは言え誤差は小数点10位以下の小さいものなので、計算をする分には問題はない。

しかし、今回の様にboolean等で正確に一致するかどうかを確認する場合には悩みがちな箇所なので、気を付けておきたい。
実際の所、この様な判定が必要になった場合、整数部分だけを比較する、等のより簡易な手を考えたい。

苦しさが先立つ「勉強」ではなく、「学ぶ」楽しさに着目したヒントが沢山載っていて参考になる本。中古が1400円程なのでKindleの方が25%程お得。
使い勝手が良いリフロー型です。

コメント

  1. […] 前の投稿前 コンピューターで小数点以下の数値を扱うにあたっての備忘録 検索: […]

タイトルとURLをコピーしました