芋の独り言

当ブログへのアクセスは当ブログのプライバシーポリシーに同意したものとみなします.

【Python】(小数の)数値の丸め:四捨六入

rounding 無効な数字を捨て,有効数字だけにまとめること

実地調査であったりとか実験だったり,機械学習でも学習および予測を行うと, 結果が数値で現れるわけですが, コンピュータでその数値を研鑽させて得た場合, 数値はその実行環境によって出力できる桁まで出力されます. Pythonだと

>>> 3.14/0.7
4.485714285714286

っと小数点第15位まで表示されていますね. 詳しくはPythonの公式のサイトを見れば書いてあると思いますが, ともかく特に指定していない場合は有効数字などを考慮せずに その実行環境で保持・出力できるだけ桁を出力します.

データとしてどっかに公表する場合はこのまま提示するとダメなわけで, 数値を丸める必要がありますよね. 実地調査や実験だと有効数字を予め決めて行っていると思うので, 決めていた有効数字の通りに丸めればいいと思いますが, 機械学習の予測結果とか情報系だとどうすりゃいいんだろ.

文書によってまちまちですな.2桁のものもあれば4桁のもの, 全桁のっけてるようなのもある. まぁ,3桁が妥当な感じかな~ 機械学習の予測結果,精度や再現率って1より小さいので, つまり,実質は小数点第3位までを示し,小数点第4位で丸める. 桁取りが大抵3桁なので,小数点以下においても3桁までを表示するのが良さそう. 状況次第ではありますがね~

ただし,単語や品詞の出現回数といった,回数や個数は丸めてないですね. 整数値だからかな. 丸める代わりに3位桁取りしてますね~ Pythonでやるならこう.

>>> a=123456
>>> f"{a:,}"
'123,456'

それで丸める際の四捨五入についてなんですが, f プリフィクスによる丸め方は

>>> a=1.45
>>> b=1.451
>>> f"{a:.1f}"
'1.4'
>>> f"{b:.1f}"
'1.5'

となります. 指定した数値の+1桁目(.1fで小数点第2位)を丸めていますね. ただし,通常の四捨五入ではないですよね.

四捨五入箇所が5で下に桁がない場合は切り捨て, 四捨五入箇所が5でその右隣の数がある場合は切り上げるという処理が行われている, ように見えます. その処理であれば四捨六入(JISが定めるところの四捨五入)というものです. しかし,いろんな数字で試してもらうと分かりますが, どうやら四捨六入でもなさそうです.

四捨五入は丸める桁の数値が5以上(5を含む,6,7,8,9の5つの数)なら切り上げ(上の桁に+1), 5未満(5を含まない,1,2,3,4の4つの数.0も入るか?)ならば切り捨てます(上の桁に+0).
これに対し,四捨六入は

  • 丸める桁の数が6未満
    • 丸める桁の数が5未満:切り捨て
    • 丸める桁の数が5で下の桁(右隣の桁)に数がある場合:切り上げ
    • 丸める桁の数が5で下の桁(右隣の桁)に数がない場合(f プリフィクスだとここの処理が異なる)
      • 丸める桁の一つ上の桁(左隣の桁)の数が奇数の場合:切り上げ
      • 丸める桁の一つ上の桁(左隣の桁)の数が偶数の場合:切り捨て
  • 丸める桁の数が6以上:切り上げ

となっています.

Pythonには decimal という有効数字を意識した数値計算を行える標準ライブラリがあり, このモジュールの使用でも数値の丸めを行うことができます.

>>> import decimal
>>> decimal.Context(prec=1,rounding=decimal.ROUND_05UP).create_decimal(0.15).to_eng_string()
'0.1'
>>> decimal.Context(prec=1,rounding=decimal.ROUND_05UP).create_decimal_from_float(0.15).to_eng_string()
'0.1'
>>> decimal.Decimal(str(0.15)).quantize(decimal.Decimal('.1'),rounding=decimal.ROUND_05UP).to_eng_string()
'0.1'

使用において,precは有効数字桁数を入れてください.quantizeを使う場合は quantizeに丸めた際になる目標の桁数の数字をDcimalしたものを入れてください. Dcimalには文字列で入れてください. float型で入れるとコンピュータ内部の誤差の影響なのか余計なものまでついてしまいます.

>>> decimal.Decimal(0.15)
Decimal('0.1499999999999999944488848768742172978818416595458984375')
>>> decimal.Decimal(str(0.15))
Decimal('0.15')

decimal.ROUND_UPが四捨五入っぽいですが, いくつか丸めの仕方が用意されていますが,四捨六入っぽい挙動をするものがないっぽい...? 【Python入門】小数点の操作を切り上げからroundまで完全理解! | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイトでよく使われるものについてまとめられており, decimal.ROUND_UPは切り上げで四捨五入はdecimal.ROUND_HALF_UPとのこと.

四捨六入のモードが分からん!っというか無さそうだな~なら,自作するか~ということで,


def round_to_even(num:float,point:int)->float:
    if point < 1:
        raise Exception(f"丸める桁数は1以上の整数を入力してください:{point}")
    elif type(point) != int:
        raise Exception(f"丸める桁数は整数で入力してください:{point}")
    
    # 文字列にして,整数部と小数部に分割
    integer_part,small_part = str(num).split(".")

    # Pythonは0から要素番号を数え始めるので
    point = point - 1 # 丸める桁

    def round_up():
        # 丸める桁が小数点第2位以降
        if point > 0:
            # 丸める桁の上の桁が小数点第2位以降
            if point-1 > 0:
                return float(integer_part + "." + small_part[:point-1-1] + str(int(small_part[point-1])+1))
            # 丸める桁の上の桁が小数点第1位
            elif point-1 == 0:
                return float(integer_part + "." + str(int(small_part[point-1])+1))
            # 丸める桁の上の小数点桁がない
            else: # point-1-1 < 0
                return float(str(int(integer_part)+1)+".0")
        # 丸める桁が小数点第1位
        elif point == 0:
            return float(str(int(integer_part)+1)+".0")

    def round_down():
        # 丸める桁が小数点第2位以降
        if point > 0:
            return float(integer_part+"."+small_part[:point])
        # 丸める桁が小数点第1位
        elif point == 0:
            return float(integer_part+".0")   

    # 5より大きいので切り上げ
    if int(small_part[point]) > 5:
        return round_up()
    # 5未満なので切り捨て
    elif int(small_part[point]) < 5:
        return round_down()
    # 丸める桁の数が5
    elif small_part[point] == "5":
        # 丸める桁の下の桁に数がある=丸める桁数が小数点以下の数字の数より小さい
        if len(small_part) > point+1:
            return round_up()
        # 丸める桁の下の桁に数がない
        else:
            # 丸める桁が小数点第2位以降
            if point > 0:
                # 丸める桁の上の桁の数が偶数
                if int(small_part[point-1])%2 == 0:
                    return round_down()
                # 丸める桁の上の桁の数が奇数
                if int(small_part[point-1])%2 == 1:
                    return round_up()
            # 丸める桁が小数点第1位
            elif point == 0:
                # 丸める桁の上の桁の数が偶数
                if int(integer_part[-1])%2 == 0:
                    return round_down()
                # 丸める桁の上の桁の数が奇数
                if int(integer_part[-1])%2 == 1:
                    return round_up()
        

if __name__ == "__main__":
    print(round_to_even(float(input("丸めたい小数:")),int(input("丸める小数桁:"))))

っと,とりあえず,小数の丸めはできるかな~?と.

⇓実行するとこんな感じ

丸めたい小数:0.15
丸める小数桁:2
0.2

とりあえず試行錯誤して書いてみた程度なので,エラーが起こるかも... ま,参考にしてください.

参考サイト

csvからlatexにする際に

kusoimox.hatenablog.jp

Pnadasのメソッドでcsvかたtexの表に変換する~ということをやりましたが, その際に, to_latexの引数float_formatに丸め方を指定できます. f プリフィクスによる丸め方による文字列を記述すればよいようです. あと,引数decimalは位取りする際の区切り文字の指定だと思うんですよね~ ま,詳しくは以下のレファレンスを見てください~ pandas.pydata.org

修正

上記で書いたスクリプトではいくつか正しく丸められないことがあり, 修正しました. あえてdecimalといった標準ライブラリは用いなかったので, グチャグチャになってしまいました...


def dummy(integer_part,small_part,point):
    # 丸める桁の上の桁の上の桁が小数点第1位
    if point-1 == 1:
        # 繰り上げると丸める桁の上の桁の上の桁のが0になる場合
        if small_part[point-1-1] == "9":
            return float(str(int(integer_part)+1)+".00")
        else:
            return float(integer_part + "." + str(int(small_part[point-1-1])+1) + "0")
    # 丸める桁の上の桁の上の桁が小数点第2位以降
    else:
        # 繰り上げると丸める桁の上の桁の上の桁のが0になる場合
        if small_part[point-1-1] == "9":
            return float(str(dummy(integer_part,small_part,point-1))+"0")
        else:
            return float(integer_part + "." + small_part[:point-1-1] + str(int(small_part[point-1-1])+1) +"0")

def round_to_even(num:float,point:int)->float:
    if point < 1:
        raise Exception(f"丸める桁数は1以上の整数を入力してください:{point}")
    elif type(point) != int:
        raise Exception(f"丸める桁数は整数で入力してください:{point}")
    
    # 文字列にして,整数部と小数部に分割
    integer_part,small_part = str(num).split(".")

    # 指定小数位桁がない場合,0で埋めておく
    if len(small_part) < point:
        while len(small_part) < point:
            small_part = small_part + "0"

    # Pythonは0から要素番号を数え始めるので
    point = point - 1 # 丸める桁   

    def round_up():
        # 丸める桁が小数点第2位以降
        if point > 0:            
            # 丸める桁の上の桁が小数点第2位以降
            if point-1 > 0:
                # 繰り上げると丸める桁の上の桁が0になる場合
                if small_part[point-1] == "9":
                    return dummy(integer_part,small_part,point)
                else:
                    return float(integer_part + "." + "".join(small_part[:point-1]) + str(int(small_part[point-1])+1))
                    #return float(integer_part + "." + str(int(small_part)+dummy))
            # 丸める桁の上の桁が小数点第1位
            elif point-1 == 0:
                # 繰り上げると小数部分が0になる場合
                if small_part[point-1] == "9":
                    return float(str(int(integer_part)+1)+".0")
                else:
                    return float(integer_part + "." + str(int(small_part[point-1])+1))
            # 丸める桁の上の小数点桁がない
            else: # point-1-1 < 0
                return float(str(int(integer_part)+1)+".0")
        # 丸める桁が小数点第1位
        elif point == 0:
            return float(str(int(integer_part)+1)+".0")

    def round_down():
        # 丸める桁が小数点第2位以降
        if point > 0:
            return float(integer_part+"."+small_part[:point])
        # 丸める桁が小数点第1位
        elif point == 0:
            return float(integer_part+".0")   

    # 5より大きいので切り上げ
    if int(small_part[point]) > 5:
        return round_up()
    # 5未満なので切り捨て
    elif int(small_part[point]) < 5:
        return round_down()
    # 丸める桁の数が5
    elif small_part[point] == "5":
        # 丸める桁の下の桁に数がある=丸める桁数が小数点以下の数字の個数より小さい
        if len(small_part) > point+1 and small_part[-1] != "0":
            return round_up()
        # 丸める桁の下の桁に数がない
        else:
            # 丸める桁が小数点第2位以降
            if point > 0:
                # 丸める桁の上の桁の数が偶数
                if int(small_part[point-1])%2 == 0:
                    return round_down()
                # 丸める桁の上の桁の数が奇数
                if int(small_part[point-1])%2 == 1:
                    return round_up()
            # 丸める桁が小数点第1位
            elif point == 0:
                # 丸める桁の上の桁の数が偶数
                if int(integer_part[-1])%2 == 0:
                    return round_down()
                # 丸める桁の上の桁の数が奇数
                if int(integer_part[-1])%2 == 1:
                    return round_up()
    else:
        print(f"{num}:faild")
        

if __name__ == "__main__":
    print(round_to_even(float(input("丸めたい小数:")),int(input("丸める小数桁:"))))

といってもまだエラーがあるかも.

修正2

上記の修正版だと,繰り上げした際にその桁も消えてしまってました. 例えば,0.9197を小数点第4位で丸めるようとすると上記スクリプトだと0.92と出力します. しかし,これだと有効数字を意識できていません. 小数点第4位で丸めるのだからこの時有効数字は3桁(整数部が0でなければ)であるので, 有効数字が3桁であることを示すために0.920と出力しなければなりません. そのように出力するために修正しました. が,その分他のトコロで間違いが発生してるかも. とりあえず,この話はこれで決着にしたいです(本記事を更新しない).


def round_to_even(num:float,point:int)->str:
    if point < 1:
        raise Exception(f"丸める桁数は1以上の整数を入力してください:{point}")
    elif type(point) != int:
        raise Exception(f"丸める桁数は整数で入力してください:{point}")
    elif type(num) != float:
        raise Exception(f"小数を入力してください:{num}")
    
    # 文字列にして,整数部と小数部に分割
    integer_part,small_part = str(num).split(".")

    # 指定小数位桁がない場合,0で埋めておく
    if len(small_part) < point:
        while len(small_part) < point:
            small_part = small_part + "0"

    # Pythonは0から要素番号を数え始めるので
    point = point - 1 # 丸める桁   

    def round_up():
        if point == 0:
            return str(int(integer_part)+1)
        else:
            dummy = str(int(small_part[:point])+1)
            while len(dummy) < len(small_part[:point]):
                dummy = "0" + dummy
            return integer_part+"."+dummy
    def round_down():
        if point == 0:
            return integer_part
        else:
            dummy = str(int(small_part[:point]))
            while len(dummy) < len(small_part[:point]):
                dummy = "0" + dummy
            return integer_part+"."+dummy 

    # 5より大きいので切り上げ
    if int(small_part[point]) > 5:
        return round_up()
    # 5未満なので切り捨て
    elif int(small_part[point]) < 5:
        return round_down()
    # 丸める桁の数が5
    elif small_part[point] == "5":
        # 丸める桁の下の桁に数がある=丸める桁数が小数点以下の数字の個数より小さい
        if len(small_part) > point+1 and small_part[-1] != "0":
            return round_up()
        # 丸める桁の下の桁に数がない
        else:
            # 丸める桁が小数点第2位以降
            if point > 0:
                # 丸める桁の上の桁の数が偶数
                if int(small_part[point-1])%2 == 0:
                    return round_down()
                # 丸める桁の上の桁の数が奇数
                if int(small_part[point-1])%2 == 1:
                    return round_up()
            # 丸める桁が小数点第1位
            elif point == 0:
                # 丸める桁の上の桁の数が偶数
                if int(integer_part[-1])%2 == 0:
                    return round_down()
                # 丸める桁の上の桁の数が奇数
                if int(integer_part[-1])%2 == 1:
                    return round_up()
    else:
        print(f"{num}:faild")
        

if __name__ == "__main__":
    print(round_to_even(float(input("丸めたい小数:")),int(input("丸める小数桁:"))))

floatでの出力をstrに. strでないと0.920と表示できないので. また,dummyはメソッドではなく変数に用いました.

・実行例

丸めたい小数:1.5
丸める小数桁:1
2
丸めたい小数:0.9197
丸める小数桁:4
0.920
丸めたい小数:0.151
丸める小数桁:2
0.2