Python 機械学習プログラミングの備忘録その４（データ前処理）

#1.はじめに
**Python機械学習プログラミング[第2版]** という本を買って勉強を始めました。効率的な学習を進める為には、勉強した内容をアウトプットするのが効果的ということで、勉強した内容を備忘録の形でここに残すことを進めています。

なお、本ではコードを一部 jupyter notebook形式で記載しているようですが、ここでは.py形式でも実行できるような形式で記載したいと思います。

今回は、第4章データ前処理です。

#2.欠損値の削除・補完
機械学習に使うデータには、欠損値があるのが普通で、これを前処理で適切に処理しておかないと、機械学習が上手く行えません。まず、テキストのコードを実行してみましょう。

```python
import pandas as pd
from io import StringIO

csv_data = '''A,B,C,D
1.0,2.0,3.0,4.0
5.0,6.0,,8.0
10.0,11.0,12.0,'''

df = pd.read_csv(StringIO(csv_data))  # サンプルデータを読み込む
print(df)  # 読み込んだサンプルデータを表示
print()
print(df.isnull().sum())  # 各列の欠損値(NaN)の数を表示
print()
print(df.dropna(axis=0))  # 欠損値(NaN)を含む行を削除
print()
print(df.dropna(axis=1))  # 欠損値(NaN)を含む列を削除
print()
print(df.dropna(how='all'))  # 全ての列が欠損値(NaN）である行を削除
print()
print(df.dropna(thresh=4))  # 非欠損値（非NaN)が4未満の行を削除
print()
print(df.dropna(subset=['C']))  # 列'C'に欠損値(NaN)が含まれている行を削除
print()
```
![スクリーンショット 2019-09-14 19.41.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/ac986ef0-c5b6-66a3-eae5-f6c9bc244a0c.png)
この様に様々な方法で、欠損値を含むデータを削除する方法はありますが、何でもかんでも欠損値を含んだデータを削除すると、データが少なくなって機械学習が上手く進みません。

そのため、欠損値を当たらずとも遠からずの値に置き換えて、欠損値を補完する手法があります。テキストのコードを実行してみましょう。

```python
# 欠損値の補完
import numpy as np
from sklearn.impute import SimpleImputer
imr = SimpleImputer(missing_values=np.nan, strategy='mean')  # 列方向の平均
imr = imr.fit(df.values)
imputed_data = imr.transform(df.values)
print(imputed_data)
```
![スクリーンショット 2019-09-14 19.54.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/3305d4a1-40ca-6c3e-d623-d7f242fb3f52.png)
3列目は (3+12)/2 = 7.5, 4列目は (4+8)/2 = 6 と補完されていますね。

テキストでは、Imputerクラスを使っていますが、私のsklearnのバージョン（最新の0.21.3）ではワーニングが出るので、SimpleImputerクラスを使っています。Imputer クラスはもうすぐ無くなり、今後はSimpleImputer クラスを使ってくれ、と言うことです。

それに伴い、numpyのインポート、SimpleImputerの引数は missing_values='NaN' → missing_values=np.nan、axis=0は削除しています。今後、axisの指定はなくなり、列方向のみの処理になると言うことですが、確かにそれで十分な気がします。

#3.カテゴリーデータの処理
さて、次にクラスラベルのエンコーディングについて、テキストを実行してみましょう。

```python
import pandas as pd
import numpy as np

# サンプルデータのセット
df = pd.DataFrame([['green', 'M', 10.1, 'class1'],
                   ['red', 'L', 13.5, 'class2'],
                   ['blue', 'XL', 15.3, 'class1']])
df.columns = ['color', 'size', 'price', 'classlabel']
print(df)
print()

# 'size'を'XL','L','M'から整数(1, 2, 3)へ変換
size_mapping = {'XL': 3, 'L': 2, 'M': 1}  
df['size'] = df['size'].map(size_mapping)
print(df)
print()

# 'size'のバリューとキー書き出す
inv_size_mapping = {v: k for k, v in size_mapping.items()}
print(df['size'].map(inv_size_mapping))
print()

# 'classlabel'に整数を割り当て
class_mapping = {label: idx for idx, label in enumerate(np.unique(df['classlabel']))}
print(class_mapping)
print()

# 'classlabel'を整数に変換
df['classlabel'] = df['classlabel'].map(class_mapping)
print(df)
print()

# 'classlabel'を元に戻す
inv_class_mapping = {v: k for k, v in class_mapping.items()}
df['classlabel'] = df['classlabel'].map(inv_class_mapping)
print(df)
print()

# 'classlabel'を元に戻す（LabelEncoderを利用）
from sklearn.preprocessing import LabelEncoder
class_le = LabelEncoder()
y = class_le.fit_transform(df['classlabel'].values)
print(y)
print()

# 'classlabel'を文字列に戻す
print(class_le.inverse_transform(y))
print()

# 'color', 'size', 'price'のバリューを表示
X = df[['color', 'size', 'price']].values
color_le = LabelEncoder()
X[:, 0] = color_le.fit_transform(X[:, 0])
print(X)
print()
```
![スクリーンショット 2019-09-15 13.07.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/6d474370-9410-1e64-97fa-b77791bd12c0.png)
この様に、整数のクラスラベルを付けたり元に戻したりすることが出来ます。

クラスラベルの付け方には、one-hotエンコーディングと言う形式があります。テキストのコードを実行してみましょう。

```python
# one-hotエンコーディング(sklearn)
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
ct = ColumnTransformer([('color', OneHotEncoder(), [0])], remainder='passthrough')
print(ct.fit_transform(df)[:,:5])
print()

# one-hotエンコーディング(pandas)
print(pd.get_dummies(df[['price', 'color', 'size']]))
print()

# one-hotエンコーディング (pandas 最初の列を削除)
print(pd.get_dummies(df[['price', 'color', 'size']], drop_first=True))
print()

# one-hotエンコーディング (pandas 列操作)
ct = ColumnTransformer([('color', OneHotEncoder(), [0])], remainder='passthrough')
print(ct.fit_transform(df)[:, 1:5])
print()
```
![スクリーンショット 2019-09-15 13.37.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/e7e7752f-1ead-c00b-c1d0-dd00ce31a6e0.png)
one-hotエンコーディングを行う場合は、pandasの方が便利な感じです。

なお、テキストでは、OneHotEncoderクラスを使っていますが、これも私のsklearnのバージョンではワーニングが出るので、ColumnTransformerクラスを使っています。

#4.データセットの分割、正規化・標準化
Wineという新しいデータセットを使って、データセットの分割、正規化・標準化を行います。テキストのコードを実行してみましょう。

```python
import pandas as pd
import numpy as np

# wineデータセットの読み込みと表示
df_wine = pd.read_csv('https://archive.ics.uci.edu/'
                      'ml/machine-learning-databases/wine/wine.data',
                      header=None)
df_wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash',
                   'Alcalinity of ash', 'Magnesium', 'Total phenols',
                   'Flavanoids', 'Nonflavanoid phenols', 'Proanthocyanins',
                   'Color intensity', 'Hue', 'OD280/OD315 of diluted wines',
                   'Proline']
print('Class labels', np.unique(df_wine['Class label']))  # クラスラベルを表示
print()
print(df_wine.head())  # 先頭から5行を表示
print()

# データセットをトレーニング：テスト＝7:3で分割
from sklearn.model_selection import train_test_split
X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values  # Xにデータ、yにクラスラベルを代入
X_train, X_test, y_train, y_test = train_test_split(X, y, 
                                   test_size=0.3,  # テストデータを30%
                                   random_state=0,  # ランダムシードは0
                                   stratify=y)  # トレーニング数＝テスト数
print('X_train = ', X_train)
print()
print('y_train = ', y_train)
print()

# 正規化 (min-max スケーリング)
from sklearn.preprocessing import MinMaxScaler
mms = MinMaxScaler()
X_train_norm = mms.fit_transform(X_train)
X_test_norm = mms.transform(X_test)
print('X_train_norm = ', X_train_norm)
print()

# 標準化
from sklearn.preprocessing import StandardScaler
stdsc = StandardScaler()
X_train_std = stdsc.fit_transform(X_train)
X_test_std = stdsc.transform(X_test)
print('X_train_std = ', X_train_std)
print()
```
![スクリーンショット 2019-09-17 12.11.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/31805e57-6ddf-59b1-476f-4722f870ec3b.png)
データセットの分割、正規化・標準化とも、ライブラリーを使って、簡単に対応出来ますね。

さて、過学習を減らす手法には、正則化、特徴量削減による次元削減があります。まず、正則化による効果をテキストのコードを実行してみてみましょう。

```python
# 正則化パラメーターCを変化させた場合の重みの変化
import matplotlib.pyplot as plt
fig = plt.figure()
ax = plt.subplot(111)    
colors = ['blue', 'green', 'red', 'cyan', 
          'magenta', 'yellow', 'black', 
          'pink', 'lightgreen', 'lightblue', 
          'gray', 'indigo', 'orange']
weights, params = [], []

for c in np.arange(-4., 6.):
    lr = LogisticRegression(solver='liblinear', multi_class='auto', penalty='l1', C=10.**c, random_state=0)
    lr.fit(X_train_std, y_train)
    weights.append(lr.coef_[1])
    params.append(10**c)

weights = np.array(weights)

for column, color in zip(range(weights.shape[1]), colors):
    plt.plot(params, weights[:, column],
             label=df_wine.columns[column + 1],
             color=color)
plt.axhline(0, color='black', linestyle='--', linewidth=3)
plt.xlim([10**(-5), 10**5])
plt.ylabel('weight coefficient')
plt.xlabel('C')
plt.xscale('log')
plt.legend(loc='upper left')
ax.legend(loc='upper center', 
          bbox_to_anchor=(1.38, 1.03),
          ncol=1, fancybox=True)
plt.show()
```
![スクリーンショット 2019-09-17 13.11.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/2e06c0bf-831e-89ea-7ed5-503c47220495.png)
正則化パラメーターCを小さくする（パラメーターの強さを高める）と、全ての重みがゼロに近づいて行き、どんどんモデルがシンプルになります。じゃあ、Cは小さければ小さいほど良いかと言うと、今度は精度が問題になって来ます。

テキストでは、正則化パラメーターCと精度の関係は触れていませんが、ついでに見ておきましょう。

上記のコードに、変数 test_accuracyを追加し、Cの刻みを小さくして（大きいと微妙な動きが見え難い）、パラメーターCによる精度変化のグラフ表示を追加し、実行します。

```python
# パラメーターCによる特徴量の重み変化
import matplotlib.pyplot as plt
fig = plt.figure()
ax = plt.subplot(111)
    
colors = ['blue', 'green', 'red', 'cyan', 
          'magenta', 'yellow', 'black', 
          'pink', 'lightgreen', 'lightblue', 
          'gray', 'indigo', 'orange']
weights, params = [], []
test_accuracy =[]  # 追加
for c in np.arange(-4., 6., 0.1):  # 刻みを1 →　0.1に変更
    lr = LogisticRegression(solver='liblinear', multi_class='auto', penalty='l1', C=10.**c, random_state=0)
    lr.fit(X_train_std, y_train)
    weights.append(lr.coef_[1])
    params.append(10**c)    
    test_accuracy.append(lr.score(X_test_std, y_test))  # 追加

weights = np.array(weights)

for column, color in zip(range(weights.shape[1]), colors):
    plt.plot(params, weights[:, column],
             label=df_wine.columns[column + 1],
             color=color)
plt.axhline(0, color='black', linestyle='--', linewidth=3)
plt.xlim([10**(-5), 10**5])
plt.ylabel('weight coefficient')
plt.xlabel('C')
plt.xscale('log')
plt.legend(loc='upper left')
ax.legend(loc='upper center', 
          bbox_to_anchor=(1.38, 1.03),
          ncol=1, fancybox=True)
plt.show()

# パラメーターCによる精度変化表示（追加）
plt.plot(params, test_accuracy)
plt.xlim([10**(-5), 10**5])
plt.xlabel('C')
plt.ylabel('test_accuracy')
plt.xscale('log')
plt.show()
```
![スクリーンショット 2019-09-17 13.24.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/9d8c788c-8b4d-1b02-90d5-8f4c530bb2bc.png)
グラフを見ると、C = 0.2以下になるとtest_accuracyが100％を割り込みます。従って、今回の場合は、C = 0.3位が多くの特徴量の重みが0になっていて、しかも精度が良いという結果になりました。

今度は、特徴量削減による次元削減のコードを実行してみましょう。

```python
from sklearn.metrics import accuracy_score
from sklearn.base import clone
from itertools import combinations

# SBSアルゴリズム
class SBS():
    def __init__(self, estimator, k_features, scoring=accuracy_score,
                 test_size=0.25, random_state=1):
        self.scoring = scoring
        self.estimator = clone(estimator)
        self.k_features = k_features
        self.test_size = test_size
        self.random_state = random_state

    def fit(self, X, y):
        
        X_train, X_test, y_train, y_test =             train_test_split(X, y, test_size=self.test_size,
                             random_state=self.random_state)

        dim = X_train.shape[1]
        self.indices_ = tuple(range(dim))
        self.subsets_ = [self.indices_]
        score = self._calc_score(X_train, y_train, 
                                 X_test, y_test, self.indices_)
        self.scores_ = [score]

        while dim > self.k_features:
            scores = []
            subsets = []

            for p in combinations(self.indices_, r=dim - 1):
                score = self._calc_score(X_train, y_train, 
                                         X_test, y_test, p)
                scores.append(score)
                subsets.append(p)

            best = np.argmax(scores)
            self.indices_ = subsets[best]
            self.subsets_.append(self.indices_)
            dim -= 1

            self.scores_.append(scores[best])
        self.k_score_ = self.scores_[-1]

        return self

    def transform(self, X):
        return X[:, self.indices_]

    def _calc_score(self, X_train, y_train, X_test, y_test, indices):
        self.estimator.fit(X_train[:, indices], y_train)
        y_pred = self.estimator.predict(X_test[:, indices])
        score = self.scoring(y_test, y_pred)
        return score


# KNN分類機
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=5)
sbs = SBS(knn, k_features=1)
sbs.fit(X_train_std, y_train)

# 特徴量の個数と精度のグラフ表示
k_feat = [len(k) for k in sbs.subsets_]
plt.plot(k_feat, sbs.scores_, marker='o')
plt.ylim([0.7, 1.02])
plt.ylabel('Accuracy')
plt.xlabel('Number of features')
plt.grid()
plt.tight_layout()
plt.show()

# k = 3の特徴量を表示
k3 = list(sbs.subsets_[10])
print(df_wine.columns[1:][k3])
print()

# 13個全ての特徴量を用いた場合の精度
knn.fit(X_train_std, y_train)
print('13_Training accuracy:', knn.score(X_train_std, y_train))
print('13_Test accuracy:', knn.score(X_test_std, y_test))
print()

# 3個の特徴量を用いた場合の精度
knn.fit(X_train_std[:, k3], y_train)
print('3_Training accuracy:', knn.score(X_train_std[:, k3], y_train))
print('3_Test accuracy:', knn.score(X_test_std[:, k3], y_test))
print()
```
![スクリーンショット 2019-09-17 14.24.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/bedb96ac-d4b3-6643-9f7f-3131b7c20861.png)
素晴らしい！　たった３個の特徴量だけでも、選び抜いたものであれば、そこそこの精度が出るわけですね。

最後は、ランダムフォレストによる特徴量の優先順位付けです。テキストのコードを実行してみましょう。

```python
# ランダムフォレストによる特徴量の優先順位付け
from sklearn.ensemble import RandomForestClassifier
feat_labels = df_wine.columns[1:]
forest = RandomForestClassifier(n_estimators=500,
                                random_state=1)
forest.fit(X_train, y_train)
importances = forest.feature_importances_
indices = np.argsort(importances)[::-1]

# 優先順位の重要度表示
for f in range(X_train.shape[1]):
    print("%2d) %-*s %f" % (f + 1, 30, 
                            feat_labels[indices[f]], 
                            importances[indices[f]]))
    
# 棒グラフの表示  
plt.title('Feature Importance')
plt.bar(range(X_train.shape[1]), 
        importances[indices],
        align='center')
plt.xticks(range(X_train.shape[1]), 
           feat_labels[indices], rotation=90)
plt.xlim([-1, X_train.shape[1]])
plt.tight_layout()
plt.show()
```
![スクリーンショット 2019-09-18 10.35.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/389fbb40-bda5-d230-fa31-c1d33bed3ea1.png)
なるほど。ライブラリーを使って、一発で特徴量の優先順位付けをしてくれるのは、便利そうですね。

scikit-learn には、ランダムフォレストの他にも、SelectFromModelというのもあり、テキストにコードがありますので、動かしてみましょう。

```python
# SelectFromModelクラスによる特徴量の優先順位付け
from sklearn.feature_selection import SelectFromModel
sfm = SelectFromModel(forest, threshold=0.1, prefit=True)
X_selected = sfm.transform(X_train)
print('Number of features that meet this threshold criterion:', 
      X_selected.shape[1])
for f in range(X_selected.shape[1]):
    print("%2d) %-*s %f" % (f + 1, 30, 
                            feat_labels[indices[f]], 
                            importances[indices[f]]))
```
![スクリーンショット 2019-09-18 10.40.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/3c68b47b-f9ee-be65-9626-e570a9030b55.png)
優先順位をどの程度まで選ぶかは、引数 threshold = 0.1 のところで指定します。