最近有小伙伴问到在栅格地图上绘制等降水量线的问题。本课程将 R 版本的代码翻译成 Python,使用 Python 生态中的 xarray、rasterio、geopandas、matplotlib 等库,实现与 R 版本完全等价的三张地图:
- 各省份年均降水量填充地图(分段色彩)
- 降水量栅格地图 + 等降水量线(连续渐变色)
- 栅格地图 + 带文字标签的等降水量线
附件中提供了 2021 年各月的降水量栅格数据:pre_2021.nc,数据来源于 中国1km分辨率逐月降水量数据集(1901-2023):https://data.tpdc.ac.cn/zh-hans/data/faae7605-a0f2-4d18-b28f-5cee413766a2 。
在学习本课程之前,建议预先了解:
使用 Python 绘制历年中国省市区县地图(小地图版本+长版):https://rstata.duanshu.com/#/brief/course/72c36d788f9d4497a7e33f23f9437598
一、R 语言与 Python 的包对应关系
绘制降水量地图涉及的核心 R 包及其 Python 对应关系:
| R 包 | Python 对应 | 主要用途 |
|---|---|---|
| terra / raster | rasterio + xarray | 读取、裁剪、处理栅格数据 |
| sf | geopandas + shapely | 矢量数据读取和处理 |
| ggplot2 + ggspatial | matplotlib + geopandas | 地图绘制 |
| nngeo::st_remove_holes | shapely exterior 提取 | 去除多边形孔洞 |
| rasterToContour | matplotlib.contour | 生成等值线 |
| rasterToPolygons | scipy.ndimage + shapely.geometry.box | 栅格转多边形 |
| geomtextpath | matplotlib ax.text + 旋转角度 | 等值线文字标签 |
二、使用 reticulate 创建与管理 Python 虚拟环境
在 R 中通过 reticulate 包来调用 Python,最好的实践是为项目创建一个专属的 Python 虚拟环境,将所需依赖隔离到独立空间,避免与系统 Python(如 Anaconda)发生版本冲突。
重要说明(避免”已初始化”报错):reticulate 在 R 会话中只能绑定一次 Python——一旦某个
{python}代码块运行,Python 解释器就被锁定,之后再调用use_virtualenv()会报错。因此,虚拟环境的激活必须在所有{python}代码块之前完成。本文档的解决方案是在setupchunk 中通过Sys.setenv(RETICULATE_PYTHON = ...)提前锁定 Python 路径。
2.1 安装 reticulate(仅首次)
# 设置 CRAN 镜像(knit 时 R 处于非交互模式,不会自动选择镜像) |
2.2 虚拟环境初始化原理(已在 setup chunk 中完成)
本文档的 setup chunk(隐藏运行)包含如下逻辑:
library(reticulate) |
2.3 安装 Python 包(仅首次)
本课程需要以下 Python 包:
- xarray:读取 NetCDF 格式的气象数据
- rasterio:栅格数据的裁剪、掩膜操作
- geopandas:矢量地理数据处理
- shapely:几何对象操作
- numpy / pandas:数值计算和表格处理
- matplotlib:地图绘制
- scipy:栅格降采样(zoom 函数)
- netCDF4:NetCDF 文件读取后端
py_pkgs <- c( |
2.4 验证激活状态
py_config() |
2.5 查看已安装的包
pkgs <- py_list_packages(".venv") |
2.6 虚拟环境管理常用命令
# 查看所有已创建的虚拟环境 |
三、全局配置与数据读取
3.1 全局参数配置
import os |
3.2 中文字体设置
def setup_chinese_font(): |
3.3 读取降水量栅格数据
降水量数据为 NetCDF 格式,包含 12 个月的月均降水量。我们需要计算年均值。
R 代码等价操作:
rast("pre_2021.nc") -> rst; app(rst, mean, na.rm=T) -> rst
from rasterio.transform import from_bounds |
3.4 裁剪到中国范围
R 代码等价操作:
st_union(prov) |> nngeo::st_remove_holes() -> cn; terra::crop(vect(cn)) |> terra::mask(vect(cn))
from rasterio.mask import mask as rasterio_mask |
四、分区域提取均值(各省降水量)
R 代码等价操作:
terra::extract(rst, vect(prov), fun=mean, na.rm=T)
此处代码需下载讲义材料查看~
年均降水量直方图
# 等价于 R 的 hist(provmap2$年度降水量_mm) |
![]()
五、读取地图数据
# 读取小地图版本的中国省级地图 |
六、绘制填充地图(pic1)
各省份年均降水量填充地图,等价于 R 版本的 pic1.png。
6.1 合并省级数据与地图
# 找合并键 |
6.2 数据分组
# 等价于 R 的 cut(年度降水量_mm, breaks=seq(-400,2400,by=400)) |
6.3 绘图
此处代码需下载讲义材料查看~
![]()
七、栅格地图 + 等降水量线
7.1 将栅格重投影到 Albers(关键步骤)
原始栅格是 WGS84 坐标,绘制等降水量线时需要与地图投影(Albers)一致。正确做法是先把栅格重投影到 Albers,然后直接在地图坐标系的 axes 上调用 ax.contour 绘制等值线,无需生成 GeoDataFrame 中转。
为什么之前的做法(WGS84 → GeoDataFrame → 转 Albers → plot)效果不好?
裁剪后的栅格边界有大量 NaN,contour会把连续等值线切成成千上万条碎片线段,这些碎片拼接后虽然位置正确,但密集的重叠会让线条看起来很粗且凌乱。ax.clabel也无法正确找到适合放标签的位置。
直接在重投影栅格上调用contour,则等值线是连续的,标签也能自动沿线排布。
等价于 R 中对 raster 对象做 projectRaster 再 rasterToContour。
from rasterio.warp import calculate_default_transform, reproject, Resampling |
7.2 栅格数据转多边形(降采样)
等价于 R 的 rst |> aggregate(fact=10) |> rasterToPolygons() |> st_as_sf()
from scipy.ndimage import zoom |
7.3 分离主图和小地图区域
等价于 R 中用 st_intersection 分离南海小地图和主图的操作:
# 小地图内的多边形 |
7.4 移动小地图到恰当位置
等价于 R 的 mutate(geometry = geometry * 0.5 + c(2100000, 1665139))
from shapely.affinity import scale as affine_scale, translate |
7.5 绘制栅格地图 + 等降水量线(pic2)
等降水量线直接在 Albers 坐标系的 axes 上调用 ax.contour 绘制,与地图坐标系完全对齐。
这等价于 R 的 geom_sf(data=line, color="white", linewidth=0.5)。
此处代码需下载讲义材料查看~
![]()
这样我们就把等降水量线添加到了地图上。
八、添加等降水量线文字标签(pic3)
R 版本使用 geomtextpath::geom_textsf 在线上自动添加文字标签。
Python 中使用 ax.clabel 实现相同效果——它会在等值线的合适位置插入标签,并将线切断,防止文字与线条重叠。
# 等价于 R 的: |
![]()
九、直接运行 Python 脚本
除了在 Rmd 文档中逐步演示,也可以直接运行 main.py 脚本一次性生成所有地图:
# 在虚拟环境中运行 main.py |
或者在终端中:
# 激活虚拟环境后运行 |
附录:R 与 Python 函数对照表
| R 代码 | Python 等价 | 说明 |
|---|---|---|
rast("pre_2021.nc") |
xr.open_dataset(nc_path) |
读取 NetCDF 栅格 |
app(rst, mean, na.rm=T) |
.mean(dim=..., skipna=True) |
计算多波段均值 |
st_union(prov) |
unary_union(prov.geometry) |
合并多边形 |
nngeo::st_remove_holes() |
Polygon(geom.exterior) |
去除多边形孔洞 |
terra::crop(vect(cn)) |
rasterio.mask.mask(crop=True) |
裁剪栅格 |
terra::mask(vect(cn)) |
rasterio.mask.mask(all_touched=True) |
掩膜裁剪 |
terra::extract(fun=mean) |
geometry_mask + np.nanmean |
区域均值提取 |
rasterToContour |
ax.contour(直接在 Albers axes 上) |
生成等值线 |
aggregate(fact=10) |
scipy.ndimage.zoom |
降采样 |
rasterToPolygons |
shapely.geometry.box 逐像元 |
栅格转多边形 |
st_transform(mycrs) |
.to_crs(MYCRS) |
坐标系转换 |
st_intersection |
gpd.overlay(how="intersection") |
空间相交 |
geometry * 0.5 + c(tx, ty) |
affine_scale + translate |
几何变换 |
geom_textsf |
ax.clabel(inline=True, fmt=...) |
等值线文字标签 |
annotation_scale |
手动绘制矩形比例尺 | 比例尺 |
annotation_north_arrow |
ax.annotate + ax.text |
指北针 |
scale_fill_gradientn |
LinearSegmentedColormap.from_list |
渐变色板 |
ggsave(dpi=400) |
fig.savefig(dpi=400) |
保存高分辨率图 |
# 为生成的图片添加水印(如需要) |
点击这里跳转到 RStata 短书平台获取附件:使用 Python 绘制填充地图、栅格地图 + 等降水量线
评论