これまで scikit-learn が提供する TransformerMixin の実装 1 は、出力に NumPy 配列を仮定していた。
そのため、pandas の DataFrame を入力しても出力は NumPy 配列になってしまい、使い勝手が良くないという問題があった。
この問題は、特に Pipeline
や ColumnTransformer
を使って処理を組むときに顕在化しやすい。
しかし、scikit-learn v1.2 で set_output API が追加されたことで、この状況に改善が見られた。 そこで、今回は set_output API の使い方について書いてみる。
使った環境は次のとおり。
$ sw_vers ProductName: macOS ProductVersion: 13.6.2 BuildVersion: 22G320 $ python -V Python 3.10.13 $ pip list | egrep "(pandas|scikit-learn)" pandas 2.1.3 scikit-learn 1.3.2
もくじ
- もくじ
- 下準備
- scikit-learn と pandas の食べ合わせの悪さについて
- TransformerMixin#set_output(transform="pandas") を指定する
- scikit-learn の Pipeline と組み合わせて使う
- scikit-learn の ColumnTransformer と組み合わせて使う
- まとめ
- 参考
下準備
まずは必要なパッケージをインストールしておく。
$ pip install scikit-learn pandas
そして Python のインタプリタを起動しておこう。
$ python
scikit-learn と pandas の食べ合わせの悪さについて
初めに、前述した scikit-learn と pandas の食べ合わせの悪さについて確認しておく。
最初にサンプルとして Iris データセットを pandas の DataFrame の形式で読み込む。
>>> from sklearn.datasets import load_iris >>> X, y = load_iris(as_frame=True, return_X_y=True) >>> type(X) <class 'pandas.core.frame.DataFrame'> >>> X.head() sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) 0 5.1 3.5 1.4 0.2 1 4.9 3.0 1.4 0.2 2 4.7 3.2 1.3 0.2 3 4.6 3.1 1.5 0.2 4 5.0 3.6 1.4 0.2 >>> type(y) <class 'pandas.core.series.Series'> >>> y.head() 0 0 1 0 2 0 3 0 4 0 Name: target, dtype: int64
そして scikit-learn の TransformerMixin を実装したサンプルとして StandardScaler
のインスタンスを用意する。
>>> from sklearn.preprocessing import StandardScaler >>> scaler = StandardScaler()
先ほど読み込んだ Iris データセットの DataFrame を学習および変換してみよう。
>>> X_scaled = scaler.fit_transform(X)
すると、返り値は NumPy 配列になってしまう。
>>> type(X_scaled) <class 'numpy.ndarray'> >>> X_scaled[:5] array([[-0.90068117, 1.01900435, -1.34022653, -1.3154443 ], [-1.14301691, -0.13197948, -1.34022653, -1.3154443 ], [-1.38535265, 0.32841405, -1.39706395, -1.3154443 ], [-1.50652052, 0.09821729, -1.2833891 , -1.3154443 ], [-1.02184904, 1.24920112, -1.34022653, -1.3154443 ]])
pandas の DataFrame を入れたのに返ってくるのが NumPy 配列だと、特にパイプライン的に処理を繰り返すような場面で使い勝手が良くない。
TransformerMixin#set_output(transform="pandas") を指定する
では、次に今回の主題となる set_output API を使ってみよう。
やることは単純で、先ほど用意した StandardScaler
インスタンスに対して set_output()
メソッドを呼ぶ。
このとき、引数として transform="pandas"
を指定するのがポイントになる。
>>> scaler.set_output(transform="pandas")
StandardScaler()
この状態で、もう一度 DataFrame を入力として学習と変換をしてみよう。
>>> X_scaled = scaler.fit_transform(X)
すると、今度は返ってくる値の型が pandas の DataFrame になっている。
>>> type(X_scaled) <class 'pandas.core.frame.DataFrame'> >>> X_scaled.head() sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) 0 -0.900681 1.019004 -1.340227 -1.315444 1 -1.143017 -0.131979 -1.340227 -1.315444 2 -1.385353 0.328414 -1.397064 -1.315444 3 -1.506521 0.098217 -1.283389 -1.315444 4 -1.021849 1.249201 -1.340227 -1.315444
このように TransformerMixin のサブクラスは、set_output API を使うことで pandas の DataFrame を返すことができるようになった。
ちなみに学習済みのインスタンスについて、途中から返す値の型を変えることもできる。
試しに set_output()
メソッドで transform="default"
を指定して元に戻してみよう。
>>> scaler.set_output(transform="default")
StandardScaler()
そして既存のデータを学習せずに変換だけしてみる。 すると、今度は NumPy 配列が返ってきた。
>>> X_scaled = scaler.transform(X) >>> type(X_scaled) <class 'numpy.ndarray'> >>> X_scaled[:5] array([[-0.90068117, 1.01900435, -1.34022653, -1.3154443 ], [-1.14301691, -0.13197948, -1.34022653, -1.3154443 ], [-1.38535265, 0.32841405, -1.39706395, -1.3154443 ], [-1.50652052, 0.09821729, -1.2833891 , -1.3154443 ], [-1.02184904, 1.24920112, -1.34022653, -1.3154443 ]])
scikit-learn の Pipeline と組み合わせて使う
また、set_output API は Pipeline についても対応している。
試しに PCA
と StandardScaler
を直列に実行する Pipeline
を用意して試してみよう。
先ほどはインスタンスの生成と set_output()
メソッドの呼び出しを行で分けていた。
今度はメソッドチェーンで一気に設定してみよう。
>>> from sklearn.decomposition import PCA >>> from sklearn.pipeline import Pipeline >>> steps = [ ... ("pca", PCA()), ... ("normalize", StandardScaler()), ... ] >>> pipeline = Pipeline(steps=steps).set_output(transform="pandas")
作成した Pipeline
でデータの学習と変換をしてみよう。
>>> X_transformed = pipeline.fit_transform(X) >>> type(X_transformed) <class 'pandas.core.frame.DataFrame'> >>> X_transformed.head() pca0 pca1 pca2 pca3 0 -1.309711 0.650541 -0.100152 -0.014703 1 -1.324357 -0.360512 -0.755094 -0.643570 2 -1.409674 -0.295230 0.064222 -0.129774 3 -1.339582 -0.648304 0.113227 0.491164 4 -1.331469 0.665527 0.323182 0.398117
ちゃんと pandas の DataFrame で返ってきていることが確認できる。
最終段を BaseEstimator
のサブクラスにした上で予測まで扱えることも確認しておこう。
先ほどのパイプラインの最終段に LogisticRegression
を挿入する。
>>> from sklearn.linear_model import LogisticRegression >>> steps = [ ... ("pca", PCA()), ... ("normalize", StandardScaler()), ... ("lr", LogisticRegression()), ... ] >>> pipeline = Pipeline(steps=steps).set_output(transform="pandas")
データを train_test_split()
関数で分割する。
>>> from sklearn.model_selection import train_test_split >>> X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42)
学習データを Pipeline に学習させる。
>>> pipeline.fit(X_train, y_train) Pipeline(steps=[('pca', PCA()), ('normalize', StandardScaler()), ('lr', LogisticRegression())])
テストデータを Pipeline に推論させる。
>>> y_pred = pipeline.predict(X_test)
推論した内容を Accuracy で評価する。
>>> from sklearn.metrics import accuracy_score >>> accuracy_score(y_test, y_pred) 0.8947368421052632
ひとまず予測の精度については横に置いておくとして、ちゃんと機能していそうなことが確認できた。
scikit-learn の ColumnTransformer と組み合わせて使う
さらに実用上は、特定のカラムに絞って処理をする ColumnTransformer
と組み合わせて使うことになるはず。
この点も確認しておこう。
カテゴリ変数の処理をしたいので Diamonds データセットを読み込んでおく。
>>> from sklearn.datasets import fetch_openml >>> X, y = fetch_openml( ... "diamonds", ... version=1, ... as_frame=True, ... return_X_y=True, ... parser="pandas" ... ) >>> X.head() carat cut color clarity depth table x y z 0 0.23 Ideal E SI2 61.5 55.0 3.95 3.98 2.43 1 0.21 Premium E SI1 59.8 61.0 3.89 3.84 2.31 2 0.23 Good E VS1 56.9 65.0 4.05 4.07 2.31 3 0.29 Premium I VS2 62.4 58.0 4.20 4.23 2.63 4 0.31 Good J SI2 63.3 58.0 4.34 4.35 2.75 >>> y.head() 0 326 1 326 2 327 3 334 4 335 Name: price, dtype: int64
ちなみに、常に TransformerMixin
を実装したクラスで pandas の DataFrame を返してほしいときは、次のようにしてグローバルに設定できる。
>>> from sklearn import set_config >>> set_config(transform_output="pandas")
カテゴリ変数には OneHotEncoder
を、連続変数には StandardScaler
をかける ColumnTransformer
を次のように用意する。
>>> from sklearn.preprocessing import OneHotEncoder >>> from sklearn.preprocessing import StandardScaler >>> ct_settings = [ ... ("cat_onehot", OneHotEncoder(sparse_output=False), ["cut", "color", "clarity"]), ... ("num_normalize", StandardScaler(), ["carat", "depth", "table", "x", "y", "z"]), ... ] >>> ct = ColumnTransformer(ct_settings)
試しにデータを学習および変換してみると、次のように結果が pandas の DataFrame で返ってくる。
>>> ct.fit_transform(X).head() cat_onehot__cut_Fair cat_onehot__cut_Good ... num_normalize__y num_normalize__z 0 0.0 0.0 ... -1.536196 -1.571129 1 0.0 0.0 ... -1.658774 -1.741175 2 0.0 1.0 ... -1.457395 -1.741175 3 0.0 0.0 ... -1.317305 -1.287720 4 0.0 1.0 ... -1.212238 -1.117674 [5 rows x 26 columns]
上記の ColumnTransformer
のインスタンスを、さらに Pipeline
に組み込んでみよう。
最終段には RandomForestRegressor
を配置する。
>>> from sklearn.ensemble import RandomForestRegressor >>> steps = [ ... ("preprocessing", ct), ... ("rf", RandomForestRegressor(n_jobs=-1)), ... ] >>> pipeline = Pipeline(steps)
データを Random 5-Fold で RMSE について交差検証してみよう。
>>> from sklearn.model_selection import cross_validate >>> from sklearn.model_selection import KFold >>> folds = KFold(n_splits=5, shuffle=True, random_state=42) >>> cv_result = cross_validate(pipeline, X, y, cv=folds, scoring="neg_root_mean_squared_error")
すると、次のようにテストデータのスコアが得られた。
>>> cv_result["test_score"] array([-552.59282602, -536.20769256, -582.69130436, -559.43303878, -533.75354186])
上記についても性能の高低は別として、エンドツーエンドで評価まで動作することが確認できた。
まとめ
今回は scikit-learn v1.2 で追加された set_output API を試してみた。 TransformerMixin を実装したクラスが pandas の DataFrame を返せるようになったことで両者の食べ合わせが以前よりも良くなった。
参考
-
具体例として
OneHotEncoder
やStandardScaler
などが挙げられる↩