sklearnテキスト分類で、vectorizerの種類とオプション指定が精度に与える影響を検証する

# 概要
`from sklearn.feature_extraction.text`のvectorizerの種類やオプションの指定によって、カテゴリ分類の精度がどのように変わるかを実験しました。具体的には、以下の要因について比較しました。

|要因|水準数|水準|
|---|---|---|
|vectorizer|3|CountVectorizer, TfidfVectorizer, HashingVectorizer|
|tokenizer|2|char, word|
|ngram_range|3|(1,1), (1,3), (3,3)|
|binary|2|True,False|

# 動機
sklearnでテキスト分類を使うときにvectorizerやオプションの指定をどうしたらいいかよくわからなかったので、感覚を掴むために実験しました。

# 結果
## 精度
教師データとしてlivedoorニュースコーパスから抽出したタイトルとカテゴリのペア7367件（カテゴリ数9）を用いました。半数のデータを使ってナイーブベイズで学習した分類モデルの精度を残りのデータで評価する、という試行を各学習条件に対して１００回ずつ行いました。結果を以下に示します。

![accuracy_boxplot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/1b8d813f-5f6b-0ab0-c1f2-bd559a5f70c9.png)

精度の平均値が最も高かったのは、(vectorizer, tokenizer, ngram_range, binary) = (CountVectorizer	, word, (1,3), True)の条件で、平均値は0.812でした。精度の平均値が最も低かったのは、(vectorizer, tokenizer, ngram_range, binary) = (HashingVectorizer, word, (3,3), False)の条件で、平均値は0.812でした。
要因別で見ると、vectorizerについては、HashingVectorizerがCountVectorizer、TfidfVectorizerよりも全体的に小さいです。tokenizerとngram_rangeは強めの交互作用がありそうで、(tokenizer, ngram_range)=(char, (1,1))または(word, (3,3))のときに精度が低くなっていました。binaryはTrueのほうが全体的に精度が高いですが、影響は相対的に小さめにみえます。

vectorizer, tokenizer, ngram_range, binaryの４要因について対応ありの分散分析の結果を示します。対応は、試行ごとに用いた学習データの対応です。

```
                                  Anova
=========================================================================
                                         F Value   Num DF  Den DF  Pr > F
-------------------------------------------------------------------------
vectorizer                              53443.2497 2.0000 198.0000 0.0000
tokenizer                                1898.8320 1.0000  99.0000 0.0000
ngram_range                             29846.5163 2.0000 198.0000 0.0000
binary                                   3226.5334 1.0000  99.0000 0.0000
vectorizer:tokenizer                      538.8225 2.0000 198.0000 0.0000
vectorizer:ngram_range                   2122.0846 4.0000 396.0000 0.0000
tokenizer:ngram_range                   78421.4171 2.0000 198.0000 0.0000
vectorizer:binary                         133.0737 2.0000 198.0000 0.0000
tokenizer:binary                         1403.4966 1.0000  99.0000 0.0000
ngram_range:binary                       1179.3318 2.0000 198.0000 0.0000
vectorizer:tokenizer:ngram_range         1968.6917 4.0000 396.0000 0.0000
vectorizer:tokenizer:binary               576.0826 2.0000 198.0000 0.0000
vectorizer:ngram_range:binary             688.8808 4.0000 396.0000 0.0000
tokenizer:ngram_range:binary              955.2296 2.0000 198.0000 0.0000
vectorizer:tokenizer:ngram_range:binary   930.2602 4.0000 396.0000 0.0000
=========================================================================
```

すべての要因の主効果および交互作用が有意でした。
F値に注目すると、tokenizerとngram_rangeの交互作用、vectorizerの主効果が目立って大きく、ngram_rangeの主効果は交互作用がより強く出ているのでとばすと、binaryの主効果が続いています。ただしbinaryの主効果はそれより上位の効果に比べればかなり小さいです。
グラフからうける印象に沿った結果となっていることがわかります。

下位検定については、グラフを見れば雰囲気はなんとなくわかるので、今回は省略します。

## 時間
精度評価の際に、分類機の学習にかかる時間も計測していましたので、その結果を以下に示します。

![time_boxplot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/30a0cb51-7ce1-45be-0629-d9396154a3c9.png)


時間はまず、外れ値はありますが、すべての条件における殆どの試行が(1回あたり）0.2秒以下となっており、試行を数百回、数千回と繰り返すケースでなければ、あまり意識する必要はなさそうです。
一応、学習時間を比較してみると、ngram_rangeの影響が強く、特にngram_rangeが(1,3)や(3,3)のときに学習時間が長くなる傾向がありそうです。またbinaryもTrueのときに学習時間が長くなる傾向がありそうです。

精度のときと同様に、vectorizer, tokenizer, ngram_range, binaryの４要因対応あり分散分析の結果を示します。

```
                                 Anova
========================================================================
                                         F Value  Num DF  Den DF  Pr > F
------------------------------------------------------------------------
vectorizer                                27.3369 2.0000 198.0000 0.0000
tokenizer                                 63.1572 1.0000  99.0000 0.0000
ngram_range                             1095.7163 2.0000 198.0000 0.0000
binary                                   661.9470 1.0000  99.0000 0.0000
vectorizer:tokenizer                      21.8070 2.0000 198.0000 0.0000
vectorizer:ngram_range                   105.7113 4.0000 396.0000 0.0000
tokenizer:ngram_range                     46.4642 2.0000 198.0000 0.0000
vectorizer:binary                         11.3311 2.0000 198.0000 0.0000
tokenizer:binary                           9.2247 1.0000  99.0000 0.0031
ngram_range:binary                        56.8929 2.0000 198.0000 0.0000
vectorizer:tokenizer:ngram_range          21.8805 4.0000 396.0000 0.0000
vectorizer:tokenizer:binary               19.7996 2.0000 198.0000 0.0000
vectorizer:ngram_range:binary             14.5234 4.0000 396.0000 0.0000
tokenizer:ngram_range:binary               1.4395 2.0000 198.0000 0.2395
vectorizer:tokenizer:ngram_range:binary    9.6528 4.0000 396.0000 0.0000
========================================================================
```

tokenizer, ngram_range, binaryの三重交互作用を除くすべての交互作用と主効果が有意でした。
F値が大きいのはngram_rangeの主効果、binaryの主効果の２つで、グラフの印象と一致する結果となっています。

# 考察
## tokenizerとngram_range
精度に関して最も影響が強かったのは、tokenizerとngram_rangeの交互作用でした。具体的には、(tokenizer, ngram_range)=(char, (1,1)) or (word, (3,3))のときに精度が低いという結果でした。(char, (1,1))は文字単位のユニグラム、(word, (3,3))は単語単位のトリグラムです。それぞれトークン化の単位として細かすぎ、荒すぎたため、精度が悪くなったと考えられます。
上記以外のtokenizerとngram_rangeの組み合わせに関しては、(word, (1,3))が少しだけ高いといえば高いですが、概ね似たような精度でした。
個人的にはそこまで精度が変わらないのであれば、形態素解析辞書を使わずに済む、(char, (3,3))が一番手軽で便利だと思いました。

なお、荒い方に関しては、より多くの学習データがある状況だったり、分類対象が長文（今回は記事タイトルで比較的短め）のケースでは、高精度が出ていた可能性もあるため、あくまで、今回用いた学習データ、評価データにおいてはの話となります。学習データの量や一文の長さと、トークン化の粗さの関係については機会があれば、別途検証してみたいです。

## vectorizer
vectorizerはtokenizerとngram_rangeの交互作用の次に影響が強かったです。
水準の差をみると、CountVectorizerとTfidfVectorizerは前者のほうが少し精度がいいですが、ほぼ同程度、HashingVectorizerが相対的に少し低めという結果でした。
[HashingVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.HashingVectorizer.html)については、算出のロジックをよく理解できていないうえ、ナイーブベイズ分類器似入力するために負値がでないようにするパラメータ設定を行ったりしているので、他２つと正当な比較ができているのかはよくわかりません。

CountVectorizerがTfidfVectorizerと同等以上の精度になっているのは少し意外でした。Tfidfは単語の登場頻度などを考慮した「気の利いた」ベクトル化になっているため、精度にいい影響があるかと思っていました。結果から考えれば、ナイーブベイズの学習自体が単語の登場頻度等を考慮した計算であるため、Tfidfとある意味で役割がかぶっており、入力値はシンプルなカウントベクトルのほうが適していたということなのかもしれません。ナイーブベイズ以外で学習したときにどういう傾向がでるのか気になります。

## binary
影響は小さいものの、binary=Trueのときには、全体的に精度が高くなる傾向がありました。binaryオプションがTrueのとき、一つの文に単語が複数回登場していても１回のみの登場として扱われるようになります（HashingVectorizerの場合、意味合いが少し違うかもしれないのですが深く理解できていません）。分類タスクにおいては単語が何文書に登場したかが重要であり、各文書に何回登場したかという情報を含めること（つまりbinary=False）は、ノイズになるということなのかもしれません。

## 制限事項
今回用いたデータはライブドアニュースコーパスの記事タイトルとカテゴリです。また、学習モデルはナイーブベイズに固定しています。ベクトル化においては、注目したパラメータ以外（例えばトークンとして採用する登場回数の最小、最大しきい値やstop_wordsなどの設定）はデフォルト値を使用しています。ただしHashingVectorizerはn_featuresは学習時間短縮のため、デフォルト値(2^20)ではなく2^16としており、ナイーブベイズ学習の妨げとなる負値を登場させないため、alternative_signをTrueにしています。tokenizer=wordの条件では、分かち書きのためmecabおよび辞書としてmecab-ipa-neologdを使用しています。これらの状況以外では、結果が変わる可能性があります。

## 次に実装するとしたら
最も精度が高かったのは単語で分かち書きする条件でしたが、分かち書きは辞書の影響もうけるので、思考停止である程度精度高く実装したい場合は、(vectorizer, tokenizer, ngram_range, binary) = (CountVectorizer, char, (3,3), True)の条件が、良さそうな気がします。
データ数が多かったり入力が長文であることが期待できるなら単語分割にしたり、ngram_rangeを大きくすることを試し見ても良いかもしれません。

# 方法
再現用に実験、分析に使ったコードを記載します。ノートブック（一部ターミナル）での実行を想定しています。

## 環境

ターミナルで実行

```bash
% sw_vers 
ProductName:	macOS
ProductVersion:	11.6.1
BuildVersion:	20G224
% python -V
Python 3.8.6
```

## インストール

```
pip install scikit-learn
pip install mecab-python3
pip install pingouin
pip install scipy
pip install statsmodels
pip install matplotlib
```
## データの用意

ターミナルで実行

```bash
curl -O "https://www.rondhuit.com/download/ldcc-20140209.tar.gz" #macの場合
tar zxvf ldcc-20140209.tar.gz
```

以下はpythonで実行

```Python
import os
import pandas as pd
path = os.path.join("text")

categories, titles, contents = [], [], []
dirs = [ v for v in os.listdir(path) if os.path.isdir(os.path.join(path, v)) ]
for dir in dirs:
  files = [os.path.join(path,dir, v) for v in os.listdir(os.path.join(path,dir)) if v.startswith(dir)]
  for file in files:
    with open(file, "r", encoding="utf-8") as f:
      lines = f.read().splitlines()
      categories.append(dir)
      titles.append(lines[2])
      contents.append(" ".join(lines[3:]))
df = pd.DataFrame({
  "category": categories,
  "title": titles,
  "content": contents
})
df.to_csv("livedoor_news.tsv", sep="\t", index=False,header=None)
```

## データの読み込み

```Python
import pandas as pd
df = pd.read_csv("livedoor_news.tsv", sep="\t", names=["label", "title", "text"])
df.head()
```

```
	label	title	text
0	movie-enter	【DVDエンター！】誘拐犯に育てられた女が目にした真実は、孤独か幸福か	2005年11月から翌2006年7月まで読売新聞にて連載された、直木賞作家・角田光代による...
1	movie-enter	藤原竜也、中学生とともにロケット打ち上げに成功	「アンテナを張りながら生活をしていけばいい」 　2月28日、映画『おかえり、はやぶさ』（...
2	movie-enter	『戦火の馬』ロイヤル・プレミアにウィリアム王子＆キャサリン妃が出席	3月2日より全国ロードショーとなる、スティーブン・スピルバーグの待望の監督最新作『戦火の馬...
3	movie-enter	香里奈、女子高生100人のガチンコ質問に回答「ラーメンも食べる」	女優の香里奈が18日、都内で行われた映画『ガール』（5月26日公開）の女子高生限定試写会に...
4	movie-enter	ユージの前に立ちはだかったJOY「僕はAKBの高橋みなみを守る」	5日、東京・千代田区の内幸町ホールにて、映画『キャプテン・アメリカ/ザ・ファースト・アベン...
```

## 学習用の関数の定義

以下の関数は、与えられたvectorizer, modelを用いて、100回学習試行をし、試行ごとの精度と所要時間のリストをします。

```Python
#vectorizerをimport
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer, HashingVectorizer
#multinomialnbをimport
from sklearn.naive_bayes import MultinomialNB
# accuracy_scoreをimport
from sklearn.metrics import accuracy_score
# train_test_splitをimport
from sklearn.model_selection import train_test_split
# timeをimport
import time

# 100回学習して、試行ごとの精度と所要時間のリストを返す
def calc_accuracy(texts, labels, model, vectorizer):
  X = vectorizer.fit_transform(texts)

  scores, times = [], []
  for random_state in range(100):
    X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.5, shuffle=True, random_state=random_state, stratify=labels)
    start = time.time()
    model.fit(X_train, y_train)
    end = time.time()
    times.append(end - start)

    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    scores.append(accuracy)
  return scores, times
```

以下の関数は日本語テキストをmecabで分かち書きしたリストにして返します。
mecabの辞書はデフォルトのものを使用しており、筆者の環境ではmecab-ipadic-neologdです。

```Python
import MeCab
mecab = MeCab.Tagger("-Owakati")

def parse_text(text):
  return mecab.parse(text).strip().split()
```

## 条件ごとの学習結果の取得

```Python
#vectorizerをimport
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer, HashingVectorizer
#multinomialnbをimport
from sklearn.naive_bayes import MultinomialNB

scores, times = [],[]
models, vectorizers = [], []
tokenizers, ngram_ranges, binaries = [],[],[]
random_states = []

# ngram_rangeとbinaryだけはループ変数として用意し、その他の条件はループの中で直接書く。
for ngram_range in [(1,1),(1,3),(3,3)]: # ngram_rangeの水準パターン
  for binary in [True, False]: # binaryの水準パターン
    #ループの進捗確認用の出力
    print("ngram_range: {}, binary: {}".format(ngram_range, binary))
    # ナイーブベイズモデルを宣言
    model = MultinomialNB()

    i=-1
    # 進捗確認用の出力
    i+=1; print(i)

    # CountVectorizer、tokenizerはmecab
    vectorizer = CountVectorizer(tokenizer=parse_text, ngram_range=ngram_range, binary=binary)
    s, t = calc_accuracy(df.title, df.label, model, vectorizer)
    scores += s
    times += t
    models += ["MultinomialNB"] * len(s)
    vectorizers += ["CountVectorizer"] * len(s)
    tokenizers += ["mecab-neologd"] * len(s)
    ngram_ranges += ["({},{})".format(ngram_range[0], ngram_range[1])] * len(s)
    binaries += [binary] * len(s)
    random_states += list(range(100))

    i+=1; print(i)
    # CountVectorizer、tokenizerはchar
    vectorizer = CountVectorizer(analyzer="char", ngram_range=ngram_range, binary=binary)
    s, t = calc_accuracy(df.title, df.label, model, vectorizer)
    scores += s
    times += t
    models += ["MultinomialNB"] * len(s)
    vectorizers += ["CountVectorizer"] * len(s)
    tokenizers += ["char"] * len(s)
    ngram_ranges += ["({},{})".format(ngram_range[0], ngram_range[1])] * len(s)
    binaries += [binary] * len(s)
    random_states += list(range(100))

    i+=1; print(i)
    # TfidfVectorizer、tokenizerはmecab
    vectorizer = TfidfVectorizer(tokenizer=parse_text, ngram_range=ngram_range, binary=binary)
    s, t = calc_accuracy(df.title, df.label, model, vectorizer)
    scores += s
    times += t
    models += ["MultinomialNB"] * len(s)
    vectorizers += ["TfidfVectorizer"] * len(s)
    tokenizers += ["mecab-neologd"] * len(s)
    ngram_ranges += ["({},{})".format(ngram_range[0], ngram_range[1])] * len(s)
    binaries += [binary] * len(s)
    random_states += list(range(100))

    i+=1; print(i)
    # TfidfVectorizer、tokenizerはchar
    vectorizer = TfidfVectorizer(analyzer="char", ngram_range=ngram_range,binary=binary)
    s, t = calc_accuracy(df.title, df.label, model, vectorizer)
    scores += s
    times += t
    models += ["MultinomialNB"] * len(s)
    vectorizers += ["TfidfVectorizer"] * len(s)
    tokenizers += ["char"] * len(s)
    ngram_ranges += ["({},{})".format(ngram_range[0], ngram_range[1])] * len(s)
    binaries += [binary] * len(s)
    random_states += list(range(100))

    i+=1; print(i)
    # HashingVectorizer、tokenizerはmecab
    vectorizer = HashingVectorizer(tokenizer=parse_text, ngram_range=ngram_range, alternate_sign=False, n_features=2**16, binary=binary)
    s, t = calc_accuracy(df.title, df.label, model, vectorizer)
    scores += s
    times += t
    models += ["MultinomialNB"] * len(s)
    vectorizers += ["HashingVectorizer"] * len(s)
    tokenizers += ["mecab-neologd"] * len(s)
    ngram_ranges += ["({},{})".format(ngram_range[0], ngram_range[1])] * len(s)
    binaries += [binary] * len(s)
    random_states += list(range(100))

    i+=1; print(i)
    # HashingVectorizer、tokenizerはchar
    vectorizer = HashingVectorizer(analyzer="char", ngram_range=ngram_range, alternate_sign=False,n_features=2**16,binary=binary)
    s, t = calc_accuracy(df.title, df.label, model, vectorizer)
    scores += s
    times += t
    models += ["MultinomialNB"] * len(s)
    vectorizers += ["HashingVectorizer"] * len(s)
    tokenizers += ["char"] * len(s)
    binaries += [binary] * len(s)
    ngram_ranges += ["({},{})".format(ngram_range[0], ngram_range[1])] * len(s)
    random_states += list(range(100))

```

## 結果の保存

```Python
#結果のデータフレーム化
df_accuracy = pd.DataFrame({
  "model": models,
  "vectorizer": vectorizers,
  "tokenizer": tokenizers,
  "ngram_range": ngram_ranges,
  "binary": binaries,
  "random_state": random_states,
  "accuracy": scores,
  "time": times
})

#結果の保存
df_accuracy.to_csv("df_accuracy.tsv", index=False, sep="\t")
```

## 結果の読み込み、確認

```Python
#結果の読み込み
df_accuracy = pd.read_csv("df_accuracy.tsv", sep="\t")
#結果の出力
df_accuracy.head()
```

## 要約情報の取得、保存

分析には直接関係ないですが、各条件の平均値、標準偏差を数値で確認しておきます。

```Python
# 結果の概要の表示
# accuracyのmeanを取得
df_describe = df_accuracy.groupby(["model", "vectorizer", "tokenizer", "ngram_range","binary"],as_index=False)["accuracy"].mean()
# accuracyの列名をaccuracy_meanに変更
df_describe.columns = ["model", "vectorizer", "tokenizer", "ngram_range", "binary","accuracy_mean"]
# accuracyのstdを取得
df_describe["accuracy_std"] = df_accuracy.groupby(["model", "vectorizer", "tokenizer", "ngram_range","binary"],as_index=False)["accuracy"].std()["accuracy"]
# timeのmeanを取得
df_describe["time_mean"] = df_accuracy.groupby(["model", "vectorizer", "tokenizer", "ngram_range","binary"],as_index=False)["time"].mean()["time"]
# timeのstdを取得
df_describe["time_std"] = df_accuracy.groupby(["model", "vectorizer", "tokenizer", "ngram_range","binary"],as_index=False)["time"].std()["time"]

# 結果の保存
df_describe.to_csv("df_describe.tsv",sep="\t",index=False)
# 結果の表示
df_describe.head()
```

```
	model	vectorizer	tokenizer	ngram_range	binary	accuracy_mean	accuracy_std	time_mean	time_std
0	MultinomialNB	CountVectorizer	char	(1,1)	False	0.709704	0.006653	0.013404	0.003324
1	MultinomialNB	CountVectorizer	char	(1,1)	True	0.723350	0.006072	0.020566	0.013112
2	MultinomialNB	CountVectorizer	char	(1,3)	False	0.797581	0.005588	0.059319	0.007868
3	MultinomialNB	CountVectorizer	char	(1,3)	True	0.807527	0.005507	0.090673	0.020877
4	MultinomialNB	CountVectorizer	char	(3,3)	False	0.798833	0.005596	0.041194	0.005625
```

## グラフの表示保存

accuracyに関する箱ひげ図を作成します。

```Python
#accuracyの全結果
import matplotlib.pyplot as plt
fig = df_accuracy.boxplot(column="accuracy", by=["vectorizer", "tokenizer", "ngram_range","binary"],rot=90, figsize=(10,5))

fig.set_ylabel("accuracy")
fig.set_ylim(0,1)
plt.savefig("./accuracy_boxplot.png", bbox_inches="tight", pad_inches=0.1)
plt.show()
```

timeに関する箱ひげ図を作成します。

```Python
fig = df_accuracy.boxplot(column="time", by=["vectorizer", "tokenizer", "ngram_range","binary"],rot=90, figsize=(10,5))
fig.set_ylabel("time (sec) per single train")
fig.set_ylim(0,1)
plt.savefig("./time_boxplot.png", bbox_inches="tight", pad_inches=0.1)
plt.show()
```

## 分散分析

必要ライブラリを読み込みます。

```Python
import statsmodels.api as sm
from statsmodels.formula.api import ols
import pandas as pd
import numpy as np
import statsmodels.stats.anova as anova
```

accuracyについての分散分析を実行します。
```Python
aov = anova.AnovaRM(df_accuracy, "accuracy", "random_state",within=["vectorizer", "tokenizer", "ngram_range","binary"])
result = aov.fit()
print(result)
```

timeについての分散分析を実行します。

```Python
aov = anova.AnovaRM(df_accuracy, "time", "random_state",within=["vectorizer", "tokenizer", "ngram_range","binary"])
result = aov.fit()
print(result)
```

## 下位検定

結果では触れていませんが、下位検定をする場合は以下で可能です。

モジュールをimportします。

```Python
#対応ありt検定のモジュールをimport
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd
import pingouin as pg
```

主効果に関する下位検定（t検定）をします。

```Python
#vectorizerのt検定
posthocs = pg.pairwise_ttests(dv='accuracy', within='vectorizer', subject='random_state', padjust="bonf",data=df_accuracy)
print(posthocs.round(3))

#tokenizerのt検定
posthocs = pg.pairwise_ttests(dv='accuracy', within='tokenizer', subject='random_state', padjust="bonf",data=df_accuracy)
print(posthocs.round(3))

#ngram_rangeのt検定
posthocs = pg.pairwise_ttests(dv='accuracy', within='ngram_range', subject='random_state', padjust="bonf",data=df_accuracy)
print(posthocs.round(3))

#binaryのt検定
posthocs = pg.pairwise_ttests(dv='accuracy', within='binary', subject='random_state', padjust="bonf",data=df_accuracy)
print(posthocs.round(3))
```

```
     Contrast                  A                  B  Paired  Parametric  \
0  vectorizer    CountVectorizer  HashingVectorizer    True        True   
1  vectorizer    CountVectorizer    TfidfVectorizer    True        True   
2  vectorizer  HashingVectorizer    TfidfVectorizer    True        True   

         T   dof alternative  p-unc  p-corr p-adjust        BF10  hedges  
0  300.048  99.0   two-sided    0.0     0.0     bonf  5.785e+143  20.653  
1  132.300  99.0   two-sided    0.0     0.0     bonf  1.019e+109   7.593  
2 -206.300  99.0   two-sided    0.0     0.0     bonf  6.997e+127 -12.589  
    Contrast     A              B  Paired  Parametric       T   dof  \
0  tokenizer  char  mecab-neologd    True        True  43.576  99.0   

  alternative  p-unc       BF10  hedges  
0   two-sided    0.0  5.158e+62   3.985  
      Contrast      A      B  Paired  Parametric        T   dof alternative  \
0  ngram_range  (1,1)  (1,3)    True        True -216.416  99.0   two-sided   
1  ngram_range  (1,1)  (3,3)    True        True   48.683  99.0   two-sided   
2  ngram_range  (1,3)  (3,3)    True        True  247.933  99.0   two-sided   

   p-unc  p-corr p-adjust        BF10  hedges  
0    0.0     0.0     bonf  7.548e+129 -15.472  
1    0.0     0.0     bonf   1.649e+67   5.106  
2    0.0     0.0     bonf  4.502e+135  20.772  
  Contrast      A     B  Paired  Parametric       T   dof alternative  p-unc  \
0   binary  False  True    True        True -56.803  99.0   two-sided    0.0   

        BF10  hedges  
0  3.563e+73  -1.222  
```

ngram_rangeとtokenizerの単純主効果の分析をします。

```Python
#ngram_rangeとtokenizerの単純主効果分析
posthocs = pg.pairwise_ttests(dv='accuracy', within=['tokenizer', 'ngram_range'], subject='random_state', padjust="bonf",data=df_accuracy)
print(posthocs.round(3))
```

```
                  Contrast      tokenizer      A              B Paired  \
0                tokenizer              -   char  mecab-neologd   True   
1              ngram_range              -  (1,1)          (1,3)   True   
2              ngram_range              -  (1,1)          (3,3)   True   
3              ngram_range              -  (1,3)          (3,3)   True   
4  tokenizer * ngram_range           char  (1,1)          (1,3)   True   
5  tokenizer * ngram_range           char  (1,1)          (3,3)   True   
6  tokenizer * ngram_range           char  (1,3)          (3,3)   True   
7  tokenizer * ngram_range  mecab-neologd  (1,1)          (1,3)   True   
8  tokenizer * ngram_range  mecab-neologd  (1,1)          (3,3)   True   
9  tokenizer * ngram_range  mecab-neologd  (1,3)          (3,3)   True   

   Parametric        T   dof alternative  p-unc  p-corr p-adjust        BF10  \
0        True   43.576  99.0   two-sided    0.0     NaN      NaN   5.158e+62   
1        True -216.416  99.0   two-sided    0.0     0.0     bonf  7.548e+129   
2        True   48.683  99.0   two-sided    0.0     0.0     bonf   1.649e+67   
3        True  247.933  99.0   two-sided    0.0     0.0     bonf  4.502e+135   
4        True -229.806  99.0   two-sided    0.0     0.0     bonf  2.677e+132   
5        True -173.003  99.0   two-sided    0.0     0.0     bonf  2.368e+120   
6        True   -7.464  99.0   two-sided    0.0     0.0     bonf   2.666e+08   
7        True  -69.816  99.0   two-sided    0.0     0.0     bonf   1.301e+82   
8        True  257.836  99.0   two-sided    0.0     0.0     bonf  2.078e+137   
9        True  356.651  99.0   two-sided    0.0     0.0     bonf   1.29e+151   

   hedges
5 -20.525  
6  -0.607  
7  -4.692  
8  32.934  
9  37.888  
```

# おわりに
scikit-learnでのテキスト分類を最近勉強し始めたのですが、パラメータが多く、何をどう設定すればよいかあまりわからない状態でした。
必ずしも一般性のある結果ではありませんが、各種パラメータが精度にどう影響を与えるのかの感覚がつかめた気がしたので良かったです。
異なる学習モデルやコーパスで学習する検証も機会があればやってみたいです。

# 参考

scikit-learnのドキュメント。特に、CountVectorizer, TfidfVectorizer, HashingVectorizerに関する部分。
https://scikit-learn.org/stable/modules/classes.html#module-sklearn.feature_extraction.text

対応あり分散分析のパッケージ
https://www.statsmodels.org/dev/generated/statsmodels.stats.anova.AnovaRM.html

pairwise_ttest用のパッケージ
https://pingouin-stats.org/generated/pingouin.ttest.html