近年の本格ミステリを可視化する【テキストマイニング】

## はじめに
本記事では、Pythonによるテキストマイニングの簡単なチュートリアルとして、ここ8年ほどの間に刊行された「本格ミステリ」に分類される小説のキーワードを解析し、可視化することを目指します。

## 使用するデータ
[出版書誌情報データベース（Books）](https://www.books.or.jp/)において、検索キーワードを「本格ミステリ」に設定し、ヒットした書誌の内容紹介からあらすじに相当する部分をExcelに手作業で（！）切り貼りすることで、本格推理小説505作のあらすじをまとめました。

![スクリーンショット 2022-09-11 15.47.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1940763/74bdf0d6-251f-deaf-2f48-69659c9315ff.png)

## 実装
あらすじから内容を象徴するようなキーワードを抽出し、その出現の頻度や関係を可視化することで分析を行います。なお、添付したコードはGoogle Colaboratoryでの実行を想定しています。

### 必要なライブラリのインストール
形態素解析(文から単語を切り出し、品詞等の判別を行うこと)にはPythonの言語処理ツールである<b>GiNZA</b>を、解析結果の可視化には自然言語の可視化を手軽にできるようにしたパッケージ<b>[nlplot](https://www.takapy.work/entry/2020/05/17/192947)</b>を用います。

```
!pip install -U ginza ja-ginza
!pip install nlplot
```
GiNZAはspaCyという汎用的な多言語の自然言語処理ライブラリのモデルの一つとして作動します。「ja_ginza」を読み込むことで以下のように、単語分割や原形の獲得、品詞の判別まで行うことができます。
```ginza.py
import spacy
nlp = spacy.load("ja_ginza")
text = "隣の客はよく柿食う客だ。"
doc = nlp(text)
for token in doc:
  print("{}\t{}\t{}\t{}".format(token,token.lemma_,token.pos_,token.tag_))
```
```
隣	隣	NOUN	名詞-普通名詞-一般
の	の	ADP	助詞-格助詞
客	客	NOUN	名詞-普通名詞-一般
は	は	ADP	助詞-係助詞
よく	よく	ADV	副詞
柿	柿	NOUN	名詞-普通名詞-一般
食う	食う	VERB	動詞-一般
客	客	NOUN	名詞-普通名詞-一般
だ	だ	AUX	助動詞
。	。	PUNCT	補助記号-句点
```

### データフレームの作成
各行にあらすじをまとめたエクセルファイルを、pandasのデータフレームとして読み込みます。
```pandas.py
import pandas as pd
df = pd.read_excel(<エクセルファイルへのパス>,names=["あらすじ"],header=None)
df
```
![スクリーンショット 2022-09-11 16.11.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1940763/5d0e5704-0a84-13d5-eeef-b28622400c6b.png)

### 形態素解析
あらすじを分かち書きした上で、意味を持つと考えられる、名詞、動詞、形容詞（自立語と呼ばれるようです）のみを取り出します。動詞、形容詞については原形に直した上で格納します。また、人名などの固有名詞もここで除去されます。
```morphological_analysis.py
from tqdm import tqdm
words_list = []
for story in tqdm(df["あらすじ"]):
  doc = nlp(story)
  words = []
  for token in doc:
    if token.tag_ in ["名詞-普通名詞-一般","動詞-一般","形容詞-一般"]:
      words.append(token.lemma_)
  words_list.append(" ".join(words))
```

### 特徴語抽出
あらすじから取り出した単語のうち、特に重要と思われるものを選別します。文書中の重要な単語（重要語、あるいは特徴語）を抽出するための指標として有名なものに、TF-IDFがあります。これは簡単にいうと、他の文書と比べてある単語の出現頻度が相対的にどれくらい高いかを示すものです。例えば、「ミステリ」や「事件」という単語は大半のあらすじで登場するため、その単語自体の重要性は低いと見做すことができ、TF-IDFも小さい値を取りやすくなります。今回はこのTF-IDFがある閾値を超えた単語をその小説の特徴語として採用し、データフレームの新たな列に保管します。
```feature_word_extraction.py
from sklearn.feature_extraction.text import TfidfVectorizer
df["特徴語"] = pd.NA
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(words_list)
words = vectorizer.get_feature_names()
for book_id, vec in zip(range(len(words_list)), X.toarray()):
  feature_words = []
  for w_id, tfidf in sorted(enumerate(vec), key=lambda x: x[1], reverse=True):
    if tfidf < 0.1:
      break
    feature_word = words[w_id]
    feature_words.append(feature_word)  
  df.at[book_id,"特徴語"] = feature_words
df
```
![スクリーンショット 2022-09-11 16.53.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1940763/74736c5e-e022-b8a7-8bc4-15c161f873d9.png)



### ストップワードの設定
一般的で役に立たない等の理由で処理対象外とする単語のことをストップワードといいます。ここでは、出現頻度が低い単語に加え、筆者が「小説の内容、種別とは関係がない」と判断した単語をストップワードに設定します。
```stopwords.py
import nlplot
npt = nlplot.NLPlot(df, target_col="特徴語")
stopwords = npt.get_stopword(top_n=0,min_freq=3) + ["こと","いう","起こる","起きる","もつ","ミステリー","ミステリ","小説","傑作","著者","よる","とも","思う","つく","はず"]
```

### 可視化
#### 出現頻度
特徴語として現れた単語を、多い順に表示します。
```unigram.py
npt.bar_ngram(
    title='uni-gram',
    xaxis_label='word_count',
    yaxis_label='word',
    ngram=1,
    top_n=30,
    stopwords=stopwords,
)
```
![newplot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1940763/84fa0dab-28d6-ec27-92df-9324113fa93d.png)

まあそうなるよねと呟きたくなる、非常に妥当な結果になっているように思われます。

#### ワードクラウド
ワードクラウドとは、文章中で出現頻度が高い単語を複数選び出し、その頻度に応じた大きさで図示する手法のことです。文章の内容を視覚的に美しく表し、一目で印象づけることができます。
```word_cloud.py
npt.wordcloud(
    max_words=100,
    max_font_size=100,
    colormap='tab20_r',
    stopwords=stopwords,
)
```
![word_cloud.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1940763/726ab49e-c7d7-bfea-d18e-735d728b661c.png)

ミステリファン垂涎の単語たちが並び、壮観です。

#### 共起ネットワーク
共起ネットワークとは、たくさんの文書で共通して登場する(共起する)特徴語同士をエッジで結んだグラフです。単語同士のつながりを可視化することで、意味の理解を深めることができます。また、ノードの大きさは（概ね）出現頻度に比例します。
```co-occurrence_network.py
npt.build_graph(stopwords=stopwords, min_edge_frequency=5)
npt.co_network(
    title='Co-occurrence network',
)
```
![newplot (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1940763/b462d4a2-a7d3-b7e6-490b-721c4ec9c197.png)

ノードの色は特に結合の強いもの同士をまとめた「コミュニティ」を表しています。「事件」「名探偵」「密室」などミステリに欠かせない言葉を中心にして、右側にはおそらく「学園ミステリ」に相当するクラスタ（黄緑色）、中央付近には「警察モノ」にそうとするクラスタ（緑色）、そして左側には王道の「本格ミステリ」のクラスタ（水色）ができているように見受けられます。

## まとめ
今回は結果を解釈するのみに留まり、何か新しく有用な知見を得るところまでは届きませんでしたが、こうしたテキストの分析と可視化は、膨大なテキストデータから経済的・学術的に有用性のある情報を引き出す可能性を秘めています。類義語を同一視する、分割されてしまった複合語を一つにまとめる（今回だと「ダイイング」と「メッセージ」など)、データの他の属性（例えば筆者や出版年月）と紐づける、などの工夫によって解析の精度と幅は向上するかもしれません。

また単純に、いつもとは違う視点でテキストを眺められるところに新鮮さと面白みがあります。自分の関心があるデータセットを用意して試してみるのもまた一興なのではないでしょうか。

## 参考書籍・サイト
[実践Data Scienceシリーズ　Pythonではじめるテキストアナリティクス入門 ](https://www.amazon.co.jp/-/en/榊剛史-ebook/dp/B0B2143QXG/ref=sr_1_1?crid=1THE9D9GLWL4N&keywords=pythonではじめるテキストアナリティクス入門)

[【python】TF-IDFで重要語を抽出してみる](https://www.haya-programming.com/entry/2018/07/09/190819)