用Python分析Spotify听歌记录:从数据清洗到可视化的完整实战

📅 发布时间:2026/9/8 11:40:26
用Python分析Spotify听歌记录:从数据清洗到可视化的完整实战
最近整理个人数据的时候顺手把Spotify的听歌记录导出了一份然后用Python做了个相对完整的分析。说实话这个项目属于那种“工作量不大但收获感很强”的类型适合刚学数据分析的人练手也适合老手快速摸清自己的音乐偏好。你不需要能访问什么内部接口只要会基础的Python和pandas就能跑通。先说结论整个流程分四步——向Spotify申请导出个人数据、拿到JSON格式的播放历史文件、用pandas清洗和统计、再用matplotlib做可视化。每一步都有坑但都不深认真看完这篇基本能一次跑通。下面我从项目设计讲到编码实现再讲我实际跑数据时踩过的几个问题最后补充一点用API扩展分析的玩法。1. 项目拆解先从“听歌数据”里能挖出什么1.1 这个项目为什么值得做很多人以为听歌数据就是“我听了多少分钟”“我最爱的歌手是谁”但实际上Spotify导出的原始数据包含的时间、时长、曲目和歌手信息能组合出非常多维度。比如说你能知道自己在一天中的哪个时段最沉迷音乐周末和工作日的听歌习惯有什么差异单曲循环最多的歌是哪几首甚至通过播放列表的历史记录反推自己口味的变化轨迹。这种分析的意义不在于“看排行榜”而在于把散落在云端的行为痕迹变成可量化的自我观察。对Python学习者来说它又是一个真实、非玩具的数据集包含时间解析、字符串处理、分组聚合、排序筛选这些高频操作比翻教科书上的示例数据有代入感得多。1.2 数据来源对比主动导出和API接口怎么选在做任何分析之前第一件事是搞清楚数据从哪来。Spotify的数据获取有两种主流方案一种是直接在账户设置里申请导出数据文件另一种是通过官方API按需拉取。两者看着都叫“数据”但差别很大。主动导出走的是隐私数据下载流程你需要在Spotify账户页面的隐私设置里找到“Download your data”提交申请后等邮件通知。下载的文件是一堆JSON和HTML的压缩包其中最重要的就是StreamingHistory开头的JSON文件里面记录了每次播放的曲目、歌手和结束时间。这个方案更适合做历史全量分析因为它能把几年前的数据都给你。API方案则灵活能实时查询当前播放的曲目、获取歌单详情、拉取音频特征数据但它需要注册开发者应用拿到client_id和client_secret而且默认配额也只覆盖用户授权范围内的数据历史播放记录并不能完整回溯。我的建议是做历史回顾用导出文件做实时扩展用API两者不冲突。1.3 整体分析流程设计我把整个项目拆成了五个阶段这样写代码和排查问题都清晰阶段一数据获取申请导出并解压确认文件结构。阶段二数据导入用Python读取JSON拼接成DataFrame。阶段三数据清洗处理时间字段、空值、异常时长。阶段四统计分析按小时、星期、月份、歌手、曲目分组。阶段五可视化生成排行榜和趋势图输出结论。这五个阶段不是串行的实际情况中经常要回头改。比如我清洗完发现时间列变成了带时区的对象导致分组统计的结果差了8个小时又得折回去重新处理时间字段。所以你写代码的时候不必追求一次完美先把流程跑通再逐步优化反而高效。2. 环境准备与数据导入把数据格式搞清楚2.1 Python环境与依赖库这个项目需要Python 3.8以上版本核心依赖只有三个pandas负责数据处理matplotlib负责画图numpy在个别计算里偶尔会用到。如果你还没安装命令行里直接执行pip install pandas matplotlib numpy如果你用的是Anaconda这三个库默认就有连安装都省了。我在Windows环境跑通后又在Linux服务器上跑了一次代码不需要改动说明跨平台没问题。建议你直接在项目目录下建一个虚拟环境避免依赖冲突。Python虚拟环境的管理工具我用的是venv够用不需要上conda那些重工具。提示虚拟环境不是可选项。我见过很多人在全局环境里装包结果某个库的版本升级把其他项目弄崩了。用venv隔离后这个分析项目的依赖就锁死了不会波及你别的项目。2.2 StreamingHistory结构的详细解读Spotify导出的压缩包解压后常见的文件夹叫MyData里面会有多个文件。我只关注StreamingHistory它通常按照数据量拆成多个文件命名规律是StreamingHistory0.json、StreamingHistory1.json以此类推。每个文件内部是一个JSON数组数组里每个对象代表一次播放记录字段结构固定如下[ { endTime: 2024-11-01 18:30, artistName: 钢琴曲收藏家, trackName: River Flows In You, msPlayed: 172000 }, { endTime: 2024-11-01 18:33, artistName: Yiruma, trackName: River Flows In You, msPlayed: 92000 } ]四个字段的含义非常直白endTime是这首歌播放结束的本地时间精确到分钟artistName是歌手名trackName是曲目名msPlayed是实际播放的毫秒数。注意它记录的是“结束时间”而不是“开始时间”这会影响你后续对时段的分析口径。我在第一次做小时分布图时就踩了这个坑后面会专门说。还有一点值得注意msPlayed并不等于歌曲总长度而是Spotify实际计算到的播放时长。用户手动切歌、断网重连、跳过前奏都会产生各种不一致的播放时长。这也意味着你可以在分析时对时长做很多文章比如判断哪些歌是播完的哪些是秒切的。2.3 数据加载与合并的完整代码拿到文件之后读取的逻辑很简单但要注意路径别写死最好让脚本自动扫描目录下的所有StreamingHistory文件import pandas as pd import json import glob # 扫描当前目录下所有StreamingHistory文件 files glob.glob(StreamingHistory*.json) print(f找到 {len(files)} 个播放历史文件) # 逐个读取并合并 all_dfs [] for f in files: with open(f, r, encodingutf-8) as fp: data json.load(fp) df pd.DataFrame(data) all_dfs.append(df) print(f{f}: {len(df)} 条记录) df pd.concat(all_dfs, ignore_indexTrue) print(f合并后总记录数: {len(df)})这段代码里有个小细节encodingutf-8必须显式指定不然在Windows中文环境下容易触发UnicodeDecodeError。还有拼接DataFrame时设了ignore_indexTrue这样行号会连续递增后面做过滤或去重会更方便。读完之后建议先看一眼数据长什么样确认列名没有因为版本更新而变化print(df.head()) print(df.dtypes)如果一切正常你会看到endTime是object类型、artistName和trackName是object类型、msPlayed是int64类型。这个类型分布很关键object代表它是Python字符串int64代表它是整数后续所有处理都基于这个认知展开。3. 数据清洗与核心统计让零散记录变成可读指标3.1 时间字段处理与时区注意项时间处理是整个项目里最容易出错、也最影响后续分析的环节。Spotify导出的endTime字符串格式是“YYYY-MM-DD HH:MM”没有秒也没有时区信息。我的建议是先把字符串转成pandas的datetime类型这样后面按小时、星期、月份分组非常方便df[endTime] pd.to_datetime(df[endTime])这里有一个隐藏问题endTime到底记录的是哪个时区从我的实际数据来看它用的是你账号当时所在位置的本地时间。如果你长期在同一个时区那就没什么影响如果你经常跨国旅行或开了代理节点那么小时分布图里会出现明显的“幽灵时段”。这种情况没有完美的修正方案因为你无法从导出文件里反推出每个时刻的真实偏移量。我的处理办法是假设绝大多数记录都来自常住时区这个假设在统计框架下是可接受的。时间列转成datetime之后我习惯同时生成几个衍生列后面能少写很多重复代码df[date] df[endTime].dt.date # 日期 df[hour] df[endTime].dt.hour # 小时 df[weekday] df[endTime].dt.dayofweek # 星期几0周一 df[month] df[endTime].dt.to_period(M) # 月份 df[duration_minutes] df[msPlayed] / 60000 # 播放时长单位分钟这里最简单也最实用的衍生列就是duration_minutes。因为msPlayed动辄十几万肉眼完全没法读比如172000毫秒你心算要反应几秒但转成2.87分钟就一目了然了。后面所有“播放时长”的分析我都用这个派生列。3.2 播放时长过滤先把噪音去掉真实数据里很大一部分记录的msPlayed都非常小。比如几秒钟就切歌了或者不小心误触播放了三秒就暂停。这些数据如果不过滤会严重干扰你对歌曲真实热度的判断——它计数了但并没有实际的收听价值。我的过滤标准是只保留播放时长大于等于30秒的记录。为什么选30秒而不是10秒因为多数平台的“播放一次”标准是30秒你用这个阈值算出的播放次数和平台官方统计口径能对上。当然你也可以用60秒看你想分析什么。如果你关心的是“哪些歌我连30秒都撑不过”那这堆短时长记录本身就是很好的分析素材。df_valid df[df[duration_minutes] 0.5].copy()过滤之后数据量通常会有10%到20%的缩减。这种“丢弃”不是浪费而是让后续分析更聚焦。同时我建议把原始df保留在内存里别急着覆盖因为后面做对比分析时可能还会用到。3.3 核心指标计算总时长、歌手画像、曲目热度处理完数据第一个要算的指标是“总播放时长”。这个值直观适合用来描述个人音乐消费的总体水平total_hours df_valid[duration_minutes].sum() / 60 print(f有效播放总时长: {total_hours:.1f} 小时)接着看歌手维度。我习惯同时算两个视角按播放次数排名和按累计播放时长排名。这两个排名经常不一样差异本身就是信息。比如某位歌手的歌你经常单曲循环那它按次数排名会很高但如果某个歌手的歌都是五六分钟的长歌每次你也都能听完整首那按时长排名就会更靠前。# 按播放次数排名 artist_count df_valid.groupby(artistName)[trackName].count().sort_values(ascendingFalse) # 按累计播放时长排名 artist_duration df_valid.groupby(artistName)[duration_minutes].sum().sort_values(ascendingFalse) artist_stats pd.DataFrame({ 播放次数: artist_count, 累计时长_分钟: artist_duration }) print(artist_stats.head(10))曲目维度的分析类似但要考虑同名歌曲的问题。不同歌手可能有同名歌曲所以groupby时最好同时按artistName和trackName分组才够精确track_stats df_valid.groupby([artistName, trackName]).agg( 播放次数(duration_minutes, count), 累计时长(duration_minutes, sum) ).sort_values(播放次数, ascendingFalse) print(track_stats.head(10))这一步跑完你已经能回答“我最常听的歌手和歌曲是什么”这个最基础的问题了。但我还想多提一个指标就是单曲循环率。它可以用“播放次数超过20次的曲目数量”占“去重后曲目总数”的比例来表示。这个比例越高说明你的听歌偏好越固定越不容易接纳新歌比例越低说明你的歌单越多元。这个指标不高深但很有个人洞察加成。4. 可视化与结果解读用图表讲出你的听歌故事4.1 图表选型与中文字体配置统计数字能说明问题但一张好图的信息密度往往比十个数字更高。我的可视化选型原则很简单能简洁就不复杂能用柱状图就不堆三层嵌套。这次项目里最常用的几个图如下Top 10 歌手柱状图横向柱子好读歌手名字不会被截断。24小时播放量分布柱状图一眼看出夜间和高峰。每周各天播放量柱状图对比工作日和周末差异。月度播放趋势折线图展示时间跨度的变化。做图之前必须先配置中文字体否则matplotlib默认字体里没有中文坐标轴的标签全部显示成方框。我的配置如下import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus也很关键。不设成False的话图表里的负号会显示成乱码方块虽然听歌数据很少涉及负数但图例和坐标轴有时仍会触发这个问题。这两个配置放在脚本开头全局生效。4.2 四张最有信息量的图第一张图是Top 10 歌手播放次数柱状图。我用的是横向条形图因为歌手名字一般都比较长横向排列阅读更自然top_artists artist_count.head(10)[::-1] # 反转顺序让最大的在顶部 fig, ax plt.subplots(figsize(10, 6)) ax.barh(top_artists.index, top_artists.values) ax.set_xlabel(播放次数) ax.set_title(Top 10 歌手播放次数) plt.tight_layout() plt.savefig(top_artists.png, dpi150)第二张图是24小时播放量分布。这部分数据能反映你的作息习惯比如你是夜猫子还是早鸟午休时间是不是也在听歌hourly_play df_valid.groupby(hour)[duration_minutes].sum() fig, ax plt.subplots(figsize(10, 5)) ax.bar(hourly_play.index, hourly_play.values) ax.set_xticks(range(0, 24)) ax.set_xlabel(小时) ax.set_ylabel(累计播放时长分钟) ax.set_title(24小时播放时长分布) plt.tight_layout() plt.savefig(hourly_distribution.png, dpi150)第三张图是星期分布。周末和工作的对比通常非常明显。有人周末听歌多因为时间自由有人反而工作日通勤路上听得多周末安静下来反而不开音乐。第四张图是月度播放趋势折线图。如果你的数据跨越两三年这张图能清晰地展示你对音乐的热情是逐年上升还是逐渐冷却。我看自己的数据时发现年中有一个明显的低谷回头看那是工作最忙的几个月份音乐消费直接腰斩。4.3 结果解读图表背后能看出什么图做出来后不要只发“哦真好看”要学会解读。比如我在自己的数据里发现了一个很有意思的现象周末深夜的播放量占比明显高于工作日。这说明我的听歌行为不只是“通勤时段”还承担着放松助眠的功能。另一个解读维度是累计播放时长的周期性。如果月度趋势图里出现明显的季节性波动先别急着下结论想想是不是因为寒暑假、年终加班季、或者某个月迷上了播客导致纯音乐时间被挤占。这种观察不一定准确但它能引导你回到原始数据里去验证这本身就是数据分析的正循环。5. 常见问题与排查技巧实录5.1 UnicodeDecodeError和中文乱码这是Windows用户最容易踩的坑。读取JSON文件时如果不指定编码或指定错了编码会出现UnicodeDecodeError: gbk codec cant decode byte类似报错。解决方案就是所有open操作都显式指定encodingutf-8读JSON用utf-8写CSV时加上encodingutf-8-sig。utf-8-sig比utf-8好在哪它会在文件开头写入BOM标记这样你用Excel打开CSV时中文不会乱码。如果只用utf-8Excel默认用ANSI解析中文就全变问号了。这个细节我吃过两次亏第一次还以为是pandas的问题后来才明白是编码标记的锅。5.2 数据量大导致内存和性能问题有些人的播放历史跨度很长数据量可能达到几十万条记录。这种情况下直接pd.concat拼接所有StreamingHistory文件通常还是没问题但如果你的电脑配置比较老每次运行脚本都要等好几秒体验会下降。我常用的优化手段有几种一是在读取时就删除不关心的列比如如果你不分析专辑信息就不加载它二是只保留需要的字段尽早减小DataFrame的尺寸三是用df_valid df[df[duration_minutes] 0.5].copy()过滤后及时把中间变量释放掉。还有一点是如果StreamingHistory拆成了几十个文件你可以改成循环里边读边拼接而不是先存一个大list再concat内存峰值会低很多。如果你后续还要做更多计算可以把清洗后的结果存成pickle或parquet格式下次直接读取比重新跑一遍JSON解析快得多df_valid.to_pickle(spotify_history.pkl) df_loaded pd.read_pickle(spotify_history.pkl)5.3 时间统计差8小时或13小时这是时区问题最常见的外在表现。我在第一次跑24小时分布时发现凌晨时段几乎没数据高峰出现在下午5点到晚上7点但我的真实听歌高峰明明是睡前11点左右。排查半天才发现原始时间字段里存的是UTC我却按本地时间处理了。后来统一把endTime转成datetime后又用dt.tz_localize(UTC).dt.tz_convert(Asia/Shanghai)做了显式转换数据才正常。不过要再次强调Spotify导出文件的endTime通常是本地时间并不是UTC。所以遇到时间偏移问题时不要盲目套用tz_convert先单独打印几条原始记录对照你自己的真实听歌时刻确认偏移方向再动手。5.4 播放时长出现0或者少数异常大值有些记录msPlayed是0说明歌曲可能改成了私密会话或者播放被打断得非常快。另一些记录可能是几个小时的播客或者某个直播类音频msPlayed会比普通歌曲长很多达到几万秒。如果这些极端值混在歌曲分析里会导致你的“最常听歌曲”排名被播客或环境音霸榜。我的处理方法是做一个使用场景判断如果你只想分析音乐类曲目可以按播放时长设置一个上限比如超过30分钟的记录全部剔除或者单独挑出来归类为“长音频”。按需过滤后歌曲排名才会回归正常。5.5 去重与重复记录原始数据里有一个容易被忽略的问题同一首歌曲在同一天内播放多次时会生成多条记录这本身是正确的但如果你不小心用相同的处理逻辑跑了两次数据合并会导致记录数翻倍统计结果直接失真。我建议在脚本里打印一个“总记录数”的校验值并且在不同阶段重复打印比对。如果发现数字异常翻倍多半是脚本被重复执行了或者concat时没有排除掉已读入的文件。还有一个去重场景是同一秒钟或同一分钟内出现两条完全一样的记录这很可能是Spotify因为网络重试机制导致的重复上报。可以用drop_duplicates()按关键列去重df_clean df_valid.drop_duplicates(subset[endTime, artistName, trackName, msPlayed])去重数量通常很少但如果有最好还是在统计之前干掉免得“最热歌曲”被重复计数虚高。6. 扩展玩法用API补充音频特征分析6.1 spotipy接入与授权流程本地导出数据能告诉你“听了什么、什么时候听、听了多久”但它回答不了“这些歌听起来是什么风格、能量多高、是否忧伤”。这就要借助Spotify官方API来补全音频特征了。Python生态里最常用的库是spotipy安装一行命令搞定pip install spotipy使用之前需要先去Spotify开发者后台创建一个应用拿到Client ID和Client Secret。然后通过用户授权流程获取访问令牌。这个流程不是直接把账号密码交给脚本而是通过OAuth协议让用户在浏览器里确认授权安全性其实更高。我第一次用的时候觉得繁琐但看完流程就明白了它本质上和你用微信登录某个网站是一个逻辑。6.2 获取音频特征的代码示例授权完成后spotipy会返回一个client对象你可以拿着歌手名或曲目名去搜索再通过track id获取音频特征。音频特征里包括danceability、energy、valence、acousticness等数值全部在0到1之间。import spotipy from spotipy.oauth2 import SpotifyOAuth sp spotipy.Spotify(auth_managerSpotifyOAuth( client_id你的client_id, client_secret你的client_secret, redirect_urihttp://localhost:8080/callback, scopeuser-library-read )) # 示例搜索一首歌并获取特征 results sp.search(qRiver Flows In You, typetrack, limit1) track results[tracks][items][0] features sp.audio_features(track[id])[0] print(fdanceability: {features[danceability]}) print(fenergy: {features[energy]}) print(fvalence: {features[valence]})拿到这些特征后可以和你前面统计出的“高频曲目表”做关联看看你反复听的歌到底偏high还是偏low、偏欢快还是偏忧郁。我跑完发现我的高频曲目里valence平均值明显偏低这倒是符合我平时用音乐平静心绪的习惯。6.3 扩展玩法的更多可能性音频特征还可以组合出很多有意思的分析方向。比如把一天24小时拆成几段分别计算你在每个时段播放歌曲的平均energy值很可能发现早上听歌能量高、晚上能量低这种规律。或者把每周每天的valence均值画成热力图配合星期数据看情绪波动。接口本身还有推荐功能能基于音乐特征生成相似曲目推荐。我知道有些朋友把这个项目做成了自动化周报每周自动分析听歌习惯变化然后推送到邮箱或聊天软件。这些都是后话但说明这个项目的扩展空间很大够你玩很久。我在实际跑完整套流程后最有感触的一点是分析自己听歌数据最大的收获不是一张张图表而是突然理解了自己很多无意识的行为模式。比如我从来没意识到自己在深夜时段播放的歌曲重复率那么高也没想到周末中午会有一个明显的播放空白期。这些洞察不靠数据分析很难浮出水面。如果你也想试着跑一遍建议先从导出数据、计算总时长和Top歌手开始跑通之后再慢慢往上加东西这个项目没有标准答案做得越多、越像你自己。