今回は PFN が公開している OSS の xfeat を使った特徴量エンジニアリングについて見ていく。 xfeat には次のような特徴がある。
- 多くの機能が scikit-learn の Transformer 互換の API で提供されている
- 多くの機能が CuPy / CuDF に対応しているため CUDA 環境で高いパフォーマンスが得られる
- 多くの機能がデータフレームを入力としてデータフレームを出力とした API になっている
使った環境は次のとおり。
$ sw_vers ProductName: macOS ProductVersion: 12.2.1 BuildVersion: 21D62 $ uname -srm Darwin 21.3.0 arm64 $ conda -V conda 4.10.3 $ python -V Python 3.9.10 $ pip list | grep xfeat xfeat 0.1.1
もくじ
- もくじ
- 下準備
- 特定の種類の変数を取り出す
- カテゴリ変数のエンコード
- TargetEncoder
- 連続変数のエンコード
- ArithmeticCombinations
- 独自の加工をするエンコーダを作る
- 集約特徴量
- 特徴量選択
- まとめ
下準備
AppleSilicon 版の Mac を使う場合、Python の実行環境に Miniforge を使う。 これは xfeat が依存しているいくつかのパッケージが、まだ pip からインストールできないため。
あらかじめ Miniforge を使って仮想環境を作る。
$ conda create -y -n venv python=3.9 $ conda activate venv
pip からインストールできない依存パッケージの LightGBM と PyArrow をインストールする。 ついでに、今回のサンプルコードで使用する scikit-learn と seaborn も入れておく。
$ conda install -y lightgbm pyarrow scikit-learn seaborn
最後に xfeat をインストールする。 現時点 (2022-02-20)では xfeat が依存しているパッケージの ml_metrics が setuptools v58 以降の環境でインストールできない。 そこで setuptools を v58 未満にダウングレードする必要がある。
$ pip install -U "setuptools<58" $ pip install xfeat
ちなみに、Intel 版の Mac であれば以下だけでインストールできる。
$ pip install -U "setuptools<58" $ pip install xfeat scikit-learn seaborn
インストールが終わったら Python のインタプリタを起動する。
$ python
今回、データセットには seaborn に同梱されている diamonds をサンプルとして用いる。 このデータセットにはカテゴリ変数と連続変数の両方が含まれているので、特徴量エンジニアリングの説明に都合が良い。
>>> import seaborn as sns >>> df = sns.load_dataset('diamonds') >>> df.head() carat cut color clarity depth table price x y z 0 0.23 Ideal E SI2 61.5 55.0 326 3.95 3.98 2.43 1 0.21 Premium E SI1 59.8 61.0 326 3.89 3.84 2.31 2 0.23 Good E VS1 56.9 65.0 327 4.05 4.07 2.31 3 0.29 Premium I VS2 62.4 58.0 334 4.20 4.23 2.63 4 0.31 Good J SI2 63.3 58.0 335 4.34 4.35 2.75 >>> df.dtypes carat float64 cut category color category clarity category depth float64 table float64 price int64 x float64 y float64 z float64 dtype: object
最後に xfeat をインポートしたら準備は終わり。
>>> import xfeat
特定の種類の変数を取り出す
まずはデータフレームから特定の種類の変数だけを取り出す機能から見ていく。
この機能を使うとデータの型に基づいてカテゴリ変数や連続変数のカラムだけを選択できる。
この機能は、後述する Pipeline
の機能と組み合わせると便利に使える。
SelectCategorical
まず、SelectCategorical
を使うとカテゴリ変数だけ取り出すことができる。
>>> select_cat = xfeat.SelectCategorical() >>> select_cat.fit_transform(df) cut color clarity 0 Ideal E SI2 1 Premium E SI1 2 Good E VS1 3 Premium I VS2 4 Good J SI2 ... ... ... ... 53935 Ideal D SI1 53936 Good D SI1 53937 Very Good D SI1 53938 Premium H SI2 53939 Ideal D SI2 [53940 rows x 3 columns]
SelectNumerical
同様に SelectNumerical
を使うと連続変数が取り出せる。
>>> select_num = xfeat.SelectNumerical() >>> select_num.fit_transform(df) carat depth table price x y z 0 0.23 61.5 55.0 326 3.95 3.98 2.43 1 0.21 59.8 61.0 326 3.89 3.84 2.31 2 0.23 56.9 65.0 327 4.05 4.07 2.31 3 0.29 62.4 58.0 334 4.20 4.23 2.63 4 0.31 63.3 58.0 335 4.34 4.35 2.75 ... ... ... ... ... ... ... ... 53935 0.72 60.8 57.0 2757 5.75 5.76 3.50 53936 0.72 63.1 55.0 2757 5.69 5.75 3.61 53937 0.70 62.8 60.0 2757 5.66 5.68 3.56 53938 0.86 61.0 58.0 2757 6.15 6.12 3.74 53939 0.75 62.2 55.0 2757 5.83 5.87 3.64 [53940 rows x 7 columns]
カテゴリ変数のエンコード
次にカテゴリ変数のエンコードに使える機能を見ていこう。
LabelEncoder
LabelEncoder
を使うとラベルエンコードができる。
scikit-learn の sklearn.preprocessing.LabelEncoder
と比べるとデータフレームをそのまま入れられるメリットがある。
デフォルトでは、入力したデータフレームに _le
というサフィックスがついたカラムが追加される 1。
>>> label_encoder = xfeat.LabelEncoder() >>> label_encoder.fit_transform(df[['cut']]) cut cut_le 0 Ideal 0 1 Premium 1 2 Good 2 3 Premium 1 4 Good 2 ... ... ... 53935 Ideal 0 53936 Good 2 53937 Very Good 3 53938 Premium 1 53939 Ideal 0 [53940 rows x 2 columns]
元のカラムがいらないときは、output_suffix
オプションに空文字を入れると上書きできる。
>>> label_encoder = xfeat.LabelEncoder(output_suffix='') >>> label_encoder.fit_transform(df[['cut']]) cut 0 0 1 1 2 2 3 1 4 2 ... ... 53935 0 53936 2 53937 3 53938 1 53939 0 [53940 rows x 1 columns]
また、複数のカラムが含まれるデータフレームを渡せば、複数のカテゴリ変数を一度にエンコードできる。
>>> label_encoder.fit_transform(df[['cut', 'color', 'clarity']]) cut color clarity 0 0 0 0 1 1 0 1 2 2 0 2 3 1 1 3 4 2 2 0 ... ... ... ... 53935 0 6 1 53936 2 6 1 53937 3 6 1 53938 1 3 0 53939 0 6 0 [53940 rows x 3 columns]
この点は、前述した SelectCategorical
と Pipeline
の機能を組み合わせると上手く動作してくれる。
>>> pipe = xfeat.Pipeline([ ... xfeat.SelectCategorical(), ... xfeat.LabelEncoder(output_suffix=''), ... ]) >>> pipe.fit_transform(df) cut color clarity 0 0 0 0 1 1 0 1 2 2 0 2 3 1 1 3 4 2 2 0 ... ... ... ... 53935 0 6 1 53936 2 6 1 53937 3 6 1 53938 1 3 0 53939 0 6 0 [53940 rows x 3 columns]
未知のデータが含まれていた際の振る舞いは、デフォルトでは -1
が入る。
>>> label_encoder = xfeat.LabelEncoder() >>> label_encoder.fit(df[['cut']]) >>> data = { ... 'cut': ['Ideal', 'Premium', 'Very Good', 'Good', 'Fair', 'Unknown', 'Unseen'], ... } >>> import pandas as pd >>> new_df = pd.DataFrame(data) >>> label_encoder.transform(new_df) cut cut_le 0 Ideal 0 1 Premium 1 2 Very Good 3 3 Good 2 4 Fair 4 5 Unknown -1 6 Unseen -1
この振る舞いは unseen
オプションを使って変更できる。
たとえば n_unique
を指定すると、これまでに出現したラベルの値から連続した値が割り当てられる。
>>> label_encoder = xfeat.LabelEncoder(unseen='n_unique') >>> label_encoder.fit(df[['cut']]) >>> label_encoder.transform(new_df) cut cut_le 0 Ideal 0 1 Premium 1 2 Very Good 3 3 Good 2 4 Fair 4 5 Unknown 5 6 Unseen 5
CountEncoder
同様に CountEncoder
を使うとカウントエンコードができる。
これは同じ値がデータの中にいくつ含まれるかを特徴量として生成する。
>>> count_encoder = xfeat.CountEncoder() >>> count_encoder.fit_transform(df[['cut']]) cut cut_ce 0 Ideal 21551 1 Premium 13791 2 Good 4906 3 Premium 13791 4 Good 4906 ... ... ... 53935 Ideal 21551 53936 Good 4906 53937 Very Good 12082 53938 Premium 13791 53939 Ideal 21551 [53940 rows x 2 columns]
ConcatCombination
ConcatCombination
では、変数の値を連結することで新しいカテゴリ変数を作り出せる。
drop_origin
オプションを True
に指定すると、元の特徴量を落としたデータフレームになる。
また、組み合わせる数は r
オプションで指定する。
>>> concat_combi = xfeat.ConcatCombination(drop_origin=True, r=2) >>> concat_combi.fit_transform(df[['cut', 'color', 'clarity']].astype(str)) cutcolor_combi cutclarity_combi colorclarity_combi 0 IdealE IdealSI2 ESI2 1 PremiumE PremiumSI1 ESI1 2 GoodE GoodVS1 EVS1 3 PremiumI PremiumVS2 IVS2 4 GoodJ GoodSI2 JSI2 ... ... ... ... 53935 IdealD IdealSI1 DSI1 53936 GoodD GoodSI1 DSI1 53937 Very GoodD Very GoodSI1 DSI1 53938 PremiumH PremiumSI2 HSI2 53939 IdealD IdealSI2 DSI2 [53940 rows x 3 columns]
上記でカラムの型を str
にキャストしているのは category
型のままだと例外になるため。
>>> concat_combi.fit_transform(df[['cut', 'color', 'clarity']]) Traceback (most recent call last): File "<stdin>", line 1, in <module> ... raise TypeError( TypeError: Cannot setitem on a Categorical with a new category (_NaN_), set the categories first
TargetEncoder
TargetEncoder
を使うとターゲットエンコードができる。
データの分割方法は fold
というオプションで指定できる。
>>> from sklearn.model_selection import KFold >>> folds = KFold(n_splits=2, shuffle=False) >>> target_encoder = xfeat.TargetEncoder(fold=folds, target_col='price') >>> target_encoder.fit_transform(df[['cut', 'price']]) cut price cut_te 0 Ideal 326 1561.627930 1 Premium 326 1949.366089 2 Good 327 1773.722046 3 Premium 334 1949.366089 4 Good 335 1773.722046 ... ... ... ... 53935 Ideal 2757 6113.638184 53936 Good 2757 5552.538574 53937 Very Good 2757 5893.533203 53938 Premium 2757 6663.661621 53939 Ideal 2757 6113.638184 [53940 rows x 3 columns]
なお、Target Encoding の詳細は下記のエントリに書いたことがある。
連続変数のエンコード
続いて連続変数のエンコードについて見ていく。
ArithmeticCombinations
ArithmeticCombinations
を使うと、複数のカラムを四則演算するといった特徴量が計算できる。
たとえば、以下では 2 つのカラムを足し合わせた特徴量を生成している。
>>> add_combi = xfeat.ArithmeticCombinations(drop_origin=True, operator='+', r=2) >>> add_combi.fit_transform(df[['x', 'y', 'z']]) xy_combi xz_combi yz_combi 0 7.93 6.38 6.41 1 7.73 6.20 6.15 2 8.12 6.36 6.38 3 8.43 6.83 6.86 4 8.69 7.09 7.10 ... ... ... ... 53935 11.51 9.25 9.26 53936 11.44 9.30 9.36 53937 11.34 9.22 9.24 53938 12.27 9.89 9.86 53939 11.70 9.47 9.51 [53940 rows x 3 columns]
独自の加工をするエンコーダを作る
その他、LambdaEncoder
を使うことで、自分で加工方法を定義したエンコーダを指定することもできる。
以下では例として値を 2 倍するエンコーダを作っている。
>>> double_encoder = xfeat.LambdaEncoder(lambda x: x * 2, drop_origin=False, output_suffix='_double') >>> double_encoder.fit_transform(df[['x', 'y']]) x y x_double y_double 0 3.95 3.98 7.90 7.96 1 3.89 3.84 7.78 7.68 2 4.05 4.07 8.10 8.14 3 4.20 4.23 8.40 8.46 4 4.34 4.35 8.68 8.70 ... ... ... ... ... 53935 5.75 5.76 11.50 11.52 53936 5.69 5.75 11.38 11.50 53937 5.66 5.68 11.32 11.36 53938 6.15 6.12 12.30 12.24 53939 5.83 5.87 11.66 11.74 [53940 rows x 4 columns]
集約特徴量
特定のカラムの値を Group By のキーにして要約統計量を計算するような特徴量は aggregation()
関数を使って計算できる。
この API は scikit-learn の Transformer 互換になっていない点に注意が必要。
以下では cut
をキーにして、x, y, z
の値にいくつかの統計量を計算している。
>>> df_agg, agg_cols = xfeat.aggregation(df, ... group_key='cut', ... group_values=['x', 'y', 'z'], ... agg_methods=['sum', 'min', 'max', 'mean', 'median'], ... )
結果はタプルで得られる。 最初の要素にはデータフレームが入っている。
>>> df_agg carat cut color clarity depth table ... agg_mean_x_grpby_cut agg_mean_y_grpby_cut agg_mean_z_grpby_cut agg_median_x_grpby_cut agg_median_y_grpby_cut agg_median_z_grpby_cut 0 0.23 Ideal E SI2 61.5 55.0 ... 5.507451 5.520080 3.401448 5.25 5.26 3.23 1 0.21 Premium E SI1 59.8 61.0 ... 5.973887 5.944879 3.647124 6.11 6.06 3.72 2 0.23 Good E VS1 56.9 65.0 ... 5.838785 5.850744 3.639507 5.98 5.99 3.70 3 0.29 Premium I VS2 62.4 58.0 ... 5.973887 5.944879 3.647124 6.11 6.06 3.72 4 0.31 Good J SI2 63.3 58.0 ... 5.838785 5.850744 3.639507 5.98 5.99 3.70 ... ... ... ... ... ... ... ... ... ... ... ... ... ... 53935 0.72 Ideal D SI1 60.8 57.0 ... 5.507451 5.520080 3.401448 5.25 5.26 3.23 53936 0.72 Good D SI1 63.1 55.0 ... 5.838785 5.850744 3.639507 5.98 5.99 3.70 53937 0.70 Very Good D SI1 62.8 60.0 ... 5.740696 5.770026 3.559801 5.74 5.77 3.56 53938 0.86 Premium H SI2 61.0 58.0 ... 5.973887 5.944879 3.647124 6.11 6.06 3.72 53939 0.75 Ideal D SI2 62.2 55.0 ... 5.507451 5.520080 3.401448 5.25 5.26 3.23 [53940 rows x 25 columns]
タプルの二番目の要素には、生成されたカラム名の入ったリストが入っている。
>>> from pprint import pprint >>> pprint(agg_cols) ['agg_sum_x_grpby_cut', 'agg_sum_y_grpby_cut', 'agg_sum_z_grpby_cut', 'agg_min_x_grpby_cut', 'agg_min_y_grpby_cut', 'agg_min_z_grpby_cut', 'agg_max_x_grpby_cut', 'agg_max_y_grpby_cut', 'agg_max_z_grpby_cut', 'agg_mean_x_grpby_cut', 'agg_mean_y_grpby_cut', 'agg_mean_z_grpby_cut', 'agg_median_x_grpby_cut', 'agg_median_y_grpby_cut', 'agg_median_z_grpby_cut']
特徴量選択
ここまでは、主に特徴量エンジニアリングの中でも特徴量抽出 (Feature Extraction) の機能を見てきた。 ここからは特徴量選択 (Feature Selection) の機能を見ていく。
DuplicatedFeatureEliminator
DuplicatedFeatureEliminator
を使うと、重複した特徴量を削除できる。
たとえば、次のようにまったく同じ値の入ったカラムが x
と x2
として含まれるデータフレームがあるとする。
>>> new_df = df[['x']].copy() >>> new_df['x2'] = df['x'] >>> new_df x x2 0 3.95 3.95 1 3.89 3.89 2 4.05 4.05 3 4.20 4.20 4 4.34 4.34 ... ... ... 53935 5.75 5.75 53936 5.69 5.69 53937 5.66 5.66 53938 6.15 6.15 53939 5.83 5.83 [53940 rows x 2 columns]
重複した特徴量は、どちらかさえあれば予測には十分なはず。
DuplicatedFeatureEliminator
を使うと、片方だけ残して特徴量を削除できる。
>>> dup_eliminator = xfeat.DuplicatedFeatureEliminator() >>> dup_eliminator.fit_transform(new_df) x 0 3.95 1 3.89 2 4.05 3 4.20 4 4.34 ... ... 53935 5.75 53936 5.69 53937 5.66 53938 6.15 53939 5.83 [53940 rows x 1 columns]
ConstantFeatureEliminator
同様に ConstantFeatureEliminator
を使うと定数になっている特徴量を削除できる。
たとえば、すべての値が 1
になっている a
というカラムの入ったデータフレームを用意する。
>>> new_df = df[['x']].copy() >>> new_df['a'] = 1 >>> new_df x a 0 3.95 1 1 3.89 1 2 4.05 1 3 4.20 1 4 4.34 1 ... ... .. 53935 5.75 1 53936 5.69 1 53937 5.66 1 53938 6.15 1 53939 5.83 1 [53940 rows x 2 columns]
分散のない特徴量は予測に寄与しないはず。
DuplicatedFeatureEliminator
を使うと、そのような特徴量を削除できる。
>>> const_eliminator = xfeat.ConstantFeatureEliminator() >>> const_eliminator.fit_transform(new_df) x 0 3.95 1 3.89 2 4.05 3 4.20 4 4.34 ... ... 53935 5.75 53936 5.69 53937 5.66 53938 6.15 53939 5.83 [53940 rows x 1 columns]
SpearmanCorrelationEliminator
SpearmanCorrelationEliminator
を使うと、高い相関を持った特徴量を削除できる。
たとえば、あるカラムに定数を加えただけのカラムを含んだデータフレームを用意する。
>>> new_df = df[['x']].copy() >>> new_df['x2'] = df['x'] + 0.1 >>> new_df x x2 0 3.95 4.05 1 3.89 3.99 2 4.05 4.15 3 4.20 4.30 4 4.34 4.44 ... ... ... 53935 5.75 5.85 53936 5.69 5.79 53937 5.66 5.76 53938 6.15 6.25 53939 5.83 5.93 [53940 rows x 2 columns]
上記の特徴量は相関係数が 1.0 になっている。
>>> new_df.corr() x x2 x 1.0 1.0 x2 1.0 1.0
極端に相関係数の高い特徴量も、予測においては片方があれば十分と考えられる。
SpearmanCorrelationEliminator
を使うと片方だけ残して削除できる。
>>> corr_eliminator = xfeat.SpearmanCorrelationEliminator() >>> corr_eliminator.fit_transform(new_df) x 0 3.95 1 3.89 2 4.05 3 4.20 4 4.34 ... ... 53935 5.75 53936 5.69 53937 5.66 53938 6.15 53939 5.83 [53940 rows x 1 columns]
GBDTFeatureSelector
GBDTFeatureSelector
を使うと GBDT (Gradient Boosting Decision Tree) を用いて、特徴量の重要度に基づいた特徴量選択ができる。
なお、ここでいう GBDT としては LightGBM が使われている。
まず、LightGBM はカテゴリ変数をそのままだと受け付けないので、一旦ラベルエンコードしておく。
>>> pipe = xfeat.Pipeline([ ... xfeat.LabelEncoder(input_cols=['cut', 'color', 'clarity'], ... output_suffix=''), ... ]) >>> df = pipe.fit_transform(df)
ここでは threshold
オプションに 0.5
を指定することで、重要と考えられる特徴量を 50% 残してみよう。
この値はハイパーパラメータなので、実際にはいくつかの値を試して予測精度や計算量のバランスを取っていく必要がある
>>> lgbm_params = { ... 'objective': 'regression', ... 'metric': 'rmse', ... 'verbosity': -1, ... } >>> lgbm_fit_params = { ... 'num_boost_round': 1_000, ... } >>> gbdt_selector = xfeat.GBDTFeatureSelector(target_col='price', ... threshold=0.5, ... lgbm_params=lgbm_params, ... lgbm_fit_kwargs=lgbm_fit_params, ... ) >>> selected_df = gbdt_selector.fit_transform(df)
上記を実行すると carat, y, depth, z
という 4 つのカラムが選ばれた。
これらが、GBDT で予測する場合には重要となる特徴量の上位 50% ということ。
>>> selected_df carat y depth z 0 0.23 3.98 61.5 2.43 1 0.21 3.84 59.8 2.31 2 0.23 4.07 56.9 2.31 3 0.29 4.23 62.4 2.63 4 0.31 4.35 63.3 2.75 ... ... ... ... ... 53935 0.72 5.76 60.8 3.50 53936 0.72 5.75 63.1 3.61 53937 0.70 5.68 62.8 3.56 53938 0.86 6.12 61.0 3.74 53939 0.75 5.87 62.2 3.64 [53940 rows x 4 columns]
GBDTFeatureExplorer
GBDTFeatureSelector
を使う場合、残す特徴量の割合を threshold
オプションとして自分で指定する必要があった。
予測精度が最も高いものが欲しい場合であれば、GBDTFeatureExplorer
を使うことで自動で探索させることもできる。
この機能は少し複雑なのでスクリプトにした。
以下にサンプルコードを示す。
以下では diamonds データセットを使って、price
カラムを目的変数に RMSE のメトリックで回帰問題として解いている。
最初に xfeat を使って特徴量抽出をしており、機械的に 265 次元まで増やしている。
そして、予測精度が最も良くなるように GBDTFeatureExplorer
を使って特徴量選択している。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- from functools import partial import pandas as pd import numpy as np import seaborn as sns import xfeat import lightgbm as lgb import optuna from sklearn.metrics import mean_squared_error from sklearn.model_selection import KFold from sklearn.model_selection import train_test_split def _lgbm_cv(train_x, train_y): """LightGBM を使った交差検証のヘルパー関数""" lgbm_params = { 'objective': 'regression', 'metric': 'rmse', 'verbosity': -1, } train_dataset = lgb.Dataset(data=train_x, label=train_y) folds = KFold(n_splits=5, shuffle=True, random_state=42) cv_result = lgb.cv(lgbm_params, train_dataset, num_boost_round=1_000, folds=folds, return_cvbooster=True, ) return cv_result def _rmse(y_true, y_pred): """RMSE を計算するヘルパー関数""" mse = mean_squared_error(y_true, y_pred) rmse = np.sqrt(mse) return rmse def _evaluate(train_x, train_y, test_x, test_y): """学習用データの CV とテストデータの誤差を確認するヘルパー関数""" cv_result = _lgbm_cv(train_x, train_y) cv_rmse_mean = cv_result['rmse-mean'][-1] print(f'CV RMSE: {cv_rmse_mean}') cvbooster = cv_result['cvbooster'] y_preds = cvbooster.predict(test_x) y_pred = np.mean(y_preds, axis=0) test_rmse = _rmse(y_pred, test_y) print(f'Test RMSE: {test_rmse}') def objective(df, selector, trial): """Optuna の目的関数""" # 次に試行する特徴量の組み合わせを得る selector.set_trial(trial) selector.fit(df) input_cols = selector.get_selected_cols() # 選択された特徴量から得られる Local CV のスコアを計算する train_x = df[input_cols].drop(['price'], axis=1) train_y = df['price'] cv_result = _lgbm_cv(train_x, train_y) # スコアの平均を返す mean_score = cv_result['rmse-mean'][-1] return mean_score def main(): # データセットを読み込む df = sns.load_dataset('diamonds') # ベースとなるカラム毎の変数の種類 categorical_cols = ['cut', 'color', 'clarity'] numerical_cols = ['carat', 'depth', 'table', 'x', 'y', 'z'] target_col = 'price' # fillna の問題があるので str にキャストする df = df.astype({ cat_col: str for cat_col in categorical_cols }) # カテゴリ変数の前処理 pipe = xfeat.Pipeline([ # カテゴリ同士の組み合わせ xfeat.ConcatCombination(r=2), # ラベルエンコード xfeat.LabelEncoder(output_suffix=''), ]) cat_df = pipe.fit_transform(df[categorical_cols]) # カテゴリ変数のリストを更新する categorical_cols = cat_df.columns.tolist() # 元のデータフレームと結合する df = pd.concat([cat_df, df[numerical_cols + [target_col]]], axis=1) print(f'add combination features: {len(df.columns)}') # カテゴリ変数を中心にした集約特徴量 for cat_col in categorical_cols: df, _ = xfeat.aggregation(df, group_key=cat_col, group_values=numerical_cols, agg_methods=[ 'sum', 'min', 'max', 'mean', 'median', ], ) print(f'add aggregation features: {len(df.columns)}') # 最終的な評価をするためにデータをホールドアウトしておく train_df, test_df = train_test_split(df, test_size=0.35, shuffle=True, random_state=42) folds = KFold(n_splits=5, shuffle=True, random_state=42) pipe = xfeat.Pipeline([ # カウントエンコード xfeat.CountEncoder(input_cols=categorical_cols), # ターゲットエンコード xfeat.TargetEncoder(input_cols=categorical_cols, target_col=target_col, fold=folds), # 組み合わせ特徴量 xfeat.ArithmeticCombinations(input_cols=numerical_cols, operator='+', r=2, output_suffix='_plus'), xfeat.ArithmeticCombinations(input_cols=numerical_cols, operator='*', r=2, output_suffix='_mul'), xfeat.ArithmeticCombinations(input_cols=numerical_cols, operator='-', r=2, output_suffix='_minus'), xfeat.ArithmeticCombinations(input_cols=numerical_cols, operator='/', r=2, output_suffix='_div'), ]) train_df = pipe.fit_transform(train_df) test_df = pipe.transform(test_df) print(f'add some features: {len(train_df.columns)}') # 選択前のスコアを計算しておく train_x, train_y = train_df.drop(target_col, axis=1), train_df[target_col] test_x, test_y = test_df.drop(target_col, axis=1), test_df[target_col] _evaluate(train_x, train_y, test_x, test_y) # 学習用データセットを使って特徴量選択をする lgbm_params = { 'objective': 'regression', 'metric': 'rmse', 'verbosity': -1, } fit_params = { 'num_boost_round': 1_000, } selector = xfeat.GBDTFeatureExplorer(input_cols=train_df.columns.tolist(), target_col=target_col, fit_once=True, threshold_range=(0.1, 1.0), lgbm_params=lgbm_params, lgbm_fit_kwargs=fit_params, ) # メトリックのスコアが良くなる特徴量の組み合わせを探索する study = optuna.create_study(direction='minimize') # 最適化する study.optimize(partial(objective, train_df, selector), n_trials=10, ) # 探索で見つかった最善の組み合わせを取り出す selector.from_trial(study.best_trial) selected_cols = selector.get_selected_cols() # 特徴量の数をどれだけ減らせたか print(f'selected features: {len(selected_cols)}') # 選択後のスコアを計算する train_x = train_df[selected_cols].drop(target_col, axis=1) test_x = test_df[selected_cols].drop(target_col, axis=1) _evaluate(train_x, train_y, test_x, test_y) if __name__ == '__main__': main()
上記を実行してみよう。
$ python example.py add combination features: 13 add aggregation features: 193 add some features: 265 CV RMSE: 544.5696268027966 Test RMSE: 515.6280330475462 [I 2022-02-20 18:44:37,774] A new study created in memory with name: no-name-5d80802a-6e8c-48b1-bf1e-f2843ca3eadd [I 2022-02-20 18:45:11,947] Trial 0 finished with value: 545.1447126832893 and parameters: {'GBDTFeatureSelector.threshold': 0.8839849802341044}. Best is trial 0 with value: 545.1447126832893. [I 2022-02-20 18:45:39,267] Trial 1 finished with value: 546.669714703891 and parameters: {'GBDTFeatureSelector.threshold': 0.7619871719011998}. Best is trial 0 with value: 545.1447126832893. [I 2022-02-20 18:45:56,204] Trial 2 finished with value: 539.8527700066101 and parameters: {'GBDTFeatureSelector.threshold': 0.4415951458583348}. Best is trial 2 with value: 539.8527700066101. [I 2022-02-20 18:46:22,874] Trial 3 finished with value: 544.7853629837771 and parameters: {'GBDTFeatureSelector.threshold': 0.7281870389232026}. Best is trial 2 with value: 539.8527700066101. [I 2022-02-20 18:46:33,338] Trial 4 finished with value: 614.75354170118 and parameters: {'GBDTFeatureSelector.threshold': 0.16941573919931688}. Best is trial 2 with value: 539.8527700066101. [I 2022-02-20 18:46:47,241] Trial 5 finished with value: 558.923413959607 and parameters: {'GBDTFeatureSelector.threshold': 0.26183305989448025}. Best is trial 2 with value: 539.8527700066101. [I 2022-02-20 18:47:09,066] Trial 6 finished with value: 541.8006025367876 and parameters: {'GBDTFeatureSelector.threshold': 0.5902889999794974}. Best is trial 2 with value: 539.8527700066101. [I 2022-02-20 18:47:22,243] Trial 7 finished with value: 545.7942530448582 and parameters: {'GBDTFeatureSelector.threshold': 0.30621508734500646}. Best is trial 2 with value: 539.8527700066101. [I 2022-02-20 18:47:35,899] Trial 8 finished with value: 543.8972261792727 and parameters: {'GBDTFeatureSelector.threshold': 0.344652063624768}. Best is trial 2 with value: 539.8527700066101. [I 2022-02-20 18:48:02,356] Trial 9 finished with value: 546.669714703891 and parameters: {'GBDTFeatureSelector.threshold': 0.8269956186739581}. Best is trial 2 with value: 539.8527700066101. selected features: 219 CV RMSE: 546.669714703891 Test RMSE: 515.1565138588921
上記では、元の 265 次元から 219 次元まで特徴量が削減されている。 特徴量が削減されると、計算量が削減できることから一回の実験にかかる時間も減らすことができる。 一方で、予測精度についてはホールドアウトしたテストデータに対してはほとんど変化していない。 この結果は、GBDT の場合は予測にあまり寄与しない特徴量が含まれていても、さほど予測性能に悪影響を及ぼさないという経験則と一致している。
まとめ
今回は xfeat を使った特徴量エンジニアリングのやり方について見てきた。
-
内部的にデータフレームはコピーされるため元のデータフレームが変更されるわけではない↩