ラベルエンコーディングについて

## 紹介文
本記事はPython初心者かつ機械学習初心者による備忘録的内容である。
また、機械学習を勉強し始めた人を対象とした記事である。
マークダウン記法なども何もわからない状態からの記事投稿のため、
今後内容や記載方法なども精査していきたい。

## エンコーディング
変数は以下の二種類に分けることができる。
- 数値変数
- カテゴリー変数（文字変数）

さらにカテゴリー変数は以下の二種類に分けることができる。
- 順序変数（ordinal）
- 名義変数（nominal）

順序変数は並び順に意味のある文字、名義変数は並び順に特に意味はないもの。  
たとえば、評価S,A,B,Cにはその並び順に意味があるし、赤、青、緑などは並び順に特に意味はない。  

/*  
余談  
SAOの劇場版オーディナルスケールはこの順序変数のオーディナルのこと。  
オーディナルは数値自体が順番を表す数字であり、カーディナルは量を表す数値である。  
たとえば、1位、2位であればordinal numberだし、1人、2人であればcardinal numberになる。  
*/  

基本的に文字変数を機械学習に使用する場合、数値変数に変換する必要がある。  
まずは順序変数を数値変数へと変換してみよう。  

## オーディナルエンコーディング

```python
import pandas as pd
#順序変数の作成
df = pd.DataFrame([
    ["S", "grade1", "class1"],
    ["A", "grade1", "class1"],
    ["B", "grade1", "class2"],
    ["A", "grade2", "class2"],
    ["C", "grade2", "class3"],
    ["S", "grade2", "class3"],
])
#列名の定義
df.columns = ["evaluation", "grade", "class"]
df
```
|evaluation|grade|class|
|--:|--:|--:|
|S|grade1|class1|
|A|grade1|class1|
|B|grade1|class2|
|A|grade2|class2|
|C|grade2|class3|
|S|grade2|class3|

上記で作成したevaluationのカテゴリは、["S", "A", "B", "C"]の4つであり、左から順に成績が良いと考える。  
その場合、対応するカテゴリーの数値としては以下のようになる。  
["S", "A", "B", "C"] = [1, 2, 3, 4]  
この変換を以下で行う。  

```python
#各カテゴリー値に対して、対応する辞書を作成する。
eval_mapping = {"S":1, "A":2, "B":3, "C":4}
#map関数を用いて、evaluationに作成した辞書を割り当てる。
df["evaluation"] = df["evaluation"].map(eval_mapping)
df
```

|evaluation|grade|class|
|--:|--:|--:|
|1|grade1|class1|
|2|grade1|class1|
|3|grade1|class2|
|2|grade2|class2|
|4|grade2|class3|
|1|grade2|class3|

まず、各カテゴリー値に対してその順位を定義した辞書を作成する。  
続いて、該当のカテゴリーに対してmap関数を用いて辞書を割り当て、文字変数を数値変数へと変換する。  
ちなみに、df["evaluation"].dtypesとエンコーディング前後に実行すれば、objectからintへと変換されていることがわかるだろう。  
これによって文字値であったカテゴリーを数値へと変換することができた。  

また、数値変数にしたカテゴリー値を文字値へと戻したい場合は以下のように再度エンコーディングを行えば良い。

```python
#先ほどとは逆の辞書を作成する。
inv_eval_mapping = {i:c for c, i in eval_mapping.items()}
df["evaluation"].map(inv_eval_mapping)
```

| |
|--:|
|S|
|A|
|B|
|A|
|C|
|S|

はじめに作った辞書に対してitems関数を用いることにより、先ほどとは逆の辞書を作成することが可能になる。  
ただし、items関数は辞書のkeyを先に返し、次にvalueを返すため、iとcの順番に気を付ける必要がある。

続いて名義変数についてエンコーディングを行っていこう。  
先ほどの順序変数については、各カテゴリに割り当てる数値に意味があった（数値によってその順位がきまるため）。  
ただし、名義変数について割り当てられる数値は重要ではない（ただし、本当に重要ではないかはこのあと議論する）。  
["class1", "class2", "class3"] = [1, 2, 3]であっても、  
["class1", "class2", "class3"] = [3, 2, 1]であってもなんでも良い。  
ただし、評価によって割り付けを行なっていないことが前提である。  
もし仮に評価によってクラスを割り付けている場合、"class"は名義変数ではなく、順序変数となるため、  
その変数が順序変数なのか名義変数なのかは意味を考慮した上で判断が必要である。  

今回は評価順に割り付けを行なっていないことを前提として、以下の通りエンコーディングを行う。  

## ラベルエンコーディング

```python
import numpy as np
#各カテゴリに対して、辞書を作成する。
class_mapping = {label:i for i, label in enumerate(df["class"].unique())}
#map関数で作成した辞書をdf["class"]に割り当てる。
df["class"] = df["class"].map(class_mapping)
df
```

|evaluation|grade|class|
|--:|--:|--:|
|1|grade1|0|
|2|grade1|0|
|3|grade1|1|
|2|grade2|1|
|4|grade2|2|
|1|grade2|2|

```python
#先ほどとは逆の辞書を作成する。
inv_class_mapping = {i:c for c, i in class_mapping.items()}
df["class"] = df["class"].map(inv_class_mapping)
df
```

今回は名義変数のため、対応させる数値はなんでも良いのでunique関数およびenumerate関数を使用して辞書を作成した。  
unique関数を用いることで、その列のカテゴリー値を重複削除した上で抽出が可能である。  
unique関数でカテゴリー値を抽出したのち、enumerate関数を使用して、カテゴリー値およびそのindexを辞書へと格納する。  
この場合もlabelおよびiの順番に気をつけよう。  
対応する辞書を作成すれば、あとは順序変数と同様にmap関数を用いて数値変数へと変換してあげれば良い。  
数値変数を文字変数へと戻す操作も、順序変数と同じように可能である。  
また、名義変数のラベルエンコーディングはsci-kit learnに用意されているLabelEncoderを用いても以下の通り行うこともできる。  

```python
from sklearn.preprocessing import LabelEncoder
#LabelEncoderのインスタンスを作成
class_le = LabelEncoder()
#classラベルを数値に変換
class_n = class_le.fit_transform(df["class"].values)
class_n
```

array([0,0,1,1,2,2])

逆変換は以下の通り、inverse_transform関数を用いて処理が可能である。

```python
class_c = class_le.inverse_transform(class_n)
class_c
```

ここまでで順序変数および名義変数を数値変数へと変換することが可能になった。  
ここで今一度、名義変数に割り当てる数値の意味について考えよう。  

たとえば今、classの分類を行うとしよう。  
classを分類したいのであれば、classの数値はなんであれ問題ない。  
ただし、classが目的変数ではなく、分類するために使用する説明変数だった場合はどうだろうか。  
その場合、class2はclass1よりも大きく、class3はclass2よりも大きいとして扱われてしまう。  
そのため、ラベルエンコーディングは説明変数に対しては使用してはならない。  
ちなみにsci-kit learnのLabelEncoderの公式ドキュメントにも記載がある。  
https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html  
一部抜粋  
>「This transformer should be used to encode target values, i.e. y, and not the input X.」  

「terget values」が目的変数であり、「input X」が説明変数である。  

しかし、classを説明変数として使用する場合であっても、文字値のままにはできないためどうにかして数値にエンコーディングしなければならない。  
そこで登場するのが「one-hotエンコーディング」という手法である。  
まずはone-hotエンコーディングを以下のように行なってみよう。  

## One-Hotエンコーディング

```python
import pandas as pd
#再度DateFrameの作成
df = pd.DataFrame([
    ["S", "grade1", "class1"],
    ["A", "grade1", "class1"],
    ["B", "grade1", "class2"],
    ["A", "grade2", "class2"],
    ["C", "grade2", "class3"],
    ["S", "grade2", "class3"],
])
df.columns = ["evaluation", "grade", "class"]

#get_dummies関数を用いてOneHotEncoding
pd.get_dummies(df[["grade","class"]], dtype=int)
```

|grade_grade1|grade_grade2|class_class1|class_class2|class_class3|
|--:|--:|--:|--:|--:|
|1|0|1|0|0|
|1|0|1|0|0|
|1|0|0|1|0|
|0|1|0|1|0|
|0|1|0|0|1|
|0|1|0|0|1|

上記の通り、get_dummies関数を用いることで、複数の名義変数について同時にone-hotエンコーディングが可能である。  
今まで名義変数にラベルエンコーディングを行った場合、class1は0、class2は1、class3は2のように、  
それぞれ順番に数字が割り振られていった。  
one-hotエンコーディングではどのようになっているだろうか。  
たとえば、class1に該当する列の場合、class_class1が1になり、それ以外が0となっている。  
class2に該当する場合はclass_class2が1になり、それ以外が0、class3についても同様である。  
（ただし、get_dummies関数のオプションで「dtype=int」としているため0または1で出力されるが、指定しないとbool型で出力される。）  
このように、one-hotエンコーディングは名前の通り該当する1つの変数のみが1（hot）になるようにエンコーディングを行うのである。  

ただし、このままでは多重共線性により回帰分析の逆行列の計算に支障をきたしてしまう。  
また、決定木系の分類に関しても、分類結果に対しては影響を与えないが、特長量の重要度の算出に支障をきたしてしまう。  
多重共線性についてはまた別の記事で解析できたらと思う。  
ここではこの多重共線性を解消する手法のみを記載したい。  
その方法は、get_dummies関数に「drop_first=True」のオプションを加えれば良い。  

```python
#get_dummies関数を用いてOneHotEncoding
pd.get_dummies(df[["grade","class"]],drop_first=True, dtype=int)
```

|grade_grade2|class_class2|class_class3|
|--:|--:|--:|
|0|0|0|
|0|0|0|
|0|1|0|
|1|1|0|
|1|0|1|
|1|0|1|

これにより、カテゴリ内の初めの値については全てが0の場合に特定が可能になるため、  
初めのカテゴリに関する変数は作成不要となり、多重共線性を解消することが可能である。  
get_dummies関数を用いることで容易にone-hotエンコーディングが可能になるが、get_dummies関数の場合、  
メモリを多く消費するため大規模データに適用しようとすると処理ができない可能性があるようです。  
（ただし、get_dummies関数では欠測値などを一つのカテゴリ値としてエンコーディングしてくれるオプションなども存在しており、一長一短である）  
以下ではsci-kit learnに用意されているOneHotEncoderを用いてエンコーディングを行ってみる。  

```python
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
#OneHotEncoderのインスタンスを作成する。
class_ohe = OneHotEncoder(categories="auto", drop="first")
#複数の変数を同時にエンコーディングする場合、ColumnTransformerを用いて、指定した列ごとにエンコーディングを行う必要がある。
transf = ColumnTransformer([("onehot", class_ohe, [1,2]),
                            ("nothing", "passthrough", [0])])
transf.fit_transform(df)
```

array([[0.0, 0.0, 0.0, 'S'],
       [0.0, 0.0, 0.0, 'A'],
       [0.0, 1.0, 0.0, 'B'],
       [1.0, 1.0, 0.0, 'A'],
       [1.0, 0.0, 1.0, 'C'],
       [1.0, 0.0, 1.0, 'S']], dtype=object)

上記の通り、OneHotEncoderを用いることで、get_dummies関数を用いた時と同じ配列を作成することができた。  
ただし、OneHotEncoderではDataFrameではなくndarrayで帰ってくるため、DataFrameへと変換した上で、元の配列と結合する必要がある。  

以上でカテゴリーに対して、数値変数へと置き換えるエンコーディングの手法を解説した。  
エンコーディングの手法には他に  
- カウントエンコーディング
- ターゲットエンコーディング
など様々な手法があり、今後まとめた記事を作成できればと思う。  

また、今回の内容では欠測値を持ったカテゴリー変数に対してエンコーディングを行う方法や、多重共線性についての解説もできていない。  
これについても今後まとめることができたらと思う。  

## まとめ
以上、今回解説した内容をまとめると、  
1. カテゴリー変数には順序変数と名義変数がある。  
2. 順序変数をエンコーディングする際には、オーディナルエンコーディングを行う必要がある。  
3. 名義変数をエンコーディングする際は、ラベルエンコーディングを行うことができるが、それは目的変数である場合のみである。  
4. 名義変数を説明変数として扱う場合は、One-Hotエンコーディングを用いることができるが、多重共線性を回避する必要がある。