news 2026/10/3 2:54:49

骑行数据可视化:Pandas+Matplotlib实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
骑行数据可视化:Pandas+Matplotlib实战解析

打开Strava或码表App,导出一份骑行记录CSV,里面的时间戳、心率、海拔、速度数据密密麻麻堆在一起,想知道上周到底骑了多远、心率区间分布怎样、爬坡时输出稳不稳定,光靠肉眼盯表格实在不直观。我当时也纠结过这个问题,后来直接用Python撸了一套骑行数据可视化分析流程,核心就两个库:Pandas负责清洗和聚合,Matplotlib负责把结果画成能直接看懂的图。这篇文章就把这套流程完整拆开讲一遍,从数据导入、清洗、聚合到图表输出,每个环节都有可复现的代码和参数选择逻辑,适合刚学Pandas和Matplotlib的Python初学者,也适合想把手表、码表里的历史数据盘活起来的骑行爱好者。

1. 项目概述与整体思路

1.1 骑行数据长什么样,为什么需要分析

大多数骑行App导出的数据都是CSV或FIT文件,CSV最常见,按行记录每次GPS采样的数据。典型的字段包括:时间戳、纬度、经度、海拔高度、心率、速度、踏频、功率等。以我平时导出的数据为例,大概长这样:

recorded_at,latitude,longitude,altitude,heart_rate,speed,cadence,power 2026-05-14 07:23:11,31.2304,121.4737,8.2,98,0.0,0,0 2026-05-14 07:23:21,31.2305,121.4737,8.5,101,5.3,62,120

这个文件通常不是专门给人类看的,而是由GPS设备每秒或每几秒生成一条记录。一次两小时的骑行会产生几千行数据,一个月下来就是几万行。靠Excel打开勉强能看,但要做趋势分析、区间统计、异常排查,Excel的体验就很吃力了。

Pandas在这里的核心价值是把零散的CSV变成一个可查询、可聚合、可清洗的DataFrame。Matplotlib的价值则是把处理后的结果以折线图、直方图、散点图的形式呈现出来,让数据自己说话。两者配合起来,一条完整的链路就是:采集数据 → 读入Pandas → 清洗整理 → 按需聚合 → 交给Matplotlib输出图表。

1.2 技术选型:为什么是Pandas+Matplotlib

市面上做数据可视化的方案很多,ECharts、Plotly、Seaborn都有人用,但我最终选择了Pandas+Matplotlib的组合,原因很实在。

第一,Pandas是数据处理事实上的标准工具。ECharts虽然图表交互性强、视觉效果很炫(我之前做农产品价格可视化项目时也用过Flask+ECharts),但它本质上是前端框架,数据清洗和聚合还是要靠后端Python来处理。Pandas对CSV、Excel、JSON各种格式的读取支持非常成熟,尤其是时间序列数据的处理能力,比如resample按分钟/小时/天重采样、rolling滑窗计算,这些在骑行数据分析里几乎每天都在用。

第二,Matplotlib虽然没有ECharts的交互效果,但胜在可控性强、输出稳定。骑行数据的图表大多数是用于个人复盘和报告输出,不一定需要网页交互。Matplotlib绘制的PNG、SVG、PDF可以随意嵌入笔记、PPT,而且图的每个视觉元素都能精细控制,比如线条粗细、坐标轴刻度密度、色系搭配,这对做一份专业性强的骑行报告非常有用。

第三,Pandas和Matplotlib天然兼容。DataFrame直接调用plot()方法就能出图,也可以把清洗好的Series、DataFrame直接传给Matplotlib的plot()函数,坐标轴标签、图例、时间格式化都能自动处理大半,开发效率高于分开处理。

我在实际项目里也试过Seaborn。它的确能画更漂亮的统计图,比如histplot、kdeplot,但这些图在骑行场景里的需求度不高,骑行分析更需要的是多子图联动、时间轴对齐、自定义配色这些功能,Matplotlib完全够用。所以最终这个项目维持在Pandas+Matplotlib的轻量组合上,不引入过多依赖,跑起来也快。

2. 数据准备与预处理难点

2.1 从CSV读入数据的正确姿势

拿到一份骑行数据CSV,第一步是读入Pandas。这里有几个容易踩坑的细节,我一个个说。

我最开始做这个项目时,直接用了最简洁的方式pd.read_csv('ride.csv'),结果发现时间列被读成了字符串对象。字符串对象做不了时间运算,更没法重采样聚合,必须转成datetime类型。

import pandas as pd df = pd.read_csv('ride.csv', parse_dates=['recorded_at'])

parse_dates参数可以让Pandas在读取时就直接把指定列解析为datetime类型,这是最省事的用法。如果CSV里时间格式比较特殊,比如带时区、带毫秒,parse_dates解析不了,可以改成pd.to_datetime()手动转换:

df['recorded_at'] = pd.to_datetime(df['recorded_at'], format='%Y-%m-%d %H:%M:%S')

另外,建议在读入后立刻检查几件事:列名是否符合预期、每列的数据类型是否合理、是否有全空列。可以用df.info()快速查看,我每次拿到新数据源都会先跑这一行,成本极低,能避免后续一堆诡异报错。

print(df.info())

骑过一次车的人都知道,GPS信号不是每时每刻都稳定的。进隧道、高楼密集区、树荫浓密路段都会丢星,丢星期间测出来的速度、位置数据是乱跳的。这类异常数据如果不过滤掉,画出来的速度曲线会出现大量脉冲毛刺,严重干扰判断。

如何处理?先看数据分布,用describe()理解每一列的均值、标准差、最小最大值。

print(df[['speed', 'heart_rate', 'altitude']].describe())

一般来说,骑行速度的最大值如果飙到80km/h以上,而实际路线是城市平路,那基本可以判定是GPS漂移导致的异常值。心率最大超过200、功率突然跳到2000瓦同样是可疑信号。我的做法是:对于明显的物理极限异常值,直接剔除;对于轻微的抖动,用滑窗平滑,而不是全部删掉。

# 剔除明显异常:速度>60km/h 且持续仅1秒的采样点 df = df[df['speed'] <= 60] # 用5个点的滑动平均做平滑,减小GPS抖动 df['speed_smooth'] = df['speed'].rolling(window=5, center=True).mean()

rolling(window=5, center=True)的意思是取当前采样点前后各2个点共5个点的均值,作为该点的平滑后的值。center=True让窗口中心对齐当前点,这样平滑后的曲线不会产生整体相位偏移。这是做速度、功率这类高频噪声较多的数据时非常常用的手段。

缺失值处理也需要分类讨论。心率传感器偶尔会丢包,采集器可能在某个时间段连续几秒没有记录心率。处理方式通常有三种:直接删除缺失行、向前填充、线性插值。我倾向于用interpolate()线性插值,因为骑行过程中的心率变化是连续的,用前后值线性估计缺失值,视觉上不会产生陡峭的跳变。

df['heart_rate'] = df['heart_rate'].interpolate(method='linear')

如果缺失值太多,比如心率丢失了70%的数据,那这段数据就不适合做心率区间分析了,建议整段丢弃或标记为无效区段,而不是强行填充。数据分析的第一原则是诚实,填充不能创造数据。

3. 核心分析维度和可视化方案设计

3.1 单次骑行分析:心率、速度、海拔三图联动

单次骑行分析是所有骑行者最常做的复盘。骑完一趟100公里的路线,不想只想看总用时和平均速度,还想知道:什么时候在上坡、心率有没有爆掉、哪段路速度掉得最厉害。

我的做法是画一个三行子图的联动图,从上到下依次是:速度、心率、海拔。三个图共享同一个X轴(时间),Y轴各自独立。这样一眼就能看出“海拔上升 → 速度下降 → 心率上升”的因果关系链。

import matplotlib.pyplot as plt fig, axs = plt.subplots(3, 1, figsize=(14, 10), sharex=True) axs[0].plot(df['recorded_at'], df['speed_smooth'], color='#1f77b4', linewidth=1.2) axs[0].set_ylabel('Speed (km/h)') axs[0].grid(alpha=0.3) axs[1].plot(df['recorded_at'], df['heart_rate'], color='#d62728', linewidth=1.2) axs[1].set_ylabel('Heart Rate (bpm)') axs[1].grid(alpha=0.3) axs[2].plot(df['recorded_at'], df['altitude'], color='#2ca02c', linewidth=1.0) axs[2].set_ylabel('Altitude (m)') axs[2].set_xlabel('Time') axs[2].grid(alpha=0.3) plt.tight_layout() plt.savefig('ride_single_analysis.png', dpi=150)

这里一个关键细节是sharex=True,让三个子图共享X轴。因为速度、心率、海拔的采样时间完全一致,共享X轴后缩放或平移一个子图时其他子图会同步,方便对比。figsize=(14, 10)是经过测试的尺寸,太小的图看不清趋势细节,太大又不方便放到报告里。

3.2 长期趋势分析:周骑行里程与累计时长统计

单次分析解决的是“这一趟骑得怎么样”的问题,长期分析解决的是“我这段时间训练量是否合理”的问题。周骑行里程和累计骑行时长的变化趋势能直观反映出训练负荷。

首先要用Pandas把每次骑行的汇总数据整理成一张总表,然后按周聚合。

# 假设已有rides表,包含每次骑行的日期、距离、时长、爬升 rides['date'] = pd.to_datetime(rides['date']) rides.set_index('date', inplace=True) # 按周统计里程和时长 weekly = rides.resample('W').agg({ 'distance_km': 'sum', 'duration_min': 'sum', 'elevation_gain': 'sum' }).reset_index() print(weekly.head())

resample('W')是Pandas里最实用的时间序列聚合操作,W代表按周(一周从周日到周六)。也可以改成MS(按月初)生成月度统计。聚合之后画图就简单了:左边画里程柱状图,右边画时长折线图,用双Y轴展示。

fig, ax1 = plt.subplots(figsize=(12, 5)) ax1.bar(weekly['date'], weekly['distance_km'], color='#4C72B0', alpha=0.8, label='Weekly Distance (km)') ax1.set_ylabel('Distance (km)') ax2 = ax1.twinx() ax2.plot(weekly['date'], weekly['duration_min'], color='#C44E52', marker='o', linewidth=1.5, label='Weekly Duration (min)') ax2.set_ylabel('Duration (min)') plt.title('Weekly Riding Overview') plt.tight_layout() plt.savefig('weekly_overview.png', dpi=150)

双Y轴图在这类分析里效果很好,因为里程和时长的量纲完全不同,放在同一Y轴会导致某一条曲线被压扁。使用twinx()创建共享X轴的第二个Y轴,两个指标的波动都能完整呈现。

3.3 心率区间分布与速度-心率散点图

骑行训练里心率区间是非常核心的指标。不同心率区间对应不同的训练强度:有氧基础、无氧耐力、最大摄氧量区间等。用直方图统计一次骑行中各心率区间的时长占比,比看平均心率更有意义。

# 定义心率区间 bins = [0, 120, 140, 160, 180, 220] labels = ['<120', '120-140', '140-160', '160-180', '>180'] df['hr_zone'] = pd.cut(df['heart_rate'], bins=bins, labels=labels) zone_counts = df['hr_zone'].value_counts().sort_index() zone_percent = zone_counts / zone_counts.sum() * 100 print(zone_percent)

pd.cut()用来将连续的心率值按区间切分,生成一个分类变量。然后用value_counts()统计每个区间的采样点数量,除以总采样数得到百分比。这个占比就近似等于在各心率区间内骑行的时间占比。

画图时我习惯用饼图或横向柱状图。饼图直观,柱状图更精确,看个人偏好。

zone_percent.plot(kind='barh', color=['#55A868', '#4C72B0', '#C44E52', '#8172B2', '#CCB974']) plt.xlabel('Percentage (%)') plt.title('Heart Rate Zone Distribution') plt.tight_layout() plt.savefig('hr_zone_distribution.png', dpi=150)

速度-心率散点图则可以用来观察不同速度下的心率反应。散点图的每个点代表一个采样时刻,X轴是速度,Y轴是心率。如果发现同样的速度下心率越来越高,说明疲劳累积明显。

plt.scatter(df['speed_smooth'], df['heart_rate'], s=2, alpha=0.4, color='#1f77b4') plt.xlabel('Speed (km/h)') plt.ylabel('Heart Rate (bpm)') plt.title('Speed vs Heart Rate') plt.tight_layout() plt.savefig('speed_hr_scatter.png', dpi=150)

alpha=0.4和s=2是调节散点密集程度的两个参数。骑行数据动辄几千个点,透明度不给低一点的话图形会糊成一团黑色。调低点的大小和透明度后,点的密度分布反而更清晰,密集区域颜色更深,稀疏区域较浅,等于额外显示了一层密度信息。

4. 完整实操流程与关键代码实现

4.1 环境准备与依赖安装

这个项目只依赖Pandas和Matplotlib,安装非常简单。我比较推荐用清华源加速下载,尤其是国内网络环境下,直接从官方PyPI拉取大包会慢得让人怀疑人生。

pip install pandas matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple

安装之前可以先确认Python环境是否已有了这两个库:

import pandas as pd import matplotlib.pyplot as plt print(pd.__version__) print(matplotlib.__version__)

如果报ModuleNotFoundError,说明还没安装。用上面那行pip命令装好就行。注意不要在系统级Python环境里乱装一堆包,强烈建议用虚拟环境(python -m venv venv),否则以后项目多了依赖版本冲突会非常痛苦。我早期吃过这个亏,全局环境里装了一堆包,最后Pandas和NumPy版本对不上,被迫重装环境,损失了一个下午。

4.2 中文字体配置:解决图里汉字变方块的问题

Matplotlib默认字体不支持中文,不加配置画出来的图,坐标轴和标题里的中文全变成小方块。这个坑几乎所有入门者都踩过,但解法很简单。

import matplotlib.pyplot as plt # 查看当前系统支持的字体 # 建议选支持中文的字体,如SimHei、Microsoft YaHei plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'PingFang SC'] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题

axes.unicode_minus也必须设置,否则坐标轴上的负号会显示成乱码或方块。这个配置全局生效,放在脚本最前面执行一次即可。

如果你的系统里没有中文字体,比如某些精简版Linux服务器,可以手动下载一个开源中文字体(如思源黑体)放到Matplotlib的字体目录,具体位置用matplotlib.get_cachedir()和matplotlib.get_data_path()查询。不过我更多时候是直接在macOS或Windows本地跑,原生就带中文字体,很少需要折腾服务器端。

4.3 横坐标日期标签太密集怎么处理

骑行数据的X轴是时间,如果绘图范围是三个月,Matplotlib默认会用AutoDateLocator自动选刻度,但有时候它选出的刻度密度很高,标签全挤在一起,完全看不清。这时候需要手动控制刻度密度。

import matplotlib.dates as mdates fig, ax = plt.subplots(figsize=(14, 6)) ax.plot(weekly['date'], weekly['distance_km'], marker='o') # 设置X轴为主刻度每隔两周显示一次 ax.xaxis.set_major_locator(mdates.WeekdayLocator(interval=2)) # 设置日期标签格式 ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) plt.xticks(rotation=45) plt.tight_layout() plt.savefig('weekly_distance_with_dates.png', dpi=150)

mdates.WeekdayLocator(interval=2)意思是每两周放一个主刻度,DateFormatter控制标签显示格式为“年-月-日”。最后plt.xticks(rotation=45)把标签旋转45度,避免相邻标签重叠。这三个组合基本能解决90%的时间标签过密问题。

如果想让X轴更清爽,还可以把日期格式简化成“%m-%d”,只显示月份和日期。这个视图表用途而定,如果只看趋势而非具体日期,简化后视觉压力小很多。

5. 实战中遇到的典型问题与排查记录

5.1 时间解析报错:Parsing dates failed

我一开始处理Strava导出的数据时,遇到过ValueError: time data ... does not match format的报错。后来检查发现,文件里有几行时间格式和大多数行不一样,比如某一次GPS冷启动后记录的时间多了毫秒后缀,为2026-05-14 07:23:11.234,而大部分行没有毫秒。pd.to_datetime()在格式不一致时就会报错。

后来我换成format='mixed'参数(Pandas 2.0+支持),或者干脆用errors='coerce'把解析失败的行置为NaT再处理:

df['recorded_at'] = pd.to_datetime(df['recorded_at'], errors='coerce') df = df.dropna(subset=['recorded_at'])

这样即使有个别脏数据也不会中断整体流程。做数据处理时,errors='coerce'是每个Pandas使用者都应该记住的参数,它把解析异常变成缺失值,让管道继续跑下去,最后再统一清理。

5.2 图表布局混乱:子图标题重叠、图例覆盖数据

画单次骑行分析的三联图时,最开始我没用tight_layout(),结果三张子图之间的标题和Y轴标签互相叠在一起,丑得没法看。

plt.tight_layout()的作用是自动调整子图间距,让各子图的标题、标签、注释都能完整显示不重叠。这是Matplotlib最省心的布局方案。如果子图数量多或结构复杂,tight_layout()调整效果有限,可以用plt.subplots_adjust(hspace=0.3, wspace=0.2)手动设置子图之间的高度和宽度间距。

我在项目里踩过最深的一个坑是:图例legend()默认放在右上角,结果正好压住了速度曲线的最高段,导致关键峰值被遮挡。后来习惯性在调用legend()时显式指定位置:

ax.legend(loc='upper left', framealpha=0.5)

framealpha=0.5让图例背景半透明,即使遮挡也不会完全盖住数据曲线。

5.3 大数据量绘图卡顿:几万行数据画出来太慢

如果你累计了一整个骑行季的数据,几万行不算夸张。Matplotlib直接画几万个点的散点图虽然不至于崩溃,但渲染会有明显卡顿。更关键的是,图里点的密度非常大之后,视觉上根本区分不出细节,反而失去了分析价值。

我的解决方案有两个。第一,对于散点图,用rasterized=True将图层栅格化为位图,这样向量图中的散点部分不会过度消耗渲染资源:

plt.scatter(df['speed_smooth'], df['heart_rate'], s=2, alpha=0.4, rasterized=True)

第二,对采样点进行降采样。Pandas的sample()或resample()都能做到。比如心率数据每秒钟采一次样,画图时降采样到每10秒一个点,视觉趋势基本不变,渲染速度却快很多:

df_downsampled = df.resample('10S', on='recorded_at').mean().dropna()

降采样到10秒后要注意,某些短时间的峰值可能会被平均抹平,所以降采样适合看整体趋势,不适合做精确的峰值分析。

5.4 数据清洗顺序不规范导致结果失真

我还犯过一个比较隐蔽的逻辑错误:先做了缺失值插值,再过滤异常速度值。结果在插值时把GPS漂移造成的异常速度也一起“平滑”进了相邻点,导致过滤异常值时部分正常数据被误删。

现在我的清洗顺序严格遵守下面这个流程:

  1. 先读入并检查列类型。
  2. 过滤明显物理不可达的异常值(如速度超过安全上限)。
  3. 再对滤波后的数据做缺失值插值。
  4. 最后做滑窗平滑。

这个顺序的核心逻辑是:先把脏数据和真实数据彻底分离,再做填充和平滑,避免把噪声当成信号传播到周围点。再次强调,这个顺序能避免很多数据分析中的隐性错误。

6. 进阶扩展:往量化分析方向走的机会

骑行的数据可视化做完之后,很多人会问下一步还能做什么。我个人的经验是两个方向:关联分析和预测建模。

关联分析是找数据之间的关系,比如速度和功率的线性回归、心率和速度在不同坡度下的变化规律。这些在Pandas里用corr()就能做简单版本,重一点的可以上scipy.stats或sklearn.linear_model做回归拟合。

预测建模则是基于历史骑行数据,用机器学习模型预测合理的骑行时间、训练后的恢复周期等。不过这一步的主要工作量不在建模,而在特征工程——三维速度向量、坡度变化率、训练负荷累积量等,特征构造和清洗占据了绝大部分时间。Pandas的rolling、shift、groupby操作几乎可以覆盖所有特征工程的实现需求,这也是Pandas在数据科学链路中不可替代的原因之一。

以我目前的使用体验,Pandas+Matplotlib这一套组合在处理骑行数据这种中量级时间序列数据时,性能和灵活性都足够。真要跑到上百万条记录再考虑DuckDB或polars也不迟,现阶段完全没必要为了“大数据”而大动干戈。工具选型服务于实际场景,能用剪刀解决的切削问题,不必上电锯。

做数据分析更重要的是养成好习惯:拿到数据先看info()和describe(),清洗前后都保留一份原始备份,每一步操作都明确知道原因。这些习惯比我在这里写的任何具体代码都管用。希望这篇分享能帮你在自己的骑行数据里挖出点有意思的规律,哪怕只是为了搞清楚“上个月到底为什么那么累”,这套流程也够用了。

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

CentOS7搭建SFTP全攻略:从配置到Tabby面板与报错排查

1. SFTP到底是什么&#xff0c;为什么你的Tabby找不到SFTP按钮先直接把结论扔给各位&#xff1a;SFTP全称是SSH File Transfer Protocol&#xff0c;它不是FTP的安全版&#xff0c;而是SSH协议自带的一个文件传输子系统。换句话说&#xff0c;只要你服务器上开着SSH服务&#x…

作者头像 李华
网站建设 2026/10/3 2:53:43

WOA鲸鱼算法联合XGBoost特征选择与参数调优实战

简介&#xff1a;该资源面向计算机、电子信息工程、数学等专业的大学生及算法初学者&#xff0c;提供一套基于Matlab的WOA鲸鱼算法特征选择与XGBOOST参数联合优化分类预测方案&#xff0c;可用于课程设计、期末大作业或毕业设计。资源包共16个文件&#xff0c;约53.31MB&#x…

作者头像 李华
网站建设 2026/10/3 2:53:20

基于Python的深圳二手房数据分析:爬虫、MySQL、可视化全链路实践

简介&#xff1a;这是一份基于Python的深圳链家二手房数据采集与分析项目&#xff0c;适合爬虫入门、数据分析实践及毕业设计参考。项目围绕链家二级域名组装二手房链接&#xff0c;完成页面筛选条件抓取、数据保存至MySQL&#xff0c;并提供分析模块&#xff0c;代码经过完整测…

作者头像 李华
网站建设 2026/10/3 2:52:50

Java秒杀系统设计:Redis Lua+网关限流+微服务隔离

简介&#xff1a;这是一份面向计算机专业本科生的毕业设计实战项目&#xff0c;聚焦高并发场景下的微服务架构实践&#xff0c;帮助学习者系统掌握商城秒杀系统的完整技术实现路径。资源以Java为核心技术栈&#xff0c;深度整合Spring Boot、Spring Cloud&#xff08;含Zuul网关…

作者头像 李华
网站建设 2026/10/3 2:51:36

Python基于LSTM预测股市:完整源码、模型与数据集搭建指南

简介&#xff1a;这份资源面向金融量化初学者与深度学习爱好者&#xff0c;提供一套基于LSTM长短期记忆网络的股市预测完整实现&#xff0c;帮助理解循环神经网络在时间序列预测中的应用。压缩包共19个文件&#xff0c;约3.92MB&#xff0c;包含9个Python脚本、2个CSV与1个Exce…

作者头像 李华
网站建设 2026/10/3 2:50:54

TRO组团谈判:如何把谈不拢的事快速谈拢

项目标题背后是一个很典型的商业协作谈判场景&#xff0c;我之前接过不少类似的协调工作。TRO这个代号&#xff0c;我们内部叫它 Talk Resolution Optimization&#xff0c;翻译成大白话就是“把谈不拢的事谈拢”。它解决的是多人在同一件事上立场不同、诉求冲突时&#xff0c;…

作者头像 李华