Python实战:用ERA5数据绘制城市热浪地图(附完整代码)

Python实战:用ERA5数据绘制城市热浪地图(附完整代码)

去年夏天,我在参与一个城市气候适应性规划项目时,遇到了一个棘手的问题:如何量化评估城市热浪的时空分布特征?当时手头有几十年的气象观测站数据,但站点稀疏、空间代表性有限,很难绘制出精细化的热浪风险图。直到我开始尝试使用欧洲中期天气预报中心(ECMWF)的ERA5再分析数据,才真正打开了城市热气候研究的新视野。

ERA5作为目前全球最权威的再分析数据集之一,提供了0.25°×0.25°(约25公里)的高时空分辨率气象数据,覆盖全球范围且时间序列完整。对于城市气候研究者、公共卫生分析师和城市规划师来说,这套数据简直是宝藏——你不再需要依赖零星的气象站点,而是可以直接获取每个网格点的温度、湿度、风速等数十个变量,时间跨度从1950年至今,每小时一次。

但数据到手只是第一步。真正的挑战在于:如何从海量的NetCDF文件中提取出对城市热浪研究有用的信息?如何定义热浪事件?如何将抽象的温度数据转化为直观的空间分布图?更重要的是,如何让这个过程可重复、可自动化,以便应用于不同城市、不同时期的研究?

这篇文章就是我在这个探索过程中的经验总结。我将带你从零开始,用Python构建一套完整的热浪识别与可视化流程。这套方法不仅适用于学术研究,也能为城市规划部门的热岛缓解策略、公共卫生部门的热相关疾病预警提供数据支撑。无论你是刚接触气候数据分析的新手,还是有经验的研究者,都能从中找到实用的代码片段和思路启发。

1. ERA5数据获取与预处理:从云端到本地

1.1 理解ERA5数据结构和访问方式

ERA5数据存储在哥白尼气候数据存储(CDS)平台上,提供两种主要访问方式:手动下载和API自动下载。对于长期研究项目,我强烈推荐使用API方式,虽然初次配置稍显繁琐,但一旦设置完成,后续的数据获取就能完全自动化。

CDS API的核心是cdsapi这个Python库。安装过程很简单:

pip install cdsapi

但安装库只是第一步,真正的关键是获取API密钥。你需要到CDS官网注册账号,然后在用户设置页面找到“API密钥”部分。系统会生成一个包含URL和密钥的文本,你需要将其保存到本地~/.cdsapirc文件中(Windows用户保存在C:\Users\用户名\.cdsapirc)。

注意:API密钥是你的个人凭证,千万不要上传到公开的代码仓库。我通常会在代码中使用环境变量来管理这类敏感信息。

1.2 批量下载城市区域气温数据

假设我们要研究长三角地区2022年的热浪事件,需要下载该区域1970-2022年夏季(6-8月)的2米气温数据。下面是一个完整的下载脚本:

import cdsapi
import calendar
from pathlib import Path
import time

def download_era5_temperature(years, months, area, output_dir):
    """
    批量下载ERA5 2米气温数据
    
    参数:
    years: 年份列表,如[1970, 1971, ..., 2022]
    months: 月份列表,如['06', '07', '08']
    area: 区域范围 [北纬, 西经, 南纬, 东经]
    output_dir: 输出目录路径
    """
    c = cdsapi.Client()
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)
    
    for year in years:
        for month in months:
            # 构建文件名
            filename = f"era5_t2m_{year}_{month}_长三角.nc"
            filepath = output_path / filename
            
            # 如果文件已存在,跳过下载
            if filepath.exists():
                print(f"文件已存在: {filename}")
                continue
            
            # 获取该月的天数
            days_in_month = calendar.monthrange(year, int(month))[1]
            days = [f"{day:02d}" for day in range(1, days_in_month + 1)]
            
            # 24小时数据
            hours = [f"{h:02d}:00" for h in range(24)]
            
            print(f"正在下载 {year}年{month}月数据...")
            
            try:
                c.retrieve(
                    'reanalysis-era5-single-levels',
                    {
                        'product_type': 'reanalysis',
                        'variable': '2m_temperature',
                        'year': str(year),
                        'month': month,
                        'day': days,
                        'time': hours,
                        'area': area,  # 长三角区域: [32.5, 118.0, 29.5, 122.0]
                        'format': 'netcdf',
                    },
                    str(filepath)
                )
                print(f"✓ 下载完成: {filename}")
                time.sleep(2)  # 避免请求过于频繁
                
            except Exception as e:
                print(f"✗ 下载失败 {year}-{month}: {e}")
                continue

# 使用示例
if __name__ == "__main__":
    # 定义下载范围
    years = list(range(1970, 2023))  # 1970-2022年
    months = ['06', '07', '08']  # 夏季月份
    
    # 长三角区域 [北纬, 西经, 南纬, 东经]
    yangtze_delta_area = [32.5, 118.0, 29.5, 122.0]
    
    # 输出目录
    output_directory = "./data/era5_raw"
    
    download_era5_temperature(years, months, yangtze_delta_area, output_directory)

这个脚本有几个关键点值得注意:

  1. 分月下载:ERA5 API对单次请求的数据量有限制,所以按年月分批下载是稳妥的做法
  2. 错误处理:网络请求可能失败,加入try-except结构确保程序不会中途崩溃
  3. 速率控制:每次下载后暂停2秒,避免触发API的速率限制
  4. 文件检查:下载前检查文件是否已存在,避免重复下载浪费资源

1.3 数据格式转换与质量检查

下载的NetCDF文件需要经过初步处理才能用于分析。我通常会用xarray库来读取和检查数据:

import xarray as xr
import numpy as np
import matplotlib.pyplot as plt

def inspect_era5_file(filepath):
    """检查ERA5数据文件的基本信息"""
    ds = xr.open_dataset(filepath)
    
    print("=== 数据集概览 ===")
    print(ds)
    
    print("\n=== 变量信息 ===")
    for var in ds.data_vars:
        print(f"{var}: {ds[var].attrs.get('long_name', '无描述')}")
        print(f"  单位: {ds[var].attrs.get('units', '未知')}")
        print(f"  形状: {ds[var].shape}")
    
    print("\n=== 坐标信息 ===")
    print(f"时间范围: {ds.time.values[0]} 到 {ds.time.values[-1]}")
    print(f"经度范围: {ds.longitude.values.min():.2f}° 到 {ds.longitude.values.max():.2f}°")
    print(f"纬度范围: {ds.latitude.values.min():.2f}° 到 {ds.latitude.values.max():.2f}°")
    
    # 可视化检查
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))
    
    # 时间序列示例
    sample_point = ds.t2m.isel(longitude=10, latitude=10)
    axes[0].plot(sample_point.time, sample_point - 273.15)  # 转换为摄氏度
    axes[0].set_title("单点温度时间序列")
    axes[0].set_ylabel("温度 (°C)")
    axes[0].tick_params(axis='x', rotation=45)
    
    # 空间分布示例(第一个时次)
    temp_map = ds.t2m.isel(time=0) - 273.15
    im = axes[1].imshow(temp_map, cmap='RdBu_r', 
                       extent=[ds.longitude.min(), ds.longitude.max(),
                               ds.latitude.min(), ds.latitude.max()])
    axes[1].set_title("空间分布示例")
    axes[1].set_xlabel("经度")
    axes[1].set_ylabel("纬度")
    plt.colorbar(im, ax=axes[1], label='温度 (°C)')
    
    # 数据统计
    temp_data = ds.t2m.values - 273.15
    axes[2].hist(temp_data.flatten(), bins=50, alpha=0.7)
    axes[2].set_title("温度分布直方图")
    axes[2].set_xlabel("温度 (°C)")
    axes[2].set_ylabel("频数")
    
    plt.tight_layout()
    plt.show()
    
    ds.close()
    return True

# 使用示例
file_path = "./data/era5_raw/era5_t2m_2022_06_长三角.nc"
inspect_era5_file(file_path)

通过这个检查步骤,你可以确认数据是否完整、单位是否正确(ERA5的温度数据默认是开尔文,需要减去273.15转换为摄氏度)、时空范围是否符合预期。

2. 热浪事件的定义与识别算法

2.1 热浪的科学定义与业务标准

在开始编码之前,我们需要明确什么是"热浪"。不同地区、不同研究目的对热浪的定义可能不同,但核心思想是一致的:一段异常高温的持续时期。常见的定义方法包括:

方法类型 具体标准 适用场景 优缺点
绝对阈值法 日最高温连续≥35°C的天数 公众健康预警 直观易懂,但忽略了气候背景差异
相对阈值法 日最高温超过历史同期第90/95百分位,持续≥3天 气候研究 考虑本地气候特征,更具科学性
综合指数法 结合温度、湿度、风速的热应激指数 人体舒适度研究 更贴近实际感受,但计算复杂

在实际项目中,我通常采用相对阈值法,因为它能更好地反映"相对于当地气候常态的异常情况"。比如,35°C在重庆可能是常态,但在哈尔滨就是极端事件。

2.2 计算气候基准与温度百分位

要使用相对阈值法,首先需要建立气候基准。我推荐使用1971-2000年或1981-2010年作为基准期,这两个时段是WMO推荐的标准气候基准期。

import xarray as xr
import numpy as np
import pandas as pd
from scipy import stats
import warnings
warnings.filterwarnings('ignore')

class HeatwaveDetector:
    """热浪检测器类"""
    
    def __init__(self, data_dir, baseline_start=1971, baseline_end=2000):
        """
        初始化热浪检测器
        
        参数:
        data_dir: ERA5数据目录
        baseline_start: 基准期起始年份
        baseline_end: 基准期结束年份
        """
        self.data_dir = Path(data_dir)
        self.baseline_start = baseline_start
        self.baseline_end = baseline_end
        self.baseline_data = None
        self.thresholds = None
        
    def load_baseline_data(self):
        """加载基准期数据"""
        print("正在加载基准期数据...")
        
        baseline_files = []
        for year in range(self.baseline_start, self.baseline_end + 1):
            for month in [6, 7, 8]:  # 夏季月份
                filename = f"era5_t2m_{year}_{month:02d}_长三角.nc"
                filepath = self.data_dir / filename
                if filepath.exists():
                    baseline_files.append(filepath)
        
        if not baseline_files:
            raise FileNotFoundError("未找到基准期数据文件")
        
        # 使用xarray的open_mfdataset高效读取多个文件
        ds_list = []
        for file in baseline_files:
            ds = xr.open_dataset(file)
            # 提取日最高温
            daily_max = ds['t2m'].resample(time='1D').max() - 273.15
            ds_list.append(daily_max)
        
        # 合并所有基准期数据
        self.baseline_data = xr.concat(ds_list, dim='time')
        print(f"基准期数据加载完成: {len(baseline_files)}个月份文件")
        return self.baseline_data
    
    def calculate_percentile_thresholds(self, percentile=90):
        """
        计算温度百分位阈值
        
        参数:
        percentile: 百分位数,默认90(即第90百分位)
        
        返回:
        每个网格点的阈值数据数组
        """
        if self.baseline_data is None:
            self.load_baseline_data()
        
        print(f"正在计算第{percentile}百分位阈值...")
        
        # 对每个网格点单独计算百分位
        # 使用xarray的apply_ufunc进行并行计算
        def _percentile_func(data, q):
            """计算单个网格点的百分位"""
            return np.nanpercentile(data, q)
        
        self.thresholds = xr.apply_ufunc(
            _percentile_func,
            self.baseline_data,
            input_core_dims=[['time']],
            output_core_dims=[[]],
            vectorize=True,
            kwargs={'q': percentile}
        )
        
        print("阈值计算完成")
        return self.thresholds
    
    def detect_heatwave_days(self, target_year, min_duration=3):
        """
        检测指定年份的热浪日
        
        参数:
        target_year: 目标年份
        min_duration: 最小持续天数,默认3天
        
        返回:
        热浪日标记的xarray数据集
        """
        if self.thresholds is None:
            self.calculate_percentile_thresholds()
        
        print(f"正在检测{target_year}年热浪日...")
        
        # 加载目标年份数据
        target_data = []
        for month in [6, 7, 8]:
            filename = f"era5_t2m_{target_year}_{month:02d}_长三角.nc"
            filepath = self.data_dir / filename
            if not filepath.exists():
                raise FileNotFoundError(f"目标年份数据缺失: {filename}")
            
            ds = xr.open_dataset(filepath)
            daily_max = ds['t2m'].resample(time='1D
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值