Pythonで数値の表示桁数を指定する 〜数値の丸め と 書式変更〜

作成日：20210409
言語：Python3


</br>
</br>

## 20210427 追記
<font color="Red">この記事は、【誤り】や【推奨されていない関数の使い方】、【最新でない情報】を含みます。</font>コメント欄で他のユーザーの方から教えていただきました。ご指摘ありがとうございます。

<font color="Red">情報を知りたい方は他の記事をご参照いただくか、 __0.目的と背景__ →　__コメント欄__ の順にお読みください。 __2.結論__ 以下は誤りを含みます。</font>


本来は訂正や新たな情報を入れた記事を出すのが最良の対応とは思いますが、次にまとまった作業ができる日がまだ確定していないので、取り急ぎ記事の冒頭でアナウンスさせていただきます。
検索の邪魔にならぬように記事を下げる（消す）ことも検討しましたが、いただいたコメントは私には非常に示唆に富む内容であったため、自分の勉強用にこのまま残しておきたいと思い、このような対応になりました（記事を限定共有にする等、検索の邪魔にならないように記事を残しておくもっと良い方法もあるかもしれませんが、その検討も含めて後日対応する予定です）。

また、コメントを下さった方々には本来個別に返信とお礼をすべきところですが、今は取り急ぎLGTMとこちらでのお礼で失礼させていただきます。教えていただいた内容を自分でも調べてから、改めて個別の返信等をできればと思っております。コメントをいただけるのはとても勉強になります。またご指摘等ありましたらコメントいただければ幸いです。よろしくお願いいたします。



## 0. 目的と背景

浮動小数点数を表示する時に、ただ`str型`に変換して`print`するだけでは桁数が揃わない。

```python:print_float.py
import numpy as np
for threshold in np.arange(0.1, 0.2, 0.01):
    print('threshold = ' + str(threshold))
    
# （出力結果）
# threshold = 0.1
# threshold = 0.11
# threshold = 0.12
# threshold = 0.13
# threshold = 0.13999999999999999
# threshold = 0.14999999999999997
# threshold = 0.15999999999999998
# threshold = 0.16999999999999998
# threshold = 0.17999999999999997
# threshold = 0.18999999999999995
```

人間側の希望としては、本当は`0.10, 0.11, 0.12, 0.13, 0.14, 0.15, ... `と表示してほしいのに、`0.14`以降を`0.13999999999999999`などのように表示されてしまう。これをなんとかして`0.14`と表示したい。




### つまり、やりたいことは

`0.13999999999999999`　← こうではなく
↓
`0.14` ← こう表示したい

</br>

ちなみに、この「数値の桁数がめっちゃ多い＆ちょっとだけズレる問題」は、コンピュータが2進法を使って数値を表現しているために起こる。2進法では10進数を正確に表示できない場合があり、小さな誤差が生じるため、`0.13999999999999999`のような「少しズレた値」になる。（ただし、必ず値がズレるわけではなく、コンピュータが数値を正確に表示できる場合もある。例えば、0.5は2の(-1)乗なので(0.5 = 1/2 = 2**(-1))、正しく0.5と表すことができる。）

小さな誤差が最終的な結果に響いてくるような精密な数値計算をするのでない限り、浮動小数点数の微小な誤差はあまり問題にならないので、私は今までほとんどこの誤差について気にしていなかった。しかし、数値を出力する時に`0.13999999999999999`などと書いてあると読みにくいので、値を丸めて`0.14`と表示できる方法を調べた。



## 1. 参考リンク

1-1. note.nkmk.me Pythonで小数・整数を四捨五入するroundとDecimal.quantize
https://note.nkmk.me/python-round-decimal-quantize/

1-2. note.nkmk.me Python, formatで書式変換（0埋め、指数表記、16進数など）
https://note.nkmk.me/python-format-zero-hex/


## 2. 結論（忙しい人用）
文字列メソッド`str.format()`で出力時のフォーマットを指定すれば良い。
数値を任意の桁数で指定し、表示することができる。
詳しくは以下の 3-方法2. を参照

```python:print_float_conclusion.py
import numpy as np
for threshold in np.arange(0.1, 0.2, 0.01):
    print('threshold = {:.2f}'.format(threshold))

# （出力結果）
# 0.10
# 0.11
# 0.12
# 0.13
# 0.14
# 0.15
# 0.16
# 0.17
# 0.18
# 0.19
```


## 3. 解決方法

### 3-方法1.　数値を任意の桁数に丸めてから表示する = 数値自体を出力したい桁数に丸める

#### 参考リンク
https://note.nkmk.me/python-round-decimal-quantize/

（再掲、上記の1-1.）

</br>
標準ライブラリ`decimal`の`quantize()`メソッドを使う。

</br>
※数値の丸めには組み込み関数`round()`を使う方法もあるが、今回は数値の丸めに関しては標準ライブラリ`decimal`の`quantize()`メソッドを使う方法だけをまとめる。

`round()`を使わない理由は、

> Python3での組み込み関数round()による丸めは一般的な四捨五入ではなく、偶数への丸め（銀行家の丸め）になるので注意。……（中略）……一般的な四捨五入や小数に対して正確な偶数への丸めを実現したい場合は、……（中略）……標準ライブラリdecimalのquantizeか新たな関数を定義する方法を使う。（[参考リンク1-1.](https://note.nkmk.me/python-round-decimal-quantize/) より引用）

とあるように、`round()`は四捨五入丸めではなく、偶数への丸めになるため。
</br>

#### サンプルコード

##### decimalモジュールの使い方
```python:decimal_usage1.py
# Decimal()でDecimal型のオブジェクトを生成できる。 （参考リンク1-1.より引用）
from decimal import Decimal, ROUND_HALF_UP

print(0.05)
# 0.05

print(str(0.05))
# 0.05

# 引数にfloat型を指定すると、実際にどのような値として扱われているかが分かる。（参考リンク1-1.より引用）
print(Decimal(0.05)) 
# 0.05000000000000000277555756156289135105907917022705078125

# float型ではなく文字列str型を指定すると正確にその値のDecimal型として扱われる。（参考リンク1-1.より引用）
print(Decimal(str(0.05))) 
# 0.05
```

##### decimalを使った値の丸め

```python:print_float_decimal.py
import numpy as np
from decimal import Decimal, ROUND_HALF_UP

# for文で指定した数を任意の桁に丸める
for threshold in np.arange(0.1, 0.2, 0.01):
    print(Decimal(str(threshold)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
    # （コードの説明）
    # Decimal(str([float]))でfloat型をdecimal型に変換する。
    # .quantize()メソッドで値の丸めができる。
    # 第一引数に求めたい桁数と同じ桁数の数値を'0.01'のように文字列で指定する。'0'なら整数への丸め。
    # 引数roundingで丸めモードを指定する。ROUND_HALF_UPは四捨五入。

# (出力結果)
# 0.10
# 0.11
# 0.12
# 0.13
# 0.14
# 0.15
# 0.16
# 0.17
# 0.18
# 0.19
```

##### 参考：出力結果の比較
```python:decimal_usage2.py
import numpy as np
from decimal import Decimal, ROUND_HALF_UP

for threshold in np.arange(0.1, 0.2, 0.01):
    print('======')
    print(threshold) #float型
    print(str(threshold)) #str型
    print(Decimal(str(threshold))) #これだと受け取った2進数の桁数を保持したままdecimal型にしてしまう
    print(Decimal(str(threshold)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)) #これで値を小数点第2位で丸められた

# （出力結果）
# ======
# 0.1
# 0.1
# 0.1
# 0.10
# ======
# 0.11
# 0.11
# 0.11
# 0.11
# ======
# 0.12
# 0.12
# 0.12
# 0.12
# ======
# 0.13
# 0.13
# 0.13
# 0.13
# ======
# 0.13999999999999999
# 0.13999999999999999
# 0.13999999999999999
# 0.14
# ======
# 0.14999999999999997
# 0.14999999999999997
# 0.14999999999999997
# 0.15
# ======
# 0.15999999999999998
# 0.15999999999999998
# 0.15999999999999998
# 0.16
# ======
# 0.16999999999999998
# 0.16999999999999998
# 0.16999999999999998
# 0.17
# ======
# 0.17999999999999997
# 0.17999999999999997
# 0.17999999999999997
# 0.18
# ======
# 0.18999999999999995
# 0.18999999999999995
# 0.18999999999999995
# 0.19
```


### 3-方法2. 値そのものは変えずに、出力する時に有効桁数を指定する = 数値自体は変えずに、表示のオプションでフォーマットを指定する

#### 参考リンク

https://note.nkmk.me/python-format-zero-hex/

（再掲、上記の1-2.）

</br>
組み込み関数`format()`または 文字列メソッド`str.format()`を使う。ここでは文字列メソッド`str.format()`を使用している。

`str.format()`では、
>小数点以下の桁数を指定するには、.[桁数]fとする。整数部の桁数によらず小数点以下が指定した桁数になる。（[参考リンク1-2.](https://note.nkmk.me/python-format-zero-hex/) より引用）


#### サンプルコード

##### 文字列メソッドstr.format()の使い方1

```python:print_float_format1.py
import numpy as np
for threshold in np.arange(0.1, 0.2, 0.01): 
    print('{:.2f}'.format(threshold)) #{}内がformat()の引数の書式の指定

# （出力結果）
# 0.10
# 0.11
# 0.12
# 0.13
# 0.14
# 0.15
# 0.16
# 0.17
# 0.18
# 0.19
```

##### 文字列メソッドstr.format()の使い方2

```python:print_float_format2.py
import numpy as np
for threshold in np.arange(0.1, 0.2, 0.01): 
    print('threshold = {:.2f}'.format(threshold))
    #上のように、「他の（任意の）文字列」と「書式を指定して代入する文字列」を組み合わせることもできる。

# （出力結果）
# threshold=0.10
# threshold=0.11
# threshold=0.12
# threshold=0.13
# threshold=0.14
# threshold=0.15
# threshold=0.16
# threshold=0.17
# threshold=0.18
# threshold=0.19
```

組み込み関数`format()`・文字列メソッド`str.format()`は、複数の文字列をフォーマットを指定して代入することもできる。（詳細は[参考リンク1-2.](https://note.nkmk.me/python-format-zero-hex/)へ）



##### str.format()メソッドの使用例

`str.format()`の返り値は文字列なので、

```python:
"This is a string"
```

などのような文字列と同様に扱うことができる。
例えば、グラフのタイトルに数値を入れる場合などに使うことができる。

```python:print_format_graphTitle.py
import numpy as np
for threshold in np.arange(0.1, 0.2, 0.01): 
    plt.figure()
    plt.imshow(np.zeros([512,512]), cmap='gray') # 例示のために、黒い正方形を描画
    plt.axis('off')
    plt.title('threshold = {:.2f}'.format(threshold))
    plt.show()
```
</br>
出力結果は、例えば`threshold = 0.13999999999999999` の時は以下のようになる。
きちんと`0.14`と表示できていることが分かる。

![threshold = 0.14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1056016/d934e044-556a-5afb-7d52-56d25aa14dfc.jpeg)


補足として、

```python:
# 上記のように、グラフタイトルに数値を入れたいような場合に、

plt.title('threshold = {:.2f}'.format(threshold))
# の代わりに

plt.title('threshold = ' + str(threshold))
# のように書くと、0.14と表示されてほしいところが0.13999999999999999と桁が多くなったり、

plt.title('threshold = ' + str(Decimal(str(threshold)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)))
# のように書くと、長くて読みにくくなったりするので、
# str.format()メソッドで簡単に書式を変更できるのはとても便利だと感じた。
```

</br>
<br/>
__終わり__