1. 为什么放着现成的"年度总结"不用,非要自己跑Python分析
每年年底,Spotify都会给你推送一份漂亮的"年度总结"——你听了几分钟、哪个歌手是你的本命、哪首歌被你翻来覆去听了多少遍。做得确实精美,动画特效、卡片式排版,发朋友圈特别有面子。但说实话,我拿到手之后总有一种"不够解渴"的感觉。它只给了我几个高光数据,我想知道的东西它一概没有:我到底在什么时间段最沉迷?凌晨三点半是哪首歌在陪着我?三个月前和三个月后的听歌口味到底发生了什么变化?这些问题,官方不会回答你。
所以我决定自己动手,把Spotify的完整听歌记录导出来,用Python做一次彻底的数据分析。这个项目我从最初只是想满足好奇心,到后来跑完一遍完整的数据清洗、分析和可视化,收获远比想象中大。它不只是一个"练手项目",更像是一次用数据重新认识自己的过程。后来我把这套方法分享给身边好几个朋友,他们照着做,也发现了很多自己都没意识到的听歌习惯。
这个项目适合谁?我认为有这三类人:第一,数据分析初学者,这个数据集是真实的、跟你有情感连接的,分析起来不会觉得枯燥,比拿超市销售数据练手有意思得多;第二,想系统学一遍pandas、Matplotlib、Plotly可视化的人,这里面的数据清洗、分组聚合、时间序列、交互图表几乎全都能练到;第三,纯粹想挖一挖自己听歌行为秘密的量化自我爱好者。无论你是哪一类,这篇文章都会给你一份可以直接照抄的完整方案,包括数据导出、字段拆解、清洗逻辑、分析代码和可视化实现,以及我在实际操作中踩过的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拿到完整听歌数据的三个步骤
2.1 先找到数据下载入口
在开始写任何Python代码之前,你得先把数据拿到手。这里有个很多人不知道的点:Spotify允许你导出自己的全部个人数据,而不仅仅是听歌记录。入口在账户页面,我用的网页版操作路径是:登录Spotify官网,进入账户设置,找到"隐私设置"部分,里面有一个"下载你的数据"的选项。点击之后,Spotify会给你发送确认邮件,就像一个"你确定要下载吗"的提示,确认之后就开始处理数据。
这里要提前说清楚:数据导出一个小时之内"永远不会完成",尤其是你听歌历史比较长的情况下。我自己的使用时间大约四五年,导出的数据大概在几十兆级别,等了将近3天才收到下载通知邮件。如果你刚注册没多久,可能几小时就完成;如果跟我一样是多年的老用户,耐心一点。Spotify处理数据是按文件分门别类打包的,等得久是正常的。
提示:下载链接的有效期通常在邮件里会写明,尽量在收到通知后尽快下载。我有个朋友拖了一个星期才去点,结果链接过期,又重新走了一遍申请流程。
2.2 下载包里到底装了什么
解压下载好的zip文件,你会看到一堆文件夹,别被吓到。里面大体包括:账户信息文件、身份信息、设备信息、付款记录、播放列表、搜索记录,以及最关键的StreamingHistory文件夹。播放列表和历史记录是最有价值的部分,其他文件夹可以先不关注。
StreamingHistory文件夹里一般有若干个JSON文件,命名类似StreamingHistory0.json、StreamingHistory1.json这样。如果你听歌年份跨度大,Spotify会按时间切分,一个文件对应一个时间段。这就意味着,你后面分析之前先把这些JSON文件合并到一起,才能得到完整的听歌流水。当时我打开文件夹看到七八个JSON文件,心里还咯噔了一下,后来想想其实是好事,分段文件通常比一个超大文件更容易处理。
2.3 JSON文件的读取方式
每个StreamingHistory文件里面其实是一个非常标准的JSON数组,数组里的每个元素就是一条听歌记录。用notepad或者VS Code打开看,结构非常清晰,四个字段:endTime、artistName、trackName、msPlayed,分别代表播放结束时间、歌手名、歌曲名、播放时长(毫秒)。没有多余字段,干净利落,这对我后续的分析非常友好。
读取用Python的json模块就够了,也可以直接用pandas的read_json,我推荐后一种,一步到位得到DataFrame。配合glob模块,可以一次性把StreamingHistory文件夹里的所有JSON文件读进来合并,省去手动判断文件数量的麻烦。整个读取过程也就十来行代码,属于新手也能轻松搞定的程度。
3. 把播放记录变成可分析的数据表:字段拆解与清洗
3.1 四个核心字段分别代表什么
拿到原始的JSON数据之后,千万不要急着做聚合分析。先把每条记录理解透,不然后面算出来的指标全都不对劲。
首先是endTime,播放结束时间。注意这个词用的是"end"而不是"start",Spotify记录的是这首歌播放结束的那一秒钟,精确到秒。其次是artistName和trackName,歌手名和歌曲名,这两个看起来没技术含量,但清洗时你会遇到不少坑。最后是msPlayed,毫秒数,表示这首歌从开始播放到停止或者切歌的时长。一首歌的实际总时长你可以通过曲库信息查,但这里记录的是你真实听了多久,哪怕只听了5秒也会有一条记录,这为后面埋下了"噪声"问题。
这四个字段拼在一起,本质上是把你在Spotify上的每一次"播放行为"记录成了一条流水。用生活化一点的话说,这就像你家里装了一个电表,每小时自动记一次你用了多少度电,只不过Spotify记的是你听歌听了多少毫秒。
3.2 时区问题:第一个必须正视的坑
endTime字段写的是ISO 8601格式的字符串,像2024-03-15T14:23:01Z这种,注意最后那个Z,它代表的是UTC时间,也就是协调世界时,不是你的本地时间。如果你在中国,北京时间是UTC+8,意味着endTime显示凌晨2点,实际本地时间是早上10点。这个时区偏移如果不处理,后面你在做"深夜听歌分析"的时候会得出完全错误的结果。
具体怎么处理?最简单的方式是,把字符串转成datetime类型之后,用pandas的dt.tz_localize和dt.tz_convert,先把不带时区的字符串标记为UTC,再转换成本地时区。代码并不复杂,但如果你不做这一步,后续所有按小时、按日期的分组统计都会偏移8小时。我之前第一版分析就是没注意这个问题,做出来的听歌时段分布图显示我每天凌晨3点听歌量最大,我一度以为自己是不是梦游放歌了,后来才发现是时区没转换。
3.3 数据清洗的具体操作
JSON数据虽然结构规整,但直接分析前还是要做几步标准清洗:
第一步,把多个StreamingHistory文件合并成一个DataFrame,索引重置一下,避免重复索引造成后续操作报错。第二步,把endTime统一转成datetime类型,并处理时区。第三步,检查缺失值——正常情况下这四个字段都会有值,但偶尔有些记录可能出现msPlayed异常,比如为0,这种大概率是播放直接失败或者刚点击就切歌,没有分析意义。第四步,构造新列,比如"播放日期"和"播放小时",方便后面按时间维度做聚合。
关于msPlayed这个字段,我建议你额外做一个"是否跳过"的判断列。一般来说,一首歌如果只播放了不到30秒,很难说是完整体验了这首歌,更可能是切歌、手动跳过或者随机播到不喜欢的歌。你可以把df['msPlayed'] >= 30000作为一个筛选条件,生成一个布尔列,后面做Top榜单时,可以直接基于"有效播放"来做,也可以同时统计"总播放次数"和"有效播放次数"两个指标。两个指标放在一起对比会很有意思,你会发现有些歌"被点次数"很高但"有效播放率"很低,说明那是你切歌前的最候一搏;而有些歌被点次数不高,但每次点开都会完整听完,那才是真正的宝藏曲目。
注意:msPlayed的单位是毫秒,不要脑子一热直接当成秒来用。我第一次写聚合代码时,把30000毫秒当成30000秒,算出来的人均播放时长直接起飞,好半天才反应过来。
4. 五个值得做的分析维度:从宏观指标到微观行为
数据清洗完成后,真正有趣的部分才开始。我强烈建议你在开始分析之前先把下面几个问题写下来,因为分析方向决定了你要用哪些聚合手段,而不是拿到DataFrame就到处groupby。我自己梳理了五个维度,几乎覆盖了所有值得关注的角度。
4.1 宏观指标:你的"听歌总量"到底有多大
先算总量。用len(df)得到总的播放记录条数,用df['msPlayed'].sum()得到总播放毫秒数,再除以3600000转成小时。这两个数字乍一看只是"晒数据"用的,但它们是真正确认你"听了多少"的基础,后续所有比例、人均计算都要用到它们。比如只有知道总小时数,才能算出日均收听时长;只有知道总记录数,才能算出跳过率——跳过记录数除以总记录数,这个比例能反映你的收听习惯有多"急躁"。
顺带做一个时间跨度统计:最早的endTime到最晚的endTime,覆盖了多少天。用这个天数除以总播放小时数,得到平均每天听几小时。我见过有的人日均听歌超过5小时,那是真的把音乐当背景音一整天都在放。
4.2 歌手与歌曲Top榜:官方榜单之外的另一个真相
这个维度最容易被想到,也是我做出来之后朋友圈反馈最好的部分。用pandas的groupby加sort_values就能实现。比如统计每个歌手的播放次数:df.groupby('artistName')['msPlayed'].count(),再按数量排序取前10。统计歌曲同理,按trackName分组。但要注意,歌曲名可能在不同专辑有重复,如果你只按trackName分组,可能会把同一首歌的不同版本混在一起。更严谨的做法是同时用trackName和artistName来分组,也就是df.groupby(['artistName', 'trackName'])。
这个拆解看完,我发现自己的"官方年度歌手"和"实际播放次数最高的歌手"并不完全一致。原因很简单:官方算法综合了多种因素,可能还加权了收听时长、收听完整度等;但我自己的简单排序只看"播放次数"。这不是谁对谁错的问题,而是提醒你,想清楚你在分析什么,再决定指标。播放次数反映的是"打开频率",播放时长反映的是"占用你耳朵的时间",两者结合才完整。
4.3 听歌时段分布:凌晨两点半的你在听什么歌
这是一个让我很惊讶的分析。把endTime转成本地时区之后,提取小时数,做一个按小时的分组统计,画出条形图,就能看到你一天24小时内听歌量的分布。如果你本来就是"晚上听歌党",这个图会非常直观地画出你的作息曲线。
更进一步,把"小时"和"星期几"结合起来做一张热力图,你能精准定位每个工作日的午后和周末的深夜,你在怎么分配耳朵。这个维度做出来之后,我的体会是"数据真的不会骗人":我以为自己是白天听歌为主,但热力图显示我从晚上10点到凌晨1点才是收听量最高的时段。后来想想也确实合理,白天工作忙,最多偶尔放几首,晚上才是真正的放飞时间。
4.4 识别"单曲循环"行为:那些让你停不下来的歌
判断单曲循环,本质上是看"同一首歌在很短的时间内是否被连续播放了多次"。方法不难:先把数据按endTime升序排序,然后判断相邻记录之间,同样歌手和同样歌曲是否出现,且时间差小于一定阈值(比如上一首歌播完到下一首歌开始之间的间隔,可以设定为播放完成时间加上几秒,或者直接看相邻记录时间差小于5分钟)。如果频繁出现"同一首歌连续出现"的模式,那基本可以判定你在单曲循环。
这个分析我用了一个滑动窗口的思路,也可以用shift来对比上一行。代码写起来不复杂,但输出的结果非常有情感冲击力:我发现自己某段时间有一首歌连续出现在深夜的记录里,时间跨度长达一周。说白了,数据记录的不只是歌单,还是你那段日子的情绪状态,这个维度和单纯的Top榜是完全不同的视角。
4.5 长期趋势:用时间序列看到你的口味变化
最后一个维度是时间序列。把endTime作为索引,用resample按月汇总播放时长,画一条折线图,能看到你这几年的收听体量变化。哪个月突然暴跌(比如考试周、出差不能听歌),哪个月急剧攀升(比如某款游戏上线、某个歌手发新专)。这种长周期的趋势图特别容易讲故事。
如果有人还想再进阶一步,可以把歌手也作为一个维度做长期"口味漂移"分析,比如每个季度挑出播放次数最高的歌手,看看这些年来你的本命变没变。这个分析计算量也不大,groupby(['年份季度', 'artistName'])就能实现。
5. 从数字到图表:可视化让分析结果真正说人话
5.1 Matplotlib打好基础
数据分析的结果如果只是打印出一堆数字,那还停留在"自嗨"阶段。可视化才是让分析结果呈现给你的关键一步。我的习惯是先用Matplotlib快速出图,因为它是Python可视化的地基,用起来直接,调试快。
画歌手Top榜的时候,我会先取前10个歌手的数据,然后画一个横向条形图,用barh而不是bar。为什么用横向?因为歌手名是文字,纵向柱状图一旦名字过长就会互相遮挡,横向图在可读性上要好得多。再配合着设置figure的尺寸,让图更清晰。颜色方面我一般直接给一个统一的颜色,不搞花哨,因为花哨颜色在社区发图时反而容易显得业余。
5.2 Plotly交互式图表更适合自己"玩"
Matplotlib出的静态图适合直接贴到报告或朋友圈里,但我想在屏幕上悬停看每一根柱子的数值时,还是Plotly更顺手。Plotly的express接口非常友好,一行代码就能生成交互式图表,鼠标移上去能动显示具体数值,还能框选缩放。
我最常用的两张交互式图表:一张是按小时听歌量分布的条形图,悬停能看到0点到23点每个小时的播放次数和播放时长,非常直观;另一张是月份播放时长的折线图,能够轻松发现季节性变化。把这两张图生成HTML文件,发到手机上也能够打开浏览,想分享给朋友,直接发文件就行。
5.3 一份可直接运行的Top歌手榜可视化代码
下面给出一个我实际用到的完整代码块,把前面说的"按歌手播放次数排序 + Matplotlib横向条形图"串起来,只要你的df已经清洗好,这段代码可以直接跑通:
python复制import matplotlib.pyplot as plt
import pandas as pd
# 假设df已经合并并清洗完毕,包含artistName、msPlayed等字段
# 按歌手统计播放次数
artist_counts = df.groupby('artistName')['msPlayed'].count().sort_values(ascending=False).head(10)
# 中文显示设置,避免歌手名里的中文变成方框
plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC', 'SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(10, 6))
artist_counts[::-1].plot(kind='barh', ax=ax, color='#1DB954')
ax.set_xlabel('播放次数')
ax.set_title('我的Spotify Top 10歌手(按播放次数)')
plt.tight_layout()
plt.show()
# 如果也想看播放时长排名,可以这样:
artist_duration = df.groupby('artistName')['msPlayed'].sum().sort_values(ascending=False).head(10)
print(artist_duration / 3600000) # 转成小时
这段代码会有两个输出:一张图、一个按小时为单位的播放时长榜。把播放次数榜和播放时长榜放在一起对比,你能立刻发现谁是高频短听、谁是一听就停不下来。我自己的结果里,有一位歌手播放次数排名第四,但播放时长排名第一,说明我很少点开他,但只要点开就会完整听完好几首。
5.4 热力图:展示你的"每周听歌节律"
如果你想做得更精致一点,推荐画一个"星期几-小时"的二维热力图。横轴是0到23小时,纵轴是周一到周日,颜色深浅代表该时段播放时长的多少。实现思路也很简单,先构造一个'weekday'列和一个'hour'列,然后用pivot_table生成一个7行24列的透视表,再用imshow或者plotly的heatmap出图。
这张热力图是很多人看完最惊讶的一张。有位朋友照着做之后发现自己工作日晚上10点有一个深色小高峰,周末的中午反而颜色偏浅。每个人的"听觉夜行曲线"都不一样,这些细节恰恰是官方年度总结不会告诉你的。
6. 复盘:我在这个项目里踩过的坑和对应的坑底解决方案
6.1 时区偏移让"深夜分析"直接翻车
这个前面提了,但我还是要放在复盘里再强调一次。第一版分析做出来,我的听歌时段最高峰在早上8点,我一度以为是上班路上听歌导致,后来发现所有时间戳其实都是UTC,换算成北京时区才全部错位。修正之后,真正的收听高峰变成晚上10点到凌晨1点,画风完全变了。如果你导出的数据时间是带Z的,一定记得先转时区再做任何与日期、小时相关的聚合。
6.2 短播放记录是Top榜的"投票幽灵"
msPlayed只有几千毫秒的记录,意味着这首歌播放一两秒就切了。这样的记录如果直接参与歌曲播放次数排名,会让一些根本没听进去的歌冲上榜首。我自己遇到一个极端案例:某首只有4秒播放记录的歌进入了我的歌曲Top 20,原因是我在一堆歌单里不断滑过它。后来我加了"有效播放"过滤条件,把30秒以下的记录单独标记,分析时多了一个视角:既能统计"总被打开的次数",也能统计"真正听了超过30秒的次数"。这种"双轨制"给了我更多解读空间。
6.3 歌曲名相同的"同名陷阱"
有一段时间我以为自己特别爱听某首歌,结果一查发现有多个同名歌曲混在里头,一位是歌手的原版,另一首是live版或者其他歌手的翻唱。简单按trackName分组会把它们合并成同一个,导致输出结果的歌名一样,但实际并不是你想的那首歌。解决办法就是前面说过的,分组时用['artistName', 'trackName']两个字段,宁可多花一点内存,也不要让同名歌曲互相污染。
6.4 数据量大时的性能问题
大部分人的听歌记录在几万到几十万条之间,pandas处理完全没压力。但如果你导出的数据覆盖5年以上,而且你要做很多个维度、很多次groupby,建议用一些"性能习惯":没必要每次操作都重新读原始文件,可以把清洗好的DataFrame直接存成parquet或者pickle,后面反复读取速度能快很多。另外,groupby之后的结果如果只是为了看Top10,就不要排序整个分组结果,用nlargest(10)直接取前10,效率高不少,代码也更清晰。
python复制# 用nlargest替代sort_values().head(10)
top_artists = df.groupby('artistName')['msPlayed'].sum().nlargest(10)
6.5 别忽略"年份范围"的陷阱
如果你的StreamingHistory文件夹里有多个JSON文件,合并之前最好先检查一下每个文件的endTime范围。我遇到过两个文件的时间范围有重叠,导致某些记录被重复统计。如果发现重叠,应该用drop_duplicates按endTime、artistName、trackName、msPlayed四个字段去重。这个方法虽然不能保证100%去掉所有重复(如果你同一秒内听同一首歌两次,理论上还是会被去重误删一次,但实际概率极低),整体还是靠谱的。
7. 分析完之后,还能怎么玩
到这一步,你已经拿到了一份完整的、清洗干净的、带有时区修正的听歌流水DataFrame,也画出了几张能发朋友圈的图。但我觉得这个项目真正的魅力不在于"做完一个分析",而在于后面有太多可以继续挖掘的方向。
我自己接下来想做的事情有两个方向:第一是做"情绪识别",把歌曲的音频特征(比如活跃度、能量值、氛围指标)通过Spotify的API拉下来,跟我的播放记录关联起来,看看我在不同时间段、不同情绪下的选歌倾向,这其实相当于给"心情燃料"打分;第二是做歌单聚类,把常听的歌曲按照歌手、风格、听歌上下文做聚类,看看能不能自动生成几个"我在深夜会听什么"的专属歌单。这个方向用scikit-learn的KMeans就能入门,不需要太复杂的算法,但结果会非常个性化。
如果你只想浅尝辄止,可以做一个小功能:写一个脚本统计你最近一个月的Top 20歌曲,然后自动用spotipy库创建一个公开歌单。代码量不大,但效果很"魔法":你打开Spotify一看,发现自己的年度歌单已经被Python帮你整理好了。
最后分享一个我摸索出来的小技巧:分析完毕之后,一定要把绘制的图和数据存下来,标注好日期和版本。因为你的听歌数据是持续增长的,每个月都能重新跑一遍脚本,对比这个月和你上个月的听歌画像有没有变化。这才是这个项目最有意思的地方——它不是一次性分析,而是一个可以长期维护的自我观察项目。今天跑完只是起点,三个月后你再跑一遍,你会看到数据在诉说你的生活发生了怎样的变化。
