
最近帮一位做平流层研究的同行处理数据图他提了一嘴“要是能有一张像Nature文章那样的北半球投影图就好了看起来特别专业。”我理解他的意思——就是那种圆盘状、以北极点为中心的极地投影地图上面叠加涡度场、气温异常或者海冰浓度配合简洁的配色一眼就能看出高纬地区的空间格局。这类图在气候动力学、海冰监测、大气化学等方向几乎是标配。但要放在几年前画这么一张图并不轻松。底图数据要下载投影要手写参数海岸线要单独处理网格线要自己算整套流程下来没半天搞不定。现在我告诉你用Python生态里成熟的绘图库核心绘图代码完全可以控制在十几行以内出图质量直接对标Nature正文配图的审美标准。这篇文章把整个流程拆开讲清楚从投影原理到每个参数含义再到实际画图过程中踩过的坑一并交代明白适合正在写论文、准备投稿或者单纯想把地图画得好看点的科研人。1. 先把方案定下来Python绘图生态怎么选1.1 为什么现在是Python Cartopy的组合地图绘制这件事Python生态里有过不少方案最古老的是Matplotlib自带的Basemap工具包当年也是主流但Basemap在2020年左右停止维护了官方明确建议用户迁移到Cartopy。如果你现在搜教程还会看到大量Basemap的老代码我的建议是直接忽略不要在新项目里用原因很简单Basemap基于的地理数据格式比较陈旧和现代NetCDF、GRIB数据的配合不太顺畅而且它的投影方式使用起来确实麻烦。Cartopy是英国气象办公室主导开发的地理空间可视化库它的底层依赖Shapely和PyKEA核心优势是做了大量矢量空间计算比如裁剪、变换、叠加同时无缝对接Matplotlib的绘图接口。说白了Cartopy在设计思路上就是“让地理数据画起来像普通数据一样简单”它把投影变换、海岸线加载、网格线生成这些通通封装成几个函数调用这是它能做到“几行代码出一张图”的根本原因。我不是说其他语言没有好方案比如R语言的ggplot2加sf包也能画JavaScript有Leaflet和D3但如果你的研究数据本身就是Python生态里处理的比如用Xarray读NetCDF、用Pandas做表格、用NumPy算统计量那用Cartopy画图是衔接成本最低的路径数据变量名直接拿来就用。1.2 北极投影图的核心思路理解两套坐标系画一张北半球的极地投影图最绕不开的概念是坐标系。我见过不少新手在这一步卡住其实搞清楚以后很简单。Cartopy里有两套坐标系在同时工作一套是你数据的坐标系另一套是地图展示的坐标系。比如你的气温数据是0.25度乘0.25度的规则网格对应的就是等距圆柱投影Cartopy里叫PlateCarree你可以在很多代码里看到这个名称这套坐标下经度从-180到180纬度从-90到90像个长方形展开的世界地图。另一套是你最终呈现的坐标系也就是以北极点为中心的方位角投影Cartopy里叫NorthPolarStereo这套坐标下纬线变成同心圆经线从圆心向四周发散。你可能会觉得“既然展示效果是圆的那我直接把数据按极地投影读进来不就行了吗”实际上不行因为你的数据源文件本身并没有采用极地投影网格绝大多数再分析资料都是规则经纬网格所以你必须先把数据放在PlateCarree坐标系里然后让Cartopy去做从“经纬网格坐标”到“投影平面坐标”的数学变换这个过程是自动的你只需要在建图时指定projection参数在绘图时指定transform参数Cartopy就能正确地把数据“投影”到圆盘上。1.3 数据准备画地图需要哪些素材画图之前先把底图数据搞定。Cartopy内置了一些矢量数据接口最常见的是Natural Earth数据集安装Cartopy时会自动下载到本地缓存目录里。这些数据包括海岸线、国界线、湖泊、河流等对大多数科研论文的配图来说Natural Earth的分辨率已经足够。我的建议是使用“50m”分辨率级别的数据它对应中等细节程度既不会像“110m”那样粗糙到丢失细节也不会像“10m”那样文件太大导致每次画图都卡顿。如果你需要更细节的海岸线比如要画某个海峡或群岛可以额外从Natural Earth官网下载10m分辨率的数据然后用Cartopy的shp文件读取接口加载进来这个后面在实操部分会说。另外一个重要的数据库是ETOPO地形数据或者叫Global Land One-km Base Elevation有时候你想要在底图上叠加地形着色或者把海洋深度表现得更丰富这时候光靠海岸线矢量就不够了需要用栅格地形数据。但就Nature风格简洁的插图来说真的不需要地形面面俱到淡色陆地加白色海洋已经是主流审美。2. 实操从环境配置到第一张圆盘图2.1 安装依赖三分钟搞定先解决环境问题。我建议你用conda创建独立环境尽量避免在base环境里装一堆地理库因为Cartopy的二进制依赖特别是GEOS、PROJ版本比较敏感直接用pip有时候会编译报错。用conda装最稳妥conda create -n geo python3.10 conda activate geo conda install -c conda-forge cartopy matplotlib numpy xarray netcdf4如果你平时还需要读GRIB格式的气象数据可以再加一个“cfgrib”库但这里暂时用不上。装完之后可以快速验证一下import cartopy import matplotlib.pyplot as plt print(cartopy.__version__)如果正常输出版本号说明安装成功。第一次调用cartopy时它可能会自动下载一些底图数据需要稍微等待片刻这个过程会在后台完成之后再用就不需要了。2.2 最核心的三行代码投影、范围、底图安装好依赖以后第一张北半球圆盘图其实只需要三行核心代码import matplotlib.pyplot as plt import cartopy.crs as ccrs import cartopy.feature as cfeature fig plt.figure(figsize(6, 6)) ax plt.axes(projectionccrs.NorthPolarStereo()) ax.set_extent([-180, 180, 50, 90], crsccrs.PlateCarree()) ax.add_feature(cfeature.LAND, facecolor#e0e0e0) ax.add_feature(cfeature.OCEAN, facecolorwhite) ax.coastlines(linewidth0.4, color#333333) plt.savefig(north_polar_test.png, dpi300, bbox_inchestight)就这一段你已经得到一张以北极点为中心的圆盘底图了。但别急着高兴你得明白这三行里面每一行的真正含义否则后面调参数时会抓瞎。ccrs.NorthPolarStereo()定义的是正射视角下的北极极地方位投影以北极点作为投影中心默认的中央经线是0度也就是说0度经线在圆盘上指向正下方这通常符合大多数人的阅读习惯不过也可以调整中央经线让地图旋转到更适合你的研究区域的方向。ax.set_extent([-180, 180, 50, 90], crsccrs.PlateCarree())这一行设置的是地图可视范围。注意它需要指定坐标参考系也就是说“-180到180经度、50到90纬度”这四个边界值是在经纬度坐标下定义的Cartopy会帮你把这个矩形范围转换到极地投影坐标系下然后裁剪出对应的区域。这个边界设置在后面的进阶操作里非常有讲究因为如果你把纬度下限设得太低比如设成0度那画出来的就是一大张半球图北极圈反而只占了一小块视觉重心就散掉了。对绝大多数北半球研究来说50度这个纬度线作为圆盘的下边缘比较合适既能涵盖欧洲、亚洲、北美大部分高纬区域又能让圆盘内部信息不至于太拥挤。ax.add_feature(cfeature.LAND, facecolor#e0e0e0)这一行是给陆地填充灰色ax.coastlines()是画海岸线轮廓。这里我特意把海洋设为白色因为后面叠加数据后变量的暖色调会比较突兀白色海洋能提供足够对比度。2.3 添加经纬网格让数值有坐标感有了陆地轮廓加上经纬网格线才是一张完整的科学地图。Cartopy里网格线的用法很直接gl ax.gridlines(draw_labelsTrue, linewidth0.3, colorgray, alpha0.5, linestyle--) gl.top_labels False gl.left_labels True gl.right_labels False gl.xlabel_style {size: 8, color: #333333} gl.ylabel_style {size: 8, color: #333333}这里有个容易忽略的细节极地投影下纬线是同心圆经线是从中心发散出去的放射线。Cartopy的网格线会自动根据投影方式调整绘制方向所以你在代码里不需要手动算经纬线的位置只需要指定画线的样式和标签样式即可。但我实测下来Cartopy的gridlines在极地投影下存在一个小问题默认情况下经度标签会沿圆盘边缘排列如果中央经线设置不当标签可能重叠在一起导致图面很乱。一个稳妥的解决方式是设置gl.rotate_labels False同时用gl.xlocator手动控制经线标注的间隔比如每30度标注一次可以显著降低重叠概率import matplotlib.ticker as mticker gl.xlocator mticker.FixedLocator([-180, -150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150])另外一个更好的方案是完全不依赖Cartopy的自动标签而是手动在特定位置写文本标注经度值。这种方法很绕弯但效果更可控Nature期刊上许多图就是这么干的因为自动标签的样式难免有程序化的僵硬感手动标注可以配合字体、色彩做出更协调的排版。2.4 叠加变量场把数据画上去底图就绪后叠加变量是重头戏。以常见的温度异常场为例如果你用Xarray读入NetCDF数据画图其实就两步import xarray as xr import numpy as np import matplotlib.pyplot as plt import cartopy.crs as ccrs ds xr.open_dataset(temperature_anomaly.nc) temp ds[t2m] - 273.15 # 转摄氏度 fig plt.figure(figsize(6, 6)) ax plt.axes(projectionccrs.NorthPolarStereo()) ax.set_extent([-180, 180, 50, 90], crsccrs.PlateCarree()) cf ax.contourf(temp.longitude, temp.latitude, temp, transformccrs.PlateCarree(), cmapRdBu_r, levels21, extendboth) plt.colorbar(cf, orientationhorizontal, shrink0.7, pad0.05)这里最关键的参数是transformccrs.PlateCarree()它明确告诉Cartopy数据本身是经纬网格坐标但地图显示是极地投影坐标两者之间需要做插值和变换。如果你漏掉这个参数数据会被默认当作已经在投影坐标系里直接导致错位、拉伸画出来跟变形了一样。contourf适合画连续场比如气温、气压、高度场。如果你要画点位数据比如观测站点可以改用scatter同样需要指定transform参数。如果你要画风场箭头用barbs或quiver也要指定transform和x、y的网格坐标。这几个接口的投影逻辑一致掌握了contourf其他都是触类旁通。2.5 出图参数让排版接近期刊标准论文插图一般有固定的单栏或双栏宽度要求所以出图尺寸不能随便定。以Nature系列期刊为例正文配图的单栏宽度大约8.6厘米双栏宽度大约17.8厘米对应的dpi至少要300起步600更保险。我通常的做法是直接在保存时指定尺寸fig.set_size_inches(6, 6) # 适用于双栏时期的方形图 plt.savefig(north_polar_final.png, dpi600, bbox_inchestight)这里有一个重要经验如果你的图形不是单纯的方形而是带有色标、标题等附加组件保存时加bbox_inchestight可以自动去除多余的白边但也可能会导致最终图形尺寸与预期不完全一致。更稳妥的方法是先创建一个布局比如用gridspec或subplots预留出colorbar的位置然后整体控制布局尺寸。3. 让图面真正“Nature化”的几个细节3.1 颜色比你想象的更有讲究Nature风格的地图之所以耐看很大程度上是颜色的功劳。那些蓝红渐变、青色到黄色渐变的方案并不是随手选的。传统Matplotlib的默认配色比如jet彩虹色饱和度太高、过渡不均容易掩盖数据的细微变化也不利于色盲读者区分。我推荐几个科研绘图常用的色带温度异常RdBu_r红蓝反向零值附近用白色这条几乎是最安全的方案。降水BrBG棕绿或cmocean.rain能体现干湿差异。海冰浓度cmocean.ice自带白青渐变和冰的视觉印象一致。地形高度cmocean.topo或terrain海陆过渡比较自然。如果你的数据是百分比、浓度等比值型变量建议用cmocean库里的感知均匀色带这类色带在设计时做了亮度均匀性处理避免了某些区域颜色跳跃过大导致的伪结构感。安装cmocean很简单几行pip就行。我自己画图的经验是一旦用上感知均匀色带再回看传统的jet会明显觉得后者颜色发腻、层次混乱。还有一点colorbar的标注字号应该略微小于正文一般在7到9磅之间因为插图在排版时会被缩放字号太大会显得笨重太小则看不清。同时把colorbar的方向改成水平放在图的下方可以让正方形的地图主体占比更大整个图的构图更紧凑。3.2 地图范围与形状控制从“能看”到“好看”很多人画出来的北极图边缘是方形的中间是圆盘四个角上一片白。严格来说这不难看但在Nature相关的文章里更常见的是圆形地图本身作为图的边界四个角都被裁掉。要实现这种效果Cartopy从2023年的0.22版本开始提供了set_boundary接口用法是在画图之前对坐标轴应用一个圆形Pathimport matplotlib.path as mpath import numpy as np theta np.linspace(0, 2*np.pi, 100) circle_path mpath.Path(np.column_stack([np.cos(theta), np.sin(theta)])) ax.set_boundary(circle_path, transformax.transAxes)这个操作会把绘图区域裁剪成一个完美的圆形让图形以圆盘形态呈现四角自动透明。实测下来这一个小改动对整个图的观感提升非常明显有一种“专业期刊配图”的即视感。需要注意的版本兼容性问题。我测试过0.21以及更早的版本有些年代较早的Cartopy版本不支持直接传入Path对象需要先把圆形转换为Polygon或者用ax.spines[geo].set_visible(False)加辅助mask的方式。但这一步在最新版本里已经很顺畅了所以建议尽量升级到0.22以上。3.3 字体与排版影响审稿人第一印象字体这块最容易被忽略但恰恰是审稿人一眼能感受到“专业”的细节。我在看稿件时有个经验只要图里的坐标数字是细小的无衬线体、字体大小统一、数值位数一致整个图就显得精神如果字体大小不一、衬线体和非衬线体混用哪怕数据再好也觉得粗糙。Matplotlib默认的DejaVu Sans作为英文字体完全够用但如果你想更贴合Nature出版的风格可以换成Arial或者Helvetica这需要额外注册系统字体。中文字体一般很少出现在地图上如果确需添加中文标注推荐使用Noto Sans CJK安装后在Matplotlib里设置字体族即可。统一字号也是关键。我的习惯是地图坐标标签8磅colorbar标签8磅图例标题和条目8磅这样保存为dpi 600后无论是单栏还是双栏排版都清晰。再有一个细节坐标轴上不要出现尾数多余的零比如标成“60N”而不是“60.0N”Cartopy的gridlines有gl.xlabel_style可以用来调整但更精细的控制可能需要手动标注。3.4 多子图一幅画里讲完一个完整故事不只是单幅地图形状Nature风格里非常常见的是多组图拼在一起。比如你想展示北半球四个季节的平均气温异常可以做一个2×2的子图布局每张子图用相同的投影范围、相同的色标范围整体上非常整齐。子图布局里一个比较实用的技巧是让多张图画共享同一个colorbar这种做法既省空间又避免误解——如果你给每张子图单独设置colorbar但色标范围不一致读者很容易误读数据间的差异。共享colorbar的实现很直接先用fig.add_subplot(1, 2, 1, projectionccrs.NorthPolarStereo())和(1, 2, 2, ...)创建坐标轴画完后在figure层面添加colorbarfig plt.figure(figsize(10, 5)) ax1 fig.add_subplot(1, 2, 1, projectionccrs.NorthPolarStereo()) ax2 fig.add_subplot(1, 2, 2, projectionccrs.NorthPolarStereo()) # 在两个子图上分别画数据 cf ax1.contourf(..., vmin-5, vmax5, cmapRdBu_r) ax2.contourf(..., vmin-5, vmax5, cmapRdBu_r) cbar fig.colorbar(cf, ax[ax1, ax2], orientationhorizontal, fraction0.03)注意contourf的vmin和vmax要显式统一不然哪怕色带相同范围不同也会导致对比误导。这个“统一量级”的思维在多图对比里特别重要也是评审人经常审到的细节。3.5 权重较高的技巧裁剪海岸线以外的数据在极地投影图里数据场往往会大面积覆盖整个圆盘但如果你只想突出陆地区域比如画北半球植被指数或地表温度就需要把海洋部分的数据遮挡成白色。Cartopy里一个高效的做法是用ax.add_feature(cfeature.OCEAN, facecolorwhite, zorder3)把它放在数据层之上这样海洋区域的颜色会被白色覆盖。不过要注意zorder的顺序数据绘制在zorder0或zorder1海洋遮罩放在zorder3才能正确覆盖。如果你的数据需要透过海洋显示但你又不想看到圆圈外的空白那可以给海洋添加一个浅灰或很浅的蓝色起到底色作用而不完全遮盖数据这个取舍看具体的应用场景。我在处理海冰浓度数据时常常会画海洋为白色再加海冰填充数据本身带着陆地标签也没有关系直接叠上去就行。4. 常见报错与问题排查实录4.1 数据范围对不上白屏或超出边界新手最常遇到的一个现象是图能画出来但数据没出现在预期位置要么整幅图空白要么数据覆盖了半个地球的变形样子。这种问题90%以上出在transform参数上。我记得有个项目里的同事在画北极涡旋位置图时因为忘了在scatter里指定transformccrs.PlateCarree()结果站点坐标全被当作投影平面坐标直接堆到了地图边缘。排查方式很简单一是检查transform参数是否存在二是检查数据的经纬度范围是否在绘图区域范围内。如果你只设置set_extent为50到90度但个别数据点纬度低于50那数据会被悄悄裁剪掉不会报错但图上看不到。这里分享一个自查技巧先用一行最简单的代码画一个点比如在0, 70处打一个红色标记然后运行完整脚本看这个点是否落在预期的位置上。如果红色标记不在圆盘圆心附近说明投影变换链路有问题。4.2 海岸线下载失败或读不到数据Cartopy第一次添加海岸线时会从Natural Earth服务器下载数据国内网络环境下经常存在下载超时的问题导致后面每次运行都报错。这时候可以手动去Natural Earth的github仓库下载对应的shapefile然后解压到Cartopy的数据目录里。一个更省心的方案是直接用cfeature.NaturalEarthFeature并指定本地路径land cfeature.NaturalEarthFeature( physical, land, 50m, edgecolork, facecolorcfeature.COLORS[land]) ax.add_feature(land)如果你的机器完全没有外网还可以用内网共享的shapefile路径。但说实话Cartopy内置的Natural Earth数据一年内也变不了几次提前缓存好就够了。4.3 经纬网标签重叠这是极地投影特有的问题。普通地图经纬线横平竖直标签位置固定而极地投影下经线汇聚于北极点标注很容易挤在一起。除了之前说的用FixedLocator控制标注间隔另一个思路是关掉部分标签比如把左、右两侧的经度标签只保留一侧同时在图外用文字注释经纬网的含义减少视觉噪音。我自己的偏好是圆盘图里通常只标纬度在圆盘外缘经度标注在图外四角或用小的十字箭头标出这样图面更干净。4.4 保存后地图变形或白边过大这个问题出在保存参数和figsize配合不好。bbox_inchestight会裁掉多余白边但也会改变图形比例。如果你需要精确控制打印尺寸可以先计算figsize然后用bbox_inchesNone即默认值保存另外用pad_inches0.02控制边距。还有一点如果你的圆盘图是用set_boundary裁剪过的保存成PDF或SVG矢量格式时裁剪效果也能保留但某些旧版PDF查看器可能显示异常。对投稿而言我的建议是同时保存PNG和PDF两个版本PNG用于快速预览PDF用于最终排版。4.5 常见问题速查表现象原因解决办法数据没出现在预期位置缺少transformccrs.PlateCarree()在绘图函数中显式指定数据坐标系圆盘外四角露出方形白边未对地图边界做圆形裁剪使用ax.set_boundary(circle_path)经度标签重叠严重网格线自动标注位置不合理用FixedLocator控制经线标注间隔海岸线加载失败首次使用时网络下载中断手动下载shapefile并配置本地数据路径多子图颜色不一致各子图vmin/vmax未统一统一设置contourf的范围与色带圆盘边缘发白模糊保存dpi过低或绘图窗口未刷新提高dpi至300以上先fig.canvas.draw()5. 更进一步从“能画出”到“画得专业”的进阶技巧5.1 添加研究区域高亮与标记在Nature风格地图中高亮研究区域是很有用的手段。比如你要分析某个特定区域北欧或白令海可以在圆盘图上添加一个基于经纬度坐标的矩形或圆形区域import cartopy.geodesic as geodesic # 经纬度坐标下的矩形区域 lons [-20, 30] lats [60, 72] ax.add_patch(plt.Rectangle((lons[0], lats[0]), lons[1] - lons[0], lats[1] - lats[0], transformccrs.PlateCarree(), facecolornone, edgecolorred, linewidth2))注意矩形依然需要transform参数。用ax.add_patch时Cartopy能自动把你给的矩形坐标用投影方式转换到极地投影空间里但有个前提是矩形边缘要沿着经纬线方向如果你的研究区域不规则可以改用Polygon给定顶点。5.2 叠加矢量与流场北半球极地区域最常见的矢量图是风场或海冰漂移矢量。Cartopy绘制风场的bash很简单ax.barbs(lon, lat, u, v, transformccrs.PlateCarree(), length5, sizes{empty: 0.2}, linewidth0.4)这里也有一个容易栽的坑极地投影下矢量方向的定义会因为投影变化而变得复杂Cartopy用transform参数自动处理了大部分情况但如果你的数据分辨率太粗比如2.5度网格画出来的barbs可能会稀疏到难以阅读。一个常见的优化是先做插值加密网格或者用quiver的regrid功能来控制箭头密度。我一般选择自己用scipy.interpolate.griddata插值到0.5度网格再画。5.3 多面板图排版如何在一页里讲清楚几个图一篇论文里地图常常只是其中的一个panel你需要把它和其他曲线图、统计箱线图放在一起。这时就有了排版问题。Cartopy自带投影的坐标轴可以和其他普通坐标轴混合使用但需要注意布局fig plt.figure(figsize(10, 4)) gs fig.add_gridspec(1, 3, width_ratios[1, 1, 0.9]) ax_map fig.add_subplot(gs[0, 0], projectionccrs.NorthPolarStereo()) ax_ts fig.add_subplot(gs[0, 1]) ax_stat fig.add_subplot(gs[0, 2])因为地图天然是方形的如果中间的时序图也做成方形整个一排看起来就很整齐。我建议所有子图的高度一致宽度按照内容分配地图约占总宽度三分之一时序图和统计图共享剩下的空间。5.4 关于审稿人眼光的一个经验我审稿时看到过的地图问题十张里有七八张不是数据算错而是“图做得不够干净”。所谓干净我的理解是元素少而有效、信息没有冗余。很多初学画图的同学恨不得把海岸线做成带阴影的、国界线加粗、经纬网又密又黑、再叠加城市和站点结果整张图花成一团。Nature风格恰恰是“少即是多”。陆地浅灰、海洋留白、海岸线细线、网格虚线若隐若现、数据字段用一套克制的色带表达。这种风格并不需要很高的代码水平需要的是一种克制力。画完图之后你可以先问自己这张图里有没有任何一条线是多余的如果有删掉它。6. 我的个人使用体会这套“几行代码绘制Nature风格北半球投影图”的方案我用过很多次从最开始自己也踩过下载数据失败的坑到后来形成一套固定流程大约只要五分钟就能从原始数据到一张符合投稿标准的圆盘图。回头来看Cartopy的学习成本不算高但关键节点就那么几个理解坐标系、用好transform、把颜色和字体控制到位、该裁剪时果断裁剪。把这些点逐一拿捏住再复杂的北半球专题图也不过是在这个骨架上叠加不同的数据层而已。最后再分享一个小技巧我一般在正式出图之前会先用低分辨率、低dpi快速跑一遍流程确认数据范围、配色、布局都没问题再调整到高dpi保存最终版。这样既能快速迭代又不用反复等待大文件写入。这个习惯帮我省下了很多不必要的时间也让我很少在投稿前的截稿日手忙脚乱。