scikit-learn には cross_val_predict()
という関数がある。
この関数は、教師データを k-Fold などで分割したときに OOF (Out of Fold) なデータの目的変数を推論する目的で使われることが多い。
なお、OOF (Out of Fold) というのは、k-Fold などでデータを分割した際に学習に使わなかったデータを指している。
ちなみに、この関数は method
オプションを指定すると fit()
メソッドのあとに呼び出すメソッド名を自由に指定できる。
典型的には、分類問題において predict_proba
を指定することで推論結果を確率として取得するために使うことが多い。
ただ、この method
オプションには predict 系のメソッドに限らず、別に何を指定しても良いことに気づいた。
そこで、今回の記事では cross_val_predict()
関数を使って OOF な特徴量を作ってみることにした。
というのも、Target Encoding と呼ばれる目的変数を利用した特徴量抽出においては OOF することが Leakage を防ぐ上で必須になる。
もし、その作業を cross_val_predict()
関数と scikit-learn の Transformer を組み合わせることでキレイに書けるとしたら、なかなか便利なのではと思った。
今回使った環境は次の通り。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.14.4 BuildVersion: 18E226 $ python -V Python 3.7.3
下準備
まずは pandas と scikit-learn をインストールしておく。
$ pip install pandas scikit-learn
サンプルコード
以下に cross_val_predict()
関数と Transformer を組み合わせて Target Encoding した特徴量を抽出するサンプルコードを示す。
TargetMeanEncodingTransformer
という名前で Target Encoding する Transformer クラスを定義している。
これをそのまま使ってしまうと Target Leakage が起こるため cross_val_predict()
関数と組み合わせることで OOF な特徴量を生成している。
ちなみに cross_val_predict()
関数の返り値は numpy の配列になるため、pandas と組み合わせて使うときはその点に考慮が必要になる。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pandas as pd from sklearn.base import BaseEstimator from sklearn.base import TransformerMixin from sklearn.model_selection import StratifiedKFold from sklearn.model_selection import cross_val_predict class TargetMeanEncodingTransformer(BaseEstimator, TransformerMixin): """Target mean encoding に使う scikit-learn の Transformer NOTE: Target leakage を防ぐために必ず OOF で特徴量を作ること""" def __init__(self, category, target=None): """ :param category: エンコード対象のカラム名 :param target: 目的変数のカラム名 """ self.category_ = category self.target_ = target # 学習したデータを保存する場所 self.target_mean_ = None def fit(self, X, y=None): # エンコード対象の特徴量をカテゴリごとにグループ化する gdf = X.groupby(self.category_) # ターゲットの比率を計算する self.target_mean_ = gdf[self.target_].mean() # 自身を返す return self def transform(self, X, copy=None): # エンコード対象のカラムを取り出す target_series = X[self.category_] # エンコードする transformed = target_series.map(self.target_mean_.to_dict()) # 特徴量を返す return transformed def get_params(self, deep=True): # NOTE: cross_val_predict() は Fold ごとに Estimator を作り直す # そのため get_params() でインスタンスのパラメータを得る必要がある return { 'category': self.category_, 'target': self.target_, } def main(): # 元データ data = [ ('Apple', 1), ('Apple', 1), ('Apple', 0), ('Banana', 1), ('Banana', 0), ('Banana', 0), ('Cherry', 0), ('Cherry', 0), ('Cherry', 0), ] columns = ['category', 'delicious'] df = pd.DataFrame(data, columns=columns) transformer = TargetMeanEncodingTransformer(category='category', target='delicious') # k-Fold しないで適用した場合 (Target leak するのでやっちゃダメ!) encoded_without_kfold = transformer.fit_transform(df) encoded_without_kfold.name = 'target_mean_encoded' print('Without k-Fold') concat_df = pd.concat([df, encoded_without_kfold], axis=1, sort=False) print(concat_df) # k-Fold して適用した場合 (データ点数の関係で実質 LOO になってるので本来はこれもすべきではない) skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=42) # NOTE: numpy の array として結果が返る点に注意する encoded_with_kfold = cross_val_predict(transformer, # NOTE: エンコード対象のカテゴリで層化抽出する df, df.category, cv=skf, # fit() した後に transform() を呼ぶ method='transform') print('With k-Fold') concat_df = pd.concat([df, pd.Series(encoded_with_kfold, name='target_mean_encoded') ], axis=1, sort=False) print(concat_df) if __name__ == '__main__': main()
上記の実行結果は次の通り。 k-Fold ありのパターンでは、ちゃんと OOF で Target Encoding した特徴量が作れているようだ。
$ python oof.py Without k-Fold category delicious target_mean_encoded 0 Apple 1 0.666667 1 Apple 1 0.666667 2 Apple 0 0.666667 3 Banana 1 0.333333 4 Banana 0 0.333333 5 Banana 0 0.333333 6 Cherry 0 0.000000 7 Cherry 0 0.000000 8 Cherry 0 0.000000 With k-Fold category delicious target_mean_encoded 0 Apple 1 0.5 1 Apple 1 0.5 2 Apple 0 1.0 3 Banana 1 0.0 4 Banana 0 0.5 5 Banana 0 0.5 6 Cherry 0 0.0 7 Cherry 0 0.0 8 Cherry 0 0.0
いじょう。
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る