☰
Matplotlib Basemap与Mplot3D:三维数据与地理地图可视化实战
2026/10/14 13:09:57 网站建设 项目流程

做数据可视化这几年,Matplotlib 是我电脑里打开频率最高的库之一。日常画折线、柱状、散点图自然不在话下,但一旦涉及两个方向——“三维数据”和“地理坐标数据”,很多人的第一反应是转去学别的库。其实 Matplotlib 自带的两个工具箱 Basemap 和 Mplot3D 就能覆盖绝大多数场景,而且和主库的 API 风格一脉相承。这篇文章就围绕这两块展开:Mplot3D 做三维散点、曲面、等高线和向量场,Basemap 做海岸线、国界、经纬网格以及地理数据叠加。如果你手里有带经纬度的数据,或者想呈现三维的分布关系,又不想引入太重的新框架,这篇内容基本可以让你直接动手。

先说个真实的背景。这两年在项目里,我经常碰到两类需求:一类是把气象站点或城市数据画到地图上,另一类是把模型的预测曲面或者参数空间的三维结构展示出来。前者我用 Basemap,后者我用 Mplot3D。虽然现在社区里 Cartopy 热度很高,但 Basemap 在很多存量项目里依然在跑,尤其是一些科研脚本和教学代码,学会了它再迁移到 Cartopy 也更容易理解投影的本质。而 Mplot3D 虽然看起来像是 Matplotlib 的“附加功能”,但它提供的 Axes3D 对象在交互式探索和论文插图之间找到了一个很实用的平衡点。下面我会从项目实操的角度,把这两个工具箱的选型逻辑、核心函数、参数陷阱和排查方法全部讲透。

1. 内容整体设计与思路拆解

1.1 为什么把 Basemap 和 Mplot3D 放在一起讲

很多教程把这两个工具箱分开讲,好像它们是两个完全独立的世界。但我在实际项目里发现,它们的配合频率远比你想象的高。举个例子,我做过一个地形可视化项目:原始数据是某区域的 DEM 高程栅格,带有经纬度坐标。我需要先借助 Basemap 将经纬度投影到平面坐标,然后基于这些平面坐标构造三维网格,再用 Mplot3D 的 plot_surface 把地形起伏画出来。这一步如果有人只学了其中一个工具,就会卡住:只用 Basemap 只能出二维平面图,高程信息只能靠颜色编码;只用 Mplot3D 则完全不知道怎么处理经纬度投影。

另一个场景是科研论文里的“地图 + 数据层”插图:底图用 Basemap 绘制海岸线和国界,数据层用散点或等值线叠加,最后如果需要展示三维趋势,再用 Mplot3D 把数据曲面画在投影坐标上。所以这两个工具箱本质上都在解决“把数据放到正确的空间坐标里”这件事,只是一个偏向平面地理空间,一个偏向三维数学空间。把它们放在同一篇文章里,不是因为它们都叫 Matplotlib 工具箱,而是因为它们的组合能解决一类更完整的数据表达问题。

1.2 方案选型:什么时候选 Basemap,什么时候直接上 Mplot3D

先说 Mplot3D。它的适用场景很明确:数据本身是三维的,或者需要第三个维度来表达强度/趋势。比如机器学习里的网格搜索误差曲面、流体仿真的速度场、信号的时频分布,这些数据天然有三个轴。Mplot3D 的优势是学习成本低——你只需要在创建图形时指定 projection='3d',之后所有绘图函数几乎都是二维版本的“升维”版本。

Basemap 的适用场景则完全不同。它适合所有带有地理坐标的数据,无论数据本身是二维还是三维。我自己的选择标准是这样的:如果数据里有 lat/lon 字段,且展示结果需要参考真实的行政边界或海岸线,那就必须用 Basemap(或者它的继任者 Cartopy);如果数据只是抽象的数学空间三维点,那直接用 Mplot3D,压根不要考虑地图。

还有一个关键区别在于坐标系处理方式。Mplot3D 是在常规笛卡尔坐标系上扩展了 Z 轴,而 Basemap 做的是“投影变换”:把球面上的经纬度坐标映射到平面上的 x/y 坐标。Basemap 的一大好处是,你可以在对象上调用 map(x, y) 方法,把经纬度数组一次性转换为平面坐标,然后交给任何需要 x/y 的绘图函数使用。这个机制让它和 Mplot3D 可以无缝衔接,后面我会专门演示。

1.3 Basemap 的“历史包袱”问题:为什么还在用

写这篇文章之前,我必须先说明一个背景:Basemap 官方已经停止更新,Cartopy 是官方推荐的替代品。但我依然认为 Basemap 值得讲,原因有三个。

第一,存量代码太多。我接手过好几个高校实验室的数据处理脚本,里面清一色是 basemap 的投影方式和绘图层逻辑,迁移到 Cartopy 需要改动大量 API 调用。看懂 Basemap 代码,是维护这些项目的基本功。

第二,Basemap 的教学价值不低。它的投影实现非常直观:你实例化一个 Basemap,指定投影名和边界参数,它就把经纬度换算成画布上的毫米坐标。理解了这个过程,再去看 Cartopy 的投影体系会轻松很多。

第三,某些历史数据文件里的投影参数说明,用的还是 Basemap 的命名方式,比如 'lcc'、'robin'、'cyl'。如果你要读取一份老数据,能识别这些字符串的含义是很有用的。

当然,如果你是全新的项目,我建议直接用 Cartopy。但本文以 Basemap 为主,是因为它和 Matplotlib 主库的耦合更简单,而且在三维地形绘制这种场景里,Basemap 的投影结果直接交给 Mplot3D 用的流程,至今仍有参考价值。

2. Mplot3D 核心用法与参数细节

2.1 三维坐标系:Axes3D 的创建方式与基本属性

Mplot3D 的入口非常简单,两种写法我都常用:

import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np # 写法一:先建 fig,再添加 3d 子图 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection='3d') # 写法二:直接指定 projection(新版推荐) fig = plt.figure(figsize=(8, 6)) ax = plt.axes(projection='3d')

第二种写法在 Matplotlib 3.x 里更干净,也不需要显式引入 Axes3D,因为 add_subplot 和 axes 方法都能识别 projection 参数。创建好坐标系之后,常用的属性设置和二维没有本质区别:set_xlabel、set_ylabel、set_zlabel、set_title 都会自动生成三维字样的标签。

但这里有一个新手最容易忽略的差距:三维坐标系的刻度标签默认是自动缩放的,而且三个轴的比例互不关联。如果你的数据在 X 方向跨度 1000,Y 方向跨度只有 10,那么图像看起来会被压扁,看不出真实的几何关系。我建议每次创建 Axes3D 后,都按需设置坐标轴范围。比如:

ax.set_xlim(0, 1000) ax.set_ylim(0, 10) ax.set_zlim(0, 50) ax.set_box_aspect((1, 1, 0.8)) # 控制三个轴显示比例

set_box_aspect 是 Matplotlib 3.4 以后才有的方法,它让三维图像的“盒子”比例可以人为控制。如果没这个方法,就得手动计算 aspect,比较麻烦。下面给出一个完整的三维散点示例,这个例子我经常用来测试坐标系是否正常:

rng = np.random.default_rng(42) x = rng.normal(0, 1, 200) y = rng.normal(0, 1, 200) z = rng.normal(0, 1, 200) fig = plt.figure(figsize=(8, 6)) ax = plt.axes(projection='3d') sc = ax.scatter(x, y, z, c=z, cmap='viridis', s=30, alpha=0.7) fig.colorbar(sc, ax=ax, shrink=0.6, label='z value') ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') plt.show()

这里的c=z让颜色反映第三维数值,alpha=0.7则避免点太多时互相遮挡。散点图的颜色映射配合 colorbar,基本上能满足九成的三维点云展示需求。

2.2 三维曲面与线框:plot_surface 和 plot_wireframe 的差异与搭配

三维曲面是 Mplot3D 里出图效果最“高级”的类型,也是论文里最常见的插图形式。plot_surface 需要网格化的数据,也就是 X、Y、Z 都是二维数组,通常来自 np.meshgrid。

x = np.linspace(-3, 3, 100) y = np.linspace(-3, 3, 100) X, Y = np.meshgrid(x, y) Z = np.sin(np.sqrt(X**2 + Y**2)) fig = plt.figure(figsize=(10, 6)) ax = plt.axes(projection='3d') surf = ax.plot_surface(X, Y, Z, cmap='coolwarm', linewidth=0, antialiased=True) fig.colorbar(surf, ax=ax, shrink=0.5, aspect=12) plt.show()

plot_surface 的几个参数值得注意。linewidth=0是为了消除网格线对曲面颜色的干扰,否则高分辨率下网格线会像布纹一样难看。antialiased=True和抗锯齿相关,保持默认即可。cmap决定了曲面的颜色编码方式,我一般用 'coolwarm' 或者 'viridis',前者适合有正负值的波动面,后者适合纯粹的数值强度。

线框图 plot_wireframe 则完全不同,它只画网格线,不填充颜色。这个图适合观察数据的骨架结构,尤其在数据量极大、曲面填充会严重遮挡视线的时候。我通常会把两者叠加:先用线框画出“骨架”,再用低透明度的曲面填充上颜色,视觉上既能看出结构又有立体感。

ax.plot_wireframe(X, Y, Z, color='k', linewidth=0.3, alpha=0.4) ax.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.6, linewidth=0)

这里有个容易踩的坑:当数据量很大时,plot_surface 的网格密度过高会让图形渲染变慢,甚至导致矢量输出文件体积巨大。解决办法是控制采样密度,比如只取 50x50 的网格,而不是 300x300。很多初学者以为网格越密图越好看,结果导出的 PDF 几十兆,投稿时直接被拒。我的经验是:显示趋势的插图 60x60 足够,需要精细的局部细节再提高密度。

2.3 三维等高线与向量场:contour、contourf 和 quiver

在三维坐标系中画等高线有两种思路:一种是把等高线投影到某个平面上,另一种是直接在三维曲面表面绘制等值线。Mplot3D 的 contour 函数默认把等高线画在坐标系的底部平面上(zdir='z' 的 offset 位置),这很像地质图里的俯视投影。如果想画在曲面上,则需要设置 offset 参数为 None 并且传入正确的 zdir。

fig = plt.figure(figsize=(10, 6)) ax = plt.axes(projection='3d') ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.5, linewidth=0) # 底面投影的等高线 ax.contour(X, Y, Z, zdir='z', offset=-2, cmap='viridis', levels=15) # 侧面投影 ax.contour(X, Y, Z, zdir='y', offset=3, cmap='viridis', levels=15) plt.show()

等高线投影对于理解三维结构的“低谷”和“峰脊”非常有帮助,尤其是当曲面之间相互遮挡的时候。你可以在一个图里同时画底部投影和侧面投影,这样三维结构的不同角度信息一目了然。

三维向量场用 quiver 绘制。quiver 的签名是quiver(X, Y, Z, U, V, W),其中 U、V、W 分别是 X、Y、Z 方向的分量。一个常见的误区是:向量场的坐标点网格过密,导致图像变成“刺猬”,完全看不清楚方向。解决方式是降采样,或者用length参数控制箭矢长度。

x = np.linspace(-2, 2, 10) y = np.linspace(-2, 2, 10) X, Y = np.meshgrid(x, y) Z = np.zeros_like(X) U = -Y V = X W = np.zeros_like(X) fig = plt.figure(figsize=(8, 6)) ax = plt.axes(projection='3d') ax.quiver(X, Y, Z, U, V, W, length=0.35, normalize=True, color='steelblue') ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') plt.show()

这个例子画的是一个平面上的旋转场,把 W 改成非零值就可以变成真正三维向量场。quiver 的normalize=True参数会把所有箭矢归一化到同样长度,适合观察方向;如果你更关注向量长度本身,就不要设置这个参数。

2.4 三维图形的视角控制与交互技巧

三维图最“劝退”新手的一点是视角选不好,出来的图全是遮挡和变形。Mplot3D 默认视角是 elevation=30 度,azimuth=-60 度。实际项目里我几乎每次都会手动调整:

ax.view_init(elev=25, azim=35)

view_init 的两个参数:elev 是观察点的高度角,0 度就是平视,90 度就是俯视;azim 是方位角,相当于绕 Z 轴旋转的角度。我的建议是先跑一版默认视角,然后打印几个候选视角存成图片对比,选择信息损失最小的那个。不少同事用交互窗口手动拖到合适角度,再用ax.view_init记录当前的 elev 和 azim 数值,这个操作可以通过ax.elev和ax.azim读取,非常实用。

另外,当图里同时有多个数据层时,zorder 控制绘制顺序的效果在三维坐标系里远不如二维明显,所以我们更多依靠 alpha 透明度和视角来避免遮挡。这也是为什么我上面反复强调 alpha 参数:三维绘图中透明度是比颜色更重要的设计变量。

3. Basemap 投影方式与地图绘制实操

3.1 安装与最基本的地图骨架

Basemap 的安装是个经典痛点。它依赖 GEOS 库,直接用 pip 在很多环境里会报错。最稳妥的方式是用 conda:

conda install basemap

如果你的环境里有多个 Python 版本混杂,建议单独建一个虚拟环境。安装完成后,先画一个最简单的世界地图,验证环境是否正常:

from mpl_toolkits.basemap import Basemap fig = plt.figure(figsize=(12, 6)) m = Basemap(projection='cyl', resolution='c', llcrnrlon=-180, llcrnrlat=-60, urcrnrlon=180, urcrnrlat=75) m.drawcoastlines(linewidth=0.6) m.drawmapboundary(fill_color='aqua') m.fillcontinents(color='coral', lake_color='aqua') plt.show()

这个例子用的是 'cyl' 投影(等距圆柱投影),llcrnrlon/llcrnrlat/urcrnrlon/urcrnrlat 分别指定左下角和右上角的经纬度边界。drawcoastlines 画海岸线,fillcontinents 给大陆填充颜色,drawmapboundary 给海洋区打底色。如果看到上述字符正常输出,说明 GEOS 库和 Basemap 都正常。

Basemap 的投影方式有几十种,常用的是以下几种:

投影名用途特点
'cyl'全球数据经纬直线网格,根本不需要球面变换
'merc'航海图、区域图等角,形状保真,面积失真
'lcc'中纬度地区兰伯特等角圆锥投影,适合区域气象图
'robin'全球专题图伪圆柱,面积大致相等,视觉均衡
'ortho'地球仪视角从太空看地球的效果,常用于示意图

选投影时需要考虑你的数据分布区。如果数据集中在中国的华东地区,用 'merc' 加合适的经纬度边界,画出来的形状最自然;如果数据是全球温度场,用 'robin' 或 'cyl' 更常见。很多文章配图喜欢用 'ortho' 的球体效果,但它只适合表达地理位置的相对关系,不适合精确读图。

3.2 在 Basemap 上叠加坐标转换与数据层

Basemap 的核心机制是投影变换。当你创建好 m 之后,传入经纬度数据前先做一次转换:

lons = np.array([116.4, 121.5, 104.1, 113.3]) lats = np.array([39.9, 31.2, 30.7, 23.1]) x, y = m(lons, lats) m.plot(x, y, 'ro', markersize=8) m.scatter(x, y, c=[10, 20, 15, 25], cmap='hot', marker='o', s=100)

这个 m(lons, lats) 的调用方式非常像函数调用,实际执行的是经纬度到平面坐标的映射。得到的 x/y 就是可以直接交给 Matplotlib 常规绘图函数的数据点。注意,尽量不要直接把经纬度传给 plot 或 scatter,否则画出来的是经纬网格坐标系,而不是 Basemap 真正的投影结果。

要画等值线或者填充等值线,思路一样。假设你有网格化的经纬度数据 grid_lons、grid_lats,对应的数据场 grid_data:

x, y = m(grid_lons, grid_lats) cs = m.contourf(x, y, grid_data, levels=12, cmap='RdYlBu_r') fig.colorbar(cs, ax=plt.gca(), shrink=0.6) m.drawcoastlines(linewidth=0.5) m.drawcountries(linewidth=0.5, color='gray')

这里有一个非常关键的细节:contourf 的 x/y 必须是二维网格,而且 grid_lons 和 grid_lats 要来自同一个 meshgrid 顺序。如果源数据是 netCDF 或 GeoTIFF 读进来的,通常需要转置。我经常遇到的现象是,数据画出来之后地图位置整体偏移或者梯度方向反转,十有八九就是维度顺序反了。排查方式是在转换前先检查 grid_lons.shape 和 grid_data.shape 是否一致,不一致就试试.T转置。

3.3 绘制国界、经纬网与区域图

Basemap 最常用的几个绘制方法是 drawcoastlines、drawcountries、drawstates 和 drawparallels/drawmeridians。它们的调用方式如下:

m.drawcoastlines(linewidth=0.8, color='black') m.drawcountries(linewidth=0.5, linestyle='--', color='gray') m.drawstates(linewidth=0.3, color='lightgray') parallels = np.arange(-60, 90, 15) meridians = np.arange(-180, 180, 30) m.drawparallels(parallels, labels=[1,0,0,0], fontsize=10) m.drawmeridians(meridians, labels=[0,0,0,1], fontsize=10)

drawparallels 和 drawmeridians 的 labels 参数是有顺序的 [左、右、上、下],表示在这个边缘是否标注刻度值。比如 labels=[1,0,0,0] 表示在左边标注纬度值,labels=[0,0,0,1] 表示在底部标注经度值。这是 Basemap 里最容易让人迷惑的参数,没有之一,因为它的顺序不是直观的“上下左右”,而是“左右上下”。

对于区域图,比如你要画某个省份的地图,Basemap 也能通过经纬度边界实现:

m = Basemap(projection='merc', llcrnrlon=100, llcrnrlat=20, urcrnrlon=125, urcrnrlat=42, resolution='l')

这里 resolution 参数决定了地图细节等级,有 'c'(粗糙)、'l'(低)、'i'(中)、'h'(高)、'f'(完整)几个选项。级别越高,海岸线和国界文件越精细,但绘制越慢,包体积也越大。建议区域图用 'l' 或 'i',全球图用 'c' 就足够。有些环境里 resolution='f' 需要额外下载数据,离线环境会报错,这是另一个坑,后面排查部分再讲。

3.4 用 Basemap 做带底色的渐变地图

有时候数据覆盖整个地图区域,我们想画出类似“热度图”的效果:底图区域填充数据等值面,陆地边界叠加在数据之上。这里要注意图层顺序:先填充数据(contourf),后画海岸线和国界,这样边界线会盖在色块上,清楚显示行政区域。

fig = plt.figure(figsize=(10, 8)) m = Basemap(projection='cyl', resolution='c', llcrnrlon=100, llcrnrlat=15, urcrnrlon=125, urcrnrlat=45) x, y = m(grid_lons, grid_lats) m.contourf(x, y, grid_data, levels=15, cmap='Spectral_r') m.drawcoastlines(linewidth=0.8) m.drawcountries(linewidth=0.5, color='black') m.drawparallels(np.arange(15, 46, 5), labels=[1,0,0,0]) m.drawmeridians(np.arange(100, 126, 5), labels=[0,0,0,1]) plt.colorbar(label='value') plt.show()

因为等值面是填充在海洋上方的,所以海洋区域的色块也会显示出来。如果你希望海洋统一显示为浅蓝色,可以使用m.drawmapboundary(fill_color='lightblue')先填充底色,再用m.fillcontinents(color='none')把陆地原有的底色盖掉。这个“先底后顶”的顺序是整个 Basemap 绘制流程里最重要的思维。

4. 三维地图:把 Basemap 和 Mplot3D 组合起来

4.1 为什么三维地形图需要这两个工具协同

我前面提到过,带经纬度的高程数据(DEM)必须经历“投影变换”才能成为三维坐标。这一节的完整思路是:先用 Basemap 把经纬度网格投影成平面坐标 x/y,再把高程值赋给 Z,然后交给 Mplot3D 的 plot_surface 绘制。这样画出来的地形图,水平方向的比例是地图投影后的真实距离关系,垂直方向是高程放缩。

这种图在展示山区地貌、流域地形、气象要素场的三维结构时非常直观。比如我做过一个降水数据三维展示:X 和 Y 是投影后的平面坐标,Z 是地形高程,颜色映射的是降水量。这样一张图,同时展示了地形和降水分布,读者一眼就能看出降水与山脉走向的关系。

4.2 一个完整的地形三维图示例

假设你有经纬度网格 lon_grid、lat_grid 和对应高程数据 dem。下面是一个可以跑通的示例:

import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.basemap import Basemap from mpl_toolkits.mplot3d import Axes3D # 模拟数据:100x100 的经纬度网格 lons = np.linspace(110, 118, 100) lats = np.linspace(30, 38, 100) lon_grid, lat_grid = np.meshgrid(lons, lats) # 模拟高程:一个山脊效果 dem = 100 * np.exp(-((lon_grid - 114)**2 + (lat_grid - 34)**2) / 10) dem += 50 * np.exp(-((lon_grid - 112)**2 + (lat_grid - 32)**2) / 5) # 1. 创建 Basemap 投影 m = Basemap(projection='merc', llcrnrlon=110, llcrnrlat=30, urcrnrlon=118, urcrnrlat=38, resolution='l') # 2. 投影转换 x, y = m(lon_grid, lat_grid) # 3. 三维绘制 fig = plt.figure(figsize=(12, 8)) ax = plt.axes(projection='3d') surf = ax.plot_surface(x, y, dem, cmap='terrain', linewidth=0, antialiased=True) fig.colorbar(surf, ax=ax, shrink=0.5, label='Elevation (m)') ax.set_xlabel('X (m)') ax.set_ylabel('Y (m)') ax.set_zlabel('Elevation (m)') ax.view_init(elev=30, azim=-60) plt.show()

这里有几个细节要强调。第一,x/y 是投影后的平面坐标,单位是米(取决于投影方式),而不是经纬度。所以坐标轴标签不要写 lon/lat,要写 X/Y 投影坐标。第二,plot_surface 要求 x/y/z 都是相同维度的二维数组,这里 lon_grid、lat_grid 经过 meshgrid 已经是二维,投影后也是二维。第三,Z 轴(dem)如果量纲和 x/y 不一致,比如平面上跨度 800 公里,高程只有 100 米,那么三维图会像一个薄片。此时需要用ax.set_box_aspect((1, 1, 0.3))调整垂直方向的比例,或者人为把高程乘以一个比例系数。

# 调整垂直比例:把高程放缩到合理视觉范围 z_scale = (x.max() - x.min()) / (dem.max() - dem.min()) * 0.15 ax.set_box_aspect((1, 1, z_scale))

这个经验来自我的实操:一开始直接把真实高程画上去,地形起伏完全看不见,因为水平跨度太大。后来换成按视觉比例放缩,才达到“像真实山体”的效果。如果你要展示的是地形趋势而非精确高度,视觉放缩是完全合理的手段。

4.3 把数据层也叠加到三维空间

在这个三维地形图上,你还可以叠加散点数据或者路径线。比如在投影后的 x/y 基础上加上一个固定的垂直偏移,把站点数据“浮”在地形上方:

# 假设观测点经纬度 obs_lons = np.array([113.5, 112.8, 115.6]) obs_lats = np.array([33.2, 31.5, 35.1]) ox, oy = m(obs_lons, obs_lats) oz = np.interp(ox, x[0], dem[:, 0]) # 简化插值,实际应二维插值 # 用二维线性插值更稳妥:scipy.interpolate.griddata ax.scatter(ox, oy, oz + 20, color='r', s=40, label='Obs Site') ax.plot([ox[0], ox[0]], [oy[0], oy[0]], [oz[0]+20, oz[0]-100], color='r', lw=0.8)

这种“站点悬空”的做法可以让观测点位和地形高程同时显示在三维空间里,比二维地图直观得多。实际项目中我会用 scipy 的 griddata 做完整的二维插值,确定每个站点对应的高程值,这里的简化示例是示意。

5. 常见问题与排查技巧实录

5.1 Basemap 安装与数据文件缺失问题

Basemap 最常出问题的环节其实不是画图,而是安装和运行时的资源文件。常见报错是ImportError: No module named mpl_toolkits.basemap,这种一般是没装好或者装进了错误的 Python 环境。我的排查顺序是:

  1. 确认当前 python 指向哪个解释器,用的是 conda 还是系统自带。
  2. 用conda install basemap而不是 pip,因为 conda 会自动拉取 GEOS 依赖。
  3. 如果 conda 里没有对应版本(比如 Python 3.10 以上的新环境),建议建一个 Python 3.8 或 3.9 的 conda 环境来装。

另一个报错是读取海岸线数据时出现IOError: file not found,这通常是 resolution 级别对应的高精度数据文件缺失。解决办法是降低 resolution 级别,或者手动下载数据文件放到 Basemap 的数据目录。离线环境装 Basemap 是最痛苦的,我建议在联网机器上把数据文件缓存好,再复制到离线环境。

5.2 投影边界困惑:为什么画出来一片空白或者区域偏了

画区域图时,很多人都会犯一个错误:投影方式选好之后,经纬度边界却不匹配。比如用 'merc' 投影时,如果 llcrnrlon 和 urcrnrlon 跨度太大,远高纬度地区在墨卡托投影下会严重拉伸,导致地图变成一个细长的条。我遇到过一次这个情况:数据采集区域在 20°N 到 50°N,横跨 60 度,用 'merc' 画出来图非常扁,后来换成 'lcc' 投影就正常了。

投影和边界必须作为整体来考虑。'merc' 适合赤道附近和中低纬度的局部区域;'lcc' 适合中纬度的大区域;'cyl' 适合全球范围;'robin' 适合全球专题图。如果你不知道选什么,制图的通用建议是:全球用 'robin' 或 'cyl',跨省跨流域用 'lcc',城市尺度用 'merc'。

还有个常见问题:经纬度数据的顺序不对。Basemap 的投影方法m(lon, lat)第一个参数必须是经度。不少从 GIS 软件导出的数据习惯写 lat/lon,直接传入会导致点在图上颠倒镜像。我的排查技巧是:先画两个已知坐标的点,比如北京(116.4, 39.9)和广州(113.3, 23.1),如果相对位置是错的,立刻检查经纬度顺序。

5.3 Mplot3D 的渲染与坐标轴问题

Mplot3D 常见的问题主要是渲染模糊、轴刻度重叠、隐藏面错误。

渲染模糊:保存图片时默认 dpi 太低,或者矢量图里曲面颜色块过多。如果是期刊投稿,保存矢量图时适当降低网格密度,否则文件体积爆炸;如果是屏幕展示,把 dpi 提到 150~200 效果会好很多。

轴刻度和标签重叠:三维图旋转之后,刻度标签可能互相遮挡。我的办法是调整 view_init 找到一个合适的观测角度,或者用ax.tick_params(axis='x', pad=5)之类的方法调整间距。如果标签实在太多,可以减少刻度数量,比如:

ax.set_xticks([-2, 0, 2]) ax.set_yticks([-2, 0, 2])

隐藏面错误(表面穿透):这是三维渲染的经典问题,当曲面自交或渲染顺序判断错误时,后面的面会错误地覆盖前面的面。解决方法比较有限:可以降低透明度(alpha 小于 0.6 通常能减少视觉冲突),或者提高网格密度减少自交概率,也可以调整视角绕过问题区域。我的经验是,不要追求完美渲染,选择角度和透明度组合让整体清晰即可。

5.4 问题速查表

现象可能原因解决动作
Basemap 导入失败GEOS 未安装 / Python 环境混用conda 安装 basemap,或重建 3.8/3.9 环境
地图区域空白边界参数与投影不匹配检查 llcrnrlon/lat 与 urcrnrlon/lat 是否合理
点位置错乱经纬度顺序反了用 m(lon, lat) 而不是 m(lat, lon)
海岸线缺失resolution 数据文件缺失降低 resolution 级别,或补齐数据文件
三维曲面被遮挡视角或透明度不合适调 view_init,调 alpha 到 0.5~0.7
三维曲面是薄片Z 轴比例与平面不一致用 set_box_aspect 调整比例
contourf 位置偏移网格维度顺序不对检查 shape 并用 .T 转置
图片文件体积巨大网格过密 + 矢量输出降低网格密度,或用 PNG 输出
刻度标签挤在一起三维视角不佳view_init 调整,或用 set_xticks 减少刻度

6. 个人实操心得与踩坑记录

说几个我反复踩过、后来形成习惯的细节。

第一个是三维图的自定义颜色映射。很多领域的配色方案有约定,比如降水喜欢用蓝绿色系,气温喜欢用红蓝色系。Mplot3D 和 Basemap 都支持 cmap,但我建议把 colorbar 的刻度边界手动设置,不要全交给自动刻度:

levels = np.linspace(data_min, data_max, 12) surf = ax.plot_surface(X, Y, Z, cmap='terrain', vmin=data_min, vmax=data_max)

vmin/vmax 显式制定后,多组图片之间可以保持颜色尺度一致,方便对比。这在论文组图里尤其重要,不然同一物理量在不同图上颜色深浅不同,容易被审稿人质疑。

第二个是关于数据投影的口诀:先转换,后绘图。不管是散点、等值面还是三维地形,只要数据带经纬度,先让 Basemap 把它变成 x/y,再交给任何 Matplotlib 绘图函数。这套“先换坐标再画图”的思维,迁移到 Cartopy 后也成立。Cartopy 不过是把投影方式封装得更面向对象,但“经纬度转平面坐标”的核心步骤并没有变。

第三个是关于配色与可读性。三维图大都信息密度高,如果用默认的连续色带,容易显得杂乱。我的做法是:优先用有明确语义的色带,比如 terrain 对应地形,RdYlBu_r 对应正负变化,viridis 默认最安全。等高线级别数量控制在 10~15 之间,太多显得密集,太少丢失细节。

第四个,也是我感触最深的一点:三维和地图可视化最耗费时间的往往不是画图本身,而是数据预处理。经纬度顺序、网格维度、缺失值处理、投影边界选择,这些环节出问题的时候,图怎么调都不对。我建议你在项目开始的时候,先写一个最小可视化样例,只用 3~5 个点位验证坐标关系,确认无误后再上全量数据。这个习惯帮我省下了大量查错时间。

这套 Basemap + Mplot3D 的组合,在我做过的多个数据可视化项目里表现稳定。如果你手头恰好有带地理坐标的数据,或者想把三维结构的趋势展示给读者,这两个工具箱足够支撑你完成从初稿到成图的全过程。等你熟悉了这里的坐标变换逻辑,再去接触更现代的绘图库,会发现很多概念都是相通的——毕竟,所有可视化最终都在回答同一个问题:数据应该摆在哪里,才最容易被理解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询