今回は scikit-learn の FeatureUnion を pandas の DataFrame を一緒に使うときの問題点とその解決策について。 scikit-learn の FeatureUnion は、典型的には Pipeline においてバラバラに作った複数の特徴量を一つにまとめるのに使われる機能。 この FeatureUnion を pandas の DataFrame と一緒に使おうとすると、ちょっとばかり予想外の挙動になる。 具体的には FeatureUnion の出力するデータが、本来なら DataFrame になってほしいところで numpy の ndarray 形式に変換されてしまう。 今回は、それをなんとか DataFrame に直す方法がないか調べたり模索してみた話。
使った環境は次の通り。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.13.6 BuildVersion: 17G65 $ python -V Python 3.7.0 $ pip list --format=columns | egrep -i "(scikit-learn|pandas)" pandas 0.23.4 scikit-learn 0.20.0
下準備
まずは今回使うパッケージをインストールしておく。
$ pip install scikit-learn pandas
FeatureUnion について
まずは前提知識として FeatureUnion について紹介する。 これは Pipeline と一緒に使うことで真価を発揮する機能で、複数の特徴量を一つにまとめ上げるのに使われる。
次のサンプルコードでは Iris データセットを題材にして Pipeline と FeatureUnion を使っている。 この中では、元々 Iris データセットに含まれていた特徴量と、それらを標準化した特徴量を並列に準備して結合している。 動作確認のために Pipeline への入力データと出力データの情報を標準出力に書き出している。
#!/usr/bin/env python # -*- coding: utf-8 -*- from sklearn import datasets from sklearn.pipeline import FeatureUnion from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import FunctionTransformer def main(): # Iris データセットを読み込む dataset = datasets.load_iris() X, y = dataset.data, dataset.target # パイプラインの入力データ print('input:') print(type(X), X.shape) print(X[:5]) steps = [ # 複数の特徴量を結合する ('union', FeatureUnion([ # 生の特徴量をそのまま返す ('raw', FunctionTransformer(lambda x: x, validate=False)), # 標準化した特徴量を返す ('scaler', StandardScaler()), ])), ] pipeline = Pipeline(steps=steps) transformed_data = pipeline.fit_transform(X) # パイプラインの出力データ print('output:') print(type(transformed_data), transformed_data.shape) print(transformed_data[:5]) if __name__ == '__main__': main()
論より証拠ということで上記を実行してみよう。 すると、入力時点では 4 次元 150 行だったデータが、出力時点では 8 次元 150 行になっている。 これは、元々 Iris データセットに含まれていた 4 次元の特徴量に加えて、それらを標準化した特徴量が 4 次元加わっているため。
$ python featunion.py input: <class 'numpy.ndarray'> (150, 4) [[5.1 3.5 1.4 0.2] [4.9 3. 1.4 0.2] [4.7 3.2 1.3 0.2] [4.6 3.1 1.5 0.2] [5. 3.6 1.4 0.2]] output: <class 'numpy.ndarray'> (150, 8) [[ 5.1 3.5 1.4 0.2 -0.90068117 1.01900435 -1.34022653 -1.3154443 ] [ 4.9 3. 1.4 0.2 -1.14301691 -0.13197948 -1.34022653 -1.3154443 ] [ 4.7 3.2 1.3 0.2 -1.38535265 0.32841405 -1.39706395 -1.3154443 ] [ 4.6 3.1 1.5 0.2 -1.50652052 0.09821729 -1.2833891 -1.3154443 ] [ 5. 3.6 1.4 0.2 -1.02184904 1.24920112 -1.34022653 -1.3154443 ]]
このように FeatureUnion を使うと特徴量エンジニアリングの作業が簡潔に表現できる。
pandas の DataFrame と一緒に使うときの問題点について
ただ、この FeatureUnion を pandas の DataFrame と一緒に使おうとすると問題が出てくる。 具体的には FeatureUnion を通すと、入力が DataFrame でも出力が numpy の ndarray の変換されてしまうのだ。
次のサンプルコードを見てほしい。 このコードでは Pipeline の入力データを pandas の DataFrame に変換している。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pandas as pd from sklearn import datasets from sklearn.pipeline import Pipeline from sklearn.pipeline import FeatureUnion from sklearn.preprocessing import FunctionTransformer def main(): # Iris データセットを読み込む dataset = datasets.load_iris() X, y = dataset.data, dataset.target # データセットを pandas の DataFrame に変換する X_df = pd.DataFrame(X, columns=dataset.feature_names) # パイプラインの入力データ print('input:') print(type(X_df)) print(X_df.head()) steps = [ ('union', FeatureUnion([ ('raw', FunctionTransformer(lambda x: x, validate=False)), ])), ] pipeline = Pipeline(steps=steps) transformed_data = pipeline.fit_transform(X_df) # パイプラインの出力データ print('output:') print(type(transformed_data)) print(transformed_data[:5]) if __name__ == '__main__': main()
上記を実行してみよう。 すると Pipeline の出力データが numpy の ndarray に変換されてしまっていることが分かる。
$ python dfissue.py input: <class 'pandas.core.frame.DataFrame'> sepal length (cm) ... petal width (cm) 0 5.1 ... 0.2 1 4.9 ... 0.2 2 4.7 ... 0.2 3 4.6 ... 0.2 4 5.0 ... 0.2 [5 rows x 4 columns] output: <class 'numpy.ndarray'> [[5.1 3.5 1.4 0.2] [4.9 3. 1.4 0.2] [4.7 3.2 1.3 0.2] [4.6 3.1 1.5 0.2] [5. 3.6 1.4 0.2]]
上記のような振る舞いになる原因は scikit-learn の以下のコードにある。 FeatureUnion では特徴量の結合に numpy.hstack() を使っているのだ。 この関数の返り値は numpy の ndarray になるので FeatureUnion の出力もそれになってしまう。
ColumnTransformer でも同じ問題が起こる
ちなみに FeatureUnion と似たような機能の ColumnTransformer を使っても同じ問題が起こる。 ColumnTransformer は処理対象のカラム (次元) を指定して特徴量が生成できる。
#!/usr/bin/env python # -*- coding: utf-8 -*- import pandas as pd from sklearn import datasets from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.preprocessing import FunctionTransformer def main(): # Iris データセットを読み込む dataset = datasets.load_iris() X, y = dataset.data, dataset.target # データセットを pandas の DataFrame に変換する X_df = pd.DataFrame(X, columns=dataset.feature_names) # パイプラインの入力データ print('input:') print(type(X_df)) print(X_df.head()) steps = [ ('union', ColumnTransformer([ ('transparent', FunctionTransformer(lambda x: x, validate=False), [i for i, _ in enumerate(X_df)]), ])), ] pipeline = Pipeline(steps=steps) transformed_data = pipeline.fit_transform(X_df) # パイプラインの出力データ print('output:') print(type(transformed_data)) print(transformed_data[:5]) if __name__ == '__main__': main()
上記を実行してみよう。 FeatureUnion のときと同じように出力データは numpy の ndarray 形式に変換されてしまった。
$ python columnissue.py input: <class 'pandas.core.frame.DataFrame'> sepal length (cm) ... petal width (cm) 0 5.1 ... 0.2 1 4.9 ... 0.2 2 4.7 ... 0.2 3 4.6 ... 0.2 4 5.0 ... 0.2 [5 rows x 4 columns] output: <class 'numpy.ndarray'> [[5.1 3.5 1.4 0.2] [4.9 3. 1.4 0.2] [4.7 3.2 1.3 0.2] [4.6 3.1 1.5 0.2] [5. 3.6 1.4 0.2]]
解決方法について
この問題について調べると、同じように悩んでいる人たちがいる。 そして、例えば次のような解決策を提示している人がいた。
上記では、自分で Transformer と FeatureUnion を拡張することで対応している。 ただ、このやり方だと作業が大掛かりで scikit-learn の API 変更などに継続的に追随していく必要があると感じた。
そこで、別の解決策として「とてもダーティーなハック」を思いついてしまったので共有してみる。 この問題は、特徴量の結合に numpy の hstack() 関数を使っていることが原因だった。 だとすれば、この関数をモンキーパッチで一時的にすり替えてしまえば問題を回避できるのではないか?と考えた。
実際に試してみたのが次のサンプルコードになる。
このコードでは unittest.mock.patch()
を使って、一時的に numpy.hstack()
を pd.concat()
にすり替えている。
これなら出力データは pandas の DataFrame にできるのではないか?
#!/usr/bin/env python # -*- coding: utf-8 -*- from functools import partial from unittest.mock import patch import pandas as pd from sklearn import datasets from sklearn.pipeline import Pipeline from sklearn.pipeline import FeatureUnion from sklearn.preprocessing import FunctionTransformer def main(): # Iris データセットを読み込む dataset = datasets.load_iris() X, y = dataset.data, dataset.target # データセットを pandas の DataFrame に変換する X_df = pd.DataFrame(X, columns=dataset.feature_names) # パイプラインの入力データ print('input:') print(type(X_df)) print(X_df.head()) steps = [ ('union', FeatureUnion([ ('transparent', FunctionTransformer(lambda x: x, validate=False)), ])), ] pipeline = Pipeline(steps=steps) # numpy.hstack を一時的に pd.concat(axis=1) にパッチしてしまう horizontal_concat = partial(pd.concat, axis=1) with patch('numpy.hstack', side_effect=horizontal_concat): transformed_data = pipeline.fit_transform(X_df) print('output:') print(type(transformed_data)) print(transformed_data.head()) if __name__ == '__main__': main()
上記を実行してみよう。 すると、見事に出力データが pandas の DataFrame になっていることが分かる。
$ python monkeypatch.py input: <class 'pandas.core.frame.DataFrame'> sepal length (cm) ... petal width (cm) 0 5.1 ... 0.2 1 4.9 ... 0.2 2 4.7 ... 0.2 3 4.6 ... 0.2 4 5.0 ... 0.2 [5 rows x 4 columns] output: <class 'pandas.core.frame.DataFrame'> sepal length (cm) ... petal width (cm) 0 5.1 ... 0.2 1 4.9 ... 0.2 2 4.7 ... 0.2 3 4.6 ... 0.2 4 5.0 ... 0.2 [5 rows x 4 columns]
この解決策の問題点としては、学習の過程において numpy.hstack()
が使えなくなるところ。
pd.concat()
は入力として pandas のオブジェクトしか渡すことができない。
もし numpy の ndarray を渡してしまうと、次のようにエラーになる。
>>> import numpy as n >>> l1 = np.array([1, 2, 3]) >>> l2 = np.array([4, 5, 6]) >>> import pandas as pd >>> pd.concat([l1, l2], axis=1) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/pandas/core/reshape/concat.py", line 225, in concat copy=copy, sort=sort) File "/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/pandas/core/reshape/concat.py", line 286, in __init__ raise TypeError(msg) TypeError: cannot concatenate object of type "<class 'numpy.ndarray'>"; only pd.Series, pd.DataFrame, and pd.Panel (deprecated) objs are valid
巨大なパイプラインであれば numpy.hstack()
をどこかで使っている可能性は十分に考えられる。
とはいえ、一旦試して駄目だったら前述した別の解決策に切り替える、というのもありかもしれない。
いじょう。
Pythonではじめる機械学習 ―scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎
- 作者: Andreas C. Muller,Sarah Guido,中田秀基
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/05/25
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
scikit-learnとTensorFlowによる実践機械学習
- 作者: Aurélien Géron,下田倫大,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/04/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
Pythonデータサイエンスハンドブック ―Jupyter、NumPy、pandas、Matplotlib、scikit-learnを使ったデータ分析、機械学習
- 作者: Jake VanderPlas,菊池彰
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/05/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る