尧图精选

用Python挖掘Spotify听歌数据:从JSON清洗到用户行为可视化

🕒 发布时间:2026/10/1 18:00:06 📁 来源:尧图网络
Spotify的年度总结每年都能刷屏朋友圈但我一直不满足于官方给的那几页年度歌单。官方总结像是电影预告片告诉你Top 3歌曲、Top 5歌手可真正有意思的细节全被藏起来了你深夜两点还在循环哪首歌、周五下午是不是更容易切歌、过去一整年你是清晨学习型听众还是深夜emo型听众——这些只有原始数据能回答。直到我试了一次Spotify的Download your data功能拿到从注册第一天开始到现在的完整听歌记录配合Python做数据分析才真正看清了自己的听歌偏好。这篇文章完整记录我的实操过程从导出数据、清洗JSON到用pandas统计排行、用matplotlib画热力图和词云再到进阶的切歌行为分析。如果你既用Spotify又对Python数据分析感兴趣这篇内容可以直接照抄作业里面的坑我都替你踩过了。1. 从Spotify导出听歌数据申请入口和文件包结构1.1 为什么Spotify允许你下载这些数据很多人不知道Spotify在隐私设置里藏了一个数据下载入口。这个功能不是商业妥协而是数据保护法规赋予用户的数据可携带权——你有权拿回平台收集的关于你自己的原始数据。对普通用户来说它的意义在于官方年度总结是加工过的成品,而数据下载给的是原材料。我做数据分析这几年的体会是拿现成报表做分析总有种隔靴搔痒的感觉。你想交叉对比任何维度都得看平台脸色比如我周几晚上听歌最多我对哪个歌手的忠诚度下降最快官方根本不提供这种视图。拿到原始播放记录后所有问题都可以自己问、自己答。这就是我认为每个重度用户都值得导一次数据的原因。1.2 申请导出的完整步骤整个申请流程不算复杂但有几个细节容易卡住我把完整路径走一遍用电脑浏览器打开Spotify官网登录你的账号。点击右上角头像进入Account页面。在账户页面里找Privacy Settings隐私设置入口通常在Account overview下方。滚动到Download your data区域点击按钮。这时会出现一个表单勾选你想导出的数据类型。为了分析听歌数据重点是Streaming History播放历史建议直接选Extended streaming history它包含从你注册以来的所有播放记录而不只是最近一年的。点击提交后Spotify会在后台准备你的数据。注意这个准备时间不是几分钟短则一两天长则一周我自己的数据包隔了大概三天。准备完成后Spotify会往你的注册邮箱发一封邮件里面有一个下载链接。链接有效期有限如果过期就要重新申请而且重新申请会重置同一个队列所以在邮箱里看到邮件后建议尽快下载。有一点要提醒整个申请流程只能用网页版客户端和手机App里找不到这个入口。我在手机上翻了半天设置最后才意识到得切到网页操作。1.3 解压后你会看到什么数据包目录拆解下载下来的是一个ZIP压缩包解压后有多个文件夹和JSON文件。我第一次解压时有点懵文件太多了逐一列一下我的实测观察路径内容是否影响本次分析AccountData/账号基本资料包括个人设置、设备信息不影响AudioData/音频播放相关的附加数据影响较小Identifiers/账号标识符等不影响Payments/付费记录、账单历史不影响Playlist/你创建和收藏的歌单可辅助分析SearchQueries/搜索历史可做兴趣词分析StreamingHistory/核心的播放历史JSON文件本次主力UserData/用户偏好和设置影响较小播放历史的核心文件是StreamingHistory/目录下的多个JSON文件命名类似StreamingHistory0.json、StreamingHistory1.json。数据量越大分片越多我拿到三个文件。打开后每个条目包含固定的几个字段artistName艺术家、trackName曲目名、endTime播放结束时间、msPlayed播放时长单位毫秒。这些JSON没有嵌套结构是非常标准的平铺记录读起来很像日志。但恰恰因为平铺才需要我们做大量清洗和变形——这正是Python数据分析擅长的事情。2. 数据清洗把几百天的听歌记录变成规整的DataFrame2.1 读取JSON与时间戳转换拿到原始数据的第一件事不是写统计而是先把所有JSON文件读进来、合并成一个DataFrame。这里有个日常容易忽略的点三四个JSON文件分开读不算大问题但如果你以后换了其他平台的数据导出可能会出现几十个分片文件靠手动load会累死。所以建议直接写个glob循环。import glob import pandas as pd # 读取StreamingHistory目录下所有的json文件 files glob.glob(Spotify Data/StreamingHistory/*.json) print(f共找到 {len(files)} 个文件) df_list [] for f in files: chunk pd.read_json(f) df_list.append(chunk) df pd.concat(df_list, ignore_indexTrue) print(df.shape) # 查看总行数 print(df.head())读进来之后你会发现endTime字段看起来是个普通字符串比如2023-05-14 08:32。但它其实不是普通文本得先转成可运算的datetime类型才能做时间维度分析。这里有一个我踩过的坑Spotify导出的endTime记录的是UTC时间不是你的本地时间。如果你直接按字符串解析会发现你深夜听的歌全部变成了第二天上午整个时间分析都会错位。df[end_time] pd.to_datetime(df[endTime]) df[local_time] df[end_time].dt.tz_localize(UTC).dt.tz_convert(Asia/Shanghai)如果你不确定自己应该用哪个时区最简单的校验方法找一首你清楚记得播放时刻的歌对比转换后的小时数是否符合记忆。不要盲信默认设置这是我处理所有带时间戳数据时的习惯。2.2 去重、排序与缺失值处理读进来的原始数据不会太干净。我遇到几类问题逐一说明处理方式第一msPlayed为0的行。这种记录通常表示播放被立即打断比如误触播放或者用户手动跳过。如果一行记录显示播放了0毫秒它对你的实际听歌时长统计会带来干扰建议直接过滤。第二trackName或artistName为空的行。理论上Spotify不会导出一条没有歌名的记录但我在自己的数据里确实看到过空白。这种行留着也没意义直接去掉。第三重复行。合并多个分片文件时如果你的数据下载过程交叉了多次有可能出现同一时间点的重复记录。处理方案是直接按endTime和trackName组合去重。# 过滤掉播放时长为0的记录 df df[df[msPlayed] 0].copy() # 去掉关键字段为空的行 df df.dropna(subset[trackName, artistName]) # 按播放结束时间升序排序 df df.sort_values(local_time).reset_index(dropTrue) # 如果发现有完全相同时间、相同歌曲的记录保留第一条 df df.drop_duplicates(subset[endTime, trackName, artistName])这里我需要说明一个取舍逻辑dropna看起来简单粗暴但数据分析的核心思路是先保证数据质量再优化统计口径。一条歌名都没有的记录后面做任何文本分析都会变成噪声删掉是合理的。2.3 衍生字段把时间维度拆细清洗完之后DataFrame还是只有原始的四个字段。要做周几最常听歌几点钟情绪最high这类分析需要从local_time里拆出更多的衍生字段# 衍生时间特征字段 df[year] df[local_time].dt.year df[month] df[local_time].dt.month df[day] df[local_time].dt.day df[weekday] df[local_time].dt.dayofweek # 0周一, 6周日 df[hour] df[local_time].dt.hour df[is_weekend] df[weekday].isin([5, 6]).astype(int) df[play_minutes] df[msPlayed] / 60000 # 转换成分钟 df[play_hours] df[msPlayed] / 3600000 # 转换成小时 # 保存一份清洗后的数据后续分析直接读取不用重新读JSON df.to_parquet(spotify_clean.parquet, indexFalse)把清洗结果保存为parquet或csv是我强烈推荐的一步。几个小时的JSON解析只做一次之后每次重启分析会话都可以秒级加载。我见过不少人每次打开Notebook都重新跑一遍整个清洗流程纯属浪费时间。3. 播放历史的三维拆解总量、时间分布与Top榜3.1 先抓住总量播放次数、总时长与日均听歌时长拿到规整的DataFrame后我习惯先做一轮宏观统计心里有个大数再往下钻取。这里的重点不在于跑多少高级算法而在于用合适的统计口径回答问题。# 宏观统计 total_plays len(df) total_hours df[play_hours].sum() active_days df[local_time].dt.date.nunique() avg_daily_plays total_plays / active_days print(f总播放次数: {total_plays}) print(f总播放时长: {total_hours:.1f} 小时) print(f活跃听歌天数: {active_days} 天) print(f日均播放次数: {avg_daily_plays:.1f} 次) # 播放时长分布中位数比均值更能反映典型状态 print(df[msPlayed].describe())这里我特别想强调中位数和均值的区别。播放时长数据天生是右偏的你可能完整听完十分钟的长篇播客也可能两秒钟就切掉一首不喜欢的歌。平均值会被那些长曲目拉高中位数才能反映一半时候你的播放时长是多少。我的真实数据里平均播放时长和中位数差了将近一倍这说明我的播放行为里存在大量短播放和少数长播放的极端对比。理解这个分布再去看排行榜就不会被个别长曲目误导。3.2 Top歌曲和Top艺术家用value_counts一分钟搞定pandas的value_counts在处理计数类问题时非常好用但很多人不知道配套的groupby操作可以按不同口径做同一件事。# Top 10 歌曲 top_tracks df.groupby([artistName, trackName])[msPlayed].agg([count, sum]).reset_index() top_tracks.columns [artist, track, plays, total_ms] top_tracks[total_minutes] top_tracks[total_ms] / 60000 top_tracks top_tracks.sort_values(plays, ascendingFalse) print(top_tracks.head(10))只看播放次数会有个偏差它不区分你完整听完还是听了几秒。同一首歌你播放五十次但每次都是三秒切走和播放十次但每次都听完整哪个才算你真正喜欢我两个指标都会看所以同时保留播放次数和累计播放时长。# 按累计播放时长排序找出真正陪伴最久的歌 top_by_duration top_tracks.sort_values(total_minutes, ascendingFalse) print(top_by_duration.head(10))艺术家维度的分析同理。把artistName单独拎出来做统计可以回答一个我一直好奇的问题我听歌到底是粉人还是粉歌如果我Top 10歌曲里出现八个不同歌手说明我是跟随单曲的听众如果我Top 10歌曲集中在两三位歌手身上说明我不折不扣是人粉。3.3 逐步缩小时间窗口按月看趋势、按年看变化宏观统计做完我习惯把时间窗口逐步缩小。第一步按月聚合看自己每个月的听歌总量波动# 按月统计播放时长 monthly df.groupby([df[local_time].dt.to_period(M)])[play_hours].sum() print(monthly)这种趋势图的价值在于发现异常月份。比如考试月听歌总量骤降旅行季听歌时间骤升这些都是数据自己告诉你的故事不需要额外解释。第二步是把星期和小时交叉起来。这一步是后面的热力图的基础我会在可视化章节详细展开。这里先说结论当你准备做任何时段×星期分析时最好的数据形态不是长表而是透视表。# 透视表行为星期、列为小时、值为播放次数 pivot df.pivot_table(indexweekday, columnshour, valuestrackName, aggfunccount, fill_value0) print(pivot)pivot_table会把每个星期的数字转换成数值矩阵方便后续画热力图。注意aggfunc选了count因为我们要统计播放次数而不是时长。想统计不同口径就改aggfunc这种灵活性是Excel透视表给不了的。4. 用图表把听歌习惯讲清楚matplotlib可视化实战4.1 每周×每小时的听歌热力图听歌行为在不同时间段的分布用文字描述总是苍白无力我周二早上听得少、周三晚上听得多……——这种描述读者根本不直观。热力图是最合适的图表类型。import matplotlib.pyplot as plt import seaborn as sns # 自定义星期标签 week_labels [周一, 周二, 周三, 周四, 周五, 周六, 周日] plt.figure(figsize(12, 6)) sns.heatmap(pivot, cmapYlGnBu, linewidths0.3) plt.title(Spotify听歌播放次数热力图星期×小时) plt.xlabel(小时) plt.ylabel(星期) plt.yticks(ticksrange(7), labelsweek_labels) plt.show()第一次跑出这张图时我相当惊讶我的收听峰值集中在晚上九点到十一点周日白天有一个补觉后听歌的小高峰这对应着我周末窝在家里收拾房间的习惯。这里有一个可视化细节值得注意如果不设置y轴标签seaborn默认显示的只是0到6的数字。看起来是小事但一旦图表要给别人看或者发到社区清晰的标签直接影响信息传达效果。4.2 Top艺术家横向条形图排序细节和高亮Top艺术家适合用横向条形图展示。横向比纵向更容易读因为艺术家名字通常较长竖排标签会互相挤到。# 取播放次数最多的Top 10艺术家 top_artists df.groupby(artistName)[trackName].count().sort_values(ascendingFalse).head(10) plt.figure(figsize(10, 6)) bars plt.barh(top_artists.index[::-1], top_artists.values[::-1], color#1DB954) plt.title(播放次数Top 10艺术家) plt.xlabel(播放次数) plt.tight_layout() plt.show()我习惯把最大值放在最上面这样看起来更符合阅读习惯。配色方面Spotify自己的品牌色是#1DB954图表里用这个绿色会让整个分析报告更有呼应感这种小细节我很看重。如果想让图表更有信息量可以在Top 10旁边标注中位数播放时长看看你常听的艺术家是常听但每次听不长还是一开就是完整专辑。我自己的结果是某位电子音乐制作人的出现频率最高但平均每次播放不到四十秒说明我经常把他当试听另一位RB歌手的播放次数不是最多但每次播放时长稳定在三分半以上这才是真正的沉浸式收听。两张图合起来才能讲完整的听歌故事。4.3 词云从曲目列表看你的音乐关键词词云虽然不是严格的统计图表但作为探索性可视化很受欢迎尤其适合快速展示曲目名的词汇倾向。from wordcloud import WordCloud import re # 合并所有曲目名为一个字符串 track_text .join(df[trackName].dropna()) # 清洗高频噪声词 noise_words {feat, the, and, remix, version, live} cleaned_words .join([w for w in track_text.split() if w.lower() not in noise_words]) wordcloud WordCloud(width1200, height600, background_colorwhite).generate(cleaned_words) plt.figure(figsize(12, 6)) plt.imshow(wordcloud, interpolationbilinear) plt.axis(off) plt.show()这里有一个窝过坑的点很多歌名带feat.、(feat.)以及各种混音版本标记。如果不做清洗词云里最大的词永远是feat和remix毫无信息量。我第一次跑的时候就是这种效果整个词云中心飘着一个巨大的feat完全没法看。词云对英文曲目支持很好但如果你的列表里有大量中文歌词云默认不会分词中文会连成一整块。简单的替代方案是跳过词云改用曲目名称的字符计数做高频字展示或者用jieba分词后再传进去。不过这个问题对你的曲库结构依赖性很强我只建议按需处理。4.4 图表会讲错的瞬间样本量和归一化可视化做得越多越容易发现一种陷阱直觉上很漂亮的热力图可能只是个别异常值撑起来的。我在做完周度趋势后特意检查了每个小格的样本量。热力图里周五凌晨两点的格子颜色很深会不会就只是某一周周五深夜我通宵写代码、连续刷了三十首歌导致的我回看原始数据确认确实有一次这样的爆发。这不代表整个周五深夜都是我的听歌高峰只是单次事件被放大了。所以做可视化时我强烈建议在图表标题或说明里标清楚样本量。不是每一张图都需要但如果你发现某个规律特别有趣下意识反问一句是不是某一周的数据误导了我然后去验证。这种谨慎比视觉冲击力重要得多。5. 进阶切歌行为、活跃时段与延伸分析5.1 一场跳过率的自查什么时间点我最容易切歌拿到播放记录之后光看排行榜和热力图还不满足我开始琢磨一个更接近收听行为的问题我到底什么时候最容易没耐心Spotify导出的数据里没有每首歌的总时长所以我们不知道一首歌被播放了多少比例但我们可以定义一个启发式的跳过规则如果一首歌的msPlayed小于5000毫秒5秒并且这首歌之后同一个分钟内还有另一首新歌开播基本可以认定这是一次主动切歌。5秒不足以听完前奏也不足以听完一段Hook大概率是试了试不喜欢就跑了。# 定义一个跳过行为播放时长少于5秒视为快速切歌 df[is_skip] df[msPlayed] 5000 # 计算各小时的跳过率 skip_rate_by_hour df.groupby(hour).agg( plays(trackName, count), skips(is_skip, sum) ) skip_rate_by_hour[skip_rate] skip_rate_by_hour[skips] / skip_rate_by_hour[plays] print(skip_rate_by_hour.sort_values(skip_rate, ascendingFalse).head(10))我的实测结论很有意思跳过率最高的时段不是在深夜而是周一早上七点到八点这个时段我的跳过率超过40%。原因并不难猜——上班通勤路上我通常使用一个混合了播客和音乐的自动播放列表遇到不想听的曲目就会频繁跳过属于开着当背景音、顺便筛选的状态。真正深夜专心听歌的时候跳过率反而很低。切歌行为分析其实方向性很强如果你想深入下去还可以定义同批次连续跳过事件连续跳过超过三首歌说明你此刻对推荐列表极度不满意这本身是个很强的行为信号。数据允许你这样定义验证起来也不难。5.2 长尾里的宝藏那些只听过一次但听完的歌在排行榜中占据前十的歌曲当然重要但我后来发现更值得挖掘的是长尾部分——那些只播放过一次却完整播放完的歌。它们往往代表偶然遇见然后瞬间击中的时刻是推荐算法每天都在尝试帮忙复现的感受。# 找出只播放过一次且完整听完5分钟以上的歌 df[track_key] df[artistName] - df[trackName] once_played df.groupby(track_key).size().reset_index(namecount) once_played_full once_played[once_played[count] 1] # 从原数据中找回完整播放的时长 merged df.merge(once_played_full, ontrack_key) full_listens merged[merged[play_minutes] 5] print(f长尾中完整播放超过5分钟的歌有 {len(full_listens)} 首)这个列表让我挺惊喜的里面好几首歌现在都成了我的常驻曲目但在当时它们只是从推荐流里随机冒出来的一次相遇。这类分析不需要特别复杂的代码关键是定义清楚长尾和真正听完这两个条件。数据分析很多时候不是靠高级模型而是靠你对业务场景的理解转化成条件。5.3 更进一步对接Spotify API取流派和音频特征本地播放记录只有artistName、trackName、endTime和msPlayed四个字段做艺术家偏好和收听行为分析已经够用但你如果想深入分析我为什么喜欢这些歌就需要补充曲目的元数据。Spotify官方Web API提供了音频特征Audio Features和流派Genres接口。用曲目ID换回每个track的能量值Energy、欢快程度Valence、节奏Tempo等指标是可以和你的播放行为做关联分析的。例如你可以算一算自己在不同时段的音乐情绪画像深夜听的是不是低能量、高valence的歌通勤时是不是偏向高能量实现思路大致是import requests # 给df的曲目匹配Spotify官方Track ID需提前通过Search API匹配 def get_audio_features(track_id, access_token): url fhttps://api.spotify.com/v1/audio-features/{track_id} headers {Authorization: fBearer {access_token}} resp requests.get(url, headersheaders) return resp.json()这一步需要你去Spotify Developer Dashboard注册应用、拿到Client ID和Client Secret然后换取Access Token。流程不难但API有请求频率限制几千首曲目逐一请求要有耐心最好加上限速和容错机制。更关键的是这套API分析本质上是对播放数据的外延增强你要考虑清楚一个成本问题如果你的目的只是回顾自己听歌习惯本地数据的四个字段已经足够支撑百分之八九十的洞察如果你做的是音乐推荐研究或独立项目才值得投入时间去对接API。我个人的建议是先把本地数据玩熟再考虑外延数据。5.4 可复用的框架把这次分析沉淀成自己的工具跑完这一套之后我把散落在Notebook里的代码整理成了几个函数这样以后重新下载数据只需要跑一遍流程。def load_spotify_data(data_dirSpotify Data): 读取并合并所有StreamingHistory文件 files glob.glob(f{data_dir}/StreamingHistory/*.json) df pd.concat([pd.read_json(f) for f in files], ignore_indexTrue) return df def clean_spotify_df(df, tzAsia/Shanghai): 清洗与衍生字段 df df[df[msPlayed] 0].dropna(subset[trackName, artistName]) df[end_time] pd.to_datetime(df[endTime]) df[local_time] df[end_time].dt.tz_localize(UTC).dt.tz_convert(tz) df[hour] df[local_time].dt.hour df[weekday] df[local_time].dt.dayofweek df[play_minutes] df[msPlayed] / 60000 return df这套工具函数不复杂却能节省大量重复劳动。以后每个季度导出一次数据跑一遍就能自动产出全套统计图表。数据分析项目做多了会发现搭建个人分析框架的价值远大于单个分析结论本身。我个人现在每次看到新的数据导出包用的就是这套流程先读JSON清洗存parquet然后从宏观统计到热力图到文本可视化逐层展开。你可以按自己的曲库特点做调整比如增加音轨语言识别、专辑归属对比等但底层的原始数据→时间特征→聚合统计→可视化这条链路是通用的换任何平台、任何时间粒度的数据都能复用。最后分享一个操作小技巧做这类个人数据项目时最好把每个阶段的输出结果单独存一份清洗后的数据、统计后的表格、生成好的图表不要只保留最终图表。因为分析过程中你会反复回看中间结果追查某个异常数据从哪来、哪个日子出了什么事中间数据在手会方便得多。我就是在回查某一天播放量突然飙高时靠着一张中间表才发现那天我循环了二十遍同一首新歌——这种数据小故事才是最值得记录下来的东西。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →