news 2026/10/9 12:46:08

用Python分析Spotify播放记录:从数据清洗到可视化全复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python分析Spotify播放记录:从数据清洗到可视化全复盘

大概是从三年前开始重度使用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-dateutil
  • pandas:数据处理的核心,读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文件里到底躺着哪些小秘密。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 12:45:11

HTTP为何能靠TCP“躺赢”?从协议栈分工到工业排障全解析

把Wireshark打开,盯一条HTTP请求的完整生命周期走一遍,你大概率会得出跟我一样的结论:HTTP跑起来实在是太省心了。丢包重传、乱序重组、连接建立与释放、流量与拥塞控制,这些又脏又累的活,统统被TCP协议扛在了自己肩上…

作者头像 李华
网站建设 2026/10/9 12:44:52

VS Code中高效下载Hugging Face数据集:断点续传与镜像加速实操

VS Code里折腾Hugging Face数据集下载,这几招真的很省事 很多朋友第一次接触Hugging Face,都是因为想找一个现成的开源数据集或者模型权重。模型还好说,直接用 snapshot_download 几行代码就拉下来了,数据集反而更绕——有的数据…

作者头像 李华
网站建设 2026/10/9 12:44:52

自定义UDP视频传输中的处理层设计:分片、重传与抖动缓冲实战解析

这活儿我干过不少次了——领导丢来一句“要做一个能在低延时下传视频的模块,网络条件不好也得凑合看”,然后你打开文档一看:不能用TCP,不能上RTSP那套,得自己定UDP协议。自定义UDP协议视频传输,听起来很自由…

作者头像 李华
网站建设 2026/10/9 12:44:17

GMT、UTC、DST、CST辨析:前端时间格式处理与UTC转北京时间实操

1. 时间格式处理为何成为前端开发的隐形雷区刚入行那会儿,我对时间格式的理解基本停留在“能显示就行”的层面。直到有一次,一个活动倒计时页面在测试环境跑得好好的,上线后用户反馈“倒计时少了8小时”,排查了半天才发现是服务端…

作者头像 李华
网站建设 2026/10/9 12:42:07

UltralSO制作Linux启动盘的底层原理与工程实践

1. 为什么现在还要亲手做Linux启动盘?——被低估的底层掌控力“UltralSO软碟通制作Linux系统盘”这个标题,乍看像十年前的老操作,但最近三个月,我在某高校开源实验室带学生做嵌入式开发实训时,连续遇到7个真实案例&…

作者头像 李华
网站建设 2026/10/9 12:42:06

SpringBoot医疗管理系统毕设:核心代码与踩坑全解析

我用 SpringBoot 把医疗管理系统卷成了毕设模板,核心代码和踩坑都在这里每年到了毕业季,总有一批人卡在选题上:既要难度适中能独立完成,又不能太水让答辩老师一眼看穿,还得有实际业务场景可以讲故事。我的建议是&#…

作者头像 李华