资讯动态

Matplotlib典型绘图场景实战:从物理曲线到XRD与统计分布

发布时间:2026/10/13 12:32:36 来源:尧图企业网站定制
做数据分析这些年matplotlib 几乎是我每次都要打开的绘图库。从物理模拟里的阻尼衰减曲线到材料分析绕不开的 XRD 衍射图谱再到三角函数、超越函数这类基础数学图像以及统计模拟用正态分布随机数画直方图——这些看起来领域跨度很大的图其实都靠同一个工具撑起来。我把这些场景集中整理成一篇 matplotlib 作图示例从原理到代码、从踩坑到修正的完整路径都写在里面。适合刚接触数据可视化、想一次看懂几种典型图该怎么画的人也适合已经会用基础接口、但希望出图细节和工程效率再进一步的同学。1. 先把绘图环境拾掇好字体、样式与导出三板斧1.1 中文字体问题的常见解法第一件事永远是解决中文显示。很多教程直接写plt.rcParams[font.family] SimHei我在 Windows 上试过没问题换到 Linux 服务器上就全线崩成方块。这类问题实在太常见所以我现在的做法是先扫描系统里有哪些可用字体从中挑一个满意度最高的中文字体来用。借助font_manager扫描一次代码也不算复杂import matplotlib from matplotlib import font_manager font_names {f.name for f in font_manager.fontManager.ttflist} candidates [SimHei, Microsoft YaHei, PingFang SC, Noto Sans CJK SC, WenQuanYi Micro Hei] font next((f for f in candidates if f in font_names), None) matplotlib.rcParams[font.family] font matplotlib.rcParams[axes.unicode_minus] False这里有个细节容易被忽略axes.unicode_minus一定要设成False。坐标轴上的负号默认使用 Unicode 的减号字符当你把全局字体切成中文字体后这个减号经常显示成空方块。我之前在某次出图时怎么查都找不到原因最后发现罪魁祸首就是这行配置。要是candidates里一个都没命中别硬刚装一个开源中文字体或者退一步让图形里全部使用英文标签都比在字体问题上死磕更高效。1.2 全局样式与色板选型环境准备第二件事是样式。matplotlib 在不同版本里的默认样式差异很大本地跑出来的配色和同事机器上完全对不上是常有的事所以推荐在脚本开头显式声明用哪一套风格。比较省事的是plt.style.use(seaborn-v0_8-whitegrid)这套风格自带浅灰色网格适合数据图也贴近很多论文的排版习惯。如果你的 matplotlib 版本比较老seaborn前缀的风格名称改过好几次遇到报错先跑一下plt.style.available看看当前版本到底支持哪些名字。样式选定之后还有一个经常被忽略但很关键的东西颜色循环。用面向对象接口画多条线时默认会从prop_cycle里自动取色。想自己控制一组图的主色调直接改配置文件即可matplotlib.rcParams[axes.prop_cycle] matplotlib.cycler( color[#4C72B0, #DD8452, #55A868, #C44E52] )我心里一般会准备两套配色一套是这种低饱和度的学术色适合论文、报告另一套是tab10这样区分度更强的适合需要快速辨别多曲线的场景。这样做的好处很明显同一批图里所有曲线颜色自动错开不用每次手动指定 color少写很多重复代码视觉上也统一。1.3 高清导出的一组合适参数最后说导出。我见过很多人画完图直接plt.savefig(output.jpg)保存出来线条发虚、字也糊。正确的关键参数是dpi300和bbox_inchestight。前者控制分辨率后者会裁掉图片周围的空白边缘避免最终成品里出现一大圈白边。不同格式的适用场景差异很大我整理了一个简单表格格式特点适合场景PNG位图放大看会发虚网页插图、PPT 快速展示PDF / SVG矢量图无限放大不糊论文插图、打印、后续二次编辑JPG有损压缩细节丢失不推荐用于数据图论文场景里我基本只用 PDF 或 SVG图片插入文档时再按需转换。矢量图的好处是文字和线条在排版软件里放大缩小都锐利坏处是复杂图形体积会很大这个问题后面讲 XRD 的时候还会再提到。2. 阻尼衰减曲线动态系统的经典衰减特征2.1 物理图像与公式拆解阻尼衰减在物理模拟、控制理论、振动分析里太常见了。它描述的是振荡系统在阻力作用下振幅随时间指数缩小的过程。典型表达式是y(t) A * exp(-β * t) * cos(ω * t φ)四个参数各有明确含义A 是初始振幅决定曲线起始高度β 是阻尼系数决定振幅衰减快慢β 越大收得越快ω 是角频率决定振荡周期φ 是初相位决定时间零点的位置。我最早画这类图时总觉得代码没问题但图像很奇怪后来才发现是没理解参数之间的相对关系——比如 β0.6 时振幅在 5 秒内已经衰减得很小此时如果还想画十几个完整周期高频振荡会挤成一团肉眼根本看不清细节。绘制这类曲线不需要逐点计算循环直接用 numpy 对整个时间数组批量计算。numpy 会把exp(-βt)和cos(ωtφ)分别按元素计算再点乘写法简洁效率也高。2.2 完整绘图流程与包络线实现下面是一个可以直接跑的完整示例import numpy as np import matplotlib.pyplot as plt t np.linspace(0, 5, 800) A, beta, omega, phi 2.5, 0.6, 4 * np.pi, 0.2 y A * np.exp(-beta * t) * np.cos(omega * t phi) env_p A * np.exp(-beta * t) env_n -env_p fig, ax plt.subplots(figsize(9, 5)) ax.plot(t, y, lw1.5, color#1f77b4, label衰减振荡) ax.plot(t, env_p, ls--, lw1.0, color#d62728, alpha0.8, label上包络线) ax.plot(t, env_n, ls--, lw1.0, color#d62728, alpha0.8, label下包络线) ax.set_xlabel(时间 t (s)) ax.set_ylabel(振幅 y) ax.set_title(阻尼衰减曲线) ax.legend(locupper right) ax.spines[[top, right]].set_visible(False) fig.tight_layout() plt.show()这里有两个细节值得说。第一采样点数量不要省我用了 800 个点。采样点过少高频余弦部分会表现为折线甚至出现类似拍频的视觉伪影让人误以为信号里有两个频率成分。第二上下包络线其实是一条正指数曲线和它的负值用红色虚线画出来读者一眼就能看出振幅衰减的边界。有些教程只画一条包络线我建议上下两条都画才完整表达振幅上下界同时收敛这个物理图像。2.3 让曲线更工程可读的标注技巧阻尼衰减曲线画完真正让它从数学练习变成工程图纸的是标注。我通常会标出两个关键信息一是振幅衰减到初始值 1/e 对应的时间二是振荡周期数。标注方法很简单t_cross 1 / beta # 振幅衰减到 A/e 的时间 ax.axvline(t_cross, colorgray, ls:, lw1.0) ax.annotate(f振幅降到 {1/np.e:.1%} 的时刻, xy(t_cross, y[np.argmin(np.abs(t - t_cross))]), xytext(t_cross 0.3, -2), arrowpropsdict(arrowstyle-, lw0.8))axvline画垂直参考线annotate在指定位置放注释并拉一根箭头到目标点。注释文字压到曲线时稍微调整xytext坐标避让开即可。我做项目汇报时发现加不加这几行标注听众理解一张图的速度差别非常大。图形本身提供信息注释直接给出结论这是工程出图里很值得养成的习惯。3. XRD 衍射数据绘图科研数据不是画出来是整理出来的3.1 数据导入与预处理XRDX 射线衍射是材料分析里非常常用的手段得到的原始数据一般是从仪器软件导出的两列文本第一列是衍射角 2θ第二列是对应强度。文件可能是.txt也可能是.csv分隔符可能是空格、制表符或逗号第一行往往还有表头。读取用np.loadtxt就能处理绝大多数情况data np.loadtxt(demo_xrd.txt, skiprows1) theta data[:, 0] intensity data[:, 1]不过实际数据远没有示例干净。我拿到一份真实衍射数据时第一步一定先把整条曲线放大看一遍确认有没有异常尖峰、零点漂移、负强度等问题。下面这段代码故意模拟了一份带背景和噪声的衍射数据用来演示后续处理流程theta np.linspace(10, 80, 3500) bg 8 0.1 * theta intensity bg 120 * np.exp(-((theta - 27.3) / 0.15) ** 2) \ 100 * np.exp(-((theta - 44.8) / 0.18) ** 2) \ 60 * np.exp(-((theta - 57.1) / 0.12) ** 2) \ np.random.normal(0, 1.2, len(theta))衍射峰用高斯函数模拟背景是一条缓变的直线再加一点随机噪声。真实仪器数据里噪声水平可能更高峰贡献也更复杂但基本结构是类似的。3.2 扣除背景与图谱布局画 XRD 图最常踩的坑是直接拿原始数据画背景太高弱峰完全被强峰埋掉。扣除背景是这类图的标准预处理。背景通常是慢变量而衍射峰是窄的局部结构所以我的做法是选择几个不含峰的窗口在这些窗口数据上拟合一条低阶多项式或直线再用它模拟整个角度范围内的基线最后从原始强度中减掉mask (theta 25) | (theta 65) coef0 np.polyfit(theta[mask], intensity[mask], 1) baseline np.poly1d(coef0)(theta) intensity_corr intensity - baseline多峰区域样品、几十个峰的情况窗口法不一定够用可以换成 SNIP 这类专门做谱图背景扣除的算法需要使用额外工具库。但窗口法作为最常用的入门方案足以应对大部分常规分析和演示。扣除背景后画出的图谱峰形更干净峰位也更醒目。如果要在同一张图里对比多个样品比如三个配方的衍射图谱直接用同一坐标系叠加往往互相遮挡。我的习惯是给每条谱线加一个垂直偏移量让它们像台阶一样上下错开再在每条线旁边标上样品名称读起来非常清晰。3.3 峰位标注与坐标轴刻度设置基线扣完、图谱画好接下来是标峰。没有分析软件时用 Python 自己写一个简单峰搜索也不难对平滑后的强度序列遍历若某点比左右邻居都高且超过绝对高度阈值就视为局部极大值def moving_average(y, win5): kernel np.ones(win) / win return np.convolve(y, kernel, modesame) def find_local_max(y, min_height): peaks [] for i in range(1, y.shape[0] - 1): if y[i] y[i - 1] and y[i] y[i 1] and y[i] min_height: peaks.append(i) return np.array(peaks) smooth moving_average(intensity_corr, win5) peak_ids find_local_max(smooth, min_height20) ax.plot(theta[peak_ids], intensity_corr[peak_ids], v, color#C44E52, markersize8, markeredgewidth0.8, label峰位) for idx in peak_ids: ax.annotate(f{theta[idx]:.2f}°, xy(theta[idx], intensity_corr[idx]), xytext(theta[idx] 1.5, intensity_corr[idx] 15), arrowpropsdict(arrowstyle-, lw0.8))find_local_max是朴素实现噪声大时容易把噪点当成峰所以检测前先做了一个窗口滑动平均把毛刺磨掉一些。标注峰位时我取的是扣除背景后的原始强度值而不是平滑后的值这样峰高不会被人为压低。坐标轴刻度也值得花心思。XRD 图的横轴是 2θ实验室常规测试范围常在 10° 到 80° 之间。如果只是展示整体形貌用默认刻度即可如果要给峰位最好把刻度手动放到关键峰位附近。但不要贪多刻度太密数字挤成一团反而影响阅读。4. 三角函数与超越函数函数图像的美学与刻度4.1 三角函数的周期与相位控制函数图像是 matplotlib 里最基础但也最容易画得难看的图。三角函数的核心是周期性和相位横坐标用弧度制时周期是 2π。画这类图时横轴刻度如果全是 0.5、1.0、1.5 这样的小数看起来非常缺乏直观性我建议直接用 π 的倍数来标x np.linspace(-2 * np.pi, 2 * np.pi, 600) y_sin np.sin(x) y_cos np.cos(x) y_shift np.sin(x np.pi / 3) fig, ax plt.subplots(figsize(8, 4)) ax.plot(x, y_sin, lw1.5, labelsin(x)) ax.plot(x, y_cos, lw1.5, ls--, labelcos(x)) ax.plot(x, y_shift, lw1.5, ls-., labelsin(xπ/3)) ticks np.arange(-4, 4) * np.pi / 2 ax.set_xticks(ticks) labels [] for t in ticks: k t / np.pi if abs(k - round(k)) 1e-9: if abs(k) 1e-9: labels.append(0) else: labels.append(f${round(k):g}\\pi$) else: labels.append(f${k:.1f}\\pi$) ax.set_xticklabels(labels) ax.axhline(0, colorblack, lw0.8) ax.axvline(0, colorblack, lw0.8) ax.set_xlabel(x) ax.set_ylabel(y) ax.legend()相位变化在图上非常明显sin(xπ/3)比sin(x)向左平移了 π/3 个单位。横轴刻度以 π/2 为间隔整数倍 π 处显示简洁的 π、2π中间显示 1.5π 这样的形式零点、极值点所在位置一目了然。采样同样要小心。我习惯只用np.linspace不用np.arange生成坐标。np.arange做浮点运算时由于舍入误差最后一个点可能恰好落在边界之外图尾就会缺一小截linspace默认包含两端可以避免这个问题。4.2 超越函数的值域与渐近线处理超越函数是指不是代数函数的函数常见代表是对数、指数、双曲函数。画这类图有两个天然雷区一是定义域处理不完整二是值域过大把图形压成一条直线。以ln(x)为例它在 x0 处发散到负无穷所以横轴不能从 0 或负数开始我一般用 0.01 作为起点。exp(x)在 x 稍大时数值会溢出产生RuntimeWarning所以要么限制 x 的范围要么画完后用ax.set_ylim把显示范围拉回合理区间。画tanh(x)时则要注意它的渐近线x 趋向正负无穷时函数值分别趋近 1 和 -1。x np.linspace(0.01, 5, 400) fig, (ax1, ax2) plt.subplots(1, 2, figsize(9, 4)) ax1.plot(x, np.log(x), color#1f77b4) ax1.axvline(0, colorgray, ls--, lw0.8) ax1.set_title(ln(x)) ax1.set_xlabel(x) ax1.set_ylabel(y) x2 np.linspace(-5, 5, 500) ax2.plot(x2, np.tanh(x2), color#C44E52) ax2.axhline(1, colorgray, ls--, lw0.8) ax2.axhline(-1, colorgray, ls--, lw0.8) ax2.set_title(tanh(x)) ax2.set_xlabel(x)把这些参考线画出来之后图形的极限行为一眼就能看出来也省得看图的人自己脑补渐近线。这个习惯适用于所有带定义域限制的函数。4.3 同一坐标系下多函数对比的配色逻辑三角、超越函数图经常需要把多个函数放在同一坐标系里对比。配色不是随便选的。我自己的原则是主函数用深色、实线参考函数用浅色、虚线或点划线一条图上最多放五条线超过五条就拆成子图否则图例都摆不开。同一组函数对比还可以用同一色系的不同深浅从深蓝到浅蓝天然形成一主三辅的层次感。线宽也要拉开差距主函数用 2.0 左右参考函数用 1.0 或 1.2。很多刚学画图的人喜欢把每条线都设置成同样的粗细和颜色打印出来以后完全分不清谁是主角。图例位置也值得调整。多条曲线在某个区域互相交叠时图例放在那里会挡住关键信息优先放角落。也可以把图例放在绘图区外部比如bbox_to_anchor配合locupper left这是论文插图常见的做法。5. 正态分布随机数配合插值多项式从散点到光滑曲线5.1 随机数的生成与直方图统计统计模拟里经常要展示一组正态分布随机数的形态。直接画散点看不出分布用直方图展示频数是常规操作。np.random.normal(0, 1, 5000)能生成 5000 个标准正态分布样本然后画直方图np.random.seed(2024) samples np.random.normal(0, 1, 5000) fig, ax plt.subplots(figsize(8, 5)) counts, bins, patches ax.hist(samples, bins50, densityTrue, alpha0.65, color#4C72B0, edgecolorwhite) bin_centers (bins[:-1] bins[1:]) / 2这里有个关键细节densityTrue把直方图的纵坐标从频数变成概率密度这样后续才能直接和理论正态分布密度函数放在同一量纲下比较。bins数量会直接影响直方图形态bins 太大单个 bin 内样本太少图形毛糙bins 太小分布细节又被抹平。以 5000 个样本来说50 左右通常是不错的起点。edgecolorwhite这里也很有用给每个矩形加白色描边相邻柱子颜色相近时会干净不少。直方图画完之后还可以把理论正态分布密度曲线叠加上去作为对照分布拟合得好不好一眼可见。5.2 多项式插值的原理与实现在直方图基础上我们用多项式来逼近这条经验密度曲线。这里要先说清楚一个概念严格意义上的插值是构造一条曲线让它精确穿过所有数据点而最小二乘多项式拟合是让曲线在整体上与数据最接近不一定穿过任何一个点。直方图每个 bin 的密度值带有统计涨落如果去严格插值反而会因为强追噪声而产生震荡所以我这里选择拟合xx np.linspace(bins[0], bins[-1], 300) for deg, color, ls in [(5, #C44E52, -), (15, #8172B2, --)]: coef np.polyfit(bin_centers, counts, deg) yy np.polyval(coef, xx) ax.plot(xx, yy, lw1.5, colorcolor, lsls, labelf多项式拟合 n{deg})np.polyfit返回从高次到低次的系数数组np.polyval负责代入 x 求值。次数选多少合适这里 5 次多项式已经能很好地贴近正态密度弧线而 15 次多项式在两端会出现明显的上下抖动——这就是数值分析里常说的 Runge 现象。次数并非越高越好高次多项式对边缘噪声过度敏感会让曲线局部剧烈摆动。实际作图时我会同时跑几个次数挑肉眼观察最平滑且拟合误差可接受的一档。如果确实需要多项式穿过某些关键点比如让曲线经过峰顶和两端几个控制点可以对选中的点做拉格朗日插值或牛顿插值。不过这类严格插值同样逃不开高次震荡问题工程上更常用分段插值样条。标题里说的插值多项式本质就是这里用多项式去逼近、重现数据分布形态的过程。5.3 随机种子与可复现性管理画随机数相关的图最基础但最容易被忽略的是随机种子。不设置np.random.seed每次运行生成的样本都不同直方图形态跟着变拟合曲线也变。别人拿到你的脚本跑出来的图和展示的完全对不上这对数据可视化来说是很尴尬的事。np.random.seed(2024)的作用是固定随机数生成器的初始状态让 5000 个样本在每次运行中保持一致。有两点值得提醒如果脚本里多次调用随机函数seed 只需在第一次采样前设置一次后续序列会连续往下走不会重复如果换了 numpy 大版本或随机算法seed 的复现性也可能受影响重要项目建议同时记录环境版本。除了种子bins 数量也是隐性参数。同一份数据bins20 和 bins100 画出来的分布形态可能差别很大。所以我在分析笔记里通常会记录完整参数链样本量、种子、bin 数、拟合次数、坐标轴范围。这样几个月后再来复现也不会因为某个参数丢了而抓狂。这个方法在团队协作和论文复现审查时尤其有用。6. 我踩过的几个坑坐标轴、图例与性能的最后提醒6.1 坐标轴范围自动调整的副作用matplotlib 默认会根据数据范围自动设置轴边界听起来方便实际出图时却经常出问题。比如画阻尼衰减曲线设了ylim(-3, 3)会把包络线末端截掉不设行尾振荡幅度极小的地方又占很大一段空白。我的经验是对任意一张图要么不设轴范围让 matplotlib 自己算要么设了就先画一遍放大检查关键区域没有被切掉再定稿输出。更可控的做法是使用ax.margins(0.02)在数据边界外统一留出 2% 空白。这样既不会被自动范围贴得太紧也不会因为手写 xlim/ylim 误切数据。尤其是在函数图像里曲线经常擦着边界走留一点点空白视觉上舒服很多。6.2 图例样式与线宽细节图例看似小事排布不好很影响读图效率。图例和曲线重叠时我会先调整loc还挤不开就把图例放到绘图区外面ax.legend(locupper left, bbox_to_anchor(1.02, 1), framealpha0.9)另外投影屏和打印机对线条的呈现差异很大。细线打印出来几乎看不见论文投稿对线宽一般有 0.5pt 以上要求但我实际的经验是屏幕上看 1.0 的线很清晰打印出来就发虚最好设到 1.5 或 2.0。字号同理font.size不要低于 89 到 10 是安全和美观的平衡点。6.3 大批量绘图时的性能建议最后聊性能问题。XRD 数据从 10° 扫到 80°步长 0.01°就是 7000 个点看起来不多但一次脚本跑 20 张图文件体积和渲染时间都会被放大。这里有两个实用手段。一是抽稀对太密的数据点按比例抽取部分点显示肉眼基本分辨不出差别。二是栅格化如果图形里某个对象比如大面积填充色是体积主要来源可以用set_rasterized(True)让这一层导出时变成位图同时保持文字和线条是矢量兼顾清晰度和文件大小。脚本循环出图时记得养成plt.close(fig)的习惯。长期不关闭图形对象内存占用会逐渐上涨在自动化批量出图脚本里尤其明显。我在一个规模较大的模拟项目里曾遇到脚本越跑越慢的问题最后定位半天才发现是没及时关图。加上这一行之后速度改善非常明显也算是一个小而关键的性能习惯。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑