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で四捨五入による丸めを行うには、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
公式ドキュメント
まとめ
numpy
で書かれたコードを書き直す場合は、端数処理に気を付ける必要がある。
参考
偶数丸めの解説がわかりやすかった。
四捨五入による丸めを行うdecimal
パッケージの解説。
numpy.round()
とPythonの組み込み関数round()
との違いについての解説。
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