カテゴリ変数系特徴量の前処理について書きます。記事「scikit-learn数値系特徴量の前処理まとめ(Feature Scaling)」のカテゴリ変数版です。調べてみるとこちらも色々とやり方あることにびっくり。
前処理種類一覧
カテゴリ変数系特徴量に対する前処理種類の一覧です。有名どころだけを一覧化しています(Entity Embeddingは有名でもない?)。
種類 | 内容 |
---|---|
Label Encoding | ラベル種類に応じた数値を割当 |
One-hot Encoding | ラベル種類ごとに特徴量を作りTrue/Falseを割当 |
Count Encoding | ラベルの出現回数を割当 |
Label Count(Count Rank) Encoding | ラベルの出現回数ランクを割当 |
Target Encoding | ラベルごとの目的変数平均値を割当 |
Hash Encoding | ハッシュ関数を使った変換 |
Entity Embedding | ニューラルネットワークを使った変換 |
以下の記事を参考にしました。
Label Encoding
ラベル種類に応じた数値を割当します。以下が例です。
ラベル | Encode後 |
---|---|
黒 | 0 |
白 | 1 |
赤 | 2 |
黒 | 0 |
赤 | 2 |
Scikit-LearnのLabel Encodingの関数一覧です。
関数 | 内容 | 未学習ラベル | None | np.nan |
---|---|---|---|---|
LabelBinarizer | ラベルの2値化 | 0を返す | エラー | エラー |
LabelEncoder | 1つの特徴量に対するラベルエンコーダー | エラー | 許容 | 許容 |
OrdinalEncoder | 複数特徴量に対する一括ラベルエンコーダー | エラー | 許容 | エラー |
LabelBinarizer
以下の例のように二値分類します。
ラベル | Encode後 |
---|---|
no | 0 |
yes | 1 |
yes | 1 |
no | 0 |
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()
df = pd.DataFrame(['no', 'yes', 'yes', 'no'], columns=['binary'])
df['encoded'] = lb.fit_transform(df['binary'])
print(df)
binary encoded
0 no 0
1 yes 1
2 yes 1
3 no 0
学習後に未学習ラベルが来るとエラー発生。
print(lb.transform(['others']))
学習データにNone
やnp.nan
を含んでいるとエラー。
# None や np.nanは Error
lb.fit([np.nan])
lb.fit([None])
LabelEncoder
以下の例のように3種類以上のラベル分類します。
ラベル | Encode後 |
---|---|
black | 0 |
white | 1 |
yellow | 2 |
black | 0 |
import pandas as pd
from sklearn.preprocessing import LabelEncoder
df = pd.DataFrame(['black', 'white', 'yellow', 'black'], columns=['label'])
le = LabelEncoder()
df['encoded'] = le.fit_transform(df['label'])
print(df)
label encoded
0 black 0
1 white 1
2 yellow 2
3 black 0
学習後に未学習ラベルが来るとエラー発生。学習データになくてテストデータにあるラベルに注意が必要。
print(le.transform(['others']))
学習データにNone
やnp.nan
を含んでいても大丈夫。3種類以上のEncoderだからでしょう。
print(le.fit_transform([np.nan, None, 'black']))
[2 1 0]
OrdinalEncoder
LabelEncoderの複数列一括対応Encoderです。二値分類も含めてまとめて使えます。
ラベル1 | ラベル2 | ラベル1Encode後 | ラベル2Encode後 |
---|---|---|---|
black | yes | 0 | 1 |
white | no | 1 | 0 |
yellow | no | 2 | 0 |
black | yes | 0 | 1 |
import pandas as pd
import numpy as np
from sklearn.preprocessing import OrdinalEncoder
df = pd.DataFrame([['black','yes'],
['white', 'no'],
['yellow', 'no'],
['black', 'yes']], columns=['label', 'binary'])
oe = OrdinalEncoder()
df.loc[:,['labelEncoded', 'binaryEncoded']] = oe.fit_transform(df)
print(df)
label binary labelEncoded binaryEncoded
0 black yes 0.0 1.0
1 white no 1.0 0.0
2 yellow no 2.0 0.0
3 black yes 0.0 1.0
LabelEncoder
同様、学習後に未学習ラベルが来るとエラー発生。学習データになくてテストデータにあるラベルに注意が必要。
print(oe.transform([['white', 'others']]))
学習データにNone
は含んでいても大丈夫。
print(oe.fit_transform([[None]]))
[[0.]]
np.nan
だとエラー発生。None
と違ってnp.nan
は数値系だったと考えているのであまり影響ない気がします(曖昧な記憶)。
print(oe.fit_transform([[np.nan]]))
One-hot Encoding
ラベル種類ごとに特徴量を作りTrue/Falseを割当。
ラベル1 | ラベル2 | ラベル1Encode後 black |
ラベル1Encode後 white |
ラベル1Encode後 yellow |
ラベル2Encode後 no |
ラベル2Encode後 yes |
---|---|---|---|---|---|---|
black | yes | 1 | 0 | 0 | 0 | 1 |
white | no | 0 | 1 | 0 | 1 | 0 |
yellow | no | 0 | 0 | 1 | 1 | 0 |
black | yes | 1 | 0 | 0 | 0 | 1 |
Scikit-LearnのOneHotEncoderを使います。OrdinalEncoder
のように一括で複数特徴量を処理できます。
デフォルトだと疎行列を返します。今回は疎行列にする必要ないので、sparse
にFalseを渡して疎行列化をOFFにします。
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder
df = pd.DataFrame([['black','yes'],
['white', 'no'],
['yellow', 'no'],
['black', 'yes']], columns=['label', 'binary'])
ohe = OneHotEncoder(sparse=False)
ohe.fit(df)
df_new = pd.DataFrame(ohe.transform(df),
columns=ohe.get_feature_names(),
dtype=np.int8)
print(pd.concat([df, df_new], axis = 1))
label binary x0_black x0_white x0_yellow x1_no x1_yes
0 black yes 1 0 0 0 1
1 white no 0 1 0 1 0
2 yellow no 0 0 1 1 0
3 black yes 1 0 0 0 1
OrdinalEncoder
同様、学習後に未学習ラベルが来るとエラー発生。学習データになくてテストデータにあるラベルに注意が必要。
print(oe.transform([['white', 'others']]))
学習データにNone
やnp.nan
を含んでいても大丈夫。
print(ohe.fit_transform([[None, np.nan]]))
[[1. 1.]]
多重共線性(マルチコ)を防ぐためにdrop
があります。drop
にfirst
を渡すと、最初のラベルはOne-Hot-Encodingから除外してくれます。例えば、先程のコードにオプションを加えただけの例です。
ohe = OneHotEncoder(sparse=False, drop='first')
ohe.fit(df)
df_new = pd.DataFrame(ohe.transform(df),
columns=ohe.get_feature_names(),
dtype=np.int8)
print(pd.concat([df, df_new], axis = 1))
出力結果で、x0_black
とx1_no
の列が先程と違って消えました。これは、他の列の値から特定できるためです(x1_yes
が1でなければno)。
label binary x0_white x0_yellow x1_yes
0 black yes 0 0 1
1 white no 1 0 0
2 yellow no 0 1 0
3 black yes 0 0 1
Count Encoding
以下の例のようにラベルの出現回数を割当します。
ラベル | Encode後 |
---|---|
black | 3(回) |
white | 2(回) |
black | 3(回) |
black | 3(回) |
white | 2(回) |
blue | 1(回) |
pythonのサンプルです。Pandas使って実現しています。Scikit-Learnには該当関数が2021年5月時点でありませんが、category_encoders
にはCount Encoderがあります。
データにNone
とnp.nan
も含めておきます。
import pandas as pd
import numpy as np
df = pd.DataFrame(['black','white','black','black','white','blue', None, np.nan], columns=['label'])
df['count'] = df.groupby('label')['label'].transform('count')
print(df)
None
とnp.nan
はカウントできませんね。
label count
0 black 3
1 white 2
2 black 3
3 black 3
4 white 2
5 blue 1
6 None NaN
7 NaN NaN
category_encoders
のCount Encoderを使ってみます。データは同じものを使います。
import category_encoders as ce
import pandas as pd
import numpy as np
count_encoder = ce.CountEncoder(cols=['label'])
df = pd.DataFrame(['black','white','black','black','white','blue', None, np.nan], columns=['label'])
df_encoded = count_encoder.fit_transform(df)
print(df_encoded)
そのまま使うと置換され、dataframeを返すようです。None
とnp.nan
は同じ扱いですね。
警告は出ますが、気にしないようにします。
FutureWarning: is_categorical is deprecated and will be removed in a future version. Use is_categorical_dtype instead
elif pd.api.types.is_categorical(cols):
label
0 3
1 2
2 3
3 3
4 2
5 1
6 2
7 2
未知のラベルを渡してみます。
y = pd.DataFrame(['black','other'], columns=['label'])
print(count_encoder.transform(y))
エラーは起きずNaN
となります。
label
0 3.0
1 NaN
Label Count(Count Rank) Encoding
ラベルの出現回数ランクを割当。先程のCount Encodingの例に列追加しました。
ラベル | Count Encoding結果 | Label Count Encoding結果 |
---|---|---|
black | 3(回) | 1(位) |
white | 2(回) | 2(位) |
black | 3(回) | 1(位) |
black | 3(回) | 1(位) |
white | 2(回) | 2(位) |
blue | 1(回) | 3(位) |
pythonのサンプルです。Count Encodingのサンプルコードに続きます。
Scikit-Learnには該当関数が2021年5月時点でありません。ひょっとしたらcategory_encoders
のCount Encoderで渡すパラメータによって実現できるかもしれません(たいして調べてない)。
count_rank = df.groupby('label')['label'].count().rank(ascending=False)
df['count_label'] = df['label'].map(count_rank)
print(df)
None
とnp.nan
はカウントできませんね。
label count count_label
0 black 3 1.0
1 white 2 2.0
2 black 3 1.0
3 black 3 1.0
4 white 2 2.0
5 blue 1 3.0
6 None NaN NaN
7 NaN NaN NaN
Target Encoding
ラベルごとの目的変数平均値を割当します。目的変数の情報を使っているのでリークが起きやすいです。使い所が難しそうで、どういう場合に使える、という点まで解説できないです。関数にいろいろなパラメータがありますが、全然試したり理解したりしていないです。
2022/4/15追記: 以下で試しました。
Greedy Target Statistics
いくつか種類がありますが、以下が一番基本でわかりやすい例です。単純にラベルごとに目的変数の平均を出力。
リークするので使っては駄目らしい。
ラベル | 目的変数 | Encodind結果 |
---|---|---|
black | 1 | $\frac{2}{3}$ |
white | 0 | $\frac{0}{2}$ |
black | 1 | $\frac{2}{3}$ |
black | 0 | $\frac{2}{3}$ |
white | 0 | $\frac{0}{2}$ |
blue | 1 | $\frac{1}{1}$ |
pythonのサンプルです。Pandas使って実現しています。このやり方だとNoneやnp.nanがあるとエラーなので注意してください。
import pandas as pd
import category_encoders as ce
import numpy as np
df = pd.DataFrame([['black', 1],
['white', 0],
['black', 1],
['black', 0],
['white', 0],
['blue', 1]], columns=['label', 'target'])
# このやり方だとNoneやnp.nanがあるとエラー
target_dict = df.groupby(['label'])['target'].mean().to_dict()
df['encoded'] = df['label'].map(lambda x: target_dict[x]).values
print(df)
label target encoded
0 black 1 0.666667
1 white 0 0.000000
2 black 1 0.666667
3 black 0 0.666667
4 white 0 0.000000
5 blue 1 1.000000
Scikit-Learnには該当関数が2021年5月時点でありませんが、category_encoders
にはTarget Encoderがあります。
データにNone
とnp.nan
があってもエラーは起きず、両者を同じラベル扱いします。
df = pd.DataFrame([['black', 1],
['white', 0],
['black', 1],
['black', 0],
['white', 0],
['blue', 1],
[None, 0],
[None, 1],
[np.nan, 1]], columns=['label', 'target'])
# Noneやnp.nanは許容し両者を同じラベル扱い
te = ce.TargetEncoder(cols=['label'])
df_encoded = te.fit_transform(df['label'], df['target'])
print(df_encoded)
少しPandasの結果と異なるのは、スムージングをしていることが理由みたいです。深く調べません。スムージングについては記事「TargetEncodingのスムーシング」がわかりやすいです。パラメータsmoothing
で調整するのですが、0に近いほどスムージングの効果を少なく出来ます。
label
0 0.653422
1 0.149412
2 0.653422
3 0.653422
4 0.149412
5 0.555556
6 0.555556
7 0.555556
8 0.555556
Leave one-out Target Statistics
自分自身のレコードを除外することによりリークを少し防いでいます。この辺からは「はじパタで学んだデータの作り方」と同じ原理でしょう。
けど結局リークするので使っては駄目らしい。
ここからは非常に簡易的にだけ書きます。
pythonのサンプルです。
Scikit-Learnには該当関数が2021年5月時点でありませんが、category_encoders
にはLeave One Outがあります。データにNone
とnp.nan
があってもエラーは起きず、両者を同じラベル扱いします。
te = ce.LeaveOneOutEncoder(cols=['label'])
df_encoded = te.fit_transform(df['label'], df['target'])
print(df_encoded)
データ数が少ないので、あまり良い結果ではないように見えます。
label
0 0.500000
1 0.000000
2 0.500000
3 1.000000
4 0.000000
5 0.555556
6 0.555556
7 0.555556
8 0.555556
Ordered Target Statistics
オンライン学習のコンセプトを取り入れることによりリークを防いでいます(よく理解していない)。
pythonのサンプルです。
Scikit-Learnには該当関数が2021年5月時点でありませんが、category_encoders
にはCatBoost Encoderがあります。データにNone
とnp.nan
があってもエラーは起きず、両者を同じラベル扱いします。
te = ce.CatBoostEncoder(cols=['label'])
df_encoded = te.fit_transform(df['label'], df['target'])
print(df_encoded)
データ数が少ないので、あまり良い結果ではないように見えます。
label
0 0.555556
1 0.555556
2 0.777778
3 0.851852
4 0.277778
5 0.555556
6 0.555556
7 0.555556
8 0.555556
こちらに詳しく書いてくれています。この記事には書いていないHold Out Target Statisticsというものもあります。私が書いたのは受け売りばかり。
Hash Encoding
ハッシュ関数を使った変換。One-Hot Encodingより次元数を少なくできる反面、衝突が起きる可能性もあります。
以下がHash Encodingの例です。blueとyellowが衝突していますね。
ラベル | Hash後1 | Hash後2 | Hash後3 |
---|---|---|---|
black | 0 | 0 | 1 |
white | 0 | 1 | 0 |
black | 0 | 0 | 1 |
black | 0 | 0 | 1 |
white | 0 | 1 | 0 |
blue | 1 | 0 | 0 |
yellow | 1 | 0 | 0 |
pythonのサンプルです。
Scikit-Learnには該当関数が2021年5月時点でありませんが、category_encoders
にはHasingがあります。パラメータn_components
で何ビットにマッピングするかを指定しています。対象のデータにNone
とnp.nan
があってもエラーは起きません。
import pandas as pd
import category_encoders as ce
import numpy as np
df = pd.DataFrame(['black', 'white','black', 'black', 'white', 'blue', 'yellow',None, np.nan], columns=['label'])
he = ce.HashingEncoder(cols=['label'], n_components=3)
print(pd.concat([df['label'], he.fit_transform(df)], axis=1))
None
はすべて0になるようです。
label col_0 col_1 col_2
0 black 0 0 1
1 white 0 1 0
2 black 0 0 1
3 black 0 0 1
4 white 0 1 0
5 blue 1 0 0
6 yellow 1 0 0
7 None 0 0 0
8 NaN 0 1 0
TensorFlow だと以下のようなTutorialもありました。
Entity Embedding
ニューラルネットワークを使った変換。これはリンクだけ載せておきます。
過去にNLPでkeras
が使ったEmbedding
関数と同じかと。非常に良い結果を出してくれました(意識していなかったですが、多分同じもの)。
参考リンク
以下のが非常に参考になります。