note.nkmk.me

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

Date: 2018-01-21 / Modified: 2018-06-15 / tags: Python

Pythonで数値(浮動小数点float型または整数int型)を四捨五入や偶数への丸めで丸める場合、以下の方法がある。

  • 組み込み関数のround()
  • 標準ライブラリdecimalquantize()
  • 新たな関数を定義

それぞれについて、小数を任意の桁数で丸める方法、整数を任意の桁数で丸める方法を示す。

なお、組み込み関数のroundは一般的な四捨五入ではなく偶数への丸めなので注意。詳細は後述。

pandasでの数値の丸め(四捨五入、偶数への丸め)については以下の記事を参照。

スポンサーリンク

組み込み関数round()

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

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

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

浮動小数点float型に対する処理の例を示す。

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

f = 123.456

print(round(f))
# 123

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

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

正の整数を指定すると小数点以下の桁、負の整数を指定すると整数の桁(位)の指定となる。-1は10の位に丸め、-2は100の位に丸める。0は整数(1の位)に丸められるが省略した場合と異なり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型に対する処理の例を示す。

第二引数を省略した場合や0、正の整数を指定した場合は元の値をそのまま返す。負の整数を指定すると対応する整数の桁に丸められる。いずれの場合も整数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()は一般的な四捨五入ではなく、偶数への丸め

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

公式ドキュメントに書いてあるように、0.50に丸められたり、50に丸められたりする。

print('0.4 =>', round(0.4))
print('0.5 =>', round(0.5))
print('0.6 =>', round(0.6))
# 0.4 => 0
# 0.5 => 0
# 0.6 => 1

print('4 =>', round(4, -1))
print('5 =>', round(5, -1))
print('6 =>', round(6, -1))
# 4 => 0
# 5 => 0
# 6 => 10

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

端数が0.5より小さいなら切り捨て、端数が0.5より大きいならは切り上げ、端数がちょうど0.5なら切り捨てと切り上げのうち結果が偶数となる方へ丸める。
端数処理 - Wikipedia

0.5が常に切り捨てられるわけではない。

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

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

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 を与えます。これはバグではありません: これはほとんどの小数が浮動小数点数で正確に表せないことの結果です。
2. 組み込み関数 round() — Python 3.6.3 ドキュメント

一般的な四捨五入や小数に対して正確な偶数への丸めを実現したい場合は、後述の標準ライブラリdecimalquantizeか新たな関数を定義する方法を使う。

また、Python2のround()は偶数への丸めではなく四捨五入なので注意。

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

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

decimalモジュールのquantize()メソッドを使うと、丸めモードを指定して数値を丸めることが可能。

quantize()メソッドの引数roundingROUND_HALF_UPを指定すると一般的な四捨五入、ROUND_HALF_EVENを指定すると偶数への丸めとなる。

decimalモジュールは標準ライブラリなので追加のインストールは不要だが、インポートは必要。

from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN

Decimalオブジェクトの生成

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

引数にfloat型を指定すると、実際にどのような値として扱われているかが分かる。

print(Decimal(0.05))
# 0.05000000000000000277555756156289135105907917022705078125

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

例のように0.05は正確に0.05とは扱われていない。上述の組み込み関数round()で例の0.05を含む小数の値で想定とは異なる値に丸められたのはこれが原因。

0.51/2(2の-1乗)なので2進法で正確に表現できる。

print(Decimal(0.5))
# 0.5

float型ではなく文字列str型を指定すると正確にその値のDecimal型として扱われる。

print(Decimal('0.05'))
# 0.05

小数を任意の桁数で四捨五入・偶数への丸め

Decimal型のオブジェクトからquantize()を呼び出し、値を丸める。

quantize()の第一引数に求めたい桁数と同じ桁数の数値を'0.1''0.01'のように文字列で指定する。

さらに引数roundingに丸めモードを指定する。ROUND_HALF_UPを指定すると一般的な四捨五入となる。

f = 123.456

print(Decimal(str(f)).quantize(Decimal('0'), rounding=ROUND_HALF_UP))
# 123

print(Decimal(str(f)).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP))
# 123.5

print(Decimal(str(f)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# 123.46

組み込み関数round()と異なり、0.51に丸められる。

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

引数roundingROUND_HALF_EVENを指定すると組み込み関数round()と同様に偶数への丸めとなる。

上述のように、Decimal()の引数に浮動小数点float型で指定するとfloat型の実際の値と等しい値のDecimalオブジェクトとして扱われるので、quantize()メソッドを使っても組み込み関数round()と同様に、想定とは異なる結果となる。

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

print('0.05 =>', Decimal(0.05).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.15 =>', Decimal(0.15).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.25 =>', Decimal(0.25).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.35 =>', Decimal(0.35).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.45 =>', Decimal(0.45).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
# 0.05 => 0.1
# 0.15 => 0.1
# 0.25 => 0.2
# 0.35 => 0.3
# 0.45 => 0.5

Decimal()の引数に文字列str型で指定すると正確にその値のDecimalオブジェクトとして扱われるので、想定通りの結果となる。

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

0.5float型でも正確に扱えるので、整数に丸める場合はDecimal()の引数にfloat型で指定しても特に問題はないが、小数点以下の桁数に丸める場合は文字列str型で指定しておいたほうが無難。

例えば2.675float型では実際は2.67499....であるため、小数点以下2桁に丸める場合はDecimal()に文字列で指定しないと、四捨五入(ROUND_HALF_UP)でも偶数への丸め(ROUND_HALF_EVEN)でも想定と異なる結果となる。

print(Decimal(2.675))
# 2.67499999999999982236431605997495353221893310546875

print(Decimal(2.675).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# 2.67

print(Decimal(str(2.675)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# 2.68

print(Decimal(2.675).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN))
# 2.67

print(Decimal(str(2.675)).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN))
# 2.68

なお、quantize()メソッドが返すのはDecimal型。float型の数値と演算したりするときはfloat()float型に変換しないとエラーになるので注意。

d = Decimal('123.456').quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)

print(d)
# 123.46

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

# print(1.2 + d)
# TypeError: unsupported operand type(s) for +: 'float' and 'decimal.Decimal'

print(1.2 + float(d))
# 124.66

整数を任意の桁数で四捨五入・偶数への丸め

整数の桁に丸めたい場合、第一引数に'10'のように指定しても所望の結果は得られない。

i = 99518

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

これは、quantize()ではDecimalオブジェクトの指数exponentに応じて丸め処理を行っているが、Decimal('10')は指数が1ではなく0であることが原因。

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)

そのままだと結果もEを使った指数表記になるので、通常の表記にしたい、あるいは、丸めたあとで整数int型と演算したい、といった場合は、int()で変換する。

print(Decimal(i).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP))
# 9.952E+4

print(int(Decimal(i).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
# 99520

print(int(Decimal(i).quantize(Decimal('1E2'), rounding=ROUND_HALF_UP)))
# 99500

print(int(Decimal(i).quantize(Decimal('1E3'), rounding=ROUND_HALF_UP)))
# 100000

引数roundingROUND_HALF_UPを指定すると一般的な四捨五入となり、例えば510に丸められる。

print('4 =>', int(Decimal(4).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
print('5 =>', int(Decimal(5).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
print('6 =>', int(Decimal(6).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
# 4 => 0
# 5 => 10
# 6 => 10

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

新たな関数を定義

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

いろんなやり方が考えられるが、例えば以下のような関数。

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

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

my_round_int = lambda x: int((x * 2 + 1) // 2)
source: my_round.py

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

以下は参考まで。

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

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

print(my_round_int(f))
# 123

print(my_round(f, 1))
# 123.5

print(my_round(f, 2))
# 123.46
source: my_round.py

roundと違い、一般的な四捨五入の通り0.51.0になる。

print(int(my_round(0.4)))
print(int(my_round(0.5)))
print(int(my_round(0.6)))
# 0
# 1
# 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(int(my_round(4, -1)))
print(int(my_round(5, -1)))
print(int(my_round(6, -1)))
# 0
# 10
# 10
source: my_round.py

注意点: 負の値の場合

上の例の関数だと、-0.50に丸められる。

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

負の値に対する四捨五入は様々な考え方があるが、-0.5-1としたい場合は、例えば以下のように修正できる。

import math

def my_round2(val, digit=0):
    p = 10 ** digit
    s = math.copysign(1, val)
    return (s * val * p * 2 + 1) // 2 / p * s

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

これらの関数をNumPy配列ndarray対応にしたものは以下の記事を参照。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事