大概是从三年前开始重度使用Spotify,今年初我闲着没事翻了翻后台的导出数据,发现里面躺着两万多次播放记录。当时脑子里冒出来一堆问题:我到底花在音乐上的时间有多少?每天深夜那一个小时在听什么?我的“本命歌手”是真爱还是只是通勤时顺手点开?于是直接写了一轮Python脚本,把这些Spotify听歌数据完整分析了一遍。整个过程不算复杂,但中间踩了不少文档里不会写的坑。这篇文章就是完整的复盘记录,按“抓数——清洗——算指标——画图——避坑”的顺序来,适合两类人看:一是单纯好奇自己听歌习惯的人,二是想找一份真实数据分析项目练手的Python新手。不需要你会爬虫,能装上环境、把代码跑通就够了。
1. 你的Spotify听歌记录里,藏着哪些意想不到的习惯
1.1 为什么我会盯上自己的播放历史
每年年底音乐平台都会给用户生成一份年度报告,看起来信息量很大,实际上只有几个封面数字:总播放时长、前几名歌手、前几名歌曲。可数据一旦到了自己手里,维度完全不一样。
举个例子,我导出数据后发现,周末上午十点的播放量比工作日高出三倍,但周末晚上十点反而比工作日低。这说明我工作日晚上靠音乐提神,周末上午则习惯把音乐当背景音。而年度报告永远不会告诉你这种节奏。
更细的还有“平台分布”:我在电脑上听歌占比其实不到8%,绝大部分播放来自手机,而手机里又分为主动选歌和系统续播。把这些拆开,才算真正“看懂”了自己的听歌行为。
1.2 先列出想回答的问题清单
动手写代码之前,我建议你先花十分钟把问题写下来。我当时列了这样一份清单:
- 总播放次数和总播放时长到底是多少,相当于多少天连续播放
- 哪些歌手/专辑/歌曲占据了我80%的播放时长
- 一天24小时里,我的收听高峰在几点,周内和周末差多少
- 这三年我的音乐偏好有没有发生漂移——从后摇转变到电子,还是越来越杂食
- 有多少次播放是我压根没听完就切走的,切歌行为集中在什么场景
为什么要先列清单?因为数据字段是固定的,但同样一张表,算均值、算分布、算占比,回答的是完全不同的问题。没有清单就去分析,很容易陷入“先把所有图画出来看看”的陷阱,最后产出几十张图却说不清结论。我的习惯是每个问题对应一段独立脚本,这样跑起来清爽,排错也方便。
2. 拿数据的两条路:官方导出与Spotify API,怎么选
2.1 官方导出的完整流程与等待时间
Spotify允许用户从账户后台导出完整的历史数据。打开账户页面,进入“隐私设置”,找到“下载你的数据”相关的入口,申请时会让你选择数据范围,我建议直接勾选最完整的订阅选项,尤其是带“扩展播放记录”的那一类。普通播放列表导出只有歌单信息,真正有用的是扩展流媒体历史,里面记录了每一次播放的精确时间、歌曲名、歌手名、专辑名、播放设备、播放时长等字段。
申请提交之后,官方会发一封邮件到注册邮箱,里面给一个下载链接,链接有效期一般有一周左右。等待时间这个事得看运气,我遇到过最快的半小时,最慢的等了快一周。别反复提交申请,每提交一次都会重新排队,只申请一次是最稳的做法。
下载下来的不是单文件,而是一个压缩包,解压后能看到多个StreamingHistory开头的JSON文件。注意,如果你的数据量比较大,还可能有一个专门存放音频文件的目录,那个我们分析中用不到,不用管。核心就是这些JSON文件里记录的每一笔“播放事件”。
2.2 API方案的适用场景与基本配置
如果觉得官方导出太慢,或者你想分析的是“未来一个月我的收听变化”,那就得用API。Spotify官方提供了一个Python库叫Spotipy,需要在开发者后台注册一个应用,拿到Client ID和Client Secret,再配置一个回调地址。
注册流程很快,填个应用名称和用途就行。拿到凭据后,用Client Credentials模式就可以直接请求那些不需要用户授权的接口,比如按歌手名搜索、查询歌手流派、查询专辑信息和音频特征。但是如果你要拿“当前用户最近播放记录”,就必须走用户授权流程,让用户点一个授权链接,然后拿到访问令牌。
我自己的最终方案是“两条腿走路”:历史分析用官方导出的JSON,因为它覆盖全量、不丢记录;流派和音频特征这类官方导出里没有的数据,用API补充。用导出文件分析全量习惯,用API给歌曲打标签,这个分工在后面的章节里非常有用。
2.3 官方导出字段表:先认识你的数据长什么样
拿到JSON后,我用pandas读进来做了个快速预览,发现每条记录包含的字段比想象中多得多。我整理个表格,方便对照:
| 字段名 | 含义 | 我的用途 |
|---|---|---|
| ts | 播放启动时间(ISO 8601,UTC时区) | 转成本地时间,分析作息规律 |
| ms_played | 这次播放持续了多少毫秒 | 统计有效时长,切歌检测 |
| master_metadata_track_name | 歌曲名 | 歌曲排行 |
| master_metadata_album_artist_name | 歌手名 | 歌手排行 |
| master_metadata_album_album_name | 专辑名 | 专辑维度分析 |
| spotify_track_uri | 歌曲唯一标识 | 去重、补全API信息 |
| reason_start | 播放启动原因(比如点击、自动播放) | 区分主动听歌/被动听歌 |
| reason_end | 播放结束原因(比如播完、手动切歌) | 分析切歌行为 |
| platform | 播放平台 | 设备偏好分析 |
字段里还有两个一眼看上去很另类的,叫ip_addr_decrypted和user_agent_decrypted,翻译过来是IP地址和用户代理。它们本意是方便你回忆“当时在哪个城市听的”,但涉及隐私,我后面会专门讲怎么处理,这里先不展开。
3. 环境准备与数据加载:pandas、plotly与那堆JSON文件
3.1 需要安装哪几个Python库
整个分析依赖的库不多,我实际装的就这几个:
pip install pandas plotly spotipy python-dateutilpandas:数据处理的核心,读JSON、分组聚合、时间序列全靠它plotly:用来画交互式图表,生成HTML文件,可以分享给朋友spotipy:可选安装,只在需要调API补全流派和音频特征时用python-dateutil:处理带时区的ISO时间字符串时更稳
很多教程会让你直接装matplotlib,但我个人更推荐plotly。原因是数据分析过程中,你通常要先探索再下结论,matplotlib画完图得逐张保存查看,而plotly生成的是交互式HTML,鼠标悬停就能看数值,翻图表像翻网页一样方便。探索效率高一个量级。
3.2 把分散在多个JSON里的StreamingHistory读取进来
官方导出的文件往往不止一个,比如我那次解压后分别看到StreamingHistory0.json、StreamingHistory1.json这样命名的一串文件。一次性把所有文件都读进来再拼接,是这个场景最简单的解法:
import json import glob import pandas as pd frame_list = [] for path in glob.glob("./Spotify Extended Streaming History/StreamingHistory*.json"): with open(path, "r", encoding="utf-8") as f: data = json.load(f) frame_list.append(pd.DataFrame(data)) df = pd.concat(frame_list, ignore_index=True) print(df.shape) print(df.head())这里有几个细节值得说。一是encoding="utf-8"必须写,不然遇到某些非英文歌曲名会直接报编码错误。二是glob.glob直接匹配文件名前缀,批量读取不用手动数文件个数。三是ignore_index=True很重要,否则拼接后索引全是重复的,后面groupby虽然不影响,但loc、iloc取数时会很别扭。
读取完成后看一眼df.shape,如果记录数和你心理预期的播放量级差很远,回头检查是否漏了文件名。我见过不止一次新用户只读了StreamingHistory0.json,最后统计结果少了一大截。
3.3 时间戳字段与时区转换:UTC和本地时间的爱恨情仇
这个坑是我第一次分析时踩得最深的。导出数据里的ts字段是ISO格式的UTC时间字符串,比如2024-11-03T12:30:05Z。如果你不去管时区,直接提取小时,那所有时间点都偏了8小时,深夜听的歌会被算到下午,整个作息分析全部错乱。
正确做法是先把字符串转成pandas的时间类型,再转换时区:
df["ts"] = pd.to_datetime(df["ts"]) df["ts_local"] = df["ts"].dt.tz_convert("Asia/Shanghai")注意,tz_convert要求时间对象本身带时区信息,所以一定要用pd.to_datetime先解析成带UTC的datetime,再转换。如果你的Python环境默认时区本来就不是UTC,解析出来的时间可能没带时区后缀,这时候先用tz_localize("UTC")补上时区,再执行tz_convert。
转换之后,后面所有按小时、按星期、按月份的分析,都基于ts_local,本地习惯才不会被时区搞乱。我当时光是修正这个点,就推翻了一半的图表结论。
4. 核心指标怎么算:播放时长、本命歌手与流派偏好
4.1 播了多少分钟:别把切歌误算成完整播放
第一条指标是总播放时长,听起来简单,但ms_played字段存的是“本次播放实际持续了多久”,单位是毫秒,它不是歌曲长度。你可能点击了一首歌,但5秒后就切走了,这条记录的ms_played就是5000左右。如果直接用全部记录求均值,你的平均播放时长会被大量手滑记录拉低。
我的统计口径分两层。先算原始总量,包括所有播放行为:
df["hours_raw"] = df["ms_played"] / 3600000 total_hours = round(df["hours_raw"].sum(), 1) print(f"包含所有播放行为,累计 {total_hours} 小时")再算“有效播放”,也就是至少听了30秒才算一次真正意义上的播放。这个阈值不是官方标准,是我结合实际听歌习惯定的。我自己实测,30秒以下的记录大多是手滑、试听、切歌误触。你可以根据自己的习惯调整,但这个步骤不能省,否则播放次数会虚高一截。
effective_df = df[df["ms_played"] >= 30000].copy() effective_df["hours"] = effective_df["ms_played"] / 3600000我当时算下来,有效播放总时长约312小时,相当于13天不眠不休连续听歌。这个数字比年度总结里报的那个数字低很多,我后来意识到,年度总结算的是“有的播放行为”,而我算的是“认真听的时间”,口径不同,没有对错,但分析时必须二选一并保持统一。
4.2 本命歌手鉴定:播放量、完成率与“遗珠歌手”
有了有效播放数据集,第二个问题就是谁是真正的本命歌手。这里不能用“播放次数”,因为它区分不了“听完整首歌”和“听30秒就切”的差别。按有效时长排,更接近真实偏好:
artist_play = ( effective_df.groupby("master_metadata_album_artist_name")["ms_played"] .sum() .sort_values(ascending=False) .reset_index() ) artist_play.columns = ["artist", "total_ms"] artist_play["hours"] = artist_play["total_ms"] / 3600000 print(artist_play.head(10))这样做完之后有个意外发现:我播放次数最多的歌手,有效总时长只排第三。原因是我经常随机播放他的热门单曲,但每首都听不了多久就切了。反而有一个相对小众的后摇乐队,播放次数只有几十次,但每次都能从头听到尾,有效时长一路爬升到前五。这种歌手我起了个外号叫“遗珠歌手”——它可能不是你的年度报告头牌,却是你真正会安静听完的声音。
为了识别这类歌手,我加了一个“完成率”指标:用ms_played除以歌曲的标准时长。先把每首歌的时长通过spotify_track_uri去API查出来,再做聚合。完成率高的歌手,才是你在不赶时间、愿意沉浸在音乐里的时候会选的。
4.3 流派偏好:把细粒度标签映射成听得懂的大类
官方导出里没有流派字段,需要自己补。最直接的办法是遍历有效播放中出现过的歌手,用Spotipy调API拿艺术家的genres列表:
import spotipy from spotipy.oauth2 import SpotifyClientCredentials sp = spotipy.Spotify( client_credentials_manager=SpotifyClientCredentials( client_id="你的Client_ID", client_secret="你的Client_Secret" ) ) artist_cache = {} def get_artist_genres(artist_name): if artist_name in artist_cache: return artist_cache[artist_name] try: results = sp.search(q=f"artist:{artist_name}", type="artist", limit=1) items = results["artists"]["items"] genres = items[0]["genres"] if items else [] except Exception: genres = [] artist_cache[artist_name] = genres return genres这里强调一点:一定要加artist_cache缓存。否则每个歌手都发一次HTTP请求,几百个歌手就会让脚本慢到怀疑人生。加了缓存之后,整个批量补全秒级完成。
流派拿到手是细粒度的英文标签,比如bedroom pop、deep house、post-rock。这些标签很有信息量,但直接统计会让你看到几十上百种流派,没法得出结论。我做了个简化的映射表,把它们归拢到几个通俗大类:独立摇滚、流行、电子、嘻哈、民谣、古典、其他。归完之后再看占比,你的口味特征就非常清楚了。
5. 时间维度挖习惯:通勤歌单与深夜emo时刻
5.1 按小时切片:我的一天怎么被音乐划分
时间分析是我这次做下来最有乐趣的部分。先把每条记录按本地时间的小时归组,再统计每个小时的播放次数和播放时长:
effective_df["hour"] = effective_df["ts_local"].dt.hour hourly = effective_df.groupby("hour")["hours"].sum().reset_index() hourly["count"] = effective_df.groupby("hour").size().reset_index(drop=True) print(hourly.head())画出来之后,结论非常像“音乐作息心电图”。我的一天有两个明显高峰:早上七点到九点,晚上九点到十一点。早高峰对应通勤地铁,晚高峰对应写代码和睡前放松。最让我意外的是凌晨两点到三点还有一个小突起,回看数据发现,那段时间我在循环播放同一个沉寂类的歌单,明显是失眠。
这些规律普通用户平时根本意识不到,因为听歌动作太“顺手”了。但数据会说话:当你把一年里几千次播放压缩到24小时坐标轴上,你的情绪周期和生活节奏都在里面。
5.2 周内节奏:工作日和周末的听歌差异
同样一张表,换成按星期几切片,又能看到新的结构。我引入了星期字段:
effective_df["weekday"] = effective_df["ts_local"].dt.dayofweek结果,周一到周四的播放量曲线很平稳,周五开始上升,周六到达顶峰,周日晚间又回落到接近工作日的水平。更细节的一个洞察是:工作日午休时段(12点到14点)的听歌量很低,因为午休我基本在补觉。而周末下午14点到17点的播放量突然拉高,那是做家务、看书的时候把音乐当背景音。
如果你愿意再叠一层“主动播放/自动续播”的区分,还能看出周末更多是主动选歌,工作日更多是让推荐算法接管。这两个状态对应完全不同的听歌体验,看得越多越觉得有意思。
5.3 长期口味漂移:用月度数据看浅层迁移
最后我在时间轴上做了一个更宏观的维度:按自然月统计流派占比的变化。流程是取月度、取每个月的流派字段、算各流派的播放时长占比,然后画成堆叠面积图。因为我跨度有三年,所以能清楚看到自己的口味漂移轨迹。
我的数据里非常明显:第一年以独立摇滚和后摇为主,第二年电子音乐占比迅速爬升,第三年嘻哈和流行掺了进来,整体变得杂食。这种变化往往对应生活阶段的变化——换城市、换工作、新认识了朋友。音乐口味不是瞬间改变,而是按月慢慢漂移的,你回头看每一年的Top歌手,发现的旧爱可能连你自己都快忘了。
6. 可视化:那些能直接发朋友圈的图表怎么做
6.1 周时间热力图:一张图看懂全部作息
我特别推荐做一张“星期-小时”热力图:横轴是24小时,纵轴是周一到周日,颜色越深表示播放时长越长。这样一整周的听歌习惯压缩在一张图里,观感很直观,也很容易在社交媒体上引起讨论。
用plotly表达式可以这样画:
import plotly.express as px heat_data = ( effective_df.groupby(["weekday", "hour"], as_index=False)["hours"] .sum() ) fig = px.density_heatmap( heat_data, x="hour", y="weekday", z="hours", color_continuous_scale="Viridis", ) fig.write_html("weekly_heatmap.html")拿到weekly_heatmap.html就可以直接分享。我这边跑完第一版,直观看到周三晚上的色块异常偏大,追查回去发现那天固定要开周会,开完会习惯性用音乐“自我疗愈”,这个洞察是列表视图里根本看不出来的。
6.2 歌手排行、累计播放曲线与榜单效应
第二类图表是歌手排行。我会同时画两张:一张是按有效时长排序的Top15歌手条形图,一张是累计播放时长随时间变化的曲线。后者特别能说明问题,斜率越大说明那段时间听得越猛,斜率变平说明忙碌到没空听歌。
累计曲线直接用cumsum就能实现:
effective_df = effective_df.sort_values("ts_local") effective_df["cum_hours"] = effective_df["hours"].cumsum() fig2 = px.line(effective_df, x="ts_local", y="cum_hours")有一种“榜单效应”也值得一提。我拖动累计曲线的局部区域,发现某张专辑发布的首周,曲线几乎是垂直上升的,听歌时长猛增;两周热度过去,曲线恢复平缓。年轻的时候会觉得这是“热爱”,但数据告诉你,这更像是对新内容的正常消费行为。
6.3 导出交互HTML:从分析到分享的最后一公里
plotly的图表默认就是交互式的,只要用write_html导出,整个页面自带缩放、悬停显示数值的功能,不需要额外部署服务端。这是我最满意的一点,做好了发给朋友,没人需要装浏览器插件,点开就能玩。
分享时有个习惯要养成:图表的标题、悬停文本和坐标轴标签尽量改成中文或通俗的描述。直接扔一个hours上去,朋友看不懂这字段什么意思。我一般会把数据列改名再画,顺手给颜色标度加上单位,这样图表一出来就是成品,而不是“分析过程中间产物”。
7. 踩坑实录:短播放、重复记录与隐私边界
7.1 短播放记录不要无脑过滤,先想清楚你要回答什么问题
前面我提到底线是30秒,但这个过滤并非万能。我第一次跑通分析时,把小于30秒的记录全部删掉,结果整个数据集的播放次数少了近四成,当时觉得“太夸张了”,后来才发现我的播放列表里混着大量电台节目片头,用户语音片段这种本来就短的东西。
所以正确做法是分场景决定是否过滤。分析“有效听歌时长”时可以过滤,分析“我的切歌行为是否频繁”时千万不能过滤——切歌本身就是你行为的核心数据。我后来把原始表和有效表拆成两个DataFrame,一份做全量统计,一份做偏好统计,再也没纠结过。
7.2 重复记录、空歌曲名与播客内容怎么处理
官方导出的JSON基本不会重复记录,但当你用API补全流派时很容易产生重复合并的问题:同一歌手的不同拼写、同一首歌在单曲和专辑里的URI不同,去重必须依据spotify_track_uri,不能依据歌曲名。示例代码:
effective_df = effective_df.drop_duplicates(subset="spotify_track_uri", keep="last")另外,播放记录里可能混入播客(Podcast)剧集。这类记录的master_metadata_track_name字段通常为空,但episode_name字段有值。如果你的分析目标是“音乐偏好”,必须把这些记录剔除,否则一个半小时的播客会被当成一次巨长播放,时长占比严重失真。判断逻辑很简单:master_metadata_track_name.isna()严格脑子说明它不是常规歌曲。
7.3 就算只分析自己,也有些隐私边界不能碰
最后必须泼一盆冷水。官方导出的数据里带有ip_addr_decrypted和user_agent_decrypted这类字段,记录了你每次播放联网时的IP和设备信息。你分析自己的数据时看到这些字段没问题,但如果把处理后的表格、图表、甚至随手分享的截图发到公开平台,这些信息很可能泄露你的地理位置、家庭宽带运营商和常用设备型号。我的建议是:导入原始数据后立刻删掉这两个字段,只留后面分析需要的列:
df = df.drop(columns=["ip_addr_decrypted", "user_agent_decrypted"], errors="ignore")再进一步,所有需要补全流派和音频特征的歌曲标识,只保留spotify_track_uri,不要保留含有个人特征的字段。这是我自己做数据项目时的底线,也是对整个分析习惯的基本尊重。
结实地跑完这一整套,我最大的感受是:技术只解决了一半的问题,另一半问题靠的是你对自己的了解。数据把“我以为我喜欢听什么”和“我实际上在听什么”之间的差距完完整整地暴露了出来。分析完自己的听歌数据之后,我专门建了三个新歌单:一个给工作日早起通勤,一个给深夜安静时段,还有一个专门留给那些完成率特别高的“遗珠歌手”。如果你也准备动手,我建议你把这篇文章读到的代码先在自己导出数据上完整跑一遍,再做任何修改。第一步永远是拿到数据,先看看那些JSON文件里到底躺着哪些小秘密。