numpy.float64の端数処理

Pythonビルトインのデータ型であるfloatとnumpy.float64は同じ浮動小数点数でも挙動が若干異なる場合がある。
numpy
Python
Published

July 25, 2024

Modified

September 23, 2024

TL;DR

Pythonビルトインのデータ型であるfloatnumpy.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 IPython import InteractiveShell
InteractiveShell.ast_node_interactivity = 'all'

import session_info
from decimal import Decimal, ROUND_HALF_UP

# 数のリストを受け取って、偶数丸めと四捨五入の表を表示しつつ、それぞれの合計値を返す関数
def print_rounded(nums):
  print("|num|to even|half up|\n|:-:|-:|-:|")
  sums = [0, 0, 0]
  for num in nums:
    rnum = round(num)
    deci = Decimal(str(num))
    rdeci = deci.quantize(Decimal('0'), ROUND_HALF_UP)
    sums[0] += round(num, 2)
    sums[1] += rnum
    sums[2] += float(rdeci)
    print(f"|{round(num, 2)}|{rnum}|{rdeci}|")
  print("\n")
  return sums

seq = [i * 0.1 for i in range(1, 21, 1)]
sums1 = print_rounded(seq[0:10])
sums2 = print_rounded(seq[10:20])
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
[i + j for i, j in zip(sums1, sums2)]
[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|:-:|-:|-:|")
  sums = [0, 0, 0]
  for num in nums:
    rnum = round(num, 1)
    npf = np.float64(num)
    rnpf= round(npf, 1)
    print(f"|{round(num, 2)}|{rnum}|{rnpf}|")
  print("\n")

seq = [i * 0.01 for i in range(1, 21, 1)]
print_rounded2(seq[0:10])
print_rounded2(seq[10:20])
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