🍺

【6日目】カテゴリー変数のエンコーディングをする【2021アドベントカレンダー】

2021/12/06に公開

2021年1人アドベントカレンダー(機械学習)、6日目の記事になります。

https://qiita.com/advent-calendar/2021/solo_advent_calendar

テーマは カテゴリー変数のエンコーディング になります。

ざっくりいうと数値ではないデータを機械学習で処理できるように数値に置き換える手法になります。

詳しくは以下のとおり。

カテゴリー変数とは、身長や年齢のように数値で表せる変数ではなく、グループ・属性(色・国など)を分類する用途で示される変数を指します。また、カテゴリー変数は「名義変数」と「順序変数」で分けることができます。

変数名 概要 変数例
数値変数 数値で構成された変数 ・身長・年齢
名義変数 並び替えや順序付けができないカテゴリー変数 ・色(赤、青、緑)
・国(日本、アメリカ、中国)
順序変数 並び替えや順序付けが可能なカテゴリー変数 ・Tシャツのサイズ(S、M、L)

https://di-acc2.com/programming/python/3737/

うち、カテゴリ変数の項目数に応じて連番を渡す Ordinal Encoding と、カテゴリ変数の項目の内訳ごとにダミー変数 に変換するOneHot Encoding を取り扱います。

ダミー変数とは何か?
0か1の値を取る変数です。
https://plaza.umin.ac.jp/~health-stat/faq/faq17/

Colab のコードはこちら Open In Colab

全てOrdinalEncoding

scikit-learn の OrdinalEncoding は欠損値があっても処理してくれますがエンコーディングはされないっぽいので欠損値を "missing" という文字列に置き換えています。
("missing"にしているのはわかりやすいからであって必然性はありません。)

また、エンコーディング処理すると numpy の array 型になってしまいます。

OrdinalEncoding の引数である handle_unknown に 'use_encoded_value' を渡してやると、未知数を unknown value で設定した数値に置き換えることができます。
-2 を設定しているのは、後ほど出てくる Category_Encoders の OrdinalEncoding は自動的に未知数を -1 に、連番を 1 から開始することから、それに合わせるために -2 を指定しています。

  • Category_Encoders の OrdinalEncoding 未知数 -1、連番 1 からスタート
  • scikit-learn の OrdinalEncoding の連番は 0 からスタートするので、未知数を -2 にして +1 すれば Category_Encoders の OrdinalEncoding と同じ処理にすることができる。

ただし、カテゴリーの変換法則を指定してやらないと両者の処理は一致しないかもしれません。

# 数値データとカテゴリーデータに分ける
X_train_number = X_train.select_dtypes(include="number")
X_test_number = X_test.select_dtypes(include="number")

X_train_category = X_train.select_dtypes(include="object")
X_test_category = X_test.select_dtypes(include="object")

# index をリセット
X_train_number = X_train_number.reset_index(drop=True)
X_test_number = X_test_number.reset_index(drop=True)

X_train_category = X_train_category.reset_index(drop=True)
X_test_category = X_test_category.reset_index(drop=True)
# カテゴリーのエンコーディング法則を指定する
ordinal_all_cols_mapping = []

for column in X_train_category.columns:
    ordinal_one_cols_mapping = []
    for category in natsorted(X_train_category[column].unique()):
        ordinal_one_cols_mapping.append(category)

    ordinal_all_cols_mapping.append(ordinal_one_cols_mapping)
from sklearn.preprocessing import OrdinalEncoder

# エンコーディング設定
ode = OrdinalEncoder(
    handle_unknown = 'use_encoded_value', # 未知数をunknown valueに置き換える設定
    unknown_value = -2,
    dtype = np.float64,
    categories = ordinal_all_cols_mapping
)

# OrdinalEncoderは欠損値があっても処理できるが、エンコーディングしないので欠損値を missing に置き換える
X_train_category = X_train_category.fillna("missing")
X_test_category = X_test_category.fillna("missing")

# エンコーディング
# trainデータは学習と変換、testデータは変換のみを実施。trainの学習パターン通りに変換するため
X_train_labels = ode.fit_transform(X_train_category)
X_test_labels = ode.transform(X_test_category)

# OrdinalEncoder は np.array型に変換してしまうため、DataFrame型で再構築する
X_train_oe = pd.DataFrame(
    X_train_labels,
    columns=X_train_category.columns
)

X_test_oe = pd.DataFrame(
    X_test_labels,
    columns=X_test_category.columns
)

# 数値データを結合
X_train_oe = pd.concat([X_train_oe, X_train_number ], axis=1)
X_test_oe = pd.concat([X_test_oe, X_test_number ], axis=1)

カテゴリー変数の内訳が少ない項目はOneHotEncoding、多い項目はOrdinalEncoding

カテゴリー変数の内訳の数が多い変数に OneHot Encoding 処理を行うと、カラム数が多すぎてメモリーエラーになる可能性がありますので、カテゴリー変数の内訳の数が多いものは Ordinal Encoding を、少ないものは OneHot Encoding を 行います。
(本来は変数の性質に併せてOneHot がよいか、Ordinal がよいか検討すべきだとは思いますが)

OrdinalEncoding と違い、OnehotEncodingは欠損値があるとエンコーディング処理されませんので欠損値処理が必要です。

OrdinalEncoding と同様、OneHotEncoding もエンコーディング処理すると numpy の array 型になってしまいます。

OneHotEncoding 処理後のカラム名は、get_feature_names_out() メソッドを使うことで取得できます。

# カテゴリー変数の内訳の数に応じてカラムを分割する
category_unique_num = df.drop(["Global_Sales",  "NA_Sales", "PAL_Sales", "JP_Sales", "Other_Sales"], axis=1).select_dtypes(include="object").nunique()
few_kinds_category_columns = category_unique_num[category_unique_num < 10].index
many_kinds_category_columns = category_unique_num[category_unique_num >= 10].index
from sklearn.preprocessing import OneHotEncoder

# エンコーディング設定
ode = OrdinalEncoder(
    handle_unknown = 'use_encoded_value', # 未知数をunknown valueに置き換える設定
    unknown_value = -2,
    dtype = np.float64,
    categories = ordinal_all_cols_mapping_en
)

ohe = OneHotEncoder(
    handle_unknown = 'ignore',
    sparse = False,
    dtype = np.int64,
    )

# OneHotEncoderは欠損値があっても処理できるが、エンコーディングしないので欠損値を missing に置き換える
X_train_category = X_train_category.fillna("missing")
X_test_category = X_test_category.fillna("missing")

# エンコーディング
# trainデータは学習と変換、testデータは変換のみを実施。trainの学習パターン通りに変換するため

# OneHotEncoder
X_train_ohe_labels = ohe.fit_transform(X_train_category[few_kinds_category_columns])
X_test_ohe_labels = ohe.transform(X_test_category[few_kinds_category_columns ])

# OrdinalEncoder
X_train_ode_labels = ode.fit_transform(X_train_category[many_kinds_category_columns]) + 1
X_test_ode_labels = ode.transform(X_test_category[many_kinds_category_columns ]) + 1

# np.array型になってしまうため、DataFrame型で再構築する
# trainデータ
X_train_ode = pd.DataFrame(
    X_train_ode_labels,
    columns=X_train_category[many_kinds_category_columns].columns
)
X_train_ohe = pd.DataFrame(
    X_train_ohe_labels,
    columns=ohe.get_feature_names_out(few_kinds_category_columns)
)
X_train_en = pd.concat([X_train_ohe, X_train_ode], axis=1)

# testデータ
X_test_ode = pd.DataFrame(
    X_test_ode_labels,
    columns=X_test_category[many_kinds_category_columns].columns
)
X_test_ohe = pd.DataFrame(
    X_test_ohe_labels,
    columns=ohe.get_feature_names_out(few_kinds_category_columns)
)
X_test_en = pd.concat([X_test_ohe, X_test_ode], axis=1)


# 数値データを結合
X_train_en = pd.concat([X_train_en, X_train_number ], axis=1)
X_test_en = pd.concat([X_test_en, X_test_number ], axis=1)

Category_Encoders を使う

カテゴリー変数のエンコーディング用のライブラリ、Category_Encoders を使います。

エンコーディング処理後の返り値を DataFrame型 にすることができますし、エンコーディング処理を行うカラムを指定できますので、かなり楽です。

# カテゴリーのエンコーディング法則を指定する
ordinal_all_cols_mapping_ce = []

for i, column in enumerate(many_kinds_category_columns):
    ordinal_one_cols_mapping = {}
    ordinal_one_cols_mapping_breakdown = {}
    for j, category in enumerate(natsorted(X_train[column].unique())):
        ordinal_one_cols_mapping_breakdown[category] = j

    ordinal_one_cols_mapping["col"] = column
    ordinal_one_cols_mapping["mapping"] = ordinal_one_cols_mapping_breakdown
    ordinal_all_cols_mapping_ce.append(ordinal_one_cols_mapping)
# エンコーディング設定
ode = ce.OrdinalEncoder(
    mapping = ordinal_all_cols_mapping_ce,
    cols = many_kinds_category_columns
)

ohe = ce.OneHotEncoder(
    use_cat_names=True,
    cols = few_kinds_category_columns
)

# 元データを壊さないようにコピーする
X_train_ce = X_train.copy()
X_test_ce = X_test.copy()

# 欠損値処理
category_columns = category_unique_num.index

X_train_ce[category_columns] = X_train_ce[category_columns].fillna("missing")
X_test_ce[category_columns] = X_test_ce[category_columns].fillna("missing")

# OneHotEncoder
X_train_ce = ohe.fit_transform(X_train_ce)
X_test_ce = ohe.transform(X_test_ce)

# OrdinalHotEncoder
X_train_ce = ode.fit_transform(X_train_ce)
X_test_ce = ode.transform(X_test_ce)

# 正規化
sc = StandardScaler()
X_train_ce = pd.DataFrame(
                            sc.fit_transform(X_train_ce),
                            columns=X_train_ce.columns
                            )

X_test_ce = pd.DataFrame(
                            sc.transform(X_test_ce),
                            columns=X_test_ce.columns
                            )

OrdinalEncodingのみ: 0.2351561219774193
sklearn で OneHot+Ordinal Encoding: 0.22883073440896592
Category Encorders で OneHot+Ordinal Encoding: 0.22648763368118405

6日目は以上になります、最後までお読みいただきありがとうございました。

https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OrdinalEncoder.html

https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html

https://contrib.scikit-learn.org/category_encoders/

Discussion