sklearn.feature_extraction.text.CountVectrizerのanalyzerを試してみた

# 概要
[前回の記事](https://qiita.com/y_nishimura/items/6d022a25beb21a186d60)で取り上げた、scikit-learnの[CountVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html)の`analyzer`キーワード引数について、変えると具体的にどう結果が変化するんだろう？
と思ったので一式コードを書いて確認。

# 環境
- Windows10 Pro
- Python 3.7.3
- scikit-learn 0.21.2

# コード

## 一通りの`analyzer`キーワード引数を使ったインスタンスを作成する
~~~ python
from sklearn.feature_extraction.text import CountVectorizer

# 単語でN-gram
cv_word = CountVectorizer(analyzer = 'word')
# 文字でN-gram
cv_char = CountVectorizer(analyzer = 'char')
# 'char_wb'でN-gram
cv_wb = CountVectorizer(analyzer = 'char_wb')
# 呼び出し可能オブジェクト
cv_callable = CountVectorizer(analyzer = lambda x: [x])
~~~

## 対象にする単語と文章の配列を作成する
即興で書いたので、英文の不味さには目をつぶってください……

~~~python
words = ['Tokyo', 'Osaka', 'Sendai', 'Fukuoka', 'Sapporo', 'Kouchi']
texts = ['Tokyo is the biggest city in japan.', 
    'Osaka is the secondary biggest city in japan.', 
    'Fukuoka is most nearly city from Koria, China in japan.',
    'Sendai called we ''Mori no Miyako''.',
    'Sapporo is most nearly city from Russia in japan.',
    'Kouchi is a japanese city, in japanese region in ''Shikoku''.']
~~~

## 単語の配列で、`fit_transform`での特徴抽出と特徴を出力
```python
print("CountVectorizer: target is words")

X = cv_word.fit_transform(words)
print("CountVectorizer: analyzer is word")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_word.get_feature_names()))

X = cv_char.fit_transform(words)
print("CountVectorizer: analyzer is char")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_char.get_feature_names()))

X = cv_wb.fit_transform(words)
print("CountVectorizer: analyzer is char_wb")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_wb.get_feature_names()))

X = cv_callable.fit_transform(words)
print("CountVectorizer: analyzer is callable object")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_callable.get_feature_names()))
```

## 単語で試した出力結果
~~~
CountVectorizer: target is words
CountVectorizer: analyzer is word
results array:
[[0 0 0 0 0 1]
 [0 0 1 0 0 0]
 [0 0 0 0 1 0]
 [1 0 0 0 0 0]
 [0 0 0 1 0 0]
 [0 1 0 0 0 0]]
feature_names:
['fukuoka', 'kouchi', 'osaka', 'sapporo', 'sendai', 'tokyo']
CountVectorizer: analyzer is char
results array:
[[0 0 0 0 0 0 0 1 0 2 0 0 0 1 0 1]
 [2 0 0 0 0 0 0 1 0 1 0 0 1 0 0 0]
 [1 0 1 1 0 0 1 0 1 0 0 0 1 0 0 0]
 [1 0 0 0 1 0 0 2 0 1 0 0 0 0 2 0]
 [1 0 0 0 0 0 0 0 0 2 2 1 1 0 0 0]
 [0 1 0 0 0 1 1 1 0 1 0 0 0 0 1 0]]
feature_names:
['a', 'c', 'd', 'e', 'f', 'h', 'i', 'k', 'n', 'o', 'p', 'r', 's', 't', 'u', 'y']
CountVectorizer: analyzer is char_wb
results array:
[[2 0 0 0 0 0 0 0 1 0 2 0 0 0 1 0 1]
 [2 2 0 0 0 0 0 0 1 0 1 0 0 1 0 0 0]
 [2 1 0 1 1 0 0 1 0 1 0 0 0 1 0 0 0]
 [2 1 0 0 0 1 0 0 2 0 1 0 0 0 0 2 0]
 [2 1 0 0 0 0 0 0 0 0 2 2 1 1 0 0 0]
 [2 0 1 0 0 0 1 1 1 0 1 0 0 0 0 1 0]]
feature_names:
[' ', 'a', 'c', 'd', 'e', 'f', 'h', 'i', 'k', 'n', 'o', 'p', 'r', 's', 't', 'u', 'y']
CountVectorizer: analyzer is callable object
results array:
[[0 0 0 0 0 1]
 [0 0 1 0 0 0]
 [0 0 0 0 1 0]
 [1 0 0 0 0 0]
 [0 0 0 1 0 0]
 [0 1 0 0 0 0]]
feature_names:
['Fukuoka', 'Kouchi', 'Osaka', 'Sapporo', 'Sendai', 'Tokyo']
~~~

### 結果から見えるもの
- `fit_transform`した結果の配列をDataFrameの値部分と考えると、Indexを元の配列、Columnを特徴（feature_name）とすることが出来る。<br />例として、一番最初の結果を表とすると以下のようになる。

| Index | fukuoka | kouchi | osaka | sapporo | sendai | tokyo |
| :---: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: |
| Tokyo | 0 | 0 | 0 | 0 | 0 | 1 |
| Osaka | 0 | 0 | 1 | 0 | 0 | 0 |
| Sendai | 0 | 0 | 0 | 0 | 1 | 0 |
| Fukuoka | 1 | 0 | 0 | 0 | 0 | 0 |
| Sapporo | 0 | 0 | 0 | 1 | 0 | 0 |
| Kouchi | 0 | 1 | 0 | 0 | 0 | 0 |

- 文字列で指定した場合は特徴（feature_name）は基本的にlower case
    - 公式を見ると、キーワード引数として`lowercase`が存在し、デフォルト値が`True`となっているので、ここの設定を変えることで変わるかも
- 特徴（feature_name）は文字の昇順で格納される
- `analyzer`に`char_wb`を指定した場合は、特徴（feature_name）に`' '`が含まれる
    - おそらくは単語境界を`' '`としている、2回カウントされるのは、前後を1回ずつ単語境界とカウントしているから？
- カテゴリ変数のエンコーディングに使う場合、lower caseな単語のみが格納されていて、単純にOne-hotエンコードの結果を取得するだけだけなら、`analyzer = 'word'`でいけるのでは？ 

## 文章の配列で、`fit_transform`での特徴抽出と特徴を出力
```python
print("CountVectorizer: target is texts")

X = cv_word.fit_transform(texts)
print("CountVectorizer: analyzer is word")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_word.get_feature_names()))

X = cv_char.fit_transform(texts)
print("CountVectorizer: analyzer is char")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_char.get_feature_names()))

X = cv_wb.fit_transform(texts)
print("CountVectorizer: analyzer is char_wb")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_wb.get_feature_names()))

X = cv_callable.fit_transform(texts)
print("CountVectorizer: analyzer is callable object")
print("results array:")
print(str(X.toarray()))
print("feature_names:")
print(str(cv_callable.get_feature_names()))
```

## 文章で試した出力結果
~~~
CountVectorizer: target is texts
CountVectorizer: analyzer is word
results array:
[[1 0 0 1 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0]
 [1 0 0 1 0 0 1 1 1 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 1 0 0]
 [0 0 1 1 1 1 1 1 1 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0]
 [0 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 1 0 0 0 1]
 [0 0 0 1 1 0 1 1 1 0 0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 0 0]
 [0 0 0 1 0 0 2 1 0 2 0 1 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0]]
feature_names:
['biggest', 'called', 'china', 'city', 'from', 'fukuoka', 'in', 'is', 'japan', 'japanese', 'koria', 'kouchi', 'miyako', 'mori', 'most', 'nearly', 'no', 'osaka', 'region', 'russia', 'sapporo', 'secondary', 'sendai', 'shikoku', 'the', 'tokyo', 'we']
CountVectorizer: analyzer is char
results array:
[[6 0 1 2 1 1 0 2 0 2 1 4 1 1 0 0 2 2 1 0 2 4 0 0 2]
 [7 0 1 5 1 2 1 3 0 2 1 4 1 1 0 0 3 2 1 1 4 3 0 0 2]
 [9 1 1 6 0 2 0 1 2 0 1 5 1 3 1 2 4 4 1 3 2 2 2 0 2]
 [5 0 1 3 0 1 2 3 0 0 0 3 0 1 2 2 2 3 0 1 1 0 0 1 1]
 [8 0 1 5 0 1 0 1 1 0 0 4 1 0 1 2 3 4 3 4 5 2 1 0 2]
 [9 1 1 5 0 2 0 5 0 1 2 7 2 3 0 0 5 3 2 1 4 1 2 0 1]]
feature_names:
[' ', ',', '.', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'w', 'y']
CountVectorizer: analyzer is char_wb
results array:
[[14  0  1  2  1  1  0  2  0  2  1  4  1  1  0  0  2  2  1  0  2  4  0  0
   2]
 [16  0  1  5  1  2  1  3  0  2  1  4  1  1  0  0  3  2  1  1  4  3  0  0
   2]
 [20  1  1  6  0  2  0  1  2  0  1  5  1  3  1  2  4  4  1  3  2  2  2  0
   2]
 [12  0  1  3  0  1  2  3  0  0  0  3  0  1  2  2  2  3  0  1  1  0  0  1
   1]
 [18  0  1  5  0  1  0  1  1  0  0  4  1  0  1  2  3  4  3  4  5  2  1  0
   2]
 [20  1  1  5  0  2  0  5  0  1  2  7  2  3  0  0  5  3  2  1  4  1  2  0
   1]]
feature_names:
[' ', ',', '.', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'w', 'y']
CountVectorizer: analyzer is callable object
results array:
[[0 0 0 0 0 1]
 [0 0 1 0 0 0]
 [1 0 0 0 0 0]
 [0 0 0 0 1 0]
 [0 0 0 1 0 0]
 [0 1 0 0 0 0]]
feature_names:
['Fukuoka is most nearly city from Koria, China in japan.', 'Kouchi is a japanese city, in japanese region in Shikoku.', 'Osaka is the secondary biggest city in japan.', 'Sapporo is most nearly city from Russia in japan.', 'Sendai called we Mori no Miyako.', 'Tokyo is the biggest city in japan.']
~~~

### 結果から見えるもの
- `analyzer`に`char`と`char_wb`を指定した場合の結果を比較すると、`analyzer = 'char'`の場合の`' '`の抽出回数を`x`としたとき、`analyzer = 'char_wb'`の場合の`' '`の抽出回数は**2x+2**回になる
    - `analyzer = 'char'`の場合は単純に文章中に含まれる`' ' `の数、`analyzer = 'char_wb'`の場合は加えて単語の前後が`' '`ではない場合も境界として`' '`に計上している
- `analyzer = lambda x: [x]`としたインスタンスで実行した場合、入力した文章が特徴（feature_name）としてそのまま戻ってくる。