Blogress

機械学習関連ばっかり書きます

カテゴリ変数(質的データ)の前処理の違いまとめ

はじめに

一般的に機械学習においてカテゴリ変数は、前処理として数値化する必要があります。
本記事ではその前処理の方法と違いについてまとめです。

データの種類と意味

下図のように変数は4つの尺度に分けられます。
今回説明するのは名義尺度と順序尺度に関する前処理の方法です。

f:id:Noleff:20210102193614p:plain

引用:人工知能プログラミングのための数学がわかる本

データ

まず、簡単なデータを以下のコードで準備します。あるユーザがある飲み物を買ったときの値段、サイズ、飲み物の種類が記載されています。
このデータでは、sizeデータはL>M>Sの関係にあるため順序尺度、drinkデータは大小関係がないため名義尺度になります。

import numpy as np
import pandas as pd

df = pd.DataFrame([
        ['Aさん', 100, 'S', 'cola'],
        ['Bさん', 150, 'M', 'tea'],
        ['Cさん', 200, 'L', 'coffee'],
        ['Dさん', 100, 'S', 'tea'],
        ['Eさん', 200, 'L', 'coffee'],
        ['Fさん', 200, 'L', 'tea'],
        ['Gさん', 150, 'M', 'tea'],
        ['Hさん', 200, 'L', 'coffee'],
        ['Iさん', 100, 'S', 'cola'],
        ['Jさん', 200, 'L', 'tea']],
        columns=['user', 'price', 'size', 'drink'])
df

f:id:Noleff:20210102203642p:plain

前処理の種類と違い

一般に2つのアプローチがあります。
1つ目は順番にラベリングしていく方法です。colaを0、teaを1、coffeeを2のようにラベルをつけます。
2つ目はダミー変数化(OneHotエンコーディング)する方法です。cola、tea、coffeeのカラムを新しく追加し、任意のカラムを1、それ以外のカラムを0にします。

1つ目の方法は決定木ベースのモデルには効果はありますが、線形モデルやNNに使う場合は注意が必要です。「teaはcoffeeと2倍の関係にある」といった解釈をされかねません。
2つ目の方法は1つ目より使われる気がします。ただし、カテゴリ変数が1000個あるといったふうに、量が多ければ多いほどカラムも増えます。膨大なカテゴリ変数は適宜集約するといった処理が必要になるかと思います。

名義尺度

ラベリング

factorize

pandasのfactorizeを使えば、簡単にカテゴリ変数をラベリングしてくれます。

ft_array, ft_index = pd.factorize(df['drink']) # tupple型で返却される
df_ft = pd.DataFrame(ft_array, columns=['ft_drink'])
df_factrize = pd.concat([df, df_ft], axis=1) # 元のデータフレームと連結
df_factrize

f:id:Noleff:20210102205057p:plain

LabelEncoder

sklearnのLabelEncoderを使えば、簡単にカテゴリ変数をラベリングしてくれます。

from sklearn.preprocessing import LabelEncoder

lenc = LabelEncoder()
lenc.fit(df['drink'])
lenc_vec = lenc.transform(df['drink'])
df_le = pd.DataFrame(lenc_vec, columns=['le_drink'])
df_lenc = pd.concat([df, df_le], axis=1) # 元のデータフレームと連結
df_lenc

f:id:Noleff:20210102205138p:plain

factorizeとLabelEncoderの違い

以下のように学習データとテストデータに分けられたデータがあるとします。
Nさんが学習データにはないサイズと飲み物の種類(LLとcider)が記載されていることに注意してください。

train = df.copy()
test = pd.DataFrame([
        ['Kさん', 200, 'L', 'cola'],
        ['Lさん', 100, 'S', 'tea'],
        ['Mさん', 150, 'M', 'coffee'],
        ['Nさん', 250, 'LL', 'cider']],
        columns=['user', 'price', 'size', 'drink'])

factorizeは値の出現順にラベルが振られるため、データフレームが別々にある場合、別のラベルが振らてしまう可能性があります。

train_ft, idx = pd.factorize(train['drink'])
test_ft, idx = pd.factorize(test['drink'])

train_df = pd.concat([train, pd.DataFrame(train_ft, columns=['ft_drink'])], axis=1)
test_df = pd.concat([test, pd.DataFrame(test_ft, columns=['ft_drink'])], axis=1)

display(train_df)
display(test_df)

f:id:Noleff:20210102213152p:plain

一度trainデータとtestデータを連結させ、1つのデータフレームとしてラベルを振れば回避することができます。

all_df = pd.concat([train, test], axis=0).reset_index(drop=True)
all_ft, idx = pd.factorize(all_df['drink'])
train_test_df = pd.concat([all_df, pd.DataFrame(all_ft, columns=['ft_drink'])], axis=1)

しかし、kaggleのようにあらかじめ学習データとテストデータがわけられている場合、わざわざ連結するのは面倒です。LabelEncoderを使えば連結せずに済みます。
なお、ラベリングの順番はfactorizeは値の出現順ですが、LabelEncoderはアルファベット順です。名義尺度のデータでは、ここはあまり気にする必要はないかと思います。

lenc = LabelEncoder()
train_df = lenc.fit(['cola', 'tea', 'coffee', 'cider']) # ここでカテゴリ変数の種類を指定
train_lenc = lenc.transform(train[['drink']])
test_lenc = lenc.transform(test[['drink']])

train_df = pd.concat([train, pd.DataFrame(train_lenc, columns=['le_drink'])], axis=1)
test_df = pd.concat([test, pd.DataFrame(test_lenc, columns=['le_drink'])], axis=1)

display(train_df)
display(test_df)

f:id:Noleff:20210102214232p:plain

ダミー変数化

get_dummies

pandasのget_dummiesを使えば、簡単にカテゴリ変数をダミー変数化してくれます。

df_gd = pd.get_dummies(df['drink'])
df_dummy = pd.concat([df, df_gd], axis=1) # 元のデータフレームと連結
df_dummy

f:id:Noleff:20210102214359p:plain

OneHotEncoder

sklearnのOneHotEncoderを使えば、同様にダミー変数化してくれます。

from sklearn.preprocessing import OneHotEncoder

oenc =  OneHotEncoder(sparse=False, dtype=int)
oenc.fit(df[['drink']]) # pandas.core.frame.DataFrame型もしくは二次元のnumpy.ndarray型が引数でないとエラー
oenc_vec = oenc.transform(df[['drink']]) # numpy.ndarray型で返却される

df_oenc = pd.DataFrame(oenc_vec, columns=['coffee', 'cola', 'tea'])
df_oht =  pd.concat([df, df_oenc], axis=1) # 元のデータフレームと連結
df_oht

f:id:Noleff:20210102214533p:plain

LabelBinarizer

sklearnのLabelBinarizerを使えば、同様にダミー変数化してくれます。

from sklearn.preprocessing import LabelBinarizer

lbnr = LabelBinarizer()
lbnr.fit(df[['drink']])
df_lbnr =  pd.concat([df, pd.DataFrame(lbnr.transform(df[['drink']]), columns=['coffee', 'cola', 'tea'])], axis=1) # OneHotEncoderとほぼ同じコードなためワンライナーで記述
df_lbnr

f:id:Noleff:20210102214533p:plain

get_dummiesとOneHotEncoder、LabelBinarizerの違い

factorizeとLabelEncoderの違い同様、 学習データとテストデータにデータがわけられているとします。

get_dummisを用いた場合、学習データとテストデータで作成されるカラムの数が異なってしまいます。factorizeとLabelEncoderの違いと同じように学習データとテストデータを連結させる方法もありますが、OneHotEncoderとLabelBinarizerを使えば回避できます。
以下のコードではLabelBinarizerの例です。

lbnr = LabelBinarizer()
lbnr.fit(train['drink'])
display(pd.DataFrame(lbnr.transform(train[['drink']]), columns=['coffee', 'cola', 'tea']))
display(pd.DataFrame(lbnr.transform(test[['drink']]), columns=['coffee', 'cola', 'tea']))

f:id:Noleff:20210102215708p:plain

ただし、データの中にnanもしくはinfが含まれている場合、get_dummisはエラーが出ませんが、OneHotEncoderとLabelBinarizerはエラーが出ます。

oenc = OneHotEncoder(sparse=False)
df['nan_and_inf'] = ['A', 'A', 'A', 'A', 'A', 'A', np.nan, np.inf, 'B', 'B']
df

f:id:Noleff:20210102225157p:plain

# エラーが出る
oenc.fit_transform(df[['nan_and_inf']]) 
# エラーが出ない
pd.get_dummies(df['nan_and_inf'])

f:id:Noleff:20210102225227p:plain

nanはスルーされますが、infはされないことに注意です。

OneHotEncoderとLabelBinarizerの違い

OneHotEncoderとLabelBinarizerの違いは複数のカラムをまとめてダミー変数化できるかどうかです。順序尺度データではありますが、sizeデータもまとめてダミー変数化してみます。

# エラーが出る
pd.DataFrame(lbnr.fit_transform(train[['drink', 'size']]), columns=['coffee', 'cola', 'tea', 'L', 'M', 'S'])
# エラーが出ない
pd.DataFrame(oenc.fit_transform(train[['drink', 'size']]), columns=['coffee', 'cola', 'tea', 'L', 'M', 'S'])

f:id:Noleff:20210102220004p:plain

順序尺度

名義尺度とは異なりラベリングが一般的かと思います。しかし、factorizeやLabelEncoderでは任意の順番にラベリングできません。また、色々調べてみましたが、pandas、sckit-learnともに任意の順番にラベリングできるメソッドはなさそうです。

そのため自分で関数なり作る必要があります。以下に例を示します。 

apply

lambda式を使えば一行で書けます。

df['ordinal_size'] = df['size'].apply(lambda x: ['S', 'M', 'L', 'LL'].index(x))

map

mapでもlambda式を使えば一行で書けます。ただmapの場合は辞書型もいける口です。ここは個人の好みでしょう。

# df['ordinal_size'] = df['size'].map(lambda x: ['S', 'M', 'L', 'LL'].index(x)) # lambdaもいける
df['ordinal_size'] = df['size'].map({'S': 0, 'M': 1, 'L': 2, 'LL':3})

また、自分でカテゴリ変数のリストを作るのが面倒な場合(もしくは多すぎる)は、学習データとテストデータ含めたすべてのデータからユニークな値を取るしかない思います。

unique_size_list = list(all_df['size'].unique())

train['ordinal_size'] = train['size'].apply(lambda x: unique_size_list.index(x))
test['ordinal_size'] = test['size'].apply(lambda x: unique_size_list.index(x))

display(train)
display(test)

f:id:Noleff:20210103012832p:plain

factorize

一応factorizeとLabelEncoderの実行結果も示します。

sizeの出現順がS、M、Lだったためたまたまうまくいっています。順番があらかじめラベリングしたい順序にソートされていればfactorizeは使えなくもないです。

ft_array, ft_index = pd.factorize(df['size']) # tupple型で返却される
df_ft = pd.DataFrame(ft_array, columns=['ft_size'])
df_factrize = pd.concat([df, df_ft], axis=1) # 元のデータフレームと連結
df_factrize

f:id:Noleff:20210102203834p:plain

LabelEncoder

アルファベット順=ラベリングしたい順序であれば使えます。sizeの例では無理です。

from sklearn.preprocessing import LabelEncoder

lenc = LabelEncoder()
lenc.fit(df['size'])
lenc_vec = lenc.transform(df['size'])
df_le = pd.DataFrame(lenc_vec, columns=['le_size'])
df_lenc = pd.concat([df, df_le], axis=1) # 元のデータフレームと連結
df_lenc

f:id:Noleff:20210102204314p:plain

まとめ

まとめると以下の表になります。

  • ○:この手法がベター、もしくはこの手法しかない
  • △:この手法よりも良い手法がある
  • ✕:この手法は一般的ではない
名義尺度 順序尺度 感想
get_dummis 学習データとテストデータのカテゴリ変数に注意
OneHotEncoder nanとinfに注意
LabelBinarizer nanとinfに注意
factorize 楽だが中途半端
LabelEncoder 楽だが中途半端
オリジナル関数 オレオレ関数は最強

表から分かる通り、名義尺度はOneHotEncoderもしくはLabelBinarizerを使う、順序尺度はオリジナル関数を定義する、というのが結論です。

参考文献

人工知能プログラミングのための数学がわかる本 | 石川 聡彦 |本 | 通販 | Amazon

Pythonでのカテゴリ変数(名義尺度・順序尺度)のエンコード(数値化)方法 ~順序のマッピング、LabelEncoderとOne Hot Encoder~ - Qiita

One-HotエンコーディングならPandasのget_dummiesを使おう | Shikoan's ML Blog

【python】機械学習でpandas.get_dummiesを使ってはいけない - 静かなる名辞

【python】sklearnでのカテゴリ変数の取り扱いまとめ LabelEncoder, OneHotEncoderなど - 静かなる名辞

pandasでカテゴリー変数を数値に変換する | 分析ノート

モデリングのための特徴量の前処理について整理した - For Your ISHIO Blog

python - LabelEncoderパンダdfの適合順序