在气象业务和研究中,把雷达基数据"画成一张好看的图"是基本功。传统上,很多人依赖 pycinrad 接口,但这些工具往往黑盒化程度较高——你很难精细控制色标、地图投影、边界线样式等细节。
本文走另一条路:手动提取数组,用matplotlib+cartopy从零画图。这样做的好处是:
目标:用
cinrad(PyCINRAD)读取中国天气雷达基数据,提取反射率,并使用matplotlib+cartopy绘制带地图底图的 PPI 图像。 技术栈:cinrad+xarray+numpy+matplotlib+cartopy+meteva配套数据:Z_RADR_I_Z9290_20230728070005_O_DOR_CB_CAP_FMT.bin.bz2(西安雷达站,SA 波段)关于包名:社区常说的 "pycinrad" 对应的 PyPI 安装包名为
cinrad(pip install cinrad)。本文统一使用import cinrad。
pip install matplotlib cartopy numpy xarray
中国雷达数据读取(PyCINRAD)pip install cinrad
中国地图底图(三选一即可)pip install frykit # 方式一: frykit,轻量且美观 pip install cnmaps # 方式二: cnmaps,支持裁剪和精细化地图 pip install meteva # 方式三: meteva,气象专用工具集自带地图
提示:
cartopy依赖 GEOS 和 PROJ,Linux 用户建议先通过 conda 安装以避免编译问题:conda install -c conda-forge cartopy。
!pip install cinrad --upgrade -i https://pypi.mirrors.ustc.edu.cn/simple/
Looking in indexes: https://pypi.mirrors.ustc.edu.cn/simple/
Requirement already satisfied: cinrad in /opt/conda/lib/python3.11/site-packages (1.9.3)
Requirement already satisfied: metpy>=0.8 in /opt/conda/lib/python3.11/site-packages (from cinrad) (1.6.3)
Requirement already satisfied: cartopy>=0.15 in /opt/conda/lib/python3.11/site-packages (from cinrad) (0.24.0)
Requirement already satisfied: pyshp!=2.0.0,!=2.0.1 in /opt/conda/lib/python3.11/site-packages (from cinrad) (2.3.1)
Requirement already satisfied: matplotlib>=2.2 in /opt/conda/lib/python3.11/site-packages (from cinrad) (3.9.1)
Requirement already satisfied: vanadis in /opt/conda/lib/python3.11/site-packages (from cinrad) (0.0.2)
Requirement already satisfied: cinrad-data>=0.1 in /opt/conda/lib/python3.11/site-packages (from cinrad) (0.1)
Requirement already satisfied: numpy>=1.23 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.15->cinrad) (1.26.4)
Requirement already satisfied: shapely>=1.8 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.15->cinrad) (2.1.2)
Requirement already satisfied: packaging>=21 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.15->cinrad) (24.1)
Requirement already satisfied: pyproj>=3.3.1 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.15->cinrad) (3.5.0)
Requirement already satisfied: contourpy>=1.0.1 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=2.2->cinrad) (1.3.1)
Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=2.2->cinrad) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=2.2->cinrad) (4.55.3)
Requirement already satisfied: kiwisolver>=1.3.1 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=2.2->cinrad) (1.4.7)
Requirement already satisfied: pillow>=8 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=2.2->cinrad) (9.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=2.2->cinrad) (3.2.0)
Requirement already satisfied: python-dateutil>=2.7 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=2.2->cinrad) (2.9.0.post0)
Requirement already satisfied: pandas>=1.4.0 in /opt/conda/lib/python3.11/site-packages (from metpy>=0.8->cinrad) (2.2.3)
Requirement already satisfied: pint>=0.17 in /opt/conda/lib/python3.11/site-packages (from metpy>=0.8->cinrad) (0.24.4)
Requirement already satisfied: pooch>=1.2.0 in /opt/conda/lib/python3.11/site-packages (from metpy>=0.8->cinrad) (1.8.2)
Requirement already satisfied: scipy>=1.8.0 in /opt/conda/lib/python3.11/site-packages (from metpy>=0.8->cinrad) (1.14.1)
Requirement already satisfied: traitlets>=5.0.5 in /opt/conda/lib/python3.11/site-packages (from metpy>=0.8->cinrad) (5.14.3)
Requirement already satisfied: xarray>=0.21.0 in /opt/conda/lib/python3.11/site-packages (from metpy>=0.8->cinrad) (2024.3.0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.11/site-packages (from pandas>=1.4.0->metpy>=0.8->cinrad) (2024.1)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.11/site-packages (from pandas>=1.4.0->metpy>=0.8->cinrad) (2024.2)
Requirement already satisfied: platformdirs>=2.1.0 in /opt/conda/lib/python3.11/site-packages (from pint>=0.17->metpy>=0.8->cinrad) (4.3.6)
Requirement already satisfied: typing_extensions>=4.0.0 in /opt/conda/lib/python3.11/site-packages (from pint>=0.17->metpy>=0.8->cinrad) (4.15.0)
Requirement already satisfied: flexcache>=0.3 in /opt/conda/lib/python3.11/site-packages (from pint>=0.17->metpy>=0.8->cinrad) (0.3)
Requirement already satisfied: flexparser>=0.4 in /opt/conda/lib/python3.11/site-packages (from pint>=0.17->metpy>=0.8->cinrad) (0.4)
Requirement already satisfied: requests>=2.19.0 in /opt/conda/lib/python3.11/site-packages (from pooch>=1.2.0->metpy>=0.8->cinrad) (2.32.3)
Requirement already satisfied: certifi in /opt/conda/lib/python3.11/site-packages (from pyproj>=3.3.1->cartopy>=0.15->cinrad) (2024.12.14)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.11/site-packages (from python-dateutil>=2.7->matplotlib>=2.2->cinrad) (1.17.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /opt/conda/lib/python3.11/site-packages (from requests>=2.19.0->pooch>=1.2.0->metpy>=0.8->cinrad) (3.4.0)
Requirement already satisfied: idna<4,>=2.5 in /opt/conda/lib/python3.11/site-packages (from requests>=2.19.0->pooch>=1.2.0->metpy>=0.8->cinrad) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/conda/lib/python3.11/site-packages (from requests>=2.19.0->pooch>=1.2.0->metpy>=0.8->cinrad) (2.2.3)
import cinrad
import cartopy
import matplotlib
import numpy as np
import xarray as xr
print("cinrad version:", cinrad.__version__)
print("cartopy version:", cartopy.__version__)
print("matplotlib version:", matplotlib.__version__)
print("numpy version:", np.__version__)
print("xarray version:", xr.__version__)
print("\n✅ 核心依赖导入成功")
cinrad version: 1.9.3
cartopy version: 0.24.0
matplotlib version: 3.9.1
numpy version: 1.26.4
xarray version: 2024.3.0
✅ 核心依赖导入成功
cinrad 提供了 read_auto() 函数,可以自动识别 SA/SB/CD/SC 等中国雷达格式:
from cinrad.io import read_auto
# 数据文件路径(请根据实际情况修改)
data_file = r'/home/mw/input/pycwr5461/Z_RADR_I_Z9898_20190828181529_O_DOR_SAD_CAP_FMT (1).bin.bz2'
radar = read_auto(data_file)
print("雷达站名:", radar.name, radar.code)
print("雷达海拔:", radar.radarheight, "m")
print("扫描时间:", radar.scantime)
print("扫描模式:", radar.scan_type)
雷达站名: haikou/"9yz Z9898
雷达海拔: 118 m
扫描时间: 2019-08-28 18:15:29+00:00
扫描模式: PPI
# 查看 REF(反射率)在哪些仰角可用
tilts = radar.available_tilt("REF")
print("REF 可用仰角索引:", tilts)
# 查看某个仰角下有哪些产品
products = radar.available_product(0)
print("仰角 0 可用产品:", products)
REF 可用仰角索引: [0, 2, 4, 5, 6, 7, 8, 9, 10]
仰角 0 可用产品: ['TREF', 'REF', 'SQI', 'ZDR', 'RHO', 'PHI', 'KDP', 'SNRH']
get_data() 是数据提取的核心接口,需要三个参数:
参数 | 含义 | 说明 |
|---|---|---|
tilt | 仰角索引 | 从 available_tilt() 中获取的整数,0 通常是最低仰角 |
drange | 显示半径 | 单位 km,常见取值 150、230、460。数据会重采样到对应距离圈 |
dtype | 产品类型 | "REF"(反射率)、"VEL"(径向速度)、"SW"(谱宽)等 |
返回的是一个 xarray.Dataset,里面已经帮你算好了每个像素对应的经纬度。
# 提取仰角 0、范围 230 km 的反射率
ds = radar.get_data(tilt=0, drange=230, dtype="REF")
print("Dataset 结构:")
print(ds)
print("\n数据变量:", list(ds.data_vars))
print("坐标:", list(ds.coords))
Dataset 结构:
<xarray.Dataset> Size: 11MB
Dimensions: (azimuth: 366, distance: 920)
Coordinates:
* azimuth (azimuth) float32 1kB 4.689 4.707 4.724 ... 4.658 4.675 4.692
* distance (distance) float64 7kB 0.25 0.5 0.75 1.0 ... 229.5 229.8 230.0
Data variables:
REF (azimuth, distance) float64 3MB nan nan nan nan ... nan nan nan
longitude (azimuth, distance) float64 3MB 110.2 110.2 110.2 ... 108.0 108.0
latitude (azimuth, distance) float64 3MB 20.0 20.0 20.0 ... 19.95 19.95
height (azimuth, distance) float64 3MB 0.1201 0.1222 ... 5.161 5.17
Attributes:
elevation: 0.48339844
range: 230
scan_time: 2019-08-28 18:15:29
site_code: Z9898
site_name: haikou
tangential_reso: 0.25
nyquist_vel: 8.83834
task: VCP21D
数据变量: ['REF', 'longitude', 'latitude', 'height']
坐标: ['azimuth', 'distance']
关键便利:
cinrad已经自动将极坐标(方位角-距离)投影到了地理坐标(经度-纬度),省去了我们手动做坐标转换的麻烦。
我们使用 pcolormesh 直接利用 cinrad 算好的地理坐标绘制 PPI,这是最保真的方式。
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
# 提取数组
ref = ds["REF"].values # (azimuth, distance)
lon = ds["longitude"].values # (azimuth, distance)
lat = ds["latitude"].values # (azimuth, distance)
print(f"数据形状: REF={ref.shape}, lon={lon.shape}, lat={lat.shape}")
# 创建图形
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
# 绘制反射率
cf = ax.pcolormesh(
lon, lat, ref,
cmap="jet",
vmin=-10,
vmax=60,
transform=ccrs.PlateCarree(),
shading="auto"
)
# 色标
cbar = plt.colorbar(cf, ax=ax, shrink=0.8, pad=0.02)
cbar.set_label("Reflectivity (dBZ)", fontsize=12)
# 设置显示范围(以雷达站为中心)
ax.set_extent([105, 115, 15, 25], crs=ccrs.PlateCarree())
# 添加基础地图要素
ax.add_feature(cfeature.COASTLINE, linewidth=0.6)
ax.add_feature(cfeature.BORDERS, linewidth=0.6)
# 标题
ax.set_title(
f" 0.5° PPI 反射率\n{radar.scantime} UTC",
fontsize=14,
pad=15
)
plt.tight_layout()
plt.show()
数据形状: REF=(366, 920), lon=(366, 920), lat=(366, 920)

output
在气象绘图中,SHP 文件管理麻烦、路径硬编码、跨平台易出错。以下三种方案无需自备 SHP,一行代码添加中国省界/国界。
pip install meteva
meteva 是中央气象台验证过的工具集,其地图数据与 MICAPS 风格一致。
from meteva.base.tool.plot_tools import add_china_map_2basemap
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent([105, 115, 15, 25], crs=ccrs.PlateCarree())
# 绘制反射率
cf = ax.pcolormesh(
lon, lat, ref,
cmap="jet", vmin=-10, vmax=60,
transform=ccrs.PlateCarree(), shading="auto"
)
plt.colorbar(cf, ax=ax, shrink=0.6, pad=0.02, label="Reflectivity (dBZ)")
# 添加中国省界/国界
add_china_map_2basemap(ax, name="province", edgecolor="black", lw=0.6, encoding="gbk")
add_china_map_2basemap(ax, name="nation", edgecolor="black", lw=1.0, encoding="gbk")
ax.set_title(f"meteva 底图 - Z9290 反射率\n{radar.scantime} UTC", fontsize=14)
plt.tight_layout()
plt.show()

output
pip install frykit
frykit 内置了中国省界、河流、九段线等数据,风格清爽,适合业务快报。
!pip install frykit -i https://pypi.mirrors.ustc.edu.cn/simple/
!pip install frykit-data -i https://pypi.mirrors.ustc.edu.cn/simple/
Looking in indexes: https://pypi.mirrors.ustc.edu.cn/simple/
Requirement already satisfied: frykit in /opt/conda/lib/python3.11/site-packages (0.8.1)
Requirement already satisfied: typing-extensions>=4.13.0 in /opt/conda/lib/python3.11/site-packages (from frykit) (4.15.0)
Requirement already satisfied: pandas>=2.0.0 in /opt/conda/lib/python3.11/site-packages (from frykit) (2.2.3)
Requirement already satisfied: shapely>=2.0.0 in /opt/conda/lib/python3.11/site-packages (from frykit) (2.1.2)
Requirement already satisfied: cartopy>=0.22.0 in /opt/conda/lib/python3.11/site-packages (from frykit) (0.24.0)
Requirement already satisfied: numpy>=1.23 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.22.0->frykit) (1.26.4)
Requirement already satisfied: matplotlib>=3.6 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.22.0->frykit) (3.9.1)
Requirement already satisfied: packaging>=21 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.22.0->frykit) (24.1)
Requirement already satisfied: pyshp>=2.3 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.22.0->frykit) (2.3.1)
Requirement already satisfied: pyproj>=3.3.1 in /opt/conda/lib/python3.11/site-packages (from cartopy>=0.22.0->frykit) (3.5.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.11/site-packages (from pandas>=2.0.0->frykit) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.11/site-packages (from pandas>=2.0.0->frykit) (2024.1)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.11/site-packages (from pandas>=2.0.0->frykit) (2024.2)
Requirement already satisfied: contourpy>=1.0.1 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=3.6->cartopy>=0.22.0->frykit) (1.3.1)
Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=3.6->cartopy>=0.22.0->frykit) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=3.6->cartopy>=0.22.0->frykit) (4.55.3)
Requirement already satisfied: kiwisolver>=1.3.1 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=3.6->cartopy>=0.22.0->frykit) (1.4.7)
Requirement already satisfied: pillow>=8 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=3.6->cartopy>=0.22.0->frykit) (9.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in /opt/conda/lib/python3.11/site-packages (from matplotlib>=3.6->cartopy>=0.22.0->frykit) (3.2.0)
Requirement already satisfied: certifi in /opt/conda/lib/python3.11/site-packages (from pyproj>=3.3.1->cartopy>=0.22.0->frykit) (2024.12.14)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas>=2.0.0->frykit) (1.17.0)
Looking in indexes: https://pypi.mirrors.ustc.edu.cn/simple/
Collecting frykit-data
Downloading https://mirrors.ustc.edu.cn/pypi/packages/6d/b7/b33034420b32338474005f19a4cb32325032b15a9e0f4643cba209c417c4/frykit_data-0.1.0-py3-none-any.whl (24.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 24.6/24.6 MB 38.9 MB/s eta 0:00:0000:0100:01
[?25hInstalling collected packages: frykit-data
Successfully installed frykit-data-0.1.0
import frykit.plot as fplt
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent([105, 115, 15, 25], crs=ccrs.PlateCarree())
cf = ax.pcolormesh(
lon, lat, ref,
cmap="jet", vmin=-10, vmax=60,
transform=ccrs.PlateCarree(), shading="auto"
)
plt.colorbar(cf, ax=ax, shrink=0.6, pad=0.02, label="Reflectivity (dBZ)")
# 添加中国省界
fplt.add_cn_province(ax, linewidth=0.8, edgecolor="black")
ax.set_title(f"frykit 底图 - Z9290 反射率\n{radar.scantime} UTC", fontsize=14)
plt.tight_layout()
plt.show()

output
pip install cnmaps
cnmaps 的强大之处在于支持按行政区划裁剪等值线,做省级精细化预报图时非常方便。
try:
from cnmaps import get_adm_maps, draw_maps
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent([105, 115, 15, 25], crs=ccrs.PlateCarree())
cf = ax.pcolormesh(
lon, lat, ref,
cmap="jet", vmin=-10, vmax=60,
transform=ccrs.PlateCarree(), shading="auto"
)
plt.colorbar(cf, ax=ax, shrink=0.6, pad=0.02, label="Reflectivity (dBZ)")
# 绘制省界和国界
draw_maps(get_adm_maps(level="省"), linewidth=0.8, color="k")
draw_maps(get_adm_maps(level="国"), linewidth=1.0, color="k")
ax.set_title(f"cnmaps 底图 - Z9290 反射率\n{radar.scantime} UTC", fontsize=14)
plt.tight_layout()
plt.show()
except ImportError:
print("cnmaps 未安装,跳过此单元。安装命令: pip install cnmaps")

output
以下代码综合了上述所有技巧,直接复制即可出一张可直接放入报告或论文的图:
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
# 创建画布
fig = plt.figure(figsize=(12, 10), dpi=120)
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
# 绘制反射率
cf = ax.pcolormesh(
lon, lat, ref,
cmap="jet",
vmin=-10,
vmax=65,
transform=ccrs.PlateCarree(),
shading="auto"
)
# 色标
cbar = fig.colorbar(cf, ax=ax, shrink=0.6, pad=0.02, aspect=30)
cbar.set_label("dBZ", fontsize=12)
cbar.ax.tick_params(labelsize=10)
# 添加地图要素
ax.add_feature(cfeature.COASTLINE.with_scale("50m"), linewidth=0.6, edgecolor="gray")
ax.add_feature(cfeature.LAKES, alpha=0.5)
ax.add_feature(cfeature.RIVERS, alpha=0.5)
add_china_map_2basemap(ax, name="province", edgecolor="black", lw=0.6, encoding="gbk")
add_china_map_2basemap(ax, name="nation", edgecolor="black", lw=1.0, encoding="gbk")
# 经纬网格线
gl = ax.gridlines(
crs=ccrs.PlateCarree(),
draw_labels=True,
linewidth=0.8,
color="gray",
alpha=0.5,
linestyle="--",
)
gl.top_labels = False
gl.right_labels = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
gl.xlabel_style = {"size": 10}
gl.ylabel_style = {"size": 10}
# 范围与标题
ax.set_extent([105, 115, 15, 25], crs=ccrs.PlateCarree())
ax.set_title(
f"CINRAD SA 雷达反射率) | 仰角:0.5° | 范围:230 km",
loc="left",
fontsize=13,
fontweight="bold"
)
ax.set_title(
f"{radar.scantime} UTC",
loc="right",
fontsize=11,
color="dimgray"
)
# 在雷达站位置打星标
ax.plot(
radar.stationlon, radar.stationlat,
marker="*", color="red", markersize=12,
transform=ccrs.PlateCarree(), label="Radar Site"
)
ax.legend(loc="lower left")
plt.tight_layout()
plt.show()

output
get_data() 报错 ValueError: invalid drange不同雷达的基数据预留给不同产品的距离库数不同。若 460 km 报错,尝试 230 km 或 150 km。你也可以先用 radar.get_range_safe() 查看该仰角下实际的最大不模糊距离。
这是正常的——原始基数据可能因 RDA(雷达数据采集单元)故障、波束遮挡或模式切换导致部分方位角缺失。pcolormesh 会如实反映这一点。如果你需要补齐为完整圆盘,可用 np.full + 角度索引手动填充,但业务上建议保留原始缺口以示诚实。
cartopy 下载 Natural Earth 数据很慢 / 失败?cartopy 默认从互联网下载地图数据。离线环境可手动下载 shapefile 放到 ~/.local/share/cartopy/shapefiles/ 下,或通过 cartopy.config["data_dir"] 指定本地路径。
本文完整演示了从读取中国雷达基数据到出版级可视化的全流程:
步骤 | 关键函数 / 工具 | 要点 |
|---|---|---|
读取数据 | cinrad.io.read_auto() | 自动识别 SA/SB/CD 等格式 |
提取产品 | radar.get_data(tilt, drange, dtype) | 返回带经纬度坐标的 xarray.Dataset |
绘制回波 | ax.pcolormesh(lon, lat, ref, ...) | 直接利用 cinrad 算好的地理坐标 |
地图底图 | frykit / cnmaps / meteva | 无需自备 SHP,一行代码添加中国省界 |
美化出图 | gridlines、colorbar、set_extent | 控制到像素级的出版标准 |
核心带走:cinrad 不仅帮你读了雷达,还帮你把极坐标转成了经纬度。你只需要把它当成普通的二维场数据,用 pcolormesh 往 cartopy 底图上一贴,就是一张专业雷达图。