Pythonで小数・整数を四捨五入するroundとDecimal.quantize

Modified: | Tags: Python, 数値

Pythonで数値(浮動小数点数floatや整数int)を四捨五入や偶数への丸めで丸める方法について説明する。

組み込み関数round()は一般的な四捨五入ではなく偶数への丸めなので注意。一般的な四捨五入を実現するには標準ライブラリのdecimalモジュールを使うか、新たな関数を定義する。

四捨五入ではなく小数点以下の切り捨て・切り上げについては以下の記事を参照。

NumPyやpandasのround()については以下の記事を参照。

組み込み関数round()

組み込み関数としてround()が提供されている。モジュールをインポートすることなく使える。

第一引数に元の数値、第二引数ndigitsに桁数(何桁に丸めるか)を指定する。

小数を任意の桁数に丸める

浮動小数点数floatに対してround()を使う例を示す。

第二引数を省略すると整数に丸められる。型も整数intになる。

f = 123.456

print(round(f))
# 123

print(type(round(f)))
# <class 'int'>

第二引数を指定すると浮動小数点数floatを返す。

正の整数を指定すると小数点以下の桁、負の整数を指定すると整数の桁(位)の指定となる。-1は十の位、-2は百の位に丸める。0は整数(一の位)に丸めるが、省略した場合と異なりfloatを返す。

print(round(f, 1))
# 123.5

print(round(f, 2))
# 123.46

print(round(f, -1))
# 120.0

print(round(f, -2))
# 100.0

print(round(f, 0))
# 123.0

print(type(round(f, 0)))
# <class 'float'>

整数を任意の桁数に丸める

整数intに対してround()を使う例を示す。

第二引数を省略した場合や0、正の整数を指定した場合は元の値をそのまま返す。負の整数を指定すると対応する整数の桁に丸められる(-1は十の位、-2は百の位…)。いずれの場合も整数intを返す。

i = 99518

print(round(i))
# 99518

print(round(i, 2))
# 99518

print(round(i, -1))
# 99520

print(round(i, -2))
# 99500

print(round(i, -3))
# 100000

round()は一般的な四捨五入ではなく、偶数への丸め

組み込み関数round()による丸めは、一般的な四捨五入ではなく、偶数への丸め(銀行家の丸め、round to even)なので注意。

0.502.52に丸められたり、十の位に丸める場合に502520に丸められたりする。

print('0.5 =>', round(0.5))
print('1.5 =>', round(1.5))
print('2.5 =>', round(2.5))
print('3.5 =>', round(3.5))
print('4.5 =>', round(4.5))
# 0.5 => 0
# 1.5 => 2
# 2.5 => 2
# 3.5 => 4
# 4.5 => 4

print(' 5 =>', round(5, -1))
print('15 =>', round(15, -1))
print('25 =>', round(25, -1))
print('35 =>', round(35, -1))
print('45 =>', round(45, -1))
#  5 => 0
# 15 => 20
# 25 => 20
# 35 => 40
# 45 => 40

偶数への丸めの定義は以下の通り。

「偶数への丸め」(round to even)は、端数が0.5より小さいなら「切り捨て」、端数が0.5より大きいならば「切り上げ」、端数がちょうど0.5なら「切り捨て」と「切り上げ」のうち結果が偶数となる方へ丸める(つまり偶数+0.5なら「切り捨て」、奇数+0.5ならば「切り上げ」となる)。 端数処理 - 偶数への丸め(round to even) - Wikipedia

round() をサポートする組み込み型では、値は 10 のマイナス ndigits 乗の倍数の中で最も近いものに丸められます; 二つの倍数が同じだけ近いなら、偶数を選ぶ方に (そのため、例えば round(0.5)round(-0.5) は両方とも 0 に、 round(1.5)2 に) 丸められます。 組み込み関数 - round() — Python 3.12.0 ドキュメント

偶数へ丸められるのは端数がちょうど0.5のときなので、例えば2.52に丸められるが2.513に丸められる。

print('2.49 =>', round(2.49))
print('2.50 =>', round(2.5))
print('2.51 =>', round(2.51))
# 2.49 => 2
# 2.50 => 2
# 2.51 => 3

小数点以下1桁以降に丸めると、偶数への丸めの定義に当てはまらない場合もある。

print('0.05 =>', round(0.05, 1))
print('0.15 =>', round(0.15, 1))
print('0.25 =>', round(0.25, 1))
print('0.35 =>', round(0.35, 1))
print('0.45 =>', round(0.45, 1))
# 0.05 => 0.1
# 0.15 => 0.1
# 0.25 => 0.2
# 0.35 => 0.3
# 0.45 => 0.5

これは公式ドキュメントにもあるように、小数を浮動小数点数で正確に表せないことが原因。

注釈: 浮動小数点数に対する round() の振る舞いは意外なものかもしれません: 例えば、 round(2.675, 2) は予想通りの 2.68 ではなく 2.67 を与えます。これはバグではありません: これはほとんどの小数が浮動小数点数で正確に表せないことの結果です。
組み込み関数 - round() — Python 3.12.0 ドキュメント

例えば、表示桁数を増やすと浮動小数点数float0.15は実際には0.14999....であることが分かる。端数が0.05より小さいため0.1に丸められる。

print(f'{0.15:.20}')
# 0.14999999999999999445

正確に小数を表したい場合や一般的な四捨五入を実現したい場合は、次に紹介する標準ライブラリdecimalを使う。

標準ライブラリdecimalのquantize()

標準ライブラリのdecimalモジュールを使うと正確な十進浮動小数点数を扱うことができる。

標準ライブラリなので追加のインストールは不要だが、インポートは必要。以降のサンプルコードでは、以下のようにインポートしている。他の丸めモードを使いたい場合は明示的にインポートする必要があるので注意。

from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN

Decimalオブジェクトの生成

Decimal()Decimalオブジェクトを生成できる。

引数に浮動小数点数floatを指定すると、実際の値のDecimalが生成される。

print(Decimal(0.05))
# 0.05000000000000000277555756156289135105907917022705078125

print(type(Decimal(0.05)))
# <class 'decimal.Decimal'>

このように浮動小数点数の0.05は誤差を含む。組み込み関数round()0.05などの値が想定と異なる値に丸められたのはこれが原因。

0.51/2、2の-1乗)や0.251/4、2の-2乗)は2進数で正確に表現できる。

print(Decimal(0.5))
# 0.5

print(Decimal(0.25))
# 0.25

文字列strを指定すると正確にその値のDecimal型として扱われる。floatstrに変換するにはstr()を使う。

print(Decimal('0.05'))
# 0.05

print(Decimal(str(0.05)))
# 0.05

小数を任意の桁数で四捨五入

Decimalquantize()メソッドを使うと、丸めモードを指定して数値を丸められる。

第一引数に求めたい桁数と同じ桁数のDecimalを指定する。Decimal()に文字列で'0.1''0.01'などを指定すればよい。整数部分を丸めたい場合は'1E1'のように指数表記を使う。詳細は後述。

f = 123.456

print(Decimal(str(f)).quantize(Decimal('0'), ROUND_HALF_UP))
print(Decimal(str(f)).quantize(Decimal('0.1'), ROUND_HALF_UP))
print(Decimal(str(f)).quantize(Decimal('0.01'), ROUND_HALF_UP))
# 123
# 123.5
# 123.46

第二引数roundingに丸めモードを指定する。ROUND_HALF_UPは一般的な四捨五入。偶数への丸めである組み込み関数round()と異なり、0.51に丸められる。

print('0.4 =>', Decimal(str(0.4)).quantize(Decimal('0'), ROUND_HALF_UP))
print('0.5 =>', Decimal(str(0.5)).quantize(Decimal('0'), ROUND_HALF_UP))
print('0.6 =>', Decimal(str(0.6)).quantize(Decimal('0'), ROUND_HALF_UP))
# 0.4 => 0
# 0.5 => 1
# 0.6 => 1

ROUND_HALF_EVENは組み込み関数round()と同じ偶数への丸め。Decimal()の引数に文字列strで指定すると正確にその値として扱われるので、floatの誤差の影響を受けるround()とは異なり、想定通りの結果となる。

print('0.05 =>', Decimal(str(0.05)).quantize(Decimal('0.1'), ROUND_HALF_EVEN))
print('0.15 =>', Decimal(str(0.15)).quantize(Decimal('0.1'), ROUND_HALF_EVEN))
print('0.25 =>', Decimal(str(0.25)).quantize(Decimal('0.1'), ROUND_HALF_EVEN))
print('0.35 =>', Decimal(str(0.35)).quantize(Decimal('0.1'), ROUND_HALF_EVEN))
print('0.45 =>', Decimal(str(0.45)).quantize(Decimal('0.1'), ROUND_HALF_EVEN))
# 0.05 => 0.0
# 0.15 => 0.2
# 0.25 => 0.2
# 0.35 => 0.4
# 0.45 => 0.4

丸めモードの一覧は以下の公式ドキュメントを参照。ROUND_HALF_UPROUND_HALF_EVEN以外にも様々な丸めモードが使用可能。

quantize()メソッドが返すのはDecimalfloat()floatに変換できるが、当然ながら、その場合はfloatで表現できる値になる。

d = Decimal('123.456').quantize(Decimal('0.01'), ROUND_HALF_UP)
print(d)
# 123.46

print(type(d))
# <class 'decimal.Decimal'>

f = float(d)
print(f)
# 123.46

print(type(f))
# <class 'float'>

print(Decimal(f))
# 123.4599999999999937472239253111183643341064453125

print(Decimal(str(f)))
# 123.46

整数を任意の桁数で四捨五入

quantize()メソッドで整数の桁に丸める場合、第一引数に'10'のように指定しても所望の結果は得られない。

i = 99518
print(Decimal(i).quantize(Decimal('10'), ROUND_HALF_UP))
# 99518

これは、quantize()Decimalオブジェクトの指数exponentに応じて丸め処理を行うため。Eを用いる指数表記の文字列(例えば'1E1')を使う必要がある。

指数exponentas_tuple()メソッドで確認できる。

print(Decimal('10').as_tuple())
# DecimalTuple(sign=0, digits=(1, 0), exponent=0)

print(Decimal('1E1').as_tuple())
# DecimalTuple(sign=0, digits=(1,), exponent=1)

整数はint型で正確に表現できるのでDecimal()にそのまま整数intで指定しても問題ない。もちろん文字列で指定してもよい。

print(Decimal(i).quantize(Decimal('1E1'), ROUND_HALF_UP))
print(Decimal(i).quantize(Decimal('1E2'), ROUND_HALF_UP))
print(Decimal(i).quantize(Decimal('1E3'), ROUND_HALF_UP))
# 9.952E+4
# 9.95E+4
# 1.00E+5

特に有効数字を考慮せず、指数表記ではなく通常の表記にしたい場合は、公式ドキュメントで紹介されている関数を使う。

def remove_exponent(d):
    return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()

d = Decimal(i).quantize(Decimal('1E2'), ROUND_HALF_UP)
print(d)
# 9.95E+4

d_remove = remove_exponent(d)
print(d_remove)
# 99500

print(type(d_remove))
# <class 'decimal.Decimal'>

Decimalを整数intに変換するにはint()を使う。

i = int(d)
print(i)
# 99500

print(type(i))
# <class 'int'>

新たな関数を定義

decimalモジュールを使う方法は正確で安心だが、型変換などが面倒な場合は、新たに関数を定義して一般的な四捨五入を実現することもできる。

例えば以下のような関数を定義する。返り値は常に浮動小数点数float

def my_round(number, ndigits=0):
    p = 10**ndigits
    return (number * p * 2 + 1) // 2 / p
source: my_round.py

桁数を指定する必要が無く、常に小数点第一位を四捨五入して整数intに変換するのであれば、もっとシンプルな形にできる。

def my_round_int(number):
    return int((number * 2 + 1) // 2)
source: my_round.py

なお、業務で使う場合など正確を期す必要があるときはdecimalを使っておいたほうが無難。

以下は参考まで。

小数を任意の桁数で四捨五入

上で定義した関数の結果の例を示す。my_round()は常に浮動小数点数floatを返すので、整数intに変換したい場合はint()を使う。

f = 123.456

print(my_round(f))
# 123.0

print(int(my_round(f)))
# 123

print(my_round(f, 1))
# 123.5

print(my_round(f, 2))
# 123.46

print(my_round_int(f))
# 123
source: my_round.py

組み込み関数round()と異なり、一般的な四捨五入のように0.51になる。

print('0.4 =>', int(my_round(0.4)))
print('0.5 =>', int(my_round(0.5)))
print('0.6 =>', int(my_round(0.6)))
# 0.4 => 0
# 0.5 => 1
# 0.6 => 1
source: my_round.py

整数を任意の桁数で四捨五入

上で定義した関数の結果の例を示す。

i = 99518

print(int(my_round(i, -1)))
# 99520

print(int(my_round(i, -2)))
# 99500

print(int(my_round(i, -3)))
# 100000
source: my_round.py

組み込み関数round()と異なり、一般的な四捨五入のように510になる。

print('4 =>', int(my_round(4, -1)))
print('5 =>', int(my_round(5, -1)))
print('6 =>', int(my_round(6, -1)))
# 4 => 0
# 5 => 10
# 6 => 10
source: my_round.py

注意点: 負の値の場合

上で定義した関数だと、-0.50に丸められる。

print('-0.4 =>', int(my_round(-0.4)))
print('-0.5 =>', int(my_round(-0.5)))
print('-0.6 =>', int(my_round(-0.6)))
# -0.4 => 0
# -0.5 => 0
# -0.6 => -1
source: my_round.py

負の値に対する四捨五入は様々な考え方があるが、-0.5-1としたい場合は、例えば以下のようにする。組み込み関数abs()で入力値を絶対値に変換し、math.copysign()で取得した符号を最後に掛ける。

import math

def my_round2(number, ndigits=0):
    p = 10**ndigits
    return (abs(number) * p * 2 + 1) // 2 / p * math.copysign(1, number)

print('-0.4 =>', int(my_round2(-0.4)))
print('-0.5 =>', int(my_round2(-0.5)))
print('-0.6 =>', int(my_round2(-0.6)))
# -0.4 => 0
# -0.5 => -1
# -0.6 => -1
source: my_round.py

関連カテゴリー

関連記事