複雑な計算をするためにコンピュータを使うというのが、この機械が開発された理由の一つです。整数の足し算、引き算、掛け算だけであれば、問題ない。しかし、浮動小数点や分数、割り算などが出てくると、非常に複雑な計算になってしまう。
一般ユーザーである私たちは、このような裏側の問題を十分に理解していないため、計算結果が意外なものになったり、不正確なものになったりすることがあります。開発者としては、コンピュータに正しい動作を指示するために、適切な対策を講じなければならない。
日常生活では、10を基準とした10進法が使われています。コンピュータは2を基本とする2進法を用いており、内部では1と0の並びで値を記憶し、処理している。私たちが扱う値は、この2つの表現の間で常に変換される必要があるのです。Pythonのドキュメントで説明されている通りです。
10進数の分数のほとんどは、2進数の分数として正確に表現することができません。その結果、一般に、入力された10進浮動小数点数は、実際にマシンに格納されている2進浮動小数点数によってのみ近似されることになります。
この動作は、ここに示すように、単純な足し算において驚くべき結果を招きます。
リスト1:浮動小数点数による不正確な処理
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
ここでわかるように、出力は不正確で、0.9という結果になるはずです。
リスト2は、小数点以下17桁の浮動小数点数をフォーマットする場合の類似のケースを示しています。
リスト2:浮動小数点数の書式設定
>>> format(0.1, '.17f')
'0.10000000000000001'
上記の例からお分かりのように、浮動小数点数の扱いは少し厄介で、正しい結果を得るため、そして計算エラーを最小限に抑えるために、さらなる工夫が必要です。値を丸めることで、少なくともいくつかの問題を解決することができます。その1つが、組み込みの round()
関数です (使い方の詳細については、以下を参照してください)。
リスト3: 丸められた値で計算する
>>> s = 0.3 + 0.3 + 0.3
>>> s
0.8999999999999999
>>> s == 0.9
False
>>> round(0.9, 1) == 0.9
True
別の方法として、math モジュールを使って、丸められた不正確な浮動小数点数ではなく、2 つの値 (分子と分母) として保存された分数を明示的に扱うこともできます。
このように値を格納するために、decimalとfractionという2つのPythonモジュールが登場します(以下の例を参照)。その前に、「丸め」という言葉をもう少し詳しく見てみましょう。
丸めとは何ですか?
四捨五入の処理を一言で言うと、以下のような意味です。
gt; …元の数値とほぼ等しいが、より短く、より単純で、より明確な表現を持つ別の数値で[値]を置き換えること。
ということです。
出典: https://en.wikipedia.org/wiki/Rounding
基本的には、正確に計算された値を短くすることで、不正確さを付加するものです。多くの場合、小数点以下の桁を削除することによって行われ、例えば、3.73から3.7、16.67から16.7、999.95から1000のようになります。
このような桁下げは、値を保存する際の省スペース化、あるいは単に使用しない桁を削除するためなど、いくつかの理由がある。さらに、アナログディスプレイや時計などの出力デバイスは、計算された値を限られた精度でしか表示できないため、調整された入力データを必要とします。
一般に、丸めには2つの単純なルールがあります。0から4までの数字は切り捨てられ、5から9までの数字は切り上げられる。下の表は、いくつかの使用例を示しています。
| original value | rounded to | result |
|----------------|--------------|--------|
| 226 | the ten | 230 |
| 226 | the hundred | 200 |
| 274 | the hundred | 300 |
| 946 | the thousand | 1,000 |
| 1,024 | the thousand | 1,000 |
| 10h45m50s | the minute | 10h45m |
丸め方
数学者は丸め問題を解決するために、さまざまな丸め方法を開発してきた。単純な切り捨て、切り上げ、切り下げ、半値切り上げ、半値切り下げ、ゼロからの半値切り捨て、偶数への半値切り捨てなどである。
例えば、欧州経済金融委員会では、通貨をユーロに変換する際にゼロから半分に丸めることを適用しています。スウェーデン、オランダ、ニュージーランド、南アフリカなどでは、「現金丸め」「1円丸め」「スウェーデン式丸め」と呼ばれるルールが採用されています。
現金丸め】は、会計の最小単位が通貨の最小物理的額面より小さい場合に発生します。現金取引の支払額は、利用可能な最小通貨単位の最も近い倍数に丸められますが、他の方法で支払われる取引は丸められません。
出典:https://en.wikipedia.org/wiki/Cash_rounding
南アフリカでは、2002年以降、現金の四捨五入は5セント単位で行われるようになりました。一般に、このような丸めは電子的な現金以外の支払いには適用されません。
一方、Python、Numpy、Pandasでは、半分を偶数に丸めるのがデフォルトで、すでに述べた組み込みの round()
関数で使われています。これは丸め法のカテゴリに属し、収束丸め、統計家丸め、オランダ丸め、ガウス丸め、奇偶丸め、バンカー丸めなどとも呼ばれる。この方法はIEEE754で定義されており、「x
の小数部が0.5であれば、y
はx
に最も近い偶数整数となる」ように動作します。これは、「データセット内の同値が切り捨てられる確率と切り上げられる確率は等しい」と仮定しており、実際には通常そうなっている。完全に完璧というわけではないが、この方法は重要な結果をもたらす。
下の表は、この方法による実際の丸めの例である。
| original value | rounded to |
|----------------|------------|
| 23.3 | 23 |
| 23.5 | 24 |
| 24.0 | 24 |
| 24.5 | 24 |
| 24.8 | 25 |
| 25.5 | 26 |
Python 関数
Pythonには組み込みの関数 round()
があり、この関数はこのような場合に非常に便利です。これは2つのパラメータを受け取ります – 元の値と、小数点以下の桁数です。以下のリストは、小数点以下が1桁、2桁、4桁の場合のメソッドの使用法を示しています。
リスト4:指定した桁数で丸める
>>> round(15.45625, 1)
15.5
>>> round(15.45625, 2)
15.46
>>> round(15.45625, 4)
15.4563
この関数を第2パラメーターなしで呼び出した場合、値は完全な整数値に丸められます。
リスト5:桁数を指定しない四捨五入
>>> round(0.85)
1
>>> round(0.25)
0
>>> round(1.5)
2
丸められた値は、絶対的に正確な結果を必要としない場合にはうまく機能します。しかし、丸められた値を比較するのは悪夢であることに注意してください。次の例では、丸め前と丸め後の値を比較することで、より明らかになります。
リスト6の最初の計算には丸め前の値が含まれており、値を合計する前に丸めることが記述されています。2つ目の計算には丸め後のまとめが含まれており、集計後の丸めを意味しています。比較の結果が異なることにお気づきでしょう。
リスト6:丸め前と丸め後の比較
>>> round(0.3, 10) + round(0.3, 10) + round(0.3, 10) == round(0.9, 10)
False
>>> round(0.3 + 0.3 + 0.3, 10) == round(0.9, 10)
True
浮動小数点演算のためのPythonモジュール
浮動小数点数を適切に扱うために、4つの人気のあるモジュールがあります。これには math
モジュール、 Numpy
モジュール、 decimal
モジュール、そして fractions
モジュールが含まれます。
mathモジュールは数学定数、浮動小数点演算、三角法などを中心に扱っています。Numpy
モジュールは自らを “the fundamental package for scientific computing” と表現しており、様々な配列メソッドを備えていることで有名である。decimalモジュールは 10 進固定小数点演算と浮動小数点演算をカバーし、
fractions` モジュールは特に有理数を扱います。
まず、リスト1の計算を改良してみましょう。リスト7が示すように、math
モジュールをインポートした後、浮動小数点数のリストを受け取るメソッド fsum()
にアクセスすることができます。最初の計算では、組み込みの sum()
メソッドと math
モジュールの fsum()
メソッドの間に違いはありませんが、2番目の計算では違いがあり、期待する正しい結果を返します。精度は、基礎となるIEEE 754アルゴリズムに依存します。
リスト 7: math
モジュールの助けを借りた浮動小数点演算
>>> import math
>>> sum([0.1, 0.1, 0.1])
0.30000000000000004
>>> math.fsum([0.1, 0.1, 0.1])
0.30000000000000004
>>> sum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
0.9999999999999999
>>> math.fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
1.0
次に、 Numpy
モジュールを見てみましょう。このモジュールには、配列として提供された値を丸めるaround()メソッドが付属しています。これは、デフォルトの round()
メソッドと同じように、1つの値を処理します。
値を比較するために、 Numpy
は equal()
メソッドを提供します。round()`と同様に、単一の値だけでなく、処理する値のリスト(いわゆるベクター)も受け取ることができます。リスト8は、丸められた値だけでなく、単一の値に対する比較も示しています。観察された動作は、前に示したメソッドと非常によく似ています。
リスト8: Numpy
モジュールのequalメソッドを使った値の比較
>>> import numpy
>>> print (numpy.equal(0.3, 0.3))
True
>>> print (numpy.equal(0.3 + 0.3 + 0.3 , 0.9))
False
>>> print (numpy.equal(round(0.3 + 0.3 + 0.3) , round(0.9)))
True
オプション3は decimal
モジュールです。これは正確な10進数表現を提供し、有効桁を保持します。デフォルトの精度は28桁で、この値はあなたの問題に必要なだけの大きさに変更することができます。リスト9は、8桁の精度を使用する方法を示しています。
リスト9: decimal
モジュールを使って10進数を作成する
>>> import decimal
>>> decimal.getcontext().prec = 8
>>> a = decimal.Decimal(1)
>>> b = decimal.Decimal(7)
>>> a / b
Decimal('0.14285714')
これで、float 値の比較が非常に簡単になり、求めていた結果にたどり着きました。
リスト10: decimal
モジュールを使った比較計算
>>> import decimal
>>> decimal.getcontext().prec = 1
>>> a = decimal.Decimal(0.3)
>>> b = decimal.Decimal(0.3)
>>> c = decimal.Decimal(0.3)
>>> a + b + c
Decimal('0.9')
>>> a + b + c == decimal.Decimal('0.9')
True
decimalモジュールには、値を丸めるためのメソッドである quantize() も用意されています。デフォルトの丸め方は半分を偶数に丸めるように設定されていますが、必要に応じて他の方法に変更することも可能です。リスト11は
quantize()` メソッドの使用方法を示しています。桁数は10進数の値をパラメーターとして指定していることに注意してください。
リスト11: quantize()
を使って値を丸める
>>> d = decimal.Decimal(4.6187)
>>> d.quantize(decimal.Decimal("1.00"))
Decimal('4.62')
最後に、fractions
モジュールを見てみましょう。このモジュールでは、浮動小数点値を分数として扱うことができます。例えば、 0.3
を 3/10 として扱うことができます。これにより、浮動小数点値の比較が簡単になり、値の丸め誤魔化してしまうことが完全になくなります。リスト12は、fractionsモジュールの使い方を示しています。
リスト 12: 分数としての浮動小数点値の保存と比較
>>> import fractions
>>> fractions.Fraction(4, 10)
Fraction(2, 5)
>>> fractions.Fraction(6, 18)
Fraction(1, 3)
>>> fractions.Fraction(125)
Fraction(125, 1)
>>> a = fractions.Fraction(6, 18)
>>> b = fractions.Fraction(1, 3)
>>> a == b
True
さらに、次の例に示すように、2つのモジュール decimal
と fractions
を組み合わせることができます。
リスト13: 小数と分数の操作
>>> import fractions
>>> import decimal
>>> a = fractions.Fraction(1,10)
>>> b = fractions.Fraction(decimal.Decimal(0.1))
>>> a,b
(Fraction(1, 10), Fraction(3602879701896397, 36028797018963968))
>>> a == b
False
結論
浮動小数点値を正しく格納し、処理することはちょっとしたミッションであり、プログラマーにとって多くの注意が必要です。値を丸めることで解決する場合もありますが、丸める順番や方法が正しいかどうかは必ず確認してください。これは金融ソフトのようなものを開発するときに最も重要なことで、四捨五入のローカル法のルールを確認する必要があります。
Pythonは必要なツールをすべて提供してくれますし、「電池込み」で提供されます。ハッキングを楽しんでください。
謝辞
本論文の作成にあたり,批判的なコメントをいただいたZoleka Hofmann氏に感謝します。