
很多人以为听歌记录只是手机里一个按月份叠加的数字其实只要把Spotify的播放历史拉出来用Python跑一遍就能看到你的音乐口味、作息规律、情绪周期甚至某个季节的尴尬单曲循环。这篇文章不准备讲大道理直接分享我处理自己几十万条听歌记录的全过程从申请Spotify数据、解析JSON到清洗时间戳、统计歌手热度、做日历热力图最后把结论变成能发朋友圈的图表。内容适合刚入门Python以及已经在用pandas但想找一个真实项目练手的读者。我最早接触这个项目是因为想搞清楚自己到底是不是真的喜欢“后摇”还是只是被氛围感骗了。数据一拉出来答案相当扎心。Python在这个场景里就是最好的工具生态成熟、可视化方便而且整个分析流程可以被拆成很小的模块随时调整。下面我按实际操作顺序写。1. 为什么要把听歌记录拿来做数据分析先聊动机。很多人一年下来会看年度总结但年度总结只告诉你“你听了多少分钟、Top歌手是谁”没告诉你具体哪天凌晨三点还在循环同一首歌也没告诉你周五晚上的曲风跟周一早上的曲风差异有多大。而这些隐藏在播放历史里的信息才是真正值得分析的内容。从技术角度看听歌数据分析是一个麻雀虽小、五脏俱全的真实项目。它包含数据采集API调用或文件导出、数据清洗JSON解析、时间戳转换、去重、多维统计分组聚合、排名、可视化静态图表、交互图表等完整流程而且数据量通常足够大能让你的pandas跑得很有体感又不至于像处理海量日志那样累人。另一个实际价值是自我认知。播放记录比你的日记诚实。分析后你可能会发现自己标榜的“喜欢电子”其实只占5%而某个藏在播放列表里的华语老歌歌手才是真正的大头。这种发现比任何推荐算法都更能帮你认清真实的音乐偏好。所以这个项目很适合Python学习路径中的中期阶段。它不会复杂到劝退但也足够让你在实操中遇到真实数据的各种脏乱差问题逼你去查文档、去调bug、去理解DataFrame的groupby到底怎么用。2. 前期准备账号、环境和数据获取2.1 环境准备Python安装与依赖我用的是Python 3.10但3.8以上的版本就跑得很顺畅。如果你还没装好Python建议直接从官网下载稳定版安装时勾选“Add Python to PATH”避免后面在命令行里找不到python。装完后建议顺手建一个独立的虚拟环境别把依赖直接装进全局。Windows上我习惯这样操作python -m venv spotify_env spotify_env\Scripts\activatemacOS或Linux下激活命令是python -m venv spotify_env source spotify_env/bin/activate接下来安装核心库。数据分析最常用的是pandas、numpy可视化的matplotlib、seaborn、plotly。这些库装起来很简单pip install pandas numpy matplotlib seaborn plotly这里提醒一句如果pip下载慢可以换国内镜像源比如清华源或阿里源这能省下不少时间。我实际使用下来pandas负责数据清洗和聚合seaborn负责画统计图plotly用来做可交互的图表三者配合基本覆盖所有需求。2.2 获取Spotify数据的两条路径获取数据是第一步也是最容易卡住人的环节。Spotify的数据主要有两个来源自助导出文件和官方API。自助导出文件最简单。在Spotify的隐私设置里可以申请下载你的完整数据副本通常几天后会收到邮件里面包含一个或多个JSON文件其中StreamingHistory0.json甚至StreamingHistory_music_*.json记录了你每天的播放历史。每条记录包含时间戳、歌手名、曲目名、播放时长毫秒四个字段。这份数据非常干净不需要鉴权适合初学者直接上手。API方式则能拿到更多维度比如每首歌的音频特征能量、声学度、舞蹈性、专辑信息、曲目时长等。你需要先在Spotify Developer Dashboard创建一个应用拿到Client ID和Client Secret然后通过OAuth授权获取访问令牌。这种方式适合那些导出文件里信息不够用的场景比如你想统计自己的音乐品味在“能量值”维度的分布只靠导出文件做不到。我建议两条路都走。先用导出文件做基础统计再调用API补充歌曲的音频特征。这样既能快速跑通全流程又能练习API调用的能力。2.3 API方式的关键配置如果打算走API路线在Spotify开发者后台创建应用时建议把Redirect URI设置为http://localhost:8888/callback。本地回调不需要公网地址这个细节很多人容易忽略导致授权流程跑不通。获取访问令牌的方式我推荐用spotipy这个库它把授权流程封装得很干净。安装pip install spotipy使用方式里需要把Client ID和Client Secret以环境变量或代码变量的方式提供。我习惯把它们放在项目根目录下的.env文件里然后用os.getenv读取避免把密钥直接暴露在代码中。import os import spotipy from spotipy.oauth2 import SpotifyOAuth client_id os.getenv(SPOTIFY_CLIENT_ID) client_secret os.getenv(SPOTIFY_CLIENT_SECRET) redirect_uri http://localhost:8888/callback scope user-read-recently-played user-top-read sp spotipy.Spotify(auth_managerSpotifyOAuth(client_idclient_id, client_secretclient_secret, redirect_uriredirect_uri, scopescope))请求完成后令牌会被缓存短时间内重复运行不会反复弹浏览器授权。这里需要记住的是Spotify API有请求频率限制如果大批量拉取音频特征必须加上time.sleep(0.05)之类的小延时否则容易收到429状态码。3. 数据加载与清洗的实战细节3.1 读懂Spotify导出的JSON结构拿到导出文件后先用记事本或VS Code打开看一眼结构。流媒体历史文件通常是这样的[ { ts: 2024-05-01T10:23:14Z, username: spotify, platform: android, ms_played: 180000, conn_country: DE, ip_addr_decrypted: null, user_agent_decrypted: null, master_metadata_track_name: Starry Night, master_metadata_album_artist_name: Some Artist, master_metadata_album_album_name: Album Name, spotify_track_uri: spotify:track:xxxxxxxx, episode_name: null, ... } ]注意不同时期导出的JSON结构并不完全一致。以前只有四个字段新版会包含device型号、国家代码、IP地址已经被脱敏处理、音轨URI等。如果你的文件里没有implicit加密信息那说明是简化版。不管哪种核心的几个字段一定要找对ts、ms_played、master_metadata_track_name、master_metadata_album_artist_name。如果导出包里有多个StreamingHistory文件先合并它们import pandas as pd import glob files glob.glob(data/StreamingHistory*.json) df_list [pd.read_json(f) for f in files] df pd.concat(df_list, ignore_indexTrue)把多个文件直接concat即可不需要做别的操作因为表格结构完全一致。3.2 清洗时间字段的核心操作时间戳是所有后续分析的基础。原始ts字段是ISO 8601格式的UTC时间例如2024-05-01T10:23:14Z。如果你直接用这个字符串做分组会发现每天的时间偏移和你的实际作息对不上因为北京时间比UTC早8小时。我习惯先把它转成带时区的时间类型再转换成本地时区。中国用户直接用Asia/Shanghai如果你在其他时区就改成对应的时区名称。df[ts] pd.to_datetime(df[ts], utcTrue) df[local_ts] df[ts].dt.tz_convert(Asia/Shanghai)转换后生成几个常用的时间维度列df[date] df[local_ts].dt.date df[hour] df[local_ts].dt.hour df[weekday] df[local_ts].dt.dayofweek # 0代表周一 df[month] df[local_ts].dt.month这里有一个小坑dt.date会返回一个datetime.date对象后续用pandas分组没问题但如果要画时间序列图建议直接保留local_ts列并以它作为分组键而不是用字符串日期。清洗过程中还有一个容易被忽视的点ms_played的单位是毫秒直接求和得到的总播放时长需要除以1000 * 60 * 60才能变成小时。很多人把单位看错最后统计出的“总时长”会比真实值少三个数量级。3.3 数据合并与异常值处理如果同时用了API补充歌曲时长你需要把播放时长和总曲目时长合并。这里要注意一点播放记录里可能有同一首歌一天播了多遍pandas合并时如果不先聚合会产生大量笛卡尔积导致行数爆炸。我建议先把播放记录按track_uri聚合一次再和API的曲目信息做left join这样能有效避免数据膨胀。下面是一个简单示例# 按歌曲URI聚合播放次数和总时长 play_counts df.groupby(spotify_track_uri).agg( play_count(spotify_track_uri, count), total_ms(ms_played, sum) ).reset_index() # 左边播放记录右边API获取的曲目信息 merged play_counts.merge(track_info, left_onspotify_track_uri, right_onid, howleft)异常值处理也不能忽略。导出的播放记录里偶尔会出现ms_played小于零、或者超过曲目时长的情况。前者可能是数据异常直接过滤掉后者通常是播放时循环或重复导致的字段错位不必太纠结直接求和也影响不大。我还会额外删除时长过短的记录比如小于5000毫秒的记录。原因是用户经常在几秒内切歌这些记录不利于分析“真正听完了什么”。4. 榜单、时段与口味偏好分析4.1 最常听的Top歌手和歌曲基础榜单是第一个必须做的分析。用pandas的groupby和value_counts就能轻松搞定。最常听的歌曲按播放次数算top_songs df.groupby(track_name)[ms_played].agg([count, sum]).reset_index() top_songs[hours] top_songs[sum] / 3600000 top_songs top_songs.sort_values(count, ascendingFalse).head(15)最常听的歌手注意不同歌曲可能有不同歌手所以按歌手分组top_artists df.groupby(artist_name).agg( play_count(track_name, count), total_hours(ms_played, lambda x: x.sum() / 3600000) ).reset_index().sort_values(total_hours, ascendingFalse)光看播放次数还不够我建议把“总播放时长”和“平均每次播放时长”放在一起看。有的歌手你播放次数不多但每次都能完整听完说明忠诚度很高有的歌手播放次数很多但平均时长只有30秒说明你经常拿他试听。一个真实例子某个周末我本来以为自己是“古典音乐爱好者”但数据告诉我古典曲目播放次数合计不到10次反而不小心循环了一首洗脑的日语歌曲几十遍。这类结论只能靠数据说出来感觉根本不靠谱。4.2 一天中的听歌节奏分析听歌时间段分析是我最喜欢的一个维度。把一天的24小时作为横轴统计每个小时的播放次数和播放总时长你能看到自己一天的精神曲线。hourly df.groupby(hour).agg( play_count(track_name, count), total_minutes(ms_played, lambda x: x.sum() / 60000) ).reset_index()实际画图后大多数人会看到典型的“双峰”或“三峰”结构早高峰通勤一次下午工作间隙一次晚上睡前一次。我的数据里最明显的是凌晨1点附近有个异常高峰仔细翻记录原来是连续两周失眠循环同一张专辑。这种信息比“每天听歌6小时”那种总结有意思得多。还可以把星期几与小时结合起来做透视表看看工作日和周末的差异。星期的分组可以在清洗阶段用dayofweek生成但要注意0-6分别代表周一到周日使用df[weekday].map({0:周一, ...})映射成人话。4.3 收听时长与播放完整度播放完整度是指“实际播放时长除以歌曲总时长”。这个指标可以检验你对一首歌的真实态度。如果一首歌被完整播放了90%以上说明你是真的喜欢如果每次只播放30%就切走说明它只是歌单里的过客。计算方式df[play_ratio] df[ms_played] / df[track_duration_ms] df[complete_play] df[play_ratio] 0.8再按歌手统计完整播放比例能看出哪些歌手是你的“耐听型”哪些是“试听型”。这个维度用API数据才能算因为导出文件里没有歌曲总时长。我还喜欢计算每首歌的“弃听率”。如果某首歌开始几秒就被跳过大概率是前奏太平淡或副歌太慢热。虽然无法直接拿到客户端交互数据但通过播放时长分布能间接推测。5. 可视化把数据变成一眼看懂的图5.1 Matplotlib与Seaborn快速上手可视化是数据分析的终点也是最容易出效果的部分。我最常用Seaborn它在Matplotlib基础之上封装了更美观的样式和统计图。如果你对中文显示不熟悉先设置一下字体否则图上会全是方块。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] # Windows plt.rcParams[axes.unicode_minus] False在macOS上可能用Arial Unicode MS或PingFang SC这个要根据系统自带的字体调整。设置完后再画Top10歌手的横向条形图sns.barplot(datatop_artists.head(10), xtotal_hours, yartist_name) plt.xlabel(播放小时数) plt.tight_layout() plt.show()5.2 时间序列与日历热力图按天统计播放时长能画出一个整体走势折线图看自己有没有因为某段生活变化而产生音乐消费波动。daily_play df.groupby(date).agg(total_hours(ms_played, lambda x: x.sum() / 3600000)).reset_index() plt.figure(figsize(14, 5)) sns.lineplot(datadaily_play, xdate, ytotal_hours) plt.xticks(rotation45) plt.show()日历热力图是我认为整个项目里最值得安利的部分。它用一格代表一天颜色深浅代表播放时长能直观看到自己一周七天的节奏。Seaborn没有内置的日历热力图我直接用Matplotlib的imshow或者网格散点图来画。如果你不想自己造轮子可以借助calmap库实际上它就是基于Matplotlib的日历级热力图库。如果不想引入新库可以用简单的pivot_table生成“月份-星期”热力图。例如df[month] df[local_ts].dt.month df[weekday] df[local_ts].dt.dayofweek pivot df.pivot_table(indexweekday, columnsmonth, valuesms_played, aggfuncsum) / 3600000 sns.heatmap(pivot, annotTrue, fmt.1f, cmapYlOrRd)这种图能告诉你在每个月哪几天听歌最多比如月初和月底往往有差异。5.3 用Plotly做交互式图表静态图适合放进报告里但交互式图表更适合自己探索数据。Plotly做得很好几行代码就能产出可缩放、可悬停、可筛选的HTML页面。import plotly.express as px fig px.scatter(df.sample(5000, random_state42), xhour, yms_played, colorartist_name, title播放时长与时间关系) fig.write_html(explore.html)我经常把交互图分享给同样做数据分析的朋友他们可以拖动滑块从不同角度去观察。Plotly的响应式体验比Matplotlib高一个档次使用频率并不高的话内存开销也可以接受。还有一个很有意思的交互图用px.bar画出所有歌手的播放次数堆叠图再把鼠标悬停到具体曲目上看自己有没有意外地听了很多冷门歌曲。如果数据量特别大建议先随机采样或者按歌手Top20过滤否则HTML会变得很大浏览器会卡。6. 常见坑与排查思路6.1 时区和时间戳的坑时区错误是这个项目里最常见的问题。如果不把UTC时间转换成上海时间你的凌晨1点会是前一天的17点整个时段分析全部错位。另一个坑是pd.to_datetime默认会猜测时间格式如果遇到极端情况比如2024-05-01T10:23:14.123Z带毫秒需要显式指定格式或让它自动解析通常没问题。把时间戳转成本地时区后一定不要再用字符串切片的方式提取小时否则时区信息会丢失。正确的路径是先转datetime64[ns, tz]再调用.dt.hour。6.2 数据量太大时的处理Spotify导出文件的播放记录可能超过十万条加上API补充的音频特征字段后内存压力会显著上升。处理策略主要有三种分批读取、只保留必要字段、做预聚合。如果使用pd.read_json一次读入太大可以设置chunksize分块读取但JSON的结构不是简单的表格chunksize对JSON行格式比较友好对标准数组格式无效。所以我更推荐一次性读入后及时删掉无用列比如username、ip_addr_decrypted、user_agent_decrypted这些与播放行为分析无关的字段。df df[[ts, track_name, artist_name, ms_played, spotify_track_uri]]如果只是做Top统计我们并不需要聚合前的明细可以先做groupby后再合并外部信息这样数据集从几万行缩到几千行后面各种操作都快得多。6.3 请求频率限制与缓存策略使用API获取音频特征时最烦的是限流。Spotify Web API的官方限制大约每30秒20个请求实际体验中如果连续执行很容易在几十个请求后收到429状态码。一个最基础的应对方式是在请求循环里加延迟import time for track_id in track_ids: try: features sp.audio_features(track_id) # 保存到本地 except Exception: time.sleep(1) time.sleep(0.6)更好的方案是把已获取的结果缓存到本地JSON或SQLite用URI作为键。下次运行代码时先查本地缓存只有没有命中的才重新请求API。这样重复实验时几乎不需要再请求既省时间又避免被封。我在第一次写这个项目时没有做缓存结果因为重复实验半小时内请求了好几次相同的曲目信息后来学乖了所有API请求都先走本地缓存。这个习惯后来在其他API项目里也帮了大忙。7. 从数据到结论一些值得关注的隐藏指标除了Top歌手和时段节奏这个项目还能挖出很多有意思的隐藏指标。比如连续播放指标统计一次完整听歌队列有没有连续不断超过30分钟这是判断“沉浸式听歌”的重要信号。你可以比较一下自己“认真听歌”和“当背景音”的时长比例。再比如播放“翻牌率”指某位歌手歌单里听过一次后马上找下一首同歌手歌曲的概率。翻牌率高说明你对该歌手的整体接受度很高。如果翻牌率低基本说明你只是被某首单曲洗脑了。我们还可以利用Spotify API提供的音频特征计算个人“能量偏好”和“心情偏好”。比如把歌曲按“valence”积极程度分组统计你在不同情绪下的听歌倾向。我分析后发现自己晚上最容易点开高能量歌曲这跟很多人以为的“晚上听舒缓音乐”完全不同最后结论是晚上健身的影响远远大于心情调节。这些隐藏指标的共同特点是单看某一天毫无意义但拉长到几个月甚至几年的播放历史之后稳定重复的行为模式就会被体现出来。这正是数据分析的价值所在。8. 最后的几个小建议这个项目越早做越好最好别等Spotify给你发年度总结才想起分析。主动申请完整数据导出通常间隔一段时间会有新的播放记录你可以写一个定时脚本每个月拉一次最新记录积累半年后再看趋势变化很明显。代码组织建议分成几个模块load.py负责读JSON和清洗stats.py负责统计visual.py负责出图main.py串联全流程。这样稍微重构一下以后换一个数据源也能复用大部分代码。我当时因为图方便全写在一个脚本里后来数据格式更新了改起来非常痛苦。最后想说的是分析听歌数据的意义不只在“用数据了解自己”还在于让你真正理解一个完整的数据分析项目是怎么串起来的。从原始数据到可视化报告中间每一个环节都有数不清的小决策数据清洗不是无趣的体力活它决定了你的结论靠不靠谱。碰到时间戳混乱、数据字段缺失、图表中文乱码这些问题解决过程本身就是编程能力最好的训练。