Kaggleで使えるFeather形式を利用した特徴量管理法
みなさま、Kaggle楽しんでいますでしょうか。 僕は現在Home Credit Default RiskとSantander Value Prediction Challengeに参加しています。
前回のKaggle記事ではpandasのテクニックについてまとめました。 多くのアクセスをいただき、人生初のホッテントリ入りまで経験してたいそう嬉しかったです。ありがとうございました!
さて。みなさんはKaggleをやっているとき、どのようにして特徴量を管理していますか?
Titanicくらいならその都度計算すれば十分ですが、 ある程度データのサイズが大きくなり、さまざまな特徴量を取捨選択するようになると特徴量のシリアライズ(保存)が欠かせません。
そこで、今回は僕が行っている特徴量管理方法を紹介したいと思います。 僕の方法はTalkingdata Adtracking Fraud Detectionコンペの1位、flowlightさんのリポジトリを参考にしています。
概要
主要なポイントをまとめると以下のとおりです。
目次
Feather形式でのシリアライズ
Featherは読み込みが非常に高速であることが特長の形式です。C++で実装されており、RとPythonのラッパーが提供されています。
Pythonで利用する場合は、
$ pip install -U feather-format
でインストールすることが可能です。
feather-format
をインストールしておけば、pandasのデータフレームに対してdf.to_feather(filepath)
とすることによってFeather形式でのシリアライズが可能です。読み込みの場合はpd.read_feather(filepath)
です。
このFeather形式を利用し、特徴量を意味のあるカタマリごとに分割して保存することを目指します。
基底クラスの実装
同じようなコードを何度も書くことはメンテナンスの都合上、望ましくありません。 特徴量の作成の場合、適切なファイル名をつけ、適切なフォルダに保存する、といったところを何度も書くことになります。
また、KaggleのKernelで公開されているコードのように1つのファイルに大量に特徴量の定義を書いていると、特徴量同士の依存関係が複雑になってきます。
そこで、以下のような基底クラスを継承して特徴量を作成することを考えます。
import re import time from abc import ABCMeta, abstractmethod from pathlib import Path from contextlib import contextmanager import pandas as pd @contextmanager def timer(name): t0 = time.time() print(f'[{name}] start') yield print(f'[{name}] done in {time.time() - t0:.0f} s') class Feature(metaclass=ABCMeta): prefix = '' suffix = '' dir = '.' def __init__(self): self.name = self.__class__.__name__ self.train = pd.DataFrame() self.test = pd.DataFrame() self.train_path = Path(self.dir) / f'{self.name}_train.ftr' self.test_path = Path(self.dir) / f'{self.name}_test.ftr' def run(self): with timer(self.name): self.create_features() prefix = self.prefix + '_' if self.prefix else '' suffix = '_' + self.suffix if self.suffix else '' self.train.columns = prefix + self.train.columns + suffix self.test.columns = prefix + self.test.columns + suffix return self @abstractmethod def create_features(self): raise NotImplementedError def save(self): self.train.to_feather(str(self.train_path)) self.test.to_feather(str(self.test_path))
timer()
これは処理時間を簡単に計測するための関数です。
コンテキストマネージャとして作られているため、with
文で利用することができます。
Featureクラス
特徴量の基底クラス(抽象クラス)です。
このクラスを継承してcreate_features
メソッドを実装すれば利用可能になります。
自身のクラス名から自動でファイル名を生成し、save
メソッドで保存まで行うことができます。
create_features
メソッドの中でself.train
とself.test
に書き込んだ内容が保存されます。
利用例は以下のようになります。(Titanicで、家族の人数を計算する場合です)
class FamilySize(Feature): def create_features(self): self.train['family_size'] = train['SibSp'] + train['Parch'] + 1 self.test['family_size'] = test['SibSp'] + test['Parch'] + 1 FamilySize().run().save()
このように書くことで、FamilySize_train.ftr
とFamilySize_test.ftr
が作成されます。
prefix
やsuffix
を指定することによって、カラム名にprefixやsuffixをつけることができます(ファイル名に、ではありません)
argparseを利用したコマンドラインツール化
特徴量を実装したらワンコマンドで実行できることが望ましいです。 また、既に計算されている特徴量は再度計算したくありません。
そこで、以下のような関数を作成しました。
import argparse import inspect def get_arguments(): parser = argparse.ArgumentParser() parser.add_argument('--force', '-f', action='store_true', help='Overwrite existing files') return parser.parse_args() def get_features(namespace): for k, v in namespace.items(): if inspect.isclass(v) and issubclass(v, Feature) and not inspect.isabstract(v): yield v() def generate_features(namespace, overwrite): for f in get_features(namespace): if f.train_path.exists() and f.test_path.exists() and not overwrite: print(f.name, 'was skipped') else: f.run().save()
get_arguments()
コマンドライン引数を解析する関数です。python hoge.py -f
のように、-f
をつけると上書きモードになります。
get_features()
Feature
を継承したクラスをインスタンス化して返すイテレータです。
generate_features()
namespace
を渡すと、そこに含まれる特徴量が既に保存済かどうか確認して、存在しない場合は計算します。
もしoverwrite
がTrue
ならすべての特徴量を計算し直します。
利用例
例によってKaggleのTitanicで特徴量を作ってみます。
先程のクラスや関数をbase.py
に実装し、特徴量を以下のようにtitanic.py
に実装したとします。
import pandas as pd from .base import Feature, get_arguments, generate_features Feature.dir = 'features' class FamilySize(Feature): def create_features(self): self.train['family_size'] = train['SibSp'] + train['Parch'] + 1 self.test['family_size'] = test['SibSp'] + test['Parch'] + 1 if __name__ == '__main__': args = get_arguments() train = pd.read_csv('input/train.csv') test = pd.read_csv('input/test.csv') generate_features(globals(), args.force)
実行結果は以下のようになり、features
内にFamilySize_train.ftr
とFamilySize_test.ftr
が保存されます。
$ python titanic.py [FamilySize] start [FamilySize] done in 0 s
特徴量の読み込み
出力した特徴量は以下のようにして読み込むことができます。
def load_datasets(feats): dfs = [pd.read_feather(f'features/{f}_train.ftr') for f in feats] X_train = pd.concat(dfs, axis=1) dfs = [pd.read_feather(f'features/{f}_test.ftr') for f in feats] X_test = pd.concat(dfs, axis=1) return X_train, X_test feats = ['FamilySize', 'Hoge', 'Fuga', 'Piyo'] X_train, X_test = load_datasets(feats)
まとめ
- 特徴量をFeather形式でブロックごとに管理する
- コマンドラインツールとしてまとめ、取り回しを楽にする
方法について解説しました。 Kaggleではスクリプトを書き散らかすと特徴量管理が崩壊してしまうため、これくらいの頑張り具合でちょうどよいのではないかと思っています。
実装はGitHubのコンペ用ライブラリ(作成中)に置いてあります。
間違い・改善点などありましたらコメントやTwitterなどでフィードバックをいただけると幸いです。よろしくおねがいします。
ちなみに明日(7/7)は僕の誕生日です。Amazon欲しいものリストを貼っておきますので、こちらも合わせてよろしくおねがいします。
それでは!