note.nkmk.me

pandasで数値を丸める(四捨五入、偶数への丸め)

Date: 2018-06-15 / tags: Python, pandas

pandas.DataFrame, pandas.Seriesの数値を丸める場合、round()メソッドを使う。round()メソッドは四捨五入ではなく偶数への丸めとなる。

四捨五入したい場合は標準ライブラリdecimalモジュールのquantize()を各要素に適用する。

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

今回のサンプルコードでの各種バージョンは以下の通り。

import pandas as pd
import numpy as np
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN
import platform

print(platform.python_version())
print(pd.__version__)
print(np.__version__)
# 3.6.5
# 0.23.0
# 1.14.3

以下の内容について説明する。

  • pandas.Seriesround()メソッドの使い方
  • pandas.DataFrameround()メソッドの使い方
    • 全ての列に一律で桁数指定
    • 各列に個別に桁数指定
  • pandasのround()メソッドは偶数への丸め
  • decimalモジュールのquantize()で正確な四捨五入・偶数への丸め

数値を文字列に変換する方法、データを変換するのではなく表示設定を変更する方法については以下の記事を参照。

スポンサーリンク

pandas.Seriesのround()メソッドの使い方

浮動小数点float型のpandas.Seriesを例とする。

s_f = pd.Series([123.456, 654.123])

print(s_f)
# 0    123.456
# 1    654.123
# dtype: float64

round()の引数に整数nを渡すと、小数点以下n桁に丸められる。

引数を省略するとn=0となり整数に丸められる。データ型はfloatのまま。整数int型に変換したい場合はastype()メソッドを使う。

print(s_f.round())
# 0    123.0
# 1    654.0
# dtype: float64

print(s_f.round().astype(int))
# 0    123
# 1    654
# dtype: int64

正の整数は小数点以下の桁数指定、負の整数は整数の桁数指定になる。-1は10の位、-2は100の位に丸められる。

print(s_f.round(2))
# 0    123.46
# 1    654.12
# dtype: float64

print(s_f.round(-2))
# 0    100.0
# 1    700.0
# dtype: float64

次に、整数int型のpandas.Seriesを例とする。

s_i = pd.Series([123, 654])

print(s_i)
# 0    123
# 1    654
# dtype: int64

round()の引数が0(省略した場合も含む)、正の整数の場合は変化なし。負の整数を指定すると対応する位に丸められる。

print(s_i.round())
# 0    123
# 1    654
# dtype: int64

print(s_i.round(2))
# 0    123
# 1    654
# dtype: int64

print(s_i.round(-2))
# 0    100
# 1    700
# dtype: int64

round()メソッドは数値が丸められた新しいオブジェクトを返し、元のオブジェクトは変更されない。pandas.DataFrameround()メソッドでも同様。

s_i_round = s_i.round(-2)

print(s_i_round)
# 0    100
# 1    700
# dtype: int64

print(s_i)
# 0    123
# 1    654
# dtype: int64

なお、round()メソッドによる数値の丸めは四捨五入ではなく偶数への丸めとなる。詳しくは後述。

pandas.DataFrameのround()メソッドの使い方

整数int型、浮動小数点float型、文字列str型(列のデータ型はobject型)の列を含む以下のpandas.DataFrameを例とする。

df = pd.DataFrame({'f': [123.456, 654.321], 'i': [123, 654], 's': ['abc', 'xyz']})

print(df)
#          f    i    s
# 0  123.456  123  abc
# 1  654.321  654  xyz

print(df.dtypes)
# f    float64
# i      int64
# s     object
# dtype: object

全ての列に一律で桁数指定

round()の引数を省略した場合および整数を指定した場合は全ての列が対応する桁数に丸められる。文字列は変化なし。

print(df.round())
#        f    i    s
# 0  123.0  123  abc
# 1  654.0  654  xyz

print(df.round(2))
#         f    i    s
# 0  123.46  123  abc
# 1  654.32  654  xyz

print(df.round(-2))
#        f    i    s
# 0  100.0  100  abc
# 1  700.0  700  xyz

各列に個別に桁数指定

キーを列名、値を桁数とする辞書を引数に指定すると、各列に個別に桁数を指定することができる。指定しない列は変化なし。

print(df.round({'f': 2, 'i': -1}))
#         f    i    s
# 0  123.46  120  abc
# 1  654.32  650  xyz

print(df.round({'i': -2}))
#          f    i    s
# 0  123.456  100  abc
# 1  654.321  700  xyz

pandasのround()メソッドは偶数への丸め

pandasのround()メソッドによる丸めは四捨五入ではなく、偶数への丸め。0.502.52に丸められたりする。

s = pd.Series([0.5, 1.5, 2.5, 3.5, 4.5])

print(s)
# 0    0.5
# 1    1.5
# 2    2.5
# 3    3.5
# 4    4.5
# dtype: float64

print(s.round())
# 0    0.0
# 1    2.0
# 2    2.0
# 3    4.0
# 4    4.0
# dtype: float64

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

偶数への丸め(round to even)は、端数が0.5より小さいなら切り捨て、端数が0.5より大きいならは切り上げ、端数がちょうど0.5なら切り捨てと切り上げのうち結果が偶数となる方へ丸める。JIS Z 8401で規則Aとして定められていて、規則B(四捨五入)より「望ましい」とされている。
端数処理 - Wikipedia

「端数が0.5より小さいなら切り捨て、端数が0.5より大きいならは切り上げ、端数がちょうど0.5なら切り捨てと切り上げのうち結果が偶数となる方へ丸める」という定義の通り、例えば10の位に丸める場合、2520に丸められるが、25.130に丸められる。

s = pd.Series([5, 15, 25, 5.1, 15.1, 25.1])

print(s)
# 0     5.0
# 1    15.0
# 2    25.0
# 3     5.1
# 4    15.1
# 5    25.1
# dtype: float64

print(s.round(-1))
# 0     0.0
# 1    20.0
# 2    20.0
# 3    10.0
# 4    20.0
# 5    30.0
# dtype: float64

小数点以下の桁数への丸めは注意が必要。

Pythonの組み込み関数round()とNumPyのaround()関数(= round()関数)は値によって異なる結果となるが、pandasのround()メソッドはNumPyのaround()関数と同じ結果となる。

print('Python round')
print('0.005 => ', round(0.005, 2))
print('0.015 => ', round(0.015, 2))
print('0.025 => ', round(0.025, 2))
print('0.035 => ', round(0.035, 2))
print('0.045 => ', round(0.045, 2))
print('2.675 => ', round(2.675, 2))
# Python round
# 0.005 =>  0.01
# 0.015 =>  0.01
# 0.025 =>  0.03
# 0.035 =>  0.04
# 0.045 =>  0.04
# 2.675 =>  2.67

print('NumPy np.around (np.round)')
print('0.005 => ', np.around(0.005, 2))
print('0.015 => ', np.around(0.015, 2))
print('0.025 => ', np.around(0.025, 2))
print('0.035 => ', np.around(0.035, 2))
print('0.045 => ', np.around(0.045, 2))
print('2.675 => ', np.around(2.675, 2))
# NumPy np.around (np.round)
# 0.005 =>  0.0
# 0.015 =>  0.02
# 0.025 =>  0.02
# 0.035 =>  0.04
# 0.045 =>  0.04
# 2.675 =>  2.68

s = pd.Series([0.005, 0.015, 0.025, 0.035, 0.045, 2.675])

print(s)
# 0    0.005
# 1    0.015
# 2    0.025
# 3    0.035
# 4    0.045
# 5    2.675
# dtype: float64

print(s.round(2))
# 0    0.00
# 1    0.02
# 2    0.02
# 3    0.04
# 4    0.04
# 5    2.68
# dtype: float64

Pythonの組み込み関数round()の結果が偶数への丸めの定義と異なるように見えるのは、公式ドキュメントにもあるように小数が浮動小数点数で正確に表せないため。

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

次に説明するdecimalモジュールを使って確認すると、例えば2.675が実際にはどのような数として扱われているかが分かる。

print(Decimal(2.675))
# 2.67499999999999982236431605997495353221893310546875

2.675は実際には2.674999...という値として扱われるため、組み込み関数round()では偶数への丸めの定義通りに2.67に丸められる。

NumPyのaround()関数およびpandasのround()メソッドがどのようにこれを回避しているのかは分からない(まだコードを読んでいない)。

正確を期すなら以下に示すdecimalモジュールを使ったほうが無難。

decimalモジュールのquantize()で正確な四捨五入・偶数への丸め

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

Decimal()Decimal型オブジェクトを生成する際に、float型を渡すのではなく文字列str型を渡すことによって所望の値として扱われる。

print(Decimal(2.675))
# 2.67499999999999982236431605997495353221893310546875

print(Decimal('2.675'))
# 2.675

Decimal()で生成したDecimal型オブジェクトのquantize()メソッドで任意の桁数・丸めモードで丸める。

quantize()メソッドでは第一引数に求めたい桁数と同じ桁数の数値を'0.1''0.01'のように文字列で指定し、引数roundingに丸めモードを指定する。ROUND_HALF_UPとすると四捨五入、ROUND_HALF_EVENとすると偶数への丸めとなる。

print(Decimal('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.68

print(Decimal('0.5').quantize(Decimal('0'), rounding=ROUND_HALF_UP))
# 1

print(Decimal('0.5').quantize(Decimal('0'), rounding=ROUND_HALF_EVEN))
# 0

quantize()が返すのはDecimal型オブジェクトなので、整数intや浮動小数点floatに変換するにはint(), float()を使う。

これらの処理を無名関数(ラムダ式)で定義し、map(), applymap()メソッドで各要素に適用する。例のようにpandas.Seriesの各要素に適用するにはmap()pandas.DataFrameの各要素に適用するにはapplymap()を使う。ラムダ式などの詳細は以下の記事を参照。

s = pd.Series([0.5, 1.5, 2.5, 3.5, 4.5])

print(s.map(lambda x: float(Decimal(str(x)).
                            quantize(Decimal('0'), rounding=ROUND_HALF_UP))))
# 0    1.0
# 1    2.0
# 2    3.0
# 3    4.0
# 4    5.0
# dtype: float64

s = pd.Series([0.005, 0.015, 0.025, 0.035, 0.045, 2.675])

print(s.map(lambda x: float(Decimal(str(x))
                            .quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))))
# 0    0.01
# 1    0.02
# 2    0.03
# 3    0.04
# 4    0.05
# 5    2.68
# dtype: float64

s = pd.Series([5, 15, 25, 5.1, 15.1, 25.1])

print(s.map(lambda x: int(Decimal(str(x))
                          .quantize(Decimal('1E1'), rounding=ROUND_HALF_UP))))
# 0    10
# 1    20
# 2    30
# 3    10
# 4    20
# 5    30
# dtype: int64

s = pd.Series([0.005, 0.015, 0.025, 0.035, 0.045, 2.675])

print(s.map(lambda x: float(Decimal(str(x))
                            .quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN))))
# 0    0.00
# 1    0.02
# 2    0.02
# 3    0.04
# 4    0.04
# 5    2.68
# dtype: float64

整数の位を指定する際は1E1のように指数表記で表すので注意。詳細は以下の記事を参照。

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

関連カテゴリー

関連記事