ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Matplotlib高级绘图:Basemap地图与Mplot3D三维可视化实战

Matplotlib高级绘图:Basemap地图与Mplot3D三维可视化实战

1. 这个项目到底要解决什么问题

1.1 为什么标题里同时出现 Basemap 和 Mplo3D

先交代一下背景。这个项目标题是"Matplotlib绘制高级图像",第6节,内容锁定在 Basemap 和 Mplo3D 两个工具上。前面几篇写的都是常规的折线图、柱状图、散点图、热力图,说白了都在二维平面里打转。但实际工作中,数据不会永远乖巧地躺在一张图上,你会遇到两类比较棘手的需求:

第一类,数据本身带地理位置属性。比如某公司在全国多个营业网点的销售额、某区域的气象站点观测值、某个物流系统里一批车辆的轨迹点。这时候普通散点图把经纬度当横纵坐标画出来,效果非常尴尬——没有海岸线、没有国界、没有地形概念,读者完全看不出这些点在地球上哪个位置。Basemap 就是专门干这个的,它能把地理底图和数据叠在一起,输出一张真正意义上的"地图"。

第二类,数据本身是三维的。比如某个函数在二维平面上的取值、三维空间中的点云分布、或者某个物理场的曲面。这时候你需要能旋转、能看深度的图表,而 Matplotlib 自带的 mpl_toolkits.mplot3d 工具集(我们习惯叫它 Mplt3D)就是最省事的入口,不用装任何额外库,直接跟着绘图代码走。

这两个工具解决的问题完全不同,但有一个共同点:它们都是 Matplotlib 生态里"进阶玩家"绕不开的组件。这篇博文会把两者拆开讲透,每个都给出可直接落地的代码、参数选择和踩坑记录,顺便把两者可以联动的地方也聊聊。

1.2 哪些人适合读这篇

如果你是这几类读者,这篇文章应该能给你省下不少查资料的力气:

  • 刚学完 Matplotlib 基础、想往地图可视化或三维可视化方向走的学生;
  • 做数据分析、地理信息或科研绘图,需要把结果画在地图或三维坐标系上的从业者;
  • 被 Basemap 安装报错、投影参数看不懂折腾过,想找个系统梳理的人。

我会尽量用"为什么这么做"的思路去讲,而不是丢一堆参数让你背。比如同一个地图数据,为什么有人用墨卡托投影,有人用兰伯特投影,背后的逻辑讲清楚了,你自己就能判断该选哪个,不用每次靠试错。

2. 动手之前的准备:环境、版本和那个绕不开的"坑"

2.1 安装 Basemap 的推荐姿势

Basemap 的安装比普通 Python 库麻烦一点,这是它劝退很多新手的第一道坎。如果你直接pip install basemap,在部分环境下确实能装上,但它依赖一个底层的地理计算库 GEOS,一旦系统里没有这个库或者版本对不上,编译阶段就会报一堆乱七八糟的错误,比如gcc failed、找不到geos_c.h之类。

我实测下来最省心的是用 conda 装:

conda install basemap

conda 会自动把 GEOS、pyproj 这些依赖一起处理好,基本不会出现缺库的问题。如果你的环境是纯 pip 管理的,可以试:

pip install basemap

装上之后立刻在 Python 里验证一下:

from mpl_toolkits.basemap import Basemap print("Basemap import ok")

这一行能顺利跑通,说明 GEOS 和 pyproj 都正常,后续就稳了。如果 import 阶段就报错,先别急着找 Basemap 的问题,优先检查 pyproj 和 GEOS 的动态库路径,这是九成安装失败的根源。

2.2 必须提前说清楚的"历史遗留"问题

这里有个很重要的背景要交代:Basemap 官方早已停止维护,最后发版停留在一个比较老的版本上,官方推荐的新替代方案是 Cartopy。这意味着什么呢?意味着如果你手里是新的 Python 环境和新的 Matplotlib 版本,Basemap 很容易出现各种兼容性报错,最典型的就是和 Matplotlib 的某些内部接口对不上。

那为什么这篇还在讲 Basemap?原因有两条。

第一,存量项目里 Basemap 的代码量极其庞大,很多老系统、论文代码、企业内部工具都是用 Basemap 写的,你早晚要读懂和迁移这些代码。第二,Basemap 的 API 设计非常直观,理解它之后再看 Cartopy 会轻松很多,甚至可以说 Basemap 就是最好的地图可视化入门教材。

我的建议是:如果是写新项目、用新环境,优先考虑 Cartopy;如果是复现老代码、维护旧系统,或者就是想低成本快速出一张地图,Basemap 依然可以跑。这篇博文以 Basemap 为主来拆解,最后我会提一下迁移到 Cartopy 时要注意的映射关系。

至于 Mplot3D,它在 Matplotlib 里属于核心工具集,一直由官方维护,不存在这类历史包袱,直接from mpl_toolkits.mplot3d import Axes3D就能用,前提只是你的 Matplotlib 版本别太老。

3. Basemap 地图可视化:从经纬度到画布像素的那层转换

3.1 先理解 Basemap 在干什么

Basemap 的核心作用一句话就能说清:它把"经纬度坐标"翻译成"画布上的像素坐标",同时在画布上画好海岸线、国界、经纬网格,再把你的数据点按照同样的翻译规则放上去。

这个"翻译"之所以必要,是因为地球是个球体,而屏幕是平面。把一个球面展开成平面,必然会有变形,不同的展开方式就是不同的投影(Projection)。Basemap 里最常用的几个投影参数值,我整理成一张表:

参数值投影类型适合场景直观感受
cyl等距圆柱投影全球分布、简单概览经纬网横平竖直,非常直观
merc墨卡托投影低纬度区域、海图、航线角度不变形,但高纬度区域被拉得很大
mill米勒圆柱投影全球数据的常规展示比 merc 变形小一点,视觉更舒服
lcc兰伯特正形圆锥投影中纬度国家、地区级地图中纬度区域变形小,常用于气象图
ortho正射投影半球视角、球体效果像从太空看地球,边缘是圆的
robin罗宾逊投影全球统计图变形均衡,视觉效果好

选择投影的关键原则不是"哪个好看",而是"哪个在你关注的区域变形最小"。你画中国区域的数据,用merc会导致高纬度省份被拉伸;用lcc中纬度区域变形很小,效果就明显更合理。画全球航线,merc的优势就出来了,因为墨卡托投影下等角航线是一条直线。

3.2 从零画第一张地图

Basemap 画底图的方式是先创建一个 Basemap 对象,然后调用它的绘图方法。最入门的一组代码是这样的:

import matplotlib.pyplot as plt from mpl_toolkits.basemap import Basemap fig = plt.figure(figsize=(10, 8)) m = Basemap(projection='mill', lon_0=105, lat_0=35, llcrnrlon=70, llcrnrlat=15, urcrnrlon=140, urcrnrlat=55) m.drawcoastlines(linewidth=0.5, color='gray') m.drawcountries(linewidth=0.8, color='black') m.drawmapboundary(fill_color='lightcyan') m.fillcontinents(color='lightgreen', lake_color='lightcyan') m.drawparallels(range(15, 56, 10), labels=[1, 0, 0, 0]) m.drawmeridians(range(70, 141, 10), labels=[0, 0, 0, 1]) plt.show()

几个关键参数解释一下。

  • projection指定投影方式,这里用mill做区域展示比较舒服。
  • lon_0、lat_0是地图中心的经度和纬度。llcrnrlon、llcrnrlat、urcrnrlon、urcrnrlat分别指定左下角和右上角的经纬范围,也就是你截取的地图区域。
  • drawcoastlines()画海岸线,drawcountries()画国界,这两个是最基础的地图要素。
  • drawparallels()画纬线,drawmeridians()画经线。那个四元素列表labels控制经纬度数字标注在哪个边上,顺序是左、右、上、下,1 表示显示,0 表示不显示。

这里有个新手常犯的错:经纬范围写反了。llcrnrlon是左下角的经度,urcrnrlon是右上角的经度,如果从左下角到右上角横跨东西方向太远,地图可能被压得比例失调。我的习惯是先把区域大概范围写在纸上,再填参数,避免凭感觉一通乱填。

3.3 在地图上叠加你的数据点

底图画好之后,叠加数据就很简单了。Basemap 对象本身是一个可调用对象,你传给它的经纬度,它会返回画布上的像素坐标x、y,然后用plot、scatter、contourf这些 Matplotlib 常规绘图函数去画就对了。

比如我要在地图上标注几个站点位置:

# 模拟几个站点的经纬度 lons = [112.5, 118.2, 121.8, 104.1, 126.6] lats = [30.4, 32.1, 34.6, 28.7, 36.5] fig = plt.figure(figsize=(10, 8)) m = Basemap(projection='lcc', lon_0=105, lat_0=32, llcrnrlon=95, llcrnrlat=25, urcrnrlon=125, urcrnrlat=40) m.drawcoastlines(linewidth=0.5) m.drawcountries(linewidth=0.8) m.fillcontinents(color='wheat', lake_color='lightcyan') x, y = m(lons, lats) m.scatter(x, y, s=80, c='red', marker='o', zorder=5) for lon, lat, xi, yi in zip(lons, lats, x, y): plt.text(xi + 50000, yi, f'({lon:.1f}, {lat:.1f})', fontsize=9) plt.show()

注意我在调用m(lons, lats)时传的是列表,Basemap 会自动把它们转换成一维数组。如果传的是单个经纬度,返回的也是单个坐标。这个"传多个返回多个"的接口设计让批量画点非常方便。

还有一个细节:数据比底图要素高一层,需要在scatter里加zorder=5。否则某些投影下,填充的大陆色块可能把点盖住。zorder是绘图层级,数字越大画在越上面,这个技巧在 Basemap 里几乎每次都要用,尤其是点很小或者颜色和底色接近的时候。

3.4 在地图上画等值线和填充图

地图上最常见的进阶操作是画等值线,比如气温分布、人口密度、降雨量。Basemap 提供了一套直接在投影坐标上操作的接口:

import numpy as np # 生成网格经纬度 lons_grid, lats_grid = np.meshgrid( np.linspace(95, 125, 60), np.linspace(25, 40, 60) ) # 模拟一个随距离衰减的"场" distance = np.sqrt((lons_grid - 112) ** 2 + (lats_grid - 34) ** 2) z_data = 30 * np.exp(-distance / 8) fig = plt.figure(figsize=(11, 8)) m = Basemap(projection='lcc', lon_0=105, lat_0=32, llcrnrlon=95, llcrnrlat=25, urcrnrlon=125, urcrnrlat=40) x_grid, y_grid = m(lons_grid, lats_grid) m.drawcoastlines(linewidth=0.5) m.drawcountries(linewidth=0.8) m.drawmapboundary(fill_color='white') contourf = m.contourf(x_grid, y_grid, z_data, levels=20, cmap='RdBu_r') m.colorbar(contourf, location='right', pad="5%") plt.show()

这里最核心的转换发生在m(lons_grid, lats_grid)这一行。你必须先把经纬度网格全部转换成投影坐标x_grid、y_grid,再传给contourf。如果你直接拿经纬度网格去画,图上的等值线位置会全部错位,因为底图是投影变形过的,而你的数据还停留在球面坐标里。

这个问题非常隐蔽,因为某些区域范围较小的时候,经纬度和投影坐标的差异肉眼看不出来,但一旦范围变大,错位就很明显了。我当年第一次画全国范围的填充图,就是因为忽略了这步转换,结果等值线和海岸线怎么都对不齐,排查了半天才反应过来。

另外,levels参数控制等值线的分级数量。数值太少,图看起来像马赛克;数值太多,相邻色块差异不明显。一般 15 到 30 之间是比较舒服的范围,具体看你数据的标准差大小。如果数据分布特别不均匀,可以传入一个显式的级别列表,比如levels=[5, 10, 15, 20, 25, 30],让色块集中在数据密度高的区间,比被动接受均匀间隔要专业得多。

4. Mplot3D 三维可视化:让数据多一个维度

4.1 三维坐标系是怎么工作的

Mplot3D 的设计思路和 Basemap 完全不同。Basemap 做的是"球面到平面的映射",而 Mplot3D 做的是"三维空间到二维画面的投影"。

你创建三维坐标轴之后,本质上是把一组(x, y, z)三元组绘制在一个可旋转的虚拟空间里,Matplotlib 用一个虚拟相机去拍这个空间,最后呈现在屏幕上的是相机视角下的二维投影。这就是为什么三维图能通过view_init(elev, azim)调视角——你动的不是数据,是相机的位置。

创建三维坐标轴的推荐方式有两种:

from mpl_toolkits.mplot3d import Axes3D import matplotlib.pyplot as plt # 方式一:add_subplot 指定 projection fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111, projection='3d') # 方式二:subplots 后用 add_subplot 绑定 # 老版本也支持 ax = fig.gca(projection='3d'),但新版不建议

这里要提醒一个变化:Matplotlib 3.x 早期版本里,很多人用ax = Axes3D(fig)这句话创建三维坐标轴,其实它依赖一个隐式的"注册"机制,现在的新版本已经不建议这么写了,统一用projection='3d'参数最稳妥。

elev是仰角,控制你从多高的位置往下看;azim是方位角,控制你绕着竖轴转了多远。默认是elev=30, azim=-60,我一般先转几圈找一个信息最全的角度,再用ax.view_init(elev, azim)固定下来。

4.2 三维散点图和线图

三维散点图和二维散点图的代码几乎一样,只是多了z数据:

import numpy as np np.random.seed(42) n_points = 300 x = np.random.randn(n_points) y = np.random.randn(n_points) z = np.random.randn(n_points) # 用到原点的距离作为颜色映射 colors = np.sqrt(x**2 + y**2 + z**2) fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111, projection='3d') scatter = ax.scatter3D(x, y, z, c=colors, cmap='viridis', s=30, alpha=0.8) fig.colorbar(scatter, ax=ax, shrink=0.5, label='Distance') ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') plt.show()

三维散点图最容易犯的错是点太多,密密麻麻堆在一起,什么分布都看不出来。300 个点已经需要加透明度alpha=0.8才能看清结构,如果是两三千个点,建议把alpha降到 0.3 左右,或者用s=0.5之类的极小的点尺寸。另一种处理方式是按某个维度做颜色映射,让点在视觉上分层,这也是我比较推荐的做法。

三维线图用在轨迹、路径、时序数据上非常直观。比如一条三维空间曲线:

t = np.linspace(0, 4 * np.pi, 500) x = np.cos(t) y = np.sin(t) z = t / 3 fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111, projection='3d') ax.plot3D(x, y, z, color='dodgerblue', linewidth=2) ax.scatter3D(x[::50], y[::50], z[::50], color='red', s=20) plt.show()

这种螺旋线图在通信、信号处理和物理轨迹演示中经常用到。plot3D内部就是ax.plot(x, y, z),写法上和二维完全一致,只是多传一组 z 数据。

4.3 曲面图和等高线:三维绘图的真正主角

三维绘图最常用的场景其实是画曲面,也就是一个函数z = f(x, y)在整个平面区域上的取值。画曲面前必须先理解np.meshgrid的作用:

x = np.linspace(-5, 5, 50) y = np.linspace(-5, 5, 50) X, Y = np.meshgrid(x, y) Z = np.sin(np.sqrt(X**2 + Y**2))

meshgrid把一维的x数组和一维的y数组扩展成网格矩阵,这样X的每一行都是同一个横坐标,Y的每一列都是同一个纵坐标,组合起来就是平面上所有格点。没有这一步,你无法对"整个平面区域"逐点计算 z 值。

网格生成后,画曲面图非常直接:

fig = plt.figure(figsize=(11, 8)) ax = fig.add_subplot(111, projection='3d') surf = ax.plot_surface(X, Y, Z, cmap='coolwarm', edgecolor='none', alpha=0.95) fig.colorbar(surf, ax=ax, shrink=0.5, label='Z value') ax.set_title('Surface Plot: z = sin(sqrt(x^2 + y^2))') plt.show()

plot_surface的几个关键参数值得单独记一下:

  • cmap控制曲面配色,coolwarm适合有正有负的数据,viridis适合单调递增的数据。
  • edgecolor='none'去掉网格线,否则曲面被密密麻麻的线框覆盖,视觉上非常脏。想在保留结构感的同时不喧宾夺主,可以用edgecolor='black', linewidth=0.1。
  • alpha控制透明度,尤其在叠加多个曲面时非常有用,缺省状态下后画的曲面会完全遮住先画的。

如果你数据本身比较粗糙,只想看大致骨架,可以用线框图plot_wireframe,它只画网格线不填充颜色。线框图的网格密度由rstride和cstride控制,这两个参数是"每隔几个点画一条线",不是线的总数。网格太密会糊成一片,太疏又看不出曲面形状,一般默认值在 50×50 的网格上表现不错,超过 100×100 建议调大步长。

4.4 三维等高线:把深度信息压回平面

三维图有个固有的痛点:旋转到某个角度时,被遮挡的部分看不见,打印出来之后没法旋转,信息就丢了。解决办法是把等高线"压"到坐标平面上,用二维的方式表达三维的信息。

Mplot3D 支持把等高线画在三个不同的平面方向上,通过zdir参数控制:

fig = plt.figure(figsize=(12, 8)) ax = fig.add_subplot(111, projection='3d') # 画曲面 surf = ax.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.6, edgecolor='none') # 在 z 方向的最低处投影等高线填充 ax.contourf(X, Y, Z, zdir='z', offset=-2.0, cmap='coolwarm', alpha=0.4) # 在 x 方向的最后面投影等高线 ax.contour(X, Y, Z, zdir='x', offset=6.0, levels=20, colors='gray', linewidths=0.5) ax.set_zlim(-2, 2) plt.show()

offset是投影平面的位置。把zdir='z'的等值线投影到offset=-2.0,意味着你在曲面下方挖了一层平面,把山地等高线铺在上面;zdir='x'则是把等值线投影在立方体的侧面。这种"俯视图加投影"的组合非常适合论文配图,既保留了立体感,又全方位表达了数据分布。

三维等高线和二维contourf有个重要区别:三维版本支持多方向投影,而二维版本只能平铺在平面图上。实际使用中,我经常用ax.view_init(elev=90, azim=-90)把图旋转成纯俯视视角,这时候三维等高线图就等价于一张二维地图,但视角调整比二维灵活得多。

5. 组合进阶:把两类高级绘图串成一套流程

5.1 用"三维表面到二维投影"的思路设计图表

Basemap 画地图、Mplot3D 画立体图,看起来是两套独立体系,但如果你把第四章的等高线投影思路想清楚,会发现它们之间有一条暗线连在一起:任何带地理属性的三维场数据,都可以先用 Mplot3D 看立体趋势,再用 Basemap 落到地图上做空间展示。

举一个典型的组合流程。假设你有一个区域内的气象观测数据,横纵坐标是经纬度,高度方向的取值是某个指标。你可以先用 Mplot3D 对经纬度网格做插值并画曲面,直观地看出这个指标在空间上的"山峰"和"低谷";然后切到 Basemap,把同样的插值网格用contourf画在地图上,叠加海岸线和国界,得到一张可以直接进报告的空间分布图。

这种"先立体看形态,再平面看位置"的两阶段分析,比只画一张图的信息量要大得多。我在实际项目中就是这么干的:第一步用 Mplot3D 找异常区域,第二步用 Basemap 精确定位异常发生在哪片区域,两步互相验证,最后成图的逻辑也说得通。

5.2 保存和导出时要注意的细节

三维图和地图在导出时都有各自的坑。Basemap 生成的图本质是一张 Matplotlib 图像,直接用plt.savefig就行,但要注意两点:

  • 底图要素、坐标标注和标题的字体大小需要在导出前调好,PDF 导出后矢量字体清晰,PNG 导出要看dpi够不够,一般报告插图建议dpi=200以上。
  • Basemap 图里如果有fillcontinents或者drawmapboundary,导出时颜色不会自动适配白色背景,如果你要把图嵌进深色背景的 PPT 里,记得把facecolor参数一并处理。

Mplot3D 的导出更麻烦一点。三维图的立体效果依赖视角,导出的静态图就只有当前视角这一个画面。如果别人需要看完整形态,可以考虑这两个备选方案:

# 方案一:调好视角再导出 ax.view_init(elev=30, azim=120) plt.savefig('output.png', dpi=200, bbox_inches='tight') # 方案二:保存为多帧 gif 或者直接交互式查看 # 交互式窗口里按住鼠标拖拽即可旋转视角

如果是做汇报,我一般导两张不同视角的静态图,一张俯视、一张侧视,信息互补;如果是给别人自己看的,直接用交互式窗口展示效果最好。

5.3 一个完整的组合示例

我把整个流程整合成一个可以直接跑通的完整脚本,坐标和数值都是虚拟数据,逻辑是完整的。这个脚本做了两件事:用 Mplot3D 展示一个三维场,再用 Basemap 把同一个场画成地图填充图。

import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.basemap import Basemap # ---- 第一步:生成虚拟网格数据 ---- lons = np.linspace(100, 120, 40) lats = np.linspace(28, 40, 40) LON, LAT = np.meshgrid(lons, lats) # 模拟一个中心高、四周低的场 dist = np.sqrt((LON - 110)**2 + (LAT - 34)**2) Z = 20 * np.exp(-dist / 5) + np.sin(LON * 0.5) * np.cos(LAT * 0.3) # ---- 第二步:三维展示 ---- fig1 = plt.figure(figsize=(10, 7)) ax1 = fig1.add_subplot(111, projection='3d') surf1 = ax1.plot_surface(LON, LAT, Z, cmap='terrain', edgecolor='none', alpha=0.9) ax1.contourf(LON, LAT, Z, zdir='z', offset=-8, cmap='terrain', alpha=0.4) ax1.set_xlabel('Longitude') ax1.set_ylabel('Latitude') ax1.set_zlabel('Value') ax1.view_init(elev=25, azim=-120) # ---- 第三步:地图展示 ---- fig2 = plt.figure(figsize=(11, 8)) m = Basemap(projection='lcc', lon_0=110, lat_0=34, llcrnrlon=98, llcrnrlat=26, urcrnrlon=122, urcrnrlat=42) m.drawcoastlines(linewidth=0.5) m.drawcountries(linewidth=0.8) m.drawmapboundary(fill_color='white') x_map, y_map = m(LON, LAT) contourf_map = m.contourf(x_map, y_map, Z, levels=20, cmap='terrain') m.colorbar(contourf_map, location='right', pad="5%") plt.show()

运行完这个脚本,左边是立体的场分布,右边是带地理要素的空间分布,同一份数据、两种表达,各自的信息重点不一样。这种组合方式非常实用。

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

6.1 问题速查表

把我在实际使用中踩过的、以及身边同事朋友问过的高频问题整理成一张速查表,按问题现象、可能原因、解决办法三列来排:

问题现象可能原因解决办法
Basemap import 即报错GEOS 或 pyproj 依赖缺失用 conda 重装 basemap,或用 conda 单独安装 geos
地图数据点位置和预期不符数据经纬度没经过 Basemap 对象转换必须调用m(lons, lats)得到投影坐标再画点
等值线和海岸线错位网格数据直接用经纬度画了 contourf先x_grid, y_grid = m(lon_grid, lat_grid)再画
三维图视角看起来是"反的"默认elev/azim不合适用ax.view_init(elev, azim)手动调整
曲面图网格太密/太疏rstride、cstride没调网格大时调大步长,只保留结构线
三维散点图密密麻麻看不清点数太多且透明度为 1设置alpha=0.3左右,或按颜色分层
Basemap 画全国图高纬度变形严重projection选成了 merc换lcc或mill,中高纬度用圆锥投影
图例/色条位置不对colorbar参数没配好Basemap 图用m.colorbar(contourf, location='right', pad='5%')

6.2 几个值得写进笔记的实操心得

第一个心得:画地图前先小范围测试投影参数,别一上来就全量跑。我习惯先用一个很小的经纬范围,比如 5度×5度,把投影、底图要素配好,确认视觉没问题了,再放大到目标区域。这样做的原因是,投影参数选错时,微观范围下很难看出来变形程度,一旦放到全国或者全球范围,变形差异立刻被放大。

第二个心得:三维图的裁切问题比很多人想象中常见。Mplot3D 在绘制曲面时,如果zlim设置得太紧,曲面超出边界的部分会被直接裁掉,看起来就像模型缺了一块。这种"缺边"现象经常被误会成数据问题。排查思路很简单:先不设zlim看完整范围,再决定要不要裁剪。同样的道理也适用于xlim和ylim。

第三个心得:配色系统的选择要跟随用途。我在 Basemap 和 Mplot3D 里常用的几套色系是固定的:地形高度用terrain,正负值对称数据用coolwarm或RdBu_r,连续单调数据用viridis或plasma,离散类别数据用tab10这类定性色图。别在一张图里混搭两套语义不同的色系,比如左边用红蓝表示正负,右边又用红蓝表示类别,读者会直接被误导。

第四个心得:关于迁移到 Cartopy。如果你今天刚学完 Basemap,准备在新项目里用 Cartopy,只需记住一个核心映射:Basemap 的Basemap(...)对象在 Cartopy 里被拆成了两个部分——投影系统用ccrs模块定义,绘图用的还是一个普通的 Matplotlib 坐标轴。数据转换的调用方式也从m(lon, lat)变成了ax.scatter(lon, lat, transform=ccrs.PlateCarree())。思路是一样的,只是包装方式不同。等你在 Basemap 上把"先转换坐标、再画图"这个心智模型建立起来,切过去基本就是半天的事情。

最后多说一句,虽然 Basemap 步入暮年,但它教会我的地图可视化逻辑到现在依然受用。每次画图前先想清楚:数据在哪个坐标系里、目标区域选什么投影、底图和数据怎么分层、配色有没有误导性。这四个问题想明白了,用哪个库只是顺手的事。

我个人在实际操作中最深的一个体会是:高级绘图工具的真正价值不在于"画得花哨",而在于"帮你发现数据里那些平面图看不出来的结构"。Mplot3D 和 Basemap 都放大了一个共同的东西——空间关系。不管是经纬度意义上的地理空间,还是数学意义上的三维数值空间,把空间关系可视化出来,很多平时藏在表格里的信息就自己浮出来了。这也是为什么我到现在依然觉得,这两个工具是 Matplotlib 进阶路上最值得花时间吃透的部分。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进