Python波形图叠加对比指南:像MATLAB一样灵活运用matplotlib

发布时间:2026/10/14 1:12:49
Python波形图叠加对比指南:像MATLAB一样灵活运用matplotlib “python波形图怎么像MATLAB一样叠在一起对比分析”这个问题我在带新人、写数据处理脚本时被问过无数次。很多人从MATLAB切到Python第一反应就是找hold on在哪里第二反应是纠结subplot怎么一次排好几张图。其实Python里要做的“叠”分两类一类是让几条曲线挤在同一个坐标系里互相参考另一类是把几条曲线上下排成不同子图共享时间轴去对照。本文会从这两条路线说起把matplotlib常用的几种对比画法、底层的坐标轴逻辑和碰过壁的坑一次性说清楚。1. 先把需求说透什么样的“叠在一起”才算对上号1.1 MATLAB那套习惯对应到Python里是怎么运作的MATLAB里大家最熟的操作就是figure; plot(t, y1); hold on; plot(t, y2); hold off;hold on决定了新画的内容是追加到当前坐标轴还是把旧的清掉重画。而Python的matplotlib用的是一套类似的“状态机”逻辑但默认行为和MATLAB有个关键差异pyplot状态下你连续调plt.plot()曲线永远是追加的不会清空。所以在matplotlib里你不需要找hold on不新建画布、不主动清轴就直接画就是叠在一起。例子很简单import matplotlib.pyplot as plt import numpy as np t np.linspace(0, 10, 500) y1 np.sin(t) y2 np.cos(t) plt.plot(t, y1) plt.plot(t, y2) plt.show()两条曲线就在同一个坐标系里。这个“不需要hold on”的机制对MATLAB转过来的人其实是个好消息但也埋了个小坑如果你在Jupyter里画了一堆图或者反复执行同一个单元格会发现一张图上叠了不止你想画的曲线。因为旧图没清。这种情况要么每次先plt.figure()新建画布要么在开头加一句plt.cla()清空当前坐标轴。1.2 从plt到ax搞懂对象模型后面所有操作都不乱很多教程通篇都是plt.plot结果一到多子图、双y轴这种复杂需求就懵了因为你其实是在“状态机”里瞎摸。我建议写正经脚本时直接换成面向对象写法一句话解释清楚plt像是黑板旁边的值班老师你说一声“画画”他就给你画到当前那块黑板上面向对象写法里fig是一块黑板本身ax是黑板上某一个绘图区你让ax干什么它才干什么互不干扰。最标准的开局是fig, ax plt.subplots(figsize(10, 5))fig控制整个画布大小、保存、总标题ax控制坐标轴、刻度、具体曲线。后面的代码都用ax.plot()、ax.set_title()而不再用plt.xxx()。当你需要在同一个画布里放多个子图subplots返回的就是一个ax数组按索引访问。心里有了fig和ax这两个对象再去理解后面所有“叠”的方法都会很顺手。2. 方法一同窗叠加把几条波形都画在一个坐标轴里2.1 最朴素的实现其实就三行代码直接上代码你要对比三条波形就这样写fig, ax plt.subplots(figsize(10, 5)) ax.plot(t, signal_raw, label原始信号, linewidth2) ax.plot(t, signal_noisy, label加噪信号, linewidth1) ax.plot(t, signal_filtered, label滤波结果, linewidth2) ax.set_xlabel(时间 (s)) ax.set_ylabel(幅值) ax.legend() ax.grid(True, alpha0.3) plt.show()这就是同窗叠加。一次调用subplots拿到ax然后连续给ax追加曲线。坐标轴的自动范围会综合所有曲线的数据具体哪条在最上层取决于调用顺序后画的默认压在先画的上面。这个方案的适用场景很明确几条曲线的量纲一致幅值范围接近你想直观看到它们之间的重合、偏差、包络关系。比如滤波前后的信号对照或者两个算法在同一组数据上的输出对比用叠加一眼就能看出差异。2.2 层次分明别让线条糊成一团同窗叠加最怕什么怕两条线颜色相近、线宽一样叠在一起分不清谁是谁。工程上的原则是给每条曲线分配“不撞车”的视觉编码。我自己常用的参数组合是alpha透明度。主线设0.8-1.0辅助线设0.4-0.6。这样重叠区域能隐约看到底下的线不会完全遮挡。linestyle实线-给主要结果虚线--、点线:、点划线-.给对比结果。即使打印成黑白图也能区分。linewidth主线2-3对比线1-1.5。线条粗细本身就是一种重要性的排序。zorder数值越大图层越靠上。如果想强调某条线即使它先被画出来也能通过zorder5压到最上面。marker如果数据点稀疏适当加点标记比如markero,markersize4比纯线条更好跟踪。举个实际操作中的例子三通道传感器数据两两之间幅值很接近直接画出来就是一团。我一般把最主要的那个通道设成实线、粗一点、alpha0.9其余两个设成虚线和点线、细一点、alpha0.6。效果立马清楚。此外不要一上来就用十种颜色。matplotlib内置的tab10色板够用但一屏内颜色建议控制在6种以内并且优先选择色盲友好的蓝、橙、绿、红。2.3 图例、网格和局部放大抢走MATLAB的便利性图例别自己手动算位置直接ax.legend()大部分时候自动位置够用。如果曲线尾部贴边导致图例遮挡了关键区域用ax.legend(locupper right, bbox_to_anchor(1.02, 1), frameonFalse)bbox_to_anchor可以把图例挤到坐标轴边框外面不遮挡任何数据。网格线建议开半透明ax.grid(True, alpha0.3)既方便读数又不抢主线视觉。还有一个MATLAB用户很爱用的操作局部放大看细节。matplotlib里同样简单只要ax.set_xlim(0.5, 1.5)就能把时间轴裁剪到0.5到1.5秒这一段。很多对比分析问题不需要花哨工具先把全局看清楚再截取局部定位差异就够了。2.4 双y轴两条不同量纲的信号怎么在同一张图里“叠”同窗叠加有个限制——如果两条曲线量纲差很多比如一个是温度读数20-30℃一个是功率0-2000W硬塞进同一个y轴会看到温度曲线被压成一条水平线。这时需要双y轴对应MATLAB里的yyaxis rightfig, ax plt.subplots(figsize(10, 5)) ax.plot(t, temperature, colortab:blue, label温度) ax.set_xlabel(时间 (s)) ax.set_ylabel(温度 (℃), colortab:blue) ax.tick_params(axisy, labelcolortab:blue) ax2 ax.twinx() ax2.plot(t, power, colortab:red, linestyle--, label功率) ax2.set_ylabel(功率 (W), colortab:red) ax2.tick_params(axisy, labelcolortab:red) # 合并图例 h1, l1 ax.get_legend_handles_labels() h2, l2 ax2.get_legend_handles_labels() ax.legend(h1 h2, l1 l2, locupper left)twinx()生成一个新的坐标轴共享x轴但独立y轴左右两边的刻度颜色和曲线颜色对应。注意双y轴能做出来但别滥用。它很容易让看图的人产生“两条曲线正相关”的错觉因为坐标缩放可以人为夸大趋势。我一般只在确实需要展示两种不同物理量在同一时间维度下的变化趋势时才用而且会在图注里提醒一句“左右轴刻度不同”。3. 方法二上下堆叠给每条波形一条专属“行车道”3.1 用subplots一次性生成多个子图想达到MATLAB里subplot(2,1,1)、subplot(2,1,2)那种效果matplotlib更推荐一次性的写法fig, axes plt.subplots(2, 1, figsize(10, 7))figsize是宽和高subplots(2, 1)表示2行1列。返回的axes是一个长度为2的数组分别对应上下两个子图。然后分别往两个子图上画axes[0].plot(t, signal_noisy) axes[1].plot(t, signal_filtered)子图和子图之间默认是独立的坐标轴范围各管各的。这个方案的优势非常明显——每条曲线有自己的y轴幅值再悬殊也不会被压扁几条波形之间天然分离不会被线条交叉干扰。对比同窗叠加堆叠牺牲了一部分“紧凑感”换来了更高的可读性是多通道信号对比里我最常用的一招。3.2sharex共享x轴才是堆叠的灵魂单纯上下排两个图还不够对比分析时最重要的一个动作是保证对齐。如果两个子图的x轴范围不一致读者根本无法把上方波形的某个峰和下方波形的某个凹陷对应起来。解决办法就是共享x轴fig, axes plt.subplots(2, 1, figsize(10, 7), sharexTrue)有了sharexTrue上下子图的x范围会保持一致你缩放其中一个另一个会同步缩放。这个特性在交互窗口里体验很好在静态图中至少保证了横轴对齐。细节上还有两个注意点最上方子图的x轴刻度标签默认也会显示太占空间。可以用plt.setp(axes[0].get_xticklabels(), visibleFalse)隐藏掉只保留最下方子图的x轴。每个子图的y轴是独立的各设置各的ylabel不要强行改成一样的范围。如果各通道单位不同也不希望y轴联动那就别加shareyTrue保持默认的独立y轴即可。3.3 控制子图高度、间距和标题排版subplots给的是均匀分配的高度但实际项目中每个通道的重要程度不同。比如想重点看滤波前后的对比上下两图高度一致其实浪费了不少空间。可以用gridspec_kw指定高度比例fig, axes plt.subplots(2, 1, figsize(10, 6), sharexTrue, gridspec_kw{height_ratios: [2, 1]})这样上方子图高度是下方的两倍。间距不足导致子图贴着子图时用fig.subplots_adjust(hspace0.3)hspace控制上下间距默认值大约0.2数值越大空隙越大。用了堆叠布局子图标题放在每个子图自己的区域里总标题用fig.suptitle(传感器三通道数据对比, fontsize14)统一放顶部。还有一个小技巧在堆叠图里用axvspan标出同一个事件区间。for ax in axes: ax.axvspan(0.3, 0.5, colorgray, alpha0.2)这样一眼就能看出某个时间段内所有通道发生了什么比单纯看曲线跳变直观得多。3.4 什么时候用叠加什么时候用堆叠很多新手纠结该用哪种其实判断标准很朴素要看曲线间的相关性、吻合度、交叉点用同窗叠加要看每个通道独立的幅值变化、且通道数量较多3路以上用上下堆叠数据量很大、波形密集堆叠可以避免线条纠缠各看各的需要对外展示堆叠图往往更专业尤其是每个子图带独立y轴刻度时。还有一种折中方案大范围用堆叠其中某个子图内部再叠加多条曲线。比如上面是滤波前噪声信号下面是滤波前、滤波后的对比叠加。这种组合在分析报告里非常常见内容分析清晰也足够专业。4. 进阶玩法从静态静态图到可交互对比4.1 用Pandas的plot偷懒如果数据本身在DataFrame里直接利用列名做图例是最高效的做法import pandas as pd df pd.DataFrame({时间: t, 原始信号: signal_raw, 噪声信号: signal_noisy}) df.plot(x时间, figsize(10, 5), gridTrue)pandas底层的plot还是matplotlib但省去了手动循环画线、设置标签的步骤。缺点是自定义程度低想精确控制颜色、线型时还是得回到ax.plot。这个适合快速预览正式图不建议偷懒。4.2 Plotly可以拖动、缩放、点图例开关曲线的方案matplotlib画出来的是静态图适合打印和报告。但如果做数据探索或者要给同事发一个能互动的HTML文件我建议用plotly。它的核心写法同样非常接近叠加逻辑import plotly.graph_objects as go fig go.Figure() fig.add_trace(go.Scatter(xt, ysignal_raw, name原始信号)) fig.add_trace(go.Scatter(xt, ysignal_noisy, name噪声信号)) fig.add_trace(go.Scatter(xt, ysignal_filtered, name滤波结果)) fig.show()plotly的好处是鼠标可以直接框选放大、悬停看数值、点击图例单独隐去某条曲线。几个通道密密麻麻叠在一起时这种交互是matplotlib静态图给不了的。尤其在会议演示现场对方问“把某一段放大看看”鼠标一拉就完事不用回头改代码重新出图。4.3 实时波形数据用PyQtGraph更顺手如果面对的是实时采集的数据比如每秒刷新几十次的传感器波形别用matplotlib硬撑。matplotlib的渲染速度在频繁更新时容易卡顿这种情况建议上pyqtgraph它的核心对象叫PlotWidget用法也不复杂import pyqtgraph as pg pg.plot(t, signal_noisy, penr) pg.plot(t, signal_raw, penb)pyqtgraph基于Qt底层渲染刷新速度快非常适合示波器式的应用场景。不过它与matplotlib的API习惯差异较大不是做实时采集的话没必要一上来就学它。4.4 导出图片分辨率、裁剪和矢量格式对比分析的图经常要放进报告或论文保存时有个黄金三件套fig.savefig(compare.png, dpi300, bbox_inchestight)dpi300保证清晰度bbox_inchestight自动裁剪掉四周空白想要矢量图把后缀改成.pdf或.svg出版级需求首选。要注意顺序savefig要在plt.show()之前调用。plt.show()之后画布的内容有可能被清空或调整过保存出来的图和屏幕上看到的不完全一样。这是我早期踩过的坑后来养成了先保存再显示的习惯。5. 完整案例三路传感器波形对比从零到一5.1 场景与模拟数据准备假设你在某个测试项目中拿到了三路信号原始正弦波、原始正弦波叠加了50Hz工频干扰、以及经过简单移动平均滤波后的结果。要用一张图对比三者的差异。先构造数据import numpy as np import matplotlib.pyplot as plt fs 1000 t np.linspace(0, 1, fs, endpointFalse) f 5 signal_raw np.sin(2 * np.pi * f * t) noise 0.3 * np.sin(2 * np.pi * 50 * t) signal_noisy signal_raw noise kernel np.ones(10) / 10 signal_filtered np.convolve(signal_noisy, kernel, modesame)移动平均的作用是把50Hz的高频噪声压下去同时保留5Hz的基波形态。数据准备完毕后分别用同窗叠加和上下堆叠来呈现。5.2 同窗叠加版本fig, ax plt.subplots(figsize(10, 5)) ax.plot(t, signal_raw, label原始信号, linewidth2) ax.plot(t, signal_noisy, label含噪信号, linewidth1, alpha0.6) ax.plot(t, signal_filtered, label滤波结果, linewidth2) ax.set_xlabel(时间 (s)) ax.set_ylabel(幅值) ax.set_title(滤波前后波形叠加对比) ax.legend() ax.grid(True, alpha0.3) plt.show()这张图适合快速看全局滤波结果和原始正弦基本贴合噪声信号范围明显更宽。缺点是含噪信号把原始信号盖住了一些可以通过调整透明度缓解。5.3 上下堆叠版本fig, axes plt.subplots(3, 1, figsize(10, 8), sharexTrue) axes[0].plot(t, signal_raw, colortab:blue) axes[0].set_ylabel(原始) axes[1].plot(t, signal_noisy, colortab:orange) axes[1].set_ylabel(含噪) axes[2].plot(t, signal_filtered, colortab:green) axes[2].set_ylabel(滤波后) axes[2].set_xlabel(时间 (s)) plt.setp(axes[0].get_xticklabels(), visibleFalse) plt.setp(axes[1].get_xticklabels(), visibleFalse) fig.suptitle(三通道传感器信号对比) for ax in axes: ax.grid(True, alpha0.3) fig.subplots_adjust(hspace0.3) plt.show()堆叠版本里每条曲线的幅值都完整展现含噪通道的波动幅度、滤波通道的平滑程度一目了然。特别适合报告截图。5.4 在同窗图里加局部放大图有时候既要看全貌又要把某一段差异看清楚可以在同一个fig里嵌一个局部放大的小图from mpl_toolkits.axes_grid1.inset_locator import inset_axes ax plt.gca() axins inset_axes(ax, width40%, height30%, locupper right, bbox_to_anchor(0, 0, 1, 1), bbox_transformax.transAxes) axins.plot(t, signal_noisy, linewidth1, alpha0.6) axins.plot(t, signal_raw, linewidth1.5) axins.set_xlim(0.1, 0.2) axins.set_title(局部放大, fontsize9)这样主图提供整体趋势角落的小图提供关键细节一套图把全局和局部都照顾到了。6. 高频踩坑与排查实录6.1 中文变方框plt.title里写中文出来全是方块这是最高频的问题。原因不是代码写错了而是matplotlib默认字体里没有中文字形。解决方式plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False第一行指定中文字体第二行解决负号显示成方块的问题。不同系统字体名不同Windows常用SimHeimacOS常用PingFang SC。如果改了设置还没效果重启一下运行环境再试配置文件的缓存有时会捣乱。6.2 三条曲线里有一条完全看不见先检查是不是被挡住了。解决思路有两个一是调zorder把要显示的曲线抬到最上层二是把被挡住的信号做垂直偏移。比如ax.plot(t, signal2 0.5, label通道2 (0.5))偏移之后曲线之间的相对形状仍然可辨很多时候比调透明度更管用。此外也检查一下ylim幅值范围差异过大会把某个小信号压成一条直线该用双y轴或子图堆叠时就别硬挤。6.3 两条曲线的时间轴对不齐常见于两个文件的数据采样率不一致。绘图时不要想当然地把两边都画在同一个t上各用各的时间数组ax.plot(t1, s1) ax.plot(t2, s2)如果两条曲线要逐点对比则必须先统一到公共时间轴上用np.interp插值t_common np.linspace(0, 1, 1000) s2_interp np.interp(t_common, t2, s2)插值前确认源数据没有异常跳变否则插值结果会污染分析。6.4 图例遮住关键波形locbest多数时候可用但波形密集时依然会撞。兜底方案是上面提过的bbox_to_anchor把图例放到图外或者设置framealpha0.5让图例框半透明既保留图例又不完全遮挡数据。如果实在没地方放把图例拆成两列ax.legend(ncol2, locupper center, bbox_to_anchor(0.5, 1.05))6.5 数据点太多draw起来卡到怀疑人生几百万个点的波形直接plot会让渲染变慢拖动缩放时尤其明显。可以抽样显示step max(1, len(t) // 100000) ax.plot(t[::step], s[::step])或者用scipy.signal.decimate做抗混叠抽取比直接切片更符合信号处理规范。交互需求更强时改用plotly或pyqtgraph。6.6 保存出来的图片比屏幕上模糊由于显示分辨率高于默认保存dpi屏幕上看挺好存成png就模糊。统一用dpi300加bbox_inchestight导出。另外Jupyter里直接右键图片另存存的是低清预览图要存图请走savefig。下面把频率最高的几个问题收成速查表现象常见原因解决办法中文变方框字体缺失或未配置设置rcParams[font.sans-serif]一条线被盖住后画的线在顶层zorder调整层级或垂直偏移时间轴对不齐采样率/长度不一致各用各的时间数组或np.interp插值图例挡住数据自动位置不合适bbox_to_anchor移到框外图很卡点数过多抽样或decimate降采样保存图发虚dpi太低savefig(dpi300, bbox_inchestight)还有一个容易忽略的小坑在脚本里同时打开多个figure窗口时如果没有显式保存对象后面引用容易串。稳妥做法始终是用fig, ax plt.subplots()拿到句柄全程操作句柄不依赖“上一个当前轴”这种隐式状态。回到最开始的问题Python波形图“像MATLAB一样叠在一起”并不难难的是搞明白fig和ax的关系以及搞清楚叠加、堆叠、双y轴分别解决什么问题。我自己调试对比图时的固定流程是先把数据老老实实画出来接着根据量纲和通道数选布局最后才调颜色、透明度、图例位置这些门面工夫。另外有个私人习惯多通道对比时我会给每个通道锁定一个固定颜色让“通道1永远是蓝色、通道2永远是橙色”形成肌肉记忆时间久了后期出图效率会高很多。希望这篇文章能帮你少走几条弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询