44
48

More than 1 year has passed since last update.

カテゴリ変数系特徴量の前処理(scikit-learnとcategory_encoders)

Last updated at Posted at 2021-05-30

カテゴリ変数系特徴量の前処理について書きます。記事「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']))

学習データにNonenp.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']))

学習データにNonenp.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']]))

学習データにNonenp.nanを含んでいても大丈夫。

print(ohe.fit_transform([[None, np.nan]]))
出力結果
[[1. 1.]]

多重共線性(マルチコ)を防ぐためにdropがありますdropfirstを渡すと、最初のラベルは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_blackx1_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があります。
データにNonenp.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)

Nonenp.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_encodersCount 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を返すようです。Nonenp.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_encodersCount Encoderで渡すパラメータによって実現できるかもしれません(たいして調べてない)。

count_rank = df.groupby('label')['label'].count().rank(ascending=False)
df['count_label'] = df['label'].map(count_rank)

print(df)

Nonenp.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があります。
データにNonenp.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があります。データにNonenp.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があります。データにNonenp.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で何ビットにマッピングするかを指定しています。対象のデータにNonenp.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関数と同じかと。非常に良い結果を出してくれました(意識していなかったですが、多分同じもの)。

参考リンク

以下のが非常に参考になります。

44
48
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
48