今、話題の人工知能(AI)などで人気のPython。初心者に優しいとか言われていますが、全然優しくない! という事を、つらつら、愚痴っていきます

383.四捨五入(偶数丸め)

»

初回:2024/09/25

 周知の事実ではあるのですが、心では判っていても体が判っていない 計算誤差について、色々と考えてみたいと思います。ちなみに計算機科学的な事や裏どりはしていませんので、いつも通りの現象だけ追いかけたいと思います。

P子「いつも通りの、なんちゃってコラムよね」※1

 事の発端は、Javaで比率(分子/分母)計算する際の小数点以下の値で、四捨五入がうまく出来ていないという相談を受け、知ったかぶりで『基本、学校で習った四捨五入じゃなくって、偶数丸め...またの名を銀行丸めされてるんとちゃう?』と説明して、一旦は理解してもらったのですが、『さっき教えてもらった値とも、ちゃうで』と言われ、『そんな奴、おらへんでー』※2と返してみたものの、出力結果を見せられて調査に乗り出すことになってしまいました。

 ここでは、Javaで調査した結果とともに、Python ではどうなっているかも確認してみました。

1.偶数丸め(銀行丸め)

 まずは、偶数丸め(銀行丸め)について説明しておきたいと思います。

 これは、『JIS Z 8401 規則A』とか、『IEEE 754 roundTiesToEven』とか『ISO 80000-1 附属書 B』とか言われているもので、

  b) 与えられた数値に等しく近い,二つの隣り合う整数倍がある場合には,次のいずれかの規則を用いる。
    1) 規則A 丸めた数値として丸めの幅の偶数倍の方を選ぶ。
      例5 丸めの幅:0.1
          与えられた数値 丸めた数値
          12.25          12.2
          12.35          12.4
  ≪参考資料1≫
  https://kikakurui.com/z8/Z8401-2019-01.html
  数値の丸め方
  日本産業規格 Z 8401:2019

 ≪参考資料2≫
  https://ja.wikipedia.org/wiki/%E7%AB%AF%E6%95%B0%E5%87%A6%E7%90%86#%E5%81%B6%E6%95%B0%E3%81%B8%E3%81%AE%E4%B8%B8%E3%82%81%EF%BC%88round_to_even%EF%BC%89
  端数処理#偶数への丸め(round to even)
  出典: フリー百科事典『ウィキペディア(Wikipedia)』

 学校で習った『四捨五入』は、5の場合は繰り上げ、4の場合は切り捨てという単純な物でした。ところがこの処理を繰り返すと累積エラーが積みあがってくるため、これを統計的に最小化する方法として、両隣りの数字が等距離の場合は偶数側に丸めるという方法が、「銀行方式の丸め」として主に米国で使用されました。

P子「つまり、5の場合、その左が奇数なら偶数に『切り上げ』て、偶数ならそのまま『切り捨て』するってことね」

 ここまでが理屈ですが、これをコンピュータで処理すると、困った事に小数を2進数で『正確に』表すことができないため『計算誤差』が生じてしまいます。

2.Javaでの事例

 整数に丸める場合は、Math.roundメソッドが使えますが、今回は小数点以下で丸めます。テスト的に小数点第二位を第一位までに丸めるプログラムで確認してみます。

 動作確認は、paiza.io というWebサイトを利用させてもらいました。

 ≪参考資料3≫
  https://paiza.io/ja/projects/new
  paiza.IO Online editor and compiler

import java.text.DecimalFormat;

public class Main {
    public static void main(String[] args) throws Exception {
        DecimalFormat decimalFormat1 = new DecimalFormat("#.#");
        for( int i=0; i<5; i++ ) {
            double x = i + 0.55;
            String val0 = decimalFormat1.format(x);
            System.out.printf("%.2f -> %s\n",x,val0);
        }
    }
}

0.55 -> 0.6
1.55 -> 1.6
2.55 -> 2.5
3.55 -> 3.5
4.55 -> 4.5
  結果がおかしいです。

 DecimalFormat ではなく、BigDecimal を使ってみましょう。

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) throws Exception {
        for( int i=0; i<5; i++ ) {
            double x = i + 0.55;
            String val0 = BigDecimal.valueOf(x).setScale(1,RoundingMode.HALF_EVEN).toString();
            System.out.printf("%.2f -> %s\n",x,val0);
        }
    }
}

0.55 -> 0.6
1.55 -> 1.6
2.55 -> 2.6
3.55 -> 3.6
4.55 -> 4.6

 正しく『偶数丸め』されています。

 ちなみに、BigDecimal.valueOf(double) としている箇所を、new BigDecimal(String) でも正常に計算できますが、new BigDecimal(double) とすると、やはり計算結果がおかしくなります。

 ちなみに、フォーマット指定で先ほどの処理を実行すると、

public class Main {
    public static void main(String[] args) throws Exception {
        for( int i=0; i<5; i++ ) {
            double x = i + 0.55;
            System.out.printf("%.2f -> %.1f\n",x,x);
        }
    }
}

0.55 -> 0.6
1.55 -> 1.6
2.55 -> 2.6
3.55 -> 3.6
4.55 -> 4.6
   となり、一見、正しく『偶数丸め』されているように見えますが、
public class Main {
    public static void main(String[] args) throws Exception {
        for( int i=0; i<5; i++ ) {
            double x = i + 0.45;
            System.out.printf("%.2f -> %.1f\n",x,x);
        }
    }
}

0.45 -> 0.5
1.45 -> 1.5
2.45 -> 2.5
3.45 -> 3.5
4.45 -> 4.5

 となり、単純な『四捨五入』の様に見えます。深く調査していませんので、これが『四捨五入』なのか、たまたまなのかよく判りません。

P子「詰めが甘いのよね」

3.Pyhtonでの事例

 やはり『Python』という名称をコラムのタイトルにしている手前、Python でも確認してみたいと思います。

P子「ほとんど、Pythonネタって、ないけどね」

 まずは、round関数を使ってみます。これは規約的には『偶数丸め』になっています。

for i in range(5):
    x = i + 0.55
    val0 = round(x,1)
    print(f'{x} -> {val0}')

0.55 -> 0.6
1.55 -> 1.6
2.55 -> 2.5
3.55 -> 3.5
4.55 -> 4.5

 結果は、Javaと同じく、『四捨五入』でも『偶数丸め』でもありません。

 ちなみに、元の値の桁数を 30桁まで表示してみましょう。

for i in range(5):
    x = i + 0.55
    val0 = round(x,1)
    print(f'{x:.30f} -> {val0}')

0.550000000000000044408920985006 -> 0.6
1.550000000000000044408920985006 -> 1.6
2.549999999999999822364316059975 -> 2.5
3.549999999999999822364316059975 -> 3.5
4.549999999999999822364316059975 -> 4.5

 コンピュータ上の計算では、『四捨五入』でも『偶数丸め』でも正解の値になっています。

P子「Python の float型って、何桁まで正確なの?」

 17桁と言われています。

x = 0.1
y = 1/3
print(f'{x:.70f}\n{y:.70f}')

0.1000000000000000055511151231257827021181583404541015625000000000000000
0.3333333333333333148296162562473909929394721984863281250000000000000000

 もちろん、Pythonにも正確な計算を行う事が出来るモジュールがあります。decimal モジュールの Decimal クラスを使用します。

from decimal import Decimal, ROUND_HALF_EVEN

for i in range(5):
    x = i + 0.55
    val0 = Decimal(str(x)).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN)
    print(f'{x} -> {val0}')

0.55 -> 0.6
1.55 -> 1.6
2.55 -> 2.6
3.55 -> 3.6
4.55 -> 4.6

 ちなみに、Decimal(str(x)) の箇所を、文字列ではなく、Decimal(x) にすると、JavaのBigDecimal と同様に、正確な『偶数丸め』が行われません。

P子「この辺りは Java と同じなのね」

 試しに、f文字列で、小数点1桁にしてみましょう。

for i in range(5):
    x = i + 0.55
    val0 = round(x,1)
    print(f'{x:.1f} -> {val0}')

0.6 -> 0.6
1.6 -> 1.6
2.5 -> 2.5
3.5 -> 3.5
4.5 -> 4.5
 round の処理結果と同じになりました。

P子「あれ? Javaの printf メソッドの時は『四捨五入』になってたのに、異なってるわね」

 言語ごとの特性というか、内部処理の違いから来ているのでしょう。

4.まとめ

 コンピュータが内部表現に 2進数を使っている関係上、小数点をきちんと扱えない事は理解しているつもりですが、画面上の表示とか何となく多くの小数点以下の桁数が表示される関係で小数点以下、1桁や2桁程度なら正確に計算してくれていると思い込んでしまいがちですが、すべて錯覚です。

 今現在、Java やPython で銀行関係や宇宙にロケットを飛ばすような計算処理を行うプログラムは作っていませんが、『計算結果がおかしい』という疑念を持たれて、システムそのものにバグがある印象を持たれるので、この手の些細な処理結果も、きちんと説明できる(または、事前に了承を頂いておく)必要があると思います。

P子「データをEXCEL で抜いて再計算して、合わないって言われるものね」

 EXCLEでの『偶数丸め』
  =if(mod(abs(A1)*10^桁数,1)=0.5,even(abs(A1)*10^桁数-0.5)/10^桁数*sign(A1),round(A1,桁数))

 EXCLEでの『四捨五入』
  =round(A1,桁数)

 桁数は、小数点第一位で丸める場合は"1"、整数で丸める場合は"0"を指定します。

 ≪参考資料4≫
  https://mbp-japan.com/tokyo/sokutei/column/5150639/
  数値の丸めについて
  浦山英樹
  2023年12月13日

 ちなみに、私はすべて整数で計算してから、必要であれば最後に小数に変換するようにしています。

 ほな、さいなら

======= <<注釈>>=======

※1 P子「いつも通りの、なんちゃってコラムよね」
 P子とは、私があこがれているツンデレPythonの仮想女性の心の声です。

※2 『そんな奴、おらへんでー』
 大木こだま・ひびき の こだま の持ちネタ
 https://ja.wikipedia.org/wiki/%E5%A4%A7%E6%9C%A8%E3%81%93%E3%81%A0%E3%81%BE%E3%83%BB%E3%81%B2%E3%81%B3%E3%81%8D
 出典: フリー百科事典『ウィキペディア(Wikipedia)』

Comment(4)

コメント

user-key.

浮動小数点の丸め処理は(2進数側で行われている為)IEEE754を見ながら、2進数で考えないと思った様に丸めることは至難の業と思います。
私も昔引っかかったことがあるので、同様にすべて整数で計算してから、必要であれば最後に小数とするか、最初からDecimalを使っています。

ちゃとらん

user-key. さん、コメントありがとうございます。


浮動小数点の計算は、何気なく処理してしまいがちですが、0.1 ですが誤差を持っているので気を付けないと痛い目を見ます。

しかも通常の計算においては誤差を含んでいても結果は一致していることも多いのでついつい忘れてしまいますが、常に注意深く処理するべきでしょう。


これらをチーム全員の開発者に徹底させることは不可能なので、コーディング規約などで、小数は使わず整数で計算した後で最後に変換するなどと決めておくのが良いのかもしれません。

user-key.

浮動小数点数で意外と気を付けないといけないのは「IF」でうっかり「==」を使うと計算の誤差の関係で見た目は同じでも結構下の桁で誤差があって結果trueにならない事が忘れがち。
ま、十進0.1を二進浮動小数点数に変換すると循環数字ですから、十進⇔二進変換している時点で誤差が出て当然なんですが。。。

ちゃとらん

user-key. さん、コメントありがとうございます。

> 「IF」でうっかり「==」を使うと
ああ、昔、そういうのを習った気がします。


for とか、while とかで、減算しながら0で終了する場合も、x == 0 .0 とかではなく、x # 整数の場合でも、== 判定は極力使いません。


コンピュータも身近ですし、ほとんどのケースでうまく処理してくれるので、なかなか気づきにくいですね。

コメントを投稿する