Python を使った機械学習でよく用いられるパッケージの scikit-learn は API の入出力に numpy の配列を仮定している。
そのため、データフレームの実装である pandas と一緒に使おうとすると、色々な場面で食べ合わせの悪さを感じることになる。
今回は、その問題を sklearn-pandas というパッケージを使うことで改善を試みる。
使った環境は次の通り。
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.14.6
BuildVersion: 18G103
$ python -V
Python 3.7.4
もくじ
下準備
まずは今回使うパッケージをインストールしておく。
seaborn についてはデータセットの読み込みにだけ用いる。
$ pip install sklearn-pandas seaborn
インストールできたら Python のインタプリタを起動しておく。
$ python
起動したら seaborn を使って Titanic データセットのデータフレームを読み込んでおく。
>>> import seaborn as sns
>>> df = sns.load_dataset('titanic')
>>> df.head()
survived pclass sex age sibsp parch fare ... class who adult_male deck embark_town alive alone
0 0 3 male 22.0 1 0 7.2500 ... Third man True NaN Southampton no False
1 1 1 female 38.0 1 0 71.2833 ... First woman False C Cherbourg yes False
2 1 3 female 26.0 0 0 7.9250 ... Third woman False NaN Southampton yes True
3 1 1 female 35.0 1 0 53.1000 ... First woman False C Southampton yes False
4 0 3 male 35.0 0 0 8.0500 ... Third man True NaN Southampton no True
[5 rows x 15 columns]
scikit-learn の API と pandas の DataFrame について
前述した通り、scikit-learn の API はその入出力に numpy の配列を仮定している。
そのため、pandas の DataFrame と一緒に使おうとすると相性があまり良くない。
例えば、LabelEncoder
を使ってみることにしよう。
>>> from sklearn.preprocessing import LabelEncoder
>>> label_encoder = LabelEncoder()
先ほど読み込んだデータフレームの中から、乗客の性別を表すカラムをエンコードしてみよう。
すると、入力は pandas の Series なのに対して出力は numpy の ndarray になっていることがわかる。
>>> result = label_encoder.fit_transform(df.sex)
>>> type(result)
<class 'numpy.ndarray'>
>>> result[:5]
array([1, 0, 0, 0, 1])
>>> type(df.sex)
<class 'pandas.core.series.Series'>
このように scikit-learn と pandas を組み合わせて使うと入出力でデータの型が変わるため扱いにくいことがある。
sklearn-pandas を使って scikit-learn API をラップする
今回紹介する sklearn-pandas を使うと、両者を組み合わせたときの食べ合わせの悪さを改善できる可能性がある。
例えば sklearn-pandas では DataFrameMapper
というクラスを提供している。
このクラスには scikit-learn の API をラップする機能がある。
>>> from sklearn_pandas import DataFrameMapper
DataFrameMapper
は、次のように使う。
まず、処理の対象としたいデータフレームのカラム名と、適用したい scikit-learn の Transformer をタプルとして用意する。
そして、オプションの df_out
に True
を指定しておく。
>>> mapper = DataFrameMapper([
... ('sex', LabelEncoder()),
... ], df_out=True)
DataFrameMapper
は scikit-learn の API を備えているため、次のように fit_transform()
メソッドが使える。
このメソッドにデータフレームを渡す。
すると、次のように先ほど指定したカラムがエンコードされた上で、結果がまたデータフレームとして返ってくる。
>>> mapper.fit_transform(df)
sex
0 1
1 0
2 0
3 0
4 1
.. ...
886 1
887 0
888 0
889 1
890 1
[891 rows x 1 columns]
結果を numpy 配列として受け取る
ちなみに、先ほど指定したオプションの df_out
を指定しないとデフォルトでは numpy の配列として結果が返ってくる。
sklearn-pandas は内部的には一旦結果を numpy の配列として受け取った上で、それをデータフレームに変換している。
>>> mapper = DataFrameMapper([
... ('sex', LabelEncoder()),
... ])
>>> mapper.fit_transform(df)
array([[1],
[0],
[0],
...(snip)...
[0],
[1],
[1]])
指定していないカラムをそのままの状態で受け取る
先ほどの例では、処理の対象となるカラムだけが入ったデータフレームが結果として得られた。
処理対象として指定していないカラムについて、そのまま受け取りたい場合は default
オプションに None
を指定すれば良い。
>>> mapper = DataFrameMapper([
... ('sex', LabelEncoder()),
... ], default=None, df_out=True)
性別 (sex) カラムについてはエンコードされており、その他のカラムについてはそのままの状態でデータフレームが返ってくる。
>>> mapper.fit_transform(df)
sex survived pclass age sibsp parch fare embarked class who adult_male deck embark_town alive alone
0 1 0 3 22 1 0 7.25 S Third man True NaN Southampton no False
1 0 1 1 38 1 0 71.2833 C First woman False C Cherbourg yes False
2 0 1 3 26 0 0 7.925 S Third woman False NaN Southampton yes True
3 0 1 1 35 1 0 53.1 S First woman False C Southampton yes False
4 1 0 3 35 0 0 8.05 S Third man True NaN Southampton no True
.. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
886 1 0 2 27 0 0 13 S Second man True NaN Southampton no True
887 0 1 1 19 0 0 30 S First woman False B Southampton yes True
888 0 0 3 NaN 1 2 23.45 S Third woman False NaN Southampton no False
889 1 1 1 26 0 0 30 C First man True C Cherbourg yes True
890 1 0 3 32 0 0 7.75 Q Third man True NaN Queenstown no True
[891 rows x 15 columns]
複数のカラムを一度に扱う
DataFrameMapper
には複数のカラムを指定することもできる。
例えば性別 (sex) と客室等級 (class) を一度にエンコードしてみよう。
>>> mapper = DataFrameMapper([
... ('sex', LabelEncoder()),
... ('class', LabelEncoder()),
... ], df_out=True)
次の通り、両方のカラムを同時にエンコードできた。
>>> mapper.fit_transform(df)
sex class
0 1 2
1 0 0
2 0 2
3 0 0
4 1 2
.. ... ...
886 1 1
887 0 0
888 0 2
889 1 0
890 1 2
[891 rows x 2 columns]
ここまでの例では単一のカラムを scikit-learn の Transformer でエンコードしてきた。
今度は複数のカラムを一度に Transformer に渡す場合を試してみる。
ただし、これはあくまで Transformer が複数のカラムを受け取れることが前提となる。
例えば OneHotEncoder
に性別と客室等級を渡してみよう。
>>> import numpy as np
>>> from sklearn.preprocessing import OneHotEncoder
>>>
>>> mapper = DataFrameMapper([
... (['sex', 'class'], OneHotEncoder(dtype=np.uint8)),
... ], df_out=True)
結果は次のようになる。
両方のカラムを別々に One-Hot エンコードしている。
>>> mapper.fit_transform(df)
sex_class_x0_female sex_class_x0_male sex_class_x1_First sex_class_x1_Second sex_class_x1_Third
0 0 1 0 0 1
1 1 0 1 0 0
2 1 0 0 0 1
3 1 0 1 0 0
4 0 1 0 0 1
.. ... ... ... ... ...
886 0 1 0 1 0
887 1 0 1 0 0
888 1 0 0 0 1
889 0 1 1 0 0
890 0 1 0 0 1
[891 rows x 5 columns]
なお、上記で得られる結果は、次のようにして得られた内容と等価になる。
>>> encoder = OneHotEncoder(dtype=np.uint8)
>>> encoder.fit_transform(df[['sex', 'class']].values).toarray()
array([[0, 1, 0, 0, 1],
[1, 0, 1, 0, 0],
[1, 0, 0, 0, 1],
...,
[1, 0, 0, 0, 1],
[0, 1, 1, 0, 0],
[0, 1, 0, 0, 1]], dtype=uint8)
ここまでの例では scikit-learn に組み込まれている Transformer
を使ってきた。
scikit-learn 組み込みの Transformer
は入力を numpy 配列と仮定して扱う。
それに対し、独自に定義した Transformer
であれば入力と pandas のデータフレームと仮定することもできる。
例として、次のような独自の Transformer
を定義してみる。
このクラスでは、入力を pandas の DataFrame と仮定としている。
そして、指定されたカラムに 1 を足す操作をする。
もちろん、実用性は皆無だけど、あくまでサンプルとして。
>>> import pandas as pd
>>> from sklearn.base import BaseEstimator
>>> from sklearn.base import TransformerMixin
>>>
>>> class PlusOneTransformer(BaseEstimator, TransformerMixin):
... def __init__(self, col):
... self.col = col
... def fit(self, X, y=None):
... assert type(X) in [pd.DataFrame]
... return self
... def transform(self, X):
... assert type(X) in [pd.DataFrame]
... X_copy = X.copy()
... X_copy[self.col] += 1
... return X_copy
...
上記をデフォルトのまま DataFrameMapper
で扱おうとすると、次のように例外になってしまう。
これは DataFrameMapper
がデフォルトで Transformer
への入力を numpy の配列に変換した上で扱おうとするため。
>>> mapper = DataFrameMapper([
... (['survived'], PlusOneTransformer('survived')),
... ], df_out=True)
>>>
>>> mapper.fit_transform(df)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...(snip)...
File "<stdin>", line 5, in fit
AssertionError: ['survived']
エラーにならないようにするには、次のように input_df
を True
にする。
このオプションは Transformer
の入力を pandas のデータフレームの状態で行うことを指定する。
>>> mapper = DataFrameMapper([
... (['survived'], PlusOneTransformer('survived')),
... ], input_df=True, df_out=True)
>>>
>>> mapper.fit_transform(df)
survived
0 1
1 2
2 2
3 2
4 1
.. ...
886 1
887 2
888 1
889 2
890 1
[891 rows x 1 columns]
普段そんなに意識していないかもしれないけど、scikit-learn はクラスによって入力する次元の仮定が異なっていることがある。
例えば MinMaxScaler
を、ここまでの例と同じように使ってみよう。
エンコードする対象のカラムは運賃 (fare) にする。
>>> from sklearn.preprocessing import MinMaxScaler
>>>
>>> mapper = DataFrameMapper([
... ('fare', MinMaxScaler()),
... ], df_out=True)
上記を使ってエンコードしてみよう。
すると、次のようにエラーになる。
>>> mapper.fit_transform(df)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...
ValueError: fare: Expected 2D array, got 1D array instead:
...
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
...
上記は MinMaxScaler
が入力に 2-d 配列を仮定しているために起こる。
それに対し、入力が 1-d 配列だったために例外となってしまった。
もうちょっと問題を単純に考えるために MinMaxScaler
をそのまま使って問題を再現してみよう。
先ほどの例は、以下と等価になる。
>>> scaler = MinMaxScaler()
>>> scaler.fit_transform(df.fare.values)
...
ValueError: fare: Expected 2D array, got 1D array instead:
...
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
上記で渡したデータは、次のように ndim が 1 の 1-d 配列とわかる。
>>> df.fare.values.ndim
1
>>> df.fare.values[:10]
array([ 7.25 , 71.2833, 7.925 , 53.1 , 8.05 , 8.4583, 51.8625,
21.075 , 11.1333, 30.0708])
実際には、次のような 2-d 配列を渡す必要がある。
>>> df.fare.values.reshape(-1, 1).ndim
2
>>> df.fare.values.reshape(-1, 1)[:10]
array([[ 7.25 ],
[71.2833],
[ 7.925 ],
[53.1 ],
[ 8.05 ],
[ 8.4583],
[51.8625],
[21.075 ],
[11.1333],
[30.0708]])
上記のような 1-d から 2-d への変換を DataFrameMapper
でどのように表現するか。
次のようにカラム名が入った配列として渡せば良い。
>>> mapper = DataFrameMapper([
... (['fare'], MinMaxScaler()),
... ], df_out=True)
>>>
>>> mapper.fit_transform(df)
fare
0 0.014151
1 0.139136
2 0.015469
3 0.103644
4 0.015713
.. ...
886 0.025374
887 0.058556
888 0.045771
889 0.058556
890 0.015127
[891 rows x 1 columns]
配列として渡すか否かの違いは、以下のように考えると理解しやすいと思う。
pandas では、データフレームのスライス操作 ([]
) に文字列をそのまま渡すと 1-d 表現の Series
オブジェクトが返ってくる。
>>> df['fare']
0 7.2500
1 71.2833
2 7.9250
3 53.1000
4 8.0500
...
886 13.0000
887 30.0000
888 23.4500
889 30.0000
890 7.7500
Name: fare, Length: 891, dtype: float64
それに対してスライス操作に、カラム名の入った配列を渡すと、こちらは 2-d 表現の DataFrame
オブジェクトが返る。
>>> df[['fare']]
fare
0 7.2500
1 71.2833
2 7.9250
3 53.1000
4 8.0500
.. ...
886 13.0000
887 30.0000
888 23.4500
889 30.0000
890 7.7500
[891 rows x 1 columns]
エンコードしたカラムに別名をつける
DataFrameMapper
では、同じカラムに別々の処理を施すこともできる。
例えば、次の例では運賃 (fare) のカラムを MinMaxScaler
と StandardScaler
で別々に標準化した結果を得ている。
>>> from sklearn.preprocessing import StandardScaler
>>>
>>> mapper = DataFrameMapper([
... (['fare'], MinMaxScaler()),
... (['fare'], StandardScaler()),
... ], df_out=True)
>>>
>>> mapper.fit_transform(df)
fare fare
0 0.014151 -0.502445
1 0.139136 0.786845
2 0.015469 -0.488854
3 0.103644 0.420730
4 0.015713 -0.486337
.. ... ...
886 0.025374 -0.386671
887 0.058556 -0.044381
888 0.045771 -0.176263
889 0.058556 -0.044381
890 0.015127 -0.492378
[891 rows x 2 columns]
ただし、上記を見ると変換した結果として得られたカラムの名前が同じになってしまっていることがわかる。
区別するために別の名前をつけたいときは、次のようにする。
これまで渡すタプルの要素が 2 つだったのに対し、3 つになっている。
3 つ目の要素は辞書オブジェクトを仮定していて、カラムごとの設定を渡す。
この中で alias
というキーを指定すると、変換後のカラムの名前が指定できる。
>>> mapper = DataFrameMapper([
... (['fare'], MinMaxScaler(), {'alias': 'fare_minmax'}),
... (['fare'], StandardScaler(), {'alias': 'fare_std'}),
... ], df_out=True)
>>>
>>> mapper.fit_transform(df)
fare_minmax fare_std
0 0.014151 -0.502445
1 0.139136 0.786845
2 0.015469 -0.488854
3 0.103644 0.420730
4 0.015713 -0.486337
.. ... ...
886 0.025374 -0.386671
887 0.058556 -0.044381
888 0.045771 -0.176263
889 0.058556 -0.044381
890 0.015127 -0.492378
[891 rows x 2 columns]
処理の対象となったカラムをそのまま残す
ちなみに、default=None
を指定した場合、処理の対象となったカラムはそのままでは残らない。
Transformer
を適用した結果で置換されてしまう。
>>> mapper = DataFrameMapper([
... (['fare'], MinMaxScaler(), {'alias': 'fare_minmax'}),
... (['fare'], StandardScaler(), {'alias': 'fare_std'}),
... ], default=None, df_out=True)
>>>
>>> mapper.fit_transform(df).columns
Index(['fare_minmax', 'fare_std', 'survived', 'pclass', 'sex', 'age', 'sibsp',
'parch', 'embarked', 'class', 'who', 'adult_male', 'deck',
'embark_town', 'alive', 'alone'],
dtype='object')
処理対象となったカラムも置換せずに残したいときは、次のように処理内容に None
を指定したタプルを別途渡す必要がある。
>>> mapper = DataFrameMapper([
... (['fare'], None),
... (['fare'], MinMaxScaler(), {'alias': 'fare_minmax'}),
... (['fare'], StandardScaler(), {'alias': 'fare_std'}),
... ], default=None, df_out=True)
>>>
>>> mapper.fit_transform(df)
fare fare_minmax fare_std survived pclass sex ... who adult_male deck embark_town alive alone
0 7.2500 0.014151 -0.502445 0 3 male ... man True NaN Southampton no False
1 71.2833 0.139136 0.786845 1 1 female ... woman False C Cherbourg yes False
2 7.9250 0.015469 -0.488854 1 3 female ... woman False NaN Southampton yes True
3 53.1000 0.103644 0.420730 1 1 female ... woman False C Southampton yes False
4 8.0500 0.015713 -0.486337 0 3 male ... man True NaN Southampton no True
.. ... ... ... ... ... ... ... ... ... ... ... ... ...
886 13.0000 0.025374 -0.386671 0 2 male ... man True NaN Southampton no True
887 30.0000 0.058556 -0.044381 1 1 female ... woman False B Southampton yes True
888 23.4500 0.045771 -0.176263 0 3 female ... woman False NaN Southampton no False
889 30.0000 0.058556 -0.044381 1 1 male ... man True C Cherbourg yes True
890 7.7500 0.015127 -0.492378 0 3 male ... man True NaN Queenstown no True
[891 rows x 17 columns]
パイプライン的な処理について
DataFrameMapper
には簡易的なパイプラインのような処理も扱える。
題材として欠損値の補完について考えてみる。
Titanic のデータセットには、いくつかのカラムに欠損値が含まれる。
>>> df.isnull().sum()
survived 0
pclass 0
sex 0
age 177
sibsp 0
parch 0
fare 0
embarked 2
class 0
who 0
adult_male 0
deck 688
embark_town 2
alive 0
alone 0
dtype: int64
かつ、scikit-learn 組み込みの Transformer
には欠損値が含まれるデータを扱えないものがある。
例えば欠損値が含まれる乗船地 (embarked) を LabelEncoder
でエンコードしてみよう。
すると、次のように例外になってしまう。
>>> mapper = DataFrameMapper([
... ('embarked', LabelEncoder()),
... ], df_out=True)
>>>
>>> mapper.fit_transform(df)
Traceback (most recent call last):
...(snip)...
TypeError: embarked: argument must be a string or number
上記の問題に対処するため、前段に欠損値を補完する処理を入れてみよう。
例として、欠損値の補完には SimpleImputer
を使って既知の値とは異なる定数を用いる。
処理を多段にパイプラインのように扱うには、タプルの第 2 要素をリストにして適用したい処理を順番に指定する。
>>> from sklearn.impute import SimpleImputer
>>>
>>> mapper = DataFrameMapper([
... (['embarked'], [SimpleImputer(strategy='constant', fill_value='X'),
... LabelEncoder()])
... ], df_out=True)
上記を試すと、今度は例外にならずエンコードできる。
ただし、SimpleImputer
の出力が 2-d 配列になっているせいで、今度は LabelEncoder
の方が 1-d 配列にしろと怒っている。
>>> mapper.fit_transform(df)
/Users/amedama/.virtualenvs/py37/lib/python3.7/site-packages/sklearn/preprocessing/label.py:235: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
y = column_or_1d(y, warn=True)
embarked
0 2
1 0
2 2
3 2
4 2
.. ...
886 2
887 2
888 2
889 0
890 1
[891 rows x 1 columns]
パイプラインの間で次元を揃えるために、次のように 2-d から 1-d に変換する Transformer
を適当に用意してみる。
>>> class FlattenTransformer(BaseEstimator, TransformerMixin):
... def fit(self, X, y=None):
... return self
... def transform(self, X):
... return X.flatten()
...
欠損値の補完とエンコードの間に処理を挟み込む。
>>> mapper = DataFrameMapper([
... (['embarked'], [SimpleImputer(strategy='constant', fill_value='X'),
... FlattenTransformer(),
... LabelEncoder()])
... ], df_out=True)
すると、今度は警告も出ない。
>>> mapper.fit_transform(df)
embarked
0 2
1 0
2 2
3 2
4 2
.. ...
886 2
887 2
888 2
889 0
890 1
[891 rows x 1 columns]
カラムの処理が分岐・合流する場合の処理について
先ほどは特定のカラムを多段的に処理してみたけど、これが使えるのは限定的な用途にとどまると思う。
なぜなら、カラムによって前処理の内容を変えるなど、処理の内容が分岐したり合流するときに使えないため。
例として、運賃 (fare) と乗客の年齢 (age) を主成分分析 (PCA) することを考えてみる。
ここで、運賃と乗客に別々の前処理をしたいとしたら、どうなるだろうか。
>>> from sklearn.decomposition import PCA
>>>
>>> mapper = DataFrameMapper([
... (['fare', 'age'], PCA()),
... ], df_out=True)
実際のところ、乗客の年齢には欠損値が含まれるため、上記はそのままだと例外になる。
>>> mapper.fit_transform(df)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...(snip)...
ValueError: ['fare', 'age']: Input contains NaN, infinity or a value too large for dtype('float64').
しかし、もしカラムごとに別々の欠損値の補完がしたいとしたら、DataFrameMapper
のパイプライン処理では表現ができない。
最初に思いつくのは DataFrameMapper
自体を scikit-learn の Pipeline に組み込んで多段で処理をすることだった。
実際にやってみよう。
>>> impute_phase = DataFrameMapper([
... ('fare', None),
... (['age'], SimpleImputer(strategy='mean')),
... ], df_out=True)
>>>
>>> encode_phase = DataFrameMapper([
... (['fare', 'age'], PCA(), {'alias': 'pca'}),
... ], df_out=True)
>>>
>>> from sklearn.pipeline import Pipeline
>>> steps = [
... ('impute', impute_phase),
... ('encode', encode_phase),
... ]
>>> pipeline = Pipeline(steps)
ただ、これをやると最終的に得られる結果が numpy 配列となってしまう。
データフレームとして扱いたいために sklearn-pandas を使っているのに、これだとちょっと微妙な感じがする。
>>> pipeline.fit_transform(df)
pca_0 pca_1
0 -25.143783 -7.055429
1 39.279465 7.294086
2 -24.366234 -3.074093
3 21.025089 4.762258
4 -24.010039 5.919725
.. ... ...
886 -19.267217 -2.204814
887 -2.478372 -10.638953
888 -8.751318 0.224921
889 -2.298521 -3.641264
890 -24.387019 2.928423
[891 rows x 2 columns]
そこで、やや近視眼的な解決方法だけど、次のように自分で多段の処理ができるようなクラスを定義してみた。
>>> class PhasedTransformer(BaseEstimator, TransformerMixin):
... def __init__(self, steps):
... self.steps = steps
... def fit_transform(self, X, y=None):
... transformed = X
... for _, transformer in self.steps:
... transformed = transformer.fit_transform(transformed, y)
... return transformed
... def transform(self, X):
... transformed = X
... for _, transformer in self.steps:
... transformed = transformer.transform(transformed)
... return transformed
...
これなら最終的に得られる結果もデータフレームのままとなる。
>>> transformer = PhasedTransformer(steps)
>>> transformer.fit_transform(df)
pca_0 pca_1
0 -25.143783 -7.055429
1 39.279465 7.294086
2 -24.366234 -3.074093
3 21.025089 4.762258
4 -24.010039 5.919725
.. ... ...
886 -19.267217 -2.204814
887 -2.478372 -10.638953
888 -8.751318 0.224921
889 -2.298521 -3.641264
890 -24.387019 2.928423
[891 rows x 2 columns]
sklearn-pandas で処理したデータを使って Titanic を予測してみる
もうちょっと複雑なパターンも試しておいた方が良いと思って、実際に予測までする一連の処理を書いてみた。
まずは欠損値の補完とエンコードの二段階で構成されたパイプラインを用意する。
>>> impute_phase = DataFrameMapper([
... ('pclass', None),
... ('sex', None),
... (['age'], SimpleImputer(strategy='mean')),
... ('sibsp', None),
... ('parch', None),
... ('fare', None),
... (['embarked'], SimpleImputer(strategy='most_frequent')),
... ('adult_male', None),
... (['deck'], SimpleImputer(strategy='most_frequent')),
... ('alone', None),
... ], df_out=True)
>>>
>>> encode_phase = DataFrameMapper([
... (['sex'], [FlattenTransformer(), LabelEncoder()]),
... (['embarked'], OneHotEncoder()),
... (['adult_male'], [FlattenTransformer(), LabelEncoder()]),
... (['deck'], OneHotEncoder()),
... (['alone'], [FlattenTransformer(), LabelEncoder()]),
... ], default=None, df_out=True)
>>>
>>> steps = [
... ('impute', impute_phase),
... ('encode', encode_phase),
... ]
>>> transformer = PhasedTransformer(steps)
上記によって、以下のようなデータが得られる。
>>> features = transformer.fit_transform(df)
>>>
>>> features.head()
sex embarked_x0_C embarked_x0_Q embarked_x0_S adult_male deck_x0_A ... alone pclass age sibsp parch fare
0 1 0.0 0.0 1.0 1 0.0 ... 0 3.0 22.0 1.0 0.0 7.2500
1 0 1.0 0.0 0.0 0 0.0 ... 0 1.0 38.0 1.0 0.0 71.2833
2 0 0.0 0.0 1.0 0 0.0 ... 1 3.0 26.0 0.0 0.0 7.9250
3 0 0.0 0.0 1.0 0 0.0 ... 0 1.0 35.0 1.0 0.0 53.1000
4 1 0.0 0.0 1.0 1 0.0 ... 1 3.0 35.0 0.0 0.0 8.0500
[5 rows x 18 columns]
上記を 5-Fold CV と RandomForest で予測してみる。
>>> from sklearn.model_selection import StratifiedKFold
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.model_selection import cross_validate
>>>
>>> folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
>>> result = cross_validate(RandomForestClassifier(n_estimators=100),
... X=features,
... y=df.survived,
... cv=folds,
... )
結果は次の通り。
データを考えると妥当な内容といえるはず。
>>> result['test_score'].mean()
0.8194004369833607
まとめ
今回は sklearn-pandas を使って scikit-learn と pandas の食べ合わせの悪さを改善する方法について書いた。
ややクセがあって自分で記述が必要な部分もあるものの、場面を選べば便利なパッケージだと感じた。
また、記述方法に関して、個人的にはデータフレームを上書きしながら手続き的に処理していく一般的なやり方はあまり好きではない。
そのため、sklearn-pandas を使うと関数的に書けるところに好印象を受ける。
独自のパイプラインを組むのにパーツとして使うのも良いかもしれない。
ちなみに、ソースコードを見るとあんまりパフォーマンスチューニングについては考慮されていない印象を受ける。
加えて、numpy の配列とデータフレームの変換にかかるコストもあるので、大きいデータだと辛いかもしれない。
いじょう。