52
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PythonAdvent Calendar 2019

Day 15

プログラミングにおける数値計算はワナがいっぱい

Last updated at Posted at 2019-12-14

はじめに

早速ですが、下記の実行結果はどうなるでしょうか?

print(0.1+0.2)

普通に考えると「0.3」ですが、実際には0.3にはなりません。
正解は「0.30000000000000004」です。(値は環境によって変わる可能性があります)

数値計算の誤差を気にしない場合は問題ないのですが、そこそこ正確に数値計算を行う場合は、色々と工夫が必要です。
なので、この記事では小数計算の誤差を回避する方法についてまとめたいと思います。!(^^)!
言語はPythonなので悪しからず、、

そもそもコンピュータにおける小数点の扱いについて

まず、前提として2進数における小数点の扱いについて解説します。10進数の小数を2進数に変換するためには、小数部分のみを2倍して、その結果の整数部分を使います。
例えば、「0.875」を2進数に変換する方法は下記となります。

0.875 * 2 = 1.75 (整数部分が1)

# 小数部分の0.75のみ2倍する
0.75 * 2 = 1.5 (整数部分が1)

# # 小数部分の0.5のみ2倍する
0.5 * 2 = 1.0 (整数部分が1)

結果が1.0になるまで小数部分を2倍していきます。
最終的には、計算結果の整数部分のみを並べた結果が、小数を2進数に変換した結果となります。
上記であれば、111なので、0.875を2進数小数にすると「0.111」となります。

では、同様の手順で0.1を2進数に変換します。

0.1 * 2 = 0.2 (整数部分が0)
0.2 * 2 = 0.4 (整数部分が0)
0.4 * 2 = 0.8 (整数部分が0)
0.8 * 2 = 1.6 (整数部分が1)
0.6 * 2 = 1.2 (整数部分が1)
0.2 * 2 = 0.4 (整数部分が0)
....

上記の結果から、0.1を2進数に変換した結果は「0.000110011....」と0011が繰りかえすことが分かります。つまり、コンピュータにおける2進数で0.1を表現することができない(メモリが有限のため)ため、ある程度の精度で結果を丸める必要がある、ということです。
小数の結果を丸めることで、小数同士の計算結果に誤差が発生します。

コンピュータにおいては、小数を正確に表現することができない数値が多くあるため、小数同士の計算は多くの場合で誤差が発生します。

10進数小数の計算

先ほどの「0.1+0.2」を正確に計算するためには、decimalモジュールを使います。
decimalモジュールを使うと、10進数小数を正確に計算することが可能です。

from decimal import Decimal

print(Decimal('0.1')+Decimal('0.2')) # 0.3
print(Decimal('0.1')*Decimal('0.2')) # 0.02

これで、小数の計算を正確に行うことができました。
つまり、正確に計算したい場合は、Decimalを使えば万事解決ですね(*^^)v

割り算における罠

上記で万事解決!と書きましたが、実はそうではありません。
例えば、下記の結果を考えてみます。

from decimal import Decimal

print(1/(1/60)) # 60.0

print(Decimal('1') / (Decimal('1')/Decimal('60'))) # 59.99999999999999999999999999

1を(1/60)で割る、つまり60をかける操作を行っているのですが、Decimalを使うと正確に計算できません。これは、(1/60)の部分が割り切れないため、計算結果が途中で丸められ、誤差が発生するからです。

そこで、fractionsモジュールを使います。
Fractionの計算では割り算や小数を分数として扱います。
例えば、Fraction('0.1')は「1/10」として扱われます。

from fractions import Fraction

print(float(Fraction('0.1')+Fraction('0.2'))) # 0.3
print(float(Fraction('1')/(Fraction('1')/Fraction('60')))) # 60.0

Fraction同士の計算であれば、誤差なく計算することが可能です。
ただし、最終的にはfloat型に直すことがほとんどかと思いますので、その際に誤差が発生しますが。。。

最後に

Pythonで数値計算をする際に、誤差なく計算するためには、Fractionを使いましょう。
ただし、整数のみの足し算・引き算の場合は、Fractionを使う必要はありません。
floatの計算→Decimalの計算→Fractionの計算、の順番に計算速度が遅くなります。
なので、計算によってどのモジュールを使うかを決めて頂ければと思います。

  • 誤差を許容できる場合:
    →そのまま計算する

  • 少数ありの数値計算(割り算がない場合):
    →Decimalを使う

  • 割り算がある数値計算:
    →Fractionを使う

少しでも皆さんのお役に立てれば幸いです。
では、(^▽^)

おまけ

float、Decimal、Fractionの計算速度について、簡単に検証してみました。
結果は下記です。

項目 速度
float経過時間 0.13815665245056152
Decimal経過時間 0.20690488815307617
Fraction経過時間 2.2113068103790283

floatが段違いで早く、Decimalもそこそこ早く、Fractionめっちゃ遅い、という結果ですね。。。
検証用のコードは下記になります。

import time
from decimal import Decimal
from fractions import Fraction

t1 = time.time() 
for i in range(2, 80000):
  a = float('{}.{}'.format(i, i)) / float('{}.{}'.format(i-1, i-1))
t2 = time.time()
elapsed_time = t2-t1
print('float経過時間:{}'.format(elapsed_time))

t1 = time.time() 
for i in range(2, 80000):
  a = Decimal('{}.{}'.format(i, i)) / Decimal('{}.{}'.format(i-1, i-1))
t2 = time.time()
elapsed_time = t2-t1
print('Decimal経過時間:{}'.format(elapsed_time))

t1 = time.time() 
for i in range(2, 80000):
  a = Fraction('{}.{}'.format(i, i)) / Fraction('{}.{}'.format(i-1, i-1))
t2 = time.time()
elapsed_time = t2-t1
print('Fraction経過時間:{}'.format(elapsed_time))
52
49
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?