上海滩 2020-04-08
数据科学是一门庞大的学科,它不断地扩展到新的行业,音乐产业就是其中之一。如果把这些应用程序当作一个“黑匣子”,可以观察到它的输入(数据)和输出(产品)。该项目旨在使用Python操作Spotify音乐数据,其范围有两个:
这个故事,最开始是为了调查音乐背后的统计数据,最后发现方程式背后也有“音乐”……
概念
假设一个情景。作为一名数据科学家,笔者工作的数据公司(Data Corp)记录了青年营销部门获得的可观利润,因此公司决定举办聚会犒劳年轻的客户。
在没有DJ的情况下,主管让笔者负责音乐,让派对持续到早上!她给了笔者一些Spotify播放列表,让笔者最终选定并创建一个。然而,忽视现代音乐趋势让笔者遇到一个棘手的问题。
使用Python访问所有播放列表,提取每个曲目的每个音频特征进行统计分析,并将最适合(派对)的曲目包装到最终的播放列表中,如何?当然,这里有一个技术性的问题:一个节拍也听不到!
为了更好地传达结果,假定:
#1:个人Spotify播放列表代表的是公司给笔者的。
#2:该聚会的目标群体是18至30岁的年轻人,这意味着……跳舞!然而,他们中的许多人可能会有家人陪伴,也就是说,一小部分客人会更好地享受到欢快的音乐。
#3:在音频特性中,Spotify只考虑了 danceability, energy, tempo, loudness & valence (见第2节)。那是因为,它们能更好地表达一首曲子是否适合一个舞会。
#4:改进所选播放列表的方法有两种:删除或添加分别被视为“坏”或“好”(涉及一个或多个音频特征)的曲目。笔者只采用后者,以扩大最终的播放列表。
笔者打算按下面的步骤来做:
一、设置
在本节中,设置所需的环境,以便应用分析技术。如果已经准备好下面列出的任何部分,可以跳过它们。
pip install spotipy pipinstall requests
install.py
# Import the libraries import os import pandas as pd import numpy as np import json importmatplotlib.pyplot as plt import seaborn as sns import spotipy importspotipy.util as util fromspotipy.oauth2 importSpotifyClientCredentials
# Declare the credentials cid ='XXXX' secret ='XXXX' redirect_uri='http://localhost:7777/callback' username ='XXXX' # Authorization flow scope ='user-top-read' token = util.prompt_for_user_token(username, scope, client_id=cid,client_secret=secret, redirect_uri=redirect_uri) if token: sp = spotipy.Spotify(auth=token) else: print("Can'tget token for", username)auth.py
请注意,与其通过笔记本直接声明凭证,还不如通过相应地设置环境变量,使它们暂时可用:
export SPOTIPY_CLIENT_ID="XXXX" exportSPOTIPY_CLIENT_SECRET="XXXX" exportSPOTIPY_REDIRECT_URI="http://localhost:7777/callback"
二、数据解释与获取
Spotify为其开发人员提供了许多表征音轨的音频特征(有关音频特性对象的更全面的解释,请参见此处)。下面列出要使用的特征,并简要说明:
数据提取部分分三步完成:访问用户播放列表;提取每个播放列表的曲目;提取每个曲目的音频特征。对于每个步骤,创建一个函数,实现相应的Spotipy方法。
(a)访问用户的播放列表
deffetch_playlists(sp,username): """ Returns theuser's playlists. """ id = [] name = [] num_tracks = [] #Make the API request playlists = sp.user_playlists(username) for playlist in playlists['items']: id.append(playlist['id']) name.append(playlist['name']) num_tracks.append(playlist['tracks']['total']) # Create the final df df_playlists = pd.DataFrame({"id":id, "name":name, "#tracks": num_tracks}) return df_playlists playlists =fetch_playlists(sp,username) playlists= playlists[:4].copy() playlists
fetch_plst.py
此函数返回一个数据帧,其中包含用户播放列表的id、name和曲目数#tracks。显然,有4个候选播放列表:
playlists 数据帧
(b) 获取播放列表的曲目
deffetch_playlist_tracks(sp, username, playlist_id): """ Returns thetracks for the given playlist. """ offset =0 tracks = [] #Make the API request whileTrue: content = sp.user_playlist_tracks(username, playlist_id, fields=None, limit=100, offset=offset,market=None) tracks += content['items'] if content['next'] isnotNone: offset +=100 else: break track_id = [] track_name = [] for track in tracks: track_id.append(track['track']['id']) track_name.append(track['track']['name']) #Create the final df df_playlists_tracks = pd.DataFrame({"track_id":track_id, "track_name": track_name}) return df_playlists_tracks
fetch_trcs.py
这个函数以playlist_id作为参数,返回一个数据帧,包括每个曲目的曲目 track_id 和 track_name 。不直接调用它,而是在下面的函数中使用它。
(c) 获取曲目的音频特征
deffetch_audio_features(sp, username, playlist_id): """ Returns theselected audio features of every track, for the givenplaylist. """ # Usethe fetch_playlist_tracks function to fetch all of the tracks playlist =fetch_playlist_tracks(sp, username, playlist_id) index =0 audio_features = [] #Make the API request while index < playlist.shape[0]: audio_features += sp.audio_features(playlist.iloc[index:index+50, 0]) index +=50 #Append the audio features in a list features_list = [] for features in audio_features: features_list.append([features['danceability'], features['energy'],features['tempo'], features['loudness'],features['valence']]) df_audio_features = pd.DataFrame(features_list,columns=['danceability', 'energy', 'tempo', 'loudness', 'valence']) # Set the 'tempo' & 'loudness' in the same range withthe rest features for feature in df_audio_features.columns: if feature =='tempo'or feature =='loudness': continue df_audio_features[feature] =df_audio_features[feature] *100 #Create the final df, using the 'track_id' as index for future reference df_playlist_audio_features = pd.concat([playlist,df_audio_features], axis=1) df_playlist_audio_features.set_index('track_id', inplace=True, drop=True) return df_playlist_audio_features
fetch_aud_ftrs.py
给定 playlist_id 作为参数,此函数返回每个曲目的 track_id, name 和音频特征(danceability, energy, tempo, loudness, valence)。因此,对于4个播放列表中的每一个,创建相应音频特征的数据帧:
df_dinner =fetch_audio_features(sp, username, '37SqXO5bm81JmGCiuhin0L') df_party=fetch_audio_features(sp, username, '2m75Xwwn4YqhwsxHH7Qc9W') df_lounge=fetch_audio_features(sp, username, '6Jbi3Y7ZNNgSrPaZF4DpUp') df_pop=fetch_audio_features(sp, username, '3u2nUYNuI08yUg877JE5FI')
aud_ftrs.py
df_dinner 数据框示例
仅仅借助API的力量就获得了所有必要数据的纯编码!
三、EDA
为了减少混乱,这里不包括数据可视化代码,但它可以在GitHub repo上使用。
首先,在一个图中描绘所有播放列表的音频特征,以便容易地感知哪个最适合聚会。
音频特征水平条形图
显然,把特征作为整体来看,Party和Pop播放列表取代了另外两个。仔细看看这两个,就能更清楚地了解哪一个占上风…
df_party 和df_pop水平横条图
除了danceability特征外,其他特征在派对播放列表中更高。这是一个必须选择和建立,以完善最终的播放列表。
四、播放列表的优化
其主要作用是尽可能增加派对播放列表的音频特征。但是,增加一个特征可能会减少另一个特性,因此必须考虑优先级。就笔者个人而言,根据假设2,danceability(年轻人)应该是主要特征,下一个特征是valence(家庭成员)。
这些变量是定量的,同时也属于比率尺度的测量。因此,箱线图可以有效地描述每个特征的个体分布。这样的图表和描述性统计表(通过pandas.DataFrame.describe方法)可以提供关于每个特定四分位数下的值的比例的良好视觉展示。
下面将详细说明原始的df_party数据框,突出显示danceability和valence平均值、第二(中位数)和第三个四分位数:
df_party数据的描述性统计和箱线图
竖直的黄线是中位数,而▴符号代表平均数。一般的目标是“推动”每一个特征的分布尽可能地向右,也就是说,沿着播放列表曲目方向增加,以便获得一个更好的“聚会”体验!根据假设4,从df_pop(第二个决赛播放列表)中添加曲目,每次都寻找机会:
方法一
一个好的出发点是,抽取一个df_pop的样本,并将其添加到主样本(df_party)中,随机的除外。通过使用pandas.DataFrame.sample()函数和weights参数,可以预先配置danceability值越大,就越有可能对相应的行进行采样。这种方法产生的数据框是df_party_exp_I(exp代表expanded)。
# Take a sample from the Pop playlist df_pop_sample_I= df_pop.sample(n=40, weights='danceability',random_state=1) df_pop_sample_I.describe() # Concatenate the original playlist with the sample df_party_exp_I= pd.concat([df_party, df_pop_sample_I]) df_party_exp_I.describe()
sample_I.py
df_party_exp_I描述性统计和方框图
方法二
这一次将利用NumPy布尔索引并过滤Pop播放列表,以便只返回满足指定条件的行。特别是,将danceability和valence特征设置为高于派对播放列表的相应平均值,分别为69.55和51.89。
# Take a sample from the Pop playlist df_pop_sample_II= df_pop[(df_pop['danceability'] >69.55) & (df_pop['valence'] >51.89)].copy() # Concatenate the original playlist with the sample df_party_exp_II= pd.concat([df_party, df_pop_sample_II]) df_party_exp_II.describe()
sample_II.py
df_party_exp_II描述性统计和方框图
方法三
一个特征的优化并不一定意味着其他特征的优化。为了改善这一缺点,将引入一个方程,其变量是声学特征,参数是赋予它们的权重。既然非常重视danceability特征,那么相应的权重应该更高。最后分数计算如下:
Score =(danceability *30)+(energy *20)+(tempo *20)+(loudness *10)+(valence *20)
为播放列表的每个单曲计算这个分数(创建一个新的列score),然后计算各自的描述性统计。这样,可以更好地评估,丰富df_party,同时在每个特征上实现更统一(根据权重)的优化。
简言之,df_party, df_party_exp_I & df_party_exp_II的平均score分别为7355分、7215分和7416分。很明显,虽然方法一相比原来的播放列表找到了更好的danceability,但它破坏了派对的整体体验(平均score从7355下降到7215)。就方法二而言,平均score提高了近62分。然而,也可以不用这两种方法…
这一次,通过使用新引入的score列,将过滤df_pop数据帧,并获取注意到score高于df_party平均值的行。因此,增加后者!
# Take a sample from the Pop playlist df_pop_sample_III= df_pop[df_pop['score'] > df_party['score'].mean()].copy() # Concatenate the original playlist with the sample df_party_exp_III= pd.concat([df_party, df_pop_sample_III]) df_party_exp_III.describe()
sample_III.py
df_party_exp_III描述性统计和方框图
事实上,这次:
作为一个完整的检查,应该一次性描述和比较所有的方块图—这可能看起来有点拥挤。幸运的是,变量的性质(见上文)允许使用KDE(内核密度图)。
KDE图
现在非常清楚了,方法三(绿色分布)是最好的,因为它实现了更高的右移。
最后,最终得到的数据帧(df_party_exp_III)包含了最终的音轨。唯一悬而未决的操作是将其转换为真正的播放列表。下面,第一个函数创建最终的播放列表,将其名称作为参数以及描述。另一个,从数据帧迁移轨迹。
请注意,授权流将再次运行,这次将使用不同的作用域(playlist modify public)。只需查看指南就好(https://github.com/makispl/Spotify-Data-Analysis/blob/master/README.md)。
defcreate_playlist(sp,username, playlist_name, playlist_description): playlists = sp.user_playlist_create(username, playlist_name, description =playlist_description) create_plst.py defenrich_playlist(sp,username, playlist_id, playlist_tracks): index =0 results = [] while index <len(playlist_tracks): results += sp.user_playlist_add_tracks(username, playlist_id, tracks =playlist_tracks[index:index +100]) index +=100
enrich_plst.py
# Make a temporary list of tracks list_track =df_party_exp_III.index # Create the playlist enrich_playlist(sp,username, '779Uv1K6LcYiiWxblSDjx7', list_track)
create_plst.py
播放列表数据帧
Bingo!
结论
到目前为止,已经处理了数百首曲目,检查了它们的音频特征,最后选择了最适合聚会的曲目,只用到了Python。通过这种方式,成功地完成了任务:
无论是从DJ还是从数据科学家的角度“深入”音乐世界,这无疑都是美妙的……
但是,有效地,当涉及到大量音乐数据集的精确性、敏捷性和彻底处理时,后者可以通过几行代码来指示计算机体面地执行。也就是说,有一件事是必然的:
文中可获得免费电子书地址!介绍目前,数据科学家正在受到很多关注,因此,有关数据科学的书籍正在激增。我看过很多关于数据科学的书籍,在我看来他们中的大多数更关注工具和技术,而不是数据科学中细微问题的解决。