气象数据分析是理解天气和气候系统的重要工具,而ERA5作为欧洲中期天气预报中心(ECMWF)的第五代再分析数据集,已经成为科研和业务应用中的黄金标准。我第一次接触ERA5数据是在研究城市热岛效应时,当时就被它丰富的时间分辨率和空间覆盖所震撼。
ERA5提供了从1940年至今的全球气候数据,时间分辨率高达每小时一次,空间分辨率为0.25度(约28公里)。这对于分析天气系统的垂直结构特别有价值,因为包含了37个标准气压层的数据,从地表1000hPa一直延伸到1hPa高空。我经常用它来分析台风结构、大气边界层特征等。
与卫星观测和地面站点数据相比,ERA5的优势在于其完整性和一致性。记得有次分析青藏高原大气状况时,站点数据严重匮乏,正是ERA5的再分析数据救了急。不过要注意,再分析数据本质上是模型输出,使用时需要了解其同化系统和模型物理过程的特点。
我推荐使用Anaconda创建专用环境,避免包冲突。最近一次配置环境时,我用的是Python 3.9,因为这个版本在稳定性和新特性之间取得了很好的平衡。以下是创建环境的命令:
bash复制conda create -n era5_analysis python=3.9
conda activate era5_analysis
conda install -c conda-forge xarray dask netCDF4 matplotlib cartopy
这里特别推荐通过conda-forge安装,它能自动处理复杂的依赖关系。有次我pip安装cartopy时遇到proj库的问题,折腾了半天,最后还是conda-forge一键解决了。
从Copernicus Climate Data Store(CDS)下载数据需要先注册账号。新手常犯的错误是直接点击下载链接,其实需要通过API获取。我整理了一个下载脚本模板:
python复制import cdsapi
c = cdsapi.Client()
c.retrieve(
'reanalysis-era5-pressure-levels',
{
'product_type': 'reanalysis',
'variable': ['temperature', 'u_component_of_wind', 'v_component_of_wind'],
'pressure_level': ['1000','925','850','700','500','300','250','200'],
'year': '2024',
'month': '05',
'day': '01',
'time': '14:00',
'format': 'netcdf',
},
'era5_data.nc')
这个脚本下载的是2024年5月1日14时的数据,包含8个常用气压层的温度、U/V风分量。实际使用时,可以根据需要调整时间范围和气压层。我建议初次使用时先下载小范围数据测试,避免下载大文件后才发现格式问题。
xarray是处理气象数据的利器,但大文件读取有讲究。我吃过内存不足的亏,后来学会了使用dask进行分块处理:
python复制import xarray as xr
# 使用dask分块加载
ds = xr.open_dataset('era5_data.nc', chunks={'time':1})
print(ds)
这个chunks参数告诉xarray按时间步分块加载,避免一次性读取全部数据。对于全球数据,还可以加上空间分块,比如chunks={'longitude':100, 'latitude':100}。
筛选特定位置时,新手容易卡在经纬度匹配上。ERA5使用-180到180的经度范围,而有些数据使用0-360。这是我常用的转换函数:
python复制def adjust_lon(ds):
"""将经度从0-360转换为-180到180"""
ds.coords['longitude'] = (ds.coords['longitude'] + 180) % 360 - 180
ds = ds.sortby(ds.longitude)
return ds
提取特定位置数据时,method='nearest'很方便,但要注意网格分辨率。有次我分析山区数据时,最近邻插值导致站点偏移了十几公里,后来改用method='linear'才解决。
温度廓线能直观展示大气层结稳定性。这是我优化过的绘图代码:
python复制import matplotlib.pyplot as plt
def plot_temperature_profile(temp_profile, location_name):
plt.figure(figsize=(8,10))
temp_profile.plot(y='level', marker='o', linestyle='-', linewidth=2, markersize=8)
plt.title(f'Temperature Profile at {location_name}', fontsize=14)
plt.xlabel('Temperature (K)', fontsize=12)
plt.ylabel('Pressure Level (hPa)', fontsize=12)
# 专业气象图的y轴惯例
plt.yscale('log')
plt.gca().invert_yaxis()
plt.grid(True, which='both', linestyle='--', alpha=0.5)
# 添加标准大气温度线作为参考
std_temp = 288.15 - 6.5 * (np.log(temp_profile.level/1000)/np.log(10)) * 10
plt.plot(std_temp, temp_profile.level, 'r--', label='Standard Atmosphere')
plt.legend()
plt.tight_layout()
return plt
这个图添加了几处改进:对数坐标更符合气象惯例、添加了标准大气参考线、优化了线型和标记。我在分析北京冬季逆温层时,这种对比特别有用。
比较不同地点的温度廓线能揭示区域差异。比如分析城市和郊区的热岛效应:
python复制def compare_profiles(profiles, locations):
plt.figure(figsize=(10,8))
colors = plt.cm.viridis(np.linspace(0,1,len(profiles)))
for prof, loc, color in zip(profiles, locations, colors):
prof.plot(y='level', marker='o', linestyle='-',
label=loc, color=color, linewidth=2)
plt.title('Temperature Profile Comparison', fontsize=14)
plt.xlabel('Temperature (K)', fontsize=12)
plt.ylabel('Pressure Level (hPa)', fontsize=12)
plt.yscale('log')
plt.gca().invert_yaxis()
plt.grid(True, which='both', linestyle='--', alpha=0.5)
plt.legend(bbox_to_anchor=(1.05,1), loc='upper left')
plt.tight_layout()
return plt
这个函数可以灵活比较任意多个站点的温度廓线。我常用它来分析海陆温差或者山地-平原差异。记得有一次分析台风眼区与外围的温度差异,这种对比图效果非常直观。
U/V分量需要合成才能得到真实风场。这是我常用的处理函数:
python复制def calculate_wind_speed_direction(u, v):
"""计算风速和风向"""
wind_speed = np.sqrt(u**2 + v**2)
wind_dir = (270 - np.rad2deg(np.arctan2(v, u))) % 360
return wind_speed, wind_dir
绘制风垂直廓线时,我习惯把风速和风向放在同一张图上:
python复制def plot_wind_profile(u_profile, v_profile, location):
wind_speed, wind_dir = calculate_wind_speed_direction(u_profile, v_profile)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14,8))
# 风速子图
wind_speed.plot(y='level', marker='o', ax=ax1, color='blue')
ax1.set_title(f'Wind Speed at {location}', fontsize=12)
ax1.set_xlabel('Wind Speed (m/s)', fontsize=10)
ax1.set_ylabel('Pressure Level (hPa)', fontsize=10)
ax1.invert_yaxis()
ax1.grid(True)
# 风向子图
wind_dir.plot(y='level', marker='o', ax=ax2, color='red')
ax2.set_title(f'Wind Direction at {location}', fontsize=12)
ax2.set_xlabel('Wind Direction (degree)', fontsize=10)
ax2.set_ylabel('')
ax2.set_yticks([])
ax2.invert_yaxis()
ax2.grid(True)
plt.tight_layout()
return fig
这种布局可以一目了然地看到风速随高度的变化规律和风向转变。分析低空急流时,这种图特别有用。
更专业的分析需要将温度和风场结合。这是我常用的综合剖面图代码:
python复制def plot_combined_profile(temp, u, v, location):
fig = plt.figure(figsize=(12,10))
# 温度廓线(左轴)
ax1 = fig.add_subplot(111)
temp.plot(y='level', marker='o', color='red', ax=ax1, label='Temperature')
ax1.set_xlabel('Temperature (K)', color='red')
ax1.tick_params(axis='x', labelcolor='red')
ax1.set_ylabel('Pressure Level (hPa)')
ax1.invert_yaxis()
# 风速廓线(右轴)
ax2 = ax1.twiny()
wind_speed = np.sqrt(u**2 + v**2)
wind_speed.plot(y='level', marker='s', color='blue', ax=ax2, label='Wind Speed')
ax2.set_xlabel('Wind Speed (m/s)', color='blue')
ax2.tick_params(axis='x', labelcolor='blue')
# 添加风向标记
for level, ud, vd in zip(temp.level, u, v):
ax1.annotate('', xy=(0.5, level),
xytext=(0.5 + 0.1*ud, level + 0.1*vd),
arrowprops=dict(arrowstyle="->", color='green'))
plt.title(f'Temperature and Wind Profile at {location}', fontsize=14)
ax1.grid(True)
fig.legend(loc='upper right')
return fig
这个图将温度、风速和风向矢量整合在一起,可以清晰看到例如逆温层与低空急流的对应关系。箭头方向表示风向,长度表示风速相对大小。
ERA5数据偶尔会有缺失值,特别是在高海拔地区。我常用的处理方法是:
python复制# 前向填充缺失值
ds_filled = ds.ffill('level')
# 或者使用插值
ds_interp = ds.interpolate_na(dim='level', method='linear')
但要注意,填充高层的缺失数据可能引入误差。我一般会检查原始数据质量标志(如果有的话)。
ERA5的温度默认单位是开尔文(K),但有时需要摄氏度(℃):
python复制# 开尔文转摄氏度
temp_c = ds.t - 273.15
temp_c.attrs['units'] = '°C'
风速单位通常是m/s,但业务中有时需要节(knots):
python复制# m/s转节
wind_knots = wind_speed * 1.94384
记得总是检查数据的units属性,避免单位混淆。有次我把开尔文当摄氏度用,结果得出了完全错误的结论。
处理长时间序列数据时,内存管理很重要。我常用的策略:
ds.isel(time=slice(0,10))先处理小样本ds.chunk({'time':10})分块处理del ds['unused_var']ds.close()显式关闭文件有一次分析全年数据时,Python进程内存暴涨到32GB,后来通过分块处理解决了这个问题。
用实际案例展示如何分析大气边界层结构。选择北京2023年1月15日08时的数据:
python复制# 筛选数据
beijing = ds.sel(longitude=116.4, latitude=39.9, method='nearest')
morning = beijing.sel(time='2023-01-15T08:00')
# 计算位温(更适合边界层分析)
theta = morning.t * (1000/morning.level)**0.286
theta.attrs['units'] = 'K'
# 绘制位温廓线
plt.figure(figsize=(8,10))
theta.plot(y='level', marker='o')
plt.axhline(850, color='gray', linestyle='--') # 标记边界层顶
plt.title('Potential Temperature Profile over Beijing', fontsize=14)
plt.xlabel('Potential Temperature (K)', fontsize=12)
plt.ylabel('Pressure Level (hPa)', fontsize=12)
plt.gca().invert_yaxis()
plt.grid(True)
这张图清晰地显示了地表附近的混合层和上部的稳定层。850hPa附近的转折点就是边界层顶,对污染物扩散研究很重要。
分析日变化需要处理多个时次数据。这是我常用的循环结构:
python复制times = pd.date_range('2023-01-01', '2023-01-31', freq='D')
results = []
for t in times:
try:
# 选择最近时次
daily_data = ds.sel(time=t, method='nearest')
# 提取所需变量
profile = daily_data.sel(latitude=39.9, longitude=116.4).t
# 存储结果
results.append(profile)
except Exception as e:
print(f'Error processing {t}: {str(e)}')
# 合并结果
combined = xr.concat(results, dim='time')
这个脚本会处理1月份每天的数据,并自动跳过有问题的时次。我后来把它封装成了函数,方便复用。
使用dask可以轻松实现并行计算:
python复制from dask.diagnostics import ProgressBar
# 定义处理函数
def process_time_step(time_slice):
return time_slice.t.mean(dim=['latitude','longitude'])
# 分块处理
chunked = ds.chunk({'time':10})
results = []
with ProgressBar():
for i in range(len(chunked.time)):
result = process_time_step(chunked.isel(time=i))
results.append(result.compute()) # 显式计算并释放内存
final = xr.concat(results, dim='time')
这种方法特别适合在多核机器上处理大量数据。我16核的工作站上,处理速度能提高8-10倍。