讓數(shù)據(jù)科學(xué)家代替DJ?Python幫你實(shí)現(xiàn)
數(shù)據(jù)科學(xué)是一門龐大的學(xué)科,它不斷地?cái)U(kuò)展到新的行業(yè),音樂(lè)產(chǎn)業(yè)就是其中之一。如果把這些應(yīng)用程序當(dāng)作一個(gè)“黑匣子”,可以觀察到它的輸入(數(shù)據(jù))和輸出(產(chǎn)品)。該項(xiàng)目旨在使用Python操作Spotify音樂(lè)數(shù)據(jù),其范圍有兩個(gè):
- 證明API(應(yīng)用程序編程接口)的存在對(duì)于向算法提供超精細(xì)數(shù)據(jù)具有重要意義。
- 演示簡(jiǎn)單的統(tǒng)計(jì)數(shù)據(jù)(適當(dāng)應(yīng)用時(shí))如何對(duì)日常行為進(jìn)行編碼,將其分解為基本要素,并在其基礎(chǔ)上構(gòu)建有價(jià)值的產(chǎn)品。
這個(gè)故事,最開(kāi)始是為了調(diào)查音樂(lè)背后的統(tǒng)計(jì)數(shù)據(jù),最后發(fā)現(xiàn)方程式背后也有“音樂(lè)”……
概念
假設(shè)一個(gè)情景。作為一名數(shù)據(jù)科學(xué)家,筆者工作的數(shù)據(jù)公司(Data Corp)記錄了青年?duì)I銷部門獲得的可觀利潤(rùn),因此公司決定舉辦聚會(huì)犒勞年輕的客戶。
在沒(méi)有DJ的情況下,主管讓筆者負(fù)責(zé)音樂(lè),讓派對(duì)持續(xù)到早上!她給了筆者一些Spotify播放列表,讓筆者最終選定并創(chuàng)建一個(gè)。然而,忽視現(xiàn)代音樂(lè)趨勢(shì)讓筆者遇到一個(gè)棘手的問(wèn)題。
使用Python訪問(wèn)所有播放列表,提取每個(gè)曲目的每個(gè)音頻特征進(jìn)行統(tǒng)計(jì)分析,并將最適合(派對(duì))的曲目包裝到最終的播放列表中,如何?當(dāng)然,這里有一個(gè)技術(shù)性的問(wèn)題:一個(gè)節(jié)拍也聽(tīng)不到!
為了更好地傳達(dá)結(jié)果,假定:
#1:個(gè)人Spotify播放列表代表的是公司給筆者的。
#2:該聚會(huì)的目標(biāo)群體是18至30歲的年輕人,這意味著……跳舞!然而,他們中的許多人可能會(huì)有家人陪伴,也就是說(shuō),一小部分客人會(huì)更好地享受到歡快的音樂(lè)。
#3:在音頻特性中,Spotify只考慮了 danceability, energy, tempo, loudness & valence (見(jiàn)第2節(jié))。那是因?yàn)椋鼈兡芨玫乇磉_(dá)一首曲子是否適合一個(gè)舞會(huì)。
#4:改進(jìn)所選播放列表的方法有兩種:刪除或添加分別被視為“壞”或“好”(涉及一個(gè)或多個(gè)音頻特征)的曲目。筆者只采用后者,以擴(kuò)大最終的播放列表。
筆者打算按下面的步驟來(lái)做:
- 設(shè)置運(yùn)行代碼的環(huán)境。
- 請(qǐng)求Spotify API獲取所有相關(guān)音樂(lè)數(shù)據(jù)并提供簡(jiǎn)要說(shuō)明。
- 使用Numpy、Pandas和一些附加的Python庫(kù)執(zhí)行EDA(探索性數(shù)據(jù)分析),以數(shù)字和可視化的方式探索候選播放列表。
- 選擇-使用兩種統(tǒng)計(jì)技術(shù)優(yōu)化最合適的播放列表。
一、設(shè)置
在本節(jié)中,設(shè)置所需的環(huán)境,以便應(yīng)用分析技術(shù)。如果已經(jīng)準(zhǔn)備好下面列出的任何部分,可以跳過(guò)它們。
- 申請(qǐng)Spotify開(kāi)發(fā)者帳戶并創(chuàng)建應(yīng)用程序。[在此過(guò)程中,將創(chuàng)建一個(gè)客戶ID,并為應(yīng)用程序提供一個(gè)客戶機(jī)密]。
- 安裝Jupyter Notebook-一個(gè)開(kāi)源的web應(yīng)用程序,用于創(chuàng)建/共享包含實(shí)時(shí)代碼、方程式、可視化和敘述性文本的文檔。
- 安裝Spotipy-用于訪問(wèn)Spotify Web API和請(qǐng)求的輕量級(jí)Python庫(kù)-用于尋址API的Python模塊。可以使用CLI(命令行界面)或Jupyter notebook來(lái)運(yùn)行以下命令:
- pip install spotipy
- pipinstall requests
install.py
- 導(dǎo)入必要的庫(kù):
- # 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
- 執(zhí)行授權(quán)代碼流:
- # 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
請(qǐng)注意,與其通過(guò)筆記本直接聲明憑證,還不如通過(guò)相應(yīng)地設(shè)置環(huán)境變量,使它們暫時(shí)可用:
- export SPOTIPY_CLIENT_ID="XXXX"
- exportSPOTIPY_CLIENT_SECRET="XXXX"
- exportSPOTIPY_REDIRECT_URI="http://localhost:7777/callback"
二、數(shù)據(jù)解釋與獲取
Spotify為其開(kāi)發(fā)人員提供了許多表征音軌的音頻特征(有關(guān)音頻特性對(duì)象的更全面的解釋,請(qǐng)參見(jiàn)此處)。下面列出要使用的特征,并簡(jiǎn)要說(shuō)明:
- Loudness:[-60.0-0db]描述了音軌的整體響度,或者說(shuō),聲音的質(zhì)量,這是與體力(振幅)相關(guān)的主要心理因素。
- Energy:[0.0-1.0]描述了對(duì)活動(dòng)和強(qiáng)度的感性測(cè)量。充滿活力的音軌讓人感覺(jué)快速、響亮和嘈雜。
- Valence:[0.0-1.0]描述音軌聽(tīng)起來(lái)“積極”的程度。高價(jià)意味著更積極的聲音(如歡快、歡快等)。
- Tempo:以BPM為單位描述音軌的總體估計(jì)節(jié)奏(每分鐘節(jié)拍),直接從節(jié)拍平均的持續(xù)時(shí)間得出。
- Danceability:[0.0-1.0]根據(jù)音樂(lè)元素(節(jié)奏、穩(wěn)定性、節(jié)拍強(qiáng)度等)的組合,描述一個(gè)軌道是否適合跳舞。低值意味著較少的舞步。
數(shù)據(jù)提取部分分三步完成:訪問(wèn)用戶播放列表;提取每個(gè)播放列表的曲目;提取每個(gè)曲目的音頻特征。對(duì)于每個(gè)步驟,創(chuàng)建一個(gè)函數(shù),實(shí)現(xiàn)相應(yīng)的Spotipy方法。
(a)訪問(wèn)用戶的播放列表
- 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
此函數(shù)返回一個(gè)數(shù)據(jù)幀,其中包含用戶播放列表的id、name和曲目數(shù)#tracks。顯然,有4個(gè)候選播放列表:

playlists 數(shù)據(jù)幀
(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
這個(gè)函數(shù)以playlist_id作為參數(shù),返回一個(gè)數(shù)據(jù)幀,包括每個(gè)曲目的曲目 track_id 和 track_name 。不直接調(diào)用它,而是在下面的函數(shù)中使用它。
(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 作為參數(shù),此函數(shù)返回每個(gè)曲目的 track_id, name 和音頻特征(danceability, energy, tempo, loudness, valence)。因此,對(duì)于4個(gè)播放列表中的每一個(gè),創(chuàng)建相應(yīng)音頻特征的數(shù)據(jù)幀:
- 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 數(shù)據(jù)框示例
僅僅借助API的力量就獲得了所有必要數(shù)據(jù)的純編碼!
三、EDA
為了減少混亂,這里不包括數(shù)據(jù)可視化代碼,但它可以在GitHub repo上使用。
首先,在一個(gè)圖中描繪所有播放列表的音頻特征,以便容易地感知哪個(gè)最適合聚會(huì)。

音頻特征水平條形圖
顯然,把特征作為整體來(lái)看,Party和Pop播放列表取代了另外兩個(gè)。仔細(xì)看看這兩個(gè),就能更清楚地了解哪一個(gè)占上風(fēng)…

df_party 和df_pop水平橫條圖
除了danceability特征外,其他特征在派對(duì)播放列表中更高。這是一個(gè)必須選擇和建立,以完善最終的播放列表。
四、播放列表的優(yōu)化
其主要作用是盡可能增加派對(duì)播放列表的音頻特征。但是,增加一個(gè)特征可能會(huì)減少另一個(gè)特性,因此必須考慮優(yōu)先級(jí)。就筆者個(gè)人而言,根據(jù)假設(shè)2,danceability(年輕人)應(yīng)該是主要特征,下一個(gè)特征是valence(家庭成員)。
這些變量是定量的,同時(shí)也屬于比率尺度的測(cè)量。因此,箱線圖可以有效地描述每個(gè)特征的個(gè)體分布。這樣的圖表和描述性統(tǒng)計(jì)表(通過(guò)pandas.DataFrame.describe方法)可以提供關(guān)于每個(gè)特定四分位數(shù)下的值的比例的良好視覺(jué)展示。
下面將詳細(xì)說(shuō)明原始的df_party數(shù)據(jù)框,突出顯示danceability和valence平均值、第二(中位數(shù))和第三個(gè)四分位數(shù):


df_party數(shù)據(jù)的描述性統(tǒng)計(jì)和箱線圖
豎直的黃線是中位數(shù),而▴符號(hào)代表平均數(shù)。一般的目標(biāo)是“推動(dòng)”每一個(gè)特征的分布盡可能地向右,也就是說(shuō),沿著播放列表曲目方向增加,以便獲得一個(gè)更好的“聚會(huì)”體驗(yàn)!根據(jù)假設(shè)4,從df_pop(第二個(gè)決賽播放列表)中添加曲目,每次都尋找機(jī)會(huì):
- 將平均值向右移動(dòng)(增加平均音頻特征)
- 或?qū)⒅形粩?shù)移到平均值的右側(cè)(確保至少50%的歌曲高于平均值)
- 或兩者兼而有之
方法一
一個(gè)好的出發(fā)點(diǎn)是,抽取一個(gè)df_pop的樣本,并將其添加到主樣本(df_party)中,隨機(jī)的除外。通過(guò)使用pandas.DataFrame.sample()函數(shù)和weights參數(shù),可以預(yù)先配置danceability值越大,就越有可能對(duì)相應(yīng)的行進(jìn)行采樣。這種方法產(chǎn)生的數(shù)據(jù)框是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描述性統(tǒng)計(jì)和方框圖
- 主要音頻特征danceability增加;平均值上升近0.5,其分布也略有優(yōu)化。中位數(shù)從68.20移到69.30,第三(上)四分位數(shù)分別從77.20移到78.90。
- 但是, valence特征下降了0.61,四分位都沒(méi)有向右移動(dòng)。鑒于此,應(yīng)該尋找進(jìn)一步的優(yōu)化機(jī)會(huì)。
方法二
這一次將利用NumPy布爾索引并過(guò)濾Pop播放列表,以便只返回滿足指定條件的行。特別是,將danceability和valence特征設(shè)置為高于派對(duì)播放列表的相應(yīng)平均值,分別為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描述性統(tǒng)計(jì)和方框圖
- danceability增加更多。這次平均值增加了將近2.17!隨著中位數(shù)和上四分位數(shù)向右移動(dòng),沿著該功能的曲目分布也得到了優(yōu)化,這基本上意味著至少50%的播放列表高于主要聲學(xué)功能的“新”較高平均值(71.71)。
- 盡管如此,valence特征下降了4.21,第二和第三個(gè)四分位數(shù)均高于平均值。
方法三
一個(gè)特征的優(yōu)化并不一定意味著其他特征的優(yōu)化。為了改善這一缺點(diǎn),將引入一個(gè)方程,其變量是聲學(xué)特征,參數(shù)是賦予它們的權(quán)重。既然非常重視danceability特征,那么相應(yīng)的權(quán)重應(yīng)該更高。最后分?jǐn)?shù)計(jì)算如下:
Score =(danceability *30)+(energy *20)+(tempo *20)+(loudness *10)+(valence *20)
為播放列表的每個(gè)單曲計(jì)算這個(gè)分?jǐn)?shù)(創(chuàng)建一個(gè)新的列score),然后計(jì)算各自的描述性統(tǒng)計(jì)。這樣,可以更好地評(píng)估,豐富df_party,同時(shí)在每個(gè)特征上實(shí)現(xiàn)更統(tǒng)一(根據(jù)權(quán)重)的優(yōu)化。
簡(jiǎn)言之,df_party, df_party_exp_I & df_party_exp_II的平均score分別為7355分、7215分和7416分。很明顯,雖然方法一相比原來(lái)的播放列表找到了更好的danceability,但它破壞了派對(duì)的整體體驗(yàn)(平均score從7355下降到7215)。就方法二而言,平均score提高了近62分。然而,也可以不用這兩種方法…
這一次,通過(guò)使用新引入的score列,將過(guò)濾df_pop數(shù)據(jù)幀,并獲取注意到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描述性統(tǒng)計(jì)和方框圖
事實(shí)上,這次:
- danceability特征提高了近1.17,valence增加了4.06(平均值的右移)
- 兩種分布都得到改善(中位數(shù)移到平均值的右側(cè))
- score為122.3,是目前為止最好的!(上下文意味著更高的潛力,播放列表在加權(quán)音頻特征上更加統(tǒng)一)
作為一個(gè)完整的檢查,應(yīng)該一次性描述和比較所有的方塊圖—這可能看起來(lái)有點(diǎn)擁擠。幸運(yùn)的是,變量的性質(zhì)(見(jiàn)上文)允許使用KDE(內(nèi)核密度圖)。

KDE圖
現(xiàn)在非常清楚了,方法三(綠色分布)是最好的,因?yàn)樗鼘?shí)現(xiàn)了更高的右移。
最后,最終得到的數(shù)據(jù)幀(df_party_exp_III)包含了最終的音軌。唯一懸而未決的操作是將其轉(zhuǎn)換為真正的播放列表。下面,第一個(gè)函數(shù)創(chuàng)建最終的播放列表,將其名稱作為參數(shù)以及描述。另一個(gè),從數(shù)據(jù)幀遷移軌跡。
請(qǐng)注意,授權(quán)流將再次運(yùn)行,這次將使用不同的作用域(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

播放列表數(shù)據(jù)幀

Bingo!
結(jié)論
到目前為止,已經(jīng)處理了數(shù)百首曲目,檢查了它們的音頻特征,最后選擇了最適合聚會(huì)的曲目,只用到了Python。通過(guò)這種方式,成功地完成了任務(wù):
- 演示了簡(jiǎn)單(描述性)的統(tǒng)計(jì)數(shù)據(jù)和編碼(如果適當(dāng)組合)是如何計(jì)算出此類耗時(shí)的活動(dòng)的。
- “嘗到”了擁有可請(qǐng)求的API的重要性,以便提取有意義的數(shù)據(jù)。
無(wú)論是從DJ還是從數(shù)據(jù)科學(xué)家的角度“深入”音樂(lè)世界,這無(wú)疑都是美妙的……
但是,有效地,當(dāng)涉及到大量音樂(lè)數(shù)據(jù)集的精確性、敏捷性和徹底處理時(shí),后者可以通過(guò)幾行代碼來(lái)指示計(jì)算機(jī)體面地執(zhí)行。也就是說(shuō),有一件事是必然的:
數(shù)據(jù)科學(xué)已經(jīng)找到了另一個(gè)發(fā)展壯大的“市場(chǎng)”,這意味著統(tǒng)計(jì)背后確實(shí)有“音樂(lè)”,達(dá)到了節(jié)拍背后有數(shù)學(xué)的水平…
Jupyternotebook已準(zhǔn)備好立即運(yùn)行,讓Pandas搖滾吧!






















