如果你和我一样,是个重度音乐流媒体用户,每到年底最期待的事情之一就是看那份年度总结,看看自己这一年到底被哪些歌“操控”了情绪。但问题在于:官方年度报告一年才出一次,而且维度固定,你想看“每天几点钟我听得最凶”“哪段时间我循环同一首歌”“上半年和下半年的口味漂移了多少”,它都不给你。
好消息是,你听过的每一首歌其实都在Spotify的后台留下了完整记录,而且官方允许你自己把数据导出来。拿到这些原始数据之后,用Python清洗、分析、画图,自由度就完全不一样了。你可以做出比官方年度报告更细、更带个人偏好的“听歌年鉴”。这篇文章我会从数据获取讲到可视化,把完整的实操流程走一遍,提供可以直接复用的代码。适合有一定的Python基础、听过pandas和matplotlib的读者,哪怕你是刚入门,照着代码一步步敲,也能把结果跑出来。
1. 先搞清楚一件事:你的听歌数据到底在哪
要分析,先得有数据。很多人卡在第一步:我知道可以导出,但不知道去哪里导出,也不知道导出来之后是什么样子。这里我先把两条主流路径讲明白。
1.1 方案A:官方数据导出——零门槛拿到全量历史
Spotify在账号体系里内置了数据下载功能,入口在账号的隐私设置里面,选项叫“Download your data”。点下去之后平台不会立刻给你文件,而是要先跑一段时间——快的时候一两天,慢的时候三四天。等收到邮件通知后,你会拿到一个压缩包,里面包含一堆JSON文件。其中命名以StreamingHistory开头的文件才是主角,比如StreamingHistory0.json、StreamingHistory1.json等等。
打开任意一个StreamingHistory文件,结构非常简单,就是一个数组,里面每个对象对应一次播放记录:
[ { "endTime": "2024-05-20 22:10:33", "artistName": "某独立乐队", "trackName": "夜行列车", "msPlayed": 215510 }, { "endTime": "2024-05-20 22:18:02", "artistName": "某电子音乐人", "trackName": "午夜信号", "msPlayed": 178203 } ]这些字段虽然少,但信息量很够。endTime表示这首曲目播放结束的时间点,精确到秒;artistName是歌手或乐团名;trackName是曲目名;msPlayed是这首曲目在你设备上停留的毫秒数。把它们拼起来,就是一份完整的听觉日志。
官方导出还有一个明显优势:数据是全量的。只要是你账号名下产生的播放行为,基本都能找到。不论你是三个月前听的还是三年前听的,都会进压缩包。对于想分析长期趋势的人来说,这是最可靠的数据源。
1.2 方案B:Spotify API——适合做实时快照和自动化采集
如果想要更动态的数据,比如每天定时抓一次最近播放记录,或者想读取官方那套“音频特征”(能量、欢快程度、节奏),那就得走API路线。好在生态里已经有非常成熟的第三方包,用spotipy就能把认证和请求封装得比较舒服。
先安装依赖:
pip install spotipy然后到Spotify的开发者后台创建一个应用,拿到一对Client ID和Client Secret,再配置一个本地回调地址。代码大约长这样:
import spotipy from spotipy.oauth2 import SpotifyOAuth sp = spotipy.Spotify( auth_manager=SpotifyOAuth( client_id="你的客户端ID", client_secret="你的客户端密钥", redirect_uri="http://localhost:8888/callback", scope="user-read-recently-played", ) ) results = sp.current_user_recently_played() for item in results["items"]: track = item["track"] print(item["played_at"], track["name"], track["artists"][0]["name"])第一次运行时,脚本会在浏览器里弹出一个授权页,你点击同意后会自动跳到本地回调地址,令牌随后被保存到本地配置里,之后就可以直接拿它查询自己的听歌数据了。
不过这里有一个现实限制:API里的最近播放接口有数量上限,只能拿很短时间窗口内的记录,不适合当作全量历史来分析。它的正确使用场景是:做定时快照,比如每天抓取一次并存入数据库,长期积累后形成你自己的数据仓库。
两种方案怎么选?我建议直接用官方导出做主分析,API作为补充快照工具。如果你只是想快速看懂自己的历史,官方导出就够了,不需要因为“没拿到API密钥”而卡住。我把两条路径的差异整理在下面:
| 对比项 | 官方数据导出 | Spotify API |
|---|---|---|
| 数据范围 | 全量历史播放记录 | 最近播放记录,有数量上限 |
| 获取门槛 | 账号设置里申请即可 | 需要注册开发者应用 |
| 获取速度 | 等待邮件通知 | 授权后实时返回 |
| 数据字段 | 时间、歌手、曲目、播放时长 | 除播放记录外还能拿到歌曲元数据与音频特征 |
| 适合场景 | 长期回顾、年度报告复刻 | 定时采集、构建个性化推荐 |
2. 数据清洗:90%的时间花在这,但特别值得
拿到原始JSON之后,离“能做分析”的干净DataFrame还有一段路。很多初学者容易犯的错是:直接把JSON丢进分析代码里硬跑,然后用一句“数据有问题”重重摔坑里。实际上清洗工作并不复杂,只要做对三件事。
2.1 合并多份流历史文件
官方导出的压缩包里通常不会只有一份StreamingHistory文件。它会按时间段拆成多份,比如StreamingHistory0到StreamingHistory5,每份几万条记录。对分析来说,拆开看没有意义,第一步永远是把它们全部合并成一个表。
用glob配合pandas可以写得很简洁:
import glob import pandas as pd files = sorted(glob.glob("StreamingHistory*.json")) frames = [pd.read_json(path) for path in files] df = pd.concat(frames, ignore_index=True) print(df.shape) print(df.head())这段代码会把所有以StreamingHistory开头的JSON文件都读进来,并按行拼接。合并完记得看一眼df.shape,重度用户一年听歌三四万条非常正常。如果发现行数多到夸张,再用后面的去重逻辑处理,别急着删数据。
现在还没有到想看任何统计的时候,先把数据结构打磨好。注意concat之后索引会乱掉,所以我在代码里用了ignore_index=True,让它重新生成一个干净的整数索引。
2.2 时间与时长字段的标准转化
原始的msPlayed单位是毫秒,对我们来说不够直觉。日常交流中我们只会说“听了3分钟”,不会说“听了180000毫秒”。所以要把毫秒换算成分钟,同时endTime也要从字符串转成真正的datetime格式,这样后面按小时、按周、按月聚合时,pandas才能正常切分:
df["endTime"] = pd.to_datetime(df["endTime"]) df["play_minutes"] = df["msPlayed"] / 60000 df = df.sort_values("endTime").reset_index(drop=True)这一步做完,DataFrame里每一行就是一次语义明确的播放记录。后面不管是画每日变化曲线,还是分析时段偏好,都需要这两个字段作为基础。
顺便说一个细节:不要急着把msPlayed删掉。有些进阶分析会需要毫秒级的精确值,比如判断一首歌是不是被完整听完,这时候毫秒比分钟的粒度更合适。等分析完再删也不迟。
2.3 异常数据过滤与时区陷阱
播放记录里混着大量无效片段:很多人会随手点开一首歌,听几秒就切走;也可能在播放列表里连续跳歌,留下大量短时长的记录。这些记录会污染Top榜,让那些“总是听开头几秒就切歌”的曲目上榜,非常失真。我通常会把播放时长小于30秒的记录先过滤掉:
df = df[df["msPlayed"] >= 30000].copy()这个阈值不是绝对的。你可以根据自己的习惯调成20秒或者40秒。如果想做得更精细,可以按“是否超过歌曲实际时长的30%”来判断,不过那需要额外请求API获取每首歌曲的时长,对小项目来说性价比不高。先用绝对阈值把明显切歌行为滤掉,结果就能看很多。
另外一个非常隐蔽的坑是时区。官方导出文件里记录的时间,在不同地区可能不是同一套口径。有人会遇到导出的endTime其实是UTC时间,和你手机里的本地时间相差好几个小时。如果你画出来的收听时段分布图显示“凌晨三点是收听高峰”,但你印象里自己那时候都在睡觉,那极大概率是时区没换算。检查方法很简单:找一条你记得确切播放时间的记录,对比endTime是否一致;如果差了几个小时,就给整列时间加上固定偏移:
df["endTime"] = df["endTime"] + pd.Timedelta(hours=8)这步一定要做对,尤其是后面画一周热力图的时候,时区不对,整张图的作息结论直接反过来。
3. 核心分析:从四个维度读懂你的听觉习惯
数据清洗完成后,现在可以做正事了。我建议不要一上来就折腾机器学习或者复杂的推荐算法,先从最基础、但最实用的四个维度入手。这四个维度跑通,你对个人听歌情况的了解就已经超过绝大多数只看周报的人了。
3.1 趋势分析:你的听歌总量如何波动
第一个问题最简单也最直接:我每天到底听了多长时间的歌?按月、按周、按日分别统计,能快速看出一年的节奏。
daily = df.groupby(df["endTime"].dt.date)["play_minutes"].sum() daily.plot(figsize=(14, 4), title="每日播放时长(分钟)")画出来之后,原始曲线会非常毛躁,周末和工作日差异可能很大。这时候做一个7天滑动平均,趋势会平滑得多,也更容易看出大阶段的变化:
daily_smooth = daily.rolling(7, min_periods=1).mean() daily_smooth.plot(figsize=(14, 4), title="每日播放时长(7日均值)")rolling窗口选7天的逻辑很简单:音乐收听本来就是一周为周期的事情,周末和工作日听歌场景完全不一样,用7天均值可以把“星期几”的周期感抹掉,露出更真实的波动原因。
3.2 Top榜分析:真正陪你最久的歌手
再来看我最喜欢的一块:歌手榜和歌曲榜。
artist_stats = df.groupby("artistName").agg( play_count=("trackName", "count"), total_minutes=("play_minutes", "sum") ).sort_values("play_count", ascending=False) print(artist_stats.head(20))这里有个数据错觉很多人会遇到:播放次数最多的歌手,不一定是你花了最多时间的歌手。原因在于不同的歌时长差异很大,有的歌只有1分半,有的歌7分钟。同一个歌手,如果它的作品普遍偏长,单曲播放一次相当于别人播三四次。所以我强烈建议你把两列都看一遍,除了按播放次数排,还要按总时长排一次。我自己的数据里就出现过“播放次数第5的歌手,总时长排第1”的情况,因为那段时间我一直在听一些超长的后摇作品。
歌曲榜同理,可以按trackName聚合,加一行排序就能得到:
track_stats = df.groupby("trackName").agg( play_count=("artistName", "count"), total_minutes=("play_minutes", "sum") ).sort_values("total_minutes", ascending=False)再深一步,你甚至可以把每首歌对应的歌手信息一起聚合出来,做一张“歌手-歌曲-播放次数-总时长”的宽表,后面做任何分析都可以直接复用。
3.3 时段画像:你是夜猫子还是清晨派
第三个维度比较有意思:你通常在一天中的哪些时间段听歌。晚上安静时听歌和上班通勤路上听歌,背后是完全不同的场景。
df["hour"] = df["endTime"].dt.hour hour_stats = df.groupby("hour")["play_minutes"].sum() hour_stats.plot(kind="bar", figsize=(10, 4), title="按小时统计的播放时长")粗看这个图,你能直接判断出自己是“夜猫子”还是“晨间派”。晚上十点到凌晨一点之间如果出现一个大峰,说明你经常睡前沉浸音乐;早晨七点到九点如果有峰,说明通勤时间音箱是你的刚需。
但只看小时分布还不够,最好是把“小时”和“星期几”结合起来看。周末凌晨两点和周五凌晨两点的含义完全不同:周五可能是熬夜放松,周末则是正常的晚睡。这时候就进入第4章的:绘制热力图正好能同时展示这两个维度。
3.4 单曲循环检测:找到最上头的歌
还有一些歌曲你会在某几天里反复循环。这种现象在官方盘点里一般被归入“单曲循环”,但官方没告诉你具体哪几天循环最猛。我们可以用滑动窗口检测。
思路是这样的:按时间排好序之后,如果同一首歌在很短的时间窗口内出现多次,就是一个循环片段。我常用的判断是“同一首歌在10分钟内出现至少3次”,条件满足就认定为一次单曲循环事件:
from collections import defaultdict df = df.sort_values("endTime").reset_index(drop=True) loop_counts = [] start = 0 for i in range(len(df)): while df["endTime"].iloc[i] - df["endTime"].iloc[start] > pd.Timedelta(minutes=10): start += 1 window = df.iloc[start:i + 1] track_window = window[window["trackName"] == df["trackName"].iloc[i]] if len(track_window) >= 3: loop_counts.append(df["trackName"].iloc[i]) from collections import Counter print(Counter(loop_counts).most_common(10))这个暴力滑窗写法虽然不优雅,但胜在直白,数据量在几万条以内跑起来完全不慢。优化空间其实也很大,比如窗口用分钟做单位,匹配时加上歌手名判断,避免不同歌手的同名歌曲被误判。
循环检测的价值在于:它能帮你找到那些“只有某段时间特别依赖”的歌,而不是总榜上的长尾歌。我通过了这个逻辑才意识到,自己每年换季的时候都会把同一张后摇专辑翻出来连着听两周,数据是不会说谎的。
4. 数据可视化:把数字变成能发朋友圈的图
分析输出数字还不够直观,要做成图。可视化工具我用的是最常见的组合:matplotlib做基础图表,seaborn做热力图和风格增强。没必要上Tableau或者Plotly,对个人项目来说,这两套库足够把故事讲清楚了。
4.1 一周热力图:5行代码看出你的作息规律
绘图之前,先把“星期几”和“小时”这两列加上:
df["weekday"] = df["endTime"].dt.dayofweek df["hour"] = df["endTime"].dt.hour pivot = df.pivot_table( index="hour", columns="weekday", values="play_minutes", aggfunc="sum", fill_value=0 )然后交给seaborn:
import seaborn as sns import matplotlib.pyplot as plt sns.heatmap(pivot, cmap="YlOrRd", linewidths=0.5) plt.xlabel("星期") plt.ylabel("小时") plt.yticks(rotation=0)出来的图会非常直观:横向是周一到周日,纵向是0到23小时,颜色深浅代表播放时长。颜色密集的区域就是你的听觉活跃带。很多用户看到这张图的第一反应都是“我居然这么有规律”,或者“我周末起得真晚”。
为了让图更容易读懂,我建议把x轴的0到6替换成“周一”到“周日”:
plt.xticks( ticks=range(7), labels=["周一", "周二", "周三", "周四", "周五", "周六", "周日"] )热力图的信息密度极高,一张图就能装下一年份的作息习惯,这是单看数字完全体会不到的。
4.2 歌手偏好变化:追踪音乐品味的漂移
另一个容易出彩的视角,是看“每个月谁是你的头号歌手”。很多人的歌单口味并不是线性的,可能上半年在听民谣,下半年突然转向电子。这种变化用折线图很难看,但按月份算冠军歌手却能讲出故事。
monthly_top = ( df.groupby([df["endTime"].dt.to_period("M"), "artistName"]) .size() .reset_index(name="count") ) monthly_champion = monthly_top.loc[monthly_top.groupby("endTime")["count"].idxmax()] print(monthly_champion)每个月取播放次数最多的那一位,一行就是一幕。排出来之后你会发现自己其实不是一直“只听一个人”,而是在不同阶段极其专一。如果觉得只看月冠军太粗颗粒,可以看每个月Top3歌手组成的榜单,再画成一个“谁长期在榜、谁只是流星”的矩阵。
这个分析不用画太复杂的图,把结果打印成Markdown表格都足够让人感慨。
4.3 中文乱码与图片导出的坑
实操中最容易翻车的就是中文显示。我开始跑图时,所有中文标题和坐标轴全变成了一个又一个小方块,非常浇灭热情。原因很简单:matplotlib默认字体不包含中文字形。在Windows上,我一般先做两行设置:
plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = FalseMac用户可以把SimHei换成PingFang SC,Linux用户换成Noto Sans CJK SC。如果机器上没有合适字体,也可以换一种思路:图表标签全部用英文或拼音,虽然观赏性低一点,但至少不报错。
保存图片时也有讲究。默认的dpi很低,放到网页或者PPT里会糊。建议统一加参数:
plt.savefig("weekly_heatmap.png", dpi=300, bbox_inches="tight")bbox_inches="tight"会把图形边缘的多余空白裁掉,特别是坐标轴标签比较长时,不加就会截断。
5. 进阶玩法:做一些官方报告没有的事
如果基础分析都跑通了,你可能会觉得不过瘾,想更进一步。下面这几个方向我在不同阶段都折腾过,难度可控,产出又有意思。
5.1 给歌曲加“情绪标签”:最不费算力、最耗回忆
官方导出的数据里没有情绪维度,你没法直接看出来这首歌是悲伤还是欢快。但你自己脑子里有。把高频播放的Top100首歌整理出来,给它们手动打上标签,比如“跑步时听”“加班时听”“深夜emo专用”,然后按标签分组统计。
你会得到类似这样的洞察:每天通勤路上听的是A标签,晚上睡前听的是B标签。这些标签完全是你自己的主观判定,没有算法可以替代,也没有人比你更懂为什么那首歌会出现在那个时间点。这不算严谨的数据科学,但却是最贴近个人体验的分析方式。
5.2 基于播放行为的简单聚类
如果你想上手机器学习,又不想一开始就做推荐算法,K-means是个非常好的起步点。特征不用多,就用每位歌手的“播放次数”和“单次平均播放时长”两个维度。
from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler artist_summary = df.groupby("artistName").agg( play_count=("trackName", "count"), avg_duration=("play_minutes", "mean") ).reset_index() features = StandardScaler().fit_transform( artist_summary[["play_count", "avg_duration"]] ) artist_summary["cluster"] = KMeans( n_clusters=4, random_state=42 ).fit_predict(features) artist_summary.groupby("cluster")[["play_count", "avg_duration"]].mean()注意一定要做标准化,否则播放次数会因为数量级过大而直接压过时长维度。聚类出来的簇通常会分成“高频短播”“低频长播”等几种类型,而“低频长播”那类往往就是你的陪伴感最强、单曲循环最集中、一整张专辑从头听到尾的歌手。
5.3 把分析结果沉淀成个人年度报告页面
当你所有脚本都跑通之后,最后一步是让流程自动化。我的习惯是在Jupyter Notebook里把清洗、分析和画图脚本整理成一个按序执行的流程,每年年底导出新数据后,直接重跑一遍,得到一套当年度报告图表。
更进阶一点,可以用Streamlit做一个本地网页。左边放个人基础画像,右边放热力图、Top榜、月度变迁图,所有交互都是实时计算。这个页面不需要部署到公网,本地用浏览器打开看就很爽。想要远程看效果再考虑部署,但一定不要把自己账号的原始数据和密钥带进去。
6. 常见问题与排查技巧实录
最后这部分是用来救命的。我在分析过程中踩过的坑不少,有些问题第一次遇到时真的会卡半小时以上,写出来给大家避雷。
6.1 导出的压缩包里找不到StreamingHistory文件
有人会遇到压缩包里全是AccountData、Inferences等文件,却找不到以StreamingHistory开头的JSON文件。这通常是因为账号选项里没有勾选“播放记录”这类数据范围。回到导出申请页面重新检查选项,或者等新压缩包重新生成。另外,旧账号的数据可能已经过文件保留期,只能导出一部分。
6.2 合并后出现重复记录
这个问题最常见于你多次申请过数据导出。两次下载的数据在时间上有重叠,合并之后出现重复播放记录。解决方法是去重,以endTime、artistName、trackName三列作为联合去重条件:
df = df.drop_duplicates(subset=["endTime", "artistName", "trackName"])严格来说,同一秒内重复播放同一首歌的记录本来就不符合常理,删掉不会损失有效信息。
6.3 播放时长的单位不对
导出的msPlayed字段单位是毫秒,有些人导入数据库后当秒用,算出来一份几百小时的数据还以为自己疯了。换算公式很简单:毫秒除以6000得到分钟?不,是除以60000。是的,毫秒转分钟,要除1000先变成秒,再除60。如果单位搞错,所有趋势图的单位都会失真。建议在清洗阶段就单独建一列play_minutes,并在代码注释里写清楚换算关系,免得隔几天回头自己都看不懂。
6.4 时间戳是UTC,导致高峰时段偏移
分析出来的“收听高峰”与你的实际作息不一致十有八九是时区没换算。个人排查时不要凭感觉,直接挑一首你明确记得播放时间的歌做一个diff测试。确认偏移不难,难的是别把偏移加错方向。如果endTime比你的记忆少了8小时,就是加8小时;反过来则减。这也是为什么我在清洗部分强调要保留一个“我知道这首是哪天听的”样本。
6.5 过滤短播放后,某些真正短的歌被误杀
用“播放时长小于30秒”作为切歌阈值是有缺陷的。有些歌本身就不到一分钟,被你完整听完后也只有40秒,看起来像切歌,但其实是正常播放。更好的办法是结合歌曲实际的完整时长来判断。如果你不想调API,可以先用一个比较低的阈值比如15秒,把明显误触过滤掉,之后再人工检查Top榜里有没有异常短时长歌曲。
6.6 隐私保护这件事一定要放在心里
官方导出压缩包里的信息密度比大多数人想象的高很多。它除了听歌历史,还有搜索记录、设备型号、地理位置,甚至一些基于你行为的推断结果。分析过程中的确需要这些字段来做更精细的画像,但它们也是高度敏感的信息。我的习惯是:原始压缩包不放进代码目录,只解压出需要的StreamingHistory文件,处理过程全部在本地完成,不经云盘不过Git,图表里也绝不出现实时坐标或账号ID。这些数据是你数字生活的一部分,拿来分析自己没问题,慎重保管好是责任。
最后再分享一点真实感受。我第一次跑完这套脚本之后,最意外的不是发现自己听了多少个小时,而是看到某个不起眼的创作歌手在我的收听历史里连续出现了一整个季节。那段时间我自己都没有意识到在靠那几首歌撑着情绪,但数据替我记了下来。也是从那一刻起,我觉得分析数据这件事真正有意思的地方不是“统计了多少”,而是通过统计重新认识了自己的日常习惯。看这篇内容的朋友,如果你也跑出了什么意外发现,或者有自己独家的分析维度,欢迎来评论区分享,我看到之后会挑几种玩法继续深挖。