ターゲットエンコーディング

# ターゲットエンコーディングとは
OneHotエンコーディングやラベルエンコーディングと同じ質的変数を数値に変換する手法の1つです。ラベルエンコーディングの強化版のようなイメージで良いのかもしれません。

ラベルエンコーディングはある質的変数を適当な数値で変換します。しかしこの適当な数値に意味はありませんので、この数値はどのような値でもよいのです。そこで**質的変数を目的変数の平均値で置き換える**ことで変換後の数値を目的変数と対応させ学習をうまくさせようというのがターゲットエンコーディングです。正確には平均値では無く中央値や標準偏差で置き換えてもよいみたいです。

なおこの記事は「[KaggleGrandmasterに学ぶ機械学習実践アプローチ](https://www.amazon.co.jp/Kaggle-Grandmaster%E3%81%AB%E5%AD%A6%E3%81%B6-%E5%AE%9F%E8%B7%B5%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81-Abhishek-Thakur/dp/4839974985)」を勉強して自分なりに解説をして理解を深める目的で書かれているので、コードはほぼそこと同じものになります。


#実装してみる
ターゲットエンコーディングを行う関数を実装してみます。
データセットは米国成人国税調査の結果で一般に公開されています。交差検証はk-fold交差検証を行っています。

```python
import copy

def target_encoding(data):
  #データセットのコピー
  df = copy.deepcopy(data)
```

``df``にデータセットをコピーします。

```python
  #数値の列
  num_cols = [
        'age',
        'fnlwgt',
        'capital.gain',
        'capital.loss',
        'hours.per.week'
  ]
```

データセットの内質的変数以外の数値のデータの列をリストにまとめます。

```python
  #目的変数を0と1に置換
  target_mapping = {
      " >50K":0,
      " <=50K":1
  }
  df.loc[:,"income"] = df.income.map(target_mapping)
```

目的変数を0と1でラベルエンコーディングします。



```python
  #目的変数とfold番号の列、数値を含む列を除き特徴量とする
  features = [
      f for f in df.columns if f not in ("kfold","income") and f not in num_cols
  ]
```

質的変数の列の名前だけを取り出し、ターゲットエンコーディングを行うための特徴量とします。

```python
  #すべての欠損値を"NONE"で補完
  #合わせて全ての列を文字型に変換
  for col in features:
    df.loc[:,col] = df[col].astype(str).fillna("NONE")

    encoder = preprocessing.LabelEncoder()

    encoder.fit(df[col])

    df.loc[:,col] = encoder.transform(df[col])
```

通常のラベルエンコーディングを行います。ここで欠損値を文字列``"NONE"``で補完しています。

```python
  #検証用データセットを格納するリスト
  encoded_dfs = []

  #全ての分割についてのループ
  for fold in range(5):
    #学習用と検証用データセット
    #引数と一致するデータを学習に使用 
    df_train = df[df.kfold != fold].reset_index(drop=True)
    #引数と一致するデータを検証に使用
    df_valid = df[df.kfold == fold].reset_index(drop=True)
```
fold毎にターゲットエンコーディングを施します。もし全てのデータに一様にターゲットエンコーディングを施すと、訓練データとテストデータで同じターゲットエンコーディングされた値を使うことになるので、実際よりも精度が高く出てしまうリークがおこります。

```python
    #全ての特徴量についてのループ
    for column in features:
      #カテゴリ毎の目的変数の平均についての辞書を作成
      mapping_dict = dict(
          df_train.groupby(column).mean()["income"] #series型をdictに渡して辞書型に変換
      )
      #元の列名の末尾に"enc"を加えた名前で新しい列を作成
      df_valid.loc[
                 :,column+"enc",
      ]=df_valid[column].map(mapping_dict)
    #リストに格納
    encoded_dfs.append(df_valid)
```
訓練データ``df_train``の目的変数である``income``から目的変数のカテゴリ毎の平均を算出し、``mapping_dict``に辞書型として格納します。
これを用いて、テストデータの質的変数をターゲットエンコーディングします。テストデータの目的変数の値を含めずに平均値を算出することでリークを防ぐ役割があります。

```python
  #結合したデータセットを返す
  encoded_df = pd.concat(encoded_dfs,axis=0)

  return encoded_df
```

最後にデータセットを結合して戻り値とします。

#コード全体

```python
import copy
import pandas as pd
from sklearn import preprocessing

def mean_target_encoding(data):
  #データセットのコピー
  df = copy.deepcopy(data)
  #数値の列
  num_cols = [
        'age',
        'fnlwgt',
        'capital.gain',
        'capital.loss',
        'hours.per.week'
  ]
  #目的変数を0と1に置換
  target_mapping = {
      " >50K":0,
      " <=50K":1
  }
  df.loc[:,"income"] = df.income.map(target_mapping)

  #目的変数とfold番号の列を除き特徴量とする
  features = [
      f for f in df.columns if f not in ("kfold","income") and f not in num_cols
  ]
  

  #すべての欠損値を"NONE"で補完
  #合わせて全ての列を文字型に変換
  #すべて質的変数になっているので問題ない
  #引数のfoldと一致しないデータを学習に使用
  for col in features:
    df.loc[:,col] = df[col].astype(str).fillna("NONE")

    encoder = preprocessing.LabelEncoder()

    encoder.fit(df[col])

    df.loc[:,col] = encoder.transform(df[col])

  #検証用データセットを格納するリスト
  encoded_dfs = []

  #全ての分割についてのループ
  for fold in range(5):
    #学習用と検証用データセット
    #引数と一致するデータを学習に使用 
    df_train = df[df.kfold != fold].reset_index(drop=True)

    #引数と一致するデータを検証に使用
    df_valid = df[df.kfold == fold].reset_index(drop=True)

    #全ての特徴量についてのループ
    for column in features:
      #カテゴリ毎の目的変数の平均についての辞書を作成
      mapping_dict = dict(
          df_train.groupby(column).mean()["income"] #series型をdictに渡して辞書型に変換
      )
      print(df_train.groupby(column).mean(),type(df_train.groupby(column)["income"].mean()))
      print(mapping_dict)
      #元の列名の末尾に"enc"を加えた名前で新しい列を作成
      df_valid.loc[
                 :,column+"enc",
      ]=df_valid[column].map(mapping_dict)

    #リストに格納
    encoded_dfs.append(df_valid)
  #結合したデータセットを返す
  encoded_df = pd.concat(encoded_dfs,axis=0)

  return encoded_df
```