from IPython import InteractiveShell
= 'all'
InteractiveShell.ast_node_interactivity
import session_info
TL;DR
Pythonビルトインのデータ型であるfloat
とnumpy.float64
は同じ浮動小数点数でも、 print()
や、round()
に渡した場合の挙動が若干異なる場合がある。
これはnumpy.round()
が、ビルトインのround()
に比べて不正確な丸めを行うためである。
偶数丸め
Pythonのビルトインのround()
は偶数丸めによる端数処理を行う。
二つの倍数が同じだけ近いなら、偶数を選ぶ方に (そのため、例えば round(0.5) と round(-0.5) は両方とも 0 に、 round(1.5) は 2 に) 丸められます。
組み込み関数
Python インタプリタには数多くの関数と型が組み込まれており、いつでも利用できます。それらをここにアルファベット順に挙げます。,,,, 組み込み関数,,, A, abs(), aiter(), all(), anext(), any(), ascii(),, B, bin(), bool(), breakpoint(), bytearray(), bytes(),, C, callabl...
https://docs.python.org/ja/3/library/functions.html#round

偶数丸めとは、四捨五入を行うことで発生するバイアスを相殺する丸め方である。 偶数丸めは四捨五入による端数処理とは異なる結果になることがある。 Pythonで四捨五入による丸めを行うには、decimal
パッケージを用いる。 以下では、[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]のそれぞれの数について、 偶数丸め(to even)と四捨五入(half up)で端数処理した結果を示す。
from decimal import Decimal, ROUND_HALF_UP
# 数のリストを受け取って、偶数丸めと四捨五入の表を表示しつつ、それぞれの合計値を返す関数
def print_rounded(nums):
print("|num|to even|half up|\n|:-:|-:|-:|")
= [0, 0, 0]
sums for num in nums:
= round(num)
rnum = Decimal(str(num))
deci = deci.quantize(Decimal('0'), ROUND_HALF_UP)
rdeci 0] += round(num, 2)
sums[1] += rnum
sums[2] += float(rdeci)
sums[print(f"|{round(num, 2)}|{rnum}|{rdeci}|")
print("\n")
return sums
= [i * 0.1 for i in range(1, 21, 1)]
seq = print_rounded(seq[0:10])
sums1 = print_rounded(seq[10:20]) sums2
num | to even | half up |
---|---|---|
0.1 | 0 | 0 |
0.2 | 0 | 0 |
0.3 | 0 | 0 |
0.4 | 0 | 0 |
0.5 | 0 | 1 |
0.6 | 1 | 1 |
0.7 | 1 | 1 |
0.8 | 1 | 1 |
0.9 | 1 | 1 |
1.0 | 1 | 1 |
num | to even | half up |
---|---|---|
1.1 | 1 | 1 |
1.2 | 1 | 1 |
1.3 | 1 | 1 |
1.4 | 1 | 1 |
1.5 | 2 | 2 |
1.6 | 2 | 2 |
1.7 | 2 | 2 |
1.8 | 2 | 2 |
1.9 | 2 | 2 |
2.0 | 2 | 2 |
端数処理された結果をみると、偶数丸めでは0.5は0に、1.5は2に丸められている。 元の数列と偶数丸め、四捨五入した数列の合計を考えると、 四捨五入した数列では1増加する間に0.5ずつ増加するため、合計値が1多くなる。 一方で、偶数丸めでは、増分が相殺されるため、合計値は元の数列と等しくなる。
sums1
sums2+ j for i, j in zip(sums1, sums2)] [i
[5.5, 5, 6.0]
[15.5, 16, 16.0]
[21.0, 21, 22.0]
numpy.round
での端数処理
では、numpy.float64
に対する丸めはどうなっているのだろうか? ビルトインのround()
にnumpy.float64
を渡すと、numpy.float64.__round__
メソッドを呼び出す。 そこで、 先ほどのコードで四捨五入していたところをnumpy
に変えつつ、 数列を少し小さな値に変更して実行してみる。
import numpy as np
# 数のリストを受け取って、builtinとnumpyで丸めた表を表示する関数
def print_rounded2(nums):
print("|num|built-in|numpy|\n|:-:|-:|-:|")
= [0, 0, 0]
sums for num in nums:
= round(num, 1)
rnum = np.float64(num)
npf = round(npf, 1)
rnpfprint(f"|{round(num, 2)}|{rnum}|{rnpf}|")
print("\n")
= [i * 0.01 for i in range(1, 21, 1)]
seq 0:10])
print_rounded2(seq[10:20]) print_rounded2(seq[
num | built-in | numpy |
---|---|---|
0.01 | 0.0 | 0.0 |
0.02 | 0.0 | 0.0 |
0.03 | 0.0 | 0.0 |
0.04 | 0.0 | 0.0 |
0.05 | 0.1 | 0.0 |
0.06 | 0.1 | 0.1 |
0.07 | 0.1 | 0.1 |
0.08 | 0.1 | 0.1 |
0.09 | 0.1 | 0.1 |
0.1 | 0.1 | 0.1 |
num | built-in | numpy |
---|---|---|
0.11 | 0.1 | 0.1 |
0.12 | 0.1 | 0.1 |
0.13 | 0.1 | 0.1 |
0.14 | 0.1 | 0.1 |
0.15 | 0.1 | 0.2 |
0.16 | 0.2 | 0.2 |
0.17 | 0.2 | 0.2 |
0.18 | 0.2 | 0.2 |
0.19 | 0.2 | 0.2 |
0.2 | 0.2 | 0.2 |
すると、0.05と0.15を丸める時の挙動がbuilt-inと異なっており、 四捨五入とも偶数丸めの挙動とも一致しないことがわかる。 これは、numpy.round
が高速だが少し不正確なアルゴリズムを使っているためらしい。
np.round
uses a fast but sometimes inexact algorithm to round floating-point datatypes.
numpy.round
公式ドキュメント
https://numpy.org/doc/stable/reference/generated/numpy.round.html
まとめ
numpy
で書かれたコードを書き直す場合は、端数処理に気を付ける必要がある。
参考
偶数丸めの解説がわかりやすかった。
偶数丸めとは? - フォーエム株式会社
既存システムのリプレイスをするにあたって、設計書に「偶数丸めをする」と記載がありました。 偶数丸めについて「端数が5の場合は偶数のほうに丸める」と説明されることが多いですが、 四捨五入と何が違うのでしょうか。
https://web.fourm.co.jp/blog20221207/

四捨五入による丸めを行うdecimal
パッケージの解説。
Pythonで小数・整数を四捨五入するroundとDecimal.quantize | note.nkmk.me
Pythonで数値(浮動小数点数floatや整数int)を四捨五入や偶数への丸めで丸める方法について説明する。 組み込み関数round()は一般的な四捨五入ではなく偶数への丸めなので注意。一般的な四捨五入を実現するには標準 ...
https://note.nkmk.me/python-round-decimal-quantize/#_3

numpy.round()
とPythonの組み込み関数round()
との違いについての解説。
NumPy配列ndarrayを四捨五入・偶数丸めするnp.round | note.nkmk.me
NumPy配列ndarrayの要素の値を任意の桁で丸めるにはnp.round()を使う。一般的な四捨五入ではなく偶数への丸めで、0.5が0.0に丸められたりするので注意。 numpy.round — NumPy v1.26 Manual 本記事では、一般的な四 ...
https://note.nkmk.me/python-numpy-round/#nproundpythonround

Sessioninfo
session_info.show()
Click to view session information
----- numpy 2.0.1 session_info 1.0.0 -----
Click to view modules imported as dependencies
PIL 10.4.0 anyio NA appnope 0.1.4 arrow 1.3.0 asttokens NA attr 23.2.0 attrs 23.2.0 babel 2.15.0 certifi 2024.07.04 cffi 1.16.0 charset_normalizer 3.3.2 comm 0.2.2 cycler 0.12.1 cython_runtime NA dateutil 2.9.0.post0 debugpy 1.8.2 decorator 5.1.1 defusedxml 0.7.1 executing 2.0.1 fastjsonschema NA fqdn NA idna 3.7 ipykernel 6.29.5 isoduration NA jedi 0.19.1 jinja2 3.1.4 json5 0.9.25 jsonpointer 3.0.0 jsonschema 4.23.0 jsonschema_specifications NA jupyter_events 0.10.0 jupyter_server 2.14.2 jupyterlab_server 2.27.3 kiwisolver 1.4.5 markupsafe 2.1.5 matplotlib 3.9.1 matplotlib_inline 0.1.7 mpl_toolkits NA nbformat 5.10.4 overrides NA packaging 24.1 parso 0.8.4 platformdirs 4.2.2 prometheus_client NA prompt_toolkit 3.0.47 psutil 6.0.0 pure_eval 0.2.3 pydev_ipython NA pydevconsole NA pydevd 2.9.5 pydevd_file_utils NA pydevd_plugins NA pydevd_tracing NA pygments 2.18.0 pyparsing 3.1.2 pythonjsonlogger NA referencing NA requests 2.32.3 rfc3339_validator 0.1.4 rfc3986_validator 0.1.1 rpds NA send2trash NA six 1.16.0 sniffio 1.3.1 stack_data 0.6.3 tornado 6.4.1 traitlets 5.14.3 uri_template NA urllib3 2.2.2 wcwidth 0.2.13 webcolors 24.6.0 websocket 1.8.0 yaml 6.0.1 zmq 26.0.3
----- IPython 8.26.0 jupyter_client 8.6.2 jupyter_core 5.7.2 jupyterlab 4.2.4 notebook 7.2.1 ----- Python 3.12.2 (main, Feb 25 2024, 03:55:42) [Clang 17.0.6 ] macOS-13.1-arm64-arm-64bit ----- Session information updated at 2024-09-23 19:06