2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Spotify API で遊ぶ ~NetworkX で関連アーティストを可視化~

Posted at

Spotify API で色々な情報を取得できると知り、面白そうだったので遊んでみました。
n 番煎じですが、あるアーティストに対して、その関連アーティストをグラフにして可視化してみます。

今回のプログラムは Google Colab で実行できるようにしています。
https://colab.research.google.com/drive/1Aev5FZ3an13QMwJuJzP9Ahekv_Oosfs1?usp=sharing

GitHub は以下。
https://github.com/yousukeayada/spotify

環境

  • macOS Catalina 10.15.6
  • Python 3.8.1
  • spotipy==2.19.0
  • networkx==2.6.2

API のテスト

まず Spotify for Developers にログインして create an app
https://developer.spotify.com/dashboard/
作成できたら client idclient secret をメモっておきます。

screenshot.png

Python のライブラリ spotipy を使ってテストします。

pip install spotipy

まず、指定したアーティストの情報を取得してみます。
https://spotipy.readthedocs.io/en/2.19.0/#getting-started
アーティスト ID は、アプリだと「リンクをシェア」から URL がわかるので、そこから抜き出します。

import json

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials

def auth_spotify():
    client_id     = "xxxxx"
    client_secret = "xxxxx"

    client_credentials_manager = SpotifyClientCredentials(client_id, client_secret)
    return spotipy.Spotify(client_credentials_manager=client_credentials_manager, language='en')

spotify = auth_spotify()

name_id = {"Sambomaster": "5ydDSP9qSxEOlHWnpbblFB", "FLOW": "3w2HqkKa6upwuXEULtGvnY"}
artist_info = spotify.artist(name_id["Sambomaster"])
print(json.dumps(artist_info))

出力を jq コマンドで整形したものが以下になります。

artist.json
{
  "external_urls": {
    "spotify": "https://open.spotify.com/artist/5ydDSP9qSxEOlHWnpbblFB"
  },
  "followers": {
    "href": null,
    "total": 240331
  },
  "genres": [
    "j-pop",
    "j-poprock",
    "j-rock"
  ],
  "href": "https://api.spotify.com/v1/artists/5ydDSP9qSxEOlHWnpbblFB",
  "id": "5ydDSP9qSxEOlHWnpbblFB",
  "images": [
    {
      "height": 640,
      "url": "https://i.scdn.co/image/ab6761610000e5ebb83514fcda99f392cabbb051",
      "width": 640
    },
    {
      "height": 320,
      "url": "https://i.scdn.co/image/ab67616100005174b83514fcda99f392cabbb051",
      "width": 320
    },
    {
      "height": 160,
      "url": "https://i.scdn.co/image/ab6761610000f178b83514fcda99f392cabbb051",
      "width": 160
    }
  ],
  "name": "Sambomaster",
  "popularity": 53,
  "type": "artist",
  "uri": "spotify:artist:5ydDSP9qSxEOlHWnpbblFB"
}

次に、関連アーティストの取得です。

# (略)

artist_info = spotify.artist(name_id["Sambomaster"])
artist_id = artist_info["id"]

related_artists = spotify.artist_related_artists(artist_id)
print(json.dumps(related_artists))

出力(長いので一部のみ)

related_artists.json
{
  "artists": [
    {
      "external_urls": {
        "spotify": "https://open.spotify.com/artist/2dP0aHVXt8dDPCw5d2Jw0m"
      },
      "followers": {
        "href": null,
        "total": 138679
      },
      "genres": [
        "j-pop",
        "j-poprock",
        "j-rock",
        "japanese alternative rock",
        "japanese punk rock"
      ],
      "href": "https://api.spotify.com/v1/artists/2dP0aHVXt8dDPCw5d2Jw0m",
      "id": "2dP0aHVXt8dDPCw5d2Jw0m",
      "images": [
        {
          "height": 640,
          "url": "https://i.scdn.co/image/ab6761610000e5eb2b458dd9eb80278b2566d299",
          "width": 640
        },
        {
          "height": 320,
          "url": "https://i.scdn.co/image/ab676161000051742b458dd9eb80278b2566d299",
          "width": 320
        },
        {
          "height": 160,
          "url": "https://i.scdn.co/image/ab6761610000f1782b458dd9eb80278b2566d299",
          "width": 160
        }
      ],
      "name": "GING NANG BOYZ",
      "popularity": 47,
      "type": "artist",
      "uri": "spotify:artist:2dP0aHVXt8dDPCw5d2Jw0m"
    },
    {
      "external_urls": {
    ...
}

以上でテスト完了です。

関連アーティストの情報をまとめる

流れとしては以下にようになります。

  1. 初めに 1 組アーティストを指定して、その関連アーティストを取得する。
  2. それぞれの関連アーティストに対して、またその関連アーティストを取得する。

繰り返しすぎるととんでもない量になるので、注意してください。
また、一部 search に失敗するアーティストがあります(結果が日本語で返ってきたりなど)が、そちらは省くことにしました。調べたところ API 側のエラーっぽいです。
search ではなく spotify.artist(id) を使うことでおそらく確実に取得できます。
https://teratail.com/questions/236378

import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import networkx as nx
import matplotlib.pyplot as plt


def auth_spotify():
    client_id     = "xxxxx"
    client_secret = "xxxxx"

    client_credentials_manager = SpotifyClientCredentials(client_id, client_secret)
    return spotipy.Spotify(client_credentials_manager=client_credentials_manager, language='en')

def add_new_artist(artist_id, df):
    artist_info = spotify.artist(artist_id)

    related_artists      = spotify.artist_related_artists(artist_id)
    related_artist_ids   = [artist["id"] for artist in related_artists["artists"]]
    related_artist_names = [artist["name"] for artist in related_artists["artists"]]

    row = pd.Series([artist_info["id"], artist_info["name"], artist_info["popularity"], artist_info["genres"], related_artist_ids, related_artist_names], index=df.columns)
    df  = df.append(row, ignore_index=True)
    return df


spotify = auth_spotify()

artists_df = pd.DataFrame(columns=["id", "name", "popularity", "genres", "related_artist_ids", "related_artist_names"])

# 最初のアーティストを取得
name_id = {"Sambomaster": "5ydDSP9qSxEOlHWnpbblFB", "FLOW": "3w2HqkKa6upwuXEULtGvnY"}
artists_df = add_new_artist(name_id["Sambomaster"], artists_df)

# 関連アーティストとその関連アーティストを取得
l = len(artists_df["related_artist_names"][0])
for i in range(l):
    ids    = artists_df["related_artist_ids"][i]
    names  = artists_df["related_artist_names"][i]
    for id, name in zip(ids, names):
        print(name)
        artists_df = add_new_artist(id, artists_df)


artists_df = artists_df.drop_duplicates(subset="id")

artists_df.to_csv("artists.csv", index=False)

最終的に以下のような DataFrame が作成できました。

screenshot2.png

NetworkX で可視化

Google Colab を使う場合は元から入っています。

pip install networkx

有向グラフで可視化します。単にグラフを描画するだけだと複雑で見辛いので、ここではいくつかルールを設けます。

  • popularity が大きいほどノードを大きくする
  • in_degree (自分に向かう矢印)が多いほどノードの色を濃くする

グラフを見やすくする工夫は他にもたくさんあると思います。例えば、ジャンルに j-rock が含まれているものだけ色を変えるなど、目的に応じて設定すればいいでしょう。

from ast import literal_eval

import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties


artists_df = pd.read_csv("artists.csv", converters={"genres":literal_eval, "related_artist_ids":literal_eval, "related_artist_names":literal_eval})

G = nx.MultiDiGraph()

# node データの追加
names = [name for name in artists_df["name"]]
G.add_nodes_from(names)

edges = []
for name, related_artist_names in zip(artists_df["name"], artists_df["related_artist_names"]):
    for related_name in related_artist_names:
        if related_name in names:
            e = (name, related_name)
            edges.append(e)
            # print(e)

# edge データの追加
G.add_edges_from(edges)

for d in zip(*G.in_degree()):
    max_in_degree = max(d)

plt.figure(figsize=(30, 30), dpi=300)
pos = nx.spring_layout(G, k=0.3, seed=23)

node_size = [popularity*40 for popularity in artists_df["popularity"]]
# in_degree を正規化して alpha とする
node_alpha = [d[1] / max_in_degree for d in G.in_degree()]
nx.draw_networkx_nodes(G, pos, node_color='red',alpha=node_alpha, node_size=node_size, edgecolors="black")
text_items = nx.draw_networkx_labels(G, pos, font_size=5, font_weight="bold")
nx.draw_networkx_edges(G, pos, alpha=0.4, edge_color='royalblue')

# 日本語フォントを使う
font_prop = FontProperties(fname="/System/Library/Fonts/ヒラギノ角ゴシック W8.ttc", weight=1000, size=5)
for t in text_items.values():
    t.set_fontproperties(font_prop)

plt.savefig("network.png")

これで可視化した画像が保存されます。

サンボマスターを起点とした場合、以下のようなグラフとなりました。

network.jpg

なんとなく 2 つグループに分かれていることが読み取れますが、詳しくはわからないので、拡大して見てみましょう。
真ん中左あたりにコブクロ、いきものがかり、スキマスイッチなど、テレビでもよく見るようなアーティストが集まっていることがわかります。

screenshot3.png

in_degree が最も多かったのは Straightener 、次点で Base Ball Bear でした。

screenshot4.png

別のアーティストでもやってみます。

FLOW を指定してみるとわかりやすく複数のグループにわかれました。詳細は画像参照。

network_edited.jpg

また、それぞれのグループの間に橋渡し的な存在を確認できます。例えば、青色と紫色を繋ぐのは UVERworld 、青とオレンジを繋ぐのは藍井エイル、青と緑は ORANGE RANGE です。

最後に、最近話題の YOASOBI でやってみます。

network.jpg

やはり若者に人気のアーティストが多い感じがします。左下には上記と同じような J-Pop 島があります。 J-Pop はあらゆるジャンルからの導線がありそうですね。

ジャンルの集計

関連アーティストのジャンルを多い順に集計します。

from ast import literal_eval
from collections import defaultdict

import pandas as pd


def count_genres(artists_df):
    genres_dict = defaultdict(int)
    for genres in artists_df["genres"]:
        for g in genres:
            genres_dict[g] += 1
    genres_dict = sorted(genres_dict.items(), key=lambda x:x[1], reverse=True)

    genres_dict2 = {}
    for k, v in genres_dict:
        genres_dict2[k] = v
    return genres_dict2

artists_df = pd.read_csv("artists.csv", converters={"genres":literal_eval, "related_artist_ids":literal_eval, "related_artist_names":literal_eval})

genres_dict = count_genres(artists_df)
for k, v in genres_dict.items():
    print(k, v)

先ほどの YOASOBI だと以下のような結果が得られます。

j-pop 117
j-rock 53
j-poprock 42
anime 29
japanese alternative rock 24
japanese teen pop 23
anime rock 16
vocaloid 10
japanese r&b 9
japanese singer-songwriter 8
j-division 6
j-pop boy group 6
j-acoustic 5
japanese indie rock 5
japanese soul 4
j-pixie 4
japanese pop rap 4
j-idol 4
japanese electropop 3
j-rap 3
classic j-pop 3
j-pop girl group 3
j-indie 3
japanese punk rock 2
japanese emo 2
japanese alternative pop 1
japanese new wave 1
city pop 1
japanese trap 1
kyushu indie 1
kawaii edm 1
japanese vtuber 1
honeyworks 1
j-punk 1
japanese pop punk 1
visual kei 1
japanese shoegaze 1
hokkaido indie 1
okinawan folk 1
okinawan pop 1
japanese indie pop 1

枝刈り

エッジが多すぎて見辛い場合は枝刈りをするといいでしょう。試しに単方向のエッジを削除して、双方向のみ残します。

import networkx as nx

def remove_unidirectional(G):
    unidirectional = [(u, v) for u, v in G.edges() if not (v, u) in G.edges()]
    for u, v in unidirectional:
        G.remove_edge(u, v)
    return G

G = nx.MultiDiGraph()
# (中略)
print(G.number_of_edges()) # 枝刈り前
G = remove_unidirectional(G)
print(G.number_of_edges()) # 枝刈り後

# 以下グラフ描画
# ...

上記サンボマスターの例で枝刈りしてみました。エッジの数は 500 ほど減ったんですが、見やすさはあまり変わらないですね...

network_bi.jpg

最後に

普段から Spotify を利用してはいるものの、音楽には全然詳しくないので、これを機に色んな曲を聴いてみようと思いました。みなさんも好きなアーティストでやってみてはどうでしょうか。新しい音楽に触れるきっかけになれば幸いです。

今回のプログラムは Colab 上でも実行できます。
https://colab.research.google.com/drive/1Aev5FZ3an13QMwJuJzP9Ahekv_Oosfs1?usp=sharing

参考

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?