Edited at

Pythonではじめる機械学習 データ表現と特徴量エンジニアリング(カテゴリ変数・ビニング)


データ表現と特徴量エンジニアリング


  • 2次元の浮動小数点数配列のデータポイントである連続値特徴量を扱ってきたが,これの他にカテゴリ特徴量離散値特徴量などがある

  • 様々な特徴量のタイプがあるが,全てに共通して言えることは特徴量の表現はモデルの性能に多大な影響を及ぼすということである

  • これまでにデータのスケール変換などを行ったが,このように最良のデータ表現を模索することを特徴量エンジニアリングと呼ぶ

  • データを正しく表現することはパラメータの調整よりも重要なため慎重に行うことが必要である

  • これからこのことについて詳しく見ていく


カテゴリ変数


  • 次にアメリカ合衆国成人の収入データ(adultデータセット)を見ていく

  • このデータセットは様々な要因により,労働者の収入が50,000ドルを超えるか予想するものである

  • このデータセットを回帰タスクとして扱うと非常に難しいものとなってしまうので,クラス分類タスクとして処理をしていく

  • このデータセットの項目には「年齢」「労働時間」といった連続値と,「雇用形態」「教育」「性別」「職業」といったカテゴリ特徴量がある

  • まずはロジスティック回帰クラス分類を行うため,ロジスティック回帰の式(y = w[0] × x[0] + w[1] × x[1] + ・・・w[p] × x[p] + b > 0)を用いてyを予測する

  • w[i]とbは訓練セットから学習した係数でx[i]は入力特徴量である

  • x[i]が数値の場合は意味を持つが,x[2]がカテゴリ特徴量の場合には意味を持たない

  • ロジスティック回帰を適用するためには,データを別の方法で表現する必要がある

  • これを解決する方法を次に示す


ワンホットエンコーディング(ダミー変数)


  • カテゴリ変数を表現する方法としてワンホットエンコーディング(ダミー変数)が最も用いられている

  • ダミー変数とはカテゴリ変数を0と1の値で置き換える手法である

  • 例えば雇用形態に「公務員」「民間」「自営業」「自営法人」があるとして,ある人物の雇用形態が対応する値の時は1を,違う時は0となるものである

  • 具体的には次のようなものである

雇用形態
公務員
民間
自営業
自営法人

公務員
1
0
0
0

民間
0
1
0
0

自営業
0
0
1
0

自営法人
0
0
0
1


  • カテゴリ変数をダミー変数に変換するにはpandasかscikit-learnを使う方法があるが,pandasの方が簡単なのでこちらを使う

  • まずはpandasを使ってCSVファイルからデータを読み込む

In[1]:

%matplotlib inline
import pandas as pd
import os
import mglearn

adult_path = os.path.join(mglearn.datasets.DATA_PATH, "adult.data")
# このファイルにはコラム名を含んだヘッダがないので,header=Noneを指定
# コラム名を"names"で明示する
data = pd.read_csv(adult_path, header=None, index_col=False,
names=['age', 'workclass', 'fnlwgt', 'education',
'education-num', 'marital-status', 'occupation',
'relationship', 'race', 'gender', 'capital-gain',
'capital-loss', 'hours-per-week', 'native-country',
'income'])
# いくつかのカラムだけを選択
data = data[['age', 'workclass', 'education', 'gender', 'hours-per-week',
'occupation', 'income']]
display(data.head())

Out[1]:


  • データセットを読み込んだ

  • 次は各列に意味あるカテゴリデータが含まれているかを確認する

  • 例えば,性別の表記が「man」「Man」「male」といった違いを処理するためである

  • これはpandasのvalue_countsを用いて確認することができる

In[2]:

print(data.gender.value_counts())

Out[2]:
Male 21790
Female 10771
Name: gender, dtype: int64


  • genderには「Male」と「Famele」しか含まれていないことがわかる

  • このデータは整理されているので,この状態でワンホットコーディングが可能である

  • pandasではget_dummies関数を使ってワンホットコーディングが可能である

In[3]:

print("Original features:\n", list(data.columns), "\n")
data_dummies = pd.get_dummies(data)
print("Features after get_dummies:\n", list(data_dummies.columns))

Out[3]:
Original features:
['age', 'workclass', 'education', 'gender', 'hours-per-week', 'occupation', 'income']

Features after get_dummies:
['age', 'hours-per-week', 'workclass_ ?', 'workclass_ Federal-gov', 'workclass_ Local-gov', 'workclass_ Never-worked', 'workclass_ Private', 'workclass_ Self-emp-inc', 'workclass_ Self-emp-not-inc', 'workclass_ State-gov', 'workclass_ Without-pay', 'education_ 10th', 'education_ 11th', 'education_ 12th', 'education_ 1st-4th', 'education_ 5th-6th', 'education_ 7th-8th', 'education_ 9th', 'education_ Assoc-acdm', 'education_ Assoc-voc', 'education_ Bachelors', 'education_ Doctorate', 'education_ HS-grad', 'education_ Masters', 'education_ Preschool', 'education_ Prof-school', 'education_ Some-college', 'gender_ Female', 'gender_ Male', 'occupation_ ?', 'occupation_ Adm-clerical', 'occupation_ Armed-Forces', 'occupation_ Craft-repair', 'occupation_ Exec-managerial', 'occupation_ Farming-fishing', 'occupation_ Handlers-cleaners', 'occupation_ Machine-op-inspct', 'occupation_ Other-service', 'occupation_ Priv-house-serv', 'occupation_ Prof-specialty', 'occupation_ Protective-serv', 'occupation_ Sales', 'occupation_ Tech-support', 'occupation_ Transport-moving', 'income_ <=50K', 'income_ >50K']
```

- 連続値のagehours-per-weekは変更されず,カテゴリ特徴量は拡張されていることがわかる

```python
In[4]:
data_dummies.head()

Out[4]:


  • カテゴリ特徴量が値に変換されたのが確認できる

  • ここでvalue属性を用いれば,data_dummies DataFrameをNumpy配列に変換しモデルに学習させることができる

  • モデルに学習させる前にターゲット変数・出力変数など(求めたい値,ここではincome列)を分離する必要がある

  • つまり,ここでは'age'から'occupation_ Transport-moving'までを抜き出して学習を行う

In[5]:

features = data_dummies.ix[:, 'age':'occupation_ Transport-moving']
# valuesでNumpy配列に変換
X = features.values
y = data_dummies['income_ >50K'].values
print("X.shape: {} y.shape: {}".format(X.shape, y.shape))

Out[5]:
X.shape: (32561, 44) y.shape: (32561,)


  • Numpy配列に変換したことで,scikit-learnが扱える形になった

  • 次に学習を行う


In[6]:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
logreg = LogisticRegression()
logreg.fit(X_train, y_train)
print("Test score: {:.2f}".format(logreg.score(X_test, y_test)))

Out[6]:
Test score: 0.81


  • この一連の流れで,カテゴリ特徴量の変換を行いモデルに学習させる


数値でエンコードされているカテゴリ


  • データセット内でカテゴリ変数が整数で表されているため()に,連続値と区別がつかない時がある

  • ある特徴量がカテゴリ変数なのか連続値なのかを区別するためにはscikit-learnのOneHotEncoderを用いるか,DataFrameの列を数値から文字列に変換してしまうのが良い

  • 2列のDataFrame(文字列と数値が1列ずつのもの)を使って説明していく

In[7]:

demo_df = pd.DataFrame({'Integer Feature': [0, 1, 2, 1],
'Categorical Feature': ['socks', 'fox', 'socks', 'box']})
display(demo_df)

In[8]:
display(demo_df)

In[9]:
demo_df['Integer Feature'] = demo_df['Integer Feature'].astype(str)
pd.get_dummies(demo_df, columns=['Integer Feature', 'Categorical Feature'])

In[7]:

カテゴリ特徴量と文字列特徴量を持つDataFrame

In[8]:

ワンホットコーディングしたもの

整数特徴量は変わっていない


In[9]:

さらにワンホットエンコーディングしたもの

整数特徴量も文字列特徴量もエンコードされている



  • このようにしてカテゴリ変数なのか連続値なのかを明らかにすることができる


ビニング


  • どのモデルを利用するかによって,最良のデータ表現は変わってくる

  • 例えば,線形モデルと決定木ベースのモデルではデータ表現が大きく異なったものになる

  • wave回帰データセット(入力特徴量が1つしかない)を用いてこの例を見てみる

In[10]:

X, y = mglearn.datasets.make_wave(n_samples=120)
line = np.linspace(-3, 3, 1000, endpoint=False).reshape(-1, 1)

reg = DecisionTreeRegressor(min_samples_leaf=3).fit(X, y)
plt.plot(line, reg.predict(line), label="decision tree")

reg = LinearRegression().fit(X, y)
plt.plot(line, reg.predict(line), label="linear regression")

plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.legend(loc="best")

Out[10]:


  • 見た通り,直線でしか表すことのできない線形モデルよりも,決定木の方がはるかに複雑なモデルを構築することができる

  • しかし,データの表現が変われば話は別である

  • データ表現を変更し,線形モデルをより強力なモデルとして扱う方法として特徴量のビニング離散化がある

  • 簡単に言うと特徴量を複数の特徴量に分割する方法である

  • 特徴量の入力レンジ(ここでは-3から3)を10個のビンに分割することを考える

  • つまり-3か3までを等間隔に10に区切る(np.linspaceで11のエントリを作る)

In[11]:

bins = np.linspace(-3, 3, 11)
print("bins: {}".format(bins))

Out[11]:
bins: [-3. -2.4 -1.8 -1.2 -0.6 0. 0.6 1.2 1.8 2.4 3. ]


  • 最初のビンには-3から-2.4までの全ての数が入り,次のビンには-2.4から-1.8までの全ての数が入る

  • 次に個々のデータポイントがどのビンに入るかを記録する(np.digitize関数で行う)

In[12]:

which_bin = np.digitize(X, bins=bins)
print("\nData points:\n", X[:5])
print("\nBin membership for data points:\n", which_bin[:5])

Out[12]:
Data points: # データポイント
[[-0.75275929]
[ 2.70428584]
[ 1.39196365]
[ 0.59195091]
[-2.06388816]]

Bin membership for data points: # それぞれのデータが入るビン
[[ 4]
[10]
[ 8]
[ 6]
[ 2]]


  • どのビンにどのデータポイントが入っているかを表現した,カテゴリ特徴量へ置き換えたものである

  • このデータをscikit-learnで扱うにはこの離散値特徴量をワンホットエンコーディングに変換する必要がある

  • これにはpreprocessingモジュールのOneHotEncoderを用いる

In[13]:

from sklearn.preprocessing import OneHotEncoder

# OneHotEncoderで変換する
encoder = OneHotEncoder(sparse=False)
# encoder.fitでwhich_binに現れる整数値のバリエーションを確認
encoder.fit(which_bin)
# transformでワンホットエンコーディングを行う
X_binned = encoder.transform(which_bin)
print(X_binned[:5])

Out[13]:
[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
[0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]]

In[14]:
print("X_binned.shape: {}".format(X_binned.shape))

Out[14]:
X_binned.shape: (120, 10)


  • 10ビンを指定したので,変換されたデータセットX_binnedには10個の特徴量ができている

  • このデータを使って,先ほどと同様に線形モデルと決定木に対して学習を行ってみる

In[15]:

line_binned = encoder.transform(np.digitize(line, bins=bins))

reg = LinearRegression().fit(X_binned, y)
plt.plot(line, reg.predict(line_binned), label='linear regression binned')

reg = DecisionTreeRegressor(min_samples_split=3).fit(X_binned, y)
plt.plot(line, reg.predict(line_binned), label='decision tree binnded')

plt.plot(X[:, 0], y, 'o', c='k')
plt.vlines(bins, -3, 3, linewidth=1, alpha=.2)
plt.legend(loc="best")
plt.ylabel("Regression output")
plt.xlabel("Input feature")

Out[15]:


  • 線形回帰と決定木が全く同じ予測を行っていることがわかる

  • これはビンごとに特徴量が一定になるので,どのようなモデルを持ってきてもビンの中では同じ値を予測するためである

  • 線形モデルは柔軟になったが,決定木では柔軟性が低下しているため決定木でビニングするメリットはない

  • また決定木では特徴量を複数扱えるが,ビニングは1つの特徴量ごとにしか行えない

  • しかし,線形モデルを扱う場合では効果は絶大である