Files
skillhub-014-scientific-vis…/references/matplotlib_examples.md
T
2026-07-13 21:35:36 +08:00

18 KiB
Raw Blame History

可发表级别的 Matplotlib 示例

概述

本参考文档提供了使用 Matplotlib、Seaborn 和 Plotly 创建可发表级别的科学示意图的实用代码示例。所有示例均遵循 publication_guidelines.md 中的最佳实践,并使用 color_palettes.md 中的色盲友好调色板。

设置与配置

发表级别 Matplotlib 配置

import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np

# 设置发表质量参数
mpl.rcParams['figure.dpi'] = 300
mpl.rcParams['savefig.dpi'] = 300
mpl.rcParams['font.size'] = 8
mpl.rcParams['font.family'] = 'sans-serif'
mpl.rcParams['font.sans-serif'] = ['Arial', 'Helvetica']
mpl.rcParams['axes.labelsize'] = 9
mpl.rcParams['axes.titlesize'] = 9
mpl.rcParams['xtick.labelsize'] = 7
mpl.rcParams['ytick.labelsize'] = 7
mpl.rcParams['legend.fontsize'] = 7
mpl.rcParams['axes.linewidth'] = 0.5
mpl.rcParams['xtick.major.width'] = 0.5
mpl.rcParams['ytick.major.width'] = 0.5
mpl.rcParams['lines.linewidth'] = 1.5

# 使用色盲友好颜色(Okabe-Ito 调色板)
okabe_ito = ['#E69F00', '#56B4E9', '#009E73', '#F0E442',
             '#0072B2', '#D55E00', '#CC79A7', '#000000']
mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=okabe_ito)

# 使用感知均匀的色图
mpl.rcParams['image.cmap'] = 'viridis'

保存辅助函数

def save_publication_figure(fig, filename, formats=['pdf', 'png'], dpi=300):
    """
    以多种格式保存图形以供发表。

    参数
    -----------
    fig : matplotlib.figure.Figure
        要保存的图形
    filename : str
        基础文件名(不含扩展名)
    formats : list
        要保存的文件格式列表 ['pdf', 'png', 'eps', 'svg']
    dpi : int
        光栅格式的分辨率
    """
    for fmt in formats:
        output_file = f"{filename}.{fmt}"
        fig.savefig(output_file, dpi=dpi, bbox_inches='tight',
                   facecolor='white', edgecolor='none',
                   transparent=False, format=fmt)
        print(f"已保存: {output_file}")

示例 1:带误差线的折线图

import matplotlib.pyplot as plt
import numpy as np

# 生成示例数据
x = np.linspace(0, 10, 50)
y1 = 2 * x + 1 + np.random.normal(0, 1, 50)
y2 = 1.5 * x + 2 + np.random.normal(0, 1.2, 50)

# 计算分箱数据的均值和标准误差
bins = np.linspace(0, 10, 11)
y1_mean = [y1[(x >= bins[i]) & (x < bins[i+1])].mean() for i in range(len(bins)-1)]
y1_sem = [y1[(x >= bins[i]) & (x < bins[i+1])].std() /
          np.sqrt(len(y1[(x >= bins[i]) & (x < bins[i+1])]))
          for i in range(len(bins)-1)]
x_binned = (bins[:-1] + bins[1:]) / 2

# 创建合适大小的图形(单栏宽度 = 3.5 英寸)
fig, ax = plt.subplots(figsize=(3.5, 2.5))

# 绘制带误差线的图
ax.errorbar(x_binned, y1_mean, yerr=y1_sem,
            marker='o', markersize=4, capsize=3, capthick=0.5,
            label='条件 A', linewidth=1.5)

# 添加带单位的标签
ax.set_xlabel('时间(小时)')
ax.set_ylabel('荧光强度(a.u.')

# 添加图例
ax.legend(frameon=False, loc='upper left')

# 移除顶部和右侧的轴脊线
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# 紧凑布局
fig.tight_layout()

# 保存
save_publication_figure(fig, 'line_plot_with_errors')
plt.show()

示例 2:多面板图形

import matplotlib.pyplot as plt
import numpy as np
from string import ascii_uppercase

# 创建多面板图形(双栏宽度 = 7 英寸)
fig = plt.figure(figsize=(7, 4))

# 定义面板网格
gs = fig.add_gridspec(2, 3, hspace=0.4, wspace=0.4,
                      left=0.08, right=0.98, top=0.95, bottom=0.08)

# 面板 A:折线图
ax_a = fig.add_subplot(gs[0, :2])
x = np.linspace(0, 10, 100)
for i, offset in enumerate([0, 0.5, 1.0]):
    ax_a.plot(x, np.sin(x) + offset, label=f'数据集 {i+1}')
ax_a.set_xlabel('时间(秒)')
ax_a.set_ylabel('振幅(V')
ax_a.legend(frameon=False, fontsize=6)
ax_a.spines['top'].set_visible(False)
ax_a.spines['right'].set_visible(False)

# 面板 B:柱状图
ax_b = fig.add_subplot(gs[0, 2])
categories = ['对照', '处理\nA', '处理\nB']
values = [100, 125, 140]
errors = [5, 8, 6]
ax_b.bar(categories, values, yerr=errors, capsize=3,
         color=['#0072B2', '#E69F00', '#009E73'], alpha=0.8)
ax_b.set_ylabel('响应(%')
ax_b.spines['top'].set_visible(False)
ax_b.spines['right'].set_visible(False)
ax_b.set_ylim(0, 160)

# 面板 C:散点图
ax_c = fig.add_subplot(gs[1, 0])
x = np.random.randn(100)
y = 2*x + np.random.randn(100)
ax_c.scatter(x, y, s=10, alpha=0.6, color='#0072B2')
ax_c.set_xlabel('变量 X')
ax_c.set_ylabel('变量 Y')
ax_c.spines['top'].set_visible(False)
ax_c.spines['right'].set_visible(False)

# 面板 D:热图
ax_d = fig.add_subplot(gs[1, 1:])
data = np.random.randn(10, 20)
im = ax_d.imshow(data, cmap='viridis', aspect='auto')
ax_d.set_xlabel('样本编号')
ax_d.set_ylabel('特征')
cbar = plt.colorbar(im, ax=ax_d, fraction=0.046, pad=0.04)
cbar.set_label('强度(a.u.', rotation=270, labelpad=12)

# 添加面板标签
panels = [ax_a, ax_b, ax_c, ax_d]
for i, ax in enumerate(panels):
    ax.text(-0.15, 1.05, ascii_uppercase[i], transform=ax.transAxes,
            fontsize=10, fontweight='bold', va='top')

save_publication_figure(fig, 'multi_panel_figure')
plt.show()

示例 3:带散点的箱线图

import matplotlib.pyplot as plt
import numpy as np

# 生成示例数据
np.random.seed(42)
data = [np.random.normal(100, 15, 30),
        np.random.normal(120, 20, 30),
        np.random.normal(140, 18, 30),
        np.random.normal(110, 22, 30)]

fig, ax = plt.subplots(figsize=(3.5, 3))

# 创建箱线图
bp = ax.boxplot(data, widths=0.5, patch_artist=True,
                showfliers=False,  # 我们将手动添加散点
                boxprops=dict(facecolor='lightgray', edgecolor='black', linewidth=0.8),
                medianprops=dict(color='black', linewidth=1.5),
                whiskerprops=dict(linewidth=0.8),
                capprops=dict(linewidth=0.8))

# 叠加个体散点
colors = ['#0072B2', '#E69F00', '#009E73', '#D55E00']
for i, (d, color) in enumerate(zip(data, colors)):
    # 为 x 位置添加抖动
    x = np.random.normal(i+1, 0.04, size=len(d))
    ax.scatter(x, d, alpha=0.4, s=8, color=color)

# 自定义
ax.set_xticklabels(['对照', '处理 A', '处理 B', '处理 C'])
ax.set_ylabel('细胞计数')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_ylim(50, 200)

fig.tight_layout()
save_publication_figure(fig, 'boxplot_with_points')
plt.show()

示例 4:带颜色条的热图

import matplotlib.pyplot as plt
import numpy as np

# 生成相关矩阵
np.random.seed(42)
n = 10
A = np.random.randn(n, n)
corr_matrix = np.corrcoef(A)

# 创建图形
fig, ax = plt.subplots(figsize=(4, 3.5))

# 绘制热图
im = ax.imshow(corr_matrix, cmap='RdBu_r', vmin=-1, vmax=1, aspect='auto')

# 添加颜色条
cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.set_label('相关系数', rotation=270, labelpad=15)

# 设置刻度和标签
gene_names = [f'基因{i+1}' for i in range(n)]
ax.set_xticks(np.arange(n))
ax.set_yticks(np.arange(n))
ax.set_xticklabels(gene_names, rotation=45, ha='right')
ax.set_yticklabels(gene_names)

# 添加网格
ax.set_xticks(np.arange(n)-.5, minor=True)
ax.set_yticks(np.arange(n)-.5, minor=True)
ax.grid(which='minor', color='white', linestyle='-', linewidth=0.5)

fig.tight_layout()
save_publication_figure(fig, 'correlation_heatmap')
plt.show()

示例 5Seaborn 小提琴图

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

# 生成示例数据
np.random.seed(42)
data = pd.DataFrame({
    'condition': np.repeat(['对照', '药物 A', '药物 B'], 50),
    'value': np.concatenate([
        np.random.normal(100, 15, 50),
        np.random.normal(120, 20, 50),
        np.random.normal(140, 18, 50)
    ])
})

# 设置样式
sns.set_style('ticks')
sns.set_palette(['#0072B2', '#E69F00', '#009E73'])

fig, ax = plt.subplots(figsize=(3.5, 3))

# 创建小提琴图
sns.violinplot(data=data, x='condition', y='value', ax=ax,
               inner='box', linewidth=0.8)

# 添加带状图
sns.stripplot(data=data, x='condition', y='value', ax=ax,
              size=2, alpha=0.3, color='black')

# 自定义
ax.set_xlabel('')
ax.set_ylabel('表达水平(AU')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

fig.tight_layout()
save_publication_figure(fig, 'violin_plot')
plt.show()

示例 6:带回归线的科学散点图

import matplotlib.pyplot as plt
import numpy as np
from scipy import stats

# 生成具有相关性的数据
np.random.seed(42)
x = np.random.randn(100)
y = 2.5 * x + np.random.randn(100) * 0.8

# 计算回归
slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)

# 创建图形
fig, ax = plt.subplots(figsize=(3.5, 3.5))

# 散点图
ax.scatter(x, y, s=15, alpha=0.6, color='#0072B2', edgecolors='none')

# 回归线
x_line = np.array([x.min(), x.max()])
y_line = slope * x_line + intercept
ax.plot(x_line, y_line, 'r-', linewidth=1.5, label=f'y = {slope:.2f}x + {intercept:.2f}')

# 添加统计文本
stats_text = f'$R^2$ = {r_value**2:.3f}\n$p$ < 0.001' if p_value < 0.001 else f'$R^2$ = {r_value**2:.3f}\n$p$ = {p_value:.3f}'
ax.text(0.05, 0.95, stats_text, transform=ax.transAxes,
        verticalalignment='top', fontsize=7,
        bbox=dict(boxstyle='round', facecolor='white', alpha=0.8, edgecolor='gray', linewidth=0.5))

# 自定义
ax.set_xlabel('预测变量')
ax.set_ylabel('响应变量')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

fig.tight_layout()
save_publication_figure(fig, 'scatter_regression')
plt.show()

示例 7:带阴影误差的时间序列图

import matplotlib.pyplot as plt
import numpy as np

# 生成时间序列数据
np.random.seed(42)
time = np.linspace(0, 24, 100)
n_replicates = 5

# 模拟多个重复
data = np.array([10 * np.exp(-time/10) + np.random.normal(0, 0.5, 100)
                 for _ in range(n_replicates)])

# 计算均值和标准误差
mean = data.mean(axis=0)
sem = data.std(axis=0) / np.sqrt(n_replicates)

# 创建图形
fig, ax = plt.subplots(figsize=(4, 2.5))

# 绘制均值线
ax.plot(time, mean, linewidth=1.5, color='#0072B2', label='均值 ± SEM')

# 添加阴影误差区域
ax.fill_between(time, mean - sem, mean + sem,
                alpha=0.3, color='#0072B2', linewidth=0)

# 自定义
ax.set_xlabel('时间(小时)')
ax.set_ylabel('浓度(μM')
ax.legend(frameon=False, loc='upper right')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_xlim(0, 24)
ax.set_ylim(0, 12)

fig.tight_layout()
save_publication_figure(fig, 'timeseries_shaded')
plt.show()

示例 8Plotly 交互式图形

import plotly.graph_objects as go
import numpy as np

# 生成数据
np.random.seed(42)
x = np.random.randn(100)
y = 2*x + np.random.randn(100)
colors = np.random.choice(['组 A', '组 B'], 100)

# Plotly 的 Okabe-Ito 颜色
okabe_ito_plotly = ['#E69F00', '#56B4E9']

# 创建图形
fig = go.Figure()

for group, color in zip(['组 A', '组 B'], okabe_ito_plotly):
    mask = colors == group
    fig.add_trace(go.Scatter(
        x=x[mask], y=y[mask],
        mode='markers',
        name=group,
        marker=dict(size=6, color=color, opacity=0.6)
    ))

# 更新布局以达到发表质量
fig.update_layout(
    width=500,
    height=400,
    font=dict(family='Arial, sans-serif', size=10),
    plot_bgcolor='white',
    xaxis=dict(
        title='变量 X',
        showgrid=False,
        showline=True,
        linewidth=1,
        linecolor='black',
        mirror=False
    ),
    yaxis=dict(
        title='变量 Y',
        showgrid=False,
        showline=True,
        linewidth=1,
        linecolor='black',
        mirror=False
    ),
    legend=dict(
        x=0.02,
        y=0.98,
        bgcolor='rgba(255,255,255,0.8)',
        bordercolor='gray',
        borderwidth=0.5
    )
)

# 保存为静态图像(需要 kaleido)
fig.write_image('plotly_scatter.png', width=500, height=400, scale=3)  # scale=3 约等于 300 DPI
fig.write_html('plotly_scatter.html')  # 交互式版本

fig.show()

示例 9:带显著性标记的分组柱状图

import matplotlib.pyplot as plt
import numpy as np

# 数据
categories = ['WT', '突变体 A', '突变体 B']
control_means = [100, 85, 70]
control_sem = [5, 6, 5]
treatment_means = [100, 120, 140]
treatment_sem = [6, 8, 9]

x = np.arange(len(categories))
width = 0.35

fig, ax = plt.subplots(figsize=(3.5, 3))

# 创建柱状条
bars1 = ax.bar(x - width/2, control_means, width, yerr=control_sem,
               capsize=3, label='对照', color='#0072B2', alpha=0.8)
bars2 = ax.bar(x + width/2, treatment_means, width, yerr=treatment_sem,
               capsize=3, label='处理', color='#E69F00', alpha=0.8)

# 添加显著性标记
def add_significance_bar(ax, x1, x2, y, h, text):
    """在两个柱状条之间添加显著性标记线"""
    ax.plot([x1, x1, x2, x2], [y, y+h, y+h, y], linewidth=0.8, c='black')
    ax.text((x1+x2)/2, y+h, text, ha='center', va='bottom', fontsize=7)

# 标记显著差异
add_significance_bar(ax, x[1]-width/2, x[1]+width/2, 135, 3, '***')
add_significance_bar(ax, x[2]-width/2, x[2]+width/2, 155, 3, '***')

# 自定义
ax.set_ylabel('活性(占 WT 对照的百分比)')
ax.set_xticks(x)
ax.set_xticklabels(categories)
ax.legend(frameon=False, loc='upper left')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_ylim(0, 180)

# 添加显著性注释
ax.text(0.98, 0.02, '*** p < 0.001', transform=ax.transAxes,
        ha='right', va='bottom', fontsize=6)

fig.tight_layout()
save_publication_figure(fig, 'grouped_bar_significance')
plt.show()

示例 10:Nature 级别可发表图形

import matplotlib.pyplot as plt
import numpy as np
from string import ascii_lowercase

# Nature 规格:89mm 单栏
inch_per_mm = 0.0393701
width_mm = 89
height_mm = 110
figsize = (width_mm * inch_per_mm, height_mm * inch_per_mm)

fig = plt.figure(figsize=figsize)
gs = fig.add_gridspec(3, 2, hspace=0.5, wspace=0.4,
                      left=0.12, right=0.95, top=0.96, bottom=0.08)

# 面板 a:时间过程
ax_a = fig.add_subplot(gs[0, :])
time = np.linspace(0, 48, 100)
for i, label in enumerate(['对照', '处理']):
    y = (1 + i*0.5) * np.exp(-time/20) * (1 + 0.3*np.sin(time/5))
    ax_a.plot(time, y, linewidth=1.2, label=label)
ax_a.set_xlabel('时间(小时)', fontsize=7)
ax_a.set_ylabel('生长(OD$_{600}$', fontsize=7)
ax_a.legend(frameon=False, fontsize=6)
ax_a.tick_params(labelsize=6)
ax_a.spines['top'].set_visible(False)
ax_a.spines['right'].set_visible(False)

# 面板 b:柱状图
ax_b = fig.add_subplot(gs[1, 0])
categories = ['A', 'B', 'C']
values = [1.0, 1.5, 2.2]
errors = [0.1, 0.15, 0.2]
ax_b.bar(categories, values, yerr=errors, capsize=2, width=0.6,
         color='#0072B2', alpha=0.8)
ax_b.set_ylabel('倍数变化', fontsize=7)
ax_b.tick_params(labelsize=6)
ax_b.spines['top'].set_visible(False)
ax_b.spines['right'].set_visible(False)

# 面板 c:热图
ax_c = fig.add_subplot(gs[1, 1])
data = np.random.randn(8, 6)
im = ax_c.imshow(data, cmap='viridis', aspect='auto')
ax_c.set_xlabel('样本', fontsize=7)
ax_c.set_ylabel('基因', fontsize=7)
ax_c.tick_params(labelsize=6)

# 面板 d:散点图
ax_d = fig.add_subplot(gs[2, :])
x = np.random.randn(50)
y = 2*x + np.random.randn(50)*0.5
ax_d.scatter(x, y, s=8, alpha=0.6, color='#E69F00')
ax_d.set_xlabel('基因 X 表达量', fontsize=7)
ax_d.set_ylabel('基因 Y 表达量', fontsize=7)
ax_d.tick_params(labelsize=6)
ax_d.spines['top'].set_visible(False)
ax_d.spines['right'].set_visible(False)

# 添加小写面板标签(Nature 风格)
for i, ax in enumerate([ax_a, ax_b, ax_c, ax_d]):
    ax.text(-0.2, 1.1, f'{ascii_lowercase[i]}', transform=ax.transAxes,
            fontsize=9, fontweight='bold', va='top')

# 以 Nature 偏好的格式保存
fig.savefig('nature_figure.pdf', dpi=1000, bbox_inches='tight',
           facecolor='white', edgecolor='none')
fig.savefig('nature_figure.png', dpi=300, bbox_inches='tight',
           facecolor='white', edgecolor='none')

plt.show()

各库的使用技巧

Matplotlib

  • 使用 fig.tight_layout()constrained_layout=True 防止重叠
  • 将 DPI 设置为 300–600 以用于发表
  • 折线图使用矢量格式(PDF、EPS
  • 在 PDF/EPS 文件中嵌入字体

Seaborn

  • 基于 matplotlib 构建,因此所有 matplotlib 的自定义功能均适用
  • 使用 sns.set_style('ticks')'whitegrid' 以获得简洁外观
  • sns.despine() 移除顶部和右侧的轴脊线
  • 使用 sns.set_palette() 设置自定义调色板

Plotly

  • 非常适合交互式探索性分析
  • 使用 fig.write_image() 导出静态图像(需要 kaleido 包)
  • 使用 scale 参数控制 DPIscale=3 ≈ 300 DPI
  • 大量调整布局以达到发表质量

通用工作流程

  1. 使用默认设置进行探索
  2. 应用发表配置(参见「设置」部分)
  3. 创建合适大小的图形(查看期刊要求)
  4. 自定义颜色(使用色盲友好调色板)
  5. 调整字体和线宽(在最终尺寸下可读)
  6. 去除图表垃圾元素(顶部/右侧轴脊线、过多网格线)
  7. 添加清晰的标签及单位
  8. 用灰度测试
  9. 以多种格式保存(矢量用 PDF,光栅用 PNG
  10. 在最终上下文中验证(导入稿件中检查尺寸)

参考资料