カテゴリー

当サイトはアフィリエイトプログラムによる収益を得ています〈景品表示法に基づく表記です)

Python基礎

【python3&四捨五入】四捨五入すると1.5も2.5も2.0になる問題

2021年9月18日

こんな人にオススメ

python3系で1.5を整数で四捨五入をするともちろん2が計算される。一方で、2.5を同じように整数で四捨五入すると3が計算されるかと思いきや2と計算される。

なんで3じゃないんだ?どうしてだよぉ!

ということで、今回はpyhon3系で四捨五入する際に一般的な四捨五入は異なる挙動を示すことに関して解説する。python2系の場合は以下のように一般的な四捨五入。

# Python 2.7.16
# python2系では今まで習ってきた四捨五入と同じ計算
print(round(1.5))
# 2.0
print(round(2.5))
# 3.0

しかし、python3系になると初めの疑問でも書いたように、2.5の計算結果が3ではなく2のまま。一体なぜなのだ。ちなみに「.5」ではない「.4」などは通常の四捨五入。

# Python 3.9.7
# python3系では1.5でも2.5でも四捨五入の値が2に
print(round(1.5))
# 2
print(round(2.5))
# 2

キーワードは丸め。python環境は以下。

  • Python 3.9.7
  • numpy 1.21.2

運営者のメガネです。YouTubeTwitterInstagramも運営中。自己紹介お問い合わせページあります。

運営者メガネ

作成したコード全文

python3系で四捨五入してみる

# .5がごとに並べた配列
tpl = (-3.0, -2.5, -2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.)

まずは上のように0.5ずつ区切った配列を考える。これを以下の4種類の四捨五入で計算結果を見てみる。

  • 標準搭載のround関数
  • numpyround関数
  • 標準搭載のround関数で整数の桁指定
  • numpyround関数で整数の桁指定
# python3系だと四捨五入の結果が偶数に集中する
for val in tpl:
    r1 = round(val)  # 標準の四捨五入
    r2 = np.round(val)  # numpyの四捨五入
    r3 = round(val, 0)  # 標準の四捨五入で0桁目の四捨五入を指定
    r4 = np.round(val, 0)  # numpyの四捨五入で0桁目の四捨五入を指定

    print(f"{val:4}: {r1:2}, {r2:4}, {r3:4}, {r4:4}")

f-stringの:4printする際に4文字分、文字数を確保しておくということ。マイナス記号があったりなかったりする際に、文字の配置をキレイにしやすい。

この指定をしなくても問題ないけど、見づらくなるのでここで入れている。出力結果は以下。

# -3.0: -3, -3.0, -3.0, -3.0
# -2.5: -2, -2.0, -2.0, -2.0
# -2.0: -2, -2.0, -2.0, -2.0
# -1.5: -2, -2.0, -2.0, -2.0
# -1.0: -1, -1.0, -1.0, -1.0
# -0.5:  0, -0.0, -0.0, -0.0
#  0.0:  0,  0.0,  0.0,  0.0
#  0.5:  0,  0.0,  0.0,  0.0
#  1.0:  1,  1.0,  1.0,  1.0
#  1.5:  2,  2.0,  2.0,  2.0
#  2.0:  2,  2.0,  2.0,  2.0
#  2.5:  2,  2.0,  2.0,  2.0
#  3.0:  3,  3.0,  3.0,  3.0

やはり一般的な四捨五入とは異なる挙動を示している。しかも全ての四捨五入の計算方法で。どうやら-2, 0, 2といった偶数の出力数が多いように感じる。

この偶数に向かうことが今回のキモとなる部分。偶数への丸め。

偶数への丸め(roung to even)

前章の出力で.5の値が偶数に向かっていることがわかった。これは俗にいう偶数への丸めというもので、Wikipediaでは以下のように書かれている。

なお、この偶数丸めは「銀行丸め」とも呼ばれているようだ。

偶数への丸め(round to even)[2][3]は、端数が0.5より小さいなら切り捨て、端数が0.5より大きいならは切り上げ、端数がちょうど0.5なら切り捨てと切り上げのうち結果が偶数となる方へ丸める(つまり偶数+0.5なら切り捨て、奇数+0.5なら切り上げとなる)。JIS Z 8401で規則Aとして定められていて、規則B(四捨五入)より「望ましい」とされている[1]

端数0.5のデータが有限割合で存在する場合、四捨五入ではバイアスが発生するが、偶数への丸めではバイアスが無い。つまり、多数足し合わせても、丸め誤差が特定の側に偏って累積することがない。ただし、偶数+0.5は現れるが奇数+0.5は現れないデータのように分布に特殊な特徴がある場合は、バイアスが発生することがある。

要するに、ちょうど0.5の値が出た時は偶数側に四捨五入されるということ。だから1.5は2に行きつつ、2.5も2に行くことになる。また、3.5は4になるけど、4.5も4になる。

これを使うことで誤差が小さくなるようだ。なら小学校で教えてほしかった(今は習っているのか?学習指導要領とか見ていないからなんとも言えないが)。

負の数の四捨五入

では、負の数の四捨五入はどうなのか。多分だけど、小学校や中学校で負の数の四捨五入は習っていない。なんで習わないのかわからんが。

先程のWikipediaのページには以下のような記載がある。

一方、JIS Z 8401では、負数は絶対値として丸める(−1.5は−2へと丸められる)。

要するに、四捨五入したい値の絶対値をとって四捨五入し、その値に元の数値の符号を付加するということ。ややこしい。だから、「-2.5」の場合は以下のように計算される。

なお、ここでは四捨五入は偶数丸めではなく、一般的に考えられている四捨五入を使用する。

  • -2.5: 元の数
  • 2.5: 絶対値とった
  • 3.0: 四捨五入
  • -3.0: 符号を付加

ややこしい。

負の数も含めた一般的な四捨五入の関数を作成

ということで、負の数も含めて一般的な四捨五入を実行するための関数を作成。ただ四捨五入するだけではなく、ここでは好きな桁数での四捨五入もサポートした。手順は以下。

  1. 好きな桁数で四捨五入するために10桁数を計算
  2. 四捨五入したい値の絶対値を取る
  3. 絶対値を取った値×2をする
  4. ×2をした値に×10桁数
  5. ×2×10桁数に+1
  6. この値を2で切り捨てる
  7. 10桁数したので逆に10桁数
  8. 符号を付加

文章だけではイメージしにくいかもしれないので、実際に具体的な数値で上記の手順を見てみる。数直線の常に表示するとわかりやすい。

上記の手順を実際に実装した関数が以下。valが四捨五入したい値でdが指定する桁数。

# 桁数を指定して四捨五入する
def rounding(val, d=0):  # d=0は初期値
    adjust = 10 ** d  # 桁数指定時の調節用
    abs_val = abs(val)
    val2 = abs_val * 2

    order = val2 * adjust  # 桁数指定時にはここで調節が入る

    val2_1 = order + 1
    floor_val = val2_1 // 2

    order2 = floor_val / adjust  # 桁数指定の調節を戻す

    ans = np.sign(val) * order2  # 符号を付加
    # # 1行でまとめると以下
    # ans = np.sign(val) * ((abs(val) * 2 * adjust + 1) // 2 / adjust)

    return ans

結構ややこしい関数となっているが、最後の方に書いてあるように1行でまとめることも可能。その場合は見づらくなるので注意。

色んなパターンで自作四捨五入を試す

# 確認用の関数
def check(tpl, d, order=4):
    for val in tpl:
        ans = rounding(val, d=d)
        print(f"{val:{order}}: {ans:4}")

ということで、自作の四捨五入の関数を使って色んな配列を四捨五入してみる。確認用の関数は上のもので、シンプルに四捨五入して出力するだけ。

tplを四捨五入

print(tpl)
# (-3.0, -2.5, -2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0)

check(tpl=tpl, d=0)
# -3.0: -3.0
# -2.5: -3.0
# -2.0: -2.0
# -1.5: -2.0
# -1.0: -1.0
# -0.5: -1.0
#  0.0:  0.0
#  0.5:  1.0
#  1.0:  1.0
#  1.5:  2.0
#  2.0:  2.0
#  2.5:  3.0
#  3.0:  3.0

まずは既に定義したtplから。2.5が3.0になっているので、しっかりと一般的な四捨五入になっている。

.5がない値を四捨五入

tpl1 = (-2.1, -2.0, -1.9, -1.6, -1.5, -1.4, -0.6, -0.5, -0.4)
check(tpl=tpl1, d=0)
# -2.1: -2.0
# -2.0: -2.0
# -1.9: -2.0
# -1.6: -2.0
# -1.5: -2.0
# -1.4: -1.0
# -0.6: -1.0
# -0.5: -1.0
# -0.4: -0.0

もちろん.5ではない小数点でも同じように四捨五入できる。ちゃんと四捨五入できてる。こちらは負の数だったが、正の数も大丈夫。

tpl2 = (0, 0.4, 0.5, 0.6, 1.4, 1.5, 1.6, 1.9, 2.0, 2.1)
check(tpl=tpl2, d=0)
#    0:  0.0
#  0.4:  0.0
#  0.5:  1.0
#  0.6:  1.0
#  1.4:  1.0
#  1.5:  2.0
#  1.6:  2.0
#  1.9:  2.0
#  2.0:  2.0
#  2.1:  2.0

桁数を指定してみる

# 四捨五入の桁を1にしてみる
tpl3 = (-1.999, -1.97, -1.46, -1.45, -1.44, 0, 1.44, 1.45, 1.46, 1.97, -1.999)
check(tpl=tpl3, d=1, order=6)
# -1.999: -2.0
#  -1.97: -2.0
#  -1.46: -1.5
#  -1.45: -1.5
#  -1.44: -1.4
#      0:  0.0
#   1.44:  1.4
#   1.45:  1.5
#   1.46:  1.5
#   1.97:  2.0
# -1.999: -2.0

作成したrounding関数では四捨五入の桁数の指定ができるようになっている。ということで桁数指定してみる。ここでは1桁とした。

1桁の場合は小数第一位まで表示されるので、小数第二位が四捨五入されることになる。ちゃんと四捨五入されているから驚き。

桁数をいろいろ試してみる。

最後に特定の値で桁数を変えた時に四捨五入の値がどうなるのかを検証する。桁数はマイナスを指定すると整数値で四捨五入される。今回は-2から5までの数値で四捨五入を試す。

14.56789

val = 14.56789
for d in range(-2, 5):
    check(tpl=[val], d=d, order=0)
# 14.56789:  0.0
# 14.56789: 10.0
# 14.56789: 15.0
# 14.56789: 14.6
# 14.56789: 14.57
# 14.56789: 14.568
# 14.56789: 14.5679

15.56789

val = 15.56789
for d in range(-2, 5):
    check(tpl=[val], d=d, order=0)
# 15.56789:  0.0
# 15.56789: 20.0
# 15.56789: 16.0
# 15.56789: 15.6
# 15.56789: 15.57
# 15.56789: 15.568
# 15.56789: 15.5679

-14.56789

val = -14.56789
for d in range(-2, 5):
    check(tpl=[val], d=d, order=0)
# -14.56789: -0.0
# -14.56789: -10.0
# -14.56789: -15.0
# -14.56789: -14.6
# -14.56789: -14.57
# -14.56789: -14.568
# -14.56789: -14.5679

-15.56789

val = -15.56789
for d in range(-2, 5):
    check(tpl=[val], d=d, order=0)
# -15.56789: -0.0
# -15.56789: -20.0
# -15.56789: -16.0
# -15.56789: -15.6
# -15.56789: -15.57
# -15.56789: -15.568
# -15.56789: -15.5679

小さなトラップ

今回はpython3系の四捨五入が一般的に使われている四捨五入ではないことを確認しつつ、一般的な四捨五入の計算になるような関数を作成した。

偶数丸めの四捨五入に気づかなかった時は、研究で出した値がなぜ異なっているのかがわからなかった。見つけた時は疑問だらけだった。

今回の話はあまり知られていないというか四捨五入ってこんなもんだよね、と信じられているから周りにも広めてほしい。四捨五入にはトラップがあると。

関連記事

【python&~】数値にチルダ(~)をつけると値が+1されて負の数になる(ビット反転)

続きを見る

【python&csv読み込み】pythonを使ってcsvを読み込み

続きを見る

【python3&zip関数】python3系にてzipは一回使ったら消える

続きを見る

【inspect&引数名取得】defのパラメータ名を取り出す

続きを見る

ガジェット

2023/11/11

【デスクツアー2022下半期】モノは少なく、でも効率的に Desk Updating #0

今回はガジェットブロガーなのにデスク環境を構築していない執筆者の ...

ライフハック

2023/9/16

【Audible vs YouTube Premium】耳で聴く音声学習コンテンツを比較

ワイヤレスイヤホンが普及し耳で学習することへのハードルが格段に下 ...

完全ワイヤレスイヤホン(TWS)

2023/9/18

【SENNHEISER MOMENTUM True Wireless 3レビュー】全てが整ったイヤホン

今回は高音質・高機能なSENNHEISERのフラグシップ完全ワイヤレスイヤホン「SENNH ...

ライフハック

2023/3/11

【YouTube Premiumとは】メリットしかないから全員入れ

今回はYouTube Premiumを実際に使ってみてどうなのか、どんなメリット/デメリット ...

マウス

2023/11/4

【Logicool MX ERGOレビュー】疲れない作業効率重視トラックボールマウス

こんな人におすすめ トラックボールマウスの王道Logicool MX ERGOが気になるけどऩ ...

ベストバイ

2023/10/29

【ベストバイ2022】今年買って良かったモノのトップ10

2022年ベストバイ この1年を振り返って執筆者は何を買ったのか。ガジェッ& ...

スマホ

2023/1/15

【楽天モバイル×povo2.0の併用】月1,000円の保険付きデュアルSIM運用

こんな人におすすめ 楽天モバイルとpovo2.0のデュアルSIM運用って実際のとこ ...

マウス

2023/9/16

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

macOSアプリケーション

2022/9/30

【Chrome拡張機能】便利で効率的に作業できるおすすめの拡張機能を18個紹介する

こんな人におすすめ Chromeの拡張機能を入れたいけど、調べても同じような ...

macOSアプリケーション

2023/5/3

【Automator活用術】Macで生産性を上げる作業の自動化術

今回はMacに標準でインストールされているアプリ「Automator」を使ってできる ...

Pythonを学びたいけど独学できる時間なんてない人へのすゝめ

執筆者は大学の研究室・大学院にて独学でPythonを習得した。

でも社会人になったら独学で行うには時間も体力もなくて大変だ。

時間がない社会人だからこそプロの教えを乞うのが効率的。

ここでは色んなタイプに合ったプログラミングスクールの紹介をする。

  • この記事を書いた人

メガネ

Webエンジニア駆け出し。独学のPythonで天文学系の大学院を修了。常時金欠のガジェット好きでM2 Pro MacBook Pro(31万円) x Galaxy Z Fold5(25万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa、 Instagram@megatenpa

-Python基礎
-,