Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

特徴量エンジニアリングのライブラリ xfeat を使ってみて便利だったこと

こんにちは。機械学習エンジニアをしている古賀です。
最近は愉快な上司@tereka114 のもと、精度の上がらないモデルに四苦八苦しています。

そんな私が普段データ分析をする際に難しいことの一つとして、特徴量エンジニアリングがあります。
特徴量エンジニアリングとは、元のデータに新たな特徴量を追加することでモデルの精度を向上させるプロセスのことです。
この結果によってモデルの精度が大きく変わりますが、正しく実行するにはデータへの深い理解やデータ分析力が必要になります。

私もあまり得意ではないのですが、これを簡単にする xfeat という便利なライブラリがあると上司が教えてくれたので、実際に使ってみて便利だったことをまとめました。

※本記事は、Pythonその3 Advent Calendar 2020 の15日目の内容になります。

目次は以下です。

xfeat とは

PFNさんが公開している特徴量エンジニアリングと特徴量探索のためのライブラリです。
一般的にデータを分析するときには、データの型を調べたり、どんな特徴量が精度に効いているか実験したりと、骨が折れますよね。
xfeat を使うと、このようなデータ分析や機械学習の際のコードをより簡単に書くことができます。
github.com

準備

xfeat を実際に使うための環境の構築と、データの準備を行います。

実行環境

Google Colaboratory を使用しました。

xfeatライブラリのインストール

pip コマンドで簡単にインストールできます。

!pip install git+https://github.com/pfnet-research/xfeat.git

ライブラリのインポート

今回使用するライブラリをあらかじめインポートしておきます。

from sklearn.model_selection import KFold
from functools import partial

import optuna
from xfeat import SelectCategorical, LabelEncoder, Pipeline, ConcatCombination, SelectNumerical, \
    ArithmeticCombinations, TargetEncoder, aggregation, GBDTFeatureSelector, GBDTFeatureExplorer

データセット

今回はみなさんおなじみ、Kaggleで初心者向けに公開されているタイタニックデータを使用しました。

データのダウンロード

下の記事を参考にして "Kaggle APIをインストール" ~ "データのダウンロード" まで実施することで、Google Colaboratory上でタイタニックデータをダウンロードできます。
qiita.com

データの読み込み

train_df = pd.read_csv("/content/train.csv")
test_df = pd.read_csv("/content/test.csv")

データの概要

1912年4月15日、氷山に衝突して沈没したタイタニック号の、乗組員の生存状況データです。
全部で1309件あり、12個の説明変数と、1個の目的変数(Survived:生存状況)からなります。
データの説明は、Kaggleのタイタニックデータページに載っています。 www.kaggle.com

中身を確認すると、下のようになっています。

train_df.head(3)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.250000 None S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.283302 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.925000 None S

xfeat を使ってみて便利だったこと

1. カテゴリカルデータ、数値データを中身を調べずに抽出可能

まずはデータセットから、カテゴリカルデータ、数値データをそれぞれ抽出してみます。
通常であれば、データの中身を確認し、型を調べてからでないと特定の型のデータだけを抽出することはできません。
しかし xfeat を使うとその手間が省け、データの中身を調べずに自動で抽出できます。

Ⅰ. カテゴリカルデータのみを抽出する

SelectCategorical().fit_transform(train_df).head(3)
Name Sex Ticket Cabin Embarked
0 Braund, Mr. Owen Harris male A/5 21171 None S
1 Cumings, Mrs. John Bradley (Florence Briggs Th... female PC 17599 C85 C
2 Heikkinen, Miss. Laina female STON/O2. 3101282 None S |

Ⅱ. 数値データのみを抽出する

SelectNumerical().fit_transform(train_df).head(3)
PassengerId Survived Pclass Age SibSp Parch Fare
0 1 0 3 22.0 1 0 7.250000
1 2 1 1 38.0 1 0 71.283302
2 3 1 3 26.0 0 0 7.925000

データの中身を調べず、たった一行で抽出できました。

2. Label Encoding が簡単にできる

特定のカテゴリカルデータに対し、Label Encodingを行う

Label Encoding とは、カテゴリカルデータの各カテゴリを整数に置き換える変換です。
ここでは、学習データからカテゴリカルデータを抽出し、それらに Label Encoding を行っています。

encoder = Pipeline([
    SelectCategorical(exclude_cols=["Name", "Ticket"]),
    LabelEncoder(output_suffix=""),
])

encoded_df = encoder.fit_transform(train_df)
encoded_df.head(3)
Sex Cabin Embarked
0 0 -1 0
1 1 0 1
2 1 -1 0

3. Target Encoding が簡単にできる

Target Encodingとは

目的変数を用いてカテゴリカルデータを数値に変換する方法のことです。
非常に有効な特徴量となる場合もありますが、目的変数を Leak させてしまう可能性もあるので、十分注意する必要があります。
※Leak とは、学習の過程では本来得られない目的変数の情報が見えてしまい、カンニング状態になってしまうことで、過学習してしまうことです。
リークを防ぐため自身のレコードの目的変数を使わないように変換をする必要があるため、少し実装が面倒なのですが、 xfeat を使うと下のようにシンプルに書くことができます。

fold = KFold(n_splits=5, shuffle=False)
encoder = TargetEncoder(
    input_cols=["Cabin"], 
    target_col="Survived",
    fold=fold,
    output_suffix="_re"
    )

encoded_df = encoder.fit_transform(train_df)
encoded_df[["Survived", "Cabin", "Cabin_re"]].head(3)
Survived Cabin Cabin_re
0 0 None 0.303867
1 1 C85 0.303867
2 1 None 0.303867

sklearn や Pandas だけを使うと結構長くなってしまう処理が、これだけ短くシンプルにかけるのはうれしいですね。

4. カテゴリカルデータの組み合わせが簡単にできる

Ⅰ. 二つのカテゴリカルデータを組み合わせる

ここでは、"Sex", "Cabin", "Embarked" のカテゴリカルデータのうち、二つを組み合わせた特徴量をそれぞれ作成しています。
もしも Pandas だけを使って作成しようとした場合、["Sex", "Cabin"], ["Cabin", "Embarked"], ["Embarked", "Sex"] の組み合わせごとに処理を書く必要があり、少し面倒です。
xfeat を使えば、特に処理を書かずに対象の変数と条件を指定するだけで生成してくれるので、実装がとても楽でした。

encoder = Pipeline([
    SelectCategorical(exclude_cols=["Ticket", "Name"]),

    # If there are many categorical columns,
    # users can specify the columns to be combined with `input_cols` kwargs.
    # `r=2` specifies the number of columns to combine the columns.
    ConcatCombination(
        # drop_origin=True, 
        output_suffix="_re", 
        r=2),
])

encoded_df = encoder.fit_transform(train_df)
encoded_df.head(3)
Sex Cabin Embarked SexCabin_re SexEmbarked_re CabinEmbarked_re
0 male None S male_NaN_ maleS _NaN_S
1 female C85 C femaleC85 femaleC C85C
2 female None S female_NaN_ femaleS _NaN_S

Ⅱ. 三つのカテゴリカルデータを組み合わせる

ここでは、"Sex", "Cabin", "Embarked" の三つのカテゴリカルデータを組み合わせた特徴量を作成しています。

encoder = Pipeline([
    SelectCategorical(exclude_cols=["Ticket", "Name"]),

    # If there are many categorical columns,
    # users can specify the columns to be combined with `input_cols` kwargs.
    # `r=2` specifies the number of columns to combine the columns.
    ConcatCombination(
        # drop_origin=True, 
        output_suffix="_re", 
        r=3),
])

encoded_df = encoder.fit_transform(train_df)
encoded_df.head(3)
Sex Cabin Embarked SexCabinEmbarked_re
0 male None S male_NaN_S
1 female C85 C femaleC85C
2 female None S female_NaN_S

5. 数値データの加算が簡単にできる

兄弟/配偶者、両親/子供の数を加算した特徴量を作成する

カテゴリカルデータと同様、数値データの組み合わせ(ここでは加算)も簡単に書くことができます。

# 2-order Arithmetic combinations.
encoder = Pipeline(
    [
        SelectNumerical(),
        ArithmeticCombinations(
            # 兄弟/配偶者、両親/子供の数を加算した特徴量を作成する
            input_cols=["SibSp", "Parch"], 
            drop_origin=True, 
            operator="+", 
            r=2,
        ),
    ]
)

encoded_df = encoder.fit_transform(train_df)

元のデータを確認しておきます。

train_df[["SibSp", "Parch"]].head(3)
SibSp Parch
0 1 0
1 1 0
2 0 0

変換後の結果は下になります。

encoded_df.head(3)
SibSpParch_combi
0 1
1 1
2 0

元データを加算した特徴量が作成されていることが確認できました。

6. Aggregation が簡単にできる

性別ごとに、年齢、Pclass の平均、最大を集計した特徴量を作成する

タイトルのような特徴量を作成しようとすると、Pandas を用いると、下のように長くて少々複雑なコードになってしまいます。

from copy import deepcopy
aggregated_df = deepcopy(train_df)
 
# 性別ごとの年齢の平均値を特徴量に追加
sex_mean_df = train_df.groupby('Sex')['Age'].mean()
aggregated_df.loc[aggregated_df['Sex'] == 'female', 'agg_mean_Age_grpby_Sex'] = sex_mean_df['female']
aggregated_df.loc[aggregated_df['Sex'] == 'male', 'agg_mean_Age_grpby_Sex'] = sex_mean_df['male']
 
# 性別ごとの年齢の最大値を特徴量に追加
sex_max_df = train_df.groupby('Sex')['Age'].max()
aggregated_df.loc[aggregated_df['Sex'] == 'female', 'agg_max_Age_grpby_Sex'] = sex_max_df['female']
aggregated_df.loc[aggregated_df['Sex'] == 'male', 'agg_max_Age_grpby_Sex'] = sex_max_df['male']
 
# 性別ごとのPclassの平均値を特徴量に追加
pclass_mean_df = train_df.groupby('Sex')['Pclass'].mean()
aggregated_df.loc[aggregated_df['Pclass'] == 'female', 'agg_mean_Pclass_grpby_Sex'] = pclass_mean_df['female']
aggregated_df.loc[aggregated_df['Pclass'] == 'male', 'agg_mean_Pclass_grpby_Sex'] = pclass_mean_df['male']
 
# 性別ごとのPclassの最大値を特徴量に追加
pclass_max_df = train_df.groupby('Sex')['Pclass'].max()
aggregated_df.loc[aggregated_df['Pclass'] == 'female', 'agg_max_Pclass_grpby_Sex'] = pclass_max_df['female']
aggregated_df.loc[aggregated_df['Pclass'] == 'male', 'agg_max_Pclass_grpby_Sex'] = pclass_max_df['male']

→この処理を、xfeatを使うと下のように非常にシンプルに書くことができます。これは嬉しいですね^^

aggregated_df, aggregated_cols = aggregation(train_df,
                     group_key="Sex",
                     group_values=["Age", "Pclass"],
                     agg_methods=["mean", "max"],
                     )
                     
cols_to_show = ["Sex"] + aggregated_cols
aggregated_df[cols_to_show].head(3)
Sex agg_mean_Age_grpby_Sex agg_mean_Pclass_grpby_Sex agg_max_Age_grpby_Sex agg_max_Pclass_grpby_Sex
0 male 30.726645 2.389948 80.0 3
1 female 27.915709 2.159236 63.0 3
2 female 27.915709 2.159236 63.0 3

7. LightGBM の feature importance を用いた特徴量の選択が簡単にできる

LightGBM の feature importance を見ると、どの特徴量がどれだけモデルの精度に影響を与えたかを知ることができます。
これにより、データの傾向を捉えたり、新たに追加した特徴量の効果があるかなどを調べたりできます。
通常、feature importance を確認するためには、

  1. モデルの作成
  2. 学習
  3. feature importance の抽出

というステップを経る必要があります。これを xfeat を使うと、

  1. 特徴量選択器の作成
  2. feature importance の抽出

と短いステップで書くことができます。

今回は、この記事で紹介した手法を使って元のデータにいくつか特徴量を追加し、特徴量の選択をしてみました。
最終的な入力データは以下です。

encoded_train_df.head(5)

f:id:acro-engineer:20201214230743p:plain

  • "Cabin_target"
    • "Cabin" データに Target Encoding した特徴量(3で紹介)
  • "SexCabin", "SexEmbarked", "CabinEmbarked"
    • "Sex", "Cabin", "Embarked" データのうち、二つを組み合わせた特徴量(4で紹介)
  • "SibSpParch_combi"
    • "SibSp", "Parch" データを加算した特徴量(5で紹介)
  • "agg_mean_Age_grpby_Sex", "agg_mean_Pclass_grpby_Sex", "agg_max_Age_grpby_Sex", "agg_max_Pclass_grpby_Sex"
    • 性別ごとに、年齢、Pclass の平均、最大を集計した特徴量(6で紹介)

この入力データに対して、特徴量の選択をします。

# LightGBMのパラメータを設定する
lgbm_params = {
    "objective": "binary",
    "metric": "binary_error",
}
fit_kwargs = {
    "num_boost_round": 10,
}

# 特徴量選択器を作成する
selector = GBDTFeatureSelector(
    target_col="Survived",
    threshold=0.5,
    lgbm_params=lgbm_params,
    lgbm_fit_kwargs=fit_kwargs,
)

GBDTFeatureSelector() で、特徴量選択をするインスタンスを作成しています。
また threshold の値で、入力データの変数のうちいくつを選択するかの設定をします。
今回は threshold の値が0.5, 元の説明変数が17個だったので、[17 * 0.5] = 8個の変数が最終的に選択されます。

selected_df = selector.fit_transform(encoded_train_df)
print("Selected columns:", selector._selected_cols)

Selected columns: ['Age', 'Fare', 'Cabin_target', 'SexCabin', 'Pclass', 'Sex', 'SibSpParch_combi', 'SexEmbarked']
年齢、運賃、キャビン番号の Target Encoding、性別×キャビン番号、チケットクラス、性別、兄弟/配偶者+両親/子供の数、性別×乗船場、の重要度が高いことがわかりました。

8. optuna と組み合わせて、特徴量探索が簡単にできる

7 ではパラメータを固定の状態で特徴量選択を行いましたが、さらに optuna と組み合わせることで、
Hyper Parameter Tuning によって LightGBM のモデルに重要な特徴量を探索できます。

今回は、特徴量選択数(入力データの変数のうちいくつを選択するか)と LightGBM のパラメータ二つ(num_leaves, max_depth)に幅を持たせて特徴量探索を行ってみました。入力データは7で使ったものと同じです。
※特徴量選択数は GBDTFeatureExplorer()threshold_range=(0.5, 1.0), で設定しています。
 optuna がこの (0.5, 1.0) の範囲で Hyper Parameter Tuning をして最適な値を探し出してくれます。  

LGBM_PARAMS = {
        "objective": "binary",
        "metric": "binary_error",
        "verbosity": -1,
}


def objective(df, selector, trial):
    selector.set_trial(trial)
    selector.fit(df)
    input_cols = selector.get_selected_cols()

    # Hyper Parameter Tuning するパラメータと範囲を設定する
    lgbm_params = {
        'num_leaves': trial.suggest_int("num_leaves", 3, 10),
        'max_depth': trial.suggest_int("max_depth", 3, 10),
    }
    lgbm_params.update(LGBM_PARAMS)

    # Evaluate with selected columns
    train_set = lgb.Dataset(df[input_cols], label=df["Survived"])
    scores = lgb.cv(lgbm_params, train_set, num_boost_round=100, stratified=False, seed=1)
    
    binary_error_score = scores['binary_error-mean'][-1]
    return 1 - binary_error_score


# 特徴量探索のための説明変数を設定する
# encoded_train_df は、No.7で作成したDataFrameと同じ
input_cols = list(encoded_train_df.columns)
input_cols.remove('Survived')


# 特徴量探索器を作成する
selector = GBDTFeatureExplorer(
    input_cols=input_cols,
    target_col="Survived",
    fit_once=True,
    threshold_range=(0.8, 1.0),
    lgbm_params=LGBM_PARAMS,
)

# Hyper Parameter Tuning を行う
study = optuna.create_study(direction="minimize")
study.optimize(partial(objective, encoded_train_df, selector), n_trials=100)

# 選択された特徴量を確認する
selector.from_trial(study.best_trial)
print("Selected columns:", selector.get_selected_cols())

Selected columns: ['Age', 'Fare', 'Cabin_target', 'SexCabin', 'SibSpParch_combi', 'Pclass', 'Cabin', 'SexEmbarked', 'Sex']
年齢、運賃、キャビン番号の Target Encoding、性別×キャビン番号、兄弟/配偶者+両親/子供の数、チケットクラス、キャビン番号、性別×乗船場、性別、の重要度が高いことがわかりました。

最終的なパラメータの値と学習データの正答率も見てみましょう。

print(study.best_params)

{'GBDTFeatureSelector.threshold': 0.5095199015409342, 'num_leaves': 9, 'max_depth': 8}

print(study.best_value)

0.8067415730337079
Cross Validation では、正答率80.7%でした。

※ちなみに、上のパラメータを使って Kaggle の Leaderboard で submit してみたところ、 スコアは 0.77511 でした。
これだけシンプルに書けて、特徴量の探索から予測までできるのは嬉しいですね。

9. cuDF にも適用可能

xfeat は、 cuDF を使って pandas.DataFrame と同様に Target Encoding や Aggregation を行うことができます。
cuDF とCuPyを使うことで、 Pandas だけを使うよりもなんと10~30倍速く処理を実行できるそうです(公式ページより)。
今回は、 cuDF を使って xfeat の Target Encoding を使ってみました。
cuDF のインストール方法や使い方についての詳細は、下の記事を参考にしてください。

acro-engineer.hatenablog.com

fold = KFold(n_splits=5, shuffle=False)
encoder = TargetEncoder(
    input_cols=["Cabin"], 
    target_col="Survived",
    fold=fold,
    output_suffix="_re"
    )

train_cdf = cudf.from_pandas(train_df)  # if cuDF is available.
encoded_train_cdf = encoder.fit_transform(train_cdf)
encoded_train_cdf[["Survived", "Cabin"]].head(3)

まとめ

xfeat を使って、タイタニックデータの特徴量エンジニアリング、特徴量探索を行ってみました。
Pandas だけを使うよりもシンプルでわかりやすく書けることが多かったので、今後も重宝しそうです。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

【データ分析】 Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のデータサイエンティストの求人 - Wantedlywww.wantedly.com