用Python解析Spotify听歌数据:Pandas清洗与可视化实战 上个月我把Spotify的完整听歌数据导出来用Python跑了三个晚上。看完结果之后我才发现自己一直以为“这个月没怎么听歌”但数据告诉我光某个周末我就把同一首歌循环了四十多遍。类似这种反直觉的结论只有从原始数据里才能挖出来。Spotify每年会出一份年度报告但那个报告是平台帮你筛选过的“好看结论”。真正原始的数据——每一首歌、每一秒播放、每一天的听歌时段——都在账户设置里那个不起眼的“下载数据”功能里。这篇文章我就把整个过程完整拆开从申请数据导出到解析两种不同格式的JSON再到用Pandas清洗整理最后用Matplotlib画出可视化。全程使用Python适合三种人想给听歌习惯做个真实记录的音乐爱好者、正在学Python数据分析想找个真实数据集练手的新手以及手里有一堆JSON数据却不知道怎么处理的人。1. 先说清楚这份数据到底有什么用1.1 比年度报告更真实、更完整的个人数据Spotify的年度报告每年帮你总结最爱的歌手、最爱的歌、听歌总时长看起来很华丽但它只展示平台想让你看到的那几页。比如它会告诉你“你今年听了800小时音乐”却不会告诉你这800小时里有多少是晚上十点到凌晨两点之间听的它告诉你“你的年度歌手是某某”却不会告诉你其实有一首纯音乐被你在深夜反复播放了几百次。而个人数据导出文件里这些细节全都有。精确到每一秒的播放记录会记录你听了哪首歌、哪张专辑、哪个歌手、什么平台、什么时间以及实际播放了多少毫秒。这些数据放倒进Pandas之后你可以问出很多年度报告根本不会回答的问题我一周七天里哪天听歌最多我最近半年哪个月突然开始狂刷某类歌同一首歌我在不同播放器上循环的次数差别大吗我是不是经常把歌听一半就切掉这些问题用几十行Python就能回答。而整个过程本身就是一次完整的数据分析练手项目比网上那些故意做出来的“随机数据集”真实得多也更有动力做完。1.2 一个可行的分析结果清单如果你还不确定拿到数据之后该做哪些分析我建议按下面这张表来设计自己的目标想知道的问题数据中的关键字段分析手段输出结果最爱听的几位歌手artistName / master_metadata_album_artist_namegroupby后按播放时长求和Top歌手柱状图循环最多的歌曲trackName msPlayed歌曲名计数或时长排序歌曲排行一天中什么时段听歌最多endTime / ts提取小时字段24小时分布柱状图一周哪天听得最久endTime / ts提取星期字段周内分布图听歌总量随月份的变化endTime / ts按月聚合月度趋势折线图播客和音乐分别占了多少episode_name / master_metadata_track_name空值判断类型占比这张表其实就是你之后写代码的“需求清单”。先把问题列清楚代码只是实现这些问题的工具不急着一步到位写很复杂的脚本。2. 第一步把Spotify里的原始数据导出来2.1 导出流程账户设置里的一键申请数据获取远比想象中简单不需要爬虫不需要调API官方就提供完整导出入口。流程如下用浏览器登录Spotify官网进入账户页面。找到“Privacy settings / 隐私设置”栏目。找到“Download your data”或“下载数据”的区域。勾选你要导出的内容最核心的是“Extended streaming history / 扩展播放记录”因为这一项才会提供完整的逐条播放记录。如果只需要账户基础信息勾选“Account data”就行。点击申请Spotify会进入准备流程准备好之后会发送下载链接到你的邮箱。这个等待时间浮动很大。我自己的数据量不大等了两天才收到邮件身边有朋友等过五天。邮件里是一个zip下载链接点击下载即可。如果邮件没找到可以去账户设置里的同一个页面查看处理状态有些地区会在页面上直接给出下载按钮。顺便回应一个很多人问的问题Spotify官方客户端确实提供Windows、macOS、Linux、iOS、Android各个版本平时用哪个平台听歌不影响数据导出网页端同样有导出入口。另外如果账号所在地区无法正常使用Spotify服务导致你没有办法登录并导出数据那也不用卡在这里——继续往下看我会在后面的内容里给出替代数据源的方案代码部分完全可以跑通。2.2 拿到手之后文件目录和数据结构解压下载好的zip文件里面通常是一个MyData目录每个文件用字母开头方便按类型区分。和听歌数据直接相关的是以下几类StreamingHistory.json旧版播放记录一个文件数组结构每条记录包含endTime、artistName、trackName、msPlayed四个字段。endsong_0.json、endsong_1.json……新版播放记录可能有多个文件每条记录包含的字段非常丰富除了歌手和歌名还包括platform播放平台、conn_country连接国家、spotify_track_uri、ms_played等。Userdata.json账号用户名、国家、创建时间等基础信息。Payments.json支付和订阅记录分析听歌行为一般用不到。如果你导出的时间比较早可能只有StreamingHistory.json如果近几年导出主要数据都在endsong_*.json里。新版文件是滚动生成的会有很多个读文件时要用glob通配符一次性读入。做任何清洗之前我强烈建议先用文本编辑器打开一个json文件肉眼确认字段名。不同时期导出的字段命名不同而且Spotify升级过好几次数据格式每次升级虽然大体结构相似但字段名和嵌套层级可能会有微调。你第一步如果写成“写死字段名”的代码下次再拿到新版本数据就会报KeyError。3. 环境准备三分钟搭好Python数据分析环境3.1 所需依赖库与安装命令这次项目用到的东西非常少核心就三个库Pandas做数据处理Matplotlib做图表Seaborn让图表更好看。如果你还想生成词云那再装一个WordCloud。打开终端或者命令提示符直接执行pip install pandas matplotlib seaborn wordcloud如果你用的是AnacondaPandas和Matplotlib一般自带了上面命令会帮你补齐Seaborn和WordCloud。执行完之后在Python里验证一下import pandas as pd import matplotlib.pyplot as plt import seaborn as sns print(pd.__version__)能正常输出版本号环境就算搭好了。3.2 常见环境问题缺少包和编码错误我在实际教学过程中见过最多的两个环境问题一个是pip不是内部命令一个是导入包的时候报ModuleNotFoundError。pip不是内部命令多半是Windows下Python没有加入PATH环境变量。最省心的解决方法不是手动去改系统环境变量而是重新运行Python安装包勾选“Add Python to PATH”选项再装一遍。装完之后重新打开终端再试。ModuleNotFoundError报错的原因很简单当前Python环境里没有这个包。你可能会在有些集成环境里看到一句提示“请安装缺失的包以使用此工作流”这句话本身不是游戏规则只是告诉你缺了依赖。这时候就老老实实回到命令行执行pip install缺哪个装哪个。如果提示pip版本太低装不上可以先执行pip install --upgrade pip更新pip。还有一个非常容易被忽略的问题读取JSON文件时的编码。Spotify导出的JSON默认是UTF-8带BOM的格式如果你直接open()不指定编码在Windows系统上可能会遇到UnicodeDecodeError。读文件时统一加上encodingutf-8最保险。with open(StreamingHistory.json, r, encodingutf-8) as f: data json.load(f)这个习惯建议从第一天就养成不只是这次项目以后处理任何外部数据文件都这样写能省掉很多莫名其妙的中文乱码和编码报错。4. 数据读取与清洗把两种格式的JSON统一成一张表4.1 旧版StreamingHistory.json解析先处理最简单的情况只有旧版数据。文件内容长这样[ { endTime: 2021-01-01 12:00, artistName: The Beatles, trackName: Hey Jude, msPlayed: 204000 }, ... ]读取并转换成DataFrame的代码非常短import json import pandas as pd with open(StreamingHistory.json, r, encodingutf-8) as f: old_data json.load(f) df_old pd.DataFrame(old_data) df_old.rename(columns{ endTime: ts, artistName: artist_name, trackName: track_name, msPlayed: ms_played }, inplaceTrue)这里我是故意把字段名统一成新版格式的命名方式方便之后合表。rename之后旧表的字段就和endsong新版数据保持对齐了。4.2 新版endsong系列文件解析与字段统一新版数据通常有多个文件每个文件是一个数组。用glob把所有匹配的文件都读进来然后concat成一个大DataFramefrom glob import glob frames [] for path in glob(endsong_*.json): with open(path, r, encodingutf-8) as f: data json.load(f) frames.append(pd.DataFrame(data)) df_new pd.concat(frames, ignore_indexTrue)新版文件的字段多到吓人但不是每个字段都有用。我们只挑后面分析会用到的列顺便把字段名统一df_new df_new.rename(columns{ master_metadata_track_name: track_name, master_metadata_album_artist_name: artist_name, master_metadata_album_album_name: album_name, spotify_track_uri: uri, ms_played: ms_played, ts: ts })有两个点值得注意。第一新版数据里ts字段是UTC时间字符串末尾带一个Z例如2023-05-01T10:00:00Z。如果不做时区转换直接当作本地时间用后面的每日时段分布会整体偏移好几个小时这个坑我后面单独讲。第二新版数据里会混入播客收听记录。特征是master_metadata_track_name为空或者episode_name不为空。纯音乐分析场景下这些记录应该过滤掉否则你的Top歌曲里可能莫名其妙出现一集“睡前冥想”而且它可能还排得很靠前。过滤方法很简单df_new df_new[df_new[track_name].notna()].copy()4.3 合并两版数据并处理时间字段把旧版和新版合并成一张总表cols [ts, artist_name, track_name, ms_played, album_name, uri] df pd.concat([df_old[cols], df_new[cols]], ignore_indexTrue)这里做了个简化旧版没有album_name和uri合并后这两列会是NaN不影响后续分析。如果你想把两边数据真正对齐也可以在内层字段里做一次二次筛选只保留两版共有的四个核心字段。我个人的习惯是保留全部列宁可多留空值也不要一开始就把信息丢掉。接下来处理时间df[ts] pd.to_datetime(df[ts], utcTrue, errorscoerce) df df.dropna(subset[ts]) df[ts_local] df[ts].dt.tz_convert(Asia/Shanghai) df[hour] df[ts_local].dt.hour df[weekday] df[ts_local].dt.dayofweek df[date] df[ts_local].dt.date df[minutes_played] df[ms_played] / 60000.0这里我把时间统一转换成了Asia/Shanghai时区也就是北京时间。强烈建议用.dt.tz_convert()而不是直接把字符串里的UTC时间去掉当作本地时间。对于高频播放的听歌记录哪怕差一小时24小时分布图的口径都会错。weekday字段的数字对应关系是0代表周一1代表周二依此类推6代表周日。后面画周分布图的时候记得把数字映射成中文或英文星期名不然图表下面出现一排0-6阅读体验很差。这个阶段做完你会得到一张几百KB甚至几MB的“总听歌表”。我自己的数据量大约四万条处理完之后的DataFrame大概长这样ts_localartist_nametrack_namems_playedalbum_nameurihourweekdayminutes_played2023-05-01 18:00:0008:00Taylor SwiftAnti-Hero210000Midnightsspotify:track:...1803.5到这一步数据已经是一张可以直接做聚合分析的标准表格了。5. 分析与可视化看看自己到底是怎么听歌的5.1 谁是年度歌单霸主Top歌手与Top歌曲先来最直观的分析统计播放时长Top 10的歌手。注意这里要用“播放时长”而不是“播放次数”因为有的歌三分钟有的歌七分钟只看次数会被短歌带偏。top_artists df.groupby(artist_name)[minutes_played].sum().sort_values(ascendingFalse).head(10) print(top_artists)画图import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, DejaVu Sans] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) top_artists.sort_values().plot(kindbarh, axax, color#1DB954) ax.set_title(播放时长Top 10歌手分钟) ax.invert_yaxis() plt.tight_layout() plt.savefig(top_artists.png, dpi150) plt.show()#1DB954是Spotify品牌绿配色能让图表看起来更有“官方报告”的感觉。Top歌曲的思路类似但要考虑到同一首歌可能出现在多张专辑里比如Live版、Remaster版、电影原声版。如果只是df[track_name].value_counts()会把“同一首歌”的不同版本拆成两行。你可以先把track_name和artist_name拼在一起再统计df[song_id] df[artist_name] - df[track_name] top_tracks df.groupby(song_id)[minutes_played].sum().sort_values(ascendingFalse).head(10)画出来之后你可能会发现Top歌曲和你的印象差很多这就是数据带来的惊喜。5.2 一天24小时的热力分布大多数人以为自己是“下午听歌多”但真实数据经常打脸。个人经验是很多人的听歌高峰出现在深夜尤其是23点到凌晨1点之间。这个分析要用到之前提取的hour字段hourly df.groupby(hour)[minutes_played].sum() fig, ax plt.subplots(figsize(10, 5)) bars ax.bar(hourly.index, hourly.values, color#1DB954, alpha0.8) 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) plt.show()如果你是做数据分析的新手这组图能帮你理解一个很重要的概念时间型数据的聚合。原始数据是几万条零散的“几点几分听了什么歌”经过groupby(hour)之后就变成了24个数字信息密度完全不一样。顺带一提如果连groupby都还没用过这个例子是最容易上手的一个分组列一个聚合列没有多级索引没有复杂合并。跑通了再去看Pandas官方文档里的其他聚合方式都会轻松很多。5.3 月度趋势哪段时间听得最多把时间按月份重采样画出线形图能看到自己听歌总量的起伏变化。比如寒暑假是不是听歌特别多年末是不是突然变少了monthly df.set_index(ts_local)[minutes_played].resample(M).sum() fig, ax plt.subplots(figsize(12, 5)) ax.plot(monthly.index, monthly.values, markero, markersize4, linewidth1.5, color#1DB954) ax.set_xlabel(月份) ax.set_ylabel(播放分钟数) ax.set_title(月度听歌时长趋势) plt.xticks(rotation45) plt.tight_layout() plt.savefig(monthly_trend.png, dpi150) plt.show()这里用的resample(M)是Pandas里处理时间序列的一个非常常用的方法。它会自动把数据按月归拢不需要手动用字符串截取月份再groupby。注意如果你的数据跨了好几年resample(M)会把每个月都单独算出来画出来是一条长线。如果图表横轴上的月份标签堆成一团用plt.xticks(rotation45)旋转一下就能解决。这是很细节的小动作但能明显提升图表可读性。5.4 进阶补充流派和设备信息上面的分析只用了Spotify自己导出的数据但你还可以进一步调用Spotify官方API把歌曲的流派、能量值、情绪特征补进来做更深一层分析。前提是要在Spotify Developer后台注册一个应用拿到Client ID和Client Secret。然后使用spotipy库pip install spotipy用歌曲URI批量获取音频特征import spotipy from spotipy.oauth2 import SpotifyOAuth sp spotipy.Spotify(auth_managerSpotifyOAuth( client_id你的Client ID, client_secret你的Client Secret, redirect_urihttp://localhost:8888/callback, scopeuser-library-read )) uris df[uri].dropna().head(50).tolist() features sp.audio_features(uris)audio_features会返回每首歌的danceability、energy、valence等分数。比如valence是“情绪正向程度”接近1代表很欢快接近0代表很忧伤。把这些分数和你每天的听歌时间关联起来你就能知道自己在深夜是不是真的更爱听伤感的歌。设备信息分析也很有意思。新版数据里有platform字段记录每条播放来自iOS、Android、web player、Windows还是cast to device。统计一下你能知道自己的主要收听设备是什么。这个分析只需要一行value_counts()但得出的结论往往出人意料比如你以为自己主要用手机听歌实际数据显示网页播放器占了四成。6. 实战中踩过的坑问题排查与避坑技巧6.1 时区偏移导致的热力分布失真我第一次跑24小时分布图的时候看到图表里有明显的“凌晨3点小高峰”和“下午5点低谷”第一反应是自己是不是有听歌的怪癖。后面仔细检查数据才发现新版ts字段是UTC时间而我直接用pd.to_datetime()把字符串转成时间后没有转换成北京时间导致所有时间都提前了8小时。这个问题最大的隐蔽性在于月度趋势、Top歌手这些分析完全不受时区影响因为它们是按“天”甚至“月”聚合的。只有按小时聚合时才暴露出来。排查方法很简单随机抽几条原始记录打印出ts和ts_local肉眼对比一下是不是差8小时。6.2 旧版与新版数据拼接后重复统计如果你的账号经历了Spotify从旧版数据格式切换到新版数据格式的过渡期导出的文件里可能两边都有记录。直接把两个表合并后再做Top统计可能会把过渡期那些同时存在于两套记录里的播放量算两遍。缓解方法有两个。一是按“时间歌曲歌手时长”四列去重因为完全相同的播放记录大概率是重复数据df df.drop_duplicates(subset[ts, artist_name, track_name, ms_played])二是分别统计旧版和新版的最早最晚时间确认两套数据时间范围是否有重叠。如果旧版数据截止到2022年3月新版数据从2022年1月开始那重叠段就得做一次去重或者干脆直接只保留新版数据。6.3 听歌记录里那些“只播了5秒”的曲目原始数据里有一条记录叫ms_played单位是毫秒。很多记录的值小得离谱比如5000毫秒相当于你刚切到一首歌又切走了。这类记录要不要剔除取决于你的分析目标。如果分析“我真正听完了哪些歌”建议过滤掉ms_played 30000的记录也就是播放不到30秒的不算一次完整收听。30秒这个阈值是业界常用的一个标准能过滤掉试听、误触、切歌等情况。如果分析“我在哪个平台停留时间更长”那就不应该过滤因为用户快速切歌造成的播放时长损失也是真实的收听行为。我的做法是保留原始数据但新建一列标记是否完整播放后面分析时按需筛选df[is_complete] df[ms_played] 300006.4 常见问题速查表问题可能原因解决办法ModuleNotFoundError: No module named pandas当前Python环境缺少依赖执行pip install pandas matplotlib seaborn wordcloudUnicodeDecodeError或中文乱码文件编码不是默认编码打开文件时指定encodingutf-8时间整体偏移了8小时新版ts字段是UTC没转换时区使用df[ts].dt.tz_convert(Asia/Shanghai)合并后统计数量翻倍旧版和新版数据时间范围重叠按ts artist_name track_name ms_played去重图表中文显示为方块Matplotlib缺少中文字体配置设置plt.rcParams[font.sans-serif]月度趋势图横轴标签挤在一起日期过多用plt.xticks(rotation45)旋转标签导出申请提交后很久没收到邮件数据量大或高峰期处理延迟去账户设置页面查看处理状态耐心等待1-5天账号所在地区无法使用Spotify拿不到个人数据地区限制用GitHub上的公开Spotify收听记录示例数据代码逻辑完全一样最后再分享一个适合扩展的小技巧整套分析跑通之后你可以用PyInstaller把脚本打包成exe文件以后每个月导一次新数据双击exe就能自动生成所有图表不用再打开Python环境。打包命令很简单pip install pyinstaller然后pyinstaller -F analyze_spotify.py。这样整个项目就从一次性的练手脚本变成了一个能长期维护的个人听歌数据看板。我自己用这个法子跑了两年多看着数据曲线一点点变化确实比任何音乐APP的年度报告都更懂自己。