Spotifyの関連アーティストからネットワーク図を作って次に聞く曲の決定支援


この記事は、[NTTコミュニケーションズ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nttcom)の22日目の記事です。
昨日は @kanatakita さんの記事、[アプリケーションのリリースに必要な会議を倒したい話](https://qiita.com/kanatakita/items/a68c6e7758524422ecb0) でした。

# はじめに
こんにちは、NTTコミュニケーションズのSkyWayチームに所属しているyuki uchidaです。
本記事は、[BERTを理解しながら自分のツイートを可視化してみるハンズオン](https://qiita.com/yuki_uchida/items/09fda4c5c608a9f53d2f)に続いて『可視化したい』シリーズ第二弾です。

プライベートでSpotifyのAPIを叩いていたところ、個人的な興味範囲であるリコメンドシステムに絡めて面白そうな可視化ができそうだと思ったため、この記事を書いています。

## 何をする記事？

Spotifyは業界最高峰のリコメンドシステムを提供していますが、そのリコメンド結果に疑問を持つことも多くあります。
今回、この記事では**納得感のあるリコメンドを行うため、Spotifyの関連アーティストからネットワーク図を作って次に聞く曲の決定支援**をします。(リコメンドの一種と言えるかもしれません)
pythonのコードも合わせて乗っけているため、自分でやってみたいと感じた方はぜひ試してみてください！！


## Spotifyのリコメンドシステム
Spotifyでは、既に非常に精度の良いリコメンドシステムが実装されています。**曲のテンポやジャンル**など、非常に多くの情報を使った上でリコメンドがなされています。
どれだけ多くの情報を使っているかはSpotify APIで取得できる情報を調べてみるとよくわかります。

[Spotifyの76,000曲の属性データを分析した結果、J-RockはRockというよりむしろPunkだった](https://qiita.com/hidekoji/items/6b9680798d4d9b03ea9a)
[Spotifyのレコメンドロジックについて語り尽くす](http://shimizun14.hatenablog.com/entry/2018/02/17/152338
)

Spotifyでは、以下のようにプレイリストからオススメの曲をリコメンドしてくれたり、好きそうなアーティストをプレイリストにしてくれたりします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124611/3e911183-f162-a9a4-8679-12ce4733fa7c.png)


Spotifyは本当に素晴らしいリコメンドシステムを有しており、私はSpotifyのおかげで音楽が好きになりました。
オススメされる曲の多くが自分の感性に非常にマッチしていて、Spotifyがオススメしてくれる曲で一日過ごせるくらいにはSpotifyが好きです。一生課金します。


## リコメンドシステムの精度指標

確かにSpotifyのリコメンドは優秀です。しかし、『どうしてこの曲をオススメしてきたか？』という疑問に答えられるようなシステムになっているわけではありません。そのため、Spotifyに限った話ではなく、今ある多くのリコメンドシステムは、『**どうしてこの曲なのかはわからないけど結果的に好きな曲だった**』となりがちなのです。

### リコメンドシステムにおける議論
多くのリコメンドシステムにおいて、精度指標として、`accuracy`が使われることが多いです。
しかし、`accuracy`のみを精度指標とすることには議論が必要です。
Charu C Aggarwaは『リコメンデーションシステム』で、「`accuracy(精度)`だけでなく、`diversity(多様性)`、`serendipity(意外性)`、`Novelty(新規性)`などの指標からも評価するべき」と述べています。

リコメンドシステムにおいては、`accracy`のみを精度指標にしてしまうと、**リコメンドするものが有名なものに限られてしまって面白みに欠ける**という問題が産まれてしまいます。そのため、他の指標を使うことも重要です。

### リコメンドシステムの最近の流れ

最近では、accuracyではなくupliftを精度指標とすることでリコメンドの精度を高めたという論文も発表されました。
リコメンドの精度指標は数多くあり、非常に興味深い議論が多くなされています。
以下の記事では`Uplift-based Evaluation and Optimization of Recommenders`という論文をわかりやすく解説しているので、ぜひ参考にしてください。(こちらのメディアでもたまに記事を書いています)
[そのリコメンド、本当に効果ありますか？accuracyではなくupliftで学習することでリコメンドは進化する。](https://ai-scholar.tech/machine-learning/ai-accuracy-343/)

現在Spotifyで提供されているリコメンドシステムは、どのような精度指標が用いられているかわからないため、今回は、**アーティストの関連度から、多様性を求めてリコメンドをしてもらうか、好きなタイプのアーティストを深掘りしていくか**自身で決定できるようにしたいと思います。


## アーティストの関連度をネットワーク図として表現し、自身が聴いてる曲をプロットする


**アーティストの関連度から、多様性を求めてリコメンドをしてもらうか、好きなタイプのアーティストを深掘りしていくか**自身で決定できるようにするため、ネットワーク図と自身の聴いているアーティストをプロットしたものを用意します。



### SpotifyAPIを使って関連アーティストをdigってネットワーク図を構築する


まずは必要なライブラリのimportを行います。

```python:test.py
import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import json
import networkx as nx
import matplotlib.pyplot as plt
```
 
次に、SpotifyAPIに接続するためのインスタンスを生成し、`spotify`という変数に格納します。

```python:test.py
client_id = 'XXXXXXXXXXXXXXX'
client_secret = 'XXXXXXXXXXXXXXX'
client_credentials_manager = spotipy.oauth2.SpotifyClientCredentials(client_id, client_secret)
spotify = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
```

以下のSpotifyのトップチャートから、人気な邦楽リストをcsvとしてダウンロードしてきましょう！
https://spotifycharts.com/regional/jp/weekly/latest
そしてそのcsvを読み込みます。

```python:test.py
top_records = pd.read_csv("regional-jp-weekly-latest.csv", header=1)
top_artists = top_records["Artist"].unique()
```

ここからはpandasのDataFrame整形のための処理のため、流して読んでください

```python:test.py
artist_info_list = []
for artist in top_artists:
    artist_info_list.append(spotify.search(q='artist:' + artist, type='artist'))
artist_info_list = [artist_info['artists']['items'] for artist_info in artist_info_list]
artist_info_list = [artist_info[0] for artist_info in artist_info_list if len(artist_info) > 0]
## 情報がないものはスキップ
artist_info_df = pd.DataFrame(artist_info_list)
artist_info_df = artist_info_df[['genres','id','name','popularity','type']]
followers = [artist_info_record['followers']['total'] for artist_info_record in artist_info_list]
artist_info_df['followers'] = followers
artist_info_df
```


csvを読み込んで整形した後、SpotifyAPIを用いて、関連アーティストをdigって行きます。再帰関数として実装して、何回digるかを決定します。今回は5回digります。これでもデータ量としては万を軽く越えるため、ご注意ください。

```python:test.py
related_artist_info = []
digged_artist_ids = []
def dig(parent_id,parent_name,limit,i):
    digged_artist_ids.append(parent_id)
#     print(parent_name)
    if len(related_artist_info) % 1000 == 0:
        print(len(related_artist_info))
    related_artists = spotify.artist_related_artists(parent_id)
    for artist_info in related_artists['artists']:
        artist_info['parent_id'] = parent_id
        artist_info['parent_name'] = parent_name
        related_artist_info.append(artist_info) 
        if i < limit:
            if artist_info['id'] in digged_artist_ids:
                pass
            else:
                dig(artist_info['id'],artist_info['name'],limit,i+1)

for parent_id,parent_name in zip(artist_info_df['id'],artist_info_df['name']):
    dig(parent_id,parent_name,limit=6,i=1)


related_artist_df = pd.DataFrame(related_artist_info)
related_artist_df = related_artist_df[['genres','id','name','popularity','type','parent_id','parent_name']]
followers = [artist_info_record['followers']['total'] for artist_info_record in related_artist_info]
related_artist_df['followers'] = followers
related_artist_df
## 親アーティストのparent_idをnullに
artist_info_df['parent_id'] = None
artist_info_df['parent_name'] = None
merge_df = pd.concat([artist_info_df,related_artist_df],sort=False)

```

ここで、ネットワーク図を作るために、関連アーティストとして出てきたアーティストのペアを作成しておきます。

```python:test.py
graph_tuple = []
for network in merge_df[['name','parent_name']].values:
    graph_tuple.append(tuple(network))
    
```


では次に、自身が聴いているアーティスト群を用意しましょう。
Spotifyと接続するためのインスタンスに、`username`と`playlist_id`を与えてあげます。

```python:test.py
import sys
import pprint
import spotipy
import spotify_token as st

client_id = 'XXXXXXXXXXXx'
client_secret = 'XXXXXXXXXXX'
client_credentials_manager = spotipy.oauth2.SpotifyClientCredentials(client_id, client_secret)
spotify = spotipy.Spotify(client_credentials_manager=client_credentials_manager)


username = "XXXXXXXXX"
playlist_name = "XXXXXXXXXXXX"

my_tracks = spotify.user_playlist(username, playlist_id=playlist_name, fields=None)
my_track_list = [track['track'] for track in my_tracks['tracks']['items'] ]
my_track_df1 = pd.DataFrame(my_track_list)
my_track_df1['artists'] = [artists[0]['name'] for artists in my_track_df1['artists']]
```

ここから、ネットワーク図を作るために、`networkx`というライブラリを用いて行きます。
おそらく、現在`graph_tuple`に格納されている関連アーティストのペアは、非常にデータ数が多く、まともに表示できないと思われるので、関連アーティストが少ないアーティストに関しては**枝切り**していきます。また、その中で自身が聴いているアーティストに関しては枝切りしないようにしておきましょう。ここは実行にかなり時間がかかるため、数分ほど待ちましょう

```python:test.py
G = nx.Graph()
G.add_nodes_from(merge_df['name'].unique())
G.add_edges_from(tuple(graph_tuple))


like_artists_name = set(track_df['artists'].unique())
for i in range(3,1,-1):
    remove_nodes = G.degree()
    if i == 1:
        print(remove_nodes)
    remove_node_names = [node[0] for node in remove_nodes if node[1] < 15*i]
    ## 自分のプレイリストに入ってるアーティストは消さない
    remove_node_names = set(remove_node_names) - like_artists_name
    G.remove_nodes_from(remove_node_names)
```

```python:test.py
# グラフの構築
# G = nx.karate_club_graph()
G = nx.Graph()
G.add_nodes_from(merge_df['name'].unique())
G.add_edges_from(tuple(graph_tuple))


like_artists_name = set(track_df['artists'].unique())
for i in range(3,1,-1):
    remove_nodes = G.degree()
    if i == 1:
        print(remove_nodes)
    remove_node_names = [node[0] for node in remove_nodes if node[1] < 10*i]
    ## 自分のプレイリストに入ってるアーティストは消さない
    remove_node_names = set(remove_node_names) - like_artists_name
    G.remove_nodes_from(remove_node_names)
    
## 1以下のやつは中心から離れ過ぎてしまっている可能性が高いので削除
remove_nodes = G.degree()
remove_node_names = [node[0] for node in remove_nodes if node[1] <= 3]
G.remove_nodes_from(remove_node_names)
    
# レイアウトの取得
pos = nx.spring_layout(G)
pr = nx.pagerank(G)
# 可視化
plt.figure(figsize=(100, 100))



# nx.draw_networkx_edges(G, pos)
nx.draw_networkx_nodes(G, pos, node_size=[75000*v for v in pr.values()], node_color=list(pr.values()), cmap=plt.cm.Reds)
# nx.convert_node_labels_to_integers(G, first_label=0, ordering='default', label_attribute=None)

add_artists_nodes = G.degree()
add_artists_names = [node[0] for node in add_artists_nodes]

like_artists_nodes = set(add_artists_names) & like_artists_name

nx.draw_networkx_nodes(G,pos,
                       nodelist=like_artists_nodes,
                       node_color='b',
                       node_size=1500,
                       label=like_artists_nodes,
                       alpha=0.5)


labels = {}
for node in G.degree():
    labels[node[0]] = node[0]
nx.draw_networkx_labels(G, pos,font_size=10)
# nx.draw_networkx_labels(G, pos, labels, font_size=8)
plt.savefig('figure.png',format = 'png', dpi=300) 
plt.show()

```

## プロット結果を使って次に聴く曲の決定
今回、トップチャートからdigったアーティストのネットワーク図と、自身のプレイリストをプロットしたものは以下のようになりました。この図をみると、**どのようなアーティストが同じような場所にあり、自分がどのアーティストを聴いているか**がよくわかりますね！！！！

![ダウンロード (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124611/1fa85eb3-a411-97c5-1988-f94dd22af696.png)
私は基本的にSpotifyがオススメした曲を聴いてプレイリストに追加するという流れが多いため、この図から、**右上の方に位置しているアーティストをよく聴いていることがわかります。**
その中から、わかりやすいアーティスト群をピックアップして見てみましょう。

![ダウンロード__1_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124611/a3aff305-fdbf-fa95-8c3b-59bb5647cc42.png)


今回、『わかりやすくて面白いなー』と感じた二つの領域(アーティスト群)を紹介します。
まず、左のアーティスト群はこのように表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124611/d2c192f9-b695-bcc6-ffcc-5d6a7bad346c.png)

ゆずやコブクロ、ケツメイシが同じような場所にあるのはすごく納得感がありますね。僕はこの辺りのアーティストが好きなことはプロット結果からわかっているので、この辺りのアーティストを埋めるようにオススメするのがいちばん分かりやすいリコメンドだと思います。



また、右側のアーティスト群はこのようになっています。

<img width="995" alt="スクリーンショット 2019-12-22 14.19.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124611/802d30a3-b2e6-8c3c-fe86-d2e87611b68e.png">

この辺りは女性アーティストが集まっているように見えますね。
また、その中でもアニソン系が集まっていそうです。(Craris/EGOIST/fripsideなど)
僕はこの辺りのアーティストを少しだけ聴いているため、周辺のアーティストをオススメすると良いかもしれません。

## まとめ
このように、アーティスト群をネットワーク図として表示することで、自身がどのようなタイプのアーティストが好きなのかよく分かります。
また、**ネットワーク図を参照することで次どのようなアーティストを聞くかの決断を支援することができます。**

![ダウンロード__1_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124611/d8fcc8b1-7140-8d1c-bd9d-d1a261553620.png)

ネットワーク図では、左側の領域や下の領域は一切聴いていない**未開拓領域**です。**自分が好きなタイプのアーティストを深掘りしていくのも面白いですが、このように多様性を求めて全く聴いていないアーティストを聴いてみるのも楽しいですね。**
とりあえず、**自分の好きなアーティストに近く、未開拓領域の方向に位置しているアーティスト** として、今回は`tofubeats`さんを聴いてみようかと思います。

![ダウンロード__1_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124611/3dda0e12-38d2-6f72-5da0-393ffceb3aa8.png)

### 個人的に実装したいもの
リコメンドでは、『**自身がどのようなアーティストを聞きたいか**』ではなく、『**自身が好きそうなアーティスト』をオススメされる**ため、受動的なシステムになっていることが多いです。

しかし、このように**ネットワーク図を用いたリコメンドでは、『多様性を求めて未開拓領域に進むのか』『好きなタイプのアーティスト深掘りしていくのか』ということを自分で決められます。**
こういった、**アシスト寄りのリコメンドシステム**があっても良いのではないかと思いました。

次は、このネットワーク図を用いて実際にリコメンドシステムを提供できるようにシステム構築を頑張ってみます。(Chrome拡張か、webアプリで)
完成したらぜひご利用ください！！


それではこの辺りで僕の記事は終わりにします。
明日は、 @__kaname__ 　さんの記事です。お楽しみに！