Python+Cartopy实战:5步搞定WRF风场数据可视化(附完整代码)
Python+Cartopy进阶实战:从WRF风场数据到专业气象图表的全流程解析
如果你刚接触气象数据处理,面对WRF模式输出的NetCDF文件,可能会觉得无从下手。那些复杂的网格数据、多维变量和坐标系统,常常让人望而却步。但别担心,今天我将带你深入探索如何用Python和Cartopy,将原始的WRF风场数据转化为直观、专业的可视化图表。这不仅仅是简单的绘图,而是一套从数据理解、处理到最终呈现的完整工作流。
我在实际项目中发现,很多初学者卡在几个关键环节:如何正确提取特定气压层的风场?如何处理地图投影和坐标转换?怎样设置中文标签才不出现乱码?以及如何让箭头图(风羽图)既美观又信息丰富。这篇文章将逐一拆解这些难题,并提供可直接复用的代码模块。无论你是撰写科研论文的研究生,还是进行业务气象分析的专业人员,这套方法都能显著提升你的工作效率。
1. 理解WRF数据:不仅仅是打开一个文件
在动手写代码之前,我们需要先理解WRF输出数据的结构。WRF模式通常输出NetCDF格式的文件,这是一种自描述的科学数据格式。一个典型的wrfout文件包含了数十个变量,从温度、气压到风速、湿度,数据维度通常包括时间、垂直层、南北网格点和东西网格点。
1.1 WRF数据的关键维度与变量
打开一个WRF输出文件,你会看到类似这样的数据结构:
from netCDF4 import Dataset
import numpy as np
# 打开WRF输出文件
file_path = "wrfout_d02_2023-06-01_18_00_00.nc"
wrf_data = Dataset(file_path)
# 查看文件结构
print("文件变量列表:")
for var in wrf_data.variables:
print(f" {var}: {wrf_data.variables[var].shape}")
运行这段代码,你会看到类似下面的输出(具体变量名可能因WRF版本和配置而异):
文件变量列表:
Times: (1, 19)
XLAT: (1, 201, 201)
XLONG: (1, 201, 201)
U: (1, 30, 201, 201) # 东西风分量
V: (1, 30, 201, 201) # 南北风分量
W: (1, 31, 201, 201) # 垂直风分量
T: (1, 30, 201, 201) # 温度
P: (1, 30, 201, 201) # 气压
PB: (1, 30, 201, 201) # 基础气压
...
这里有几个关键点需要注意:
- 时间维度:WRF输出通常包含多个时间步长,第一个维度通常是时间
- 垂直层:第二个维度是eta层(地形追随坐标),不是标准的气压层
- 网格坐标:XLAT和XLONG是网格点的经纬度坐标,但注意这是网格坐标而非地理坐标
1.2 从eta层到气压层的转换
WRF在垂直方向上使用地形追随坐标(eta坐标),而我们在分析时通常需要标准气压层的数据。这就是为什么需要插值操作。wrf-python库提供了interplevel函数来处理这个转换:
from wrf import getvar, interplevel, to_np, latlon_coords
# 获取气压和风场数据
pressure = getvar(wrf_data, "pressure") # 全气压场
u_wind = getvar(wrf_data, "ua", units="m s-1") # 东西风分量
v_wind = getvar(wrf_data, "va", units="m s-1") # 南北风分量
# 获取经纬度坐标
lats, lons = latlon_coords(pressure)
# 插值到500hPa等压面
target_pressure = 500.0 # 目标气压层(hPa)
u_500 = interplevel(u_wind, pressure, target_pressure)
v_500 = interplevel(v_wind, pressure, target_pressure)
注意:
getvar函数是wrf-python库的核心,它能智能地处理WRF数据的单位转换和坐标提取。使用units参数可以确保我们得到正确单位的变量。
1.3 数据质量控制与预处理
在实际应用中,原始数据可能包含缺失值或异常值。我建议在可视化前进行基本的数据检查:
def check_wrf_data(u_data, v_data, lats, lons):
"""检查WRF风场数据的质量"""
print(f"U分量形状: {u_data.shape}")
print(f"V分量形状: {v_data.shape}")
print(f"纬度范围: [{lats.min():.2f}, {lats.max():.2f}]")
print(f"经度范围: [{lons.min():.2f}, {lons.max():.2f}]")
# 检查缺失值
u_nan_count = np.isnan(u_data).sum()
v_nan_count = np.isnan(v_data).sum()
print(f"U分量缺失值数量: {u_nan_count}")
print(f"V分量缺失值数量: {v_nan_count}")
# 检查物理合理性
u_range = (u_data.min(), u_data.max())
v_range = (v_data.min(), v_data.max())
print(f"U分量范围: {u_range} m/s")
print(f"V分量范围: {v_range} m/s")
# 计算风速
wind_speed = np.sqrt(u_data**2 + v_data**2)
print(f"风速范围: [{wind_speed.min():.2f}, {wind_speed.max():.2f}] m/s")
return wind_speed
# 执行检查
wind_speed_500 = check_wrf_data(u_500, v_500, lats, lons)
这个检查步骤能帮你快速识别数据问题,比如异常的大值或缺失值,避免在后续可视化中出现意外结果。
2. Cartopy地图基础:构建专业气象底图
Cartopy是Python中最强大的地理数据可视化库之一,它基于PROJ和matplotlib构建,支持多种地图投影和地理特征。对于WRF数据可视化,选择合适的投影至关重要。
2.1 投影选择与地图初始化
WRF数据通常使用经纬度坐标,但根据研究区域的不同,我们可能需要不同的地图投影。以下是一些常见选择:
| 投影类型 | 适用场景 | 特点 |
|---|---|---|
| PlateCarree | 全球或大区域 | 等距圆柱投影,简单但变形大 |
| LambertConformal | 中纬度地区 | 保形投影,适合中尺度模拟 |
| Mercator | 低纬度地区 | 等角投影,航海常用 |
| Orthographic | 半球视图 | 透视投影,适合展示全球环流 |
对于中国区域的风场分析,我通常使用PlateCarree或LambertConformal投影。下面是创建基础地图的代码:
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
def create_basemap(projection=ccrs.PlateCarree(), figsize=(12, 8),
extent=None, dpi=150):
"""创建基础地图"""
fig = plt.figure(figsize=figsize, dpi=dpi)
ax = fig.add_subplot(1, 1, 1, projection=projection)
# 设置显示范围(如果指定)
if extent is not None:
ax.set_extent(extent, crs=ccrs.PlateCarree())
# 添加地理特征
ax.add_feature(cfeature.COASTLINE.with_scale('50m'), linewidth=0.8)
ax.add_feature(cfeature.BORDERS.with_scale('50m'), linewidth=0.5, linestyle=':')
ax.add_feature(cfeature.LAKES.with_scale('50m'), alpha=0.5)
ax.add_feature(cfeature.RIVERS.with_scale('50m'))
# 添加陆地海洋颜色
ax.add_feature(cfeature.LAND, facecolor='lightgray', alpha=0.3)
ax.add_feature(cfeature.OCEAN, facecolor='lightblue', alpha=0.3)
return fig, ax
# 创建中国区域地图
china_extent = [70, 140, 15, 55] # [min_lon, max_lon, min_lat, max_lat]
fig, ax = create_basemap(extent=china_extent)
2.2 高级地理特征与自定义边界
对于更精细的区域分析,你可能需要添加省界、河流、湖泊等特征。Cartopy支持多种数据源,包括Natural Earth和本地Shapefile:
from cartopy.io.shapereader import Reader
import os
def add_custom_features(ax, shp_path=None):
"""添加自定义地理特征"""
# 添加标准特征
ax.add_feature(cfeature.OCEAN.with_scale('50m'), facecolor='#d0e7ff', alpha=0.8)
ax.add_feature(cfeature.LAND.with_scale('50m'), facecolor='#f5f5dc', alpha=0.5)
# 添加等高线(如果需要地形)
try:
ax.add_feature(cfeature.NaturalEarthFeature(
'physical', 'terrain', '50m',
edgecolor='none', facecolor='#d9d9d9', alpha=0.3
))
except:
print("注意:地形数据可能未下载,使用cartopy.feature.natural_earth.download()下载")
# 添加自定义Shapefile边界(如省界)
if shp_path and os.path.exists(shp_path):
try:
provinces = Reader(shp_path)
ax.add_geometries(
provinces.geometries(),
ccrs.PlateCarree(),
facecolor='none',
edgecolor='darkgray',
linewidth=0.6,
alpha=0.8
)
except Exception as e:
print(f"加载Shapefile失败: {e}")
return ax
# 使用示例
fig, ax = create_basemap(extent=[105, 125, 20, 45]) # 华东区域
ax = add_custom_features(ax, shp_path="path/to/china_provinces.shp")
2.3 网格线与坐标标注的专业设置
气象图表对坐标标注有特殊要求。我们需要显示经纬度网格,并正确格式化标签:
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import matplotlib.ticker as mticker
def add_professional_gridlines(ax, extent, grid_interval=2.0,
fontsize=10, line_style='--', alpha=0.5):
"""添加专业的气象网格线"""
# 创建网格线对象
gl = ax.gridlines(
crs=ccrs.PlateCarree(),
draw_labels=True,
linewidth=0.8,
color='gray',
alpha=alpha,
linestyle=line_style,
x_inline=False,
y_inline=False
)
# 设置哪些边显示标签
gl.top_labels = False
gl.right_labels = False
gl.left_labels = True
gl.bottom_labels = True
# 设置标签样式
gl.xlabel_style = {'size': fontsize, 'color': 'black'}
gl.ylabel_style = {'size': fontsize, 'color': 'black'}
# 设置网格间隔
gl.xlocator = mticker.FixedLocator(
np.arange(extent[0], extent[1] + grid_interval, grid_interval)
)
gl.ylocator = mticker.FixedLocator(
np.arange(extent[2], extent[3] + grid_interval, grid_interval)
)
# 设置格式化器
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
# 添加经纬度刻度
ax.set_xticks(
np.arange(extent[0], extent[1] + grid_interval, grid_interval),
crs=ccrs.PlateCarree()
)
ax.set_yticks(
np.arange(extent[2], extent[3] + grid_interval, grid_interval),
crs=ccrs.PlateCarree()
)
return gl
# 应用网格线
extent = [110, 120, 23, 31] # 示例范围
gl = add_professional_gridlines(ax, extent, grid_interval=2.0)
这个函数创建了标准的经纬度网格,标签只出现在左下方,避免了图表拥挤。grid_interval参数让你可以控制网格密度,对于不同尺度的区域可以灵活调整。
3. 风场可视化:从基础箭头到专业风羽
风场可视化有多种方式,最常用的是箭头图(quiver)和流线图(streamplot)。每种方法都有其适用场景。
3.1 基础箭头图的绘制与优化
箭头图是最直观的风场表示方法,但默认设置往往不够美观。以下是我优化后的箭头图函数:
def plot_wind_quiver(ax, lons, lats, u, v, scale=200, width=0.0035,
headwidth=3, headlength=4, headaxislength=3.5,
color='darkblue', alpha=0.8, key_length=20,
key_position=(0.05, 0.95), key_label='20 m/s',
subsample=5):
"""绘制优化后的风场箭头图"""
# 对数据进行下采样以提高可读性
if subsample > 1:
lons_s = lons[::subsample, ::subsample]
lats_s = lats[::subsample, ::subsample]
u_s = u[::subsample, ::subsample]
v_s = v[::subsample, ::subsample]
else:
lons_s, lats_s, u_s, v_s = lons, lats, u, v
# 计算风速用于颜色映射
wind_speed = np.sqrt(u_s**2 + v_s**2)
# 绘制彩色箭头
quiver = ax.quiver(
lons_s, lats_s, u_s, v_s,
wind_speed, # 用风速着色
cmap='viridis',
scale=scale,
width=width,
headwidth=headwidth,
headlength=headlength,
headaxislength=headaxislength,
alpha=alpha,
transform=ccrs.PlateCarree(),
pivot='middle', # 箭头中点位于数据点
angles='uv', # 使用UV分量计算角度
scale_units='inches'
)
# 添加图例(参考箭头)
qk = ax.quiverkey(
quiver,
key_position[0], key_position[1],
key_length,
key_label,
labelpos='E',
coordinates='axes',
fontproperties={'size': 10, 'weight': 'bold'},
color='black'
)
# 添加颜色条
cbar = plt.colorbar(quiver, ax=ax, orientation='vertical',
pad=0.05, shrink=0.8)
cbar.set_label('Wind Speed (m/s)', fontsize=11)
cbar.ax.tick_params(labelsize=9)
return quiver
# 使用示例
fig, ax = create_basemap(extent=[110, 120, 23, 31])
quiver = plot_wind_quiver(ax, lons, lats, u_500, v_500,
subsample=4, scale=250)
plt.title('500 hPa Wind Field', fontsize=14, fontweight='bold', pad=15)
提示:
subsample参数非常重要。WRF数据分辨率通常很高,直接绘制所有箭头会导致图表过于拥挤。我通常使用4-8的下采样因子,在保持风场特征的同时确保可读性。
3.2 流线图:展示风场整体结构
对于某些分析场景,流线图能更好地展示风场的整体结构和特征:
def plot_wind_streamplot(ax, lons, lats, u, v, density=2, color='black',
linewidth=1.2, arrowsize=1.5, arrowstyle='->'):
"""绘制风场流线图"""
# 创建流线图
stream = ax.streamplot(
lons, lats, u, v,
density=density,
color=color,
linewidth=linewidth,
arrowsize=arrowsize,
arrowstyle=arrowstyle,
transform=ccrs.PlateCarree()
)
return stream
# 结合箭头图和流线图
fig, ax = create_basemap(extent=[110, 120, 23, 31])
# 先绘制流线图展示整体结构
stream = plot_wind_streamplot(ax, lons, lats, u_500, v_500,
density=1.5, color='gray', linewidth=0.8)
# 再叠加箭头图显示局部细节
quiver = plot_wind_quiver(ax, lons, lats, u_500, v_500,
subsample=6, scale=300, width=0.003,
color='darkred', alpha=0.7)
plt.title('500 hPa Wind Field with Streamlines',
fontsize=14, fontweight='bold', pad=15)
这种组合方式既展示了风场的整体流向(通过流线),又提供了局部风速信息(通过箭头),特别适合分析天气系统结构。
3.3 风速填色图:识别急流和风切变
除了箭头和流线,风速的填色图能直观显示风速大小分布,对于识别急流等特征特别有用:
import matplotlib.colors as mcolors
def plot_wind_speed_contourf(ax, lons, lats, u, v, levels=20,
cmap='YlOrRd', extend='max'):
"""绘制风速填色图"""
# 计算风速
wind_speed = np.sqrt(u**2 + v**2)
# 创建自定义颜色映射
if cmap == 'custom_wind':
colors = ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1',
'#6baed6', '#4292c6', '#2171b5', '#08519c',
'#08306b']
cmap = mcolors.LinearSegmentedColormap.from_list('wind_cmap', colors)
# 绘制填色图
contourf = ax.contourf(
lons, lats, wind_speed,
levels=levels,
cmap=cmap,
extend=extend,
transform=ccrs.PlateCarree(),
alpha=0.7
)
# 添加等值线
contour = ax.contour(
lons, lats, wind_speed,
levels=levels[::2], # 每隔一条等值线
colors='black',
linewidths=0.5,
alpha=0.5,
transform=ccrs.PlateCarree()
)
# 添加等值线标签
ax.clabel(contour, inline=True, fontsize=8, fmt='%1.0f')
# 添加颜色条
cbar = plt.colorbar(contourf, ax=ax, orientation='vertical',
pad=0.05, shrink=0.8)
cbar.set_label('Wind Speed (m/s)', fontsize=11)
cbar.ax.tick_params(labelsize=9)
return contourf, contour
# 使用示例
fig, ax = create_basemap(extent=[110, 120, 23, 31])
# 绘制风速填色图
contourf, contour = plot_wind_speed_contourf(
ax, lons, lats, u_500, v_500,
levels=np.arange(0, 41, 2), # 0-40 m/s,间隔2 m/s
cmap='YlOrRd'
)
# 叠加箭头图
quiver = plot_wind_quiver(ax, lons, lats, u_500, v_500,
subsample=5, scale=350, width=0.0025,
color='black', alpha=0.9)
plt.title('500 hPa Wind Speed and Direction',
fontsize=14, fontweight='bold', pad=15)
这种多层叠加的图表能同时传达风速大小和风向信息,是气象分析中非常有效的可视化方式。
4. 高级技巧与实战问题解决
在实际项目中,你会遇到各种具体问题。这里分享几个我积累的实用技巧。
4.1 中文显示问题的彻底解决
Cartopy和matplotlib的中文显示问题困扰过很多人。以下是经过验证的完整解决方案:
import matplotlib.pyplot as plt
from matplotlib import rcParams
import os
def setup_chinese_font(font_path=None, font_size=12):
"""配置中文字体支持"""
# 方法1:使用系统字体(推荐)
if os.name == 'nt': # Windows
chinese_font = 'SimHei' # 黑体
english_font = 'Times New Roman'
elif os.name == 'posix': # Linux/Mac
chinese_font = 'DejaVu Sans' # 备选方案
english_font = 'DejaVu Serif'
# 尝试寻找中文字体
possible_fonts = ['WenQuanYi Micro Hei', 'Noto Sans CJK SC',
'SimHei', 'Microsoft YaHei']
for font in possible_fonts:
try:
plt.font_manager.FontProperties(fname=font)
chinese_font = font
break
except:
continue
else:
chinese_font = 'DejaVu Sans'
english_font = 'DejaVu Serif'
# 方法2:如果提供了字体文件路径
if font_path and os.path.exists(font_path):
import matplotlib.font_manager as fm
fm.fontManager.addfont(font_path)
font_name = fm.FontProperties(fname=font_path).get_name()
chinese_font = font_name
# 配置全局字体
rcParams.update({
'font.family': 'serif',
'font.serif': [english_font, chinese_font],
'font.sans-serif': [chinese_font, 'DejaVu Sans'],
'font.size': font_size,
'axes.unicode_minus': False, # 正确显示负号
'mathtext.fontset': 'stix', # 数学字体
})
print(f"中文字体设置: {chinese_font}")
print(f"英文字体设置: {english_font}")
return chinese_font, english_font
# 使用示例
chinese_font, english_font = setup_chinese_font(font_size=11)
# 测试中文显示
fig, ax = plt.subplots(figsize=(8, 6))
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_xlabel('经度 (°E)', fontproperties={'family': chinese_font, 'size': 12})
ax.set_ylabel('纬度 (°N)', fontproperties={'family': chinese_font, 'size': 12})
ax.set_title('风场可视化示例', fontproperties={'family': chinese_font, 'size': 14, 'weight': 'bold'})
plt.show()
这个函数自动检测操作系统并选择合适的字体,同时提供了手动指定字体文件的选项。关键是同时配置了serif和sans-serif字体族,确保中英文都能正确显示。
4.2 多子图与时间序列分析
在实际研究中,我们经常需要比较不同时间或不同高度的风场。多子图布局是解决这个需求的利器:
def create_multi_panel_wind_plot(wrf_files, pressure_levels, extent,
figsize=(16, 12), dpi=150):
"""创建多面板风场对比图"""
n_files = len(wrf_files)
n_levels = len(pressure_levels)
n_panels = n_files * n_levels
# 创建子图网格
fig, axes = plt.subplots(n_files, n_levels,
figsize=figsize, dpi=dpi,
subplot_kw={'projection': ccrs.PlateCarree()},
constrained_layout=True)
# 如果只有一行或一列,调整axes形状
if n_files == 1:
axes = axes.reshape(1, -1)
if n_levels == 1:
axes = axes.reshape(-1, 1)
# 遍历所有文件和气层
for i, wrf_file in enumerate(wrf_files):
wrf_data = Dataset(wrf_file)
for j, pressure_level in enumerate(pressure_levels):
ax = axes[i, j]
# 提取数据
pressure = getvar(wrf_data, "pressure")
u_wind = getvar(wrf_data, "ua", units="m s-1")
v_wind = getvar(wrf_data, "va", units="m s-1")
lats, lons = latlon_coords(pressure)
# 插值到目标气压层
u_interp = interplevel(u_wind, pressure, pressure_level)
v_interp = interplevel(v_wind, pressure, pressure_level)
# 设置地图范围
ax.set_extent(extent, crs=ccrs.PlateCarree())
# 添加地理特征
ax.add_feature(cfeature.COASTLINE.with_scale('50m'), linewidth=0.6)
ax.add_feature(cfeature.BORDERS.with_scale('50m'), linewidth=0.4, linestyle=':')
# 绘制风场
quiver = plot_wind_quiver(ax, lons, lats, u_interp, v_interp,
subsample=6, scale=300, width=0.003,
color='darkblue', alpha=0.7)
# 添加标题
time_str = wrf_data.variables['Times'][0].tostring().decode('utf-8')
title = f"{time_str}\n{pressure_level} hPa"
ax.set_title(title, fontsize=11, pad=10)
# 添加网格线(只在边缘子图)
if i == n_files - 1: # 最后一行
gl = ax.gridlines(draw_labels=True, linewidth=0.5, alpha=0.5)
gl.top_labels = False
gl.right_labels = False
else:
ax.gridlines(linewidth=0.5, alpha=0.5, draw_labels=False)
# 添加整体标题
fig.suptitle('WRF风场时间序列与垂直剖面',
fontsize=16, fontweight='bold', y=0.98)
return fig, axes
# 使用示例
wrf_files = [
"wrfout_d02_2023-06-01_00_00_00.nc",
"wrfout_d02_2023-06-01_06_00_00.nc",
"wrfout_d02_2023-06-01_12_00_00.nc",
"wrfout_d02_2023-06-01_18_00_00.nc"
]
pressure_levels = [850, 500, 250] # 850hPa, 500hPa, 250hPa
extent = [105, 125, 20, 45]
fig, axes = create_multi_panel_wind_plot(wrf_files, pressure_levels, extent)
plt.savefig('wind_field_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
这个函数创建了一个专业的多面板图表,可以同时比较不同时间和不同高度的风场。constrained_layout=True参数确保子图之间的间距合理,避免标签重叠。
4.3 性能优化:处理大型WRF数据集
WRF数据集可能非常大,特别是高分辨率或长时间序列的模拟。以下是一些性能优化技巧:
import xarray as xr
from dask.diagnostics import ProgressBar
def process_large_wrf_dataset(file_pattern, pressure_level,
time_indices=None, spatial_subsample=2):
"""高效处理大型WRF数据集"""
# 使用xarray打开多个文件
ds = xr.open_mfdataset(file_pattern, combine='by_coords',
parallel=True, chunks={'Time': 1})
# 选择时间子集
if time_indices is not None:
ds = ds.isel(Time=time_indices)
# 提取变量(延迟加载)
u = ds['U']
v = ds['V']
p = ds['P'] + ds['PB'] # 计算全气压
# 使用dask进行并行计算
with ProgressBar():
# 插值到目标气压层
u_interp = u.interp(pressure=pressure_level)
v_interp = v.interp(pressure=pressure_level)
# 计算风速
wind_speed = np.sqrt(u_interp**2 + v_interp**2)
# 触发实际计算
wind_speed_computed = wind_speed.compute()
# 空间下采样
if spatial_subsample > 1:
wind_speed_computed = wind_speed_computed[::spatial_subsample, ::spatial_subsample]
return wind_speed_computed
# 使用示例
file_pattern = "wrfout_d02_2023-06-*.nc" # 通配符匹配多个文件
pressure_level = 500.0 # hPa
time_indices = slice(0, 24, 6) # 每6个时间步取一个
print("开始处理大型WRF数据集...")
wind_speed_500 = process_large_wrf_dataset(
file_pattern, pressure_level,
time_indices=time_indices,
spatial_subsample=2
)
print(f"处理完成,数据形状: {wind_speed_500.shape}")
这个函数使用了xarray和dask库来处理大型数据集。关键优化包括:
- 延迟加载:直到需要时才实际读取数据
- 并行计算:利用多核CPU加速插值运算
- 分块处理:将大数据集分成小块处理,减少内存使用
- 进度显示:使用
ProgressBar显示处理进度
4.4 自动化报告生成
在实际业务中,我们经常需要生成包含多个图表的分析报告。以下是一个自动化报告生成的示例:
from datetime import datetime
import pandas as pd
def generate_wind_analysis_report(wrf_file, output_dir='./reports'):
"""生成完整的风场分析报告"""
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
# 读取数据
wrf_data = Dataset(wrf_file)
# 提取时间信息
time_str = wrf_data.variables['Times'][0].tostring().decode('utf-8')
report_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 定义要分析的气压层
pressure_levels = [1000, 850, 700, 500, 300, 200]
# 创建HTML报告
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>WRF风场分析报告 - {time_str}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; }}
.header {{ text-align: center; margin-bottom: 30px; }}
.section {{ margin-bottom: 40px; }}
.plot {{ text-align: center; margin: 20px 0; }}
.stats {{ background-color: #f5f5f5; padding: 15px; border-radius: 5px; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: center; }}
th {{ background-color: #4CAF50; color: white; }}
</style>
</head>
<body>
<div class="header">
<h1>WRF风场分析报告</h1>
<p>模拟时间: {time_str} | 生成时间: {report_time}</p>
</div>
"""
# 为每个气压层生成图表和统计
stats_data = []
for i, level in enumerate(pressure_levels):
# 提取数据
pressure = getvar(wrf_data, "pressure")
u_wind = getvar(wrf_data, "ua", units="m s-1")
v_wind = getvar(wrf_data, "va", units="m s-1")
lats, lons = latlon_coords(pressure)
# 插值
u_interp = interplevel(u_wind, pressure, level)
v_interp = interplevel(v_wind, pressure, level)
# 计算统计量
wind_speed = np.sqrt(u_interp**2 + v_interp**2)
stats = {
'气压层 (hPa)': level,
'平均风速 (m/s)': np.nanmean(wind_speed),
'最大风速 (m/s)': np.nanmax(wind_speed),
'最小风速 (m/s)': np.nanmin(wind_speed),
'标准差 (m/s)': np.nanstd(wind_speed)
}
stats_data.append(stats)
# 创建图表
fig, ax = create_basemap(extent=[105, 125, 20, 45], figsize=(10, 8))
# 绘制风速填色图
contourf, _ = plot_wind_speed_contourf(
ax, lons, lats, u_interp, v_interp,
levels=np.arange(0, 61, 5),
cmap='RdYlBu_r'
)
# 叠加箭头图
quiver = plot_wind_quiver(
ax, lons, lats, u_interp, v_interp,
subsample=5, scale=400, width=0.0025,
color='black', alpha=0.7
)
# 添加标题
ax.set_title(f'{level} hPa 风场', fontsize=14, fontweight='bold', pad=15)
# 保存图表
plot_filename = f'{output_dir}/wind_{level}hpa.png'
plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
plt.close(fig)
# 添加到HTML报告
html_content += f"""
<div class="section">
<h2>{level} hPa 风场分析</h2>
<div class="plot">
<img src="wind_{level}hpa.png" alt="{level}hPa Wind Field" style="width: 80%;">
</div>
<div class="stats">
<h3>统计摘要</h3>
<p>平均风速: {stats['平均风速 (m/s)']:.2f} m/s</p>
<p>最大风速: {stats['最大风速 (m/s)']:.2f} m/s</p>
<p>最小风速: {stats['最小风速 (m/s)']:.2f} m/s</p>
<p>风速标准差: {stats['标准差 (m/s)']:.2f} m/s</p>
</div>
</div>
"""
# 添加统计汇总表
df_stats = pd.DataFrame(stats_data)
html_content += """
<div class="section">
<h2>各气压层风速统计汇总</h2>
<table>
<tr>
<th>气压层 (hPa)</th>
<th>平均风速 (m/s)</th>
<th>最大风速 (m/s)</th>
<th>最小风速 (m/s)</th>
<th>标准差 (m/s)</th>
</tr>
"""
for _, row in df_stats.iterrows():
html_content += f"""
<tr>
<td>{row['气压层 (hPa)']}</td>
<td>{row['平均风速 (m/s)']:.2f}</td>
<td>{row['最大风速 (m/s)']:.2f}</td>
<td>{row['最小风速 (m/s)']:.2f}</td>
<td>{row['标准差 (m/s)']:.2f}</td>
</tr>
"""
html_content += """
</table>
</div>
</body>
</html>
"""
# 保存HTML报告
report_path = f'{output_dir}/wind_analysis_report.html'
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html_content)
# 保存统计数据为CSV
stats_path = f'{output_dir}/wind_statistics.csv'
df_stats.to_csv(stats_path, index=False, encoding='utf-8-sig')
print(f"报告已生成: {report_path}")
print(f"统计数据: {stats_path}")
return report_path, stats_path
# 使用示例
report_file, stats_file = generate_wind_analysis_report(
"wrfout_d02_2023-06-01_18_00_00.nc",
output_dir="./wind_analysis_report"
)
这个自动化报告生成函数创建了一个完整的HTML报告,包含:
- 各气压层的风场图表
- 详细的统计摘要
- 交互式数据表格
- 可下载的CSV格式统计数据
在实际业务中,你可以将这个函数集成到自动化工作流中,定期生成风场分析报告。
5. 完整实战案例:台风过程风场演变分析
让我们通过一个完整的案例,展示如何分析台风过程中的风场演变。这个案例结合了前面介绍的所有技术。
def analyze_typhoon_wind_evolution(wrf_files, typhoon_center, extent,
output_dir='./typhoon_analysis'):
"""分析台风过程风场演变"""
os.makedirs(output_dir, exist_ok=True)
# 创建多时间步长的风场动画
fig, axes = plt.subplots(2, 2, figsize=(16, 12),
subplot_kw={'projection': ccrs.PlateCarree()},
constrained_layout=True)
axes = axes.flatten()
# 分析每个时间步
for idx, wrf_file in enumerate(wrf_files[:4]): # 只分析前4个时次
wrf_data = Dataset(wrf_file)
# 提取850hPa风场(台风主要影响层次)
pressure = getvar(wrf_data, "pressure")
u_wind = getvar(wrf_data, "ua", units="m s-1")
v_wind = getvar(wrf_data, "va", units="m s-1")
lats, lons = latlon_coords(pressure)
u_850 = interplevel(u_wind, pressure, 850)
v_850 = interplevel(v_wind, pressure, 850)
# 计算相对涡度(台风强度指标)
from wrf import vorticity
vort = vorticity.get_uvmet(u_850, v_850, wrf_data)
ax = axes[idx]
ax.set_extent(extent, crs=ccrs.PlateCarree())
# 添加地理特征
ax.add_feature(cfeature.COASTLINE.with_scale('50m'), linewidth=0.8)
ax.add_feature(cfeature.BORDERS.with_scale('50m'), linewidth=0.5, linestyle=':')
# 绘制相对涡度填色
vort_contour = ax.contourf(
lons, lats, vort,
levels=np.linspace(-0.0005, 0.0005, 21),
cmap='RdBu_r',
extend='both',
transform=ccrs.PlateCarree(),
alpha=0.6
)
# 绘制风场箭头
quiver = ax.quiver(
lons[::6, ::6], lats[::6, ::6],
u_850[::6, ::6], v_850[::6, ::6],
scale=400,
width=0.003,
color='black',
transform=ccrs.PlateCarree()
)
# 标记台风中心
ax.plot(typhoon_center[idx][0], typhoon_center[idx][1],
'r*', markersize=15, transform=ccrs.PlateCarree(),
markeredgecolor='black', markeredgewidth=1)
# 添加时间标题
time_str = wrf_data.variables['Times'][0].tostring().decode('utf-8')
ax.set_title(f'Time: {time_str}\n850 hPa Wind & Vorticity',
fontsize=11, pad=10)
# 添加比例尺
if idx == 0:
ax.quiverkey(quiver, 0.05, 0.95, 20, '20 m/s',
labelpos='E', coordinates='axes')
# 添加整体标题
fig.suptitle('Typhoon Wind Field Evolution at 850 hPa',
fontsize=16, fontweight='bold', y=0.98)
# 添加颜色条
cbar_ax = fig.add_axes([0.15, 0.08, 0.7, 0.02])
cbar = fig.colorbar(vort_contour, cax=cbar_ax, orientation='horizontal')
cbar.set_label('Relative Vorticity (s$^{-1}$)', fontsize=11)
# 保存图表
output_path = f'{output_dir}/typhoon_evolution.png'
plt.savefig(output_path, dpi=300, bbox_inches='tight')
plt.close(fig)
print(f"台风分析图表已保存: {output_path}")
# 生成风速时间序列
wind_speeds = []
times = []
for wrf_file in wrf_files:
wrf_data = Dataset(wrf_file)
# 提取时间
time_str = wrf_data.variables['Times'][0].tostring().decode('utf-8')
times.append(time_str)
# 计算区域平均风速
pressure = getvar(wrf_data, "pressure")
u_wind = getvar(wrf_data, "ua", units="m s-1")
v_wind = getvar(wrf_data, "va", units="m s-1")
u_850 = interplevel(u_wind, pressure, 850)
v_850 = interplevel(v_wind, pressure, 850)
wind_speed = np.sqrt(u_850**2 + v_850**2)
mean_speed = np.nanmean(wind_speed)
wind_speeds.append(mean_speed)
# 创建时间序列图
fig, ax = plt.subplots(figsize=(10, 6))
# 转换时间格式
time_objs = [datetime.strptime(t.decode() if isinstance(t, bytes) else t,
'%Y-%m-%d_%H:%M:%S')
for t in times]
ax.plot(time_objs, wind_speeds, 'b-o', linewidth=2, markersize=8)
ax.fill_between(time_objs, 0, wind_speeds, alpha=0.3)
ax.set_xlabel('Time', fontsize=12)
ax.set_ylabel('Mean Wind Speed at 850 hPa (m/s)', fontsize=12)
ax.set_title('Typhoon Intensity Evolution', fontsize=14, fontweight='bold')
ax.grid(True, alpha=0.3)
ax.tick_params(axis='x', rotation=45)
plt.tight_layout()
ts_path = f'{output_dir}/wind_speed_timeseries.png'
plt.savefig(ts_path, dpi=300, bbox_inches='tight')
plt.close(fig)
print(f"风速时间序列图已保存: {ts_path}")
return output_path, ts_path
# 使用示例(假设有多个时间步的WRF输出)
wrf_files_sorted = sorted(glob.glob("wrfout_d02_2023-08-*.nc"))
typhoon_centers = [
(125.0, 22.5), # 初始位置
(123.5, 23.0), # 6小时后
(122.0, 23.5), # 12小时后
(120.5, 24.0) # 18小时后
]
analysis_extent = [115, 130, 20, 30]
chart1, chart2 = analyze_typhoon_wind_evolution(
wrf_files_sorted[:4], # 前4个时次
typhoon_centers,
analysis_extent,
output_dir='./typhoon_case_study'
)
这个实战案例展示了如何:
- 分析台风过程中低层风场的演变
- 结合相对涡度场识别台风中心
- 追踪台风路径并标记中心位置
- 生成风速时间序列分析台风强度变化
- 创建专业的多面板图表用于报告或演示
通过这个完整的工作流,你可以将原始的WRF输出转化为具有科学价值和业务意义的分析产品。
更多推荐
所有评论(0)