Instantly share code, notes, and snippets.
Last active
January 2, 2021 06:47
-
Star
1
(1)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save Kautenja/71f139eee58099b77e91a0d775e42b47 to your computer and use it in GitHub Desktop.
Visualizing Spotify related artist networks using Gephi.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Spotify API\n", | |
"\n", | |
"instead of directly interacting with the Spotify Restful API, I use a lightweight Python wrapper for ease." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 26, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"import spotipy\n", | |
"from spotipy.oauth2 import SpotifyClientCredentials\n", | |
"# spotify credentials for the application:\n", | |
"# \"Related Artist Network Visualizer\"\n", | |
"client_id = 'REDACTED'\n", | |
"client_secret = 'REDACTED'\n", | |
"# create a credential manager and api layer\n", | |
"client_credentials_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret)\n", | |
"sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Root Node\n", | |
"\n", | |
"I'll start with a root node of on of my favorite artists, [Bassnectar](https://www.bassnectar.net)." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 28, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# the spotify id for the artist Bassnectar\n", | |
"bassnectar = 'spotify:artist:1JPy5PsJtkhftfdr6saN2i'" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Building A Network\n", | |
"\n", | |
"To build a network, I'll use a modified form of depth limited DFS to generate a dictionary of artists to a list of related artists." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 70, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def related_network(artist_id, depth: int=3) -> dict:\n", | |
" \"\"\"\n", | |
" Return a dictionary of arist names to lists of their related artists.\n", | |
" \n", | |
" Args:\n", | |
" arist_id: the id of the artist to start the graph from\n", | |
" depth: the depth into the related artist network \n", | |
" \n", | |
" Returns: a dictionary of strings (artist name) to lists (related artists)\n", | |
" \"\"\"\n", | |
" graph = dict()\n", | |
" _related_network(artist_id, depth, graph)\n", | |
" return graph\n", | |
"\n", | |
"def _related_network(artist_id, depth, graph):\n", | |
" \"\"\"\n", | |
" Recursively collect related artists and store the results in the graph.\n", | |
" \n", | |
" Args:\n", | |
" artist_id: the artist to get the related artists of\n", | |
" depth: the current depth in the graph\n", | |
" graph: the dictionary to put the related artist results into\n", | |
" \"\"\"\n", | |
" if depth == 0:\n", | |
" return\n", | |
" name = sp.artist(artist_id)['name']\n", | |
" like_artist = sp.artist_related_artists(artist_id)\n", | |
" graph[name] = [related['name'] for related in like_artist['artists']]\n", | |
" [_related_network(related['id'], depth - 1, graph) for related in like_artist['artists']]" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 74, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": [ | |
"{'Bassnectar': ['Datsik',\n", | |
" 'Pretty Lights',\n", | |
" 'Liquid Stranger',\n", | |
" 'Zeds Dead',\n", | |
" 'Flux Pavilion',\n", | |
" '12th Planet',\n", | |
" 'PANTyRAiD',\n", | |
" 'Big Gigantic',\n", | |
" 'Excision',\n", | |
" 'Tipper',\n", | |
" 'Doctor P',\n", | |
" 'Ganja White Night',\n", | |
" 'GRiZ',\n", | |
" 'Opiuo',\n", | |
" 'Break Science',\n", | |
" 'Borgore',\n", | |
" 'Minnesota',\n", | |
" 'Feed Me',\n", | |
" 'Zomboy',\n", | |
" 'Space Jesus']}" | |
] | |
}, | |
"execution_count": 74, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"# check the base case\n", | |
"related_network(bassnectar, 1)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 71, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"like_nectar = related_network(bassnectar)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Formatting the Network for Gephi" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 92, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"import pandas as pd\n", | |
"import numpy as np" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## nodes.csv" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 152, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def nodes(graph: dict) -> pd.DataFrame:\n", | |
" \"\"\"\n", | |
" Return a dataframe of nodes for the given graph.\n", | |
" \n", | |
" Args:\n", | |
" graph: the graph to generate a unique table of nodes from\n", | |
" \n", | |
" Returns: a dataframe with nodes and unique ids\n", | |
" \"\"\"\n", | |
" _nodes = []\n", | |
"\n", | |
" # iterate over all the artists in the list\n", | |
" for artist, related_list in like_nectar.items():\n", | |
" _nodes.append(artist)\n", | |
" [_nodes.append(related) for related in related_list]\n", | |
"\n", | |
" # keep only unique nodes\n", | |
" _nodes = np.unique(_nodes)\n", | |
" # make a dataframe to generate ids\n", | |
" _nodes = pd.DataFrame(_nodes, columns=['label'])\n", | |
" # use the index columns as the id\n", | |
" _nodes['id'] = _nodes.index\n", | |
" return _nodes" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 153, | |
"metadata": { | |
"scrolled": true | |
}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<div>\n", | |
"<style>\n", | |
" .dataframe thead tr:only-child th {\n", | |
" text-align: right;\n", | |
" }\n", | |
"\n", | |
" .dataframe thead th {\n", | |
" text-align: left;\n", | |
" }\n", | |
"\n", | |
" .dataframe tbody tr th {\n", | |
" vertical-align: top;\n", | |
" }\n", | |
"</style>\n", | |
"<table border=\"1\" class=\"dataframe\">\n", | |
" <thead>\n", | |
" <tr style=\"text-align: right;\">\n", | |
" <th></th>\n", | |
" <th>label</th>\n", | |
" <th>id</th>\n", | |
" </tr>\n", | |
" </thead>\n", | |
" <tbody>\n", | |
" <tr>\n", | |
" <th>0</th>\n", | |
" <td>12th Planet</td>\n", | |
" <td>0</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>1</th>\n", | |
" <td>16 Bit</td>\n", | |
" <td>1</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>2</th>\n", | |
" <td>501</td>\n", | |
" <td>2</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>3</th>\n", | |
" <td>6Blocc</td>\n", | |
" <td>3</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>4</th>\n", | |
" <td>ABSRDST</td>\n", | |
" <td>4</td>\n", | |
" </tr>\n", | |
" </tbody>\n", | |
"</table>\n", | |
"</div>" | |
], | |
"text/plain": [ | |
" label id\n", | |
"0 12th Planet 0\n", | |
"1 16 Bit 1\n", | |
"2 501 2\n", | |
"3 6Blocc 3\n", | |
"4 ABSRDST 4" | |
] | |
}, | |
"execution_count": 153, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"nectar_nodes = nodes(like_nectar)\n", | |
"nectar_nodes.head()" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## edges.csv" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 137, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def edges(graph: dict, nodes: pd.DataFrame) -> pd.DataFrame:\n", | |
" \"\"\"\n", | |
" Return a dataframe of edges based on the graph and table of node ids.\n", | |
" \n", | |
" Args:\n", | |
" graph: the graph to find edges in\n", | |
" nodes: the table of nodes with unique node ids\n", | |
" \n", | |
" Returns: a table of targets to destinations by unique id\n", | |
" \"\"\"\n", | |
" _edges = []\n", | |
"\n", | |
" for artist, related_list in graph.items():\n", | |
" artist_node = nodes['id'][nodes['label'] == artist].values[0]\n", | |
" for related in related_list:\n", | |
" related_node = nodes['id'][nodes['label'] == related].values[0]\n", | |
" _edges.append((artist_node, related_node))\n", | |
"\n", | |
" return pd.DataFrame(_edges, columns=['Source','Target'])" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 139, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<div>\n", | |
"<style>\n", | |
" .dataframe thead tr:only-child th {\n", | |
" text-align: right;\n", | |
" }\n", | |
"\n", | |
" .dataframe thead th {\n", | |
" text-align: left;\n", | |
" }\n", | |
"\n", | |
" .dataframe tbody tr th {\n", | |
" vertical-align: top;\n", | |
" }\n", | |
"</style>\n", | |
"<table border=\"1\" class=\"dataframe\">\n", | |
" <thead>\n", | |
" <tr style=\"text-align: right;\">\n", | |
" <th></th>\n", | |
" <th>Source</th>\n", | |
" <th>Target</th>\n", | |
" </tr>\n", | |
" </thead>\n", | |
" <tbody>\n", | |
" <tr>\n", | |
" <th>0</th>\n", | |
" <td>34</td>\n", | |
" <td>92</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>1</th>\n", | |
" <td>34</td>\n", | |
" <td>295</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>2</th>\n", | |
" <td>34</td>\n", | |
" <td>220</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>3</th>\n", | |
" <td>34</td>\n", | |
" <td>428</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>4</th>\n", | |
" <td>34</td>\n", | |
" <td>141</td>\n", | |
" </tr>\n", | |
" </tbody>\n", | |
"</table>\n", | |
"</div>" | |
], | |
"text/plain": [ | |
" Source Target\n", | |
"0 34 92\n", | |
"1 34 295\n", | |
"2 34 220\n", | |
"3 34 428\n", | |
"4 34 141" | |
] | |
}, | |
"execution_count": 139, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"nectar_edges = edges(like_nectar, nodes(like_nectar))\n", | |
"nectar_edges.head()" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Full Stack Graph Generation" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 150, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"def graph_for(artist_id: str, depth: int=3) -> tuple:\n", | |
" \"\"\"\n", | |
" Return a related artist network for the artist with the given id.\n", | |
" \n", | |
" Args:\n", | |
" artist_id: the spotify id of the artist\n", | |
" depth: the depth of related artists to consider\n", | |
" \n", | |
" Returns: a tuple with both the table of unique nodes, and the table of edges\n", | |
" \"\"\"\n", | |
" related = related_network(artist_id, depth)\n", | |
" _nodes = nodes(related)\n", | |
" _edges = edges(related, _nodes)\n", | |
" return _nodes, _edges" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 151, | |
"metadata": { | |
"scrolled": false | |
}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
" label id\n", | |
"0 12th Planet 0\n", | |
"1 16 Bit 1\n", | |
"2 501 2\n", | |
"3 6Blocc 3\n", | |
"4 ABSRDST 4\n", | |
" Source Target\n", | |
"0 34 92\n", | |
"1 34 295\n", | |
"2 34 220\n", | |
"3 34 428\n", | |
"4 34 141\n" | |
] | |
} | |
], | |
"source": [ | |
"# generate a related artist graph for bassnectar\n", | |
"graph = graph_for(bassnectar)\n", | |
"# print the nodes and save them to disk for Gephi\n", | |
"print(graph[0].head())\n", | |
"graph[0].to_csv('./nodes.csv', index=False)\n", | |
"# print the edges and save them to disk for Gephi\n", | |
"print(graph[1].head())\n", | |
"graph[1].to_csv('./edges.csv', index=False)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Result\n", | |
"\n", | |
"After playing with some settings in Gephi, we're left with this nice graph.\n", | |
"\n", | |
"" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
" " | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.6.1" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment