M1グランプリ決勝前日！決勝進出者に関係する芸人の相関図を作ってM1がより楽しめるように予習してみた。

# はじめに
この記事はMYJLab Advent Calender2022の17日目の記事で、私が2日連続担当での投稿になります。
ほんとは昨日の[NotionとGASを使ってタスク管理継続するために足掻いた話](https://qiita.com/ultrasoulso/items/887f9d4613a79742a0f4)を前後編でやろうとしてたんですが、ちょっとやりたいネタがあったので昨日と全く違うものを書いていきます。
昨日の記事は詳細な部分が雑に終わってしまってるので、どこかでちゃんとしたバージョンを出そうと思います。

# 書かれていること
M1決勝が明日ということで予習のために、**芸人さんの人間関係**の相関図を作ってみました。
その芸人さん自体を知らなくても、知ってる芸人さんと何らか関係があったら取っ掛かりやすくなりそうですよね！
具体的には[Google Colaboratory](https://colab.research.google.com/?hl=ja)上でPythonの[pyvis](https://pyvis.readthedocs.io/en/latest/)というライブラリを使用してよりわかりやすく可視化できるようにしてみました。

使用した芸人さんのデータは、Wikipediaの情報が構造的に格納されている[DBpedia](https://www.dbpedia.org/)というデータセットから取得しました。
このデータセットはWikipediaに記載されている情報がデータとして格納されているので、(情報の真偽はともかく)芸人さんの「影響を受けた芸人」のような様々なつながりに関する情報を活用することができます。

**出来上がった相関図としては以下のような感じ**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/fe85c006-6a7b-af5f-65ab-c59e99169000.png)

そんなわけで以下が本文となります。

# M1グランプリの決勝前日！
明日は[M1グランプリの決勝戦](https://www.m-1gp.com/)が行われるようです。
僕はお笑いに関してにわかではあるんですが、毎年M1は楽しみにしています。

今年も無事に開催されるようで、前日の今日から楽しみですが一点問題があります。
**今回の決勝進出者の芸人さんの半分くらいが知らない人だ、、、** という状況になっているということなんです。

もちろん知らない芸人さんのネタでも楽しんで見ることはできるんですが、事前知識がある方がより良いかなと思います。
ですので、今回はM1の予習のために決勝出場者のことを知りたい！という動機で始めております。


# データの取得
まずは人間関係に関するデータの取得について考えていきます。
ざっくりとDBpediaの説明をしたあとに、DBpediaからデータを取得するためのクエリ言語である[SPARQL](https://ja.wikipedia.org/wiki/SPARQL)のクエリ、Pythonでのコードを紹介します。

この項の参考文献は以下です。

https://www.slideshare.net/takeda/what-isdpbedia

https://qiita.com/pika_shi/items/eb56fc205e2d670062ae

https://qiita.com/AAA0125/items/ba73bc216916d158dc90

## DBpediaについて
DBpediaでは情報が[RDF形式](https://ja.wikipedia.org/wiki/Resource_Description_Framework)という形式で格納されています。
ここでは詳細に記述することはしないのですが、データを「主語 -> 動詞 -> 目的語」というような形で格納しているイメージになります。「千鳥はダウンタウンに影響を受けた」という情報を考えると以下のようにグラフで表せますが、このような情報が格納されていると思ってください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/592bbef4-deb8-b8c2-9395-c6371d209b98.png)
Wikipediaに記載のある情報を(全てではないですが)参照できるので、今回のようにデータ間の関係性を扱いたいときに便利になります。

## SPARQLについて
SPARQLはDBpediaのようなデータセットから情報を得たい時に用いることができるクエリ言語です。
ここでも具体的な構文についての説明は省きますが、SQLライクな書き方をすることができます。
今回可視化したい芸人のデータは以下のクエリを使って取得しました
```
    select ?p ?concerned
    where 
    { 
    <調べたい芸人のDBpediaリンク> ?p ?concerned . 
    ?concerned dct:subject ?c .
    FILTER(?c = dbpedia-ja:Category:日本のお笑いコンビ || ?c = dbpedia-ja:Category:お笑い芸人)
    FILTER(?p = <http://dbpedia.org/ontology/influenced> 
      || ?p = <http://ja.dbpedia.org/property/影響>
      || ?p = <http://ja.dbpedia.org/property/同期>
      || ?p = <http://ja.dbpedia.org/property/メンバー>
      || ?p = <http://ja.dbpedia.org/property/相方>
      || ?p = <http://ja.dbpedia.org/property/師匠>
      || ?p = <http://ja.dbpedia.org/property/弟子>
      || ?p = <http://ja.dbpedia.org/property/ネタ作成者>)
    }
    order by ?concerned
```

例えば今回決勝に進んでいるロングコートダディはリンクのような情報が格納されているので、この中から取得したいプロパティを指定して上げる感じです。
https://ja.dbpedia.org/page/ロングコートダディ

## SPARQLWrapper
上記のクエリを使ってデータを取得するために、Pythonのライブラリである[SPARQLWrapper](https://sparqlwrapper.readthedocs.io/en/latest/)を使用します。
詳しい使い方はドキュメントか参考文献を読んでほしいのですが、ロングコートダディの情報を先程のクエリで取得するコードは以下となります。
```py
!pip install SPARQLWrapper

from SPARQLWrapper import SPARQLWrapper
sparql = SPARQLWrapper(endpoint='http://ja.dbpedia.org/sparql', returnFormat='json')
sparql.setQuery("""                                                                                                                                                                      
select ?p ?concerned
where 
{ 
<http://ja.dbpedia.org/resource/ロングコートダディ> ?p ?concerned . 
?concerned dct:subject ?c .
FILTER(?c = dbpedia-ja:Category:日本のお笑いコンビ || ?c = dbpedia-ja:Category:お笑い芸人)
FILTER(?p = <http://dbpedia.org/ontology/influenced> 
  || ?p = <http://ja.dbpedia.org/property/影響>
  || ?p = <http://ja.dbpedia.org/property/同期>
  || ?p = <http://ja.dbpedia.org/property/メンバー>
  || ?p = <http://ja.dbpedia.org/property/相方>
  || ?p = <http://ja.dbpedia.org/property/師匠>
  || ?p = <http://ja.dbpedia.org/property/弟子>
  || ?p = <http://ja.dbpedia.org/property/ネタ作成者>)
}
order by ?concerned
""")

results = sparql.query().convert()
```
今回は出力をjson形式で指定しているので、出力としては以下のようになります。
```console
{'head': {'link': [], 'vars': ['p', 'concerned']},
 'results': {'bindings': [{'concerned': {'type': 'uri',
                                         'value': 'http://ja.dbpedia.org/resource/EXIT_(お笑いコンビ)'},
                           'p': {'type': 'uri',
                                 'value': 'http://ja.dbpedia.org/property/同期'}},
                          {'concerned': {'type': 'uri',
                                         'value': 'http://ja.dbpedia.org/resource/りんたろー。'},
                           'p': {'type': 'uri',
                                 'value': 'http://ja.dbpedia.org/property/同期'}},
                          {'concerned': {'type': 'uri',
                                         'value': 'http://ja.dbpedia.org/resource/インディアンス_(お笑いコンビ)'},
                           'p': {'type': 'uri',
                                 'value': 'http://ja.dbpedia.org/property/同期'}},
                          {'concerned': {'type': 'uri',
                                         'value': 'http://ja.dbpedia.org/resource/セルライトスパ'},
                           'p': {'type': 'uri',
                                 'value': 'http://ja.dbpedia.org/property/同期'}}],
             'distinct': False,
             'ordered': True}}
```

## データ取得の関数
取得の仕方がわかったので、決勝進出者全員分のデータ取得のために関数にしておきます。
後で可視化する都合上、pairsという配列にデータ入れておくことにしました。
```python
def makePairs(comedians):
  pairs = []
  for i in range(len(comedians)):
    sparql = SPARQLWrapper(endpoint='http://ja.dbpedia.org/sparql', returnFormat='json')
    sparql.setQuery("""                                                                                                                                                                      
    select ?p ?concerned
    where 
    { 
    <http://ja.dbpedia.org/resource/"""+comedians[i]+"""> ?p ?concerned . 
    ?concerned dct:subject ?c .
    FILTER(?c = dbpedia-ja:Category:日本のお笑いコンビ || ?c = dbpedia-ja:Category:お笑い芸人)
    FILTER(?p = <http://dbpedia.org/ontology/influenced> 
      || ?p = <http://ja.dbpedia.org/property/影響>
      || ?p = <http://ja.dbpedia.org/property/同期>
      || ?p = <http://ja.dbpedia.org/property/メンバー>
      || ?p = <http://ja.dbpedia.org/property/相方>
      || ?p = <http://ja.dbpedia.org/property/師匠>
      || ?p = <http://ja.dbpedia.org/property/弟子>
      || ?p = <http://ja.dbpedia.org/property/ネタ作成者>)
    }
    order by ?concerned
    """)

    results = sparql.query().convert()
    p = []
    bindings = results['results']['bindings']
    for j in range(len(bindings)):
      concerned = bindings[j]['concerned']['value']
      prop = bindings[j]['p']['value']
      concerned_name = concerned.replace('http://ja.dbpedia.org/resource/','')
      prop_name = prop.replace('http://ja.dbpedia.org/property/','')
      if prop_name == "influenced":
        prop_name = "影響"
      names = [comedians[i],concerned_name,prop_name]
      p.append(names)
    pairs.append(p)
  # print(pairs) 出力用
  return pairs
```
一応pairsは、以下のようになっています。
```
[[], [['男性ブランコ', 'Kento_fukaya', '同期'], ['男性ブランコ', 'Kento_fukaya', '同期'], ['男性ブランコ', 'ZAZY', '同期'], ['男性ブランコ', 'しゃかりき', '同期'], ['男性ブランコ', 'コロコロチキチキペッパーズ', '同期'], ['男性ブランコ', 'ビスケットブラザーズ', '同期'], ['男性ブランコ', 'マユリカ', '同期'], ['男性ブランコ', 'ラーメンズ', '影響'], ['男性ブランコ', '霜降り明星', '同期']], [['ヨネダ2000', 'サンタモニカ_(お笑いコンビ)', '同期'], ['ヨネダ2000', '令和ロマン', '同期']], [['ダイヤモンド_(お笑いコンビ)', 'いぬ_(お笑いコンビ)', '同期'], ['ダイヤモンド_(お笑いコンビ)', 'ゲラゲラ星人', '同期'], ['ダイヤモンド_(お笑いコンビ)', 'ニューヨーク_(お笑いコンビ)', '同期'], ['ダイヤモンド_(お笑いコンビ)', 'マテンロウ', '同期'], ['ダイヤモンド_(お笑いコンビ)', '大自然_(お笑いコンビ)', '同期'], ['ダイヤモンド_(お笑いコンビ)', '忘れる。', '同期'], ['ダイヤモンド_(お笑いコンビ)', '横澤夏子', '同期'], ['ダイヤモンド_(お笑いコンビ)', '鬼越トマホーク', '同期']], [['ロングコートダディ', 'EXIT_(お笑いコンビ)', '同期'], ['ロングコートダディ', 'りんたろー。', '同期'], ['ロングコートダディ', 'インディアンス_(お笑いコンビ)', '同期'], ['ロングコートダディ', 'セルライトスパ', '同期']], [['さや香', 'エンペラー_(お笑い)', '同期'], ['さや香', 'チュートリアル_(お笑いコンビ)', '影響'], ['さや香', 'トニーフランク', '同期'], ['さや香', 'ミキ_(お笑いコンビ)', '同期'], ['さや香', 'ムラムラタムラ', '同期'], ['さや香', '亜生', '同期'], ['さや香', '本多スイミングスクール', '同期'], ['さや香', '松本人志', '影響'], ['さや香', '隣人_(お笑いコンビ)', '同期']], [['ウエストランド', 'ラブレターズ', '同期']], [['真空ジェシカ', 'オズワルド_(お笑いコンビ)', '同期'], ['真空ジェシカ', 'プール_(お笑いコンビ)', '同期'], ['真空ジェシカ', 'マヂカルラブリー', '影響'], ['真空ジェシカ', '空気階段', '同期'], ['真空ジェシカ', '裸月光', '同期'], ['真空ジェシカ', '魂ず', '同期']], [['カベポスター', 'さまぁ〜ず', '影響'], ['カベポスター', 'ダウンタウン_(お笑いコンビ)', '影響'], ['カベポスター', 'ダブルヒガシ', '同期'], ['カベポスター', '丸亀じゃんご', '同期']]]
```


# 相関図の可視化
上記で情報の取得ができましたので、その情報をもとに実際に相関図を作ってみます。
pyvisというライブラリを使ってネットワーク図を作成するという形にしました。

この項の参考文献は以下です。

https://qiita.com/baby-degu/items/bf97545595a781050cb5

https://qiita.com/niship2/items/9d7e2b6ab2ca1be18eaf


今回はNetworkXからpyvisに変換するという流れではなく、直でpyvisで可視化するようにしました。
中心性など求めるようなネットワーク分析をするのであれば、NetworkXで最初に作ったほうが良いと思います。
可視化においてはpyvisを使うとインタラクティブな図を出力することができるので、遊ぶにはNetworkXよりおすすめです。

## 可視化の実装
詳細な実装方法は説明省かせてもらうんですが、可視化するための関数を以下に載せておきます。
```python
def visialize(comedians, pairs):
  # ネットワークのインスタンス生成
  network = Network(
      height="800px",  # デフォルト "500px"
      width="800px",  # デフォルト "500px"
      notebook=True,  # これをTrueにしておくとjupyter上で結果が見れる
      bgcolor='#ffffff',  # 背景色。デフォルト "#ffffff"
      directed=True,  # Trueにすると有向グラフ。デフォルトはFalseで無向グラフ
  )

  for i in range(len(pairs)):
    # add_node でエッジを追加
    # 関係性が取れなかった芸人はそのままノードを追加するだけ
    if len(pairs[i]) == 0:
      network.add_node(comedians[i], color="lightpink")
    else:
      smooth="cubicBezier"
      for j in range(len(pairs[i])):
        if pairs[i][j][1] in comedians:
          network.add_node(pairs[i][j][1], color="lightpink")
        else:
          network.add_node(pairs[i][j][1], color="lightblue")
      network.add_node(pairs[i][0][0], color="lightpink")
      # add_edge でエッジを追加
      for j in range(len(pairs[i])):
        if pairs[i][j][2] in ["影響","弟子"]:
          network.add_edge(pairs[i][j][1],pairs[i][j][0],color="blue", title=pairs[i][j][2],smooth=smooth)
        elif pairs[i][j][2] in ["同期"]:
          network.add_edge(pairs[i][j][0],pairs[i][j][1],color="lightblue", title=pairs[i][j][2],smooth=smooth)
          network.add_edge(pairs[i][j][1],pairs[i][j][0],color="lightblue", title=pairs[i][j][2],smooth=smooth)
        else:
          network.add_edge(pairs[i][j][0],pairs[i][j][1],color="lightblue", title=pairs[i][j][2],smooth=smooth)

  # 指定したファイル名でHTMLを出力。
  network.show("pyvis_sample1.html")
  display(HTML('pyvis_sample1.html'))
```

特殊な処理としては、今回のクエリだとキュウさんと関係のある芸人さんが取得することができませんでした。
(芸人さんの著名度などによってWikipediaの記述量に差があるので難しい部分です、、、)
なので、そのあたりの処理を加えています。


## 実際に出力してみた
上記の関数を使って、図を出力してみました。
comediansという今回の決勝進出者のWikipedia上の名前のリストを用意して、それを入力としています。

```python
comedians = ["キュウ","男性ブランコ","ヨネダ2000","ダイヤモンド_(お笑いコンビ)","ロングコートダディ","さや香","ウエストランド","真空ジェシカ","カベポスター"]
visialize(comedians,makePairs(comedians))
```

実際の出力がこちらです。

![52530927f8cd9eb4c52d7ad011de54fe.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/55000fc7-8e5c-a009-2b60-b94a915b01d1.gif)

ロングコートダディがEXITの同期だったり、真空ジェシカがマヂカルラブリーに影響を受けていたなどの発見がありました！

# おわりに
M1の決勝戦に向けて予習ができるような人物相関図を作ってみました。
今回の出場者ですと、関係者同士がつながるといったことがなく、いろんな文脈の人が出ているのかなぁと言う印象でした。
あくまでWikipedia上の情報から取ってきたという前提なので、他の情報と組み合わせるとより面白くなりそうですね！

明日のM1楽しみです！


# おまけ
## 昨年の決勝出場者
昨年の決勝進出者の相関図も作成してみました。
昨年は出場者同士が同期の関係だったり、
若手のオズワルドさんが影響を受けたおぎやはぎさんと同期の錦鯉さんが出ていたりして面白い繋がり方がありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666097/63669535-0b8c-aa9c-362e-9bc45fa97fb7.png)

## コード全文
```python
!pip install SPARQLWrapper
!pip install pyvis==0.1.9

from SPARQLWrapper import SPARQLWrapper
from pyvis.network import Network
from IPython.core.display import display, HTML

def makePairs(comedians):
  pairs = []
  for i in range(len(comedians)):
    sparql = SPARQLWrapper(endpoint='http://ja.dbpedia.org/sparql', returnFormat='json')
    sparql.setQuery("""                                                                                                                                                                      
    select ?p ?concerned
    where 
    { 
    <http://ja.dbpedia.org/resource/"""+comedians[i]+"""> ?p ?concerned . 
    ?concerned dct:subject ?c .
    FILTER(?c = dbpedia-ja:Category:日本のお笑いコンビ || ?c = dbpedia-ja:Category:お笑い芸人)
    FILTER(?p = <http://dbpedia.org/ontology/influenced> 
      || ?p = <http://ja.dbpedia.org/property/影響>
      || ?p = <http://ja.dbpedia.org/property/同期>
      || ?p = <http://ja.dbpedia.org/property/メンバー>
      || ?p = <http://ja.dbpedia.org/property/相方>
      || ?p = <http://ja.dbpedia.org/property/師匠>
      || ?p = <http://ja.dbpedia.org/property/弟子>
      || ?p = <http://ja.dbpedia.org/property/ネタ作成者>)
    }
    order by ?concerned
    """)

    results = sparql.query().convert()
    p = []
    bindings = results['results']['bindings']
    for j in range(len(bindings)):
      concerned = bindings[j]['concerned']['value']
      prop = bindings[j]['p']['value']
      concerned_name = concerned.replace('http://ja.dbpedia.org/resource/','')
      prop_name = prop.replace('http://ja.dbpedia.org/property/','')
      if prop_name == "influenced":
        prop_name = "影響"
      names = [comedians[i],concerned_name,prop_name]
      p.append(names)
    pairs.append(p)
  return pairs

def visialize(comedians, pairs):
  # ネットワークのインスタンス生成
  network = Network(
      height="800px",  # デフォルト "500px"
      width="800px",  # デフォルト "500px"
      notebook=True,  # これをTrueにしておくとjupyter上で結果が見れる
      bgcolor='#ffffff',  # 背景色。デフォルト "#ffffff"
      directed=True,  # Trueにすると有向グラフ。デフォルトはFalseで無向グラフ
  )

  for i in range(len(pairs)):
    # add_node でエッジを追加
    # 関係性が取れなかった芸人はそのままノードを追加するだけ
    if len(pairs[i]) == 0:
      network.add_node(comedians[i], color="lightpink")
    else:
      smooth="cubicBezier"
      for j in range(len(pairs[i])):
        if pairs[i][j][1] in comedians:
          network.add_node(pairs[i][j][1], color="lightpink")
        else:
          network.add_node(pairs[i][j][1], color="lightblue")
      network.add_node(pairs[i][0][0], color="lightpink")
      # add_edge でエッジを追加
      for j in range(len(pairs[i])):
        if pairs[i][j][2] in ["影響","弟子"]:
          network.add_edge(pairs[i][j][1],pairs[i][j][0],color="blue", title=pairs[i][j][2],smooth=smooth)
        elif pairs[i][j][2] in ["同期"]:
          network.add_edge(pairs[i][j][0],pairs[i][j][1],color="lightblue", title=pairs[i][j][2],smooth=smooth)
          network.add_edge(pairs[i][j][1],pairs[i][j][0],color="lightblue", title=pairs[i][j][2],smooth=smooth)
        else:
          network.add_edge(pairs[i][j][0],pairs[i][j][1],color="lightblue", title=pairs[i][j][2],smooth=smooth)

  # 指定したファイル名でHTMLを出力。
  network.show("pyvis_sample1.html")
  display(HTML('pyvis_sample1.html'))

comedians = ["キュウ","男性ブランコ","ヨネダ2000","ダイヤモンド_(お笑いコンビ)","ロングコートダディ","さや香","ウエストランド","真空ジェシカ","カベポスター"]
visialize(comedians,makePairs(comedians))

```