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

カテゴリ変数系特徴量の前処理について書きます。[記事「scikit-learn数値系特徴量の前処理まとめ(Feature Scaling)」](https://qiita.com/FukuharaYohei/items/7508f2146c63ffe16b1e)のカテゴリ変数版です。調べてみるとこちらも色々とやり方あることにびっくり。

# 前処理種類一覧
カテゴリ変数系特徴量に対する前処理種類の一覧です。有名どころだけを一覧化しています(Entity Embeddingは有名でもない？)。

|種類|内容|
|:--|:--|
|Label Encoding|ラベル種類に応じた数値を割当|
|One-hot Encoding|ラベル種類ごとに特徴量を作りTrue/Falseを割当|
|Count Encoding|ラベルの出現回数を割当|
|Label Count(Count Rank) Encoding|ラベルの出現回数ランクを割当|
|Target Encoding|ラベルごとの目的変数平均値を割当|
|Hash Encoding|ハッシュ関数を使った変換|
|Entity Embedding|ニューラルネットワークを使った変換|

以下の記事を参考にしました。

http://nami3373.hatenablog.com/entry/2018/07/26/230655

https://qiita.com/ground0state/items/f516b97c7a8641e474c4

## Label Encoding
ラベル種類に応じた数値を割当します。以下が例です。

|ラベル|Encode後|
|:-:|:-:|
|黒|0|
|白|1|
|赤|2|
|黒|0|
|赤|2|

Scikit-LearnのLabel Encodingの関数一覧です。

|関数|内容|未学習ラベル|None|np.nan|
|:-:|:--|:--|:--|:--|
|[LabelBinarizer](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelBinarizer.html)|ラベルの2値化|0を返す|エラー|エラー|
|[LabelEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html)|1つの特徴量に対するラベルエンコーダー|エラー|許容|許容|
|[OrdinalEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OrdinalEncoder.html)|複数特徴量に対する一括ラベルエンコーダー|エラー|許容|エラー|

### [LabelBinarizer](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelBinarizer.html)
以下の例のように二値分類します。

|ラベル|Encode後|
|:-:|:-:|
|no|0|
|yes|1|
|yes|1|
|no|0|

```python
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
```

学習後に未学習ラベルが来るとエラー発生。

```python:エラー発生するサンプル
print(lb.transform(['others']))
```

学習データに`None`や`np.nan`を含んでいるとエラー。

```python:エラー発生するサンプル
# None や np.nanは Error
lb.fit([np.nan])
lb.fit([None])
```

### [LabelEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html)
以下の例のように3種類以上のラベル分類します。

|ラベル|Encode後|
|:-:|:-:|
|black|0|
|white|1|
|yellow|2|
|black|0|

```python
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
```

学習後に**未学習ラベルが来るとエラー発生**。学習データになくてテストデータにあるラベルに注意が必要。

```python:エラー発生するサンプル
print(le.transform(['others']))
```

学習データに`None`や`np.nan`を含んでいても大丈夫。3種類以上のEncoderだからでしょう。

```python
print(le.fit_transform([np.nan, None, 'black']))
```

```:出力結果
[2 1 0]
```

### [OrdinalEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OrdinalEncoder.html)
[LabelEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html)の複数列一括対応Encoderです。二値分類も含めてまとめて使えます。

|ラベル1|ラベル2|ラベル1Encode後|ラベル2Encode後|
|:-:|:-:|:-:|:-:|
|black|yes|0|1|
|white|no|1|0|
|yellow|no|2|0|
|black|yes|0|1|

```python
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`同様、学習後に**未学習ラベルが来るとエラー発生**。学習データになくてテストデータにあるラベルに注意が必要。

```python:エラー発生するサンプル
print(oe.transform([['white', 'others']]))
```

学習データに`None`は含んでいても大丈夫。

```python
print(oe.fit_transform([[None]]))
```

```:出力結果
[[0.]]
```

`np.nan`だとエラー発生。`None`と違って`np.nan`は数値系だったと考えているのであまり影響ない気がします(曖昧な記憶)。

```python:エラー発生するサンプル
print(oe.fit_transform([[np.nan]]))
```

## One-hot Encoding
ラベル種類ごとに特徴量を作りTrue/Falseを割当。

|ラベル1|ラベル2|ラベル1Encode後<br>black|ラベル1Encode後<br>white|ラベル1Encode後<br>yellow|ラベル2Encode後<br>no|ラベル2Encode後<br>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](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html)を使います。`OrdinalEncoder`のように一括で複数特徴量を処理できます。
デフォルトだと疎行列を返します。今回は疎行列にする必要ないので、`sparse`にFalseを渡して疎行列化をOFFにします。

```python
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`同様、学習後に**未学習ラベルが来るとエラー発生**。学習データになくてテストデータにあるラベルに注意が必要。

```python:エラー発生するサンプル
print(oe.transform([['white', 'others']]))
```

学習データに`None`や`np.nan`を含んでいても大丈夫。

```python
print(ohe.fit_transform([[None, np.nan]]))
```

```:出力結果
[[1. 1.]]
```

**[多重共線性(マルチコ)](https://bellcurve.jp/statistics/glossary/1792.html)を防ぐために`drop`があります**。`drop`に`first`を渡すと、最初のラベルはOne-Hot-Encodingから除外してくれます。例えば、先程のコードにオプションを加えただけの例です。

```python
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](https://contrib.scikit-learn.org/category_encoders/count.html)があります。
データに`None`と`np.nan`も含めておきます。

```python
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](https://contrib.scikit-learn.org/category_encoders/count.html)を使ってみます。データは同じものを使います。

```python
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
```

未知のラベルを渡してみます。

```python
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](https://contrib.scikit-learn.org/category_encoders/count.html)で渡すパラメータによって実現できるかもしれません(たいして調べてない)。

```python
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追記: 以下で試しました。

https://qiita.com/FukuharaYohei/items/88cc562ab7800cbc01cb

### 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があるとエラー**なので注意してください。

```python
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](https://contrib.scikit-learn.org/category_encoders/targetencoder.html)があります。
データに`None`と`np.nan`があってもエラーは起きず、両者を同じラベル扱いします。

```python
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のスムーシング」](https://mikebird28.hatenablog.jp/entry/2018/06/14/172132)がわかりやすいです。パラメータ`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
自分自身のレコードを除外することによりリークを少し防いでいます。この辺からは[「はじパタで学んだデータの作り方」](https://qiita.com/FukuharaYohei/items/0bae9bbaef0099627751#221-%E5%AD%A6%E7%BF%92%E3%83%87%E3%83%BC%E3%82%BF%E3%81%A8%E3%83%86%E3%82%B9%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9)と同じ原理でしょう。
けど結局**リークするので使っては駄目**らしい。
ここからは非常に簡易的にだけ書きます。

pythonのサンプルです。
Scikit-Learnには該当関数が2021年5月時点でありませんが、`category_encoders`には[Leave One Out](https://contrib.scikit-learn.org/category_encoders/leaveoneout.html)があります。データに`None`と`np.nan`があってもエラーは起きず、両者を同じラベル扱いします。


```python
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](https://contrib.scikit-learn.org/category_encoders/catboost.html)があります。データに`None`と`np.nan`があってもエラーは起きず、両者を同じラベル扱いします。

```python
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**というものもあります。私が書いたのは受け売りばかり。

https://blog.amedama.jp/entry/target-mean-encoding-types


## 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](https://contrib.scikit-learn.org/category_encoders/hashing.html)があります。パラメータ`n_components`で何ビットにマッピングするかを指定しています。対象のデータに`None`と`np.nan`があってもエラーは起きません。

```python
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もありました。

https://www.tensorflow.org/recommenders/examples/featurization#using_feature_hashing

## Entity Embedding
ニューラルネットワークを使った変換。これはリンクだけ載せておきます。
過去にNLPで`keras`が使った[`Embedding`関数](https://keras.io/ja/layers/embeddings/)と同じかと。非常に良い結果を出してくれました(意識していなかったですが、多分同じもの)。

https://pompom168.hatenablog.com/entry/2019/03/25/001548

https://qiita.com/kznovo/items/caa389dfd178174ad93a

# 参考リンク
以下のが非常に参考になります。


http://rio-cloud.hatenablog.com/entry/2018/05/02/175319

https://thefinance.jp/datascience/201109-2#:~:text=%E4%BB%8A%E5%9B%9E%E3%81%AF%E3%80%81%E4%BB%A3%E8%A1%A8%E7%9A%84%E3%81%AA,Target%20Encoding%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E8%A7%A3%E8%AA%AC%E3%81%99%E3%82%8B%E3%80%82



https://qiita.com/Hyperion13fleet/items/afa49a84bd5db65ffc31

https://contrib.scikit-learn.org/category_encoders/

https://github.com/scikit-learn-contrib/category_encoders