ysaz (ImanazaS) blog

最近はデータ分析や機械学習が中心。たまに音楽や書評。

カテゴリカル変数のEncoding手法について

Structured Data(構造化データ)の下処理をおこなう際に避けて通れないのがFeature Engineering(特徴量エンジニアリング)。
特に悩ましいのがカテゴリ変数の扱いで、どのように扱えば良いか困ることが多く、また、使った手法もすぐに忘れてしまいがちなので、自分なりに整理して記事にまとめておきたいというのが趣旨。

1.よく使われる手法

まずはよく用いられる定番の手法から。次元を増やすかどうかで大まかに次の2つに分類できる。

・次元を増やさない場合(Label, Count, LabelCount, Target)
・次元を増やす場合(One-Hot, Entity Embedding)

これらの手法について、python上の実装例とともに見ていきたい。
用いたライブラリは主にPandas、少しScikit-Learn。

2.次元を増やさない場合

Label Encoding

最もシンプルな手法で、与えられたカテゴリ変数に数字を割り当てるもの。
たとえば、「東京」、「大阪」、「名古屋」というカテゴリに対して、
0、1、2といったようにラベルをふって単純に数値化するだけ。

以下、実行例。
Method 1はPandasを使ったもので、Method 2はScikit-Learnを使ったもの。

import pandas as pd
import numpy as np

d = {'city': ['tokyo', 'nagoya', 'osaka', 'tokyo', 'nagoya', 'osaka', 'tokyo', 'osaka', 'tokyo'],
	'target': [0, 1, 0, 1, 0, 1, 0, 1, 0]}

df = pd.DataFrame(d)
df['city'] = df['city'].astype('category')
df.dtypes

# method 1
df['label_enc'] = df['city'].cat.codes

# method 2
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['label_enc'] = le.fit_transform(df['city'])
Count Encoding

これも比較的シンプルな手法で、データに含まれるカテゴリ変数の出現回数を数えるもの。

以下、Pandasのgroupbyを使った実行例を2つ(Method 1, 2)。

# method 1
count_mean = df.groupby('city').target.count()
df['count_enc'] = df['city'].map(count_mean)

# method 2
df['count_enc'] = df.groupby('city')['target'].transform('count')
LabelCount (Count Rank) Encoding

次は上述したCount Encodingの応用編で、カテゴリ変数の出現回数が多い順に順位づけするもの。

以下、Pandasを使った実行例。

count_rank = df.groupby('city')['target'].count().rank(ascending=False)
df['count_rank'] = df['city'].map(count_rank)
Target Encoding

Mean EncodingやLikelihood Encodingとも呼ばれる手法で、目的変数(Target)に対してカテゴリ変数の処理をおこなう。
Kaggle等の機械学習コンペで頻繁に用いられている。

やり方としては、目的変数が2値のみをとるブーリアン型であればカテゴリ毎の確率を、目的変数が数値であればカテゴリ毎の平均を取るというもの。

ただし、データセットが学習データとテストデータに分かれている場合、テストデータの目的変数は未知であるため、演算は学習データに対しておこない、得られた特徴量をテストデータ内のカテゴリへ適用する。

この部分に注意しないと、Cross Validation をおこなったときにデータがリークすることになってしまう。

以下、Pandasのgroupbyを使った実行例を2つ(Method 1, 2)。

# method 1
target_mean = df.groupby('city').target.mean()
df['target_enc'] = df['city'].map(target_mean)

# method 2
df['target_enc'] = df.groupby('city')['target'].transform('mean')

3.次元を増やす場合

One hot encoding

データ中に存在するカテゴリ変数の数だけ次元(構造化データでいうと、新しい列)を用意して、各行に含まれているカテゴリ変数に対応する次元を1に、それ以外を0にする方法。
これにより、カテゴリ変数を単に連続する数としてではなく、独立する値として扱うことが可能となる。

以下、実行例。
Method 1はPandasを使ったもので、Method 2はScikit-Learnを使ったもの。
ただしMethod 2ではカラム名が自動で返ってこないため、何らかの形で指定してあげる必要がある。

# method 1
oh_enc = pd.get_dummies(df['city'])
df = pd.concat([df, oh_enc], axis=1)

# method 2
from sklearn.preprocessing import OneHotEncoder
oh_enc = OneHotEncoder(sparse=False)
df[['nagoya', 'osaka', 'tokyo']] = oh_enc.fit_transform(df.label_enc.reshape(len(df.label_enc), 1))


ここまでの実行結果は下表の通り。

city target label_end count_enc count_rank target_enc nagoya osaka tokyo
tokyo 0 2 4 1.0 0.250000 0.0 0.0 1.0
nagoya 1 0 2 3.0 0.500000 1.0 0.0 0.0
osaka 0 1 3 2.0 0.666667 0.0 1.0 0.0
tokyo 1 2 4 1.0 0.250000 0.0 0.0 1.0
nagoya 0 0 2 3.0 0.500000 1.0 0.0 0.0
Entity Embedding

KaggleのRossmannコンペで有名になった手法。
これまで挙げてきたものとはややアプローチが異なるが、ニューラルネットワークでカテゴリ変数に対応する重み行列を学習させ、その重み行列とOHEの積をとったもの(と理解している)。
最終的には数値データと合わせてニューラルネットワークの全結合層に放り込むこととなる。

KaggleのPorto Seguroコンペデータを使った実験を行い、以下Githubに公開したので、参考までに。

github.com

次のステップとして、抽出した重み行列を特徴量としてGradient Boosting Decision Tree (GBDT) にうまくまぜられないかと思案中。

4.参考記事

参考にしたのは以下のblog記事。とてもうまくまとめられており、インスパイアされるきっかけとなった。

jotkn.ciao.jp

あとは、英語だが以下の記事もEncodingについて体系的に書かれており、非常に役に立った。

pbpython.com

Entity Embeddingについてはこの記事を参照。

puyokw.hatenablog.com

最後に、最近発売されたこの本。大全というだけあって網羅的に整理されており、辞書的な使い方ができるのがよい。