欠損値を含むデータセットの前処理について

半年ほど機械学習に触れてなかったため、忘れたことを復習しました。

# 欠損値を扱う方法について

提供してもらったデータセットには、時折NaN(Not a Number)という欠損値データが含まれていることがある。
これを扱う方法について記載する。

### サンプルデータの作成、NaNの扱い

```python
# サンプルデータを作成
import numpy as np
import pandas as pd
# ハードディスク上にcsvファイルを作成せずに実行したいのでStringIO関数をインポート
from io import StringIO

# csvファイルを作成する
csv_sample_data = '''
A,B,C,D
1.0,,3.5,5.0
4.0,,6.0,9.0
big,small, ,normal
red,blue,green,white
20,30,,40
'''

# DataFrameオブジェクトに格納
df = pd.read_csv(StringIO(csv_sample_data))
df
# print(df) # jupyterを使っている際、テーブル形式で表示させたくない場合(そんな人あまりいないと思うが)
# Kaggleのチュートリアルなどではprint文を使用していたが、ターミナルやjupyterでは無くても実行できる

-------------------------------------------------
     A      B      C       D
0  1.0    NaN    3.5     5.0
1  4.0    NaN    6.0     9.0
2  big  small         normal
3  red   blue  green   white
4   20     30    NaN      40
```

pandasのread_csv関数は、CSVフォーマットファイルを読み込んだ時、欠損値を上記のように*NaN*に変換する。
ただし、**[Big, small, ,normal]**のような半角スペースを空けるとNaNに変換してくれない(半角スペースも1特徴量として扱われるため)。

```python
# 各列の欠損値(NaN)をカウントする
df.isnull().sum()

-------------------------------------------------
A    0
B    2
C    1
D    0
dtype: int64
```
DataFrameオブジェクトのisnull().sum()関数を使うことで各列に含まれるNaNの数を数えることができる。
では、NaN(欠損値)はどう処理すれば良いのだろうか。

### 解決策1) データセットから欠測値を取り除く

邪魔なのでデータセットから捨てる。
この際、行列が崩れないように１行単位で取り除く。

```python
# 元のデータセット
df

-------------------------------------------------  
   A      B      C       D
0  1.0    NaN    3.5     5.0
1  4.0    NaN    6.0     9.0
2  big  small         normal
3  red   blue  green   white
4   20     30    NaN      40

# NaNを含む行を削除する
df.dropna()

-------------------------------------------------
    A      B      C       D
2  big  small         normal
3  red   blue  green   white
```
DataFrameオブジェクトのdropna関数を使用することで、1行目と４行目が取り除かれて表示されている。
他にも引数を指定することで削除する内容を変更できる。
詳しい内容については公式ドキュメントを参照すること。
[pandas.DataFrame.dropna](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.dropna.html)

```python
# NaNを含む"列"を削除する
df.dropna(axis='columns')
# df.dropna(axis=1) # こちらの指定方法でも可
# 行を削除する場合、axis=0　または axis="index"でも可

-------------------------------------------------
     A       D
0  1.0     5.0
1  4.0     9.0
2  big  normal
3  red   white
4   20      40

# B列の中でNaNが含まれる行だけを削除する

# 余談ですが、僕はこの模擬データセットを作ったときにカンマの後に半角スペースを入れたせいで
# 列名が異なってしまったためKeyErrorが発生しました。
# 見やすさのためにカンマの後に半角スペースを入れないようにしましょう。

df.dropna(subset=['B'])

-------------------------------------------------
     A      B      C       D
2  big  small         normal
3  red   blue  green   white
4   20     30    NaN      40
```

しかし、これでは他の特徴量も一緒に捨てられてしまう。
そのため、有益な情報を失ってしまう可能性がある。
ある行に含まれるNaNの比率が90%を超える場合などに使用に限定しよう。

#### トレーニングデータセットとテストデータセットから同じ列を削除する

データサイエンティストが扱うデータセットは多くの場合、トレーニングデータセットと
テストデータセットを持っている。
そのため、2つのデータセットに差異が発生しないよう、両方から同じ列を削除するようにする。

やり方は簡単、削除する列をリストで持っておき、df.drop()関数で削除する。

```python
delete_col_list = ['B', 'D']
df.drop(delete_col_list, axis=1)

-------------------------------------------------
    A      C
0  1.0    3.5
1  4.0    6.0
2  big       
3  red  green
4   20    NaN
```

今回は1つしかサンプルデータセットを作成していないが､
df.drop(delete_col_list, axis=1)を別のDataFrameオブジェクトで実行すれば良い｡

### 解決策2) 欠測値の補完

有益なデータを削除しないように、NaNを同じ列の値の平均値などで補完する。
これはscikit-learnのImputerクラスを使用すると楽。
[sklearn.preprocessing.Imputer](http://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.Imputer.html)

なお、平均(mean)の他に以下もstrategy引数として用意されている。

- 中央値(median)
- 最頻値(most_frequent)

```python
from sklearn.preprocessing import Imputer
imr = Imputer(missing_values='NaN', strategy='mean', axis=0)

# この後の説明のため、新規にDataFrameオブジェクトを作成
df2 = pd.DataFrame({"A":[1.0, 3.0, np.nan,np.nan],
                   "B":[1.0, np.nan,5.0,6.0],
                   "C":[9.0,np.nan,13.0, 2.0],
                   "D":[9.0,np.nan,5.0, np.nan]
                   })
df2

-------------------------------------------------
     A    B     C    D
0  1.0  1.0   9.0  9.0
1  3.0  NaN   NaN  NaN
2  NaN  5.0  13.0  5.0
3  NaN  6.0   2.0  NaN

# 平均値で補完する
imputed_data = imr.fit_transform(df2) # fit()とtransform()を１行で実行する関数
imputed_data

-------------------------------------------------
array([[  1.,   1.,   9.,   9.],
       [  3.,   4.,   8.,   7.],
       [  2.,   5.,  13.,   5.],
       [  2.,   6.,   2.,   7.]])
```
###### jupyter-notebookでグラフィカルに表示する

```python
df2 = pd.DataFrame(data=imputed_data[0:,0:],
                   index=range(4),
                   columns=['A','B','C','D'])
df2
```
![無題.png](https://qiita-image-store.s3.amazonaws.com/0/130458/715f8a6b-f9d4-76e7-ad00-84d9167082c4.png)


各列間でNaNを除いた値で平均値を算出し、NaNに補完してくれている。
では、文字列を含むDataFrameオブジェクトではどうだろうか。

```python
imputed_data2 = imr.fit_transform(df)
imputed_data2

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-21-4a8a719ead77> in <module>()
----> 1 imputed_data2 = imr.fit_transform(df)
      2 imputed_data2

/.../site-packages/sklearn/base.py in fit_transform(self, X, y, **fit_params)
    492         if y is None:
    493             # fit method of arity 1 (unsupervised transformation)
--> 494             return self.fit(X, **fit_params).transform(X)
    495         else:
    496             # fit method of arity 2 (supervised transformation)

/.../site-packages/sklearn/preprocessing/imputation.py in fit(self, X, y)
    154         if self.axis == 0:
    155             X = check_array(X, accept_sparse='csc', dtype=np.float64,
--> 156                             force_all_finite=False)
    157 
    158             if sparse.issparse(X):

/.../site-packages/sklearn/utils/validation.py in check_array(array, accept_sparse, dtype, order, copy, force_all_finite, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, warn_on_dtype, estimator)
    380                                       force_all_finite)
    381     else:
--> 382         array = np.array(array, dtype=dtype, order=order, copy=copy)
    383 
    384         if ensure_2d:

ValueError: could not convert string to float: 'white'
```

NaNを同じ列の数値の平均値で補完するため、文字列である'white'をfloat型に変換できないと言われてしまった。
そのため、この方法ではできない。

このようなカテゴリの値を含むデータセットを補完するにはどうするべきだろうか。
僕が考える対処法は以下の通り(素人の回答です)。

#### 名義的特徴量(red, blue, green, whiteのような大小関係のない特徴量)の場合
1. 「その他」というカテゴリを作成し、欠損値をそれに置き換える
2. one-hotエンコーディングで数値に置き換える

##### 順序特徴量(big, normal, small)の場合
1. 一番小さなカテゴリ値としてマッピングする

他にもっと良い回答があると思います。