ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python分析Spotify听歌数据:从导出到可视化全流程

用Python分析Spotify听歌数据:从导出到可视化全流程 前几天我突然起了个念头自己在 Spotify 上这些年到底听了些什么。点开 App 里的年度回顾永远只能看到一份官方精修过的榜单——那是一种漂亮的、被规整过的总结但真实的听歌数据要更粗糙也更有意思。干脆直接去 Spotify 的隐私中心导出了完整听歌历史拿到一堆 JSON 文件然后用 Python 把它们变成了图表、排位和统计数字。这套分析完全跑通之后我发现自己的收听规律和想象中差了很远深夜才是真正的播放高峰有些歌只在通勤路上出现所谓“最爱的乐队”其实只占据了我很小一段 listening time。这篇文章就把完整的分析思路、代码实现和踩过的坑都拆给你照着做一遍你也能生成属于自己的听歌年度报告而且是完全按你自己的口径来算的版本。这个项目核心就是三件事把你的 Spotify 数据导出成文件、用 Python 读取并清洗、然后做聚合统计和可视化。适合三类人一是刚开始学 pandas 和 matplotlib 的数据分析初学者手头缺一个够真实、不无聊的练手数据集二是对隐私数据好奇的普通用户想看看平台到底记了你什么三是想用数据重新认识自己的音乐口味而不是被平台算法反向定义的人。1. 项目思路与整体设计先想清楚数据长什么样1.1 核心需求解析你的听歌历史本质上是一份日志拿到导出包后我打开第一个 JSON 文件时愣了一下——每条记录就是一个对象里面有时间戳、歌曲名、艺术家、专辑、播放了多久、用什么设备播的、为什么开始、为什么结束。这其实和运维领域熟悉的日志分析是同一套东西一个带时间、来源、事件类型、持续时长的流水表。理解了这一点整个分析思路就顺了先做数据清洗把脏的、重复的、无效的记录去掉再做时间序列聚合看趋势和周期性最后做分组统计找 Top 排名和分布规律。唯一要注意的是这份日志有两种版本格式。2023 年前后的导出包字段差别很大老格式是StreamingHistory0.json字段是endTime、artistName、trackName、msPlayed这种简洁命名新格式是endsong_0.json字段变成了ts、master_metadata_track_name、master_metadata_album_artist_name、ms_played这种带前缀的完整命名还多了reason_start、reason_end、platform、shuffle等一堆行为字段。写脚本的时候必须两种都兼容这是第一个实操要点。为什么要强调这一点因为我发现很多网上的教程只针对老版本你拿着新导出的数据直接套跑出来的全是空值。而且新格式里还有播客数据混在里面字段是episode_name而不是master_metadata_track_name如果你不把它们单独挑出来统计图表里会莫名其妙多出一堆“剧集”。1.2 方案选型为什么必须走官方导出而不是直接调用 API可能有人会问不是有 Spotify Web API 吗为什么还要费劲导出数据这里有一个任何用 API 做过音乐数据的人都会立刻理解的限制官方 API 里的recently played接口最多只能拿到最近 50 条播放记录而且接口有配额限制currently playing接口只能返回当前这一首。也就是说API 适合做“此刻在播什么”或者“最近一小段”的应用但它根本拿不到你的历史全量数据——API 的定位是给第三方 App 做播放控制用的不是给你做个人数据分析用的。唯一能拿到完整历史记录的合法途径就是走 Spotify 的账号隐私设置提交“Download your data”请求。这个导出流程通常需要几天到两周官方会发邮件通知你文件准备好然后你下载一个 zip 压缩包。包里有账号数据、扩展听歌历史、搜索记录等多个子目录其中Extended streaming history目录下的endsong_*.json就是我们分析的核心数据。还有一个很少被提到的点导出时你会看到两个选项——账号数据和扩展听歌历史。如果你只选了前者下载包里虽然也有播放记录但没有reason_start、reason_end这些行为字段。做深度分析的话一定要选完整历史记录不然很多分析维度就没了。1.3 分析管线设计先跑通一条小样本再全量计算拿到数据之后不要急着写一个大而全的脚本。我自己的习惯是先写几行代码读取一个 JSON 文件把字段打印出来确认格式然后只挑其中一段数据做字段映射和可视化跑通整条链路最后再全量加载、合并、清洗。这样能避免“写完三千行脚本一跑发现字段名拼错了”这种灾难。技术选型上我用了 pandas 做数据清洗和聚合matplotlib 加 seaborn 做可视化就是最普通的 Python 数据分析三件套。不用 Excel 处理是因为数据量往往是几万到几十万条记录Excel 打开一个 50MB 的 JSON 就卡死了而且我们后面要做时间序列重采样、分组聚合这类操作pandas 的语法比手写循环高效得多。另外提一句如果最后想要精细报表可以顺手用df.to_excel()把聚合结果导成 Excel方便在不写代码的时候翻看但这只是锦上添花。2. 数据获取与环境准备把原始材料收拾干净2.1 申请导出数据的完整流程去 Spotify 网站登录自己的账号进入账户设置里的隐私中心找到“Download your data”选项卡点击请求导出。页面会提示你可以选择导出范围这里选“Extended streaming history”也就是完整的扩展听歌历史。提交之后就是等待官方说最长可能 30 天但我几次实测下来从请求到收到邮件一般是一周以内。收到通知邮件后下载 zip 包并解压。我这次拿到的目录结构大概是这样的MyData/ ├── AccountData/ │ ├── Identity.json │ ├── UserDetails.json │ └── ... ├── Extended streaming history/ │ ├── endsong_0.json │ ├── endsong_1.json │ └── ... ├── Identifiers/ │ ├── identifiers.json │ └── ... └── Inferences/如果是老账号在Extended streaming history目录下也可能出现StreamingHistory0.json到StreamingHistory9.json这种老命名文件。这里顺带提醒一句解压出来的包里有identifiers.json里面有设备 ID、广告 ID 之类的标识信息分析的时候用不到但是属于比较敏感的数据注意不要随便把整个文件夹发到网上去。2.2 新老格式的字段对照与理解为了写兼容脚本我把两种格式的核心字段列了一个表这样一眼就能看出映射关系含义老格式字段新格式字段播放结束时间endTimets歌曲名trackNamemaster_metadata_track_name艺术家artistNamemaster_metadata_album_artist_name专辑无master_metadata_album_album_name播放时长毫秒msPlayedms_played曲目 URI无spotify_track_uri播放平台无platform开始原因无reason_start结束原因无reason_end是否离线无offline注意老格式的endTime是形如2021-10-05 14:32的本地时间字符串不带时区信息新格式的ts是 ISO 8601 格式通常是 UTC 时间后面带着Z或者00:00这样的时区标记。这个差异直接影响到你算“每天几点听歌最多”的时候小时分布对不对。我一开始忽视了这个问题结果发现所有时间都比实际慢或快了几个小时排查了半天才发现是时区没统一。2.3 Python 环境与依赖库安装分析环境不需要多复杂Python 3.9 以上就行。如果你还没装 Python去官网下载安装包安装的时候记得勾选“Add Python to PATH”。装完之后在命令行里执行pip install pandas matplotlib seaborn这三个库是核心。pandas 负责数据结构和聚合matplotlib 负责基础绘图seaborn 在 matplotlib 之上提供更美观的热力图和统计图。如果你打算后面调用 Spotify 的 Web API 来补充分数数据再装一个spotipypip install spotipy代码组织上我建议把分析脚本放在一个单独目录里例如spotify-analysis/解压后的 MyData 文件夹放在同目录下这样脚本里用相对路径引用数据文件不会因为路径问题报错。不要直接用压缩包里的路径也不要拷到云盘同步目录里因为某些云同步服务会在后台占用文件句柄导致 Python 读取时遇到奇怪的权限错误。3. 核心代码实现从原始 JSON 到干净 DataFrame3.1 读取与合并多个文件兼容新旧格式第一步是把所有 JSON 文件都读进来并且统一成一套字段。因为新老格式存在我写了一个兼容函数核心逻辑是先判断记录里有没有master_metadata_track_name这个键如果有就按新格式解析没有就按老格式解析import json import glob import pandas as pd def load_spotify_history(folderMyData): patterns [ MyData/**/endsong_*.json, MyData/**/StreamingHistory*.json, ] files [] for pat in patterns: files glob.glob(pat, recursiveTrue) rows [] for path in files: with open(path, r, encodingutf-8) as f: data json.load(f) for rec in data: if master_metadata_track_name in rec: rows.append({ ts: rec.get(ts, ), artist: rec.get(master_metadata_album_artist_name, ) or , track: rec.get(master_metadata_track_name, ) or , album: rec.get(master_metadata_album_album_name, ) or , ms_played: rec.get(ms_played, 0), platform: rec.get(platform, ), reason_start: rec.get(reason_start, ), reason_end: rec.get(reason_end, ), track_uri: rec.get(spotify_track_uri, ), is_podcast: bool(rec.get(episode_name, )), }) else: rows.append({ ts: rec.get(endTime, ), artist: rec.get(artistName, ) or , track: rec.get(trackName, ) or , album: , ms_played: rec.get(msPlayed, 0), platform: , reason_start: , reason_end: , track_uri: , is_podcast: False, }) return pd.DataFrame(rows) df load_spotify_history() print(df.shape) print(df.head())这里有一个关键设计新格式里只有播客记录才有episode_name所以is_podcast这一列可以直接从字段是否存在来判断。老格式因为没有播客数据直接全部置为 False。这一步如果不做后面的所有统计都会把播客混进歌曲里排名图表会很奇怪。3.2 数据清洗时间解析、时区统一和无效记录过滤读进来之后接下来是数据清洗。核心操作有三个时间解析、时区统一、无效记录过滤。时间解析这一步最容易踩坑因为新旧格式时间格式不一致。我的处理方式是先统一用pd.to_datetime解析然后判断解析出来的时间是否带有时区如果没有时区信息说明是旧版本地时间就把它当成你所在的时区来本地化如果已经有时区信息则直接转换到目标时区。from datetime import datetime def normalize_time(s, tzAsia/Shanghai): try: t pd.to_datetime(s) if t.tz is None: t t.tz_localize(tz) else: t t.tz_convert(tz) return t except Exception: return pd.NaT df[ts] df[ts].apply(normalize_time) df df.dropna(subset[ts])时区选择哪里取决于你大部分时间生活在哪个时区。因为老格式的时间是“播放结束时的本地时间”没有偏移记录你只能根据自己经常待的地区来设定。这个选择不会影响总时长、排名这些统计但会影响“按小时分布”的横轴位置。我自己常驻国内所以都用Asia/Shanghai统一。接下来是无效记录过滤。Spotify 的记录里有大量播放很短的情况比如切歌、误触、自动播放被中断。行业里一个常见口径是播放时长超过 30 秒才算是有效收听这也是 Spotify 自己年度总结的统计口径之一。我建议先保留全量数据但新增一个标记列而不是直接删除短记录df[minutes] df[ms_played] / 60000 df[is_valid] df[ms_played] 30000为什么保留而不删除因为有时候“被跳过的歌”本身就是一种信号——一首歌你反复点了又跳过说明你可能讨厌它而自动播放的歌反而听完说明算法比你更懂你自己。不同的分析目标会用不同的过滤口径所以标记比删除更灵活。3.3 数据增强给每条记录加上分析上下文清洗完之后要给 DataFrame 增加一批从时间戳里派生出来的维度字段这样后续聚合会非常方便。我增加了这些列df[hour] df[ts].dt.hour df[weekday] df[ts].dt.dayofweek # 0周一6周日 df[year] df[ts].dt.year df[month] df[ts].dt.month df[date] df[ts].dt.date df[is_weekend] df[weekday] 5 df[full_play] df[reason_end] trackdonehour、weekday、year这些列就是用来做时间维度分析的。full_play列更有意思reason_end字段新格式才有老格式全为空。当reason_end等于trackdone时代表这首歌是自然播完的而不是被手动切换、返回、关掉 App 打断的。这能用来衡量“完整收听率”某种程度上比播放次数更能说明你对一首歌的真实喜爱程度。有一点要提醒如果数据里混着老格式reason_end会是空字符串这个full_play标记对老数据没有意义。所以后续如果有按完整收听率排名的分析必须只针对新格式数据子集来做不然老格式会全部被算成“未完整播放”结果完全失真。4. 深度分析与可视化把数字变成你的音乐故事4.1 总览指标与月度趋势一眼看清你的音乐消费总量清洗完成之后先算几个总览数字。我这次导出的数据里总计有 4 万多条播放记录。用 pandas 算总时长和去重曲目数量非常直接total_minutes df[minutes].sum() valid_minutes df.loc[df[is_valid], minutes].sum() total_plays len(df) valid_plays df[is_valid].sum() unique_tracks df.loc[df[track] ! , track_uri].nunique() print(f总播放次数: {total_plays}) print(f有效播放次数(30s): {valid_plays}) print(f累计播放时长: {total_minutes / 60:.1f} 小时) print(f有效播放时长: {valid_minutes / 60:.1f} 小时) print(f去重歌曲数: {unique_tracks})这里有一个要注意的细节ms_played的单位是毫秒所以除以 60000 才是分钟很多人第一次分析会把单位看错导致数字大得离谱。另外一条记录里ts是播放结束时间ms_played是这次播放持续的时间而不是歌曲本身的时长。如果你的网络不好一首歌可能分了两次记录播完这时候按记录次数统计就会把同一首歌算两次但按时长统计仍然准确。月度趋势可以直接用 pandas 的重采样功能monthly df.set_index(ts).resample(M)[minutes].sum() monthly monthly / 60 # 转为小时 ax monthly.plot(kindbar, figsize(14, 4), color#1DB954) ax.set_title(每月累计播放时长小时) ax.set_xlabel(月份) ax.set_ylabel(小时) plt.tight_layout() plt.savefig(monthly_trend.png, dpi150)从月度趋势图上可以看到非常明显的周期性变化每年年初播放量骤降年末回升暑假也有一个小高峰。这种周期性本身就是你生活方式的数据投影——比如考试月、工作忙季听歌量明显变少。4.2 艺术家与歌曲排名统计口径不同结论完全不一样排名是所有人都最想看的部分但这里有一个非常关键的口径问题按播放次数排名还是按累计播放时长排名大多数排行榜网站用的是播放次数但对个人分析来说累计时长往往更符合“你有多爱这个艺术家”的真实感受——因为一首 6 分钟的后摇能撑起很大的时长而一首 2 分钟的流行歌需要播三次才能追平。artist_plays df[df[is_valid]].groupby(artist)[track].count().sort_values(ascendingFalse) artist_minutes df[df[is_valid]].groupby(artist)[minutes].sum().sort_values(ascendingFalse) top10_plays artist_plays.head(10) top10_minutes artist_minutes.head(10)我跑出来两个榜单之后发现差异很大按次数排前几名全是一两分钟的快歌歌手按时长排排在前面的是那些动辄五六分钟的乐队。这两种口径没有谁对谁错取决于你想回答什么问题——“哪个歌手让我频繁点开”和“哪个歌手的音乐真正陪伴了我最久”是两个不同的答案。歌曲维度的排名类似但我习惯把歌曲和艺术家组合在一起做分组避免同名歌曲在不同专辑里混淆track_stats ( df[df[is_valid]] .groupby([track, artist]) .agg(plays(ms_played, count), total_minutes(minutes, sum)) .sort_values(plays, ascendingFalse) .reset_index() )播放次数最靠前的歌里面有好几首是我自己都没意识到听了那么多次的歌——它们不是我最喜欢的但它们出现在我的歌单开头位置每次随机播放都会遇到积少成多就成了“播放王”。这其实也是年度总结类 App 的一个盲区平台知道你的播放数但只有把“主动收听”和“被动被播放”分开来看才是真正属于你的音乐画像。4.3 收听时段与生活规律热力图揭示你的生物钟时间维度的分析是这次最有意思的部分。先看按小时的播放分布hourly df[df[is_valid]].groupby(hour)[minutes].sum() / 60 ax hourly.plot(kindbar, figsize(12, 4), color#191414) ax.set_title(一天 24 小时累计播放时长小时) ax.set_xlabel(小时) ax.set_ylabel(播放时长小时) plt.tight_layout() plt.savefig(hourly_distribution.png, dpi150)我的结果里凌晨 1 点到 3 点有明显的播放高峰早 9 点到 10 点是一个次高峰下午时段反而是低谷。深夜高峰很好理解很多人睡前会听歌但更高的一个深夜峰值出现在凌晨 2 点那个时间点还在听歌说明我经常失眠或者深夜才是真正属于自己的时间。更有信息量的是“星期 × 小时”的热力图用 pandas 的透视表加 seaborn 画import seaborn as sns heat_data df[df[is_valid]].pivot_table( indexweekday, columnshour, valuesminutes, aggfuncsum, fill_value0 ) / 60 # 转为小时 fig, ax plt.subplots(figsize(16, 4)) sns.heatmap(heat_data, cmapGreens, linewidths0.1, axax, cbar_kws{label: 小时}) ax.set_xticklabels([f{h:02d}:00 for h in range(24)]) ax.set_yticklabels([周一, 周二, 周三, 周四, 周五, 周六, 周日]) ax.set_title(星期 × 小时 播放时长热力图) plt.tight_layout() plt.savefig(weekday_hour_heatmap.png, dpi150)从热力图能看出来这个账号的听歌行为在工作日和周末完全是两种模式工作日是中午 12 点和晚上 9 点的双峰周末则整体后移下午 2 点到晚上 11 点几乎整段都亮。这种数据比任何问卷调查都诚实——带有时间戳的行为记录是骗不了人的。4.4 显式内容、完整收听率与更深的音乐口味分析除了排名和时间分布还可以做几个有意思的口味分析。新格式里没有直接的“是否显式内容”字段但可以通过曲目 URI 去 Spotify Web API 里查询歌曲的详细信息包括explicit字段以及valence情感积极度、energy能量值、danceability跳舞适配度这些音频特征。这里我用了spotipyimport spotipy from spotipy.oauth2 import SpotifyClientCredentials # 需要先在 Spotify Developer Dashboard 创建应用获取凭证 sp spotipy.Spotify(auth_managerSpotifyClientCredentials( client_idYOUR_CLIENT_ID, client_secretYOUR_CLIENT_SECRET ))因为 API 有配额限制几千首曲目要分批查询直接用大列表请求很容易触发限流。实际处理时我写了一个简单的批量函数每次传 50 个曲目 ID 查询拿到结果后和主 DataFrame 做一次合并。这样就能画出自己的“听歌情感曲线”——按月份聚合valence的平均值看看自己是不是在某段时间特别低落。完整收听率也是一个值得关注的维度。用reason_end等于trackdone的占比来衡量full_play_rate ( df[df[reason_end] ! ] .groupby(artist)[full_play] .mean() .sort_values(ascendingFalse) )完整收听率高的艺术家很可能是你主动循环的、真正沉淀下来的内容播放次数高但完整收听率低往往说明这个歌手的歌经常被当作背景音乐随机放到又被随手切走。这两个指标放在一起看才是一个立体的人。5. 常见问题与排查技巧实录5.1 时间戳解析失败或时区偏移很多人在导入老版本数据时会遇到endTime格式是2021-10-05 14:32这种没秒数、没时区的字符串。直接用pd.to_datetime(df[endTime])一般能解析成功但解析出来的对象是不带时区的naive time。如果后续直接把它和新版 UTC 时间合并会导致两种数据的时间基准不一致统计出来的“按小时分布”会有一批记录偏移若干小时。排查方法很简单用df[ts].dt.tz看一下是否为空如果两者时区不一致就统一成同一个时区。新版时间戳的格式也可能不是标准Z可能是01:00这种带偏移量的写法pd.to_datetime通常也能处理但如果遇到极端格式、解析失败会返回NaT。所以清洗阶段一定要做dropna(subset[ts])否则后面聚合全是 NaN。5.2 中文字体在图表里变成方块matplotlib 默认字体在 Linux 环境里对中文支持很差图表的标题和坐标轴标签会变成一个个方框。这个问题在 Windows 上相对少见因为系统自带 SimHei 这类中文字体但如果你在 macOS 上运行或者用远程 Linux 服务器跑脚本几乎必现。最简单的解决方案有两种。一种是在脚本里显式指定系统里已有的中文字体import matplotlib import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [PingFang SC, SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False另一种更省事的做法是图内的标签全部用英文只在正文里用中文解释。不是所有平台都需要中文标签而且英文标签在任何环境中都不会乱码。我自己最后是项目初稿用英文标签等确认分析逻辑无误之后再改成中文出图省去了反复排查字体问题的麻烦。5.3 播放次数总和与平台显示的年度总结对不上这是最常见的“对不上”问题。背后的原因有三个任何一个都会导致数据不一致。第一Spotify 年度总结只统计完整播放或超过 30 秒的记录而原始数据是全部记录。第二同一账号可能有多个文件重复记录——比如在多个设备上登录、或者导出了多次数据合并时没有去重。第三如果你导出的数据包括新老两种格式同一个时间段可能存在版本重叠。解决方式是在合并之前做一个去重操作df df.drop_duplicates(subset[ts, track, artist, ms_played])这行代码按时间、歌曲、艺术家、播放时长四个维度去重基本可以消除多文件重复记录。但要注意如果你确实在同一个时间点反复听了同一首歌比如用循环播放那不算是重复记录因为播放时长通常并不完全相同。5.4 隐私提醒导出包里有些字段别乱发最后想特别提醒一点。导出包里的数据远不止听歌记录。identifiers.json里有设备 ID 和广告 IDUserDetails.json里甚至有邮箱、用户名信息旧版数据里的 IP 地址也会随着播放记录一起出现。这些字段用来做分析完全没有必要。所以在分析的时候建议先做一次字段过滤只保留ts、track、artist、album、ms_played、platform、reason_start、reason_end这几个列然后再开始绘图。如果要把分析过程分享到社交平台发布图表之前务必再检查一次图片内容不要把包含 IP 或设备信息的 DataFrame 截图直接发出去。数据是好东西但自己的数据更要自己有意识地保护。6. 一点后续扩展和个人体会这套分析做完之后我最大的感受是平台给你看的“年度报告”只是它想让你看的故事而你自己动手算出来的数据才是真正属于你的记录。用 Python 分析自己的 Spotify 听歌数据本质上不是一次性的小玩具它是一套可以反复运行的流程——每年导出一次跑一遍脚本就能看到当年的音乐行为变化。如果你把它写成函数模块甚至可以做跨年对比看自己的口味迁移轨迹。后续想继续深挖的话有两个方向我强烈推荐。一个是歌词文本分析把排名前 50 的歌曲歌词抓下来做分词、词频和情绪倾向分析看看自己常听的歌里是不是藏着某种特定的审美偏好。还有一个是音频特征分析用spotipy批量拉取valence、energy、danceability这些字段按时间维度聚合画一条“情绪曲线”——你会发现那段时间自己是不是真的状态不太好音乐数据往往比记忆更诚实。最后分享一个做图的小技巧分析脚本里所有图表统一用plt.savefig而不是plt.show这样每次跑完脚本所有图片都会自动存到当前目录方便自己连续对比。我在实际运行中光是把所有图表输出到一个output/文件夹就已经让整个项目的回看体验提升了一大截。
返回列表