Читать нас в Telegram
Иллюстратор: Анна Андреева

Мы уже писали про Spotify, а точнее о том, как сервис угадывает наши предпочтения в музыке. Сегодня мы попытаемся сами проанализировать наши музыкальные предпочтения, а точнее, понять, что о нашем вкусе говорит наш плейлист. А сделаем мы это при помощи открытого API от Spotify.

Spotify хранит на своих серверах информацию о каждом треке. Здесь есть данные о размерности трека, его энергичности, темпе и прочие музыкальные характеристики. Вот с ними мы и будем работать.

Вооружаемся!

Какая задача перед нами стоит? Сначала следует зарегистрироваться на Spotify как разработчику, создать свое приложение, подключить его к API, получить информацию о своем плейлисте. Дальше мы сформируем из данных таблицу и скачаем ее на компьютер — в принципе на этом можно и остановиться и дальше работать в Excel, Tableau, Datawraper.io, Power BI и прочих программах для работы с данными. Но мы визуализируем данные прямо в нашей IDE.

Что такое IDE? Это среда разработки, в которой мы и будем писать код. Нам понадобится Jupyter — эта IDE позволяет запускать код фрагментами, менять их местами, делать пометки markdown-текстом. Для нашей задачи — очень удобная среда. Установить Jupyter можно через дистрибутив Anaconda (сборник разных IDE для дата сайентистов).

Экран Anaconda — третья плитка — нужный нам Jupyter

Но давайте обо всем по порядку.

На старт, внимание, кодим! А, нет, фальстарт

Для начала работы нам нужно создать приложение на платформе Spotify. Переходим по ссылке developer.spotify.com, логинимся под обычным аккаунтом сервиса и нажимаем на блок Create an App:

Называем приложение, задаем его описание (это обязательно!), соглашаемся с необходимостью договориться со Spotify, если наше приложение будет коммерческим и соглашаемся с условиями для разработчиков и использованием брендбука компании. Сейчас мы не ставим задачу создать коммерческое приложение и брендбук использовать не будем, но галочки поставить необходимо.

Попадаем на экран нашего приложения. Там нажимаем Edit settings и в поле Redirect URIs печатаем: «http://www.example.com/callback» и сохраняем изменения:

Затем копируем Client ID и Client Secret — последний изначально скрыт. Увидеть его можно нажав на кнопку Show client secret.

Теперь нам нужно узнать свой id в Spotify. Для этого переходим по ссылке www.spotify.com/ru-ru/account/overview и копируем «Имя пользователя» — не пугайтесь, там действительно странная абракадабра из букв и цифр. Теперь у нас есть все, чтобы начать кодить!

Вот теперь — кодим!

Теперь, подробней о тех библиотеках, которые мы будем использовать:

  • spotipy — библиотека для работы со Spotify Web API через Python. spotipy.oauth2 позволит нам подключиться к API, spotipy.util — поможет нам продлить действие выданного нам токена (по факту, разрешения на работу в API)
  • matplotlib — добавит возможность создавать 2d-графики.
  • seaborn — надстройка над matplotlib, поможет нам облагородить вид графиков,
  • pylab — позволит нам отображать сразу несколько графиков и диаграмм в одном output’е — позже об этом поговорим.
  • pandas — с помощью этой библиотеки мы сформируем из полученных данных таблицу и скачаем excel-файл с данными.

Для того, чтобы использовать библиотеку spotipy ее сначала нужно установить:

!pip install spotipy
# Импортируем библиотеки
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import spotipy.util as util
import matplotlib.pyplot as plt
import seaborn as sns
import pylab
import pandas as pd

Важно: обратите внимание на «as plt/sns/pd» — так мы просто сократим название библиотек — теперь обратиться к ним (воспользоваться их возможностями) мы можем по аббревиатуре (алиасу).

Затем записываем в переменные client ID и client secret, redirected URI, имя пользователя и scope.

### ПОДКЛЮЧАЕМСЯ К API

# Записываем в переменные:  
cid = '7f86427681624beebcf052ac3b240f26' # client ID
secret = '378042dd0e1d430fbb82922b9729bf03' # client secret
rur = 'https://example.com/callback/' # redirected URI
scope = 'user-library-read' # допуск к возможностям API
user = 'bmrmnx9nzd0m1xuaqi6q5091y' # имя пользователя

Первые два — своего рода паспорт нашего приложения: мы даем сервису понять, что к API пытается подключится именно наше приложение «Системный Блок{ъ}». Redirected URI, который мы прописали в настройках приложения ранее позволит нам зарегистрировать пользователя в приложении. Имя пользователя, которое мы записали в переменную user покажет программе: регистрировать нужно именно нас (в смысле пользователя Spotify).

Самое интересное здесь — это scope — это своего рода «допуск» к некоторым из возможностей, которые дает разработчикам Spotify API. В нашем случае мы просим разрешения на чтение библиотеки (аудиотеки) пользователя, который залогинится в нашем приложении. О других scopes можете прочитать вот здесь.

Если что, client ID и client secret можете использовать наши — в таком случае создавать свое приложения на developer.spotify.com совсем необязательно. А вот в переменную user нужно записать именно свое имя пользователя (если вы не хотите получать данные о плейлисте автора этой статьи, конечно)

Далее каждая записанная нами в переменную информация передается через протокол авторизации SpotifyOAuth, и все это также записывается в переменную sp. Это можно сравнить с пакетом документов: вот мой паспорт (client ID, client secret), вот мой допуск (scope), вот мой допуск к определенной аудиотеке (username), вот откуда мы его получили (redirected URI)

# подключаемся с помощью SpotifyOAuth к WEB API
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(
    client_id=cid, 
    client_secret=secret, 
    redirect_uri=rur, 
    scope=scope, 
    username=user)) 

Когда мы запустим ячейку с кодом, в котором мы подключаемся к API, то откроется тот самый Redirected URI, он сменится страницей аутентификации пользователя и снова вернется Redirected URI, но непростой, а с добавленным кодом. Весь новый адрес нужно скопировать и вставить в поле «Enter the URL you were redirected to:» — так мы завяжем первый контакт нашего приложения и Spotify API.

Но бывает и так, что действие нашего токена, выданного нам сервисом (мы его скормили коду вместе с измененным ранее URI). Тогда его необходимо продлить, что мы и делаем с помощью атрибута prompt_for_user_token():

# Продлеваем действия токена
util.prompt_for_user_token(username=user,
                           scope=scope,
                           client_id=cid,
                           client_secret=secret,
                           redirect_uri=rur)

В самом крайнем случае можно создать новое приложение на сайте и перезаписать информацию о приложении в протокол SpotifyOAuth.

Внутрь атрибута записываем всю ту же информацию, что записывали в SpotifyOAuth чуть раньше.

Дальше создаем словарь lib, в который впишем данные о нашей музыке:

lib = {'band':[], 
       'track':[],
       'id':[],
       'acousticness':[], 
       'danceability':[], 
       'duration_ms':[], 
       'energy':[], 
       'instrumentalness':[], 
       'key':[], 
       'liveness':[],
       'loudness':[], 
       'mode':[], 
       'speechiness':[],  
       'tempo':[], 
       'time_signature':[], 
       'valence':[]}

Теперь стоит поговорить о том, что мы вообще хотим вытащить.

Помимо информации об артисте (Spotify использует «artists» как идентификатор, но мы пишем bands — так чуть короче) и названия трека (track), мы записываем также id — по нему можно найти всю информацию о треке.

О каждом треке в Spotify есть множество информации, в том числе о музыкальных характеристиках трека. Музыкальные характеристики нужно «вызывать» отдельно, но об этом позже. Какие характеристики есть?

  • acousticness — «мера уверенности» (от 0 до 1) того, что в треке сделан упор на акустические инструменты,
  • danceability — эта характеристика показывает, насколько трек может считаться танцевальным. В Spotify говорят, что оценивают «темп, стабильность ритма, силу битов и общую регулярность».
  • duration_ms — продолжительность в миллисекундах,
  • energy — энергичность трека, изменяется от 0 до 1,
  • instrumentalness — показывает наличие вокала в записи. Чем ближе значение к единице, тем меньше вокала в треке, а значит, тем более он инструментальный,
  • key — показывает преобладание определенной тональности в записи. Подробней об этом — здесь,
  • liveness — показывает, присутствовали ли при записи слушателили (как правило, не молчащие),
  • loudness — показывает помехи звукозаписывающего оборудования. Шкала расположена от минус бесконечности до нуля — чем ближе значение к нулю, чем чище запись от помех,
  • mode — тут все просто, минор или мажор, 0 или 1,
  • speechiness — чем ближе значение этой характеристике к единице, значит, чем больше в записи голоса, а инструментов (музыки) меньше,
  • tempo — темп композиции. Измеряется в BPM («beats per minute») — ударов в минуту,
  • time_signature — размерность трека, показывает сильные доли. Например, классический размер — 4/4 (каждая четвертая нота — ударная). Размеры могут быть и аритмические, 5/4 и больше — это скорее, характерно для музыкальных стилей, работающих с аритмией — мат-метал, например,
  • valence — позитивность трека. Чем ближе значение к единице, тем более трек позитивный.

Через тернии — к данным

Теперь наша задача — получить через API нужные нам данные. Для этого используем атрибут current_user_saved_tracks() В него прописываем два параметра.

Limit — сколько одновременно мы получим треков из плейлиста. Значение не бесконечное, от 1 до 50. Важный момент: если мы не пропишем этот параметр, ничего страшного не случится — код по умолчанию покажет 20 треков.

Offset — задает стартовую отметку, с которой API начнет «возвращать» нам данные. Например, если пропишем «9», то нам вернут 20 треков, начиная с десятого. Почему с десятого? Нормальный человек начинает счет с единицы, Python — с нуля. Поэтому, прописывая числа для offset важно проверять себя и ставить число на единицу меньше желаемого.

Еще есть атрибут market — он поможет возвращать только те песни, которые доступны в определенных странах — нам это не нужно, мы хотим забрать информацию обо всех треках.

Теперь напишем код:

# Получаем список треков
# Записываем в словарь исполнителя, название трека и его id

i = 0 # задаем стартовое значение для offset
while True:
        results = sp.current_user_saved_tracks(limit=50, offset=i)
        if len(results['items']) != 0:
            for idx, item in enumerate(results['items']):
                track = item['track']
                print(idx, track['artists'][0]['name'], " – ", track['name'], track['id'])
                lib['band'].append(track['artists'][0]['name'])
                lib['track'].append(track['name'])
                lib['id'].append(track['id'])

            i+=50
        else: 
            break

Не бойтесь, сейчас мы объясним, почему все так монструозно выглядит.

Итак, у нас проблема: current_user_saved_tracks() выдает максимум по пятьдесят треков — у нас их явно больше пятидесяти. Все ведь много и часто лайкают треки в стриминговых сервисах? Если нет, то делайте это чаще, учите нейросеть — вашу личную ищейку в мире музыки.

Для чистоты эксперимента на новеньком аккаунте автора статьи в плейлисте 129 песен.

Поэтому нам надо зациклить код, чтобы тот сам по себе менял значение offset. Поэтому для начала задаем переменную i со значением 0 (помните, Python начинает счет с нуля!). Затем записываем «while True» — это интересная конструкция — обычно после while мы должны написать условие, по которому Python будет ориентироваться — продолжать исполнять код в цикле или нет.

Здесь мы создаем цикл без явного условия — код внутри цикла будет исполняться, пока ему что-то не помешает. Далее в переменную results записываем информацию о треках из плейлиста юзера, по пятьдесят за раз. Для offset прописываем i — в дальнейшем мы будем обновлять эту переменную. Именно поэтому «i = 0» мы записали до цикла — иначе значение i каждый раз будет сбрасываться до нуля.

В results программа выдаст словарь, с такой структурой:

{‘items’: [{‘track1’:‘некая информация о треке 1 номер раз’, 
                     ‘некая информация о треке 1 номер два’,
                     ‘некая информация о треке 1 номер три’]},
           {‘track2’:‘некая информация о треке 2 номер раз’, 
                     ‘некая информация о треке 2 номер два’,
                     ‘некая информация о треке 2 номер три’]}
}

Непонятно? Смотрите: в словаре выше же у нас есть key (ключ) items, у которого есть values (значения), они скомпонованы в список (выделяется квадратными скобками), а внутри списков у нас есть словари (выделяются фигурными скобками со своими ключами и значениями).

Для нас выдача информации в виде словаря очень удобна. Мы можем проследить длину списка results[‘items’] — и здесь уже Python заботится о нас и показывает реальное число, не считая первый элемент за ноль. То есть, если длина списка будет равна пятидесяти, Python так и напишет: «50».

Поэтому мы и пишем, что длина словаря не должна быть равна нулю, а иначе цикл должен остановиться:

if len(results['items']) != 0:
            for idx, item in enumerate(results['items']):
                track = item['track']
                print(idx, track['artists'][0]['name'], " – ", track['name'])
                lib['band'].append(track['artists'][0]['name'])
                lib['track'].append(track['name'])
                lib['id'].append(track['id'])

            i+=50
        else: 
            break

Дальше мы в цикле for пишем примерно следующее: «для каждого ключа (idx) и значения (item) в пронумерованном списке треков (функция enumerate присвоит каждому треку порядковый номер и сделает из него ключ) выполнять…»

Что выполнять? Во-первых, в переменную track запишем один трек, затем выведем его порядковый номер, исполнителя и название трека. Затем в тот самый словарь lib запишем исполнителя, название трека и его id— последнее позволит нам позже выкачать всю информацию о музыкальных характеристиках треков.

После всего всего этого мы прибавляем к переменной i — 50 — это позволит на следующем витке цикла продолжить забирать информацию о треках без повторов и пропусков— помним, что стартовали мы с нуля, а значит, что current_user_saved_tracks() остановится на 49-ом элементе, а при новом витке стартует с 50-ого.

Теперь можем приступить к скачиванию информации о музыкальных характеристиках:

for i in range(len(lib['track'])):
    tf = sp.audio_features(lib['id'][i])
    lib['acousticness'].append(tf[0]['acousticness'])
    lib['danceability'].append(tf[0]['danceability'])
    lib['duration_ms'].append(tf[0]['duration_ms'])
    lib['energy'].append(tf[0]['energy'])
    lib['instrumentalness'].append(tf[0]['instrumentalness'])
    lib['key'].append(tf[0]['key'])
    lib['liveness'].append(tf[0]['liveness'])
    lib['loudness'].append(tf[0]['loudness'])
    lib['mode'].append(tf[0]['mode'])
    lib['speechiness'].append(tf[0]['speechiness'])
    lib['tempo'].append(tf[0]['tempo'])
    lib['time_signature'].append(tf[0]['time_signature'])
    lib['valence'].append(tf[0]['valence'])

Мы опять используем цикл. Здесь каждый элемент списка, в котором хранятся id треков (lib[’id’][i]) мы записываем в переменную tf и добавляем всю информацию о треке и прогоняем цикл, пока не запишем информацию обо всех треках.

Что за [0] в строчке, например, tf[0][’acousticness’]?

Вот как выглядит наш элемент tf:

[{'danceability': 0.552,
  'energy': 0.955,
  'key': 4,
  'loudness': -5.808,
  'mode': 0,
  'speechiness': 0.0617,
  'acousticness': 4.31e-05,
  'instrumentalness': 0.835,
  'liveness': 0.345,
  'valence': 0.436,
  'tempo': 138.001,
  'type': 'audio_features',
  'id': '2WoJYRWReDltBFsr5M05p7',
  'uri': 'spotify:track:2WoJYRWReDltBFsr5M05p7',
  'track_href': 'https://api.spotify.com/v1/tracks/2WoJYRWReDltBFsr5M05p7',
  'analysis_url': 'https://api.spotify.com/v1/audio-analysis/2WoJYRWReDltBFsr5M05p7',
  'duration_ms': 285217,
  'time_signature': 4}]

Дело в том, что то, что выдает нам атрибут audio_features() помещено в словарь внутри списка. Соответственно, по tf — мы адресуемся к элементу audio_features(), то есть нужную нам информацию, дальше с помощью [0] — адресуемся к первому элементу списка, то есть тот самый словарь, а уже потом вызываем значение по ключу [’acousticness’]

Заметили, что у нас есть информация о длительности трека, но в миллисекундах (‘duration_ms’)? Переведем это значение в минуты. В нашем словаре создадим ключ duration_min со значением в виде пустого списка — list()

А затем каждое из значений duration_ms делим на 60 тысяч и получим минуты. Затем запишем полученное в значения для ключа duration_min.

lib['duration_min'] = list()

for i in range(len(lib['duration_ms'])):
    lib['duration_min'].append(lib['duration_ms'][i] / 60000)

Теперь самое важное.

Мы записываем в переменную наш словарь, который мы превратили в объект DataFrame из библиотеки pandas (его суть в табличном отображении данных), а затем сохраняем наши данные как excel файл. Чтобы узнать куда он сохранился напишите «!cd» в новой ячейке — в выводе Jupyter подскажет путь до файла:

ldf = pd.DataFrame.from_dict(lib)
ldf.to_excel('spotify_playlist_features.xlsx')

Теперь мы можем оставить Jupyter и работать с данными в Excel’е, например. Но так ведь не интересно, поэтому мы визуализировали все с помощью seaborn.

Seaborn — самые быстрые графики на всем диком Windows

Мы не будем писать подробно о работе с этой библиотекой, если хотите поиграться с seaborn у них есть хорошая документация.

Мы же поясним самые важные на наш взгляд моменты.

Итак, мы хотим визуализировать, например, распределенность треков в нашем плейлисте по длительности в минутах (не зря же мы делили миллисекунды, в конце-концов).

# Длительность в минутах
ax = sns.kdeplot(ldf['duration_min'], shade=True, color="#1DB954", cut=0)

plt.xlabel("Длительность (в минутах)") # подпись оси x
plt.ylabel("Распределение (в %)") # подпись оси y
plt.title("Распределенность аудиотеки по длительности") # подпись заголовка

Здесь мы используем диаграмму распеределенности (kdeplot) библиотеки seaborn (здесь sns) в параметрах пишем: данные, которые хотим визуализировать (ldf[’duration_min’]), оставляем заливку ниже линии распределенности (shade=True), указываем цвет линии и заливки (color=»#1DB954″ — здесь мы взяли официальный зеленый Spotify) убрали у графика предсказывающую функцию (cut=0) — график распределенности предсказывает какие значения могут принимать наши данные, при расширении шкалы, по которой рассчитывается распределенность.

Дальше мы просто создаем подписи к осям и пишем заголовок к графику — в принципе, это делать не обязательно, но желательно.

И вот что вышло:

Кроме того, на один график можно поместить две линии распределенности:

# Распределённость valence и energy
ax = sns.kdeplot(df['valence'], shade=False, color="red", cut=0)
ax = sns.kdeplot(df['energy'], shade=False, color="blue", cut=0)
plt.xlabel("Значение (0:1)")
plt.ylabel("Распределение (в %)")
plt.title("Распределённость valence и energy")

Можно посмотреть на зависимости некоторых из музыкальных характеристик. Например, с помощью точечных диаграмм (scatterplot). В аттрибуты записываем значения по x и y, в data — данные. В нашем случае в data мы записываем DataFrame с данными, а в x и y — нужные нам ключи с данными (danceability, energy)

# Зависимость danceability и energy
ax = sns.scatterplot(x="energy", y="valence", data=ldf)
plt.title("Зависимость danceability и energy")

Но самое интересное — это построить сразу несколько графиков:

# Строим 9 графиков разом
fig, axes = plt.subplots(nrows=3, ncols=3, figsize=(10, 10), dpi=300)
fig.subplots_adjust(hspace=0.4, wspace=0.7)

Здесь мы используем модуль pylab для того, чтобы сначала построить сетку, которую мы потом наполним графиками:
сначала мы записываем характеристики сетки в subplots: nrows (количество строк), ncols (количество колонок), figsize (размер каждого графика в дюймах), dpi (плотность точек на дюйм — чем больше, чем четче график).

Записываем мы subplots в две переменные (fig, axes) — первая поможет прописать в атрибут subplots_adjust расстояние между графиками между строк (hspace) и на расстояние между колонок (wspace)

pylab.subplot (3, 3, 1)
ac = sns.kdeplot(ldf['acousticness'], shade=False, color="green", cut=0)

pylab.subplot (3, 3, 2)
ac = sns.kdeplot(ldf['danceability'], shade=False, color="green", cut=0)
[...]
plt.show()

Затем мы начинаем заполнять сетку. Сначала указываем место, которое будет занимать график — pylab.subplot (3, 3, 1). обратите внимание, что первые два числа — всегда повторение nrows и ncols. Затем, по знакомой схеме создаем график (и он может быть любым) и командой plt.show() выводим графики на экран.

И — вуаля!

Так, одно API и две библиотека Python помогают нам лучше понять себя и свои музыкальные пристрастия: любим мы более энергичную музыку или нет, а также позитивная она, меланхоличная или вовсе, депрессивная; любим мы свои песни без слов или с текстами (сейчас любители классики и рэпа с подозрительно посмотрели на друг друга).

В сочетании характеристики дают ещё более полную картину: например, автор текста теперь знает, что большая часть его аудиотеки (ну ладно-ладно, только часть) — энергичная, и громкая, но достаточно мрачная, но при этом под не вполне можно танцевать (что достаточно неожиданно).

Так или иначе, теперь вы можете получить доступ к данным о своей аудиотеке, а что с ними делать и как их анализировать — решать вам.

Источники