小数計算の誤差とDecimal関数による対応

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

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

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

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

浮動小数点型の不思議

浮動小数点型で定義した小数を含む数値は、簡単な演算においても微妙な誤差が生じます。

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

誤差の発生

小数0.1を2回までは良いですが、3回足し合わせると細かいところで誤差が生じます。

少数の足し算
  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}')
  4. print(f'{1015.2:.60f}')

0.100000000000000005551115123125782702118158340454101562500000
0.300000000000000044408920985006261616945266723632812500000000
0.299999999999999988897769753748434595763683319091796875000000
1015.200000000000045474735088646411895751953125000000000000000000
1. 0.1フォーマット済み文字列リテラルを使い小数点以下60桁まで表示すると、第18位から誤差が生じ、小数点第55位まで計算され、それ以降は切り捨てられます。
2. 0.1を3回足した場合、小数点第17位から誤差が生じ、小数点第52位まで計算され、それ以降は切り捨てられます。
3. 0.3の場合、計算結果はこれより少し小さくなり小数点第54位まで計算されます。
4. 1015.2のような整数分部と小数部分のある数値については、整数部分4桁と小数部分14桁、計18桁まで正しく表示されます。また、曲がりなりにも表示されるのは小数部分が43桁までであり、整数部分と合わせて47桁になります。

これらのことから、正しくされるのは整数部分も含め17桁程度、曲がりなりにも表示されるのは50桁程度になります。

浮動小数点方式の割り算における誤差

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

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

0.12345679012345678
1.2345679012345678
0.123456790123456783270228243054589256644248962402343750000000
1. 10/81のような割り算で、小数点第17位まで計算しても割り切れない場合は、それ以降の表示は省略されます。なお、10/81は12345679の8桁が無限に循環するので、最後の桁は9になるはずで、微妙な誤差が生じていることがわかります。
2. 上記の計算の分子を10倍して計算すると、整数1桁+小数点以下16位までの表示になります。表示できる桁数は、小数点以下の桁数ではなく、初めに0以外の数字(ここでは1)が現れてから何桁表示されるかということになります。このような、整数部分と小数部分を合わせた桁数を有効桁数といいます。100/81の場合、有効桁数は17ということになります。
3. 1.と同じ計算の結果を、フォーマット済み文字列リテラルで小数点第60位まで出力しています。ここでも、微妙な誤差がありつつも有効桁数は17桁にとどまり、小数点第54位以下は切り捨てられてしまいます。

このように、整数同士の割り算においても有効桁数は17桁程度であり、曲がりなりにも表示されるのは53桁程度になります。

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

Decimal関数による少数の定義

浮動小数点方式では、有効桁数は17桁程度であり、それ以降は表示されてもあまり意味のない数値であることがわかりました。実務的に問題になることは余り考えられませんが、整数論の計算など本当に正確に計算する必要があるときには、decimalモジュールのDecimal関数を使う必要があります。Decimal関数を使うと学校で習ったような正確な計算が可能になります。

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

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

0.1000000000000000055511151231257827021181583404541015625
0.1
<class 'decimal.Decimal'> <class 'decimal.Decimal'>
0.3000000000000000444089209850062616169452667236328125
0.3
0.3
True
1015.2 1015.200000000000045474735088646411895751953125

1. Decimal関数を使うためには、decimalモジュールからDecimal関数をimportします。
2. Decimal関数の引数で単純に小数を含む数値を渡すと、浮動小数点方式と同じ誤差を含んだ数値として認識されます。
3. Decimal関数の引数の中でクォート('...')を付けて数値を指定すると、誤差のない小数として認識されます。
4. 2.3.のようにDecimal関数で小数を指定すると、Decimal型として認識されます。
5. Decimal関数の引数の中で計算した値についても、誤差を含んだ数値となります。
6. Decimal関数で1つ1つ値を指定するとそれぞれの値が正しく認識されるので、正確な演算が可能になります。
7. Decimal関数を使うと、演算結果の比較も正しく行うことができます。
8. 整数部分と小数部分がある数値についても、クォート('...')を付けて数値を指定すると、誤差のない小数として認識されます。

Decimal関数を使った割り算

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

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

0.1234567901234567901234567901
1.234567901234567901234567901
0.1234567901234567901234567901
<class 'float'> <class 'decimal.Decimal'>
0.1234567901234567901234567901
0.1234567901234567888543403925
2. Decimal関数を使って整数同士の割り算をすると、小数点以下第28位まで正しく表示され、それ以降は切り捨てられます。
3. 割り算の計算結果に整数が1桁含まれる場合は、小数点以下第27位までの表示になります。このことからDecimal関数の場合、特に指定しなければ有効桁数は28桁になることがわかります。
4. 整数同士であれば、クォート('...')無しでDecimal関数を適用しても割り算の結果は正しく表示されます。
5. 上記のことは、単純に割り算をすると計算結果はfloat型になりその特質から誤差が生じるのに対し、Decimal関数で指定した数値同士の割り算は計算結果がDecimal型になり、誤差が生じないためです。
6. 小数を含む数値がある割り算の場合、クォート('...')付きでDecimal関数を適用することで正しく計算することができます。
7. Decimal関数でクォート('...')無しで定義した小数を含む数値による割り算では、それぞれの小数が正しく認識されないので、計算結果にも誤差が生じてしまい、正しく計算されるのは17桁程度にとどまります。

このように、Decimal関数で割り算をする数値が全て整数であれば、クォート('...')付にする必要はありません。そうでない場合はクォート('...')を付きで指定する方が無難です。

Decimal関数の有効桁数を増やす

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

Decimal関数で有効桁数を増やす
  1. from decimal import Decimal,getcontext
  2. getcontext().prec = 50
  3. print(Decimal(10) / Decimal(81))
  4. print(Decimal('100') / Decimal('0.81'))
  5. print(f'{Decimal(10) / Decimal(81):.60f}')

0.12345679012345679012345679012345679012345679012346
123.45679012345679012345679012345679012345679012346
0.123456790123456790123456790123456790123456790123460000000000

2. decimalモジュールのgetcontext().precで桁数を指定すると、有効桁数を変更することができます。ここでは有効桁数を50桁としています。
3. Decimal関数同士で割り算をすると計算結果はDecimal型となり、2.で設定した有効桁数が適用されます。ここでの結果は整数部分が0なので、小数点以下50桁まで正しく計算されます。ただし、最後の1桁で微妙な誤差が発生します。
4. 計算結果に整数部分があるときは、整数部分1桁と小数部分49桁を合計した50桁まで正しく計算されます。この場合も最後の1桁で微妙な誤差が発生します。
5. 計算結果にフォーマット済み文字列リテラルを使い表示桁数を増やしても、有効桁数である50桁を超える部分は0と表示されてしまいます。

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

変数には小数が代入されている場合、この変数に対してDecimal関数とstr関数を適用すると正確な数値演算をすることができます。

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. 1.で浮動小数点型の小数を代入した変数に対し、Decimal関数を適用することができます。この場合、浮動小数点型にともなう誤差を含んだ値になります。
3. Decimal関数にクォート('...')付きで適用するのと同様に、変数にstr関数を適用することにより、正確に小数を含む数値を定義することができます。
7. 浮動小数点型で定義したそれぞれの変数に対し、str関数とDecimal関数を組み合わせて適用することで、正確な演算をすることが可能になります。