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 半球视图 透视投影,适合展示全球环流

对于中国区域的风场分析,我通常使用PlateCarreeLambertConformal投影。下面是创建基础地图的代码:

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}")

这个函数使用了xarraydask库来处理大型数据集。关键优化包括:

  • 延迟加载:直到需要时才实际读取数据
  • 并行计算:利用多核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'
)

这个实战案例展示了如何:

  1. 分析台风过程中低层风场的演变
  2. 结合相对涡度场识别台风中心
  3. 追踪台风路径并标记中心位置
  4. 生成风速时间序列分析台风强度变化
  5. 创建专业的多面板图表用于报告或演示

通过这个完整的工作流,你可以将原始的WRF输出转化为具有科学价值和业务意义的分析产品。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐