Pythonプログラミング：Qiita記事のタグからNetworkXを使ってグラフを描画してみた

# はじめに

前回の記事([Qiita記事のタグからTreemapを描画してみた](https://qiita.com/Blaster36/items/8fe99c2d861476ba52cc))では、Qiita記事に付与されたタグを取得し、Treemapを作りました。
今回はそれらタグ間の関係性に着目して、[グラフ](https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%A9%E3%83%95%E7%90%86%E8%AB%96)を描画します。

最終的に、以下のようなモノを作ります。
![trends_of_qiita_networkx_shortver2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/d07c9c11-b4f1-82d3-7ea0-d74c9688be78.png)

## 本稿で紹介すること

- Qiita APIからの記事情報の取得
- グラフの描画

以下のリンク、4つ目に掲載されたCodeを見本とし、Qiitaの記事を分析対象として試行しました！

[Qiita API v2ドキュメント](https://qiita.com/api/v2/docs)
[NetworkX &#8212; NetworkX  documentation](https://networkx.github.io/)
[Overview of NetworkX &mdash; NetworkX 2.4 documentation](https://networkx.github.io/documentation/stable/)
[[Python]NetworkXでQiitaのタグ関係図を描く](https://qiita.com/inoory/items/088f719f2fd9a2ea4ee5)
[Python の NetworkX 入門](https://qiita.com/tomov3/items/64f7315474747fc01a6c)
[Pythonでネットワークを分析・可視化しよう！必要手順まとめ](https://www.sejuku.net/blog/91371)

## 本稿で紹介しないこと

- Pythonライブラリの使い方
  - requests
  - json
  - collections
  - itertools
  - matplotlib
  - networkx ※グラフ描画用のPythonライブラリ

# サンプルコード

Code量が多いので、全体をいくつかのパートに分けてCodeを紹介。
ポイントは3つ。

## 1. GETリクエストを実行する際、アクセストークンを指定
[Qiita API v2ドキュメント](https://qiita.com/api/v2/docs)に以下の記載があるように、アクセストークンを取得し、Codeに埋め込むのがベター。
>利用制限
認証している状態ではユーザごとに1時間に1000回まで、認証していない状態ではIPアドレスごとに1時間に60回までリクエストを受け付けます。

## 2. イメージを描画する際、日本語対応フォントを指定
[Text properties and layout - Matplotlib 3.3.0 documentation](https://matplotlib.org/tutorials/text/text_props.html#default-font)に以下の項があるように、日本語対応フォントを（適宜インストールして）指定するのがベター。
>Default Font
The base default font is controlled by a set of rcParams. To set the font for mathematical expressions, use the rcParams beginning with mathtext (see mathtext).

## 3. 重要性の小さいエッジ（Edge）およびノード（Node）を除去
前回の記事([Qiita記事のタグからTreemapを描画してみた](https://qiita.com/Blaster36/items/8fe99c2d861476ba52cc))で出現頻度の低いタグを切り捨てて描画しました。
今回も重要性の小さいエッジおよびノードを刈り取り、スッキリした描画を目指します。

## Codeを紹介
まずは、Qiita APIから記事情報を取得する部分です。
グラフを描画する部分で試行錯誤の積み重ねが予想され、実行の都度にHTTPリクエストをかけていると時間ロスになる（HTTPリクエスト先にも負荷になる）ため、ファイル保管しています。

```python:analyzeQiita_Networkx1
import requests
import json
import codecs

url = 'https://qiita.com/api/v2/items?per_page=100&page='
headers = {'Authorization': 'Bearer ${YOUR ACCESS-TOKEN}'}

tags = []
for i in range(5):
    print('=====')
    print('Downloading ... ' + url + str(i+1))
    print('-----')
    #response = requests.get(url + str(i+1))
    response = requests.get(url + str(i+1), headers=headers)

    print(json_dirpath + '\item%s.json' %(i))
    f = codecs.open(json_dirpath + r'\item%s.json' %(i), 'wb', 'UTF-8')
    f.write(response.text)
    f.close()
    print('=====')
```

次に、記事情報をファイルから読み込み、各記事に付与されたタグを取得します。

```python:analyzeQiita_Networkx2
import glob
import json
import os
import codecs

json_filenames = [p for p in glob.glob(json_dirpath + r'\*.json', recursive=True) if os.path.isfile(p)]
#print(json_filenames)

tags_list = []
for filename in json_filenames:
    #print('==========')
    #print(filename)
    #print('==========')
    f = codecs.open(filename, 'rb', 'UTF-8').read()
    listJson = json.loads(f)
    for article in listJson:
        #print(article['title'])
        #print([tag['name'] for tag in article['tags']])
        tags_list.append([tag['name'] for tag in article['tags']])
```

次に、分析対象の全タグのうち、出現頻度の高い順に上位50件のタグを取得します。

```python:analyzeQiita_Networkx3
from collections import Counter
import itertools

c = Counter(itertools.chain.from_iterable(tags_list))
#print(c)

# 出現回数が多いものから順にTop50を列挙（重複なし）
tagSurface, tagCount = zip(*c.most_common(50))
#print(tagSurface)
#print(tagCount)
```

ここから、グラフを描画してゆきます。
エッジの重みを初期化し、同じノード間に複数エッジを描く代わり、エッジの重みを増やします。

```python:analyzeQiita_Networkx4
import networkx as nx

G = nx.Graph()
#G.add_nodes_from([(tag, {"count":count}) for tag, count in tag_count])
G.add_nodes_from(tagSurface)

for tags in tags_list:
    for node0,node1 in itertools.combinations(tags, 2):
        if not G.has_node(node0) or not G.has_node(node1):
            continue
        if G.has_edge(node0, node1):
            G.edges[node0, node1]['weight'] += 1.0
        else:
            G.add_edge(node0, node1, weight=1.0)
```

日本語対応のフォントを使用し、ノード、ラベル、エッジを順に描画します。

```python:analyzeQiita_Networkx5
%matplotlib inline
import matplotlib.pyplot as plt

#日本語フォントの指定
plt.rcParams["font.family"] = 'Yu Gothic'

plt.figure(figsize=(15,15))

# nodeの配置方法の指定
pos = nx.spring_layout(G)

# nodeを描画
nx.draw_networkx_nodes(G, pos)

# labelを描画、日本語フォントの指定
nx.draw_networkx_labels(G, pos, fontsize=14, font_family='Yu Gothic', font_weight="bold")

# edgeを描画
nx.draw_networkx_edges(G, pos, alpha=0.4, edge_color="darkgrey")

plt.axis("off")
plt.savefig("../result/trends_of_qiita_networkx_shortver1.png")
plt.show()
```

以下、描画結果です。ごちゃごちゃしていて、イマイチな感じです。
![trends_of_qiita_networkx_shortver1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/3d2bdaf2-b448-60f0-8223-4c8679821b25.png)

更に、重要性の小さいエッジおよびノードを除去します。
オリジナルのグラフに直接変更を加えるのではなく、Copyを作った上でCopyしたグラフに変更を加えます。
刈り込みの終わったCopyしたグラフを、改めてオリジナルのグラフに還元して、描画します。

あと、ノードおよびエッジの描画方法も少しアレンジしました。
- ノードの大きさと色の濃淡にPageRankのScoreを反映させ、重要なノードほど大きく色濃く描くようにしました。
- エッジの太さにエッジの重みを反映させ、重みの大きい（つながりの強い）エッジほど太く描くようにしました。

```python:analyzeQiita_Networkx6
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

# weightが閾値以下のedgeを削除
G_deep = G.copy()
for (u,v,d) in G.edges(data=True):
    if d["weight"] <= 2:
        G_deep.remove_edge(u, v)
G = G_deep.copy()

# edgeのなくなったnodeを削除
nodesWithoutEdge = [n for n in G.nodes if len([i for i in nx.all_neighbors(G, n)]) == 0]
for n in nodesWithoutEdge:
    G_deep.remove_node(n)
G = G_deep.copy()

plt.figure(figsize=(15,15))

# nodeの配置方法の指定
seed = 0
np.random.seed(seed)
pos = nx.spring_layout(G, k=0.8, seed=seed)  # k = node間反発係数

# PageRankを計算
pr = nx.pagerank(G)
# nodeを描画、PageRank値を大きさに反映
nx.draw_networkx_nodes(G, pos, node_color=list(pr.values()),
                       cmap=plt.cm.Blues,
                       alpha=0.6,
                       node_size=[60000*v for v in pr.values()])

# labelを描画、日本語フォントの指定
nx.draw_networkx_labels(G, pos, fontsize=14, font_family="Yu Gothic", font_weight="bold")

# edgeを描画、edgeのweight値を太さに反映
nx.draw_networkx_edges(G, pos, alpha=0.4, edge_color="darkgrey", width=[d['weight']*0.8 for (u,v,d) in G.edges(data=True)])

plt.axis('off')
plt.savefig("../result/trends_of_qiita_networkx_shortver2.png")
plt.show()
```

以下、描画結果です。スッキリしたかな、という印象です。
![trends_of_qiita_networkx_shortver2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/56368c7c-ede7-07e1-e30e-14a9136b3e24.png)

ノードが密集している部分やエッジで結ばれる2つのノードを眺めてみると、何となく近い意味合いでタグが接続されたかたちになっていると思います。

ちなみに、分析対象の記事数を増やした上でTop100のタグで同じ処理をかけると、以下のようになります。
![trends_of_qiita_networkx2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/e8382c64-d6f5-b601-7273-cf9b2e32dc42.png)

やはり、Pythonが強いですね。

# まとめ
Qiita記事のタグを使って、グラフを描画する方法を紹介。