生态学报画图规范(Acta Ecologica Sinica Plotting Guide)
本技能由一套已经过《生态学报》编辑部返修验收的 Matplotlib 绘图脚本提炼而来,
覆盖论文中最常见的四类图。所有代码均遵循同一套"编辑部终版"约定,可直接复用。
一、何时使用本技能
- 画空间分布图(连续型空间变量 / 遥感栅格),需要指北针、比例尺、统一色标、去经纬度坐标
- 画预测 vs 真实散点对比图(深度学习/机器学习模型精度评估)
- 画误差分级空间图(相对误差±分级,diverging 色带)
- 画时间序列趋势折线图(SHAP 值、年际变化,带拟合线与公式标注)
- 期刊修改图要求"改图"(统一字体、去框、加注记、输出矢量/高清格式)
参考脚本见 references/:
- 空间分布图_标准范式.py —— 空间分布图(终版,去经纬度的标准范式)
- 空间分布图_经纬度版.py —— 空间分布图(带经纬度刻度版本,早期用)
- 误差分级空间图.py —— 误差分级空间图(diverging 9 级分类 + 图例)
- 误差分级空间图_统计版.py —— 误差分级图另一版(含 TXT 统计输出)
- 散点对比图_多模型.py —— 预测 vs 真实散点对比图(3×3)
- 散点对比图_R2标注版.py —— 散点对比图(带 R² 方框标注版)
- 时间序列趋势折线图.py —— 时间序列趋势/SHAP 折线图(散点+拟合线+公式)
二、全局字体与 rcParams(所有图通用,必须放在最前面)
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 中文用黑体;若 SimHei 缺失可加 'Microsoft YaHei'
plt.rcParams['axes.unicode_minus'] = False # 正常显示负号
# PDF / AI 编辑友好:嵌入 TrueType 字体,避免投稿后中文变方块
plt.rcParams['pdf.fonttype'] = 42
plt.rcParams['ps.fonttype'] = 42
plt.rcParams['svg.fonttype'] = 'none'
# 加粗(编辑部偏好的"醒目"风格)
plt.rcParams['font.weight'] = 'bold'
plt.rcParams['axes.labelweight'] = 'bold'
plt.rcParams['axes.titleweight'] = 'bold'
约定:用一个 FONT_SCALE = 1.35(地图类)或 1.5(误差类)的缩放系数统一放大字号,
例如 fontsize=int(20 * FONT_SCALE)。改图时只调这一个系数即可整体放大/缩小。
三、空间分布图(地图/栅格类,最核心)
3.1 统一约定
| 项 | 规范 |
|---|---|
| 投影 | cartopy.crs.PlateCarree(),子图 projection=ccrs.PlateCarree() |
| 数据裁剪 | rasterio.mask.mask 按省界 shp 裁剪,shp 外像素置 np.nan |
| 连续色带 | 连续型变量用绿系;误差用 RdBu diverging(见第七节) |
| 统一色标范围 | 预测 + 真实合并后取 1–99 分位(np.percentile(..., 1) / (..., 99)),保证多子图可比 |
| 行政边界 | ShapelyFeature,facecolor='none', edgecolor='black', linewidth=1.2~1.5 |
| 指北针 | 右上角 add_north_arrow |
| 比例尺 | 右下角 add_scale_bar |
| 终版去框 | 编辑部要求去掉经纬度坐标、刻度、外框(remove_axes_frame_ticks,见 3.3) |
| 子图标题 | 去掉 (a)(b)(c) 角标,改为 模型A(年份) 这类"模型(年份)"格式 |
| 公共色标 | 底部水平 colorbar,ScalarMappable + set_clim(vmin, vmax) |
| 输出 | JPG 600dpi + PDF(AI 可编辑)+ TIFF 三份(见第八节) |
3.2 指北针函数(右上角)
def add_north_arrow(ax, length=0.09, xy=(0.92, 0.918), text='N', facecolor='k'):
xy_arrow_base = (xy[0], xy[1] - length)
ax.annotate('', xy=xy, xytext=xy_arrow_base,
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(facecolor=facecolor, edgecolor=facecolor,
width=4, headwidth=12, headlength=15))
ax.text(xy[0], xy[1] + 0.01, text, transform=ax.transAxes,
ha='center', va='bottom', fontsize=int(16 * FONT_SCALE),
color=facecolor, fontweight='bold')
3.3 比例尺函数(右下角,自动选 20/50/100/200/300/500/800/1000/1500 km 整值)
def geodesic_km_per_degree(lat_deg):
km_per_deg_lat = 111.32
km_per_deg_lon = 111.32 * np.cos(np.deg2rad(lat_deg))
return km_per_deg_lon, km_per_deg_lat
def add_scale_bar(ax, extent, length_km=None, location=(0.90, 0.10), height=0.020, color='k'):
minx, maxx, miny, maxy = extent
mid_lat = 0.5 * (miny + maxy)
km_per_deg_lon, _ = geodesic_km_per_degree(mid_lat)
map_width_km = (maxx - minx) * km_per_deg_lon
nice_lengths = np.array([20, 50, 100, 200, 300, 500, 800, 1000, 1500])
if length_km is None:
target = map_width_km / 4.5
length_km = nice_lengths[np.argmin(np.abs(nice_lengths - target))]
length_deg = length_km / km_per_deg_lon
x1 = minx + location[0] * (maxx - minx)
y0 = miny + location[1] * (maxy - miny)
x0 = x1 - length_deg
ax.plot([x0, x1], [y0, y0], transform=ccrs.PlateCarree(), color=color, linewidth=3.5)
ax.plot([x0, x0], [y0, y0 + (maxy - miny) * height], transform=ccrs.PlateCarree(), color=color, linewidth=3.5)
ax.plot([x1, x1], [y0, y0 + (maxy - miny) * height], transform=ccrs.PlateCarree(), color=color, linewidth=3.5)
ax.text((x0 + x1) / 2, y0 - (maxy - miny) * height * 0.7, f"{int(length_km)} km",
transform=ccrs.PlateCarree(), ha='center', va='top', fontsize=int(16 * FONT_SCALE), color=color)
3.4 终版"去经纬度坐标/刻度/外框"(编辑部硬性要求)
def remove_axes_frame_ticks(ax):
ax.set_xticks([]); ax.set_yticks([])
ax.set_xlabel(''); ax.set_ylabel('')
ax.tick_params(left=False, bottom=False, right=False, top=False,
labelleft=False, labelbottom=False, labelright=False, labeltop=False)
for spine in ax.spines.values():
spine.set_visible(False)
if hasattr(ax, 'outline_patch'): # 兼容旧版 Cartopy
ax.outline_patch.set_visible(False)
if hasattr(ax, 'background_patch'):
ax.background_patch.set_visible(False)
try:
ax.spines['geo'].set_visible(False) # 兼容新版 Cartopy
except Exception:
pass
ax.patch.set_visible(False)
> 注:早期版本(空间分布图_经纬度版.py)保留经纬度刻度,用 LongitudeFormatter/LatitudeFormatter +
> 自定义 funcFormatter(f'{x:.0f}°E')。终版一律去掉,靠指北针+比例尺定位即可。
四、预测 vs 真实散点对比图(模型精度评估)
标准范式(见 散点对比图_多模型.py / 散点对比图_R2标注版.py):
- 布局:3 行 × 3 列(3 模型 × 3 年),
figsize=(24, 24) - 单子图内容:散点(随机采样 ≤ 10000 点,
alpha≈0.55,s=10)+ 回归线(红色实线,linewidth=3~4)+ 1:1 线(黑色虚线) - R² 标注:
ax.text(0.48, 0.95, f'$R^2$ = {r2:.4f}', transform=ax.transAxes, fontweight='bold') - 终版可加黄色圆角方框
bbox=dict(boxstyle='round,pad=0.5', facecolor='yellow', alpha=0.8) - 坐标轴:只保留 left + bottom spine(
top/right隐藏),spines线宽 3,黑色 - 刻度:
direction='in'(朝内),labelsize≈31,刻度标签加粗 - 子图不写横纵轴标题,改用
fig.supxlabel/fig.supylabel写中英文公共轴标签:
r'真实值 Real value / g C m$^{-2}$ a$^{-1}$'(单位用-2上标、a$^{-1}$年) - 图例:
frameon=False或带边框、无阴影,loc='upper left' - 输出:JPG 600dpi(主)+ 早期版另有 EPS 600dpi
五、误差分级空间图(相对误差,diverging)
标准范式(见 误差分级空间图.py):
- 相对误差:
(pred - true) / |true_safe| * 100,true_safe防除零取max(|true|, 1e-6) - 极值过滤:
|rel_err| > 500置nan - 9 级分类(与 RdBu 色带对应):
error_classes = [
(-np.inf, -50, '严重低估', '#053061'),
(-50, -25, '显著低估', '#2166AC'),
(-25, -15, '中度低估', '#4393C3'),
(-15, -10, '轻微低估', '#92C5DE'),
(-10, 10, '预测良好', '#FFFFFF'),
(10, 15, '轻微高估', '#F4A582'),
(15, 25, '中度高估', '#D6604D'),
(25, 50, '显著高估', '#B2182B'),
(50, np.inf, '严重高估', '#67001F'),
]
cmap_classified = ListedColormap([c[3] for c in error_classes])
- 绘图:
ax.imshow(classified_masked, cmap=cmap_classified, vmin=0, vmax=8, alpha=0.9, interpolation='nearest') - 图例:用
matplotlib.patches.Patch生成色块图例,分两排(legend_elements[:5]一排、[5:]一排,ncol=5/ncol=4),加边框 - 同样去经纬度坐标(
remove_axes_frame_ticks)+ 指北针 + 比例尺 - 输出 JPG 600 + PDF + TIFF
六、时间序列趋势 / SHAP 折线图(见 时间序列趋势折线图.py)
- 单图
figsize=(13.5, 8.5), dpi=200 - 散点:
marker='o'/'s'/'^'(圆/方/三角区分变量),edgecolor='black',linewidth=1.5,s≈115~130 - 拟合线:全部实线
linestyle='-',linewidth≈3.2 - 公式标注(右上角自上而下排列):
f"{name} = {slope:.5f}x {intercept:+.2f} ($R^2$ = {r2:.3f})" - 轴标签中英双行:
"平均 |SHAP|值\nMean |SHAP| value" - 去网格:
ax.grid(False) - 保留四条外框(spine 全可见,
linewidth=1.5,黑色) - 刻度:
direction='out',labelsize≈22, 刻度标签加粗 - 横坐标按 2 年步进
np.arange(2001, 2020, 2);纵坐标FormatStrFormatter("%.2f") - 输出:JPG 600dpi + PDF(AI 可编辑)
七、常用配色方案
连续型变量绿系(10 色,由浅到深)
colors = ['#F7F4F0', '#E8E0D2', '#DFC27D', '#BFAE72', '#A6A663',
'#80B774', '#5AAE61', '#1B7837', '#00682A', '#00441B']
误差分级(diverging RdBu,9 色,见第五节)
#053061 → #2166AC → #4393C3 → #92C5DE → #FFFFFF → #FDDBC7 → #F4A582 → #D6604D → #B2182B → #67001F
变量散点配色(趋势图)
SKT: #ef6a63 / #e74c3c、SSR: #5dade2 / #3498db、DEM: #58d68d / #2ecc71
八、输出格式与 dpi(投稿要求)
| 阶段 | 格式 | dpi | 说明 |
|---|---|---|---|
| 终版(编辑部用) | JPG | 600 | 主投稿图,bbox_inches='tight', pil_kwargs={'quality':95} |
| 终版 | 600 | AI 可编辑,配合 pdf.fonttype=42 嵌入字体 |
|
| 终版 | TIFF | 600 | 部分期刊要求位图投稿 |
| 早期 | PNG | 300 | 初稿预览 |
| 早期 | EPS | 600 | 矢量初稿 |
保存示例:
plt.savefig('空间分布图_600dpi.jpg', dpi=600, format='jpg',
bbox_inches='tight', pil_kwargs={'quality': 95})
plt.savefig('空间分布图_AI可编辑.pdf', dpi=600, format='pdf', bbox_inches='tight')
plt.savefig('空间分布图.tif', dpi=600, format='tiff', bbox_inches='tight')
九、返修改图 checklist(编辑部常见要求)
- ✅ 中文字体为 SimHei / Microsoft YaHei,
axes.unicode_minus=False - ✅ PDF 输出设
pdf.fonttype=42(否则 AI 打开中文乱码) - ✅ 地图类图去掉经纬度坐标、刻度、外框(用
remove_axes_frame_ticks) - ✅ 地图类图保留指北针(右上)+ 比例尺(右下)
- ✅ 多子图统一色标范围(合并 1–99 分位),底部用公共水平 colorbar
- ✅ 子图标题用
模型(年份)格式,不带 (a)(b) 角标(终版) - ✅ 单位书写规范:
g C m$^{-2}$ a$^{-1}$(负指数用$^{-1}$上标) - ✅ 主图送 JPG 600dpi,另存 PDF(AI 可编辑) + TIFF
- ✅ 线宽加粗(spine 1.5~3)、字体加粗,整体醒目
- ✅ 预测散点图显示 R²,含回归线 + 1:1 线,只留 left/bottom 框