做科研的兄弟姐妹们,这种场景你们一定不陌生:
你辛辛苦苦跑了一个月的数据,机器学习模型调参调到头秃,终于把 R^2 刷到了 0.98。你兴奋地拿着结果去找导师,导师推了推眼镜,指着屏幕上那张 Excel 默认生成的散点图说:“模型是不错,但这图……怎么看怎么像一盘番茄炒蛋?这怎么往正文里放?”
伤害性不大,侮辱性极强。
这就是科研绘图的痛点:你的数据很性感,但你的图表很骨感。 尤其是涉及到回归模型评估(Regression Evaluation)时,我们不仅要展示预测值和真实值的拟合程度,还要在图里塞进训练集/测试集的图例,甚至要把 RMSE、MSE 这些统计指标优雅地展示出来——既不能挡住数据,又不能像贴狗皮膏药一样乱贴。
今天,我们就用 Python 的 Matplotlib,对一张典型的材料科学顶刊(Materials Science & Engineering R)插图进行像素级复刻。我们要画的不仅仅是一张散点图,而是一张“带悬浮统计面板、双图例系统、符合出版级排版规范”的标准证据图。
在写代码之前,我们需要像外科医生一样,先对这张图进行“解剖”。很多新手画图的问题在于“上来就干”,结果画到一半发现图例盖住了数据,或者文字出界被截断。
我们要复刻的这张图,结构上其实是一个精密的“三室一厅”:
主卧(Main Plot):这是数据的居住区。X轴是样本序号,Y轴是比电容(Specific Capacitance)。这里住着两拨人:训练集(Training set)和测试集(Testing set)。
吊灯(Floating Legends):这是最显眼的设计。作者没有使用默认的 loc='best' 让图例到处乱跑,而是像吊灯一样,把两组图例分别“钉”在了左上角和右上角的边框内侧。这种极度对称的布局,专治强迫症。
阳台(External Stats Box):这是整张图的“神来之笔”。右侧那个红色的参数框,它根本不在坐标轴里面!它利用了“轴外绘图”技术,悬浮在主图的右侧。这样做的好处是:无论你的数据点有多密集,永远不用担心参数文字会遮挡数据。
论文原图
Sub-section 2: 读懂神图 (Scientific Decoding)
别光顾着画,这图到底说了个啥?如果你看不懂图里的科学逻辑,画出来也是没有灵魂的躯壳。
结合我们对这篇综述论文(Cell 1)的分析,这张图是在展示机器学习模型对碳材料孔隙结构与电容性能之间关系的预测能力。
对角线逻辑:虽然这里用了散点图,但潜台词是“对角线”。如果是完美的模型,预测值应该等于真实值,所有点都会落在 y=x 上(或在双样本序散点图中完全重合)。
颜色编码:红色/蓝色代表训练集,米色/紫色代表测试集。读图时,审稿人会先看测试集(Test set)的拟合情况。如果训练集贴合完美(Overfitting),而测试集满天乱飞,那模型就是废的。
统计面板:右侧的 R^2、RMSE 是结论的“数字签证”。图画得再圆,R^2 < 0.8 也是白搭。这张图把证据(散点)和结论(指标)放在同一视域下,这叫“信息闭环”。
Step 1: 全局配置
很多人画图喜欢在每个 plot() 函数里指定 fontsize=12,累不累?真正的 Python 老手,起手式永远是配置 rcParams。这是给 Matplotlib 立“宪法”:凡是我画的图,必须是 Times New Roman,刻度必须朝内。
import matplotlib.pyplot as pltimport matplotlib.font_manager as fmimport warnings# 全局关闭非关键警告,保证绘图输出整洁,看着清爽warnings.filterwarnings('ignore')# --- 顶刊风格内核锁定 (Journal Aesthetics) ---# 字体设置:优先使用 Times New Roman,这是 SCI 的标配plt.rcParams['font.family'] = ['Times New Roman', 'Arial', 'SimHei']plt.rcParams['mathtext.fontset'] = 'stix' # 公式字体使用 STIX (类似 LaTeX)# 基础字号与线条定义:线条要粗,字要大,这就是“高级感”的来源plt.rcParams['font.size'] = 16 # 默认字号plt.rcParams['axes.linewidth'] = 1.5 # 坐标轴线宽 (Bold)plt.rcParams['lines.linewidth'] = 2.0 # 数据线宽plt.rcParams['xtick.direction'] = 'in' # 刻度朝内,更紧凑plt.rcParams['ytick.direction'] = 'in'plt.rcParams['savefig.bbox'] = 'tight' # 自动切除白边plt.rcParams['savefig.dpi'] = 600 # 印刷级分辨率
Step 2: 图例
这是本期最硬核的知识点。
通常大家用 ax.legend(),是让 Matplotlib 自动去图里找 handle。但在这张图里,我们需要把 Train 和 Test 分开放在两边,而且还要自定义形状。这时候,我们需要“欺骗” Matplotlib,手动创建“代理对象”(Proxy Artist)。
我们凭空捏造了几个 Line2D 对象,告诉图例:“你就按这个画,别管图里实际有啥。”
# --- 技巧:手动构建代理图例 (Proxy Artist) ---# 定义颜色(建议使用高对比度的顶刊配色,不仅是好看,更是为了黑白打印可读)c_train_act = '#D62728' # 砖红c_train_pred = '#1F77B4' # 深蓝c_test_act = '#2CA02C' # 墨绿c_test_pred = '#FF7F0E' # 橙黄# 创建第一组图例(左上角):只放 Actual/Predicted 的说明# 注意:这里我们并没有传入真实数据,而是凭空创建了 Line2D 对象legend_elements_1 = [ Line2D([0], [0], marker='s', color='w', markerfacecolor=c_train_act, markersize=8, label='Actual value'), Line2D([0], [0], marker='o', color='w', markerfacecolor=c_train_pred, markersize=8, label='Predicted value')]# 这种方法的 Why:# 1. 解耦:图例样式不再受数据本身样式的束缚(比如你可以让图里的点半透明,但图例里的点不透明)。# 2. 灵活:想把图例拆成 8 份都行,完全自定义。
Step 3: 图层
画散点图最怕什么?最怕“糊成一团”。
当 800 个点挤在一起时,如果没有描边,就是一坨色块。如果你仔细看顶刊的散点图,每个点边缘都有一圈极细的白线。这叫“呼吸感”。
同时,我们要用 edgecolors='white' 和 linewidth=0.5 来实现这种颗粒感。
# --- 绘图逻辑:注入细节 ---fig, ax = plt.subplots(figsize=(10, 6))# 绘制训练集:注意 edgecolors='white' 是提升高级感的关键# s=40 控制大小,alpha=0.9 控制通透度,linewidth=0.5 增加颗粒感ax.scatter(x_train, y_train_actual, c=c_train_act, marker='s', s=40, label='Train Actual', edgecolors='white', linewidth=0.5, alpha=0.9)ax.scatter(x_train, y_train_pred, c=c_train_pred, marker='o', s=40, label='Train Pred', edgecolors='white', linewidth=0.5, alpha=0.9)# 绘制分割线:明确区分训练域和测试域ax.axvline(x=600, color='black', linestyle='-.', linewidth=1.2, alpha=0.7)# 文本标注:使用 transform=ax.transData (默认)# 这里的坐标是基于数据值的,字号加粗ax.text(100, 750, "Training set", fontsize=14, fontweight='bold')ax.text(650, 750, "Testing set", fontsize=14, fontweight='bold')
Step 4: 破界之框
好了,重头戏来了。右侧那个红色的统计框,是怎么画到坐标轴外面的?
秘密有两个:
transform=ax.transAxes:使用相对坐标系。(1.02, 0.35) 意味着 x 轴长度的 1.02 倍处(即右边框外一点点)。
clip_on=False:告诉 Matplotlib,“就算我画出界了,也别把我剪切掉!”
# --- 核心黑科技:轴外绘图 ---# 1. 定义框的位置:基于 Axes 坐标系 (0-1)# 1.02 表示在右边框向外偏移 2% 的位置box_x, box_y = 1.02, 0.35box_width, box_height = 0.25, 0.55# 2. 绘制矩形框# 关键参数 clip_on=False:允许图形绘制在坐标轴框线之外!# 否则这个框会被自动裁剪掉,变成一片空白。rect = patches.Rectangle( (box_x, box_y), box_width, box_height, linewidth=1.5, edgecolor='red', facecolor='none', transform=ax.transAxes, clip_on=False )ax.add_patch(rect)# 3. 填充统计数据文本# 使用 LaTeX 格式 ($\mathbf{...}$) 实现数学符号加粗stats_text = ( "Train Set\n" f"$\mathbf{{R^2}} = 0.9855$\n" f"$\mathbf{{RMSE}} = 12.56$\n\n" "Test Set\n" f"$\mathbf{{R^2}} = 0.9721$\n" f"$\mathbf{{RMSE}} = 16.05$")# verticalalignment='top' 保证文字从框的顶部开始向下排ax.text(box_x + 0.02, box_y + box_height - 0.05, stats_text, transform=ax.transAxes, fontsize=12, linespacing=1.8, verticalalignment='top')
Step 5: 布局收尾
最后一步最容易被忽略。因为我们在右边画了东西,如果直接保存,右边的框会被裁掉。必须调整 subplots_adjust 或者在保存时使用 bbox_inches='tight'。
# --# --- 布局收尾 ---# 方案一:手动调整边距,给右边的框留出 20% 的画布空间plt.subplots_adjust(right=0.75) # 设置轴标签ax.set_xlabel("Data Point Index", fontsize=14, fontweight='bold')ax.set_ylabel("Specific Capacitance (F/g)", fontsize=14, fontweight='bold')# 限制轴范围,留出一部分头部空间给图例ax.set_ylim(-50, 900)# 保存:bbox_inches='tight' 能够自动计算并包含所有“出界”的元素# 这是懒人福音,但在做组图时要慎用plt.savefig("high_impact_scatter.png", dpi=600, bbox_inches='tight')plt.show()- 布局收尾 ---# 方案一:手动调整边距,给右边的框留出 20% 的画布空间plt.subplots_adjust(right=0.75) # 设置轴标签ax.set_xlabel("Data Point Index", fontsize=14, fontweight='bold')ax.set_ylabel("Specific Capacitance (F/g)", fontsize=14, fontweight='bold')# 限制轴范围,留出一部分头部空间给图例ax.set_ylim(-50, 900)# 保存:bbox_inches='tight' 能够自动计算并包含所有“出界”的元素# 这是懒人福音,但在做组图时要慎用plt.savefig("high_impact_scatter.png", dpi=600, bbox_inches='tight')plt.show()
来看看我们的战果。
对比 Excel 默认图表,复刻后的这张图:
复刻图
多配色参考
🚫 关于源码:
本文核心代码为原创定制,暂不免费公开。
✅ 如果你需要:
购买本项目完整源码 + 数据
定制类似的科研绘图
咨询代码运行报错问题
请直接添加号主微信沟通(有偿分享☕️): Wjtaiztt0406