当前位置:首页>python>期刊配图复现 | 手把手教你用 Python 绘制 SHAP 全局与局部解释组合图

期刊配图复现 | 手把手教你用 Python 绘制 SHAP 全局与局部解释组合图

  • 2026-09-10 16:35:00
期刊配图复现 | 手把手教你用 Python 绘制 SHAP 全局与局部解释组合图

01

前言

在机器学习辅助材料发现(Materials Discovery)的浪潮中,我们经常面临一个痛点:模型跑分很高,但审稿人总问“Why?”—为什么你的模型认为这个材料好?这时候,单纯的R^2或 RMSE 已经无法满足顶刊对“机理解释”的渴望。

今天要复刻的这张图,来自 Journal of Environmental Chemical Engineering (2025) 的一篇关于 MOF 甲烷吸附的研究 。作者不仅仅预测了吸附量,更使用了 SHAP (Shapley Additive Explanations) 方法,通过一张极其优雅的 “Bar + Beeswarm” 组合图(即文中 Fig. 3),一站式展示了特征的全局重要性(Global Importance)和局部影响方向(Local Interpretation) 。

这张图的精妙之处在于:左边告诉你“谁重要”,右边告诉你“它是怎么影响结果的(正相关还是负相关)”。很多同学直接调用 shap 库的默认绘图函数,往往面临字体无法统一、DPI 不够、无法排版进大图的尴尬。

今天,我们就用 Python (Matplotlib),把这张图从底层逻辑完整复现。我们要做的不是简单的“调包”,而是像搭积木一样,把左边的条形图和右边的蜂群图精准对齐,并注入符合出版要求的审美灵魂。

02

原图解析

在写代码之前,我们需要像外科医生一样,先对这张图进行“解剖”。只有看懂了结构,才能写对代码。

论文原图

Sub-section 2: 读懂神图 (Scientific Decoding)

别光顾着画,这图到底说了个啥?如果你看不懂图里的科学逻辑,画出来也是没有灵魂的躯壳。来,我带你读懂它:

这张图揭示了在 5.8 bar 低压条件下,哪些 MOF 特征决定了甲烷吸附量 。

看懂了吗?左边看“地位”,右边看“态度”。

03

代码复刻

Step 1 全局配置

“高手画图,都是先立规矩。” 我们先定义好字体、字号和线条粗细,确保出图就是期刊标准,不用后期在 AI 里一个个改字。

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 画布构建

这里不能简单用 plt.subplots(1, 2)。因为原文中,左图(Bar)和右图(Beeswarm)的宽度比例不是 1:1,而且中间的间距(wspace)需要精细控制。我们要用 GridSpec 来切蛋糕。

# 2. 构建画布与网格布局fig = plt.figure(figsize=(10, 6), dpi=150)# width_ratios=[1, 1.5]: 让右边的散点图宽一点,展示更多细节# wspace=0.05: 让两张图紧紧挨在一起,看起来像一张图gs = gridspec.GridSpec(1, 3, width_ratios=[0.8, 1.2, 0.05], wspace=0.05)ax_bar = fig.add_subplot(gs[0])       # 左面板ax_bee = fig.add_subplot(gs[1])       # 右面板ax_cbar = fig.add_subplot(gs[2])      # 色条面板 (最右侧细条)# 锁定 Y 轴范围,确保两图每一行严格对齐y_pos = np.arange(n_features)ax_bar.set_ylim(-0.5, n_features - 0.5)ax_bee.set_ylim(-0.5, n_features - 0.5)# 隐藏右图的 Y 轴刻度标签,因为它共享左图的标签ax_bee.set_yticks([]) # 但保留 Y 轴的 Tick 线吗?原文中右图中间有一条灰线,我们稍后处理
Step 3 左面板:条形图 (Bar Chart)

这一步比较基础,但要注意细节:颜色选择深蓝色,加上黑色边框增加质感。

# 3. 绘制左图:Global Interpretation(Bar Chart)# 使用 barh 绘制水平条形图bars = ax_bar.barh(y_pos, importance, height=0.6,                    color='#1f77b4', edgecolor='black', linewidth=0.8, zorder=3)# 设置标签ax_bar.set_yticks(y_pos)ax_bar.set_yticklabels(feature_names, fontsize=11, fontweight='bold')ax_bar.set_xlabel('mean absolute SHAP value', fontsize=11)# 美化左图:去顶线和右线ax_bar.spines['top'].set_visible(False)ax_bar.spines['right'].set_visible(False)ax_bar.xaxis.set_ticks_position('bottom')ax_bar.yaxis.set_ticks_position('left')# 添加网格线,放在底层 (zorder=0)ax_bar.grid(axis='x', linestyle='--', alpha=0.5, zorder=0)# 反转 X 轴?不,原文是正常的。但为了好看,我们可以留点余量ax_bar.set_xlim(0, max(importance) * 1.1)# 标题ax_bar.set_title("Global interpretation", fontsize=12, pad=10)

Step 4 右面板:蜂群散点图 (Beeswarm Plot)

这是整张图的灵魂。我们需要模拟 shap 库的抖动效果。这里我用了一个简单的随机抖动(Jitter)技巧来实现蜂群效果,配合颜色映射。

# 4. 绘制右图:Local Interpretation(Beeswarm Style)# 定义颜色映射:从蓝(低)到红(高)cmap = plt.get_cmap('coolwarm')norm = mcolors.Normalize(vmin=0, vmax=1) # 归一化特征值# 循环绘制每一行特征的散点for i in range(n_features):    # 提取当前特征的数据    s_vals = shap_vals[i] # SHAP 值 (X坐标)    f_vals = feature_vals[i] # 特征值 (颜色)    # 核心技巧:计算 Jitter (Y坐标抖动)    # 这里用简单的随机抖动模拟蜂群效果    # 实际项目中可以使用核密度估计来做更完美的 beeswarm    y_jitter = np.random.normal(loc=i, scale=0.08, size=n_samples)    # 限制抖动范围,防止点溢出到别的行    y_jitter = np.clip(y_jitter, i - 0.3, i + 0.3)    # 绘制散点    sc = ax_bee.scatter(s_vals, y_jitter,                         c=f_vals, cmap=cmap, norm=norm,                         s=15, alpha=0.7, edgecolors='none', zorder=3)# 添加基准线 x=0ax_bee.axvline(x=0, color='gray', linestyle='-', linewidth=1, zorder=1)# 添加行分割虚线for y in y_pos:    ax_bee.axhline(y=y, color='gray', linestyle=':', alpha=0.3, zorder=0)ax_bee.set_xlabel('SHAP value', fontsize=11)ax_bee.set_title("Local interpretation", fontsize=12, pad=10)# 美化右图:只保留底轴ax_bee.spines['top'].set_visible(False)ax_bee.spines['left'].set_visible(False) # 这一步很关键,隐藏左轴线ax_bee.spines['right'].set_visible(False)

Step 5 注入灵魂

最后,我们要把色条(Legend)加上去。原文的色条不是在下面,而是在右侧,并且标注了 "Low" 和 "High"。

# 5. 添加色条 (Colorbar)# 在我们预留的 ax_cbar 上画色条cb = plt.colorbar(sc, cax=ax_cbar, orientation='vertical')# 设置色条标签:只显示 Low 和 Highax_cbar.set_ylabel('Feature value', fontsize=11, labelpad=10)# 隐藏中间刻度,只保留两端文本cb.set_ticks([]) # 手动添加文本ax_cbar.text(1.5, 0.02, 'Low', transform=ax_cbar.transAxes, ha='left', va='center', color='black')ax_cbar.text(1.5, 0.98, 'High', transform=ax_cbar.transAxes, ha='left', va='center', color='black')# 调整色条样式cb.outline.set_visible(False) # 去掉色条边框,更有现代感# 最终布局调整plt.tight_layout() # 虽然用了GridSpec,有时候微调还是需要的plt.show()

对比我们生成的图(复刻图)和原图 :

  • 结构:左右面板的比例和对齐方式完全复刻了原图的逻辑。

  • 细节:左图的 Bar 带有黑色边框,增加了硬朗感;右图的散点通过 alpha 参数处理了重叠,这在原图处理几万个数据点时尤为重要。

  • 改进:原图中右侧散点图的 y 轴其实有一条灰色的轴线(spine),我们在代码中通过 ax_bee.spines['left'].set_visible(False) 去掉了,这在现代审美中更显干净,当然如果你想 100% 还原,可以设为 True 并把颜色设淡。

复刻图

04

多配色参考

多配色参考

05

代码获取

👇 关注公众号【嗡嗡的Python日常】

🚫 关于源码: 本文核心代码为原创定制,暂不免费公开。

✅ 如果你需要:

  • 购买本项目完整源码 + 数据

  • 定制类似的科研绘图

  • 请直接添加号主微信沟通(有偿分享☕️): Wjtaiztt0406

微信号丨Wjtaiztt0406

最新文章

随机文章