WordCloudを使って単語の頻度解析入門

# はじめに
WordCloudを使って単語の出現頻度を視覚的にわかりやすく表示することができます。
こういうやつです。

![メロスの単語.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/2b1f3965-68a5-4ffa-9217-e95db6b918a9.png)

多く出現するキーワードは大きく表示されます。
見てわかるようにこれは太宰治の「走れメロス」のデータを使っています。
「走れメロス」ではメロスというキーワードが多く使われていることが見てすぐにわかりますね。

まずはこのような画像を`Google Colab`で作成して、その後は`Qiita API`を使って自分の記事を解析して頻出ワードの画像を作ってみたいと思います。

# 記事を取得
はじめに基となるデータを取得します。青空文庫さんがGithubで公開しているのでそれを使いました。

[青空文庫のGithub](https://github.com/aozorabunko/aozorabunko)

[青空文庫のサイト](https://www.aozora.gr.jp/index.html)

まずはサイト検索して目的の「走れメロス」のページに行きます。

URLの`000035`が著者のIDです。
![スクリーンショット 2025-04-02 21.10.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/e5533f6a-e2ff-44fc-8dfb-b78fc7faa4bc.png)

該当ページを下にスクロールするとファイルのダウンロードがあるのでここからダウンロードしてもいいです。
が、`Google Colab`でコマンドで取得したかったのでここから取得はせずにファイル名（リンク）の`1567_ruby_4948.zip`をメモしておいて、青空文庫のGithubに行きます。
![スクリーンショット 2025-04-02 21.12.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/9707c724-f9f0-46af-84c9-ee7609a8e2c8.png)

フォルダを潜っていくと見つけました！
![スクリーンショット 2025-04-02 21.29.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/e943da38-1404-4474-8dbb-4ecd69c4ab65.png)
しかしまだダウンロードはしない。（意地）

リンク先はわかったので`Google Colab`でこのURLからファイルを取得していきたいと思います。

```bash
!wget https://github.com/aozorabunko/aozorabunko/blob/master/cards/000035/files/1567_ruby_4948.zip
!unzip 1567_ruby_4948.zip
```
Google colabでは`!`マークを使ってlinuxコマンドが使えます。（これがしたかった）
`weget`コマンドでファイルを取得して `unzip`コマンドで解凍したいと思います。

```bash
Resolving github.com (github.com)... 140.82.113.3
Connecting to github.com (github.com)|140.82.113.3|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: ‘1567_ruby_4948.zip’

1567_ruby_4948.zip      [ <=>                ] 266.30K  --.-KB/s    in 0.1s    

2025-04-02 12:33:41 (2.66 MB/s) - ‘1567_ruby_4948.zip’ saved [272691]

Archive:  1567_ruby_4948.zip
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of 1567_ruby_4948.zip or
        1567_ruby_4948.zip.zip, and cannot find 1567_ruby_4948.zip.ZIP, period.
```
zipファイルじゃないようなエラーになりました。
原因はURLの途中にある`blob`というURLだとWebページを表示するURLなので直接ダウンロードはできないようでした。
改めて`blob`の部分を`raw`に変更することで無事にファイルを取得できました。

```bash
!wget https://github.com/aozorabunko/aozorabunko/raw/master/cards/000035/files/1567_ruby_4948.zip
!unzip 1567_ruby_4948.zip
```

```bash
# 現在のディレクトリを確認
!ls

# 出力
1567_ruby_4948.zip  hashire_merosu.txt	sample_data
```

「hashire_merosu.txt」が取得できているのを確認できました。
中身を確認してみます。

```bash
!head hashire_merosu.txt

# 出力
���ꃁ���X
���Ɏ�

-------------------------------------------------------
�y�e�L�X�g���Ɍ����L���ɂ��āz

�s�t�F���r
�i��j�גq�\�s�s���Ⴟ�ڂ����Ⴍ�t

�b�F���r�̕t��������̎n�܂����肷��L��
```

めちゃくちゃ文字化けしてますね。文字コードを調べる `nkf`(Network Kanji Filter)というLinuxコマンドを使ってみます。

```bash
# インストール
!apt install nkf
```

`--guess` オプションを使用すると、`nkf` はファイルのエンコーディングを検出し、その結果を出力します。

```bash
!nkf --guess hashire_merosu.txt

# 出力
Shift_JIS (CRLF)
```
文字コードは`Shift_JIS`が使われていることがわかりました。

次にファイルの中身を取り出していきます。その時に`Shift_JIS`も指定します。
```python
text_list = []
with open('hashire_merosu.txt', encoding='shift_jis') as f:
    # readlines()で1行ずつ取り出す
    text_list = f.readlines()
```
これで`text_list`には「走れメロス」の中身が1行ずつリスト形式で格納されているはず。
先頭の5行だけ確認してみます。
```python
text_list[:5]

# 出力
['走れメロス\n',
 '太宰治\n',
 '\n',
 '-------------------------------------------------------\n',
 '【テキスト中に現れる記号について】\n']
```
```python
# 行数を確認
len(text_list)

# 出力
105
```
105行入っていることがわかりました。内容あまり覚えてないですが「走れメロス」意外と短いんですね。

# データの前処理
形態素解析ライブラリの「janome」をインストールします。
<details><summary>形態素解析とは</summary>

ざっくり言うと

- 文章を単語に区切る
- それぞれの単語が名詞、動詞、形容詞…のどれかを判断する

これらをすることです。

「私は学校へ行きます。」という文を形態素解析すると、

- 私/名詞
- は/助詞
- 学校/名詞
- へ/助詞
- 行き/動詞
- ます/助動詞
- 。/記号

となります。
</details>

```bash
!pip install janome
```

Tokenizerを使って形態素解析を行います。
```python
from janome.tokenizer import Tokenizer

t = Tokenizer()

words = []
for text in text_list:                           # 一行ずつ取り出す
    tokens = t.tokenize(text)                    # 取り出した行を解析する
    for token in tokens:                         # 単語ごとに取り出す
        pos = token.part_of_speech.split(',')[0] # 取り出した単語の品詞をposに入れる
        if pos in '名詞':                         # 品詞が名詞だけ指定
            words.append(token.surface)          # 単語をリストに追加する

text = ' '.join(words) # リストの内容をスペース区切りで連結
```

 ```python
# 中身を確認
print(text[100:150])

# 出力
列 始まり 特定 記号 例 疲労 困憊 こんぱい ＃］： 入力 者 注 主 外字 説明 傍点 位置 
 ```

 名詞だけ指定していても関係ないものもありますね。とりあえずこのまま次に進みます。

 # WordCloudで可視化
wordcloud は、デフォルトでは英語のフォントを使用するように設定されています。
日本語を表示するためのフォントをインストールします。
```bash
!apt -y install fonts-ipafont-gothic
```
表示させるために`WordCloud`と`matplotlib`をインポートします。
`matplotlib`はグラフとか図を表示させるライブラリのイメージがありましたが、ここでもWordCloudを表示させるために使います。
```python
from wordcloud import WordCloud
import matplotlib.pyplot as plt
```
次のコードでようやく表示できます。

```python
fpath = '/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf' # フォントを指定
wordcloud = WordCloud(background_color="white", font_path=fpath, width=900, height=500).generate(text) # テキストデータからワードクラウドを生成
plt.figure(figsize=(10, 10)) # 画面の縦横の大きさ
plt.imshow(wordcloud) # 画像準備
plt.axis("off") # 軸を非表示
plt.show # 画像表示
```
![1回目メロス.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/ed2dfc40-5eb5-4d3b-a43b-31530295b74e.png)
できた！
けど「の」とかいらないです。他にも不要な言葉が結構含まれています。
不要な単語は削除したいと思います。

```python
import re
def normalize_text(text):
    # 削除する単語のリスト
    words_to_remove = [
        'あれ', 'うち', 'え', 'えい', 'いま', 'お',
        'こと', 'こ', 'これ', 'さ', 'さま', 'しわ',
        'そ', 'それ', 'た', 'たち', 'ため', 'とき',
        'つもり', 'とこ', 'な', 'なん', 'の', 'ぱい',
        'ほう', 'ま', 'まま', 'も', 'もの', 'や',
        'よう', 'ら', 'ろ', 'ん', 'い', 'め', 'わ', 'ゆ'
    ]

    # 正規表現パターンを生成
    pattern = '|'.join(map(re.escape, words_to_remove))  # 特殊文字をエスケープ

    # 正規表現で単語を削除
    text = re.sub(pattern, "", text)

    # 前後の空白を削除
    text = text.strip()

    return text
```
この関数を使って再度テキストデータを作成していきます
```python
new_text_list = []
for text in text_list:
    text = normalize_text(text)
    new_text_list.append(text)
```
```python
from janome.tokenizer import Tokenizer

t = Tokenizer()
words = []
for text in new_text_list:
    tokens = t.tokenize(text)
    for token in tokens:
        pos = token.part_of_speech.split(',')[0]
        if pos in '名詞':
            words.append(token.surface)

text = ' '.join(words)
```
再度画像を作成してみます。
```python
fpath = '/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
wordcloud = WordCloud(background_color="white", font_path=fpath, width=900, height=500).generate(text)
plt.figure(figsize=(10, 10))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
```

![メロス2回目.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/58b01978-d3cc-4094-ba1b-d306be626077.png)
だいぶいい感じになったんじゃないでしょうか？
配置とかは実行するたびに変わるので気に入らなければ再実行してみてください。

# Qiitaの記事で作成する
次はQiita APIを使って自分の記事を全て取得して、同様に解析してWordCloudの画像を表示させたいと思います。

## Qiitaから投稿一覧を取得する
以下のコードで特定のユーザーの記事を全て取得することができます。今回は私自身の記事を取得していきたいと思います。
まずはマイページに行ってユーザーIDをメモしておきます。
ユーザーIDはURLのこの部分です。

![スクリーンショット 2025-04-03 16.15.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/65b6591a-0c49-4909-a047-0733bc3d1e3d.png)

次に`python`の`requests`を使って記事を取得します。
さっきメモしたユーザーIDをurlの以下の部分に入れてください。
```python
# 使用例
http://qiita.com/api/v2/users/ユーザーID/items
```

以下のようにして取得できます。`requests`の使い方などは今回は省きます。
```python
import requests
url = 'http://qiita.com/api/v2/users/kojiro513/items' # 
response = requests.get(url)
response.encoding = response.apparent_encoding
data = response.json()
```
取得した記事を一つのテキストにまとめます。
```python
all_text = ''
for article in data:
    all_text += article['body'] + '\n' # 各記事のbodyをall_textに追加。記事ごとに改行を入れる
```
一旦確認してみます。
```python
# 文字数を確認
len(all_text)

# 出力
66992
```
現時点で20記事くらい書いていたのでそこそこの量がありました。

## データ整形
すでにわかっている不要なものは削除しようと思います。

```python
def clean_text(text):
  text = re.sub(r'https?://\S+', '', text) # URLの削除
  text = re.sub(r'\n', '', text) # 改行の削除
  text = re.sub(r'[!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~「」…、。]', '', text) # 記号の削除
  return text

cleaned_text = clean_text(all_text)
len(cleaned_text)

# 出力
60983
```
URLや記号を削除して6000文字ほど削減できました。


前回と同様に解析して名詞のみを取り出します。
```python
from janome.tokenizer import Tokenizer

t = Tokenizer()

words = []
for text in text_list:
    tokens = t.tokenize(cleaned_text)
    for token in tokens:
        pos = token.part_of_speech.split(',')[0]
        if pos in '名詞':
            words.append(token.surface)

text = ' '.join(words)
```

```python
text[:100]

# 出力
はじめ 普段 MySQL 使用 こと の PostgreSQL 使用 際 コマンド 違い 備忘録 コマンド 違い # 前提 ローカル インストール Docker PostgreSQL コンテナ 使
```
不要な「こと」や「の」など削除する関数を作成してさらに整形します。
ここの単語は一度WordCoudで画像を作成して不要なものを追加していってます。

```python
import re
def normalize_text(text):
    # 削除する単語のリスト
    words_to_remove = [
        'あれ', 'うち', 'え', 'えい', 'いま', 'お',
        'こと', 'こ', 'これ', 'さ', 'さま', 'しわ',
        'そ', 'それ', 'た', 'たち', 'ため', 'とき',
        'つもり', 'とこ', 'な', 'なん', 'の', 'ぱい',
        'ほう', 'ま', 'まま', 'も', 'もの', 'や',
        'よう', 'ら', 'ろ', 'ん', 'い', 'め', 'わ', 
        'ゆ', 'れ'
    ]

    # 正規表現パターンを生成
    pattern = '|'.join(map(re.escape, words_to_remove))  # 特殊文字をエスケープ

    # 正規表現で単語を削除
    text = re.sub(pattern, "", text)

    # 前後の空白を削除
    text = text.strip()

    return text
```

```python
text = normalize_text(text)
```

```python
len(text)

# 出力
59641
```
さらに1000文字くらい減りました。

## WordCloudで可視化

```python
fpath = '/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf'
wordcloud = WordCloud(background_color="white", font_path=fpath, width=900, height=500).generate(text)
plt.figure(figsize=(10, 10))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
```

![mykiji.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/4d0f14d2-02a2-40d6-97db-09b77cb997ef.png)

できました！ちょっと削除できていない単語もあるようですが、そこそこの出来ではないでしょうか。
もう少しデータ整形の精度を上げたほうが良さそうですが、それは今後の課題にしたいと思います。

## グラフにしてみる
最後に単語の出現回数をグラフにしてみたいと思います。

`matplotlib`で日本語が文字化けしないように以下をインストールします。
```python
!pip install japanize_matplotlib
```

グラフ描画は以下のようにしました。
`seaborn`の`countplot`というやつで回数を数えてグラフにしてくれます。
```python
import japanize_matplotlib
import seaborn as sns
import matplotlib.pyplot as plt
import string

japanize_matplotlib.japanize() # matplotlibを日本語対応にする

# テキストの前処理を行う関数
def preprocess_text(text):
    text = re.sub(r'[0-9/]', '', text) # 数字とスラッシュを削除
    text = re.sub(f"[{re.escape(string.punctuation)}]", '', text) # 句読点を削除
    text = text.lower() # すべて小文字に変換

    return text

processed_text = preprocess_text(text) # データ整形
words = processed_text.split() # 単語のリストを作成

# 単語の出現回数をカウント（word_countsには辞書型でデータが入る）
word_counts = Counter(words)

# 上位20個の単語を抽出 
top_words = [word for word, count in word_counts.most_common(20)]

plt.figure(figsize=(10, 6))
sns.countplot(y=words, order=top_words, palette="pastel") # 単語のリストwordsを渡す
plt.title("単語の出現回数")
plt.xlabel("出現回数")
plt.ylabel("単語")
plt.tight_layout()
plt.show()
```

![count.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3813857/e283316a-4a7f-412e-bd2f-ccc33b0bc98a.png)

こんな感じに出力されました！

# さいごに
最後まで読んでいただきありがとうございます。

これが誰かの参考になれば幸いです。