Pythonのround()で2.5が2になる原因と四捨五入する方法|偶数丸め・round(2.675, 2)が2.67になる理由・Decimalの使い方(Java・C#・PowerShell比較)

Pythonのround()で2.5が2になる原因と四捨五入する方法

こんばんは、おうどんです🍜

うどん屋の閉店後。 今日は替え玉の日でした。

お客さんが食べた玉数を、Python で集計します。 2.5玉食べた人は、四捨五入で3玉ぶんのお会計。のはずです。

for x in [0.5, 1.5, 2.5, 3.5, -0.5, -2.5]:
    print(f"round({x}) = {round(x)}")
round(0.5) = 0
round(1.5) = 2
round(2.5) = 2
round(3.5) = 4
round(-0.5) = 0
round(-2.5) = -2

(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です)

2.5 が 2。 3.5 は 4 なのに。

店長(おうどん)「roundさん、2.5玉は3玉でしょ。小学校で習ったよ?」

roundさん「2 と 3、ちょうど同じ距離なので、偶数のほうにしました」

店長「なんで偶数?」

roundさん「偶数のほうが、割り切れて気持ちいいので」

……会計のルールを、気分で決めないでほしい。

(会話は説明用の架空の場面です)

でも、これは Python のバグではありません。 しかも仲間がいます。

  • round(2.5) が 2、round(0.5) が 0 になる
  • round(2.675, 2) が 2.68 ではなく 2.67 になる
  • Java や C# と結果が合わない
  • 自作の四捨五入関数が、マイナスの数で変なことになる

今回は、roundさんの偶数愛の正体と、ちゃんと四捨五入する方法を見ていきます。

最初に、初心者向けのチェックリストです。

  • 四捨五入が必要なら、round() ではなく decimal の ROUND_HALF_UP を使う
  • Decimal には、数値ではなく文字列で渡す(Decimal("2.675"))
目次

round() は四捨五入ではなく「偶数丸め」

roundさんの言い分は、実は Python の公式ルールそのものです。 Python の round() は、ちょうど真ん中のときだけ、偶数のほうに寄せます。

Python の組み込み関数のドキュメントには、2つの候補が同じだけ近いときは偶数のほうに丸める、と書かれています。例として round(0.5) と round(-0.5) は 0、round(1.5) は 2 と載っています。

この丸め方を、偶数丸め(最近接偶数への丸め)と呼びます。銀行丸め(バンカーズ・ラウンディング)という呼び名もあります。

値四捨五入偶数丸め(round())
0.510
1.522
2.532
3.544
-2.5-3-2

ちょうど .5 のときだけ違いが出ます。 2.4 や 2.6 のように真ん中でなければ、四捨五入と同じ結果です。

roundさん「真ん中以外は、ふつうに近いほうに行きます。わたし、真ん中のときだけ偶数の味方なんです」

店長「その真ん中が、替え玉の日は全員なんだよ」

……半玉の注文が多い店で、いちばん出番が多いこだわり。

じゃあ round() は不良品?

不良品ではありません。 偶数丸めは、たくさんの数を丸めて合計したときに、ずれが片寄りにくい丸め方です(なぜなのかは上級者向けの章で見ます)。 ただ、学校で習った四捨五入とは違う。ここを知らないとハマります。

round(2.675, 2) が 2.67 になるのは、偶数丸めのせいではない

次の仲間です。

from decimal import Decimal

print(round(2.675, 2))
print(round(1.005, 2))
print(Decimal(2.675))
print(Decimal(1.005))
2.67
1.0
2.67499999999999982236431605997495353221893310546875
1.00499999999999989341858963598497211933135986328125

(今回の検証環境の Python 3.13.1 で実行した結果です)

round(2.675, 2) が 2.67。 「8 は偶数なんだから、偶数丸めでも 2.68 では?」と思った人、するどいです。

これは偶数丸めではなく、2.675 がそもそも 2.675 ではないのが原因です。

Decimal(2.675) の行を見てください。 コンピューターの中の 2.675 は、正確には 2.67499999999999982… という、ほんの少しだけ小さい数でした。 真ん中より小さいので、偶数かどうか以前に、ふつうに切り捨てられたわけです。

Python の round() のドキュメントにも、この round(2.675, 2) が 2.67 になる例が注意書きとして載っていて、バグではなく、ほとんどの小数は float(浮動小数点数:小数を2進数で近似して持つ型)で正確に表せないためだ、と説明されています。

2.675「わたし、2.675 です」

Decimal「戸籍を調べたら、2.67499999999999982236431605997495353221893310546875 さんでした」

……名前、長すぎて受付票に書ききれない。

1.005 も同じで、中身は 1.00499… です。2けたで丸めると 1.00、表示は 1.0 になりました。

じゃあ 2.675 と書いたのに、なんで print すると 2.675 と出るの?

print は、中身の長い数を全部の桁で出すのではなく、短く書いて見せているからです。 画面では 2.675、中身は 2.67499…。顔と戸籍が違うタイプです。

四捨五入は decimal の ROUND_HALF_UP で、文字列から

ちゃんと四捨五入したいなら、decimal モジュールで、文字列から作った Decimal を ROUND_HALF_UP で丸めます。

decimal は、小数を10進数のまま正確に扱う、Python 標準のモジュールです。お金の計算向きです。

from decimal import Decimal, ROUND_HALF_UP

print(Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
print(Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP))
print(Decimal("-2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP))
print(Decimal(2.675).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
print(Decimal(str(2.675)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
2.68
3
-3
2.67
2.68

(今回の検証環境の Python 3.13.1 で実行した結果です)

読み方はこうです。

  • quantize(Decimal("0.01")):小数第2位までにそろえる。Decimal("1") なら整数にそろえる
  • rounding=ROUND_HALF_UP:真ん中のときは 0 から遠いほうへ(いわゆる四捨五入)
  • -2.5 は -3 になる。マイナスも「0 から遠いほう」です

decimal のドキュメントでは、ROUND_HALF_UP は「真ん中のときは 0 から遠いほうへ」、ROUND_HALF_EVEN は「真ん中のときは偶数へ」と説明されていて、何も指定しないときの既定は ROUND_HALF_EVEN です。 つまり Decimal さんも、ほうっておくと偶数派です。

Decimal「わたしも初期設定は偶数派です。ただ、言われたらちゃんと四捨五入します」

roundさん「わたしは、言われても偶数派です」

……頑固さで勝負しないでほしい。

4行目に注目です。 Decimal(2.675) と数値のまま渡すと、2.67 になりました。さっきの長い戸籍の数がそのまま Decimal になるからです。 decimal のドキュメントでも、float から作ると2進数の値がそのまま正確に変換される例(Decimal(1.1) が長い数になる例)が載っています。

だから Decimal には文字列で渡す。 float しか手元にないなら、5行目のように str() を通してから渡す方法もあります。ただし、すでに計算で生じた誤差まで消えるわけではありません。お金の計算は、入力の時点から文字列で Decimal に渡すのが基本です。

店長「じゃあ明日から、お客さんには玉数を文字で申告してもらおう」

……「にてんごたまです」と言われても、それはそれで集計がしんどい。

🔰 ここまで読めば今日から困らない

ここまでで、よくあるトラブルは直せます。

  • round(2.5) が 2 → round() は偶数丸め。四捨五入したいなら decimal の ROUND_HALF_UP
  • round(2.675, 2) が 2.67 → 2.675 が中では 2.67499…。Decimal に文字列で渡す
  • Decimal(2.675) で丸めても直らない → Decimal("2.675") か Decimal(str(x))

迷ったら、この関数をコピーしてください。

from decimal import Decimal, ROUND_HALF_UP

def round_half_up(x, digits=0):
    """Round half away from zero (schoolbook rounding)."""
    q = Decimal(1).scaleb(-digits)  # digits=2 -> 0.01, digits=-2 -> 1E+2
    r = Decimal(str(x)).quantize(q, rounding=ROUND_HALF_UP)
    return int(r) if digits <= 0 else r

for x, d in [(2.5, 0), (-2.5, 0), (2.675, 2), (1.005, 2), (1250, -2)]:
    print(f"round_half_up({x}, {d}) = {round_half_up(x, d)}")
round_half_up(2.5, 0) = 3
round_half_up(-2.5, 0) = -3
round_half_up(2.675, 2) = 2.68
round_half_up(1.005, 2) = 1.01
round_half_up(1250, -2) = 1300

(今回の検証環境の Python 3.13.1 で実行した結果です)

digits は round() の第2引数と同じで、小数第何位までにするかです。-2 なら百の位まで(1250 → 1300)。 scaleb(-digits) は「1 を 10 の -digits 乗倍する」という意味で、0.01 や 1E+2(100)のような、そろえる位の見本を作っています。 整数まで丸めたときは int で返し、小数が残るときは Decimal のまま返します。お金の計算は、そのまま Decimal で続けるのがおすすめです。

roundさん「わたしの仕事、とられました?」

店長「真ん中じゃないときは、引き続きお願いします」

……真ん中のときだけ、代打が立つ。

これ、毎回 import 書くのめんどう。round() の設定で四捨五入にできないの?

できません。 float に対する round() の偶数丸めには、設定で変える場所がありません。Decimal を渡した場合は別で、桁数も指定するとコンテキストの丸め方が使われます(後の章で見ます)。関数を1つ作って、それを呼ぶのがいちばん平和です。 ……roundさんの偶数愛は、設定画面よりずっと奥にしまわれています。

ここから先は中級者向け。 自作の四捨五入の罠、消費税の計算、ほかの言語との違い、完成コードです。

「0.5 を足して切り捨て」は、マイナスと境目で壊れる

四捨五入を自作するとき、よく見るのがこの形です。 math.floor(x + 0.5) は、マイナスの数と、0.5 のすぐ下の数で、四捨五入になりません。

import math

for x in [2.5, -2.5, 0.49999999999999994]:
    print(f"{x}: floor(x + 0.5) = {math.floor(x + 0.5)}")

print(f"{2.5:.0f} {3.5:.0f} {2.675:.2f}")
2.5: floor(x + 0.5) = 3
-2.5: floor(x + 0.5) = -2
0.49999999999999994: floor(x + 0.5) = 1
2 4 2.67

(今回の検証環境の Python 3.13.1 で実行した結果です。math.floor は小さいほうの整数に切り下げる関数です)

  • -2.5 が -2:-2.5 + 0.5 = -2.0 なので、切り下げても -2。四捨五入(0 から遠いほう)なら -3 です
  • 0.49999999999999994 が 1:0.5 より小さいのに 1。足し算 0.49999999999999994 + 0.5 の答えが、float では 1.0 に丸められてしまうためです(今回の検証でも 1.0 と表示されました)

自作関数「0.5 を足して、床に落とします!」

店長「マイナスのお客さんは?」

自作関数「……床が、思ったより上にありました」

……マイナスのうどん、という存在自体はさておき。

最後の行は、f 文字列(f"...")の書式指定です。 f"{2.5:.0f}" は 2、f"{2.675:.2f}" は 2.67。Python では、表示用の書式指定も四捨五入ではありません。今回の検証では round() と同じ結果になりました。 「画面に出すときだけ四捨五入されるはず」も、Python では通りません。

表示だけでも四捨五入してくれたら、見た目は平和なのに。

見た目だけ平和だと、帳簿と画面が合わない日に、もっと平和じゃなくなります。

消費税の計算:1.1 を掛けるところから、もう危ない

実務でいちばんハマるのは、お金です。 税込み価格を 価格 * 1.1 で計算すると、float の誤差と round() の偶数丸めが、両方まとめてやってきます。

from decimal import Decimal, ROUND_HALF_UP

for price in [105, 125, 135]:
    f = price * 1.1
    d = (Decimal(price) * Decimal("1.1")).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
    print(f"{price}: float={f!r} round={round(f)} Decimal={d}")
105: float=115.50000000000001 round=116 Decimal=116
125: float=137.5 round=138 Decimal=138
135: float=148.5 round=148 Decimal=149

(今回の検証環境の Python 3.13.1 で実行した結果です。!r は repr() で表示する指定です)

3行とも「.5」に見えるのに、round() の動きがバラバラです。

  • 105円:float では 115.50000000000001。真ん中より少し大きいので、偶数丸めとは関係なく 116
  • 125円:ちょうど 137.5。偶数丸めで 138(8 が偶数なので、たまたま四捨五入と同じ)
  • 135円:ちょうど 148.5。偶数丸めで 148。四捨五入なら 149

roundさん「135円のお客さまは、148円です」

店長「1円まけたの?」

roundさん「8 が偶数だったので」

……値引きの理由が、数字の好みです。

Decimal で計算すると、3行とも四捨五入どおりになります。 もうひとつ、端数の処理を「切り捨て」にしたいなら price * 110 // 100 のように整数だけで計算する手もあります(// は切り捨ての割り算。マイナスの値では小さいほうへ切り下げる点に注意)。 四捨五入・切り捨て・切り上げのどれにするかは、言語ではなく、そのシステムの仕様(会社や契約のルール)で決めることです。

次に、丸めるタイミングです。

from decimal import Decimal, ROUND_HALF_UP

prices = [Decimal(105)] * 3
rate = Decimal("0.08")
one = Decimal("1")

per_line = sum((p * rate).quantize(one, rounding=ROUND_HALF_UP) for p in prices)
total = (sum(prices) * rate).quantize(one, rounding=ROUND_HALF_UP)
print(f"tax per item={prices[0] * rate}  round each line={per_line}  round the total={total}")
tax per item=8.40  round each line=24  round the total=25

(今回の検証環境の Python 3.13.1 で実行した結果です)

105円の商品3つに 8% の税。 1つずつ丸めると 8 × 3 = 24円、合計してから丸めると 315 × 0.08 = 25.2 で 25円。 同じ四捨五入でも、丸める場所で1円ずれます。ただし、消費税は会社の仕様だけで自由に決められるわけではありません。国税庁の端数計算の解説のとおり、適格請求書に記載する消費税額等は、請求書1通につき税率ごとに1回の端数処理が必要です。上の例は計算順の違いを示す比較で、商品ごとに丸めた税額の合計をそのまま記載してよい、という意味ではありません。

店長「じゃあ1行ずつ丸めて、合計も丸めて、高いほうをもらおう」

……それは仕様ではなく、商売の姿勢の問題です。

ほかの言語では「既定の丸め」が違う

Python から Java や C# に移ると、また話が変わります。 「round という名前の関数」の動きは、言語ごとにバラバラです。

まず Java です。

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        System.out.println("Math.round(2.5)  = " + Math.round(2.5));
        System.out.println("Math.round(-2.5) = " + Math.round(-2.5));
        System.out.println("Math.rint(2.5)   = " + Math.rint(2.5));
        System.out.println("new BigDecimal(2.675)       -> " + new BigDecimal(2.675).setScale(2, RoundingMode.HALF_UP));
        System.out.println("BigDecimal.valueOf(2.675)   -> " + BigDecimal.valueOf(2.675).setScale(2, RoundingMode.HALF_UP));
        System.out.println("new BigDecimal(\"-2.5\")      -> " + new BigDecimal("-2.5").setScale(0, RoundingMode.HALF_UP));
        System.out.println("String.format(\"%.2f\", 2.675) = " + String.format("%.2f", 2.675));
    }
}
Math.round(2.5)  = 3
Math.round(-2.5) = -2
Math.rint(2.5)   = 2.0
new BigDecimal(2.675)       -> 2.67
BigDecimal.valueOf(2.675)   -> 2.68
new BigDecimal("-2.5")      -> -3
String.format("%.2f", 2.675) = 2.68

(今回の検証環境の Java 23.0.2 で実行した結果です)

Java の Math クラスのドキュメントでは、Math.round は「真ん中のときは正の無限大の方向へ」、Math.rint は「真ん中のときは偶数へ」と説明されています。 だから Math.round(-2.5) は、四捨五入の -3 ではなく -2。プラスの方向に寄せるので、マイナスでは「0 に近いほう」になります。

Java の Math.round「真ん中なら、とりあえずプラスに進みます。前向きなので」

店長「マイナスのお客さんにも前向きなんだね」

……-2.5 を -2 にする前向きさ。借金が少し減って見えるタイプです。

BigDecimal も Python の Decimal と同じ話で、BigDecimal のドキュメントには、new BigDecimal(0.1) が 0.1000000000000000055511151231257827021181583404541015625 になること、double から変換するなら BigDecimal.valueOf(double) が一般に望ましい(Double.toString の結果から作るのと同じ値になる)ことが書かれています。

次に C# です。

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Math.Round(2.5)  = " + Math.Round(2.5));
        Console.WriteLine("Math.Round(-2.5) = " + Math.Round(-2.5));
        Console.WriteLine("AwayFromZero(2.5)  = " + Math.Round(2.5, MidpointRounding.AwayFromZero));
        Console.WriteLine("AwayFromZero(-2.5) = " + Math.Round(-2.5, MidpointRounding.AwayFromZero));
        Console.WriteLine("Convert.ToInt32(2.5) = " + Convert.ToInt32(2.5));
        Console.WriteLine("2.5.ToString(\"F0\")   = " + 2.5.ToString("F0"));

        double v = 11.0;
        for (int i = 0; i < 5; i++) v += 0.1;
        Console.WriteLine("11.0 + 0.1 x5 = " + v.ToString("R") + " -> " + Math.Round(v, MidpointRounding.AwayFromZero));
    }
}
Math.Round(2.5)  = 2
Math.Round(-2.5) = -2
AwayFromZero(2.5)  = 3
AwayFromZero(-2.5) = -3
Convert.ToInt32(2.5) = 2
2.5.ToString("F0")   = 3
11.0 + 0.1 x5 = 11.499999999999998 -> 11

(今回の検証環境の .NET Core SDK 2.1.202 で実行した結果です。最後の行は上級者向けの章で使います)

.NET の Math.Round のドキュメントでは、Math.Round(Double) は真ん中の値を最も近い偶数に丸める、と書かれています。C# の既定も、Python と同じ偶数派です。 四捨五入にしたいときは MidpointRounding.AwayFromZero を渡します。

ところが、2.5.ToString("F0") は 3。 同じ C# の中で、計算の丸め(Math.Round)と表示の丸め(書式指定)の結果が違いました。今回の記録は .NET Core SDK 2.1.202 を使った環境のものですが、SDKの版と実行時のランタイムの版は別です。Microsoftの数値書式の解説では、.NET Frameworkと.NET Core 2.0以前は真ん中を0から遠いほうへ、.NET Core 2.1以降は偶数へ丸めます。つまり、この「3」という結果を、.NET Core 2.1以降のランタイムにもそのまま当てはめることはできません。

C#「帳簿には2玉、レシートには3玉と書いておきました」

……同じお客さんの玉数が、紙によって違う店。

最後に PowerShell(Windows PowerShell 5.1)です。

[math]::Round(2.5)
[math]::Round(2.5, [MidpointRounding]::AwayFromZero)
[int]2.5
[int]3.5
"{0:F0}" -f 2.5
2
3
2
4
3

(今回の検証環境の Windows PowerShell 5.1 で実行した結果です)

PowerShell は .NET の [math]::Round をそのまま使えるので、既定は偶数丸めです。 [int]2.5 のような型の変換(キャスト)でも 2、[int]3.5 は 4 になりました。小数を整数に変換するだけのつもりでも、偶数丸めが働いています。

まとめるとこうです(すべて今回の検証環境で実行した結果)。

書き方2.5-2.5真ん中の扱い
Python round(x)2-2偶数へ
Python Decimal(...).quantize(..., ROUND_HALF_UP)3-30 から遠いほうへ
Java Math.round(x)3-2正の無限大の方向へ
Java Math.rint(x)2.0(未実行)偶数へ
C# Math.Round(x)2-2偶数へ
C# Math.Round(x, MidpointRounding.AwayFromZero)3-30 から遠いほうへ
PowerShell [int]x2-2偶数丸めになった

roundさん「偶数派、意外と多いでしょう?」

Java の Math.round「わたしはプラス派です」

……派閥が3つあります。うどんの好みより割れている。

もう全言語で同じにしてほしい。

それぞれの言語に、それぞれの歴史と事情があります。 言語をまたぐシステムでは、「丸めはどちらか一方でだけやる」と決めておくと、合わない1円を探す夜が減ります。

完成コード:四捨五入・偶数丸め・切り捨てをそろえたモジュール

ここまでを全部入れた、コピーして使える形です。 float は repr() の表記(2.675 なら ‘2.675’)を経由して Decimal にし、bool は数として受け付けません(Python では True が 1 として計算できてしまうため)。

"""rounding_utils.py - schoolbook rounding helpers based on decimal."""
from decimal import Decimal, InvalidOperation, ROUND_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP


def to_decimal(x):
    """Convert int / float / str / Decimal to Decimal without binary noise."""
    if isinstance(x, bool):
        raise TypeError("bool is not a number here")
    if isinstance(x, Decimal):
        return x
    if isinstance(x, float):
        return Decimal(repr(x))  # repr(2.675) -> '2.675'
    return Decimal(x)  # int or str


def _round(x, digits, mode):
    q = Decimal(1).scaleb(-digits)  # digits=2 -> 0.01, digits=-2 -> 1E+2
    r = to_decimal(x).quantize(q, rounding=mode)
    return int(r) if digits <= 0 else r


def round_half_up(x, digits=0):
    """2.5 -> 3, -2.5 -> -3 (ties away from zero)."""
    return _round(x, digits, ROUND_HALF_UP)


def round_half_even(x, digits=0):
    """2.5 -> 2, 3.5 -> 4 (same rule as built-in round())."""
    return _round(x, digits, ROUND_HALF_EVEN)


def round_down(x, digits=0):
    """Truncate toward zero: 2.9 -> 2, -2.9 -> -2."""
    return _round(x, digits, ROUND_DOWN)


if __name__ == "__main__":
    cases = [
        (round_half_up, 2.5, 0, 3),
        (round_half_up, -2.5, 0, -3),
        (round_half_up, 2.675, 2, Decimal("2.68")),
        (round_half_up, "1.005", 2, Decimal("1.01")),
        (round_half_up, 1250, -2, 1300),
        (round_half_even, 2.5, 0, 2),
        (round_down, -2.9, 0, -2),
        (round_down, 105 * 1.1, 0, 115),
    ]
    for func, x, d, expected in cases:
        got = func(x, d)
        mark = "OK" if got == expected else "NG"
        print(f"{mark} {func.__name__}({x!r}, {d}) = {got!r}")

    try:
        round_half_up(1e30, 2)
    except InvalidOperation as e:
        print("too many digits:", type(e).__name__)
OK round_half_up(2.5, 0) = 3
OK round_half_up(-2.5, 0) = -3
OK round_half_up(2.675, 2) = Decimal('2.68')
OK round_half_up('1.005', 2) = Decimal('1.01')
OK round_half_up(1250, -2) = 1300
OK round_half_even(2.5, 0) = 2
OK round_down(-2.9, 0) = -2
OK round_down(115.50000000000001, 0) = 115
too many digits: InvalidOperation

(今回の検証環境の Python 3.13.1 で、python rounding_utils.py として実行した結果です)

ポイントは3つです。

  • round_half_up(2.675, 2) が 2.68:float でも repr() 経由なので、戸籍の長い数にならない
  • round_down(105 * 1.1, 0) が 115:115.50000000000001 の切り捨て。ROUND_DOWN は 0 に向かって切り捨てるので、マイナスでも -2.9 → -2 です
  • 最後の行:1e30 を小数第2位まで、のように桁が多すぎると InvalidOperation という例外になります。decimal の既定の精度(28けた)を超えるためです

decimal のドキュメントの quantize の説明にも、丸めたあとの係数の長さが精度を超えると InvalidOperation になる、とあります。 巨大な金額を扱うシステムなら、decimal.localcontext() で精度を上げてから呼ぶなどの対策を検討してください(この対策自体は今回の検証では実行していません)。

店長「1e30 円のお会計って、どんなうどん?」

……具が地球ぐらい乗っているんだと思います。

完成コード、長い。round_half_up だけでよくない?

よいです。 🔰 の章の関数だけで足りるなら、それが正解です。偶数丸めや切り捨ても同じ書き方でそろえておきたいとき、こちらを使ってください。

切り分けチェックリスト

症状から逆引きできるようにまとめました。

症状まず疑うこと確かめ方直し方
round(2.5) が 2round() の偶数丸めround(0.5) が 0 になるか見るdecimal の ROUND_HALF_UP
round(2.675, 2) が 2.67float の中身が 2.67499…print(Decimal(2.675))Decimal("2.675") か Decimal(str(x))
Decimal で丸めても直らないfloat のまま Decimal に渡しているDecimal(x) の x が float か見る文字列で渡す
マイナスだけ合わないfloor(x + 0.5) の自作関数、Java の Math.round-2.5 を入れてみるROUND_HALF_UP / RoundingMode.HALF_UP
画面と帳簿が1円合わない表示の書式指定と計算の丸めが別ルール同じ値を両方で出す丸めは計算側で1回だけ
合計が1円合わない丸める場所(1行ずつか合計か)両方の方法で計算してみる仕様で決めて統一
言語をまたぐと合わない言語ごとに既定の丸めが違う上の比較表を見る丸める場所を1か所にする

roundさん「わたしが悪い行、1つしかないですよね」

店長「1行目から、きみだからね」

……容疑者リストの先頭に立つ自覚はあるらしい。

表を見たら、ほとんど float さんのせいでは?

float さんは、2進数で持つと決まった日から、ずっとこうです。 悪いのは float さんではなく、float さんに10進数の帳簿をつけさせた人のほうです。

ここから先は上級者向け。読み飛ばしてもOKです。 偶数丸めが既定になっている理由、Decimal の round() のくせ、浮動小数点の精度と丸めの関係を見ていきます。

なぜ偶数丸めが既定なのか:片寄りを打ち消すため

偶数丸めは、真ん中の値を一律に同じ方向へ丸めないので、ずれが片寄りにくいんです。ただし、必ず半分ずつになるわけではなく、データに出てくる数の組み合わせによります。

.NET の Math.Round のドキュメントには、0 から遠いほうへの丸め(四捨五入)がいちばん広く知られている一方、最も近い偶数への丸めは金融や統計の計算での標準で、IEEE 754(浮動小数点数の国際規格)の第4節に準拠している、と書かれています。 同じページには、真ん中の値をいつも同じ方向に丸めると平均がずれる例が載っています。それを Python の decimal で再現してみました。

from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN

values = [Decimal(s) for s in ["1.15", "1.25", "1.35", "1.45", "1.55", "1.65"]]
tenth = Decimal("0.1")

true_mean = sum(values) / len(values)
up = sum(v.quantize(tenth, rounding=ROUND_HALF_UP) for v in values) / len(values)
even = sum(v.quantize(tenth, rounding=ROUND_HALF_EVEN) for v in values) / len(values)
print("true mean :", true_mean)
print("HALF_UP   :", up)
print("HALF_EVEN :", even)
true mean : 1.40
HALF_UP   : 1.45
HALF_EVEN : 1.4

(今回の検証環境の Python 3.13.1 で実行した結果です。値は .NET のドキュメントの例と同じものを使いました)

本当の平均は 1.40。 四捨五入で丸めてから平均すると 1.45 に増え、偶数丸めなら 1.4 で本当の平均と同じになりました。 四捨五入は、真ん中の値を全部上に送るので、真ん中がたくさん出るデータほど、合計が少しずつふくらむわけです。

roundさん「わたしの偶数好き、気分じゃなくて統計だったんです」

店長「最初からそう言ってよ」

roundさん「割り切れて気持ちいい、のほうが伝わるかと思って」

……伝わり方を、ボケで選ばないでほしい。

Java の Math.rint のドキュメントにも、IEEE 754 の roundToIntegralTiesToEven(偶数への丸め)に対応する、と書かれています。 偶数丸めは Python だけのこだわりではなく、浮動小数点の世界の標準の一つなんです。

じゃあ、学校の四捨五入は統計的に間違い?

間違いではありません。 1回だけ丸める、人が暗算する、ルールが決まっている書類。そういう場面では四捨五入が分かりやすい。「たくさん丸めて足す」場面だけ、片寄りが目立ってくる、という話です。

round(Decimal) は、桁数の指定なしだとコンテキストを無視する

decimal には、丸めの既定を変える「コンテキスト」(計算の設定一式)があります。 ところが、round(Decimal) を桁数の指定なしで呼ぶと、コンテキストで四捨五入にしていても偶数丸めでした。

from decimal import Decimal, ROUND_HALF_UP, localcontext

print(round(Decimal("2.5")), round(Decimal("2.665"), 2))

with localcontext() as ctx:
    ctx.rounding = ROUND_HALF_UP
    print(round(Decimal("2.5")), round(Decimal("2.665"), 2))
2 2.66
2 2.67

(今回の検証環境の Python 3.13.1 で実行した結果です)

コンテキストを ROUND_HALF_UP にしたあと、桁数を指定した round(Decimal("2.665"), 2) は 2.67 に変わりました。 でも round(Decimal("2.5")) は 2 のまま。

これは今回の検証環境(Python 3.13.1)で確認した動きで、どのバージョンでも同じかは確かめていません。 いずれにしても、丸め方は quantize(..., rounding=...) で毎回はっきり渡すのがいちばん安全です。コンテキストに任せると、読む人も「いま何派?」と迷います。

Decimal「コンテキストで四捨五入派に転向しました」

round「整数にするときは、わたしの流儀でいきます」

……転向したはずの店で、まだ偶数派が働いている。

コンテキストって、グローバルに変えたら全部効くのでは?

効く範囲が広すぎるのが、逆に心配なところです。 ほかのライブラリの計算まで巻きこむので、localcontext() で範囲を区切るか、quantize に直接渡すほうが事故が少ないです。

浮動小数点の「ほぼ .5」は、どの丸めでも救えない

最後に、丸め方では直らない話です。 丸め方(偶数・四捨五入)が効くのは、値がちょうど真ん中のときだけ。float の計算で真ん中から少しずれたら、偶数丸めと四捨五入のどちらを選んでも、同じ近い側へ丸められます。切り上げ・切り捨てまで同じ結果になる、という意味ではありません。

さっきの C# の結果の最後の行です。

11.0 + 0.1 x5 = 11.499999999999998 -> 11

(上の C# のコードを、今回の検証環境の .NET Core SDK 2.1.202 で実行した結果の最終行です)

11.0 に 0.1 を5回足したら 11.5 のはずが、11.499999999999998。 AwayFromZero(四捨五入)を指定しても 11 になりました。 .NET の Math.Round のドキュメントの「丸めと精度」の節に同じ例があり、精度が失われた結果 .5 より少し小さくなり、真ん中の丸め規則が使われずに切り下げられる、と説明されています。

Python の round(2.675, 2) が 2.67 になったのと、同じ仕組みです。 「2.675 は真ん中」と思っているのは人間だけで、コンピューターから見ると、最初から真ん中ではなかったんです。

roundさん「真ん中だと聞いて来たのに、着いたら真ん中じゃなかったんです」

店長「待ち合わせ場所が、到着前にずれてたのね」

……2進数の地図で10進数の住所を探すと、こうなります。

ちなみに、今回の検証では、C# の Math.Round(2.675, 2) と PowerShell の [math]::Round(2.675, 2) は 2.68 を返しました。Python の round(2.675, 2) は 2.67 です。 同じ double の 2.675 なのに、言語で結果が違う。内部でどう計算しているかは、今回の資料では確認できなかったので断定しません。 Java の String.format("%.2f", 2.675) も 2.68 でした。

つまり、float の値をそのまま丸めると、言語やライブラリの実装しだいで結果が変わることがある。 お金や件数のように「1 の違いが問題になる」値は、最初から Decimal や BigDecimal、または整数(円や銭の単位)で持つのが、いちばん確実な設計です。

全部、整数の「銭」で持てばいいのでは?

それも立派な設計です。 ただ、銭まで持つと、うどん1杯が 50000銭になって、ちょっと高級に見えます。

まとめ:2.5玉は、偶数派の roundさんに当たっていた

冒頭では、2.5玉食べたお客さんのお会計が、2玉ぶんになっていました。

  • Python の round() は偶数丸め。2.5 は 2、3.5 は 4
  • round(2.675, 2) が 2.67 なのは偶数丸めではなく、2.675 が中では 2.67499… だから
  • 四捨五入は Decimal("文字列").quantize(..., rounding=ROUND_HALF_UP)。float は str() を通す
  • floor(x + 0.5) の自作は、マイナスと境目で壊れる
  • Java の Math.round は正の方向、C# の Math.Round は偶数、PowerShell の [int] も偶数になった
  • お金は Decimal・BigDecimal・整数で持ち、丸める場所と方法を仕様で決める

roundさん「明日から、2.5玉のお客さまは3玉ぶんでいいですか?」

店長「そこは Decimal さんにお願いしたから、大丈夫」

roundさん「では、わたしは真ん中じゃないお客さまを担当します」

……分業が成立しました。

じゃあ明日から round() は使用禁止?

禁止しなくて大丈夫です。 真ん中じゃない数の丸めや、偶数丸めがほしい集計なら、roundさんは優秀な会計係です。……替え玉の日だけ、シフトから外しておきましょう。

四捨五入したいなら、round() ではなく、文字列から作った Decimal に ROUND_HALF_UP を渡す。それだけで、替え玉の日のお会計が、偶数の好みで決まることはなくなります。

まずは自分のコードの中から round( を1つ探してみてください。 それがお金や件数を丸めていたら、Decimal さんに交代してもらいましょう🍜

参考資料

  • Python 組み込み関数:round() — 真ん中のときは偶数に丸めること、round(0.5) と round(-0.5) が 0、round(2.675, 2) が 2.67 になるのはバグではなく小数を float で正確に表せないためであること
  • Python:decimal — quantize の使い方、ROUND_HALF_UP・ROUND_HALF_EVEN・ROUND_DOWN の意味、既定のコンテキストが ROUND_HALF_EVEN・精度28けたであること、float から作ると2進数の値が正確に変換されること、quantize で精度を超えると InvalidOperation になること
  • Java SE 23:Math — Math.round は真ん中のとき正の無限大の方向へ、Math.rint は偶数へ(IEEE 754 の roundToIntegralTiesToEven に対応)
  • Java SE 23:BigDecimal — new BigDecimal(0.1) が 0.1 ちょうどにならないこと、double からは BigDecimal.valueOf(double) が一般に望ましいこと
  • .NET:Math.Round メソッド — 既定は最も近い偶数への丸め、MidpointRounding.AwayFromZero、偶数丸めが IEEE 754 に準拠し片寄りを減らすこと(平均の例)、精度が失われた 11.499999999999998 が切り下げられる例

確認日:2026-10-06。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

おうどん|癒しと創作をたのしむ雑食クリエイター
写経アプリやクレイセラピー、優しい和風デザインがすき。
ZARDと刀剣と文字に癒されて、今はアプリ作ってます。

コメント

コメントする

目次