天色グラフィティ

機械学習やプログラミングでいろいろ作って遊ぶブログ

Kaggleで使えるFeather形式を利用した特徴量管理法

みなさま、Kaggle楽しんでいますでしょうか。 僕は現在Home Credit Default RiskSantander Value Prediction Challengeに参加しています。

前回のKaggle記事ではpandasのテクニックについてまとめました。 多くのアクセスをいただき、人生初のホッテントリ入りまで経験してたいそう嬉しかったです。ありがとうございました!

amalog.hateblo.jp

さて。みなさんは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.trainself.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.ftrFamilySize_test.ftrが作成されます。

prefixsuffixを指定することによって、カラム名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を渡すと、そこに含まれる特徴量が既に保存済かどうか確認して、存在しない場合は計算します。

もしoverwriteTrueならすべての特徴量を計算し直します。

利用例

例によって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.ftrFamilySize_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のコンペ用ライブラリ(作成中)に置いてあります。

github.com

間違い・改善点などありましたらコメントやTwitterなどでフィードバックをいただけると幸いです。よろしくおねがいします。

ちなみに明日(7/7)は僕の誕生日です。Amazon欲しいものリストを貼っておきますので、こちらも合わせてよろしくおねがいします。

http://amzn.asia/fijawJ1

それでは!