☰
多散点子图复现指南:用matplotlib画出Nature级科研图表
2026/10/17 7:10:31 网站建设 项目流程

看到一篇带有多个散点子图的论文,第一反应往往是“这图真干净、信息量真大”,然后自己动手复现时,却发现怎么调都差点意思:点的大小不对、颜色发闷、边缘线太粗、标签挤在一起。所谓 Nature 图表复现,难点从来不是把散点画出来,而是把图里的信息层级、坐标轴规范、配色逻辑和统计注释放到它们该在的位置。这篇内容我就以“多散点子图”为切入,讲清楚我从读图到复现的完整思路,包括数据怎么构造、matplotlib 参数怎么调、统计标签怎么加、保存时有哪些坑,适合正在做科研绘图、准备组会材料、或者想把论文图表提升一个档次的人参考。

1. 复现之前,先看懂多散点子图到底在表达什么

很多人在复现图表时犯的第一个错误,是直接找代码、调参数,忽略了图本身的设计意图。一张多散点子图之所以会出现在 Nature 论文里,不是因为散点图“好看”,而是因为它能在一个版面内高效回答一个核心问题:多个变量或多种条件下的两两关系是否一致。

1.1 多散点子图的典型构成与叙事功能

一个标准的多散点子图,通常由一组小倍数面板(small multiples)组成。每个面板是一张独立的散点图,但所有面板共享同一个坐标轴范围、同一种点样式、同一种颜色映射。它的叙事功能很明确:并排对比。

举个例子,某课题组研究不同处理条件下两个代谢指标的关系。如果只画一张总散点图,所有处理混在一起,很难看出规律;如果画成 2×4 的八张小图,每个处理占一小格,那读者一眼就能看出哪一组相关性更强、哪一组数据更离散。这比任何文字描述都直接。

我在自己的项目里也复现过类似结构,当时需要对比六组不同样本的相关性。直接画六张独立图,尺寸、轴范围、点大小不统一,放在一起非常乱;后来改成统一坐标轴的多散点子图,整页图的信息密度和美观度立刻不一样了。

1.2 视觉秩序从哪里来:一致性压倒一切

Nature 图表之所以显得“高级”,不是因为用了复杂的颜色或特效,恰恰相反,是因为它做了很多减法。这种视觉秩序主要来自几个一致性约束:

  • 坐标轴范围一致:所有面板使用相同的 x 轴和 y 轴范围,这样可以直接通过点云的形状和斜率比较相关性强弱。
  • 点样式一致:点的大小、透明度、边缘色、填充色在所有面板中保持完全一致。
  • 字体和刻度一致:所有面板使用同一字号、同一刻度密度、刻度朝外。
  • 颜色数量极少:通常只用一个主色,或者用一个连续色带区分第三维信息。

这些设计原则意味着,复现时不能每张子图单独设参数,而要把整张图当做一个整体来配置。这也是后面代码里我们反复强调“先定义全局参数,再统一绘图”的原因。

1.3 设计意图先行的复现思路

我把复现流程拆成四步:读图、模拟数据、搭框架、调细节。读图阶段要回答几个问题:每个面板里有什么?点是否按第三维着色?有没有拟合线和置信区间?有没有统计注释?坐标轴单位和刻度是怎么标的?这些问题决定了你的代码结构。

比如,如果你发现原图里每个点的大小和该点对应的样本量成正比,那你在模拟数据时就要带权重点;如果你发现图的角落里有 r 值和 p 值,那你的代码里就要预留注释文本的位置。这些都是在动手写代码之前就明确的,不是调参时临时加的。

2. 数据准备:没有原始数据,怎么构造一张可复现的练习数据

复现一张图,最大的障碍往往是没有原始数据。期刊只给图,不给数据表。那怎么办?自己模拟。模拟数据的核心原则是:形态上尽可能接近原图,并且带明确统计关系,这样画出来的图才有说服力。

2.1 模拟数据的基本思路

假设我们要复现一张 2×4 的多散点子图,每个面板展示一个指标组合(或一种处理条件)。每个面板内部的点云呈现不同的相关强度和离散程度。我们需要生成八组二维数据,每组包含一定数量的点,并且预设相关系数。

数据模拟有一个简单可靠的技巧:给定相关系数 r,先随机生成长度为 n 的标准正态变量 x,再构造 y = r*x + sqrt(1 - r^2)*z,其中 z 是另一个独立的标准正态变量。这样生成的 x 和 y 之间的理论相关系数就会非常接近 r。

为什么这个公式成立?因为 x 的方差是 1,r*x 部分的方差是 r^2,z 部分带来的独立方差是 1-r^2,两部分叠加后总方差归一,相关系数恰好是 r。在实现时,我会先对 x 和 y 做标准化,这样即使加入了不同的均值和标准差,相关性也不会受影响。

2.2 构造面板化的数据表结构

多散点子图画起来方便不方便,很大程度上取决于数据结构。我更推荐把所有面板数据放在同一个 DataFrame 里,用一列表示面板编号,一列表示 x,一列表示 y。这种长表结构,用 seaborn 的 FacetGrid 也方便,用 matplotlib 手写循环也方便。

我的模拟代码大概是这样的:

import numpy as np import pandas as pd rs = [0.9, 0.6, 0.3, -0.4, 0.8, 0.5, -0.6, 0.2] # 预设相关系数 panels = [f"Panel {i+1}" for i in range(8)] n_points = 60 frames = [] for pid, r in zip(panels, rs): rng = np.random.default_rng(seed=42) # 固定随机种子,保证可复现 x = rng.normal(loc=0, scale=1, size=n_points) z = rng.normal(loc=0, scale=1, size=n_points) y = r * x + np.sqrt(1 - r**2) * z # 统一缩放到有实际意义范围,比如 x 在 [0, 10] x = (x - x.min()) / (x.max() - x.min()) * 10 y = (y - y.min()) / (y.max() - y.min()) * 10 frames.append(pd.DataFrame({"panel": pid, "x": x, "y": y})) df = pd.concat(frames, ignore_index=True)

这里有个细节:随机种子 rng 是在循环内部定义的,这样每个面板生成的序列完全不同,而不是每轮复用同一组噪声。如果你把 rng 定义放到循环外部,生成的数据会有隐藏的跨面板相关性,画出来的图看着不对劲,又说不清是哪里的问题。

2.3 分组着色与额外维度扩展

如果原图中的点还按照某个第三维变量着色,那么在模拟数据里就要加入分组标签。比如每个面板内部有两种颜色,分别代表不同的亚组。这就需要在上面的 DataFrame 中增加一列 group,可以是二值变量,也可以是连续变量。

连续变量的着色我会单独处理,因为它对应的是 colormap 映射,而不是离散图例。离散分组则适合用颜色和 marker 双重编码,这样即使黑白打印也能区分。如果你打算让读者在黑白打印下也能读懂图的含义,建议 marker 的形状差异比颜色差异更重要。

3. 核心绘制:用 matplotlib 搭出多散点子图的全过程

数据就绪后,进入正式绘图阶段。我用的主力工具是 matplotlib,因为它的可控性最强,每个像素级别的细节都能调整。seaborn 虽然方便,但到了需要精准控制出版细节的时候,还是得回到 matplotlib 手动调。

3.1 画布、子图布局与共享轴设定

先确定整体布局:2×4 的网格,每个面板大小适合阅读。Nature 的图通常单栏宽度 8.9cm,双栏宽度 18.3cm,我们作图时用英寸表示,双栏图常见宽度是 7~8 英寸。配合 dpi=300,这个尺寸导出的图清晰度足够。

一种做法是直接用 plt.subplots 创建 2 行 4 列的子图,再用 sharex 和 sharey 参数统一坐标轴范围。这是最省力的方式,也是多散点子图能保持一眼可比性的关键。

import matplotlib.pyplot as plt import matplotlib as mpl mpl.rcParams.update({ "font.size": 8, "axes.titlesize": 8, "axes.labelsize": 8, "xtick.labelsize": 7, "ytick.labelsize": 7, "axes.linewidth": 0.8, "xtick.direction": "out", "ytick.direction": "out", "xtick.major.size": 3, "ytick.major.size": 3, "font.family": "sans-serif", }) fig, axes = plt.subplots(2, 4, figsize=(7, 3.6), sharex=True, sharey=True, constrained_layout=True) axes = axes.flatten()

这里我把全局字体压到 8pt,和出版图的字号一致。很多人画图时用默认字号 10pt 或者更大,出来的图放进论文页面里就显得笨重。字号小不代表看不清,配合高清晰度导出,打印出来非常精致。

3.2 散点参数的选择逻辑:点大小、透明度与边缘线

散点参数是整个图观感的基石。我最终确定的一套参数是:

  • s=18:点面积适中,太大会糊成一片,太小则信息密度低;
  • alpha=0.7:透明度兼顾点重叠区域的遮挡问题和整体辨识度;
  • facecolor 和 edgecolor 分离:填充色和边缘色用不同颜色,能显著提升点的清晰度;
  • linewidth=0.3:边缘线越细越精致,太粗会让点显得廉价。

为什么边缘色要有?因为在点密度高的区域,透明填充重叠后颜色会变深,边缘线的存在能帮助分隔个体点。白色边缘线是散点图里最经典的做法,尤其适合深色填充。

colors = plt.cm.viridis(np.linspace(0, 1, 8)) for ax, panel_id, c in zip(axes, df["panel"].unique(), colors): sub = df[df["panel"] == panel_id] ax.scatter(sub["x"], sub["y"], s=18, c=[c], alpha=0.7, facecolors="none", edgecolors="white", linewidths=0.3, label=None) ax.set_title(panel_id, pad=4)

注意一个小细节:facecolors="none" 和 c=[c] 同时使用时,填充色被边缘色替代,这句代码的意图是画空心散点。空心散点在高重叠度数据里很占优势,因为填充点叠在一起后会变成一个大色块,空心点可以保持个体的边界清晰。这也是我在复现过程里常用空心点方案的原因。

3.3 拟合线与统计注释:把“相关性”说清楚

多散点子图里最常见的统计元素是线性拟合线和相关系数标注。如果你画散点图却不加这些,读者只能靠肉眼估计趋势,这在学术图表里是不合格的。当我们看到原图角落有 r 和 p 值时,复现时就要把这部分补上。

计算部分我用 scipy.stats.pearsonr,然后绘制回归线。回归线的绘制有两种思路:一种是用 np.polyfit 做一次多项式拟合,另一种是用 scipy.stats.linregress 拿到斜率和截距。效果一样,linregress 的好处是同时返回 r、p 和标准误,方便写注释。

from scipy.stats import pearsonr, linregress def add_regression(ax, x, y, color="black"): res = linregress(x, y) xs = np.linspace(x.min(), x.max(), 100) ax.plot(xs, res.intercept + res.slope * xs, color=color, lw=0.8, ls="--", alpha=0.8) r, p = pearsonr(x, y) ax.text(0.05, 0.9, f"$r={r:.2f}$", transform=ax.transAxes, fontsize=7, va="top") ax.text(0.05, 0.78, f"$p={p:.2g}$", transform=ax.transAxes, fontsize=7, va="top")

关于 r 和 p 的显示位置,放在面板左上角的做法最常见,因为它不遮挡点云主体。但具体位置要看数据的分布,如果左上角被占满,就要移到右上角,这个得在生成图后目测调整。

3.4 坐标轴美化的几个细节:spines、刻度与标签

出版图表的坐标轴是有“规范”的。Nature 风格倾向于至少隐去上方和右侧的边框,也就是只保留左侧 y 轴和底部 x 轴的线,其余两边隐藏。这样图面更干净,信息不被多余的线条干扰。

刻度方向设为向外,这是学术图表的默认偏好。刻度线的长度和粗细已经在前面的 rcParams 里设好了,不需要每个子图重复设置。坐标轴标签放在轴的外侧,用 8pt 字号,不要加粗,不要斜体。

for ax in axes: ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.tick_params(width=0.8, colors="black") ax.set_xlabel("X value", fontsize=8) ax.set_ylabel("Y value", fontsize=8)

这里有个常见的坑:如果用了 sharex=True,set_xlabel 会在每个子图上都显示 x 轴标签,导致重复标签堆在底部。正确的做法是只给最下面一行子图设置 xlabel,或者直接留到整体调整后再统一标注。我个人更倾向于只给底部一行加标签,避免标签重叠。

3.5 颜色选择:出版级配色不是越鲜艳越好

颜色是整个多散点子图里最容易翻车的地方。经验不足的人喜欢用一堆高饱和颜色,出来的效果像玩具。观察那些复现得好的 Nature 图,颜色通常是这样几类:单一色系的深浅渐变、经典科学色带(viridis、inferno、cividis)、或者低饱和度的专业配色。

在模拟数据里,我给每个面板分配了 viridis 色带上的一个采样颜色。viridis 在色觉障碍人群里也能分辨,并且在灰度打印后仍然有序。对于一般图表,蓝、橙、红这三种低饱和主色已经够用。

如果你要复现的原图是黑白灰色系,那更简单:点用灰色填充、白色边缘,回归线用黑色虚线。这种极简风格在 Nature 论文里非常常见,也最难出错。

4. 常见问题与调参细节:我踩过的坑和解决办法

画图这件事,只有真正动手才会遇到那些文档里查不到的问题。下面这些问题我都在实际复现过程中碰到过,写出来帮你少走弯路。

4.1 点重叠导致视觉混乱怎么办

当样本量大,或者点聚集在某个区间时,散点图会变成一团黑。这时有几种处理手段:

  • 降低 alpha:透明度降到 0.3~0.5,重叠区域的深色级别就会淡很多;
  • 改用空心点:facecolors="none",保留白色边缘,个体分界更清楚;
  • 抽稀:如果样本量过万,随机抽取一部分点作为显示样本,再叠加密度估计;
  • 抖动:对离散坐标变量(比如实验重复编号)加一点随机噪声让点分开。

我最常用的是“透明度 + 空心点”组合。这个组合在样本量几千的场景下都非常稳。

4.2 坐标轴标签被裁切的经典原因

用 constrained_layout 或 tight_layout 可以解决大多数子图间距问题,但如果你在 7 英寸画布上画了太多的子图,再大的间距也救不回来。更常见的是:子图内部有长标题或长刻度标签被裁掉。

我建议在代码早期就打开 constrained_layout=True,不要等到画完了再靠手工调。另一个容易被忽略的是坐标轴标签的方向:如果 y 轴标签是横着放的,它占用的宽度会和竖着放完全不同。在窄面板中,我会把 y 轴标签旋转为水平方向放左上方,避免挤压绘图区域。

4.3 保存图片时锯齿、字体和分辨率问题

保存时最稳妥的配置是:

fig.savefig("multi_scatter.png", dpi=300, bbox_inches="tight", facecolor="white") fig.savefig("multi_scatter.pdf", bbox_inches="tight")

同时输出 PNG 和 PDF。PNG 用于 PPT、组会汇报和在线预览,PDF 用于论文投稿。pdf 是矢量格式,不会因为放大而变糊。如果你只保存 PNG,投搞时 dpi 不足 300 会被要求返工。

字体嵌入是另一个常见的坑:本地用中文字体没问题,但换一台电脑跑同样的代码,可能显示方框。解决方案是统一用标准 sans-serif 字体,或者安装并注册你要用的字体,然后设置 font.family 和 font.sans-serif 列表。

4.4 常见错误速查表

  • 坐标轴范围不一致:共享轴参数漏了,或手动设置范围时写错位置
  • 图例重复:循环里给每个点都加了 label,图例出现多次
  • 注释文字重叠:多行注释放在同一位置没调整偏移,需要手动微调 y
  • 点色与图例不一致:颜色列表与分组列表顺序不同,用一致的枚举顺序即可规避
  • 数据标准化失败:y 的公式里忘了开 sqrt,导致相关系数数值偏差

这些问题的共性原因,多半是设计阶段没理清面板顺序,或者数据分组枚举顺序和颜色顺序错位。最简单的方法是把所有面板名和对应的颜色、相关系数放在同一个配置表里,画图循环里去读取这个表,而不是分散写在多个列表里。

5. 进阶扩展:当数据变复杂时,多散点子图还能怎么改

基础版的多散点子图学会后,你会发现它在很多场景都能用。我想再分享三个变体,它们都在实际项目中派上过用场。

5.1 分组着色加上标记编码

如果每个面板里有两类样本,需要在散点里区分开来,可以按组别设置不同的 marker 和颜色。marker 建议用圆形和三角形这类差异明显的形状,而不是圆形和星形这种容易混淆的。颜色可以用两套低饱和色,比如蓝灰和橙色,并统一加白边。

5.2 散点加边际分布图

多散点子图的另一个常见升级版是:主区域画散点,上侧和右侧附带对应变量的直方图或箱线图。这样单张图同时展示了数据点的二维分布和每个维度的边缘分布。实现方式可以用 gridspec 创建三块区域:一个主图、一个顶部条形区、一个右侧条形区,然后用 GridSpec 的宽度比例和高度比例控制主图的占幅。

5.3 多面板联动着色和动态交互

如果你要在网页上发布动态版本,可以把 matplotlib 的参数逻辑迁移到 plotly 或 bokeh。动态图的优势是可以悬浮查看每个点的数值、放大局部区域。复现思路上依旧要先构造统一的长表结构,再一次性传递给绘图函数。这套数据结构的迁移成本很小,因为画图逻辑从未变过:一个面板编号对应一组坐标和一组样式参数。

这三种扩展思路,都不会影响已经写好的核心绘图参数,只是在数据或布局层面做增量调整。这也是为什么我强调最开始就要把数据结构设计成长表:后续加维度、加子图、加交互,都只需要在 DataFrame 上追加列。

复现完这张多散点子图,我自己最大的体会是:画图工具只是手段,真正决定图表质量的,是你有没有在动手前理清“这张图要回答什么问题”。Nature 图里多散点子图的成功,在于它让每个面板都保持克制,只做一件事,但八张面板合在一起,却把一个复杂的对比问题讲得清清楚楚。如果你准备自己动手复现,我建议先从模拟数据练手,把所有参数调到成熟,再换真实数据。有真实数据时也别想着一次性画完美,先出低保真度版本,再逐步调整字号、颜色、坐标轴这些细节,最终的结果基本不会差。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询