Pythonの小数の計算の問題点と対応

Pythonで数値を扱う変数の型の1つとして、浮動小数点型があります。浮動小数点型の変数は、整数だけではなく小数点以下の数字を扱うことができます。浮動小数点型の考え方は非常に複雑で一筋縄ではいかないところがあります。そこで、少しずつその正体に迫っていきます。

浮動小数点型

浮動小数点型の変数の定義

浮動小数点型の定義
  1. xi = 2
  2. xf = 2.0
  3. print(xi, type(xi))
  4. print(xf, type(xf))

2 
2.0 
1. 整数を変数に代入すると、3.のように整数型(int)で定義されます。
2. 2.0のように小数点を付けて変数に代入すると、4.のように浮動小数点型(float)で定義されます。

浮動小数点型の不思議

浮動小数点型の数値に対し、簡単な演算をするとおかしなことが起こります。

小数点第17位付近から誤差が発生

誤差の発生

小数0.1を2回までは良いですが、3回足し合わせると小数点第17位付近から誤差が発生します。

少数の足し算
  1. print(0.1)
  2. print(0.1+0.1)
  3. print(0.1+0.1+0.1)
  4. print(0.3)
  5. print(0.1+0.1+0.1 == 0.3)

0.1
0.2
0.30000000000000004
0.3
False
3. 0.1を3回足すと0.30000000000000004と小数点第17位のところで誤差が発生します。
4. 単純に0.3を表示すると正しく表示されます。
5. 上記のことから0.1を3回足した値と0.3は等しくなりません。
このように、浮動小数点型の演算において、極めて小さな値ですが誤差が生じます。ここで17という数値には大きな意味があります。

表示桁数の変更

状況を調べるため、小数点第60位までフォーマット済み文字列リテラルで出力します。

Pythonの小数点以下の表示桁数を変更する
  1. print(f'{0.1:.60f}')
  2. print(f'{0.1+0.1+0.1:.60f}')
  3. print(f'{0.3:.60f}')

0.100000000000000005551115123125782702118158340454101562500000
0.300000000000000044408920985006261616945266723632812500000000
0.299999999999999988897769753748434595763683319091796875000000
2. 0.1を単純にprint関数で出力すると、小数点第18位から誤差が出だし、小数点第55位まで計算がされ、それ以降は切り捨てられます。
3. 0.1を3回足した場合、小数点第17位から誤差が出だし、小数点第52位まで計算がされて、それ以降は切り捨てられます。
4. 0.3の場合、0.3よりも小さくなり、小数点第54位まで計算されます。

大きな数字の表示

大きな数値に整数を足していく

浮動小数点方式において、$2^53$のような大きな数値になると細かい部分で誤差が発生します。

$2^{53}-1$付近の表示
  1. f53 = 2**53-1 # 9007199254740991
  2. for i in range(-5, 0):
  3. print(f'2^53-1{i:>3}={f53+float(i):>20.1f}')
  4. for i in range(10):
  5. print(f'2^53-1+{i:>2}={f53+float(i):>20.1f}')

2^53-1 -5=  9007199254740986.0
2^53-1 -4=  9007199254740987.0
2^53-1 -3=  9007199254740988.0
2^53-1 -2=  9007199254740989.0
2^53-1 -1=  9007199254740990.0
2^53-1+ 0=  9007199254740991.0
2^53-1+ 1=  9007199254740992.0
2^53-1+ 2=  9007199254740992.0
2^53-1+ 3=  9007199254740994.0
2^53-1+ 4=  9007199254740996.0
2^53-1+ 5=  9007199254740996.0
2^53-1+ 6=  9007199254740996.0
2^53-1+ 7=  9007199254740998.0
2^53-1+ 8=  9007199254741000.0
2^53-1+ 9=  9007199254741000.0

数値の値が$2^{53}-1=9007199254740991.0$を超えると、1から順次、足し合わせていっても、1の位は2ずつしか増えません。しかもその増え方も2,2,4,6,6,6,8というように不規則になります。

大きな数値に小数を足していく

$2^{53}-1$に1以下の小数を足し合わせていっても、誤差が生じます。

$2^{53}-1$付近の整数の表示
  1. print(f53+0.1)
  2. print(f53+0.2)
  3. print(f53+0.3)
  4. print(f53+0.4)
  5. print(f53+0.5)
  6. print(f53+0.6)

9007199254740991.0
9007199254740991.0
9007199254740991.0
9007199254740991.0
9007199254740992.0
9007199254740992.0

$2^{53}-1=9007199254740991.0$のような大きな数値になると、小数点以下の数値を足していっても切り捨てられて表示されません。なぜなら、計算できる桁数に限りがあるので、影響の少ない桁の部分は切り捨てられてしまうためです。もっとも$2^{53}$は約9007兆1992億なので、特殊な使用法をすることを除き問題になることはまずありません。

さらに大きな数字の表示

$10^{16}$を超えるような数値を表示すると、表示方法が省略されたものになります。

10^{16}を超える場合
  1. print(9999999999999998.0)
  2. print(9999999999999999.0)
  3. print(12345678901234567.8)

9999999999999998.0
1e+16
1.2345678901234568e+16
1. 9999999999999998.0までの数値までは、誤差があるものの省略されずに表示されます。
2. 9999999999999998.0に1を足すと1e+16という表示になります。これは9999999999999999.0が10000000000000000とみなされ、これ以上の数値になるとみづらくなるためです。1e+16は$1×10^{16}$を表します。このような表示方法は指数表記(Exponential Notation)といい、小文字のeはExponentialを意味します。
3. 例として12345678901234567.8のような大きな数値になると、$1.2345678901234568×10^{16}$のように省略されます。
実際には$1×10^{16}$は1京(きょう:兆の1万倍)というとてつもなく大きな値になるので、実務上はますます問題になることは少ないと思われます。

小数点以下の表示桁数

浮動小数点方式においては、小数点以下の計算においても誤差が生じたりある桁数より小さな部分は切り捨てられます。

割り算で割り切れない場合の表示
  1. print(10/81)
  2. print(100/81)
  3. print(f'{1/81:.60f}')

0.12345679012345678
1.2345679012345678
0.012345679012345678327022824305458925664424896240234375000000
1. 10/81のような割り算の結果、小数点第17位まで計算しても割り切れない場合は、それ以降の表示は省略されます。
2. 上記の計算の分子を10倍して計算すると、整数1桁+小数点以下16位までの表示になります。このように、表示できる桁数は、小数点以下の桁数ではなく、初めに0以外の数字(ここでは1)が現れてから何桁表示されるかということになります。このような実質的な桁の数を、有効桁数といいます。この場合、有効桁数は17ということになります。
3. 小数点第60位までフォーマット済み文字列リテラルで出力しています。実際には1/81は12345679の桁が無限に循環します。このため小数点第18位以降は結果が正しくないことになります。また、小数点第54位以下は切り捨てられてしまいます。
このように、浮動小数点方式においては、正しく計算できるのは17桁程度であり、まがりなりとも計算できるのは53桁までとなります。

decimalモジュールを使い小数を正しく計算する

Decimal関数による少数の定義

倍精度浮動小数点方式では、正しく計算されるのは17桁程度であり、それ以降は誤差が発生することがわかりました。多くの場合は問題になりませんが、本当に正確に計算するときには、decimalモジュールのDecimal関数を使います。Decimal関数は学校で習ったような計算をしてくれます。

Decimal関数による少数の定義と計算

Decimal関数を使った小数の足し算
  1. from decimal import Decimal
  2. print(Decimal(0.1))
  3. print(type(Decimal(0.1)))
  4. print(Decimal(0.1+0.1+0.1))
  5. print(Decimal('0.3'))
  6. print(Decimal('0.1')+Decimal('0.1')+Decimal('0.1'))
  7. print(Decimal('0.1')+Decimal('0.1')+Decimal('0.1') == Decimal('0.3'))

0.1000000000000000055511151231257827021181583404541015625

0.3000000000000000444089209850062616169452667236328125
0.3
0.3
True

2. Decimal関数の引数で単純に数値を渡すと、浮動小数点方式と同じ誤差を含んだ数値として認識されます。
3. Decimal関数で小数を指定するとDecimal型になります。
4. Decimal関数の引数の中で計算した値についても誤差を含んだ数値となります。
5. Decimal関数の引数の中でクォート('...')を付けて数値を指定します。すると誤差のない小数として認識されます。
7. Decimal関数では1つ1つの値が正しく認識されるので、演算の結果も正しくなります。
8. Decimal関数を使うと、演算結果の比較も正しく行うことができます。

Decimal関数による大きな数値の計算

大きな数値に整数を足していく

前節では$2^{53}-1=9007199254740991.0$より大きな数値を浮動小数点型として定義すると、計算に誤差が発生することを確認しました。そこで同じ数値をDecimal関数で定義し、同じ計算をます。

$2^{53}-1=9007199254740991.0$付近の表示
  1. print(Decimal('9007199254740991'),' ',Decimal('9007199254740991.0'))
  2. d53=Decimal('9007199254740991.0') #9007199254740991
  3. for i in range(-5,0):
  4. print(f'2^53-1{i:>3}={d53+Decimal(str(i)):>30.1f}')
  5. for i in range(10):
  6. print(f'2^53-1+{i:>2}={d53+Decimal(str(i)):>30.1f}')

2^53-1 -5=            9007199254740986.0
2^53-1 -4=            9007199254740987.0
2^53-1 -3=            9007199254740988.0
2^53-1 -2=            9007199254740989.0
2^53-1 -1=            9007199254740990.0
2^53-1+ 0=            9007199254740991.0
2^53-1+ 1=            9007199254740992.0
2^53-1+ 2=            9007199254740993.0
2^53-1+ 3=            9007199254740994.0
2^53-1+ 4=            9007199254740995.0
2^53-1+ 5=            9007199254740996.0
2^53-1+ 6=            9007199254740997.0
2^53-1+ 7=            9007199254740998.0
2^53-1+ 8=            9007199254740999.0
2^53-1+ 9=            9007199254741000.0
1. Decimal関数でクォート('...')を付けて数値を指定し変数に代入すると、引数で指定した値の小数点以下の桁数がそのまま記録されます。
2. 小数点以下の桁数を1として、Decimal関数を使い、$2^{53}-1$を変数d53として定義します。
3. $2^{53}-1$より小さな数値では正しく計算することができます。
5. $2^{53}-1$より大きな数値については、倍精度浮動小数点型で定義した場合には誤差が発生しましたが、Decimal関数で定義すると正しく計算されます。

大きな数値に小数を足していく

Decimal関数を使うと、相当大きな数値であっても小数点以下の数値であっても正しく計算されます。

$2^{53}-1$付近の表示
  1. d53=Decimal(2**53-1) #9007199254740991
  2. print(d53+Decimal('0.1'))
  3. print(d53+Decimal('0.2'))
  4. print(d53+Decimal('0.3'))
  5. print(d53+Decimal('0.4'))
  6. print(d53+Decimal('0.5'))
  7. print(d53+Decimal('0.6'))

9007199254740991.1
9007199254740991.2
9007199254740991.3
9007199254740991.4
9007199254740991.5
9007199254740991.6

9007199254740991.0について小数を足しこんでいく場合も正しく表示されます。

Decimal関数を使った割り算

小数点以下の計算で一番問題になるのは割り算をするときです。そこで、割り算をするときのDecimal関数の使い方を確認します。

割り切れない場合の計算
  1. from decimal import Decimal
  2. print(Decimal('10') / Decimal('81'))
  3. print(Decimal(10) / Decimal(81))
  4. print(Decimal('0.1') / Decimal('0.81'))
  5. print(Decimal(0.1) / Decimal(0.81))

0.1234567901234567901234567901
0.1234567901234567901234567901
0.1234567901234567901234567901
0.1234567901234567888543403925
2. Decimal関数を使って割り算をすると小数点第17位以降も正しく計算され、28桁まで表示することができます。
3. 整数同士であれば、クォート('...')無しでDecimal関数を適用しても割り算の結果は正しく表示されます。
4. 小数を含む割り算の場合にはクォート('...')付きでDecimal関数を適用しないと正しく計算されません。

このため、実際には割り算をする数値が整数であることが確実であればクォート('...')を付について意識する必要はありません。そうでない場合はクォート('...')を付きで指定する方が無難です。

大きな数値の計算でのDecimal関数の使用

Decimalモジュールでの有効桁数と指数表示

Dceimal関数は28桁ということは$10^{28}-1$まで表すことができます。$10^{28}$は1穣(じょう)というとんでもない数字になります。ちなにみに兆の1万倍が京(けい)、垓(がい)、杼(じょ)の次が穣になります。

Decimal 28桁の9の周辺の計算
  1. getcontext().prec = 28
  2. print(Decimal('9999999999999999999999999999'))
  3. print(Decimal('9999999999999999999999999999')-Decimal('1'))
  4. print(Decimal('9999999999999999999999999999')+Decimal('1'))

9999999999999999999999999999
9999999999999999999999999998
1.000000000000000000000000000E+28

このように29桁の数値になると指数表記(Exponential Notation)になります。

Decimalモジュールでの有効桁数と小数

Decimal関数で定義した数値に対し、小数を加減し有効桁数28桁を超えた値になると、少数以下の部分が丸められます。

Decimal関数での27桁の9+小数1桁の計算
  1. print(Decimal('999999999999999999999999999.9'))
  2. print(Decimal('999999999999999999999999999.9')-Decimal('0.1'))
  3. print(Decimal('999999999999999999999999999.9')+Decimal('0.1'))
  4. print(Decimal('999999999999999999999999999.9')+Decimal('0.7'))
  5. print(Decimal('999999999999999999999999999.9')+Decimal('1'))

999999999999999999999999999.9
999999999999999999999999999.8
1000000000000000000000000000
1000000000000000000000000001
1000000000000000000000000001
1. Decimal関数で整数27桁、小数1桁、合計28桁の数値を定義します。
2. 上記の値から0.1を引いても有効桁数の範囲内なので問題なく計算されます。
3. 逆に0.1を加え、有効桁数を超えると小数の部分が切り捨てられます。
4. さらに0.7を加える小数の部分が切り上げられます。4.と比較すると小数部分が加算され有効桁数を超えると小数以下が丸められます。
5. 同様に整数を加えていくと少数の部分が切り捨てられます。

有効桁数を増やす

Decimal型の小数は有効桁数が28桁に設定されており、ほとんどの場合にはこの範囲で問題なく計算することができます。ところがこれを超える桁数や精度を求める場合にはgetcontext()関数のprecを設定することで有効桁数を増やすことができます。

Decimal関数では割り算で割り切れない場合や大きな数値の計算において有効桁数は28桁になります。これよりも高い精度が求められる場合、有効桁数を増やす方法があります。

有効桁数を増やす
  1. from decimal import getcontext
  2. getcontext().prec = 100
  3. print(Decimal(0.1) / Decimal(980.1))
  4. print(Decimal(1) / Decimal(9801))
  5. print(Decimal('0.1') / Decimal('980.1'))

0.0001020304050607080943079403148293932202457801132090495050793265976283802824659262837973162939589960441
0.0001020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505
0.0001020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505

2. decimalモジュールのgetcontext().precで桁数を指定すると、有効桁数を変更することができます。
3. Decimal関数同士で割り算をすると計算結果はDecimal型となり、有効桁数は100桁になります。0.1/980.1の値は0.00010203040506070809・・・のように規則的に数値が並びます。クォート('...')を付けずにDecimal関数で小数を定義して計算すると、正しく計算されるのは有効桁数17桁にとどまります。
4. Decimal関数で整数を定義する場合、クォート('...')を付けなくてもgetcontext().precで指定した桁数まで正しく計算されます。
5.  Decimal関数でクォート('...')を付けて小数を定義すれば、getcontext().precで指定した桁数まで正しく計算されます。

変数にDecimal型のデータを代入する

変数には小数が代入されており、この変数に対してDecimal関数を適用することができます。

Decimal型のデータを変数に代入する
  1. x = 0.1
  2. print(Decimal(x))
  3. print(Decimal(str(y)))
  4. y1 = 0.1
  5. y2 = 0.1
  6. y3 = 0.1
  7. print(Decimal(str(y1))+Decimal(str(y2))+Decimal(str(y3)))

0.1000000000000000055511151231257827021181583404541015625
0.1
0.3

2. 変数に倍精度浮動小数点型の小数が代入されていて、この値に対してDecimal関数を適用することができます。この場合、浮動小数点型にともなう誤差を含んだ値になります。
3. Decimal関数にクォート('...')付きで適用するのと同様に誤差なく計算するときはstr関数を使うことにより、クォート('...')を付けたのと同じ効果が生じます。