Pythonのround()で四捨五入がずれる?その理由と正しい使い方を徹底解説
はじめに
Pythonでプログラミングを行っていると、数値を扱う場面は非常に多くあります。特に、計算結果を整数や特定の小数点以下の桁数に丸める「丸め処理」は、日常的に使用する機能の一つです。Pythonには、そのための組み込み関数として round() が用意されています。
しかし、この round() 関数を使ったことがある方の中には、次のような経験をしたことがあるかもしれません。
“`python
round(2.5)
2
round(3.5)
4
“`
「あれ? round(2.5) は 3 になるはずじゃないの?」
多くの人が小学校で習う「四捨五入」では、小数点以下が .5 の場合は切り上げるのがルールでした。そのため、round(2.5) の結果が 2 になるのは、直感に反するように感じられ、「Pythonのバグだ」「round()は正しく動作しない」と誤解してしまうことがあります。
結論から言うと、これはバグではありません。Pythonの round() 関数は、私たちが慣れ親しんだ「四捨五入」とは異なる、より公平で数学的に優れた丸め方式を採用しているのです。さらに、コンピュータが内部で小数を扱う際の「誤差」も、この問題に深く関わっています。
この記事では、なぜ round() が一見すると「ずれる」ように見えるのか、その背景にある「銀行家の丸め」という考え方から、コンピュータの浮動小数点数の仕組みまでを、初心者にも分かりやすく、かつ深く掘り下げて徹底解説します。
さらに、一般的な「四捨五入」や、金融計算などで求められる厳密な丸め処理をPythonで正しく実装するための具体的な方法も紹介します。
この記事を読み終える頃には、あなたはPythonにおける数値の丸め処理を完全にマスターし、自信を持って数値を扱えるようになっているでしょう。
1. round()関数の基本的な使い方
まずは、round() 関数の基本的な構文と使い方を確認しておきましょう。
round() は、数値を丸めるための組み込み関数で、2つの引数を取ります。
python
round(number, ndigits=None)
number: 丸めの対象となる数値(整数または浮動小数点数)。ndigits(オプション): 丸めた後の小数点以下の桁数を指定する整数。省略した場合、最も近い整数に丸められ、返り値も整数 (int型) になります。
ndigits を省略した場合 (整数への丸め)
ndigits を省略すると、number は最も近い整数に丸められます。
“`python
.5未満は切り捨て
round(3.14)
3
.5より大きい場合は切り上げ
round(3.78)
4
負の数の場合も同様
round(-3.14)
-3
round(-3.78)
-4
“`
ここまでは直感的で分かりやすいですね。問題は .5 の場合です。
“`python
round(2.5)
2
round(3.5)
4
“`
先ほども見たように、.5 の場合の挙動が一定ではありません。この謎については、後のセクションで詳しく解き明かしていきます。
ndigits を指定した場合 (小数点以下の桁数を指定した丸め)
ndigits に正の整数を指定すると、その桁数まで小数点以下が丸められます。
“`python
小数点第2位で丸める (第3位を基準)
round(3.14159, 2)
3.14
小数点第3位で丸める (第4位を基準)
round(3.14159, 3)
3.142
“`
ndigits に負の整数を指定することも可能です。この場合、小数点より左側、つまり整数部分が丸められます。
“`python
10の位で丸める (-1を指定)
round(1234.56, -1)
1230.0
100の位で丸める (-2を指定)
round(1234.56, -2)
1200.0
“`
このように、round() は非常に柔軟な丸め処理が可能ですが、その核心にある丸め戦略が、多くの混乱の原因となっています。
2. 「四捨五入がずれる」現象の正体:銀行家の丸め
round(2.5) が 2 に、round(3.5) が 4 になる。この一見奇妙な振る舞いの理由は、Python 3 の round() 関数が、私たちが一般的に「四捨五入」と呼ぶ方法(Round half up)ではなく、「最近接偶数への丸め (Round half to even)」 という方式を採用しているためです。
この方式は、通称「銀行家の丸め (Banker’s Rounding)」や「JIS丸め」とも呼ばれます。
「銀行家の丸め」のルール
銀行家の丸めのルールは非常にシンプルです。
丸める対象の数値が、2つの整数(または丸め先の桁)のちょうど中間にある場合(例: 2.5, 3.5, 4.15など)、丸めた結果の末尾が偶数になる方へ丸める。
このルールを先ほどの例に適用してみましょう。
-
round(2.5)の場合:2.5は2と3のちょうど中間です。- 丸め先の候補は
2(偶数) と3(奇数) です。 - 結果が偶数になる
2が選ばれます。
-
round(3.5)の場合:3.5は3と4のちょうど中間です。- 丸め先の候補は
3(奇数) と4(偶数) です。 - 結果が偶数になる
4が選ばれます。
小数点以下の桁数を指定した場合も同じです。
-
round(2.45, 1)の場合:- 小数点第1位に丸めるため、
2.4と2.5のどちらかに丸めます。 - 丸めた結果の末尾(小数点第1位)が偶数になるのは
2.4です。 - したがって、
round(2.45, 1)の結果は2.4となります。
- 小数点第1位に丸めるため、
-
round(2.55, 1)の場合:2.5と2.6のどちらかに丸めます。- 丸めた結果の末尾(小数点第1位)が偶数になるのは
2.6です。 - したがって、
round(2.55, 1)の結果は2.6となります。
このように、丸める桁の次が 5 の場合に、結果が偶数になるように切り上げたり切り捨てたりするのが「銀行家の丸め」の核心です。5以外の数字(0-4, 6-9)の場合は、通常の四捨五入と同じように、最も近い方に丸められます。
3. なぜ「銀行家の丸め」が採用されているのか?
なぜPythonは、多くの人にとって直感的ではない「銀行家の丸め」を標準の挙動として採用したのでしょうか?その理由は、この方式が持つ「公平性」にあります。
3.1. 伝統的な「四捨五入」の問題点:統計的バイアス
私たちが慣れ親しんでいる「四捨五入」(.5を常に切り上げる)は、一見公平に見えますが、大量のデータを扱う統計的な観点から見ると、ある種の偏り(バイアス)を生み出してしまいます。
考えてみてください。丸める桁の次が0, 1, 2, 3, 4の場合は切り捨てられます(5パターン)。一方、5, 6, 7, 8, 9の場合は切り上げられます(5パターン)。ここまでは公平に見えます。しかし、ちょうど中間である .5 の扱いが問題です。これを常に切り上げると、切り上げられるケースが切り捨てられるケースよりもわずかに多くなってしまいます。
例えば、次のようなデータのリストがあり、これを整数に丸めて合計値を計算するとします。
python
data = [1.5, 2.5, 3.5, 4.5]
original_sum = sum(data) # -> 12.0
このデータを「四捨五入」で丸めてみましょう。
- 1.5 → 2
- 2.5 → 3
- 3.5 → 4
- 4.5 → 5
丸めた後の合計値は 2 + 3 + 4 + 5 = 14 となり、元の合計値 12.0 と比べて +2 の誤差が生じています。これは、.5 がすべて切り上げられたために、全体として上方向に偏ってしまった(正のバイアスがかかった)結果です。
このようなバイアスは、データ数が少なければ無視できるかもしれませんが、金融計算、科学技術計算、センサーデータの集計など、何百万、何千万というデータを扱う分野では、無視できないほど大きな誤差につながる可能性があります。
3.2. 「銀行家の丸め」の利点:公平性と精度の向上
ここで「銀行家の丸め」の真価が発揮されます。銀行家の丸めは、中間値 .5 を切り上げるか切り捨てるかを、結果が偶数になるか奇数になるかで判断します。これにより、切り上げと切り捨てが発生する確率がほぼ均等(50%ずつ)になります。
先ほどのデータを「銀行家の丸め」で処理してみましょう。
- 1.5 → 2 (結果が偶数になる方へ)
- 2.5 → 2 (結果が偶数になる方へ)
- 3.5 → 4 (結果が偶数になる方へ)
- 4.5 → 4 (結果が偶数になる方へ)
丸めた後の合計値は 2 + 2 + 4 + 4 = 12 となり、元の合計値 12.0 と完全に一致します。誤差は 0 です。
このように、銀行家の丸めは、多数のデータを集計した際に生じる丸め誤差の累積を最小限に抑え、統計的なバイアスを効果的に打ち消すことができます。この公平性と精度の高さから、IEEE 754(浮動小数点数に関する標準規格)でもデフォルトの丸め方式として推奨されており、多くのプログラミング言語やシステムで採用されています。
Pythonが round() 関数のデフォルトとしてこの方式を採用したのは、一部のユーザーの直感よりも、数学的な正しさ、公平性、そして他のシステムとの互換性を重視した結果なのです。
4. さらなる落とし穴:浮動小数点数の内部表現
「なるほど、銀行家の丸めが理由だったのか。じゃあ、そのルールさえ覚えておけば round() は完璧に使いこなせるんだな!」
そう思った方もいるかもしれませんが、残念ながら、話はもう少し複雑です。round() の挙動を理解するには、もう一つの重要な概念、コンピュータにおける「浮動小数点数」の仕組みを知る必要があります。
実は、次のような不可解な現象も起こります。
“`python
round(2.675, 2)
2.67
“`
銀行家の丸めのルールに従えば、2.675 を小数点第2位に丸める場合、2.67 と 2.68 の中間なので、末尾が偶数になる 2.68 が返ってくるはずです。しかし、実際の結果は 2.67 です。
これはなぜでしょうか? round() がバグっているのでしょうか?
いいえ、これもバグではありません。原因は、2.675 という数値が、コンピュータの内部では私たちが期待している通りの値として表現されていないことにあります。
4.1. コンピュータは10進数の小数を正確に表現できない
私たちは普段、数値を10進数で扱っていますが、コンピュータの内部では、すべてのデータは2進数(0と1の羅列)で処理されます。
整数であれば、10進数から2進数への変換は問題なく行えます。しかし、小数の場合は事情が異なります。10進数では有限の桁で表現できる小数(例: 0.1, 0.2, 2.675)が、2進数に変換すると無限小数になってしまうことがあるのです。
これは、私たちが10進数で 1/3 を 0.3333... としか表現できないのと同じ現象です。
例えば、0.1 という数値を2進数で表現しようとすると、0.0001100110011... というように、0011 の繰り返しが無限に続く循環小数になります。コンピュータのメモリは有限なので、この無限に続く数値をどこかで打ち切って、近似値として保存するしかありません。
この浮動小数点数の標準的な表現方法が IEEE 754 です。この方式では、非常に高い精度で数値を表現できますが、それでも一部の10進数小数では微小な誤差(表現誤差)が避けられません。
Pythonの float 型も、このIEEE 754に準拠しています。2.675 という値が内部でどのように保持されているか、実際に見てみましょう。
“`python
f-stringを使って内部の値を詳しく表示してみる
print(f'{2.675:.55f}’)
2.6749999999999998223643160599749535322189331054687500000
“`
驚くべきことに、2.675 はコンピュータ内部では 2.674999... という、2.675 に非常に近いけれども、わずかに小さい値として格納されていたのです。
4.2. 内部表現が round() の結果に与える影響
この事実を知れば、round(2.675, 2) の結果が 2.67 になる理由は明らかです。
- Pythonは
round(2.675, 2)というコードを受け取ります。 - しかし、
2.675というリテラルは、メモリ上では2.674999...という近似値として表現されます。 round()関数が処理するのは、この2.674999...という値です。- 小数点第3位の数字は
4なので、round()はこれを単純に切り捨てます。 - 結果として
2.67が返されます。
この場合、丸める桁の次が 5 ではないため、「銀行家の丸め」のルールが適用されるまでもなく、単純な切り捨てが行われたわけです。
これが、「round() はバグだらけだ」と誤解される最大の原因です。round() 関数自体は仕様通りに忠実に動作していますが、その入力となる float 型の数値が、そもそも微小な誤差を含んでいる可能性があるのです。
5. 目的別:正しい丸め処理の実装方法
では、私たちはこの複雑な状況にどう対処すればよいのでしょうか。round() が期待通りに動かないことがあるのなら、何を使えばよいのでしょうか。
幸い、Pythonには目的に応じて丸め処理を正確にコントロールするための、いくつかの方法が用意されています。
5.1. 一般的な「四捨五入」を実装したい場合
銀行家の丸めではなく、小学生の時に習ったような「.5は常に切り上げる」方式の四捨五入を実装したい場面はよくあります。そのための最も確実で推奨される方法は、decimal モジュールを使用することです。
Decimal モジュールを使う (推奨)
decimal モジュールは、10進数をベースとした、正確な小数演算を提供するために設計されています。float 型が2進数ベースで誤差を生じる可能性があるのに対し、Decimal 型は私たちが書いた通りの10進数の値をそのまま保持し、計算することができます。金融計算や請求書処理など、1セントの誤差も許されない場面では必須のモジュールです。
Decimal を使って四捨五入を行うには、quantize() メソッドを使用します。
重要なポイント: float 型の誤差を避けるため、Decimal オブジェクトは文字列から生成するのが鉄則です。
“`python
from decimal import Decimal, ROUND_HALF_UP
floatから生成すると、誤差も引き継いでしまう
float_val = 2.675
d_from_float = Decimal(float_val)
print(f’floatから生成: {d_from_float}’) # -> Decimal(‘2.674999…’)
文字列から生成すれば、正確な値を保持できる
string_val = ‘2.675’
d_from_string = Decimal(string_val)
print(f’文字列から生成: {d_from_string}’) # -> Decimal(‘2.675’)
quantize()で丸め処理を実行
第1引数: 丸めの桁数を指定 (‘0.01’は小数点第2位)
rounding引数: 丸めモードを指定
ROUND_HALF_UP = 四捨五入
result = d_from_string.quantize(Decimal(‘0.01’), rounding=ROUND_HALF_UP)
print(f’四捨五入の結果: {result}’) # -> 2.68
print(type(result)) # ->
“`
quantize() メソッドの rounding 引数には、様々な丸めモードを指定できます。
ROUND_HALF_UP: 四捨五入(.5は0から遠ざかる方向に丸める)ROUND_HALF_EVEN: 銀行家の丸め(round()と同じ挙動)ROUND_CEILING: 切り上げ(正の無限大方向へ)ROUND_FLOOR: 切り捨て(負の無限大方向へ)ROUND_DOWN: 0方向への切り捨てROUND_UP: 0から遠ざかる方向への切り上げ
このように、Decimal を使えば、意図した通りの正確な丸め処理を、明示的に指定して実行できます。
Decimal のメリット・デメリット:
* メリット:
* 10進数を正確に表現できるため、誤差がない。
* 多様な丸めモードを明示的に指定でき、制御性が高い。
* デメリット:
* float に比べて計算速度が遅い。
* float との計算には明示的な型変換が必要で、コードが少し冗長になる。
科学技術計算のような速度が最優先される場面以外では、特に金銭を扱う場合は、Decimal を使うのが最も安全で確実な選択です。
5.2. 切り上げ・切り捨てをしたい場合
常に切り上げたり、切り捨てたりしたい場合は、math モジュールが便利です。
math.ceil(x):x以上の最小の整数を返す(天井関数、切り上げ)。math.floor(x):x以下の最大の整数を返す(床関数、切り捨て)。math.trunc(x):xの小数部分を切り捨て、0に近い整数を返す。
“`python
import math
x = 3.14
y = -3.14
切り上げ (ceil)
print(f’math.ceil({x}) -> {math.ceil(x)}’) # -> 4
print(f’math.ceil({y}) -> {math.ceil(y)}’) # -> -3
切り捨て (floor)
print(f’math.floor({x}) -> {math.floor(x)}’) # -> 3
print(f’math.floor({y})-> {math.floor(y)}’) # -> -4
0への切り捨て (trunc)
print(f’math.trunc({x}) -> {math.trunc(x)}’) # -> 3
print(f’math.trunc({y}) -> {math.trunc(y)}’) # -> -3
“`
特定の小数点以下の桁数で切り上げ・切り捨てを行いたい場合は、一度桁を上げてから計算し、元に戻すというテクニックが使えます。
“`python
小数点第2位で切り上げ
def ceil_decimal(num, digits):
factor = 10 ** digits
return math.ceil(num * factor) / factor
小数点第2位で切り捨て
def floor_decimal(num, digits):
factor = 10 ** digits
return math.floor(num * factor) / factor
val = 3.14159
print(ceil_decimal(val, 2)) # -> 3.15
print(floor_decimal(val, 2)) # -> 3.14
“`
ただし、この方法も入力が float 型である限り、浮動小数点数の表現誤差の影響を受ける可能性が残ります。厳密性が必要な場合は、ここでも Decimal の quantize() と ROUND_CEILING や ROUND_FLOOR を使うのが最も安全です。
5.3. 用途別実装方法のまとめ表
| 目的 | 推奨される実装方法 | コード例 | 注意点 |
|---|---|---|---|
| 銀行家の丸め (統計的に公平) |
round() (組み込み) |
round(2.5) |
浮動小数点数誤差の影響を受ける。round(2.675, 2) は 2.67 になる。 |
| 一般的な四捨五入 (.5は常に切り上げ) |
Decimal.quantize |
Decimal('2.5').quantize(Decimal('1'), rounding=ROUND_HALF_UP) |
文字列からDecimalを生成すること。金融計算など正確性が求められる場合に最適。 |
| 切り上げ | math.ceil() |
math.ceil(3.14) |
整数への切り上げのみ。小数点以下の桁指定は工夫が必要。 |
| 切り捨て | math.floor() |
math.floor(3.14) |
整数への切り捨てのみ。小数点以下の桁指定は工夫が必要。 |
| 厳密な丸め全般 | Decimal.quantize |
Decimal('...').quantize(..., rounding=...) |
速度は float より劣るが、最も安全で制御性が高い方法。 |
6. Python 2 と Python 3 の round() の違い
この round() の挙動は、Pythonのバージョンによって異なることにも注意が必要です。
- Python 2:
round()は、私たちが慣れ親しんだ「四捨五入」に近い挙動(Round half up)をします。.5は0から遠い方の整数に丸められます。
python
# Python 2.7の実行結果
# >>> round(2.5)
# 3.0
# >>> round(3.5)
# 4.0 - Python 3:
round()は、この記事で解説してきた「銀行家の丸め」(Round half to even)を採用しています。
この変更は、Python 3がより数学的に正しく、標準規格に準拠した挙動を目指した結果です。古いPython 2のコードをPython 3に移行する際には、round() の結果が変わる可能性があるため、特に注意が必要です。
まとめ
長くなりましたが、Pythonの round() 関数にまつわる謎と、その正しい使い方についてまとめます。
round()はバグではない:round()が「四捨五入とずれる」ように見えるのは、バグではなく、「銀行家の丸め(最近接偶数への丸め)」という仕様に基づいているためです。- 銀行家の丸めは公平: この丸め方は、中間値
.5を切り上げたり切り下げたりすることで、多数のデータを集計した際の統計的な偏り(バイアス)を最小限に抑える、公平で優れた方法です。 - 浮動小数点数の誤差に注意:
round()の挙動をさらに複雑にしているのが、コンピュータ内部での浮動小数点数 (float) の表現誤差です。2.675のような数値が内部では2.674999...として扱われるため、round(2.675, 2)の結果が2.67のようになってしまいます。 Decimalモジュールが最強の解決策: 一般的な「四捨五入」や、金融計算のような1円の誤差も許されない厳密な丸め処理を行いたい場合は、decimalモジュールを使用するのが最も安全で確実です。Decimalオブジェクトを文字列から生成し、quantize()メソッドで丸めモードを明示的に指定しましょう。- 目的に応じた使い分けを: 高速な科学技術計算で統計的な公平性が重要な場合は
round()、常に切り上げ・切り捨てたい場合はmath.ceil()/floor()、そして何よりも正確性が求められる場合はDecimalと、目的に応じて最適なツールを使い分けることが、Pythonで数値を正しく扱うための鍵となります。
round() 関数の挙動は、一見するとPythonの奇妙な仕様に見えるかもしれません。しかしその裏には、コンピュータ科学の深い原理と、数学的な公平性への配慮が隠されています。この記事が、あなたのPythonにおける数値計算への理解を深め、より堅牢で正確なコードを書くための一助となれば幸いです。