2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】四捨五入にはDecimal.quantize()を使う

Last updated at Posted at 2023-03-18

使用Version:Python 3.11.2

結論

Pythonで四捨五入する際にはround関数ではなく Decimal.quantize() を利用する
round関数は四捨五入するのではなく偶数丸め(銀行丸め)をする

120.5 を一の桁まで四捨五入する場合
from decimal import *  # あるいは from decimal import Decimal, ROUND_HALF_UP

# 121 が返る
Decimal("120.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP)
# 120 が返る
round(120.5)

decimalモジュール

decimalモジュールは十進数を正確に表現するためのPython3標準ライブラリである
https://docs.python.org/ja/3/library/decimal.html

decimalモジュールで四捨五入するための手順

  1. Decimal型のオブジェクトを作る
    • Decimal(<文字列>)で宣言する
    • 数値型を渡すと正しい精度で計算できないため必ず文字列型を渡すこと
  2. quantizeメソッドで四捨五入する
    • Decimal(<文字列>).quantize(有効桁、rounding=ROUND_HALF_UP)
      • 有効桁も Decimal型オブジェクト で表現する
      • ex. 一の桁までならDecimal("1")
      • ex. 小数点第一位までならDecimal("0.1")

丸めモードにROUND_HALF_UPを指定することで四捨五入する
切り上げや切り捨て処理に変更することも可能
https://docs.python.org/ja/3/library/decimal.html#rounding-modes

round関数

偶数丸め(銀行丸め)は結果の最終桁が偶数になるように丸め処理する

  • 最終桁が5のとき四捨五入の結果と異なる場合がある
  • つまり数によっては四捨五入の結果と偶数丸めの結果が一致することもある

0.1 から 1.0 を一の位まで丸める場合】

image.png

  • 0.5 のときについて
    • Decimal.quantize() は 1 を返している
      • 正しく四捨五入できている
    • round() は 0 を返している
      • 01 のうち偶数である 0 が選ばれる

1.1 から 2.0 を一の位まで丸める場合】

image.png

  • 1.5 のときについて
    • Decimal.quantize() は 2 を返している
      • 正しく四捨五入できている
    • round() も 2 を返している
      • 12 のうち偶数である 2 が選ばれる

実験

様々な関数を用いて丸め処理を実行する

test.py
from decimal import *
import unittest

# 四捨五入する関数(有効桁ごとに用意)
my_round_a = lambda n:Decimal(str(n)).quantize(Decimal('1'), rounding=ROUND_HALF_UP)
my_round_b = lambda n:Decimal(str(n)).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP)
my_round_c = lambda n:Decimal(str(n)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
my_round_d = lambda n:Decimal(str(n)).quantize(Decimal('0.001'), rounding=ROUND_HALF_UP)

# テストケースの作成
class testMyRound(unittest.TestCase):
    def setUp(self):
        pass

    def test_round_1(self):
        # 0.5
        self.assertEqual(round(0.5), 0)
        self.assertEqual(my_round_a(0.5), Decimal('1'))
        # 1.5
        self.assertEqual(round(1.5), 2)
        self.assertEqual(my_round_a(1.5), Decimal('2'))
        # 2.5
        self.assertEqual(round(2.5), 2)
        self.assertEqual(my_round_a(2.5), Decimal('3'))

    def test_round_2(self):
        # 1.5000を一の桁まで四捨五入
        self.assertEqual(round(1.5000), 2)
        self.assertEqual(round(1.5000, 0), 2.0)
        self.assertEqual(my_round_a(1.5000), Decimal('2'))
        # 1.4500を小数点第一位まで四捨五入
        self.assertEqual(round(1.4500, 1), 1.4)
        self.assertEqual(my_round_b(1.4500), Decimal('1.5'))
        # 1.5450を小数点第二位まで四捨五入
        self.assertEqual(round(1.5450, 2), 1.54)
        self.assertEqual(my_round_c(1.5450), Decimal('1.55'))
        # 1.5545を小数点第三位まで四捨五入
        self.assertEqual(round(1.5545, 3), 1.554)
        self.assertEqual(my_round_d(1.5545), Decimal('1.555'))

    def test_round_3(self):
        # 2.5000を一の桁まで四捨五入
        self.assertEqual(round(2.5000), 2)
        self.assertEqual(round(2.5000, 0), 2.0)
        self.assertEqual(my_round_a(2.5000), Decimal('3'))
        # 2.5500を小数点第一位まで四捨五入
        self.assertEqual(round(2.5500, 1), 2.5)
        self.assertEqual(my_round_b(2.5500), Decimal('2.6'))
        # 2.5550を小数点第二位まで四捨五入
        self.assertEqual(round(2.5550, 2), 2.56)
        self.assertEqual(my_round_c(2.5550), Decimal('2.56'))
        # 2.5555を小数点第三位まで四捨五入
        self.assertEqual(round(2.5555, 3), 2.555)
        self.assertEqual(my_round_d(2.5555), Decimal('2.556'))

    def tearDown(self):
        pass

if __name__ == "__main__":
    suite = unittest.TestSuite()
    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(testMyRound))
    unittest.TextTestRunner(verbosity=2).run(suite)
実行結果
test_round_1 (__main__.testMyRound) ... ok
test_round_2 (__main__.testMyRound) ... ok
test_round_3 (__main__.testMyRound) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

Decimal.quantize()を使うことで正しく四捨五入できることが確認できる
image.png
image.png
image.png

テストケースtest_round_3ではround関数で正しく偶数丸めできていない
その原因はfloatが浮動小数点数を正確に表せないことにある

https://docs.python.org/ja/3/library/functions.html?highlight=round#round
浮動小数点数に対する round() の振る舞いは意外なものかもしれません: 例えば、 round(2.675, 2) は予想通りの 2.68 ではなく 2.67 を与えます。これはバグではありません: これはほとんどの小数が浮動小数点数で正確に表せないことの結果です。

decimalモジュールを使わない方法

Python3の組み込み関数のみでも四捨五入することができる
(コメント欄にて情報いただきました。ありがとうございます。)

# 書き方その1
"""
Args:
   x: 四捨五入対象の値
   n: 桁の指定(round関数と同様)
"""
round2 = lambda x,n=0:(1 if x >= 0 else -1) * int(abs(x) * 10**n + 0.5)/10**n

# 書き方その2
def round2(x,n=0):
   if x>=0:
      int(x * 10**n + 0.5)/10**n
   else
      int(x * 10**n - 0.5)/10**n

負の数の四捨五入では一度絶対値について四捨五入してから符号を付ける
(JIS Z 8401規格にて定義されている)

  1. 絶対値を取得する
  2. 10の累乗倍する
  3. 0.5を足す
  4. int関数で端数を切り捨てする
  5. 10の累乗倍で割る
  6. 適切な符号を付ける

参考記事

2
4
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?