[Python][pandas]データフレーム中の欠損値の確認/削除/補完方法まとめ

# これは何？
Python/pandasを使用したデータ分析で欠損値の扱いに困ったので、確認方法/対処方針とそのコードをまとめる。
私と同じデータ分析初心者の人の参考になればと思います。

## データの準備
```python
import pandas as pd
import numpy as np

# サンプルデータフレームのカラム名
list_column =  ['name', 'age', 'sex', 'height', 'weight']

# サンプルデータフレームの内容
temp_arr = np.array([
    ['ALice', 23, 0, 160.3, 52.6],
    ['Bob', 28,  1, None, 74.2],
    ['Carol', 31,  0, 154.4, None],
    ['Dave', 38, 1, None, 89.1],
    ['Ellen', 43, 0, 149.1, 51.4]
])

# サンプルデータフレームの作成
df = pd.DataFrame(data=temp_arr, columns=list_column)
```

```:実行結果
>>> df
    name age sex height weight
0  ALice  23   0  160.3   52.6
1    Bob  28   1   None   74.2
2  Carol  31   0  154.4   None
3   Dave  38   1   None   89.1
4  Ellen  43   0  149.1   51.4
```

## 欠損値の場所/数を特定する
###欠損値を表示（欠損値の場所が"True"として表示される）
```python
df.isnull()
```

```:実行結果
    name    age    sex  height  weight
0  False  False  False   False   False
1  False  False  False    True   False
2  False  False  False   False    True
3  False  False  False    True   False
4  False  False  False   False   False
```
Noneになっていた部分がTrueになってますね。

###カラムごとに欠損値の数を表示
```python
df.isnull().sum()
```

```:実行結果
name      0
age       0
sex       0
height    2
weight    1
dtype: int64
```
sum()関数で、isnull()関数を実行した結果から各列の合計値を表示している。
この時、「True = 1」「False = 0」と扱われるらしい。

## 欠損値への対処
前述のコードで欠損値(None)の場所と数を明らかにしたので、それらの欠損値に対処する。
ググってみた感じ、方針は大きく2つ。

1. 欠損値を含む行/列を取り除く
2. 欠損値を何らかの値で補完する
3. 欠損値を無視する

「3.欠損値を無視する」と、色々と不都合が多いのでやめた方が良さそう。

### 方針①：欠損値を含む行/列を取り除く
#### 欠損値を含む**行**を削除する
```python
df.dropna(axis=0) #引数は無くても同じ挙動をする
```

```:実行結果
    name age sex height weight
0  ALice  23   0  160.3   52.6
4  Ellen  43   0  149.1   51.4
```

#### 欠損値を含む**列**を削除する
```python
df.dropna(axis=1)
```

```:実行結果
    name age sex
0  ALice  23   0
1    Bob  28   1
2  Carol  31   0
3   Dave  38   1
4  Ellen  43   0
```

#### 欠損値の数が閾値以上の**列**を削除する
```python
# 閾値（欠損値の列を削除する基準）を計算する
# 今回は全データ数の4割以上がNAなら列を削除する
threshold = len(df)*0.4

# 欠損値の数が閾値以下の列名リストを取得
series_under_threshold = df.isna().sum() < threshold
list_isna_under_thresholt = series_under_threshold[series_under_threshold].index

# 取得したリストの列のみ抽出
df = df[list_isna_under_thresholt]
```

```:実行前
    name   age  sex  height  weight
0  ALice  23.0  0.0   160.3    52.6
1    Bob  28.0  1.0     NaN    74.2
2  Carol  31.0  0.0   154.4     NaN
3   Dave  38.0  1.0     NaN    89.1
4  Ellen  43.0  0.0   149.1    51.4
```

```:実行結果
    name   age  sex  weight
0  ALice  23.0  0.0    52.6
1    Bob  28.0  1.0    74.2
2  Carol  31.0  0.0     NaN
3   Dave  38.0  1.0    89.1
4  Ellen  43.0  0.0    51.4
```

### 方針②：欠損値を何らかの値で補完する
#### 欠損値を同列の平均値で補完する
地道に列ごとの平均値を計算⇒欠損値へ代入しても補完できるが、scikit-learnにImputer(補完機)クラスという便利なクラスが用意されているのでこれを利用する。

##### scikit-learnのImputer(補完用クラス)を使って平均値補完を実行する
```python
from sklearn.preprocessing import Imputer

# 欠損値を保管するためのImputerを生成して、dfの値に適合させる
imputer = Imputer(missing_values='NaN', strategy='mean', axis=0)
imputer = imputer.fit(df.values)

# Imputerによる補完を実行する
arr_imputed= imputer.transform(df.values)
```

* 4行目の`Imputer(missing_values='NaN', strategy='mean', axis=0)`について<br>
`missing_values`：何の値を欠損値として扱うか。今回は`None`を保管対象にしたいので`'NaN'`を指定<br>
`strategy`：補完値の計算方法。{ 'mean' ⇒ 平均値 , 'median' ⇒ 中央値 , 'most_frequent' ⇒ 最頻値 } <br>
`axis`：補完値を求める方向。{ 0 ⇒ 列方向 , 1 ⇒ 行方向 }<br>
詳細は https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.Imputer.html で確認してください

```:実行結果
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\User\Anaconda3\lib\site-packages\sklearn\preprocessing\imputation.py", line 155, in fit
    force_all_finite=False)
  File "C:\Users\User\Anaconda3\lib\site-packages\sklearn\utils\validation.py", line 433, in check_array
    array = np.array(array, dtype=dtype, order=order, copy=copy)
ValueError: could not convert string to float: 'Ellen'
```

ダメだった理由：
　inputer.fit()メソッドには数値型の値しか渡せないっぽい。
　⇒ "name"列の"Ellen"が数値型に変換できないからエラーとなってしまった。。
　　 （元のデータフレームに数値しか入っていない場合はエラーにならない）

#### 文字列が混じったデータフレームでもエラーにならないように修正

エラーを回避するために
　①：数値型の列名リスト（list_columns_include_number）を取得
　②：元データフレーム（df）から、数値型の列のみの新データフレーム（df_include_number）を抽出
　③：②で抽出した新データフレームに対し、Imputerクラスを利用して欠損値を補完
　④：元データフレームの数値型列を新データフレームの値で上書き
の手順で補完する。

```python
from sklearn.preprocessing import Imputer

# 数値型のみのデータフレームを作成する（数値型の列名リスト取得 ⇒ 数値型の列のみ抽出）
list_columns_include_number = df.select_dtypes(include='number').columns.values
df_include_number = df[list_columns_include_number]

# 欠損値を保管するためのImputerを生成して、dfの値に適合させる
imputer = Imputer(missing_values='NaN', strategy='mean', axis=0)
imputer = imputer.fit(df_include_number.values)

# Imputerによる補完を実行する
arr_imputed = imputer.transform(df_include_number.values)

# 補完した結果をデータフレームに変換する
df_imputed = pd.DataFrame(data=arr_imputed, columns=list_columns_include_number)

# 元のデータフレームの値を更新する
df[list_columns_include_number] = df_imputed
```

```:実行前
>>> df
    name   age  sex  height  weight
0  ALice  23.0  0.0   160.3    52.6
1    Bob  28.0  1.0     NaN    74.2
2  Carol  31.0  0.0   154.4     NaN
3   Dave  38.0  1.0     NaN    89.1
4  Ellen  43.0  0.0   149.1    51.4
```

```:実行結果
>>> list_columns_include_number
array(['age', 'sex', 'height', 'weight'], dtype=object)

>>> df_imputed
    age  sex  height  weight
0  23.0  0.0   160.3  52.600
1  28.0  1.0   154.6  74.200
2  31.0  0.0   154.4  66.825
3  38.0  1.0   154.6  89.100
4  43.0  0.0   149.1  51.400

>>> df
    name   age  sex  height  weight
0  ALice  23.0  0.0   160.3  52.600
1    Bob  28.0  1.0   154.6  74.200
2  Carol  31.0  0.0   154.4  66.825
3   Dave  38.0  1.0   154.6  89.100
4  Ellen  43.0  0.0   149.1  51.400
```

### 方針③：欠損値を無視する
これはやめた方が良いかと。。。
どこかしらでエラーや見落としが発生しがちなので…

# おわりに
* 数値型以外の欠損値補完についてはよく分かってないので、調べた上でその内まとめたいなぁ
* 「欠損値の数が閾値以上の列を削除する」の↓のコード、もっときれいに書けないかなぁ<br>
`# 欠損値の数が閾値以下の列名リストを取得`<br>
`series_under_threshold = df.isna().sum() < threshold`<br>
`list_isna_under_thresholt = series_under_threshold[series_under_threshold].index`
* 初めてまともにMarkdown形式で記事書いたけど、慣れないせいで時間かかった...