前回はfeaturetoolsを使って、簡単に特徴量の自動生成をする方法を記事にしたが、記事冒頭に記載しているCategoryEncodersの方が地味によく使っている。
いや、なんだったら本当はこっちのCategoryEndodersの方が特徴量生成をする上では重要なんじゃないかとすら思っている。
#これが該当する人は読むといいかも?
- 文字列がデータに入っているとモデルがエラーになるからいつも除外している
- 除外はしてないけど、数値にEncodeするのが毎回だるい、死にたい
-
sklearn.preprocessing
よ、なんでお前pd.DataFrameで投げたのにnp.arrayで返してくんねん!!! - いや、待てと。そもそもなんでOneHotEncoderってクラス名のくせに、ダイレクトに文字からOnehotに展開してくれへんねん!!そこは気ぃ効かせてくださいよ!
- いや、待て待て。文字列を含むカテゴリ変数は普通、1つだけでは収まらんねん。いっぱいあんねん。それ1つづつ展開させるつもりっすか?マジっすか?
- 商品名をカテゴリ変数として持たせたいんです。5000品番はあるんです。でも私のメモリは1GBしかないんです。OneHotに展開したら即メモリがOverflowしちゃうんです。
- えぇぇ!!
pd.get_dummies
ってあんまり使わない方がいいんです??
#Category Encodersとは?
公式リファレンスによれば、
A set of scikit-learn-style transformers for encoding categorical variables into numeric with different techniques. Currently implemented are:
Ordinal
One-Hot
Binary
Helmert Contrast
Sum Contrast
Polynomial Contrast
Backward Difference Contrast
Hashing
BaseN
LeaveOneOut
Target Encoding
- カテゴリ特徴量をいくつかの変換方法でNumeric型の特徴量に変換するモジュール
- scikit-learnの変換スタイルで使える(fit,transformみたいなやつやな)
とのこと。比較的最近公開されたモジュールらしく、リファレンスが書きかけだったりソースコードにちょいちょい誤字があったりするけど私はそんなこと気にしない。
ちなみに同じような機能は、scikit-learnのpreprocessingでも提供されている。
このpreprocessingの機能については別の記事でも紹介されているのでここでは特に触れない。
【翻訳】scikit-learn 0.18 User Guide 4.3. データ前処理
機械学習のための前処理 (scikit-learn Preprocessing)
わざわざCategory Encoders使う必要ありますかねぇ?
わざわざ記事に書くぐらいなんで使う必要あるという結論に至るわけですが、まぁ聞いてください。。。
具体的にsklearn.preprocessing
とこのcategory_encoders
の違いを示せばきっとこっちを使いたくなるでしょう。
ワタシ、今までモデリング用のデータ作る時いつもpreprocessing使ってたデスヨ。
でも、もう使ってないデスヨ。。あまりにcategory_encordersが良い子だから。。。
具体的にサンプルデータとしてfeaturetoolsにあるデモデータを流用しつつ、どう違うのか見ていく。
こんな感じのデータ↓
import featuretools as ft
# load data
data = ft.demo.load_mock_customer()
df_session = data['sessions']
df_session.head()
head結果
Index | session_id | customer_id | device | session_start |
---|---|---|---|---|
0 | 1 | 1 | desktop | 2014-01-01 00:00:00 |
1 | 2 | 1 | desktop | 2014-01-01 00:17:20 |
2 | 3 | 5 | mobile | 2014-01-01 00:28:10 |
3 | 4 | 3 | mobile | 2014-01-01 00:43:20 |
4 | 5 | 2 | tablet | 2014-01-01 01:10:25 |
このdeviceのカラムをモデルに突っ込むために文字列をNumeric型に変える必要があるケースを考える。
##カテゴリ特徴量をOneHotに展開する
device列を存在する文字のユニーク数と同じだけ横持ちに展開するOneHotEncoding(0 or 1のバイナリ型)をやってみる。
###scikit-learnなら
from sklearn import preprocessing as pp
target_col = 'device'
ohe = pp.OneHotEncoder()
ohe.fit(df_session[target_col])
ohe.transform(df_session[target_col])
>ValueError: could not convert string to float: 'desktop'
ぎょえぇ。。OneHotEncoderはdevice列を事前に数値に変換しとかないと変換できないぃ!!
しかも、指定した列だけしかできないぃ。
ちゃんとやろうとすると
LabelBinarizerを使うのが手っ取り早い。
# LabelBinarizerを使う
target_col = 'device'
lb = pp.LabelBinarizer()
arr_device_onehot = lb.fit_transform(df_session[target_col])
arr_device_onehot
>array([[1, 0, 0],
> [1, 0, 0],
> ...
> [1, 0, 0]])
出力結果を見ても分かるけど、np.arrayで返してくる。いや、うん。まぁ、えぇけど。。
つまり、OneHotEncodeした結果をpd.Dataframeにしたければこうする必要がある。
# pd.DataFrameに変換して元のpd.DataFrameにくっつける
df_device = pd.DataFrame(arr_device_onehot,columns=lb.classes_)
df_session_onehot = pd.concat([df_session,df_device],axis=1)
df_session_onehot.head()
Index | session_id | customer_id | device | session_start | desktop | mobile | tablet |
---|---|---|---|---|---|---|---|
0 | 1 | 2 | desktop | 2014-01-01 00:00:00 | 1 | 0 | 0 |
1 | 2 | 5 | mobile | 2014-01-01 00:17:20 | 0 | 1 | 0 |
2 | 3 | 4 | mobile | 2014-01-01 00:28:10 | 0 | 1 | 0 |
3 | 4 | 1 | mobile | 2014-01-01 00:44:25 | 0 | 1 | 0 |
4 | 5 | 4 | mobile | 2014-01-01 01:11:30 | 0 | 1 | 0 |
め、め、めんどくせぇ!!!!
こんなん、他にカテゴリ化したい変数が10個とかあったら発狂待った無しやん。。。
(ほんとはもっといい方法があるけど私が知らないだけだったらゴメンね、scikit-learn)
category_encodersなら
では、真打ち登場。
pip install category_encoders
で事前にライブラリは入れておいてください
ドヤァアアアァァ
import category_encoders as ce
# Eoncodeしたい列をリストで指定。もちろん複数指定可能。
list_cols = ['device']
# OneHotEncodeしたい列を指定。Nullや不明の場合の補完方法も指定。
ce_ohe = ce.OneHotEncoder(cols=list_cols,handle_unknown='impute')
# pd.DataFrameをそのまま突っ込む
df_session_ce_onehot = ce_ohe.fit_transform(df_session)
df_session_ce_onehot.head()
Index | device_0 | device_1 | device_2 | device_-1 | session_id | customer_id | session_start |
---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 1 | 1 | 2014-01-01 00:00:00 |
1 | 1 | 0 | 0 | 0 | 2 | 1 | 2014-01-01 00:17:20 |
2 | 0 | 1 | 0 | 0 | 3 | 5 | 2014-01-01 00:28:10 |
3 | 0 | 1 | 0 | 0 | 4 | 3 | 2014-01-01 00:43:20 |
4 | 0 | 0 | 1 | 0 | 5 | 2 | 2014-01-01 01:10:25 |
まず、そもそもpd.DataFrameで投げたらpd.DataFrameで返してきてくれる!!これよ!!ワシぁ、これを待ってたのよ!!
しかも、変換したい列をリストで渡せば全て{カラム名}_{カラム内の値}の形式でOneHotに展開してくれる。これで、変換したい列がいくらあっても(メモリに制約がない限り)簡単に変換できる!
scikit-learnにできないことを平然とやってのけるッ!! そこにシビれる あこがれるゥ!!
#Category Encodersを使うべき3つの理由
ちょっと、前置きが長くなったがここでcategory_encoders
をカテゴリ特徴量の前処理モジュールとして最優先で使うべき理由をまとめておく。
# | 理由 |
---|---|
1 | pandasのフォーマットがサポートされているので、np.array → pd.DataFrameのような変換が不要 |
2 | 数値に変換する列を指定して変換が可能 |
3 | scikit-learnのpreprocessingよりも多くの変換アルゴリズムが用意されている |
上の例でも見たように、#1や#2が特に病みつきになる便利さ。#3もシチュエーションに合わせて、多様な変換ができるから今更preprocessingを使う理由が見つからない。。
#Category Encodersを活用するシチュエーション3選
①RandomForestやGBMみたいな木構造系のモデルを使うから、Ordinal EncodeでOKな場合
category_encoders.OrdinalEncoder
を用いるケースが実用上、最頻出な気がする。
このパターンは、重回帰モデルのような線形のモデルでは、カテゴリ変数を序数に変換すると結果がワケワカランことになるので、変換した後のモデルを木構造系のモデルにすることを決めている場合にしか使えない。が、変数をスパースに展開せずに数値型に変換できるのでめちゃ便利。
特に、貧弱なメモリでデータを取り扱っている場合、自然言語のようなスパースなデータを特徴量としてOneHotに展開するとすぐにスパースな行列が生成されて即MemoryOverflowになるため、スパースな行列にならないように情報を保持する方法を知っておくのはすごく重要。
まぁ、自然言語をOrdinalEncodeするのがいいかと言われるとそれはそれで最善解ではないだろうけど。。。
使い方はこんな感じ。
import category_encoders as ce
# Eoncodeしたい列をリストで指定。もちろん複数指定可能。
list_cols = ['device']
# 序数をカテゴリに付与して変換
ce_oe = ce.OrdinalEncoder(cols=list_cols,handle_unknown='impute')
df_session_ce_ordinal = ce_oe.fit_transform(df_session)
df_session_ce_ordinal.head()
head結果
Index | session_id | customer_id | session_start | device |
---|---|---|---|---|
0 | 1 | 1 | 2014-01-01 00:00:00 | 0 |
1 | 2 | 1 | 2014-01-01 00:17:20 | 0 |
2 | 3 | 5 | 2014-01-01 00:28:10 | 1 |
3 | 4 | 3 | 2014-01-01 00:43:20 | 1 |
4 | 5 | 2 | 2014-01-01 01:10:25 | 2 |
deviceが数値に無事変わっている。
ちなみにdeviceの数値が何に対応するかは、.category_mapping
で確認できる。
Index | column | label | ord_num |
---|---|---|---|
0 | device | desktop | 0 |
1 | device | mobile | 1 |
2 | device | tablet | 2 |
②カーディナリティ低めなカテゴリ特徴量を変換する場合
都道府県や性別のようにユニークな値の種類が限られている(カーディナリティが低い)カテゴリ特徴量は、OneHotに展開してもメモリに影響を与えにくいのでOneHotEncodingで事足りる。
上で紹介したようにcategory_encoders.OneHotEncoder
もあるが、category_encoders.BinaryEncoder
もある。
category_encoders.BinaryEncoder
は、3つのユニークな値がある場合、2種類のバイナリ変数があれば残りの1つは自動的に決まるので明示的に展開されないところや未知データの取り扱いがOneHotEncorder
と異なるところだが、基本的なコンセプトとしては同じ。
OneHotに展開している限り、線形モデルでも非線形モデルでも利用可能なので応用範囲は広い。
list_cols = ['customer_id','device']
# Binary表現を用いる
bine = ce.BinaryEncoder(cols=list_cols,handle_unknown='impute')
bine.fit_transform(df_session).head()
head結果
Index | customer_id_0 | customer_id_1 | customer_id_2 | customer_id_3 | device_0 | device_1 | device_2 | session_id | session_start |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 2014-01-01 00:00:00 |
1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 2 | 2014-01-01 00:17:20 |
2 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 3 | 2014-01-01 00:28:10 |
3 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 4 | 2014-01-01 00:44:25 |
4 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 5 | 2014-01-01 01:11:30 |
device:2(Tablet)はdevice_1=1,device_2=1で表現されている。
ちなみに、pandasにはget_dummies
というOneHotに特徴量を展開してくれる便利なメソッドがあるが、あまり使わないようにしている。
なぜなら、モデリング時点で存在しない番号や名称が予測を適用するタイミングで現れたり、逆にモデリング時には存在していたものが予測時点でなくなっている場合、双方のデータの列数が一致しなくなり使い物にならないから。
target_col = 'device'
# 意図的に出現パターンが異なるデータを2種類作る
# desktop,mobile,tablet全てが含まれるパターン
df_train = df_session.head(4)
# desktopしか含まれないパターン
df_test = df_session.tail(2)
pd.get_dummies(df_train[target_col]) #3列に展開
pd.get_dummies(df_test[target_col]) #1列に展開
df_train
Index | desktop | mobile | tablet |
---|---|---|---|
0 | 1 | 0 | 0 |
1 | 1 | 0 | 0 |
2 | 0 | 1 | 0 |
3 | 0 | 1 | 0 |
4 | 0 | 0 | 1 |
df_test
Index | desktop |
---|---|
0 | 1 |
1 | 1 |
これでは、df_trainで作ったモデルをdf_testに当てはめることはできない。モデリングすることが前提なら素直にcategory_encordersを使いましょう。
target_col = ['device']
ohe = ce.OneHotEncoder(cols=target_col,handle_unknown='impute') #imputeを指定すると明示的にfitdataに含まれない要素が入って来た場合に[列名]_-1列に1が立つ
ohe.fit(df_train)
# trainに含まれている要素がなくても変換可能
ohe.transform(df_test)
Index | device_0 | device_1 | device_-1 | session_id | customer_id | session_start |
---|---|---|---|---|---|---|
33 | 1 | 0 | 0 | 34 | 3 | 2014-01-01 08:28:05 |
34 | 1 | 0 | 0 | 35 | 1 | 2014-01-01 08:45:25 |
逆にdesktopしか含まれないdf_test
で学習しても処理可能。
target_col = ['device']
ohe2 = ce.OneHotEncoder(cols=target_col,handle_unknown='impute')
ohe2.fit(df_test)
ohe2.transform(df_train)
Index | device_0 | device_-1 | session_id | customer_id | session_start |
---|---|---|---|---|---|
0 | 1 | 0 | 1 | 1 | 2014-01-01 00:00:00 |
1 | 1 | 0 | 2 | 1 | 2014-01-01 00:17:20 |
2 | 0 | 1 | 3 | 5 | 2014-01-01 00:28:10 |
3 | 0 | 1 | 4 | 3 | 2014-01-01 00:43:20 |
うーむ、素晴らしい!!
③カーディナリティ高めなカテゴリ特徴量を変換する場合
さて、問題はこのケース。顧客コードや店番号、製品番号などOneHotに展開していたらメモリがどれだけあっても足りないようなカーディナリティの高いカテゴリ特徴量をどう捌くべきかという問題。
一つ目の回答としては①で示したようにOrdinalEncoderを使うという方法。まぁだいたいこれでいいのでは?と思っているものの他にもやり方はいくつかあるようで。
category_encoders.LeaveOneOutEncoder
やcategory_encoders.TargetEncoder
は目的変数を指定してカテゴリ特徴量を変換してくれるらしい。
どちらも実装のリファレンスがkaggleから派生しているので、理論がはっきりあって作られたというより、有効そうだから実装しているといった印象。
Targetに設定したyの平均を対象のカテゴリごとに計算するのがメインコンセプト。
スムージングしてたりするけど、kaggleのコメントを見てるとあんまり成果が出ている変換方法でもなさそう。
# 適当にTarget列を生成
df_session['target'] = np.random.normal(loc=0,scale=1,size=len(df_session))
# カテゴリ毎にtarget平均をとるように変換
list_cols = ['customer_id','device'] #試しに複数列で実施
loo = ce.LeaveOneOutEncoder(cols=list_cols)
loo.fit_transform(X=df_session,y=df_session['target'])# target列をyに指定
Index | session_id | session_start | target | customer_id | device |
---|---|---|---|---|---|
0 | 1 | 2014-01-01 00:00:00 | -0.591633 | -0.126731 | -0.774547 |
1 | 2 | 2014-01-01 00:17:20 | -1.091073 | -0.126731 | -0.774547 |
2 | 3 | 2014-01-01 00:28:10 | -0.788976 | -1.047591 | -0.201740 |
3 | 4 | 2014-01-01 00:43:20 | -0.749195 | -0.813062 | -0.201740 |
4 | 5 | 2014-01-01 01:10:25 | 0.289323 | -0.540192 | -0.212356 |
5 | 6 | 2014-01-01 01:22:20 | -0.014197 | -0.126731 | -0.774547 |
うーむ確かに、平均値っぽく変換されている。
df_session.groupby(['device'],as_index=False)['target'].mean()
叩いて出したカテゴリ平均値と一致しているので、やはり単純にカテゴリ毎に平均とってるだけだな。。。
微妙だ。。。
素直にOrdinalEncoderで変換しよ。
#まとめ
category_encoders
を使うと面倒なカテゴリ特徴量をストレスなく変換できる。
グレートですよこいつはァ!!
このcategory_encoders
はgithub上で日々更新されているのだけれど、よくよくレポジトリを掘っていくとこのモジュールの他にもsklearn-pandasとか複数の互換PJが進行しているっぽいのでこちらも注目して見ておきたい
おしまい
#参考
公式リファレンス
githubレポジトリ
Target Encoderの元論文
High cardinalityなカテゴリ特徴量の扱い方に関するkaggleでのdiscussion