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
原图解析
在写代码之前,我们需要像外科医生一样,先对这张图进行“解剖”。只有看懂了结构,才能写对代码。
🦴 骨架 (Structure):这是一个典型的 双面板(Two-panel) 布局。
左面板:水平条形图 (Horizontal Bar Chart)。X 轴是 SHAP 绝对值的平均值(Mean Absolute SHAP Value),代表特征的“统治力”;Y 轴是特征名称。
右面板:蜂群图 (Beeswarm/Strip Plot)。X 轴是 SHAP Value(实际贡献值),Y 轴与左图严格对齐。每一个点代表一个样本(MOF 结构)。
布局技巧:这两个图必须共享 Y 轴的刻度位置,但左图只显示标签,右图隐藏 Y 轴标签,中间留出极小的缝隙(Gap),形成视觉上的连贯性。
🍰 核心技巧 (The Trick):
对齐的艺术:右图的散点必须准确地落在左图对应条形的“中心线”上。这需要极高精度的坐标控制。
颜色映射:右图使用了 “红-蓝”发散色阶(Red-Blue Diverging Colormap)。红色代表特征值高(High Feature Value),蓝色代表特征值低(Low Feature Value)。注意,这里颜色的含义是“特征本身的数值”,而点的位置(X轴)代表“对预测结果的影响”。
🎨 配色 (Palette):
左边条形图使用了稳重的 深蓝色 (#1f77b4 变体),带有黑色边框,强调确定性。
右边使用了 Coolwarm 或类似的渐变色,同时配合半透明度(Alpha),处理大量样本重叠带来的视觉拥堵问题。

论文原图
Sub-section 2: 读懂神图 (Scientific Decoding)
别光顾着画,这图到底说了个啥?如果你看不懂图里的科学逻辑,画出来也是没有灵魂的躯壳。来,我带你读懂它:
这张图揭示了在 5.8 bar 低压条件下,哪些 MOF 特征决定了甲烷吸附量 。
左图排序:排第一的 f-lig-S-2(配体共价半径相关特征)柱子最长,说明它对模型预测的影响最大 。
右图趋势:看 f-lig-S-2 对应的右侧散点。红点(特征值高)主要分布在 0 线右侧(SHAP > 0),蓝点(特征值低)在左侧。这意味着:该特征数值越大,越能提升甲烷吸附量(正相关) 。
反直觉现象:看第五个特征 Vf (孔隙率相关)。它的红点(高 Vf)竟然分布在左侧(SHAP < 0)!这说明在低压下,过大的孔隙率反而可能不利于吸附(或者说某种特定范围更优),这与我们通常认为的“孔越大吸附越多”形成了有趣的科学反差 。
看懂了吗?左边看“地位”,右边看“态度”。

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 线吗?原文中右图中间有一条灰线,我们稍后处理
这一步比较基础,但要注意细节:颜色选择深蓝色,加上黑色边框增加质感。
# 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坐标抖动)# 这里用简单的随机抖动模拟蜂群效果# 实际项目中可以使用核密度估计来做更完美的 beeswarmy_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