[Python3] numpyの float16 / float32 / float64の精度・速度比較

# 概要

- 「float64よりfloat32を使った方が高速らしいよ」  
( ・ω・) (^ワ^ )
- 「でも、大事なデータが破損してしまうのでは....？」  
 ( ´・ω・`) (^ワ^;) (え？)
- 「よし、データサイズを小さくしたらどのくらい精度が落ちるのか、確かめることにしよう！」  
(`・ω・´)

# 精度測定実験

### 1. 大きな整数
```python:jupyter-notebook
bigint = np.array([123456789012345678901234567])
bigint
-> array([123456789012345678901234567], dtype=object)

# このままだとちょっとしか表示されないが...
bigint.astype('float64')
-> array([1.23456789e+26])

# ndarray内の要素を一つ指定すると、保持されている桁が最後まで見える
# (konandoiruasaさんの指摘により)
bigint.astype('float64')[0]
-> 1.2345678901234568e+26 # 16桁目までは元の値を保持

bigint.astype('float32')[0]
-> 1.2345679e+26 # 7桁目までは元の値を保持

bigint.astype('float16')[0]
-> inf # 草w
```

### 2. 小さな小数
```python:jupyter-notebook
small_float = np.array([-0.00001234567890123456789])
small_float[0]
-> -1.2345678901234568e-05 # 生成してすぐに17桁目以降が破損...

small_float.astype('float64')[0]
-> -1.2345678901234568e-05 # 変化なし

small_float.astype('float32')[0]
-> -1.2345679e-05 # 7桁目までは元の値を保持

small_float.astype('float16')[0]
-> -1.234e-05 # 4桁目までは元の値を保持
```

### 3. 間を取ってみる
```python:jupyter-notebook
halfway_number = np.array([1234.567890123456789012345])
halfway_number[0]
-> 1234.567890123457 # 生成してすぐに16桁目以降が破損...

halfway_number.astype('float64')[0]
-> 1234.567890123457 # 変化なし

halfway_number.astype('float32')[0]
-> 1234.5679 # 7桁目までは元の値を保持

halfway_number.astype('float16')[0]
-> 1235.0 # 3桁目までしか元の値が保持されない...
```

### わかったこと（精度）(^ワ^*)
- `float16`  
数値が~~4桁~~**3桁**に収まるならOK
- `float32`  
数値が~~8桁~~7桁に収まるならOK?  
ただし、[konandoiruasaさんの指摘](https://qiita.com/siruku6/items/27dd9fb84aa84186eda5?utm_campaign=email&utm_content=link&utm_medium=email&utm_source=public_comment#comment-9377a90160ddc1da8110)にあるように、7桁であっても**8388608**までしか保持されないと考えると、**8388608以上**の数値は壊れてしまう可能性がある  
というわけで、**6桁**で済む情報を格納する場合に限定した方がよさそう  
※ただし、float32に関する [不具合事例（20191022追記）](https://qiita.com/siruku6/items/27dd9fb84aa84186eda5#%E4%B8%8D%E5%85%B7%E5%90%88%E4%BA%8B%E4%BE%8B20191022%E8%BF%BD%E8%A8%98)も参照
- `float64`  
扱える数値は~~12桁~~16桁までっぽい?  
ただし、同じ理由で**4503599627370496**以上の値は壊れてしまう可能性がある  
そうすると、**15桁**までしか保持されないと考えるのがよさそう

こんな感じか

|numpyの型|桁数|
|:--:|:--:|
|float16|3|
|float32|6|
|float64|15|

### 不具合事例（20191022追記）

float32を使うと、

* たとえ6桁に収まる小数であっても最下位の桁が想定外の値になる
* roundメソッドによる丸め処理の結果がおかしい

事例がありました。

#### sample

```python
df = pd.DataFrame({'i': [132.2, 332.11, 5.555555555]})
df.astype('float32')
 	i
0 	132.199997  <= 元々4桁にもかかわらず、最下位がずれる
1 	332.109985  <= 元々5桁にもかかわらず、最下位がずれる
2 	5.555555    <= まあ予想通り
```

更に、**float32**だと**roundメソッドによる丸めすら行えない**？！

```python
# float32 の dataframe を round(3) しても何も起こらない...
df.astype('float32').round(3)
	i
0 	132.199997 <= 3桁目以下も残っている....
1 	332.109985
2 	5.556000

# float64 なら round で下位の桁を排除できる
df.astype('float64').round(3)
 	i
0 	132.200
1 	332.110
2 	5.556
```

ただし、生の値を実際に取り出せば、たとえfloat32であっても期待した値を取得できる

```python
df.astype('float32').round(3).values

array([[132.2  ],
       [332.11 ],
       [  5.556]], dtype=float32)

いずれも、3桁目以下が丸められて消えており、期待した値になっている
```

#### ここからわかること

* dtype=float32 の dataframe は信用できない  
（小数点がない場合は調べてないので不明）  
float32だと、dataframeから値を取り出してみるまで、実際にどんな値が入っているのかわからないことがある.....（-□-;）

こうなると、float16やfloat32を使うべきタイミングは、相当限られてきそう

#### float32でも精度が十分足りる場合

```python
# この程度の桁数なら、十分正しく動作する
df = pd.DataFrame({'消費税': [0.100000, 0.080000, 0.050000]})
df.astype('float32').round(3)

	消費税
0 	0.10
1 	0.08
2 	0.05
```

# 速度測定実験
### 1. ひたすらndarrayを連結し続ける
測定対象のメソッドはこちら

```python:jupyter-notebook
def generate_ndarray(num, np_type):
    number = np.array([1234.56789123456789]).astype(np_type)
    np_ar = np.array([]).astype(np_type)
    count_ar = np.arange(num)

    for i in count_ar:
        np_ar = np.concatenate([np_ar, number])
    return np_ar
```
結果はこちら

```python:jupyter-notebook
%timeit generate_ndarray(num=1000, np_type=np.float64)
-> 5.88 ms ± 1.12 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit generate_ndarray(num=1000, np_type=np.float32)
-> 5.96 ms ± 328 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit generate_ndarray(num=1000, np_type=np.float16)
-> 5.77 ms ± 512 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```
何も変わらないじゃないか！！！ヾ(`д´)ﾉ
気を取り直して....
### 2. 巨大行列同士の掛け算
```python:jupyter-notebook
a = np.zeros((1000, 1000), dtype=np.float64)
b = np.zeros((1000, 1000), dtype=np.float32)
c = np.zeros((1000, 1000), dtype=np.float16)
```
```python:jupyter-notebook
%timeit a * a
-> 12.2 ms ± 1.13 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit b * b
-> 6.44 ms ± 461 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit c * c
-> 31.5 ms ± 910 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
# 遅くなった！！！！
```
おや....これはどういうことだ.....( ´・ω・`) (･ω･｀;)
### わかったこと？（速度）(｡ŏ﹏ŏ)
- float64 -> float32  
処理によっては時間が半分で済むが、あまり時間が変わらない場合もある  
値を計算に使用したとき、初めて差が出てくるのかも？  
もう少し調べてみたい
- float64 -> float16  
むしろ遅くなる！！！
float16についてはもうちょっと継続調査しないとだめかも...？

# 参考サイト
より詳細が知りたい方はこちらへ....

- [Pythonの浮動小数点数float型の範囲（最大値・最小値）](https://note.nkmk.me/python-sys-float-info-max-min/)
- [1.3.3. より込み入った配列](http://www.turbare.net/transl/scipy-lecture-notes/intro/numpy/elaborate_arrays.html)