Python
機械学習
pandas
特徴量
前処理

featuretoolsで特徴量を自動生成して機械学習モデルの構築を楽に早くする手法

概要

機械学習モデルを作るときに、特徴量を増やすことでモデルの精度を向上させようと試みるタイミングがあります。例えば、学習用データを作成するときに SELECT id, COUNT(hoge) FROM table GROUP BY id みたいに、何かの回数とかの変数を新たに作って追加するといった感じです。
こういった特徴量設計は機械学習の前処理の中でもそこそこに重要な工程であると思っています。

課題

しかしながら、こういった特徴量設計では

  • ドメイン知識がないとうまい変数を思いつかない
  • どのくらいの変数を用意すればよいのか見当がつかない
  • 特徴量生成自体の作業が割とめんどくさい

など、重要だけど大変な面もあるという感じになっており、これを

  • できれば特徴量は自動でいい感じに作って欲しい
  • コードをシンプルにしてスッキリさせたい

という形で実現できないものかと思うところです。

解決策

以上の課題に対して、 featuretoolsというPythonのライブラリを用いてこれを解決できないか試みてみようと思います。
きっかけはDataFrameで特徴量作るのめんどくさ過ぎる。。featuretoolsを使って自動生成したろ の記事でこのライブラリの存在を知ったことです。そして同時に使い方についても大変参考になりました。
単純な使い方の解説はこの記事に譲るとして、実際にKaggleで使ってみたケースについて当記事では解説しようと思います。

題材

Kaggleの Home Credit Default Risk を実際にfeaturetoolsを用いて取り組んでみました。

今回使用した手法とその結果

手法としては、

  • featuretoolsで何も考えずに特徴量を量産
  • LightGBMモデルで学習、予測

という実にシンプルなものです。

先にこの手法の結果を記しておくと、
Score: 0.782 (1,105 位 / 3,020 Terms中, 2018/06/21現在)
という形になりました。

かなり思考停止な手法の割にそれなりなスコアが出ているので、ゴリゴリにチューニングすればもっとよい結果になりそうです。

実際のコード

データの準備・特徴量生成前までの前処理

各種データはディレクトリ内に事前にダウンロードしておきます。
今回使われているデータの内容や構造についてはHome Credit Default Risk/Dataをご覧ください。

データを読み込んでカテゴリカルデータをダミーデータ化する前処理を施します。

# data load 
data = pd.read_csv('application_train.csv')
test = pd.read_csv('application_test.csv')
POS_CASH = pd.read_csv('POS_CASH_balance.csv')
buro = pd.read_csv('bureau.csv')
buro_balance = pd.read_csv('bureau_balance.csv')
credit_card = pd.read_csv('credit_card_balance.csv')
payments = pd.read_csv('installments_payments.csv')
prev = pd.read_csv('previous_application.csv')

# 目的変数のみ取り出す
y = data['TARGET']
del data['TARGET']

# 前処理
dataset = pd.concat([data,test])

# payments 以外カテゴリカルデータを持っているので事前にダミー変数化しておく
categorical_features = [col for col in data.columns if data[col].dtype == 'object']
buro_cat_features = [col for col in buro.columns if buro[col].dtype == 'object']
prev_cat_features = [col for col in prev.columns if prev[col].dtype == 'object']
POS_CASH_cat_features = [col for col in POS_CASH.columns if POS_CASH[col].dtype == 'object']
credit_card_cat_features = [col for col in credit_card.columns if credit_card[col].dtype == 'object']
buro_balance_cat_features = [col for col in buro_balance.columns if buro_balance[col].dtype == 'object']

dataset = pd.get_dummies(dataset, columns=categorical_features)
buro = pd.get_dummies(buro, columns=buro_cat_features)
prev = pd.get_dummies(prev, columns=prev_cat_features)
POS_CASH = pd.get_dummies(POS_CASH, columns=POS_CASH_cat_features)
credit_card = pd.get_dummies(credit_card, columns=credit_card_cat_features)
buro_balance = pd.get_dummies(buro_balance, columns=buro_balance_cat_features)

# DataFrameにユニークなIDを持たないものはreset_indexしておく
# 後でfeaturetoolsを使うときに必要になる
POS_CASH.reset_index(inplace=True)
payments.reset_index(inplace=True)
credit_card.reset_index(inplace=True)
buro_balance.reset_index(inplace=True)

featuretoolsによる特徴量生成

Entity 作成

個人的にはrelationshipを定義するだけでよしなにやってくれるので、いちいち pd.merge() する必要がなかったり、 df.gropuby()pd.pivot_table() などで集計する必要がないのがコードをシンプルにしてくれて大変良い感じです。

# Entity 作成
es = ft.EntitySet(id='dataset')

# Entity 追加
# 親テーブル
es.entity_from_dataframe(entity_id='app', dataframe=dataset, index='SK_ID_CURR')

# 子テーブル
es.entity_from_dataframe(entity_id='buro', dataframe=buro, index='SK_ID_BUREAU')
es.entity_from_dataframe(entity_id='prev', dataframe=prev, index='SK_ID_PREV')
es.entity_from_dataframe(entity_id='POS_CASH', dataframe=POS_CASH, index='index')
es.entity_from_dataframe(entity_id='payments', dataframe=payments, index='index')
es.entity_from_dataframe(entity_id='credit_card', dataframe=credit_card, index='index')

# 孫テーブル
es.entity_from_dataframe(entity_id='buro_balance', dataframe=buro_balance, index='index')

# DataFrame間のrelationshipを作成
# 親と子
r_app_buro = ft.Relationship(es['app']['SK_ID_CURR'], es['buro']['SK_ID_CURR'])
r_app_prev = ft.Relationship(es['app']['SK_ID_CURR'], es['prev']['SK_ID_CURR'])
r_app_POS_CASH = ft.Relationship(es['app']['SK_ID_CURR'], es['POS_CASH']['SK_ID_CURR'])
r_app_payments = ft.Relationship(es['app']['SK_ID_CURR'], es['payments']['SK_ID_CURR'])
r_app_credit_card = ft.Relationship(es['app']['SK_ID_CURR'], es['credit_card']['SK_ID_CURR'])

# 子と孫
r_buro_buro_balance = ft.Relationship(es['buro']['SK_ID_BUREAU'], es['buro_balance']['SK_ID_BUREAU'])

# relationshipをリンクさせる
es.add_relationships(relationships=[
    r_app_buro, 
    r_app_prev, 
    r_app_POS_CASH,
    r_app_payments,
    r_app_credit_card,
    r_buro_buro_balance
])

作成したEntityをもとにDFSを実行して特徴量を生成する

# 集約関数
# いろいろな種類があるが、今回はこの4つで試してみた
list_agg = ['count', 'min', 'max', 'mean']

# run dfs 
df_features, features_defs = ft.dfs(entityset = es, 
                                    target_entity = 'app', 
                                    agg_primitives = list_agg, 
                                    max_depth = 2, #孫テーブルまであるので2で指定
                                    chunk_size = 0.025, 
                                    verbose = True)

# SK_ID_CURR が index になっているのでカラムにする
df_features.reset_index(inplace=True)

verbose = True にしてるので経過を出力させてます。出力はこんな感じ。

Built 1084 features
Elapsed: 3:39:49 | Remaining: 00:00 | Progress: 100%|██████████| Calculated: 41/41 chunks  

合計 1,084もの特徴量を約3時間半ほどで生成しています。演算はCPUでやったので、GPU環境とかあったらもっと早くなるでしょう。

訓練データとテストデータに分割

最初の前処理で訓練データとテストデータを混ぜてしまったので、再び分けます。

data = df_features[df_features['SK_ID_CURR'].isin(data['SK_ID_CURR'])]
test = df_features[df_features['SK_ID_CURR'].isin(test['SK_ID_CURR'])]

data.reset_index(drop=True, inplace=True)
test.reset_index(drop=True, inplace=True)

LightGBMで学習

モデルはLightGBMを使っています。なんかKaggleで結構使われてる印象です。人気なんですかね。
LightGBMについてはPython: LightGBM を使ってみるをご覧ください。
k-分割交差検証法で学習してます。
モデルのハイパーパラメータはコンペのKernelの[Updated 0.792 LB] LightGBM with Simple Features
を参考にしました。

from sklearn.model_selection import KFold
from lightgbm import LGBMClassifier
from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix
import gc

gc.enable()

folds = KFold(n_splits=8, shuffle=True, random_state=12345)

oof_preds = np.zeros(data.shape[0])
sub_preds = np.zeros(test.shape[0])

feature_importance_df = pd.DataFrame()

feats = [f for f in data.columns if f not in ['SK_ID_CURR']]

for n_fold, (trn_idx, val_idx) in enumerate(folds.split(data, y)):
    trn_x, trn_y = data[feats].iloc[trn_idx], y.iloc[trn_idx]
    val_x, val_y = data[feats].iloc[val_idx], y.iloc[val_idx]

    clf = LGBMClassifier(
        n_estimators=10000,
        learning_rate=0.02,
        num_leaves=34,
        colsample_bytree=0.9497036,
        subsample=0.8715623,
        max_depth=8,
        reg_alpha=0.041545473,
        reg_lambda=0.0735294,
        min_split_gain=0.0222415,
        min_child_weight=39.3259775,
        silent=-1,
        verbose=-1,
        )

    clf.fit(trn_x, trn_y, 
            eval_set= [(trn_x, trn_y), (val_x, val_y)], 
            eval_metric='auc', 
            verbose=100, 
            early_stopping_rounds=300
           )

    oof_preds[val_idx] = clf.predict_proba(val_x, num_iteration=clf.best_iteration_)[:, 1]
    sub_preds += clf.predict_proba(test[feats], num_iteration=clf.best_iteration_)[:, 1] / folds.n_splits

    fold_importance_df = pd.DataFrame()
    fold_importance_df["feature"] = feats
    fold_importance_df["importance"] = clf.feature_importances_
    fold_importance_df["fold"] = n_fold + 1
    feature_importance_df = pd.concat([feature_importance_df, fold_importance_df], axis=0)

    print('Fold %2d AUC : %.6f' % (n_fold + 1, roc_auc_score(val_y, oof_preds[val_idx])))
    del clf, trn_x, trn_y, val_x, val_y
    gc.collect()


print('Full AUC score %.6f' % roc_auc_score(y, oof_preds)) 

学習結果

これによる結果としては、

  • k-分割交差検証法によるバリデーションデータでの最終的なAUC: 0.7875
  • テストデータに対するScore: 0.782

となりました。この結果は、最初にも書きましたが 、
1,105 位 / 3,020 Terms中 (2018/06/21現在)
に相当する結果です。

このように、かなり思考停止でシンプルに特徴量を生成しただけなのにも関わらず、そこそこのスコアを算出することができました。

メリットとしては特徴量生成についてあまり深く考えずにサクッとモデルを作ってみたいときに使えそうです。
Kaggleのみならず、普段のモデルを作成する際にもかなり使える実践的な手法なのではないでしょうか?

みなさんの参考になれば幸いです。

参考文献

今回の記事の執筆に参考にさせていただいたもの

Kaggle・機械学習など周辺知識について知りたい方向け