データフレーム出力可能なsklearnライクな変数選択器を実装してみる

# やりたいこと
自作のfeature selectorをパイプラインに組み込み、前処理から学習を一括管理したい。
その際、削除された変数を確認したいので、データフレームで出力できるようにする。

# 最低限の実装
今回は練習用に、同じ値の要素が閾値以上の列を削除するfeature selectorを実装してみる。
似たような機能を持つsklearnの`VarianceThreshold`[^1]のソースコード[^2]を参考に作成した。
データフレームでの出力方法以外は、すでに解説記事[^3]があるのでそちらを参照して頂きたい。

まずは最低限の実装を示す。
こちらの実装では、fitメソッドの引数Xはデータフレームでなければならない。
```python
from scipy.stats import mode
from sklearn.base import BaseEstimator
from sklearn.feature_selection import SelectorMixin


class MySelector(SelectorMixin, BaseEstimator):
    def __init__(self, threshold=1):
        self.threshold = threshold

    def fit(self, X, y=None):
        self.feature_names_in_ = X.columns
        _, counts = mode(X, axis=0)
        self.freq_ratios = (counts / X.shape[0]).flatten()
        return self

    def _get_support_mask(self):
        return self.freq_ratios < self.threshold
```

`_get_support_mask`メソッドを実装すれば、`SelectorMixin`から`transform`メソッドと`inverse_transform`メソッドが実装される。
また`BaseEstimator`を継承しているので、インスタンス変数`feature_names_in_`をセットすることで、次のように`set_output`メソッドに`transform="pandas"`を渡すことで出力をデータフレームにできる。
```python
selector = MySelector(threshold=1).set_output(transform="pandas")
selector.fit(X)
X_transformed = selector.transform(X)
```

ひとまず目的は達成されたが、`fit`の引数としてデータフレームしか受け取れない問題が残っている。
（他にも色々あると思われるが・・・）
これは型チェック用の関数を導入することで解決できる。

# 型チェックを加えた実装
`fit`メソッドの中で`_validate_data`メソッドを呼び出せば良い。内部では`check_array`関数によって入力データが2次元配列であることをチェックしつつ、`_check_feature_names`メソッドが呼び出され、インスタンスの`feature_names_in_`属性に値をセットしてくれる。
また、`check_array`に渡すキーワード引数を`_validate_data`メソッドに渡しておくことで、入力データの様々な型チェックが可能[^4]。今回は出力のデータ型を維持するように`dtype=None`を渡しておく。

ついでに、`_get_support_mask`メソッドの中で`check_is_fitted`メソッドを呼び出すことで、`fit`メソッドの実行前に`transform`メソッドを実行しようとしたときに、 "fitメソッドが実行されていないよ" というエラーメッセージを返してくれる。

以上を加えた実装はこちら。

```python
from scipy.stats import mode
from sklearn.base import BaseEstimator
from sklearn.feature_selection import SelectorMixin
from sklearn.utils.validation import check_is_fitted


class MySelector(SelectorMixin, BaseEstimator):
    def __init__(self, threshold=1):
        self.threshold = threshold

    def fit(self, X, y=None):
        X = self._validate_data(X, dtype=None)
        # self.feature_names_in_ = X.columns
        _, counts = mode(X, axis=0)
        self.freq_ratios = (counts / X.shape[0]).flatten()
        return self

    def _get_support_mask(self):
        check_is_fitted(self)
        return self.freq_ratios < self.threshold
```

# パイプライン化
上記のように実装したクラスはsklearnのパイプラインに組み込むことができる。
またパイプラインでは出力をデータフレームにする設定をまとめて行うことができる[^5]。
以下は、自作feature selectorで全て同じ値の変数を削除した後に標準化を行う例。
```python
pipeline = Pipeline(
    [("my_selector", MySelector(threshold=1)), ("standard_scaler", StandardScaler())]
)
pipeline.set_output(transform="pandas")
pipeline.fit(X)
X_transformed = pipeline.transform(X)
```

[^1]: https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.VarianceThreshold.html
[^2]: https://github.com/scikit-learn/scikit-learn/blob/364c77e04/sklearn/feature_selection/_variance_threshold.py#L13
[^3]: https://qiita.com/roronya/items/fdf35d4f69ea62e1dd91
[^4]: https://scikit-learn.org/stable/modules/generated/sklearn.utils.check_array.html
[^5]: https://scikit-learn.org/stable/auto_examples/miscellaneous/plot_set_output.html