scikit-learnでカテゴリ値のリスト型の特徴をモデルに入力して特徴重要度を出す方法

# scikit-learnでカテゴリ値のリスト型の特徴をモデルに入力して特徴重要度を出す方法

## 背景

binaryエンコーディングやonehotエンコーディングによってID単位で算出したfeature-importanceでは、そもそもそのリスト自体の重要性がどの程度なのかわかりにくい。リストの状態でfeature-importanceを算出したかった。
なお、単なる実験コードのためデータはpandasのデータフレームで入力。

## 最終的な実現方法

1. **各特徴単位**で**[CountVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html)**を使用して特徴変換器を作成(リストをbinaryエンコーディング)
2. [ColumnTransformer](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html)を使用して特徴変換器をまとめる
3. [Pipeline](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html)を使用して変換器と識別器を接続
4. [permutation_importance](https://scikit-learn.org/stable/modules/generated/sklearn.inspection.permutation_importance.html)を使用して特徴重要度を算出

ポイントは、「各特徴単位」というのと「CountVectorizerを使用」という点。

## 途中経過

### MultiLabelBinarizer

scikit-learnのAPIを眺めてみて、まずは[MultiLabelBinarizer](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MultiLabelBinarizer.html)が近そうかなと思い、試してみる。
しかし、単一列のみを入れて実験した場合にはうまくいくが[ColumnTransformer](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html)に組み込むと引数が多いと怒られてしまう。[MultiLabelBinarizer](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MultiLabelBinarizer.html)(のfitやfit_transform)はyのみを引数にとるのに対し、[ColumnTransformer](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html)(のfitやfit_transform)はX,y両方を引数にとるのが原因。(y=Noneだったとしても)
確かに"Label"と書いてあるし、そういう使い方は想定していないのだろう。

### CountVectorizer

そこで、[ここ](https://github.com/scikit-learn/scikit-learn/issues/11309)にある通り[CountVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html)でbinaryエンコーディングを実施した。

```python
transformer = CountVectorizer(analyzer=set), "column")
```

### ColumnTransformer

カテゴリ値のリストが入っている特徴が複数あるため、[ColumnTransformer](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html)に列名を以下のように渡すと意味不明な挙動になってしまう。

```python
columns = ["column1", "column2"]
ColumnTransformer(
    transformers=[("CountVectorizer", CountVectorizer(analyzer=set), columns)],
    remainder="passthrough")
```

調べてみると、[CountVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html)に対して、データフレームをforで回したときのように、列名のみが渡されていた。

> A scalar string or int should be used where transformer expects X to be a 1d array-like (vector), otherwise a 2d array will be passed to the transformer.

とあるので、仕様なのだろうか？でも、列名が順次渡されてるので単に[CountVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html)が列名でデータを渡されるのに対応していないのだろうか？
ともあれ、列名のリストではなく、単一の列名を渡すと正常に動作する。

```python
columns = ["column1", "column2"]
transformers = [(f"binary-encoder-{c}", CountVectorizer(analyzer=set), c)
    for c in data.columns]
preprocessor = ColumnTransformer(
    transformers=transformers, remainder="passthrough")
```

ここまでできてしまうと、後は問題なく動作する。

## 全体のコード

```python
columns = ["column1", "column2"]
transformers = [(f"binary-encoder-{c}", CountVectorizer(analyzer=set), c)
    for c in columns]
preprocessor = ColumnTransformer(
    transformers=transformers, remainder="passthrough")
pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', classifier)])
model = pipeline.fit(X, y)
inspector = permutation_importance(model, X, y)
for i in inspector.importances_mean.argsort()[::-1]:
    print(f"{data.columns[i]:<8}",
          f"{inspector.importances_mean[i]:.3f}",
          f" +/- {inspector.importances_std[i]:.3f}")
```