用Python分析Spotify听歌数据:从导出到可视化实战
你有没有想过自己那动不动几百上千小时的流媒体听歌记录除了年末被平台官方总结“炫耀”一下之外还能干点什么我个人的答案是把它导出来交给 Python自己做一套全量分析。这个过程不复杂但你能从播放记录里看到远超想象的东西——几点钟最容易单曲循环、某段时间为什么突然迷上某个乐队、周末和深夜的听歌口味到底差了多少。这篇文章就用“使用Python分析你的Spotify听歌数据”这条线索从数据导出、字段拆解、Python 处理再到各种坑和扩展完整走一遍。非常适合刚接触数据分析的开发者也适合纯粹想把音乐数据玩弄于股掌之间的音乐爱好者。1. 为什么要把听歌记录拉到本地自己分析1.1 平台自带统计和个人分析的根本差异流媒体平台的年度总结做得确实漂亮但它的解读逻辑永远是平台给的排行榜、收听时长、风格标签。这些数据汇总得很聪明却也局限在“展示”层面。一旦你想回答一些更私人化的问题官方渠道基本给不了答案。比如我想知道自己一年里到底为哪首歌贡献了最多播放次数这个排行榜它能给。但我想进一步看看这些播放是集中在某几天还是均匀分布在全年它就未必说得清楚。再比如我想分析“工作日的早上九点我总在听什么类型的歌”这属于典型的自定义时间切分平台不会帮你做。把数据拉到本地后逻辑就变了。所有字段归你控制你可以按任意维度切、任意规则算。Streaming history 这类数据本质上就是一个带时间戳的事件流适合用来观察个人行为节奏。你说它是数据分析练习也行说它是自我观察也行关键是分析的主语变成了你而不是平台的推荐算法。1.2 这件事到底适合谁来折腾我见过不少类似需求的人大致分三类。第一类是刚学 Python 的人pandas 还没用熟正好拿真实数据练手比教程里的假数据有意思太多。第二类是对隐私和数据所有权比较敏感的人他们想知道平台到底存了哪些关于自己的细节。第三类纯粹是音乐发烧友想从数字里反推自己的审美变化。无论属于哪一类这件事的门槛都不高。基础要求只有两条能装 Python 环境会看基础语法。剩下的就是跟着数据格式走边查边写。分析完之后你收获的不只是一张统计表还会顺手学会 JSON 数据解析、时间序列处理、分组聚合甚至简单的可视化。这套技能用到其他场景比如分析自己的通讯记录、购物订单完全同样适用。2. 数据准备先把播放记录从平台里取回来2.1 导出数据的常见方式与选择思路做分析的第一步是拿到数据。以 Spotify 这类主流流媒体平台为例一般都在账号设置里的隐私或数据板块提供导出入口。点一下申请平台会生成一份“你的全部个人数据”打包文件。这个过程通常需要几天时间因为后台要打包的不仅是你听的歌还有播放列表、搜索记录、关注列表等。如果你只想分析听歌历史下载下来后在文件夹里找带“StreamingHistory”字样的文件即可。这类文件通常是 JSON 或 JSONL 格式每行代表一条播放事件。有人会问为什么不直接调 APIAPI 确实能拿到用户最近播放的歌曲也能实时获取当前播放状态但它的局限非常明显。平台 API 一般只能拿到近期数据有的是最近 1 小时有的是最近几十条根本拿不到完整历史。官方导出文件虽然要等胜在完整一次性能给你过去所有的播放记录。所以我的建议是做深度分析老老实实走导出流程。2.2 导出文件里的字段到底长什么样拿到打包文件后别急着写代码先打开 StreamingHistory 文件看看结构。不同时期、不同格式的导出文件字段命名略有差异但核心信息都很稳定。比较常见的有以下几个字段名含义典型示例ts播放发生的时间戳2024-05-14T13:22:31Zusername账号名通常是你的用户名或脱敏后的 IDplatform播放设备平台iOS、Android、web player、Windowsms_played本次播放的毫秒数183000conn_country播放时的网络地区码US、DE、JPip_addr播放时使用的IP地址脱敏值可能包含部分真实网段user_agent_decrypted客户端环境信息浏览器或客户端的 UA 字符串master_metadata_track_name歌曲名某首歌的标题master_metadata_album_artist_name艺术家名某个乐队或歌手master_metadata_album_album_name专辑名某张专辑标题spotify_track_uri歌曲的唯一资源标识spotify:track:xxxxx第一眼看到这些字段可能觉得信息量很足但这个文件并不是“一行一次完整播放”的干净流。有人播放 3 秒切歌有人整首听完还有人用离线模式播放这些数据在导出的文件里都可能留下不一样的痕迹。理解这些细节是后面清洗数据的前提。2.3 我为什么优先推荐 JSON 而非 CSV有些导出的数据会附带 CSV 文件直接拖进 pandas 就能用看着省事。但我仍然建议优先处理 JSON原因很简单JSON 保留了字段嵌套关系也保留了完整的 URI 和原始时间戳。CSV 在导出时可能已经做过一层展平丢失一部分信息比如某些平台会把时间戳转成本地时间让后续时区处理变麻烦。如果你拿到的只有一个 CSV也别急pandas 照样能读但遇到和分析本文提到的坑时记得多留个心眼。最好先对照导出的字段说明搞清楚每个列是从哪个原始字段生成的。3. Python 分析从零跑通读取、清洗到出结果3.1 环境准备与数据加载分析环境不用多复杂一个 Python 脚本文件加 pandas 就够。如果你还没有安装相关库在命令行里执行pip install pandas matplotlib jupyter我个人习惯先用 Jupyter Notebook 做探索因为数据量大、字段杂交互式地看每一行输出会比较直观。等理清逻辑后再把脚本整理成标准的 .py 文件方便下次直接跑。假设你的播放历史文件叫StreamingHistory0.json和脚本放在同一目录加载代码非常简单import pandas as pd file_path StreamingHistory0.json df pd.read_json(file_path) print(df.shape) print(df.columns.tolist()) print(df.head())如果文件是 JSONL 格式也就是每行一个 JSON 对象压成 DataFrame 前最好先按行读取再合并import json import pandas as pd records [] with open(StreamingHistory0.jsonl, r, encodingutf-8) as f: for line in f: records.append(json.loads(line)) df pd.DataFrame(records) print(df.shape)用这种方式加载能避开read_json对 JSONL 格式解析不准的问题。我遇到过导出文件明明是 JSONL却因为第一行带了个方括号导致 pandas 直接报错的情况手动逐行读取最稳妥。3.2 基础清洗时间戳、时长和播放次数拿到原始 DataFrame 后第一件事永远是看缺失值和数据格式。听歌记录里最常见的坑有三个时间戳是字符串、播放时长是毫秒、部分行没有歌曲元数据。我们先把时间戳转成 pandas 的时间类型再把毫秒转成秒和分钟df[ts] pd.to_datetime(df[ts], utcTrue, errorscoerce) df[play_seconds] df[ms_played] / 1000 df[play_minutes] df[play_seconds] / 60 # 剔除时间解析失败的行 df df.dropna(subset[ts]) print(df.dtypes)有个容易被忽略的细节ms_played里的值不一定是完整播放时长。当歌曲被切歌时它的值会小于歌曲实际长度。所以后面做总时长统计时要用这个字段求和而不是去 join 歌曲信息表按歌曲长度计算。用字段求和是“你实际听了多久”按歌曲长度算则是“你本可以听多久”两个含义完全不同。同时建议把歌曲名、艺术家名里的首尾空格清理掉不然排名时同一个歌手会因为一处空格被拆成两个名字for col in [master_metadata_track_name, master_metadata_album_artist_name, master_metadata_album_album_name]: df[col] df[col].astype(str).str.strip()3.3 时间维度分析你都在什么时候听歌这是整个项目里我最喜欢的一部分因为你会发现很多意外规律。准备几个新列小时、星期几、月份、年份。df[hour] df[ts].dt.hour df[weekday] df[ts].dt.dayofweek # 0周一 6周日 df[month] df[ts].dt.month df[year] df[ts].dt.year先看一下按小时分布的播放次数和播放时长hourly df.groupby(hour).agg( play_count(ts, count), total_seconds(play_seconds, sum) ).sort_index() print(hourly.head(24))如果你的导出时间跨度够长这批数据能直接回答一个经典问题我到底是深夜型听众还是通勤型听众我拿自己数据跑出来的结果晚上 8 点到 11 点是峰值白天 10 点有一个小高峰恰好对应通勤和摸鱼时间。这个结论不看数据完全想不到因为体感上我一直觉得自己是“全天都在听”。星期分布也很有意思。可以用交叉表看周内每天和小时的联合分布weekday_hour df.pivot_table( indexweekday, columnshour, valuesplay_seconds, aggfuncsum, fill_value0 ) print(weekday_hour)矩阵的行是周一到周日列是 0 到 23值是当天该小时的总播放秒数。这张表一旦用热力图展示立刻就能看出周末和作息的关系。周六凌晨的峰值如果很高说明你是个夜猫子如果工作日上午 8 点出现孤峰说明你主要在通勤路上听。3.4 歌曲与艺术家排名不只看播放次数统计播放次数是最简单直白的排名思路但它有个缺陷一首歌只播了 10 秒就被切掉依然算一次播放。真实反映“听得多久”的指标应该是累计播放时长。下面这个表两个指标都算出来排名对比着看ranking df.groupby( [master_metadata_track_name, master_metadata_album_artist_name] ).agg( play_count(ts, count), total_seconds(play_seconds, sum), avg_seconds(play_seconds, mean) ).reset_index() ranking[total_minutes] ranking[total_seconds] / 60 ranking ranking.sort_values(total_seconds, ascendingFalse) print(ranking.head(20))这个结果会告诉你一件事单曲循环次数最多和累计时长最长的歌很可能是两批完全不同的歌。次数多可能因为歌短或者你经常听一半切歌时长长则说明你是真的沉浸进去了。两个指标放在一起你对自己的“真爱曲目”会有一个更立体的判断。艺术家维度同理但要注意同一首歌曲在多个专辑出现的情况比如现场版、重制版、单曲版。如果你只想统计“按录音室版”可以直接用spotify_track_uri去重后再分组。如果想放宽点也可以把所有版本合并看这个艺术家在你播放记录里的总权重。分析目标不同清洗口径不同这一点在动手前就要想清楚。3.5 可视化把统计数字变成一眼能看懂的图数字表格能提供精确结论但如果你想展示给别人或者想更直观地感知趋势可视化不会缺席。用 matplotlib 画一个按年份、月份播放时长的趋势线再画一张小时热力图基本就能覆盖主要分析目标。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] # 根据你的系统字体调整 plt.rcParams[axes.unicode_minus] False monthly df.groupby(df[ts].dt.to_period(M))[play_seconds].sum() / 3600 fig, ax plt.subplots(figsize(12, 5)) monthly.plot(kindline, markero, axax) ax.set_title(每月累计播放时长(小时)) ax.set_xlabel(月份) ax.set_ylabel(小时) plt.tight_layout() plt.show()如果你之前没装 seaborn执行pip install seaborn就行。热力图部分可以基于前面的weekday_hour矩阵fig, ax plt.subplots(figsize(10, 5)) sns.heatmap(weekday_hour, cmapviridis) ax.set_title(听歌时间热力图) plt.show()图表的作用不是堆砌而是验证发现。画图时我会问自己这个结果是否能回答最开始提出的问题如果只是把统计数字换个形式输出那图反而没有存在意义。4. 实际操作中踩过的坑与排查技巧4.1 时区偏移同一个时间戳你的本地时间和平台不一样这个问题几乎人人都会遇到。导出的ts字段末尾带一个Z代表 UTC 时间。如果你直接拿它做“我几点听歌”的统计得到的是 UTC 时间而不是你所在时区的真实时间。举个例子你晚上 10 点在东八区听了一首歌导出的时间戳是当天 14:00Z按小时直接分组会被算成下午 2 点。解决办法有两种。最稳妥的是加载时就把时间转成目标时区local_tz Asia/Shanghai df[ts] pd.to_datetime(df[ts], utcTrue).dt.tz_convert(local_tz)之后所有按小时的统计都基于本地时间。另一个办法是只做纯 UTC 分析不关心本地时区但这样你分析“深夜听歌”就不成立了。强烈建议第一步就统一时区否则后面所有小时级分析都得推翻重来。有个让我印象很深的坑夏令时地区的数据。如果导出数据里包含其他国家的播放记录或者你旅途中在不同时区听歌单一时区转换只能解决“当前居住地”的时间表达无法还原每首歌播放那一刻的当地感受。处理这类场景时需要结合conn_country字段再做一个映射但这通常超出个人分析的需求除非你专门分析旅行期间的听歌行为。4.2 播放时长为零和未完成的播放播放记录里经常出现ms_played为 0 的行看起来毫无意义但它们确确实实占着一段历史。产生原因很多歌被立即暂停、播放出错、平台记录异常。这些行会给“所有歌曲平均播放时长”的计算带来偏差我一般直接在清洗时过滤掉df df[df[ms_played] 0]还有一种更隐蔽的情况一首歌的播放时长超过了歌曲本身长度。这和歌曲的实际时长无关而是离线播放后重新联机补传导致的记录错误。遇到这种行要么剔除要么用歌曲真实时长做截断处理。个人分析可以简单选择剔除毕竟这样的数据占比很低。4.3 内存占用与文件读取效率有几年的完整播放历史后行数会迅速膨胀。我见过一个导出文件里面光 StreamingHistory 就拆成了 5 个 JSON 文件合并后有接近百万行。直接在 DataFrame 上做多次 groupby 和排序内存占用可能冲到几个 GB普通笔记本电脑会卡到怀疑人生。我的建议是读取后优先做字段瘦身。如果只分析听歌行为把ip_addr、user_agent_decrypted、username这些用不到的字段直接删掉。操作顺序也很重要先过滤ms_played 0再删除大字段能明显缩短处理时间。df df.drop(columns[ip_addr, user_agent_decrypted, username, conn_country], errorsignore)如果你要长期做分析还可以将清洗后的数据一次性存成 parquet 格式后续读取比 JSON 快很多df.to_parquet(cleaned_history.parquet, indexFalse) # 下次读取 df pd.read_parquet(cleaned_history.parquet)4.4 同名歌曲与艺术家字符串不统一不同来源的数据字符串格式会很折磨人。同一个歌手的名字在 A 专辑里是“某歌手”在现场专辑里可能多了一个年份后缀在 Feat 版本里还会出现一堆合作歌手名。直接按原字符串分组会把同一个音乐人的数据拆得七零八落。我的处理技巧是先把master_metadata_album_artist_name里的“Feat.”、“×”等杂质统一清理再决定是否合并统计。简单做法是保留主歌手只取第一个分号或逗号前的部分。高级做法是用 URI 去关联但 URI 层面也可能存在专辑版、单曲版、现场版不同 URI 的差异所以个人分析还是优先保证“大致准确”。不要追求完全精确。你不是在做科研论文你是在探索自己的音乐口味。统计结果相差 1% 或 2%不影响洞察结论。5. 分析完还能往哪些方向扩展5.1 做成一个定期自动更新的报告脚本一次性的分析跑完就丢价值有限。更好的做法是把整个流程整理成脚本设定每个月自动跑一次生成当月听歌报告。数据源怎么更新只需要每个月重新下载一次导出文件或者写一个定时任务帮你从下载文件夹里找到最新导出并覆盖旧文件。脚本输出可以是 Markdown 报告也可以是一张汇总图。我自己的方案是脚本每个月生成一份单页 HTML里面包含当月播放时长、最爱歌曲、新发现艺术家、每日听歌时段热力图。生成逻辑不复杂核心就是 pandas 聚合 matplotlib 存图 模板拼接但坚持下来以后你会拥有一个自己专属的音乐月度档案比任何平台总结都有说服力。5.2 尝试做更高级的关联分析与推荐实验当数据积累到一定程度可以玩的东西会变多。比如把播放记录按天聚类看看哪些天听歌风格更像或者把艺术家按流派标签化分析自己的“音乐情绪轨迹”甚至可以构建一个简单的协同过滤模型用自己不同时期的播放记录预测现阶段的潜在喜好。不过这些方向越往后越容易陷入“为了复杂而复杂”。我自己的经验是先完成基础统计想清楚问题再做模型。如果只是好奇把艺术家名和播放时长构建成一个简单的偏好评分表再用距离度量算每个月口味迁移就足够有趣了。5.3 千万别忽略隐私边界本地分析个人数据的最大优势是隐私可控。所有数据只存在于你电脑里不要随便把包含 IP、账号名的原始导出文件传到公开仓库。即使脱敏后分享分析结果也建议先删除ip_addr、username这类字段。代码可以开源个人数据应当留在自己的硬盘里。我也提醒一句任何第三方“在线导入 Spotify 数据生成分析报告”的工具都需要你上传个人数据。除非完全信任对方否则谨慎使用。自己写脚本跑一遍的成本其实很低而且完全掌控过程这比什么在线工具都可靠。写在最后的一点经验我把这套流程跑通之后最大的感受是数据分析最有价值的部分不是那个排名表而是数据帮你看见了很多被日常忽略的行为模式。比如我从来没意识到自己每周日的上午会严重偏向听一类特别安静的器乐而工作日午后却高频播放节奏激烈的电子乐。这种发现以后你再打开音乐软件时会下意识地留意自己当下的选择。如果你也想动手做同样的事记住一个关键节奏先拿小文件跑通单次分析再扩展到大文件先看整体趋势再深入局部细节。别一开始就追求画酷炫的图表不然大概率会卡在某个时区坑或字段编码问题上下不来。把数据导出来打开 DataFrame 看一眼你和这份数据的故事就算正式开始了。