據(jù)清洗到可視化的完整實戰(zhàn))
最近整理個人數(shù)據(jù)的時候順手把Spotify的聽歌記錄導(dǎo)出了一份然后用Python做了個相對完整的分析。說實話這個項目屬于那種“工作量不大但收獲感很強”的類型適合剛學(xué)數(shù)據(jù)分析的人練手也適合老手快速摸清自己的音樂偏好。你不需要能訪問什么內(nèi)部接口只要會基礎(chǔ)的Python和pandas就能跑通。先說結(jié)論整個流程分四步——向Spotify申請導(dǎo)出個人數(shù)據(jù)、拿到JSON格式的播放歷史文件、用pandas清洗和統(tǒng)計、再用matplotlib做可視化。每一步都有坑但都不深認真看完這篇基本能一次跑通。下面我從項目設(shè)計講到編碼實現(xiàn)再講我實際跑數(shù)據(jù)時踩過的幾個問題最后補充一點用API擴展分析的玩法。1. 項目拆解先從“聽歌數(shù)據(jù)”里能挖出什么1.1 這個項目為什么值得做很多人以為聽歌數(shù)據(jù)就是“我聽了多少分鐘”“我最愛的歌手是誰”但實際上Spotify導(dǎo)出的原始數(shù)據(jù)包含的時間、時長、曲目和歌手信息能組合出非常多維度。比如說你能知道自己在一天中的哪個時段最沉迷音樂周末和工作日的聽歌習(xí)慣有什么差異單曲循環(huán)最多的歌是哪幾首甚至通過播放列表的歷史記錄反推自己口味的變化軌跡。這種分析的意義不在于“看排行榜”而在于把散落在云端的行為痕跡變成可量化的自我觀察。對Python學(xué)習(xí)者來說它又是一個真實、非玩具的數(shù)據(jù)集包含時間解析、字符串處理、分組聚合、排序篩選這些高頻操作比翻教科書上的示例數(shù)據(jù)有代入感得多。1.2 數(shù)據(jù)來源對比主動導(dǎo)出和API接口怎么選在做任何分析之前第一件事是搞清楚數(shù)據(jù)從哪來。Spotify的數(shù)據(jù)獲取有兩種主流方案一種是直接在賬戶設(shè)置里申請導(dǎo)出數(shù)據(jù)文件另一種是通過官方API按需拉取。兩者看著都叫“數(shù)據(jù)”但差別很大。主動導(dǎo)出走的是隱私數(shù)據(jù)下載流程你需要在Spotify賬戶頁面的隱私設(shè)置里找到“Download your data”提交申請后等郵件通知。下載的文件是一堆JSON和HTML的壓縮包其中最重要的就是StreamingHistory開頭的JSON文件里面記錄了每次播放的曲目、歌手和結(jié)束時間。這個方案更適合做歷史全量分析因為它能把幾年前的數(shù)據(jù)都給你。API方案則靈活能實時查詢當前播放的曲目、獲取歌單詳情、拉取音頻特征數(shù)據(jù)但它需要注冊開發(fā)者應(yīng)用拿到client_id和client_secret而且默認配額也只覆蓋用戶授權(quán)范圍內(nèi)的數(shù)據(jù)歷史播放記錄并不能完整回溯。我的建議是做歷史回顧用導(dǎo)出文件做實時擴展用API兩者不沖突。1.3 整體分析流程設(shè)計我把整個項目拆成了五個階段這樣寫代碼和排查問題都清晰階段一數(shù)據(jù)獲取申請導(dǎo)出并解壓確認文件結(jié)構(gòu)。階段二數(shù)據(jù)導(dǎo)入用Python讀取JSON拼接成DataFrame。階段三數(shù)據(jù)清洗處理時間字段、空值、異常時長。階段四統(tǒng)計分析按小時、星期、月份、歌手、曲目分組。階段五可視化生成排行榜和趨勢圖輸出結(jié)論。這五個階段不是串行的實際情況中經(jīng)常要回頭改。比如我清洗完發(fā)現(xiàn)時間列變成了帶時區(qū)的對象導(dǎo)致分組統(tǒng)計的結(jié)果差了8個小時又得折回去重新處理時間字段。所以你寫代碼的時候不必追求一次完美先把流程跑通再逐步優(yōu)化反而高效。2. 環(huán)境準備與數(shù)據(jù)導(dǎo)入把數(shù)據(jù)格式搞清楚2.1 Python環(huán)境與依賴庫這個項目需要Python 3.8以上版本核心依賴只有三個pandas負責(zé)數(shù)據(jù)處理matplotlib負責(zé)畫圖numpy在個別計算里偶爾會用到。如果你還沒安裝命令行里直接執(zhí)行pip install pandas matplotlib numpy如果你用的是Anaconda這三個庫默認就有連安裝都省了。我在Windows環(huán)境跑通后又在Linux服務(wù)器上跑了一次代碼不需要改動說明跨平臺沒問題。建議你直接在項目目錄下建一個虛擬環(huán)境避免依賴沖突。Python虛擬環(huán)境的管理工具我用的是venv夠用不需要上conda那些重工具。提示虛擬環(huán)境不是可選項。我見過很多人在全局環(huán)境里裝包結(jié)果某個庫的版本升級把其他項目弄崩了。用venv隔離后這個分析項目的依賴就鎖死了不會波及你別的項目。2.2 StreamingHistory結(jié)構(gòu)的詳細解讀Spotify導(dǎo)出的壓縮包解壓后常見的文件夾叫MyData里面會有多個文件。我只關(guān)注StreamingHistory它通常按照數(shù)據(jù)量拆成多個文件命名規(guī)律是StreamingHistory0.json、StreamingHistory1.json以此類推。每個文件內(nèi)部是一個JSON數(shù)組數(shù)組里每個對象代表一次播放記錄字段結(jié)構(gòu)固定如下[ { endTime: 2024-11-01 18:30, artistName: 鋼琴曲收藏家, trackName: River Flows In You, msPlayed: 172000 }, { endTime: 2024-11-01 18:33, artistName: Yiruma, trackName: River Flows In You, msPlayed: 92000 } ]四個字段的含義非常直白endTime是這首歌播放結(jié)束的本地時間精確到分鐘artistName是歌手名trackName是曲目名msPlayed是實際播放的毫秒數(shù)。注意它記錄的是“結(jié)束時間”而不是“開始時間”這會影響你后續(xù)對時段的分析口徑。我在第一次做小時分布圖時就踩了這個坑后面會專門說。還有一點值得注意msPlayed并不等于歌曲總長度而是Spotify實際計算到的播放時長。用戶手動切歌、斷網(wǎng)重連、跳過前奏都會產(chǎn)生各種不一致的播放時長。這也意味著你可以在分析時對時長做很多文章比如判斷哪些歌是播完的哪些是秒切的。2.3 數(shù)據(jù)加載與合并的完整代碼拿到文件之后讀取的邏輯很簡單但要注意路徑別寫死最好讓腳本自動掃描目錄下的所有StreamingHistory文件import pandas as pd import json import glob # 掃描當前目錄下所有StreamingHistory文件 files glob.glob(StreamingHistory*.json) print(f找到 {len(files)} 個播放歷史文件) # 逐個讀取并合并 all_dfs [] for f in files: with open(f, r, encodingutf-8) as fp: data json.load(fp) df pd.DataFrame(data) all_dfs.append(df) print(f{f}: {len(df)} 條記錄) df pd.concat(all_dfs, ignore_indexTrue) print(f合并后總記錄數(shù): {len(df)})這段代碼里有個小細節(jié)encodingutf-8必須顯式指定不然在Windows中文環(huán)境下容易觸發(fā)UnicodeDecodeError。還有拼接DataFrame時設(shè)了ignore_indexTrue這樣行號會連續(xù)遞增后面做過濾或去重會更方便。讀完之后建議先看一眼數(shù)據(jù)長什么樣確認列名沒有因為版本更新而變化print(df.head()) print(df.dtypes)如果一切正常你會看到endTime是object類型、artistName和trackName是object類型、msPlayed是int64類型。這個類型分布很關(guān)鍵object代表它是Python字符串int64代表它是整數(shù)后續(xù)所有處理都基于這個認知展開。3. 數(shù)據(jù)清洗與核心統(tǒng)計讓零散記錄變成可讀指標3.1 時間字段處理與時區(qū)注意項時間處理是整個項目里最容易出錯、也最影響后續(xù)分析的環(huán)節(jié)。Spotify導(dǎo)出的endTime字符串格式是“YYYY-MM-DD HH:MM”沒有秒也沒有時區(qū)信息。我的建議是先把字符串轉(zhuǎn)成pandas的datetime類型這樣后面按小時、星期、月份分組非常方便df[endTime] pd.to_datetime(df[endTime])這里有一個隱藏問題endTime到底記錄的是哪個時區(qū)從我的實際數(shù)據(jù)來看它用的是你賬號當時所在位置的本地時間。如果你長期在同一個時區(qū)那就沒什么影響如果你經(jīng)??鐕眯谢蜷_了代理節(jié)點那么小時分布圖里會出現(xiàn)明顯的“幽靈時段”。這種情況沒有完美的修正方案因為你無法從導(dǎo)出文件里反推出每個時刻的真實偏移量。我的處理辦法是假設(shè)絕大多數(shù)記錄都來自常住時區(qū)這個假設(shè)在統(tǒng)計框架下是可接受的。時間列轉(zhuǎn)成datetime之后我習(xí)慣同時生成幾個衍生列后面能少寫很多重復(fù)代碼df[date] df[endTime].dt.date # 日期 df[hour] df[endTime].dt.hour # 小時 df[weekday] df[endTime].dt.dayofweek # 星期幾0周一 df[month] df[endTime].dt.to_period(M) # 月份 df[duration_minutes] df[msPlayed] / 60000 # 播放時長單位分鐘這里最簡單也最實用的衍生列就是duration_minutes。因為msPlayed動輒十幾萬肉眼完全沒法讀比如172000毫秒你心算要反應(yīng)幾秒但轉(zhuǎn)成2.87分鐘就一目了然了。后面所有“播放時長”的分析我都用這個派生列。3.2 播放時長過濾先把噪音去掉真實數(shù)據(jù)里很大一部分記錄的msPlayed都非常小。比如幾秒鐘就切歌了或者不小心誤觸播放了三秒就暫停。這些數(shù)據(jù)如果不過濾會嚴重干擾你對歌曲真實熱度的判斷——它計數(shù)了但并沒有實際的收聽價值。我的過濾標準是只保留播放時長大于等于30秒的記錄。為什么選30秒而不是10秒因為多數(shù)平臺的“播放一次”標準是30秒你用這個閾值算出的播放次數(shù)和平臺官方統(tǒng)計口徑能對上。當然你也可以用60秒看你想分析什么。如果你關(guān)心的是“哪些歌我連30秒都撐不過”那這堆短時長記錄本身就是很好的分析素材。df_valid df[df[duration_minutes] 0.5].copy()過濾之后數(shù)據(jù)量通常會有10%到20%的縮減。這種“丟棄”不是浪費而是讓后續(xù)分析更聚焦。同時我建議把原始df保留在內(nèi)存里別急著覆蓋因為后面做對比分析時可能還會用到。3.3 核心指標計算總時長、歌手畫像、曲目熱度處理完數(shù)據(jù)第一個要算的指標是“總播放時長”。這個值直觀適合用來描述個人音樂消費的總體水平total_hours df_valid[duration_minutes].sum() / 60 print(f有效播放總時長: {total_hours:.1f} 小時)接著看歌手維度。我習(xí)慣同時算兩個視角按播放次數(shù)排名和按累計播放時長排名。這兩個排名經(jīng)常不一樣差異本身就是信息。比如某位歌手的歌你經(jīng)常單曲循環(huán)那它按次數(shù)排名會很高但如果某個歌手的歌都是五六分鐘的長歌每次你也都能聽完整首那按時長排名就會更靠前。# 按播放次數(shù)排名 artist_count df_valid.groupby(artistName)[trackName].count().sort_values(ascendingFalse) # 按累計播放時長排名 artist_duration df_valid.groupby(artistName)[duration_minutes].sum().sort_values(ascendingFalse) artist_stats pd.DataFrame({ 播放次數(shù): artist_count, 累計時長_分鐘: artist_duration }) print(artist_stats.head(10))曲目維度的分析類似但要考慮同名歌曲的問題。不同歌手可能有同名歌曲所以groupby時最好同時按artistName和trackName分組才夠精確track_stats df_valid.groupby([artistName, trackName]).agg( 播放次數(shù)(duration_minutes, count), 累計時長(duration_minutes, sum) ).sort_values(播放次數(shù), ascendingFalse) print(track_stats.head(10))這一步跑完你已經(jīng)能回答“我最常聽的歌手和歌曲是什么”這個最基礎(chǔ)的問題了。但我還想多提一個指標就是單曲循環(huán)率。它可以用“播放次數(shù)超過20次的曲目數(shù)量”占“去重后曲目總數(shù)”的比例來表示。這個比例越高說明你的聽歌偏好越固定越不容易接納新歌比例越低說明你的歌單越多元。這個指標不高深但很有個人洞察加成。4. 可視化與結(jié)果解讀用圖表講出你的聽歌故事4.1 圖表選型與中文字體配置統(tǒng)計數(shù)字能說明問題但一張好圖的信息密度往往比十個數(shù)字更高。我的可視化選型原則很簡單能簡潔就不復(fù)雜能用柱狀圖就不堆三層嵌套。這次項目里最常用的幾個圖如下Top 10 歌手柱狀圖橫向柱子好讀歌手名字不會被截斷。24小時播放量分布柱狀圖一眼看出夜間和高峰。每周各天播放量柱狀圖對比工作日和周末差異。月度播放趨勢折線圖展示時間跨度的變化。做圖之前必須先配置中文字體否則matplotlib默認字體里沒有中文坐標軸的標簽全部顯示成方框。我的配置如下import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus也很關(guān)鍵。不設(shè)成False的話圖表里的負號會顯示成亂碼方塊雖然聽歌數(shù)據(jù)很少涉及負數(shù)但圖例和坐標軸有時仍會觸發(fā)這個問題。這兩個配置放在腳本開頭全局生效。4.2 四張最有信息量的圖第一張圖是Top 10 歌手播放次數(shù)柱狀圖。我用的是橫向條形圖因為歌手名字一般都比較長橫向排列閱讀更自然top_artists artist_count.head(10)[::-1] # 反轉(zhuǎn)順序讓最大的在頂部 fig, ax plt.subplots(figsize(10, 6)) ax.barh(top_artists.index, top_artists.values) ax.set_xlabel(播放次數(shù)) ax.set_title(Top 10 歌手播放次數(shù)) plt.tight_layout() plt.savefig(top_artists.png, dpi150)第二張圖是24小時播放量分布。這部分數(shù)據(jù)能反映你的作息習(xí)慣比如你是夜貓子還是早鳥午休時間是不是也在聽歌hourly_play df_valid.groupby(hour)[duration_minutes].sum() fig, ax plt.subplots(figsize(10, 5)) ax.bar(hourly_play.index, hourly_play.values) ax.set_xticks(range(0, 24)) ax.set_xlabel(小時) ax.set_ylabel(累計播放時長分鐘) ax.set_title(24小時播放時長分布) plt.tight_layout() plt.savefig(hourly_distribution.png, dpi150)第三張圖是星期分布。周末和工作的對比通常非常明顯。有人周末聽歌多因為時間自由有人反而工作日通勤路上聽得多周末安靜下來反而不開音樂。第四張圖是月度播放趨勢折線圖。如果你的數(shù)據(jù)跨越兩三年這張圖能清晰地展示你對音樂的熱情是逐年上升還是逐漸冷卻。我看自己的數(shù)據(jù)時發(fā)現(xiàn)年中有一個明顯的低谷回頭看那是工作最忙的幾個月份音樂消費直接腰斬。4.3 結(jié)果解讀圖表背后能看出什么圖做出來后不要只發(fā)“哦真好看”要學(xué)會解讀。比如我在自己的數(shù)據(jù)里發(fā)現(xiàn)了一個很有意思的現(xiàn)象周末深夜的播放量占比明顯高于工作日。這說明我的聽歌行為不只是“通勤時段”還承擔著放松助眠的功能。另一個解讀維度是累計播放時長的周期性。如果月度趨勢圖里出現(xiàn)明顯的季節(jié)性波動先別急著下結(jié)論想想是不是因為寒暑假、年終加班季、或者某個月迷上了播客導(dǎo)致純音樂時間被擠占。這種觀察不一定準確但它能引導(dǎo)你回到原始數(shù)據(jù)里去驗證這本身就是數(shù)據(jù)分析的正循環(huán)。5. 常見問題與排查技巧實錄5.1 UnicodeDecodeError和中文亂碼這是Windows用戶最容易踩的坑。讀取JSON文件時如果不指定編碼或指定錯了編碼會出現(xiàn)UnicodeDecodeError: gbk codec cant decode byte類似報錯。解決方案就是所有open操作都顯式指定encodingutf-8讀JSON用utf-8寫CSV時加上encodingutf-8-sig。utf-8-sig比utf-8好在哪它會在文件開頭寫入BOM標記這樣你用Excel打開CSV時中文不會亂碼。如果只用utf-8Excel默認用ANSI解析中文就全變問號了。這個細節(jié)我吃過兩次虧第一次還以為是pandas的問題后來才明白是編碼標記的鍋。5.2 數(shù)據(jù)量大導(dǎo)致內(nèi)存和性能問題有些人的播放歷史跨度很長數(shù)據(jù)量可能達到幾十萬條記錄。這種情況下直接pd.concat拼接所有StreamingHistory文件通常還是沒問題但如果你的電腦配置比較老每次運行腳本都要等好幾秒體驗會下降。我常用的優(yōu)化手段有幾種一是在讀取時就刪除不關(guān)心的列比如如果你不分析專輯信息就不加載它二是只保留需要的字段盡早減小DataFrame的尺寸三是用df_valid df[df[duration_minutes] 0.5].copy()過濾后及時把中間變量釋放掉。還有一點是如果StreamingHistory拆成了幾十個文件你可以改成循環(huán)里邊讀邊拼接而不是先存一個大list再concat內(nèi)存峰值會低很多。如果你后續(xù)還要做更多計算可以把清洗后的結(jié)果存成pickle或parquet格式下次直接讀取比重新跑一遍JSON解析快得多df_valid.to_pickle(spotify_history.pkl) df_loaded pd.read_pickle(spotify_history.pkl)5.3 時間統(tǒng)計差8小時或13小時這是時區(qū)問題最常見的外在表現(xiàn)。我在第一次跑24小時分布時發(fā)現(xiàn)凌晨時段幾乎沒數(shù)據(jù)高峰出現(xiàn)在下午5點到晚上7點但我的真實聽歌高峰明明是睡前11點左右。排查半天才發(fā)現(xiàn)原始時間字段里存的是UTC我卻按本地時間處理了。后來統(tǒng)一把endTime轉(zhuǎn)成datetime后又用dt.tz_localize(UTC).dt.tz_convert(Asia/Shanghai)做了顯式轉(zhuǎn)換數(shù)據(jù)才正常。不過要再次強調(diào)Spotify導(dǎo)出文件的endTime通常是本地時間并不是UTC。所以遇到時間偏移問題時不要盲目套用tz_convert先單獨打印幾條原始記錄對照你自己的真實聽歌時刻確認偏移方向再動手。5.4 播放時長出現(xiàn)0或者少數(shù)異常大值有些記錄msPlayed是0說明歌曲可能改成了私密會話或者播放被打斷得非???。另一些記錄可能是幾個小時的播客或者某個直播類音頻msPlayed會比普通歌曲長很多達到幾萬秒。如果這些極端值混在歌曲分析里會導(dǎo)致你的“最常聽歌曲”排名被播客或環(huán)境音霸榜。我的處理方法是做一個使用場景判斷如果你只想分析音樂類曲目可以按播放時長設(shè)置一個上限比如超過30分鐘的記錄全部剔除或者單獨挑出來歸類為“長音頻”。按需過濾后歌曲排名才會回歸正常。5.5 去重與重復(fù)記錄原始數(shù)據(jù)里有一個容易被忽略的問題同一首歌曲在同一天內(nèi)播放多次時會生成多條記錄這本身是正確的但如果你不小心用相同的處理邏輯跑了兩次數(shù)據(jù)合并會導(dǎo)致記錄數(shù)翻倍統(tǒng)計結(jié)果直接失真。我建議在腳本里打印一個“總記錄數(shù)”的校驗值并且在不同階段重復(fù)打印比對。如果發(fā)現(xiàn)數(shù)字異常翻倍多半是腳本被重復(fù)執(zhí)行了或者concat時沒有排除掉已讀入的文件。還有一個去重場景是同一秒鐘或同一分鐘內(nèi)出現(xiàn)兩條完全一樣的記錄這很可能是Spotify因為網(wǎng)絡(luò)重試機制導(dǎo)致的重復(fù)上報。可以用drop_duplicates()按關(guān)鍵列去重df_clean df_valid.drop_duplicates(subset[endTime, artistName, trackName, msPlayed])去重數(shù)量通常很少但如果有最好還是在統(tǒng)計之前干掉免得“最熱歌曲”被重復(fù)計數(shù)虛高。6. 擴展玩法用API補充音頻特征分析6.1 spotipy接入與授權(quán)流程本地導(dǎo)出數(shù)據(jù)能告訴你“聽了什么、什么時候聽、聽了多久”但它回答不了“這些歌聽起來是什么風(fēng)格、能量多高、是否憂傷”。這就要借助Spotify官方API來補全音頻特征了。Python生態(tài)里最常用的庫是spotipy安裝一行命令搞定pip install spotipy使用之前需要先去Spotify開發(fā)者后臺創(chuàng)建一個應(yīng)用拿到Client ID和Client Secret。然后通過用戶授權(quán)流程獲取訪問令牌。這個流程不是直接把賬號密碼交給腳本而是通過OAuth協(xié)議讓用戶在瀏覽器里確認授權(quán)安全性其實更高。我第一次用的時候覺得繁瑣但看完流程就明白了它本質(zhì)上和你用微信登錄某個網(wǎng)站是一個邏輯。6.2 獲取音頻特征的代碼示例授權(quán)完成后spotipy會返回一個client對象你可以拿著歌手名或曲目名去搜索再通過track id獲取音頻特征。音頻特征里包括danceability、energy、valence、acousticness等數(shù)值全部在0到1之間。import spotipy from spotipy.oauth2 import SpotifyOAuth sp spotipy.Spotify(auth_managerSpotifyOAuth( client_id你的client_id, client_secret你的client_secret, redirect_urihttp://localhost:8080/callback, scopeuser-library-read )) # 示例搜索一首歌并獲取特征 results sp.search(qRiver Flows In You, typetrack, limit1) track results[tracks][items][0] features sp.audio_features(track[id])[0] print(fdanceability: {features[danceability]}) print(fenergy: {features[energy]}) print(fvalence: {features[valence]})拿到這些特征后可以和你前面統(tǒng)計出的“高頻曲目表”做關(guān)聯(lián)看看你反復(fù)聽的歌到底偏high還是偏low、偏歡快還是偏憂郁。我跑完發(fā)現(xiàn)我的高頻曲目里valence平均值明顯偏低這倒是符合我平時用音樂平靜心緒的習(xí)慣。6.3 擴展玩法的更多可能性音頻特征還可以組合出很多有意思的分析方向。比如把一天24小時拆成幾段分別計算你在每個時段播放歌曲的平均energy值很可能發(fā)現(xiàn)早上聽歌能量高、晚上能量低這種規(guī)律?;蛘甙衙恐苊刻斓膙alence均值畫成熱力圖配合星期數(shù)據(jù)看情緒波動。接口本身還有推薦功能能基于音樂特征生成相似曲目推薦。我知道有些朋友把這個項目做成了自動化周報每周自動分析聽歌習(xí)慣變化然后推送到郵箱或聊天軟件。這些都是后話但說明這個項目的擴展空間很大夠你玩很久。我在實際跑完整套流程后最有感觸的一點是分析自己聽歌數(shù)據(jù)最大的收獲不是一張張圖表而是突然理解了自己很多無意識的行為模式。比如我從來沒意識到自己在深夜時段播放的歌曲重復(fù)率那么高也沒想到周末中午會有一個明顯的播放空白期。這些洞察不靠數(shù)據(jù)分析很難浮出水面。如果你也想試著跑一遍建議先從導(dǎo)出數(shù)據(jù)、計算總時長和Top歌手開始跑通之后再慢慢往上加?xùn)|西這個項目沒有標準答案做得越多、越像你自己。