资讯动态

Basemap与mplot3d:Matplotlib高级地图与三维绘图详解

发布时间:2026/10/14 21:49:10 来源:尧图企业网站定制
Matplotlib走到高级阶段绕不开两个工具包Basemap和mplot3d。Basemap负责把经纬度数据变成地图上的点、线、等值面mplot3d负责把三维数据变成能旋转观察的散点、曲面和轨迹。很多人被“高级图像”四个字吓住觉得一定需要海量参数和复杂算法其实这两个工具包的核心逻辑都很直白先做坐标转换再选图元类型最后调整视角和配色。这篇我想把这条主线讲透中间穿插能直接跑的代码和我实际摔倒过的地方。如果你已经用Matplotlib画过折线图和散点图知道figure和axes大概是干什么的那接下来这些内容就吃得动。尤其平时要处理“带地理位置的数据”或者想把“有高度、有深度”的结果展示给别人看我敢说你迟早会碰到地图投影和三维坐标这两道坎与其到时候临时查文档不如现在跟着把这两块拼图补上。1. 这两个“老伙计”到底在解决什么问题1.1 Basemap的核心不是画地图是坐标系转换Basemap第一眼看上去像个画地图的工具drawcoastlines画海岸线drawcountries画国界线fillcontinents填大陆颜色甚至还有卫星影像底图。但如果你只把它当成“贴地图”的库后面一定会踩坑。它真正的灵魂是那个在初始化时创建的投影对象以及一个看起来不起眼的调用方式把经纬度数组传进去得到画布上的平面坐标。我举个例子。在等距圆柱投影下经纬度和图上坐标接近线性关系你偶尔不转换也能碰巧画对但换成兰伯特等角圆锥投影或者正射投影经纬网是弯的直接把经纬度当平面坐标去画散点数据会全部挤压到某个角落看起来像“地图上莫名其妙多了一坨点”。Basemap的价值就是把这类投影变换封装好你用m(lons, lats)拿到x、y再用x、y去调用plot、scatter、contourf数据才真正长在地图上。反变换同样重要。m(x, y, inverseTrue)能把图上坐标还原成经纬度。这个功能在“鼠标点击地图取经纬度”“把图上不规则区域映射回地理坐标”等场景里很实用。我见过不少教程只讲正向转换不讲反变换导致读者想从图上反查地理位置时无从下手。1.2 mplot3d的本质是二维画布上的三维投影mplot3d这个名字容易让人误会它是个真正的3D引擎。实际上它还是跑在Matplotlib的二维画布上只是把三维坐标通过透视投影换算成平面坐标再交给普通绘图命令渲染。这也解释了为什么它不做精细的遮挡计算两个曲面交错时谁挡谁经常要依靠zorder或者视角微调来补救而不是像专业三维软件那样自动处理。理解了这一层很多困惑会迎刃而解。比如为什么在Notebook里旋转视角之后某个散点突然从曲面后面冒出来了因为每次旋转都会重新计算投影关系而Matplotlib对线框和曲面的绘制顺序并不总是符合真实深度。遇到这种情况别怀疑自己代码写错绝大多数时候是绘制顺序和投影算法共同作用的结果。1.3 为什么停更的库和新手库都值得认真学Basemap已经停止维护很多年mplot3d也不算新工具但现实是大量老项目还在用Basemap跑地图出图大量教学和科研代码在用mplot3d快速看三维形态。学它们不纯粹是学“过时技术”而是学两件能带走的东西地图投影到底怎么影响数据呈现三维可视化如何在保真和信息量之间取舍。这两类思维换任何工具都适用。2. Basemap停维后的环境搭建安装、验证与数据文件2.1 安装最容易卡住的地方由于Basemap不再更新在新版Python上安装的体验经常一言难尽。直接pip install basemap大概率会在编译阶段报“找不到GEOS”这类错误因为源码包需要本地的GEOS、PROJ等地理库参与编译而Windows用户往往没有完整的C编译环境。我实际使用中比较稳的安装顺序是这样的优先用condaconda install -c conda-forge basemap。conda会把GEOS等依赖一起处理好省去手动折腾。如果只能用pip去找与当前Python版本和系统架构完全匹配的预编译wheel包下载到本地再pip install。这一步要注意Python版本必须严格对应否则装了也会在import时报错。Linux环境可以先安装geos库的开发包再尝试pip install但我很少推荐走这条编译路线耗时且容易出幺蛾子。一个更现实的建议不要在Python 3.12以上的环境里硬装Basemap。老库对高版本解释器的兼容基本断档折腾半小时不如换思路要么降到conda环境要么直接切到后面会聊的Cartopy。2.2 安装后先跑通一个最小地图案例装好之后不要急着画漂亮地图先跑一段最小验证代码确认核心依赖没问题。下面这段用来验证Basemap能正常创建投影对象并完成坐标转换from mpl_toolkits.basemap import Basemap import numpy as np m Basemap(projectioncyl, llcrnrlon100.0, urcrnrlon130.0, llcrnrlat20.0, urcrnrlat45.0, resolutionl) x, y m([105.0, 120.0], [30.0, 40.0]) print(x) print(y)如果这段代码能打印出两个数组说明Basemap可以正常创建投影并执行经纬度转平面坐标。注意这里只是虚构区域示例经纬度范围我随手写的重点在于能跑通。接着再叠加第一个图层import matplotlib.pyplot as plt fig plt.figure(figsize(8, 6)) m Basemap(projectioncyl, llcrnrlon100.0, urcrnrlon130.0, llcrnrlat20.0, urcrnrlat45.0, resolutionl) m.drawcoastlines(linewidth0.8) m.drawcountries(linewidth0.5) m.drawparallels(np.arange(20, 45, 5), labels[True, False, False, False]) m.drawmeridians(np.arange(100, 130, 5), labels[False, False, False, True]) plt.show()如果drawcoastlines报错找不到数据集那就进入了下面这个小坑。2.3 数据文件、分辨率与地形底图Basemap画海岸线和国界线依赖包内自带的数据文件它们通常位于mpl_toolkits/basemap/data目录下。有时候你升级或更换了Basemap版本旧的数据目录没跟着更新drawcoastlines就会找不到文件。Windows环境下还要注意路径中是否有空格或中文字符某些版本会因此读取失败。比这更麻烦的是etopo、shadedrelief这类地形底图数据。它们不会随pip包一起自动下载需要单独获取数据文件并放到data目录。文件体积不小而且很多旧下载链接已经失效属于典型的“停更后遗症”。我的建议是别在这种底图上死磕干净利落地用drawcoastlines加fillcontinents做出最简底图把精力留给数据本身。分辨率参数从低到高是c、l、i、h、f分别对应crude、low、intermediate、high、full。测试阶段用l足够出图阶段再根据范围调整。如果范围很大还选f级光渲染海岸线就能等半天。3. mplot3d的高级绘图技巧从散点到曲面3.1 先选图元再想效果mplot3d的入口是创建一个三维坐标轴ax fig.add_subplot(111, projection3d)。拿到ax之后能画的图形类型和二维ax高度对应只是多了一个z维度。我自己习惯先根据需求选图元再调样式这样不会在参数细节里迷路。下面这张表是我平时用的速查表按“想表达什么”来选函数需求函数数据量参考最容易翻车的点离散点分布ax.scatter数千点没问题点重叠、颜色不分层轨迹线ax.plot数千点可以线条自遮挡、走向误导光滑曲面ax.plot_surface网格别超150x150网格太密卡顿、颜色断层快速线框ax.plot_wireframe网格别超100x100线太密看起来像一团乱麻底平面等值线ax.contourf网格同曲面容易忘了offset参数向量场ax.quiver网格30x30左右箭头长度和比例要额外调“数据量参考”是我实际使用的经验值不是硬性限制。plot_surface的网格并不是越细越好超过300x300后旋转视角会明显卡顿保存PDF时文件体积也容易爆炸。如果需要非常精细的曲面优先考虑缩小范围而不是无脑增加网格密度。3.2 一个完整的曲面绘制案例用经典函数Z np.sin(np.sqrt(X² Y²))来演示三维曲面的基本流程这段代码把创建网格、计算曲面、上色、加颜色条、设视角全部走了一遍import numpy as np import matplotlib.pyplot as plt from matplotlib import cm fig plt.figure(figsize(9, 6)) ax fig.add_subplot(111, projection3d) x np.linspace(-5, 5, 80) y np.linspace(-5, 5, 80) X, Y np.meshgrid(x, y) R np.sqrt(X**2 Y**2) Z 2.5 * np.sin(R) / (R 1e-3) surf ax.plot_surface(X, Y, Z, cmapcm.coolwarm, linewidth0, antialiasedTrue, alpha0.95) fig.colorbar(surf, axax, shrink0.6, aspect12) ax.view_init(elev25, azim-55) ax.set_xlabel(X) ax.set_ylabel(Y) ax.set_zlabel(Z) plt.show()这里有几个容易被忽略的细节R 1e-3是为了避免除零让函数在原点附近依然平滑linewidth0能去掉曲面上的网格线视觉干净不少alpha0.95留了一点点透明这样如果后面叠加contourf之类的图层还能隐约看到曲面轮廓colorbar的shrink和aspect控制颜色条的长短粗细不同版本的Matplotlib可能需要微调数值。3.3 视角、三轴比例和标签的细节ax.view_init(elev, azim)是控制三维视角的核心。elev是俯仰角azim是方位角。在Jupyter Notebook里你可以用鼠标拖拽旋转但一旦要出图、做报告就必须手动定一个合适角度。我个人常用的组合是elev25到30azim-60到-70这个范围能看到曲面顶面和部分侧面立体感比较强。另一个容易翻车的地方是三轴比例。mplot3d默认会把三个坐标轴各自自动缩放导致一个本应该对称的球体被压成扁盘子。解决办法是显式声明三个轴等比ax.set_box_aspect((1, 1, 1))如果数据范围差异很大比如x从0到100z从0到0.1强行等比会让z方向的变化根本看不出来。这时候要冷静判断你到底想强调哪个维度的差异不是所有图都适合等比。还有一个小坑ax.legend()在三维图里经常不好使。三维散点有多个类别时图例默认能生成但marker在图例中会变得非常小甚至看不清。可以在legend里加markerscale参数比如ax.legend(locupper left, markerscale2.0)图例中的点会放大两倍看起来舒服很多。4. Basemap投影实战选择逻辑、散点、等值线叠加4.1 投影不是滤镜是坐标变换选择投影时不要只看“好看”。投影的本质是把球面坐标展开到平面任何平面地图都不可能完全不变形关键在于你想保留哪个性质是角度准确、面积准确还是视觉上比较均衡。Basemap支持的投影非常多我实际常用的有这几类投影关键特征适合场景cyl经纬网是直线低纬变形小全球经纬度格点展示merc等角高纬面积放大明显局部航海、中低纬区域lcc中纬地区变形小中纬度区域天气图aea面积保持准确人口密度、资源分布ortho从球外看地球的效果全球鸟瞰示意robin全球变形比较均衡全球综合现状展示不同投影的参数差别很大。Mercator一般需要lat_0和lon_0LCC在标准纬线场景还要提供lat_1、lat_2AEA也需要两条标准纬线。参数设错时不一定会直接报错但画出来的范围会和预期对不上。我建议第一次使用某个投影时先打印一下边界坐标确认投影范围符合预期再继续叠加数据。4.2 把网格数据画成等值面关键一步是转换坐标很多人在Basemap上画等值线时出错根因都是一样的没有先把经纬度网格转换成地图坐标。下面这段代码用LCC投影演示区域填色等值线的完整流程from mpl_toolkits.basemap import Basemap import numpy as np import matplotlib.pyplot as plt fig plt.figure(figsize(9, 7)) m Basemap(projectionlcc, lon_0105, lat_030, width2.4e6, height2.0e6, resolutionl) m.drawcoastlines(linewidth0.8) m.drawcountries(linewidth0.5) m.drawmapboundary() lons np.linspace(102, 112, 50) lats np.linspace(27, 37, 50) Lons, Lats np.meshgrid(lons, lats) X, Y m(Lons, Lats) data (np.exp(-0.02 * ((Lons - 107)**2 (Lats - 32)**2)) - 0.3 * np.exp(-0.05 * ((Lons - 109.5)**2 (Lats - 31.2)**2))) cs m.contourf(X, Y, data, levels15, cmapinferno) cb plt.colorbar(cs, shrink0.7, pad0.05) plt.show()这里的核心是X、Y来自m(Lons, Lats)它们和Lons、Lats的形状一一对应data仍然在经纬度网格上计算。只有把经纬度网格转换到地图坐标之后contourf才知道该把等值面画在地图的哪个位置。如果你把原始经纬度直接传给contourf在LCC投影下坐标网格不是均匀的等值线会错位甚至乱成一团。4.3 叠加散点、海岸线和其他图层等值面底图之上最常见的需求是再叠加一批站点观测点。做法和之前类似先把站点经纬度转成地图坐标再调用绘图命令station_lons np.array([104.2, 108.5, 111.3, 106.7, 110.0]) station_lats np.array([29.8, 33.4, 35.1, 28.2, 31.6]) xpts, ypts m(station_lons, station_lats) m.plot(xpts, ypts, ko, markersize5)如果站点数量很多建议用散点按数值大小映射颜色s m.scatter(xpts, ypts, cstation_values, cmapviridis, s30, edgecolork) plt.colorbar(s, shrink0.7)要注意Basemap对scatter的c参数支持得不错但s的单位仍然是“点”站点密集时要把s调小并且加edgecolor让重叠的点不至于糊成一片。还有一点散点的颜色条最好单独生成不要贪方便绑在等值线的colorbar上不然图例含义会混。5. 组合玩法在Basemap地图上叠加三维数据5.1 一个可以直接跑的混合案例Basemap和mplot3d到底能不能混用答案是能而且Basemap支持传入ax参数可以让部分图层直接画在三维坐标轴上。下面这个案例我把海岸线和经纬网格作为“地面”三维散点表示某个虚构区域里的观测高度整体立体感很强from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.basemap import Basemap import matplotlib.pyplot as plt import numpy as np fig plt.figure(figsize(10, 7)) ax fig.add_subplot(111, projection3d) m Basemap(projectioncyl, llcrnrlon100.0, urcrnrlon130.0, llcrnrlat20.0, urcrnrlat45.0, resolutionl, axax) m.drawcoastlines(linewidth0.6) m.drawcountries(linewidth0.4) m.drawparallels(np.arange(20, 45, 5), labels[True, False, False, False]) m.drawmeridians(np.arange(100, 130, 5), labels[False, False, False, True]) rng np.random.default_rng(42) n 100 lons rng.uniform(103, 126, n) lats rng.uniform(23, 42, n) height 200 1500 * np.exp(-0.02 * ((lons - 115)**2 (lats - 33)**2)) xpts, ypts m(lons, lats) s ax.scatter(xpts, ypts, height, cheight, cmapplasma, s45, edgecolorgray, linewidth0.3) ax.set_zlim(0, 2000) ax.set_zlabel(height) fig.colorbar(s, axax, shrink0.6, aspect12) ax.view_init(elev25, azim-65) plt.show()这段代码里Basemap的线状图层会落在z0的平面上三维散点则拔高到各自的高度。颜色条映射的还是散点高度语义很清楚。cyl投影只是图方便换成lcc也一样能跑只要保证所有经纬度都先经过m()转换。5.2 坐标对齐与图层遮挡的两个坑混用时最大的坑是坐标基准不统一。Basemap返回的x、y是投影坐标mplot3d画数据也要用同一套坐标。如果没有经过m()转换直接把经纬度传进3D散点在cyl投影下也许能碰巧对上换成lcc就全乱套。所有想画到地图上的散点、柱体、曲面必须先过一遍m()这一点没有任何偷懒空间。第二个坑是填充和图片底图在三维轴上经常不工作。fillcontinents在3D轴里会形成一个很怪的平面色块卫星影像底图也很难正确平铺。我试了几次之后基本放弃在3D轴上用这类图层只保留海岸线、国界线、经纬网格这些线状元素。需要区域感的时候用浅色半透明柱体或让散点自己去讲故事效果反而干净。第三个坑是颜色条。fig.colorbar(s, axax)在三维轴里能用但某些版本下shrink参数会失效颜色条位置需要手动调整。我通常的做法是先不设shrink等出图时看尺寸再微调bbox反而更快。5.3 什么时候不该把地图和3D硬凑在一起有一段时间我很喜欢做三维地图后来发现很多场景强行3D化反而损失信息。如果只是想展示空间分布二维地图加colorbar是表达效率最高的方案读者能快速定位峰值区域。三维适合增加“真的有意义”的高度轴比如地形高程、大气层高度、地下深度。如果第三个维度只是为了视觉上酷一点会让读者难以精确读值得不偿失。另外三维图的交互旋转在静态PDF或打印稿里完全不可用。你调整好的立体角度在纸面上取决于elev和azim选得好不好。选得差读者很容易把两个不同高度的点误判成同一水平线上的点。所以最终出图前我会让身边人先看一眼确认没有明显误导。6. 停更之后Basemap的替代选择与新项目建议6.1 Basemap vs Cartopy本质差异在哪里Basemap停更多年官方推荐替代方案是Cartopy。两者解决的问题高度重合但代码风格完全不同。Basemap面向过程你得先调用m(lons, lats)手动转换坐标Cartopy则把transform参数直接写进绘图函数比如ax.scatter(lons, lats, transformccrs.PlateCarree())。初学者看到Cartopy时最容易懵的就是这套声明式写法尤其是当一个图里存在不同投影的数据源时每个图层都要声明transform漏一个就错位。我整理了一个简单对比维度BasemapCartopy维护状态已停止更新持续更新底层依赖GEOS、PROJPROJ为主代码风格用m()转换坐标过程式绘图时传transform声明式学习曲线容易理解但细节靠记初期概念多理顺后更省心三维轴支持可传ax画线状图层较复杂需要额外处理新项目建议不推荐推荐两者不是零和关系。会了Basemap的投影概念理解Cartopy的transform会更快反过来习惯了Cartopy也能更清楚Basemap当年封装了什么。6.2 老项目迁移与旧脚本维护的心得如果你只是偶尔复现旧脚本其实不必急着迁移。Basemap虽然停更但旧版本包和数据集仍然能找到在受控环境里可以继续跑。我的建议是锁定Python和Basemap版本不要在全新环境里反复试错浪费时间。真到了必须迁移到Cartopy的那天我自己的套路分三步走把Basemap初始化投影改写成Cartopy的投影设置把m()转换调用改成绘图函数里的transform参数处理resolution对应的海岸线。Cartopy通常用NaturalEarthFeature或GRIDDED海岸线数据图层粒度更细但参数也多。迁移中最容易漏掉的是经纬网格标签。Cartopy不像Basemap那样一行就能画出带标签的经纬线需要自己处理gridlines。我第一次迁移时在这个地方卡了半天后来才发现要单独设置xlocator和ylocator。这部分没有捷径纯属熟能生巧。6.3 给新手的路线建议如果你今天刚开始学我的建议很直接用Cartopy画地图用mplot3d画三维图。mplot3d会长期存在Cartopy能让你走得更远Basemap可以作为阅读老代码时的一个“低层理解工具”不值得花费大量精力去深挖。遇到相关需求或旧代码时再回来补Basemap的投影概念也不迟。最后分享一个我调过很多次的小经验无论Basemap还是mplot3d出图前先把figsize和dpi固定否则每次看图里的字号、图例位置和最终文件版本都不一样。Basemap这类工具对中文字体支持一般地图上想要加中文标注时要先设置plt.rcParams[font.sans-serif]再检查负号是否显示正常这个坑看起来小但真遇到了很烦人。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑