Files
2026-07-13 21:35:36 +08:00

621 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 可发表级别的 Matplotlib 示例
## 概述
本参考文档提供了使用 Matplotlib、Seaborn 和 Plotly 创建可发表级别的科学示意图的实用代码示例。所有示例均遵循 `publication_guidelines.md` 中的最佳实践,并使用 `color_palettes.md` 中的色盲友好调色板。
## 设置与配置
### 发表级别 Matplotlib 配置
```python
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'
```
### 保存辅助函数
```python
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:带误差线的折线图
```python
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:多面板图形
```python
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:带散点的箱线图
```python
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:带颜色条的热图
```python
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 小提琴图
```python
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:带回归线的科学散点图
```python
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:带阴影误差的时间序列图
```python
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 交互式图形
```python
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:带显著性标记的分组柱状图
```python
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()
```
## 示例 10Nature 级别可发表图形
```python
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. **在最终上下文中验证**(导入稿件中检查尺寸)
## 参考资料
- Matplotlib 文档:https://matplotlib.org/
- Seaborn 画廊:https://seaborn.pydata.org/examples/index.html
- Plotly 文档:https://plotly.com/python/
- Nature Methods Points of View:数据可视化专栏存档