【Python】scikit-learnのSVMで文字列データを使用する

# はじめに
前記事の「[【R】"Need numeric dependent variable for regression."の原因と対応](https://qiita.com/yaju/items/6ac66b6a246e4f16c367)」でR言語でサポートベクターマシンを使用した分類問題が動作させることが出来た。
今度はこれを R言語からPythonに移植しようとした際に、R言語では文字列データ(正確には因子型)で出来たが、Pythonでは数値型に変換しないと下記のエラーになってしまう。

```python
model.fit(sze.iloc[train, 1:], sze.iloc[train, 0]) 

ValueError: could not convert string to float: 'G'
```

それだと不便だし、自分が知らないだけで本当はPythonでも出来るのではないかと疑い調べました。

# 環境
* Google Colaboratory
* Python 3.6.9
* sklearn 0.22.2

# 調査
「scikit-learn SVM 文字列」で検索してもズバリの回答が得られない、日本語の検索は無理と判断して英語に変更し「scikit-learn SVM String」や「scikit-learn SVM Non-Integer」で検索して下記サイトを見つけた。
[Non-Integer Class Labels Scikit-Learn - stackoverflow](https://stackoverflow.com/questions/13300160/non-integer-class-labels-scikit-learn)

これを実行してもエラーにはならない。

```python
from sklearn.svm import SVC
clf = SVC()
x = [[1,2,3], [4,5,6]]
y = ['dog', 'cat']
clf.fit(x,y)

yhat = clf.predict([[1,2,5]])
print yhat[0]
```

それではと使用したいのはジャンケンデータなのでGとCとPの文字列を使用して、学習データと正解ラベルに文字列を用いてみた。しかし、こうすると同じエラーが出てしまう。

```python
from sklearn.svm import SVC
clf = SVC()
x = [['G',2,3],['C',5,6]]
y = ['G', 'C']
clf.fit(x,y)

yhat = clf.predict([['P',2,5]])
print(yhat[0])

ValueError: could not convert string to float: 'G'
```

サンプル同様に正解ラベルのみ文字列にした場合、エラーは出なかった。

```python
from sklearn.svm import SVC
clf = SVC()
x = [['1',2,3],['4',5,6]]
y = ['G', 'C']
clf.fit(x,y)

yhat = clf.predict([['1',2,5]])
print(yhat[0])
```

>SVM(サポートベクターマシン)は線形二値分類のアルゴリズムなので、学習データにURLのような文字データが入ってきても分類できないと思われます。
[機械学習ValueErrorでURL文字列を読み込めない。
](https://teratail.com/questions/213091)

学習データに関しては、文字列ではなく数値型を使用する必要がある。

# 文字列型から数値型にする
文字列型から数値型にするには、カテゴリーエンコーディングを使用する。
カテゴリーエンコーディングには、ラベルエンコーディングやOne-Hotエンコーディングなど多くの種類がある。
機械学習のための前処理([scikit-learn.preprocessing](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.preprocessing))として提供されている

## LabelEncoder
```python
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
le.fit(['C', 'P', 'G', 'C', 'C'])

print(le.classes_)
print(le.transform(['G', 'C', 'P', 'C'])) 
print(le.inverse_transform([1, 0, 2, 0]))

# 結果
['C' 'G' 'P']
[1 0 2 0]
['G' 'C' 'P' 'C']
```

`le.fit()` の部分で変換したいデータを選択します。
`le.classes_`では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
`le.ｔransform（）` で数値へ変換します。
`le.inverse_transform（）`で文字列データに戻せます。

## OneHotEncoder
[One-hotベクトル（ワンホット表現）の意味とメリット・デメリット](https://mathwords.net/onehot)

`LabelEncoder`のように1次元配列で渡すと、次のエラー「'Expected 2D array, got 1D array instead'」になるので2次元配列で渡します。1次元配列で変換したい場合、後述のラベルバイナライザーを使用します。
pandasのDataFrameを使用する上では2次元配列の方が都合がいい。

```python
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer

ohe = OneHotEncoder()
ohe.fit([['C'], ['P'], ['G'], ['C'], ['C']])
ct = ColumnTransformer([("category", ohe, [0])], remainder="passthrough")

print(ohe.categories_)
print(ct.fit_transform([['G'], ['C'], ['P'], ['C']])) 
print(ohe.inverse_transform([[0,1,0],[1,0,0],[0,0,1],[1,0,0]]))

# 結果
[array(['C', 'G', 'P'], dtype=object)]
[[0. 1. 0.]
 [1. 0. 0.]
 [0. 0. 1.]
 [1. 0. 0.]]
[['G']
 ['C']
 ['P']
 ['C']]
```

`ohe.fit()` の部分で変換したいデータを選択します。
`ohe.classes_`では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
`ct.fit_ｔransform（）` で数値へ変換します。
`ohe.inverse_transform（）`で文字列データに戻せます。

ColumnTransformerを使うと複数の列の変換も可能、引数の意味合いなどは下記サイトを参考

* [ColumnTransformerの使い方](https://network-beginner.xyz/onehotencoder_categorical_features_typeerror)
* [カテゴリー機能に関するOneHotEncoderの問題](https://www.it-swarm-ja.tech/ja/scikit-learn/%E3%82%AB%E3%83%86%E3%82%B4%E3%83%AA%E3%83%BC%E6%A9%9F%E8%83%BD%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8Bonehotencoder%E3%81%AE%E5%95%8F%E9%A1%8C/830787175/)

OneHotEncoderのサンプルでみる'categorical_features'キーワードですが、バージョン0.20で非推奨、0.22で削除となり、代わりに'ColumnTransformer'を使用するように提示されます。
[scikit-learnのColumnTransformerを使ってみる - 静かなる名辞](https://www.haya-programming.com/entry/2019/06/29/194801)

pandasには'get_dummies'を使用してOne-Hotエンコーディングができます。用途に合わせて使いましょう。
[pandasのget_dummiesを使う - 静かなる名辞](https://www.haya-programming.com/entry/2018/12/02/042049)

本来の使い方としては、下記のように複数の組み合わせでOne-Hot表現したい場合となります。
[OneHotEncoder - Taustation](http://taustation.com/sklearn-preprocessing-onehotencoder/)

```python
from sklearn.preprocessing import OneHotEncoder

X = [
    ['Tokyo', 'Male'],
    ['Tokyo', 'Female'],
    ['Osaka', 'Male'],
    ['Kyoto', 'Female'],
    ['Osaka', 'Female'],
    ['Osaka', 'Male']
]

ohe = OneHotEncoder(sparse=False)
ohe.fit(X)

print(ohe.categories_)
print(ohe.transform(X)) 

# 結果
[array(['Kyoto', 'Osaka', 'Tokyo'], dtype=object), array(['Female', 'Male'], dtype=object)]
[[0. 0. 1. 0. 1.]
 [0. 0. 1. 1. 0.]
 [0. 1. 0. 0. 1.]
 [1. 0. 0. 1. 0.]
 [0. 1. 0. 1. 0.]
 [0. 1. 0. 0. 1.]]
```

## LabelBinarizer
One-Hotエンコーディングでは2次元配列で渡していましたが、ラベルバイナライザーは1次元配列でOne-Hot表現に変換できます。

```python
import numpy as np
from sklearn.preprocessing import LabelBinarizer

lｂ = LabelBinarizer()
lｂ.fit(['C', 'P', 'G', 'C', 'C'])

print(lｂ.classes_)
print(lｂ.transform(['G', 'C', 'P', 'C'])) 
print(lｂ.inverse_transform(np.array([[0,1,0],[1,0,0],[0,0,1],[1,0,0]])))

# 結果
['C' 'G' 'P']
[[0 1 0]
 [1 0 0]
 [0 0 1]
 [1 0 0]]
['G' 'C' 'P' 'C']
```

`lｂ.fit()` の部分で変換したいデータを選択します。
`lｂ.classes_`では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
`lｂ.ｔransform（）` で数値へ変換します。
`lｂ.inverse_transform（）`で文字列データに戻せます。※np.arrayが必要

[Scikit-learn's LabelBinarizer vs. OneHotEncoder - stackoverflow](https://stackoverflow.com/questions/50473381/scikit-learns-labelbinarizer-vs-onehotencoder)

## OrdinalEncoder
`LabelEncoder`と同じようにアルファベットの昇順に数値が割り振られます。違いとしてはOneHotEncoderと同様に2次元配列で渡します。
pandasのDataFrameを使用する上では2次元配列の方が都合がいい。

```python
from sklearn.preprocessing import OrdinalEncoder

oe = OrdinalEncoder()
oe.fit([['C'], ['P'], ['G'], ['C'], ['C']])

print(oe.categories_)
print(oe.fit_transform([['G'], ['C'], ['P'], ['C']])) 
print(oe.inverse_transform([[1],[0],[2],[0]]))

# 結果
[array(['C', 'G', 'P'], dtype=object)]
[[1.]
 [0.]
 [2.]
 [0.]]
[['G']
 ['C']
 ['P']
 ['C']]
```

`oe.fit()` の部分で変換したいデータを選択します。
`oe.classes_`では割り振りの値を確認できます。アルファベットの昇順に数値が割り振られています。
`oe.fit_ｔransform（）` で数値へ変換します。
`oe.inverse_transform（）`で文字列データに戻せます。

`OrdinalEncoder`は複数の組み合わせで使用できるので、`LabelEncoder`が正解ラベル側として使用するのに対し、学習データ側は`OrdinalEncoder`を使用するイメージです。

```python
from sklearn.preprocessing import OrdinalEncoder

X = [
    ['Tokyo', 'Male'],
    ['Tokyo', 'Female'],
    ['Osaka', 'Male'],
    ['Kyoto', 'Female'],
    ['Osaka', 'Female'],
    ['Osaka', 'Male']
]
 
oe = OrdinalEncoder()
oe.fit(X)

print(oe.categories_)
print(oe.transform(X)) 

# 結果
[array(['Kyoto', 'Osaka', 'Tokyo'], dtype=object), array(['Female', 'Male'], dtype=object)]
[[2. 1.]
 [2. 0.]
 [1. 1.]
 [0. 0.]
 [1. 0.]
 [1. 1.]]
```

## CustomerRating
サザエさんのジャンケンでは、統計情報からG>C>Pの強さになっているので、`LabelEncoder`や`OrdinalEncoder`のようにアルファベットの昇順に数値が割り振られると、結果に違いが出てしまう。CをGとPの間にある別のアルファベットに変更すれば、`LabelEncoder`や`OrdinalEncoder`でもそのまま使用できるが、記号を維持したまま数値を割り当てたい。

mapを使用して割り当てたい数値に変換している。

```python
import pandas as pd

rating = {'G' : 1, 'C' : 2, 'P' : 3}
df = pd.DataFrame(['C', 'P', 'G', 'C', 'C'])
print(df.applymap(lambda x : rating[x]))

# 結果
0  2
1  3
2  1
3  2
4  2
```

* [Label Encoding in Python Explained](https://www.mygreatlearning.com/blog/label-encoding-in-python/#ordinalencodingpython)
* [Encoding Ordinal Values in Python - stackoverflow](https://stackoverflow.com/questions/54440507/encoding-ordinal-values-in-python)

# 最後に
同じ列に異なる数値があるとモデルのデータによってはある種の順序（0 < 1 < 2）であると誤解する。この問題を克服するために、One Hot表現を使用する。逆にある種の順序（G > C > P）の優先度を考慮するならば、同じ列に異なる数値を用いてもいい。

One Hot表現を使用する意味がやっと分かった気がする。
入力（説明変数、独立変数）と出力（目的変数、従属変数）があって、説明変数側は回帰でも分類でも基本的 One Hot表現を使用すればいいわけだ、目的変数側は文字列であっても問題ないよってことだね。

[【scikit-learn】Label EncoderとOne Hot Encoderの使い分けについて - teratail](https://teratail.com/questions/216326)