漫画研究室

ジャンプ作品の感想や考察を書いている個人ブログです。ジャンプ感想を毎週更新中。ワートリ感想を毎月更新中。呪術廻戦・アンデラの単行本感想を随時更新中。

Python素人によるクラスタリング分析・アソシエーション分析・共起分析・ネットワーク作図

ジャンプ+作品の読者層の繋がりをネットワーク図として可視化しました。
クラスタリング分析とアソシエーション分析と共起分析をPythonで行いました。

ド素人なので90%コピペです。「取り敢えずやってみたい!」という欲望を満たすための最短経路。

成果物

f:id:mangalab:20220323202712j:plain

このネットワーク自体についての説明はこちらを参照。

今回は具体的にどういった手法で成果物に辿り着いたかという話を書きます。自分用の備忘録です。

方法

1. アンケート回収

解析に用いるデータはGoogleフォームで回収。
「読んでいる作品」を複数選択可で質問しました。

f:id:mangalab:20220306111850j:plain

Googleフォームの回答結果はcsvファイルとしてダウンロード可能。
複数選択のデータはコンマ区切りで1つのセルに記録されていて扱いづらいため、簡単な処理をエクセル上で行います。

<加工前>

性別 年齢 読んでいる作品
男性 19 SPY×FAMILY,タコピーの原罪,怪獣8号
女性 20 SPY×FAMILY

<加工>
作品ごとに判定用の列を追加→COUNTIF関数で作品名を含んでいるか判定。

=COUNTIF(読んでいる作品のセル,"*"&作品名のセル&"*")

絶対参照と相対参照を適切に使えばオートフィルで1発です。

<加工後>

読んでいる作品 SPY×FAMILY タコピーの原罪 怪獣8号
SPY×FAMILY,タコピーの原罪,怪獣8号 1 1 1
SPY×FAMILY, 1 0 0




2. クラスタリング分析

作品のクラスタリングを行います。
クラスタリングというのは性質の似ているものをグループ分けする作業…みたいなイメージです。詳しく知りたい方はググってください。

クラスタリングには「階層クラスタリング」と「非階層クラスタリング」があるそうですが、今回は「非階層クラスタリング」である「ウォード法」を採用しました。
クラスタリングの様子を系統図で確認して、結果に納得感が欲しかったからです。

Pythonで実行。環境はGoogle Colaboratory。
コードはコピペ。弄った部分はファイルのパスだけです。

Pythonによる階層型クラスタリングの実行方法 | データサイエンス情報局

結果。日本語のラベルは後付けです。
f:id:mangalab:20220323211029j:plain

全選択肢40作品を対象にするつもりでしたが、回答者数の少ない6作品が上手くクラスタリングできていないようだったので除外しました(画像は除外後)
この除外基準も含め、主観に頼った場面が多いことが今回の反省点。妥当性の検討みたいなのが本来は必要なはずです。

3. アソシエーション分析

アソシエーション分析を行います。
簡単に説明すると「一緒に買われている商品を把握するための分析手法」みたいなやつです。詳しく知りたい方はググってください。このサイトとかオススメです。
商品分析の手法(ABC分析、アソシエーション分析) | データ分析基礎知識

こちらもPythonでやりました。

Pythonでアソシエーション分析 - Qiita

基本的にコピペなのでコードは載せませんが、変更点だけ載せておきます。

<変更前>

freq_items1 = apriori( basket_df, min_support = 0.06, use_colnames = True)

<変更後>

freq_items1 = apriori( basket_df, min_support = 0, use_colnames = True, max_len = 2)

「min_support」はレアケースの処理を省いて負荷を軽減するためのコードです。変更前だとsupport(母集団のうち当てはまる人の割合)が6%未満のケースを省いています。
今回は全ケースを確認したかったため「min_support = 0」にしてます。

「max_len」は一緒に読まれている作品数の最大値です。仮にこれを5にすると「作品A・作品Bを読んでいる人が作品C・作品D・作品Eを読んでいるケース」なども検討対象になります。
今回は一対一の情報で十分なので「max_len=2」としました。処理負荷の軽減にも繋がります。

終結果はcsvファイルとして出力します。

a_rules1.to_csv( 'ファイル名' )

4. 共起分析(Jaccard係数)

Jaccard係数は、2つの集団の類似度を示す指標で、言語の共起分析でよく使われるそうです。例のごとく詳しく知りたい方はググってください。

【技術解説】集合の類似度(Jaccard係数,Dice係数,Simpson係数) - ミエルカAI は、自然言語処理技術を中心とした、RPA開発・サイト改善・流入改善レコメンドエンジンを開発

Jaccard係数については、先人のコードを発掘できなかったため自分で書きました。汚いのはご愛嬌。

import pandas as pd
df = pd.read_csv( 'ファイル名' )
Jaccard =[ ]
for i in range(40):
 for j in range(i+1,40):
  x = (((df[workname[i]] == 1 ) & (df[workname[j]]==1)).sum()) / ( (df[workname[i]] == 1 ).sum() + (df[workname[j]] == 1 ).sum() - ( (df[workname[i]] == 1 ) & (df[workname[j]]==1)).sum())
  Jaccard.append ( [workname[i], workname[j], x] )
df = pd.DataFrame( Jaccard )
df.to_csv( 'ファイル名' )




5. ネットワーク作図の準備

ネットワーク図に載せる情報を整理します。

<ファイルA>
各作品のノード(円)の情報をまとめます。
1列目が作品名、2列目が回答人数、3列目が色です。日本語は使わないのが無難でしょう。

spy 137 tomato
nitengo 93 hotpink

色はクラスタリング分析の結果を反映させます。
使用できる色の名前はこちらを参照。16進数やrgbaでも指定できるらしいです。
color example code: named_colors.py — Matplotlib 2.0.2 documentation

<ファイルB>
作品間を繋ぐエッジ(矢印)の情報をまとめます。
このへんの整理は1つのエクセルファイルに情報を統合してやると捗ります。データの統合はXLOOKUP関数を多用しましょう。

まず、終点側の作品のsupportをもとにエッジの数を決定しました(タコピ10本、ダンダダン8本のように)。計150本くらいが視認の限界のように思います。
作品ごとに、アソシエーション分析のLift値と共起分析のJaccard係数が上位の関係性を採用しました。
エッジの太さは、Lift値とJaccard係数が両方とも最上位のものを2、片方が最上位のものを1.5…のようにランクづけしました。
エッジの色は、始点側の作品のグループの色と揃えました。

start goal width color
kaiju spy 1 tomato
kutisake himesama 1.5 goldenrod

6. ネットワーク作図(Networkx)

ネットワーク作図です。
Pythonの「Networkx」というライブラリを使用しました。

ネットワークというかグラフ理論についての基本的な知識はこちら。
グラフ理論とNetworkX — Pythonオンライン学習サービス PyQ(パイキュー)ドキュメント
ネットワーク可視化の基本的な考えはこちら。
改訂版: プログラマーが効果的な可視化を作成する (前編) - Qiita

例のごとく沢山のサイトを参考にさせていただきましたが、あまりにも色々と参考にしすぎたせいでベースにしたサイトを見失ったため、実際のコードを載せます。

mport networkx as nx
import matplotlib.pyplot as plt
import csv

node_sizes=[ ]
node_labels=[ ]
node_colors=[ ]

with open( 'ファイルAのパス', encoding='utf-8-sig' ) as f:
 x = csv.reader(f)
 for row in x:
  node_sizes.append( float(row[1]) *10 )
  node_labels.append( row[0] )
  node_colors.append( row[2] )

edgelist =[ ]

with open('ファイルBのパス', encoding='utf-8-sig') as f:
 reader = csv.reader(f)
 hed = next(reader)
 for row in reader:
  edgelist.append([row[0], row[1], {'width': (float(row[2])), 'color': row[3])

G = nx.DiGraph()
G.add_nodes_from( node_labels )
G.add_edges_from( edgelist )

edge_width = [ d['width'] for (u,v,d) in G.edges(data=True)]
edge_color = [d['color'] for (u,v,d) in G.edges(data=True)]

plt.figure(figsize=(12,12))
pos = nx.nx_pydot.graphviz_layout( G)

nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_colors, alpha=1)
nx.draw_networkx_labels(G, pos)
nx.draw_networkx_edges(G, pos, width = edge_width, edge_color = edge_color, alpha=0.7, arrows=True, arrowsize=15, min_target_margin=20, min_source_margin=20)

plt.savefig("画像ファイル名", format="png", dpi=500)

作品名ラベルは後付けした方が綺麗に仕上がると思います。
日本語を導入するのは面倒くさいですし、フォント指定の自由度も低くなるので。

おわりに

具体的な備忘録を書くつもりでしたが、かなり端折った説明になってしまいました。

初挑戦ということもあり個人的には満足できるクオリティに至ったつもりです。一方で、まだまだレベルアップできる余地があることも理解しています。今後ネットワーク図に再挑戦することがあれば、今回の反省を踏まえて更に的確な可視化をしようと思います。

この備忘録を参考にして下さる方が一人でもいらっしゃったら幸いです。それでは。