こんにちは。機械学習エンジニアをしている古賀です。
最近は愉快な上司@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"]),
ConcatCombination(
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"]),
ConcatCombination(
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. 数値データの加算が簡単にできる
兄弟/配偶者、両親/子供の数を加算した特徴量を作成する
カテゴリカルデータと同様、数値データの組み合わせ(ここでは加算)も簡単に書くことができます。
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_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_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 を確認するためには、
- モデルの作成
- 学習
- feature importance の抽出
というステップを経る必要があります。これを xfeat を使うと、
- 特徴量選択器の作成
- feature importance の抽出
と短いステップで書くことができます。
今回は、この記事で紹介した手法を使って元のデータにいくつか特徴量を追加し、特徴量の選択をしてみました。
最終的な入力データは以下です。
encoded_train_df.head(5)
- "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で紹介)
この入力データに対して、特徴量の選択をします。
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()
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)
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
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,
)
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)
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