Round half away from zero と Round half to even

端数処理の話。

ずっと Python 3 で変更された round の仕様がどこにも詳しく書かれてないと思い込んでたが違ってた。What’s New In Python 3.0 にちゃんと書かれていた。

無知なもんでこれを知るまでいわゆる四捨五入しか知らず、What’s New In Python 3.0 ではじめて half to even の存在を知った次第である。ガッコじゃ習わない、てか義務教育では習わないし、アタクシ、「こんぴゅうたな専門教育」を受けたこともなかったこともあるんだろうけれど、最近では常識だったりするのかしら。

まぁあえて可視化せずとも言葉だけでもわかることではあるんだけどね:

同時には実現出来ないのでこれを Py27 と Py35 で別々に実行
 1 import sysconfig
 2 import pickle
 3 
 4 d = []
 5 j = -3.0
 6 for i in range(13):
 7     d.append((float("%.1f" % j), round(j)))
 8     j += 0.5
 9 pn = "round-{}.pickle".format(sysconfig.get_python_version())
10 pickle.dump(d, open(pn, "wb"), protocol=2)
可視化
 1 import pickle
 2 import matplotlib.pyplot as plt
 3 
 4 d27 = pickle.load(open("round-2.7.pickle", "rb"))
 5 d35 = pickle.load(open("round-3.5.pickle", "rb"))
 6 t = [s for (s, v) in d27]
 7 r = ([v for (s, v) in d27], [v for (s, v) in d35])
 8 p = (
 9     ('g', 'or', "half away from zero"),
10     ('b', 'or', "half to even"),
11 )
12 
13 fig, axes = plt.subplots(nrows=2)
14 for i, ax in enumerate(axes):
15     ax.grid(True)
16     ax.axhline(linewidth=1.5, color="black", linestyle="dotted")
17     ax.axvline(linewidth=1.5, color="black", linestyle="dotted")
18     ax.plot(t, r[i], p[i][0])
19     ax.plot(t, r[i], p[i][1])
20     ax.set_title(p[i][2])
21 
22 plt.savefig("round.png")

Python 本体だけで「Round half away from zero と Round half to even 両方」は出来ないのでプログラム的にやろうとするとちょい面倒ね。

さて。ここまでは「字句通り」というか、定義通りの「そりゃそうだぁ」な話、なんだけれどもね。

実はあえてこんな記事を書こうと思ったのは、WikiPedia の説明がどうしても腑に落ちなかったから。日本語版の場合は最近接偶数への丸めのこんな説明がされている:

端数0.5のデータが有限割合で存在する場合でも、バイアスがないのが特徴であり、多数足し合わせても丸め誤差が特定の側に偏って累積することがない

わかってしまえばなんてことはなかったんだけれど、これの意味がなぜだか全然つかめずに悶々としていた。これ、2つポイントがあるんだけれど、「わからなかった方」は要はこれだけのことなのね:

本文中の※は読んでね
 1 Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32
 2 Type "help", "copyright", "credits" or "license" for more information.
 3 >>> def round_m1(v):
 4 ...     return int(round(v, -1))
 5 ...
 6 >>> original = list(range(5, 100, 10))
 7 >>> rounded = list(map(round_m1, original))
 8 >>> 
 9 >>> print("original     : {}".format(original))
10 original     : [5, 15, 25, 35, 45, 55, 65, 75, 85, 95]
11 >>> print("rounded      : {}".format(rounded))
12 rounded      : [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
13 >>> print("sum(original): {}".format(sum(original)))
14 sum(original): 500
15 >>> print("sum(rounded) : {}".format(sum(rounded)))
16 sum(rounded) : 550
17 >>> 
本文中の※は読んでね
 1 Python 3.5.1 (v3.5.1:37a07cee5969, Dec  6 2015, 01:54:25) [MSC v.1900 64 bit (AMD64)] on win32
 2 Type "help", "copyright", "credits" or "license" for more information.
 3 >>> def round_m1(v):
 4 ...     return int(round(v, -1))
 5 ...
 6 >>> original = list(range(5, 100, 10))
 7 >>> rounded = list(map(round_m1, original))
 8 >>> 
 9 >>> print("original     : {}".format(original))
10 original     : [5, 15, 25, 35, 45, 55, 65, 75, 85, 95]
11 >>> print("rounded      : {}".format(rounded))
12 rounded      : [0, 20, 20, 40, 40, 60, 60, 80, 80, 100]
13 >>> print("sum(original): {}".format(sum(original)))
14 sum(original): 500
15 >>> print("sum(rounded) : {}".format(sum(rounded)))
16 sum(rounded) : 500
17 >>> 

「バイアスがある(偏りがある)」の意味がどうしてもピンと来なかったけれど、WikiPedia 文章の後半部分「多数足し合わせても丸め誤差が特定の側に偏って累積しない」だけに注目してれば多分混乱することはなかったのね。

「2つのポイント」のもう一つは実はあんまし WikiPedia 日本語版ではなんだか伝わりにくくなってる「towards zero」「away from zero」「towards negative infinity」「towards positive infinity」の違いなのね。アタシは「正負でアシンメトリになる」ことをバイアスと言ってるという理解から入ってしまったのな、なぜだか。で、Python 2 の round も Python 3 の round も、どちらも「正負でシンメトリ」なことには変わりはないので…、頭混乱した、てことでした。

検証コードで「丸めたものを加算」していることに注意。これは普通は避けるべき演算ね。良識あるエンジニアには常識だけれども念のため。