ERA5逐小时数据聚合为日数据的三种方法:CDO、NCL与Python实战指南
1. 项目概述:从小时到天,数据聚合的必经之路
处理气象再分析数据,尤其是像ERA5这样的高分辨率全球数据集,是很多气象、气候乃至水文、生态领域研究者的日常。当你从Copernicus Climate Data Store(CDS)辛辛苦苦下载了TB级别的逐小时数据后,面临的第一个现实问题往往就是:如何高效、准确地将这些高频数据聚合成更易管理的逐日数据?无论是为了节省存储空间、匹配其他日尺度数据,还是直接进行气候态分析,这个“降尺度”在时间维度上的操作都是绕不开的一步。
我自己在几年前刚开始用ERA5做极端降水分析时,就曾在这个环节上踩过不少坑。比如,直接用编程循环去读取成千上万个NetCDF文件做日平均,结果内存爆了;又比如,没搞清楚累积量和瞬时量的区别,把“总降水”的日总和算成了小时平均值的24倍,闹了笑话。所以,今天我想结合自己的实战经验,系统聊聊从ERA5逐小时数据获取逐日数据的三种主流方法:使用气候数据运算符(CDO)、利用NCAR命令语言(NCL),以及用Python(xarray)进行灵活处理。这三种方法各有优劣,适合不同的工作流和需求场景,我会把它们的核心命令、背后的计算逻辑、我踩过的坑以及如何避坑都详细拆解给你。
无论你是刚接触气象数据处理的新手,还是想优化现有流程的老手,这篇文章都能给你提供可直接“抄作业”的方案和值得注意的细节。我们的目标很明确:安全、准确、高效地把原始的小时数据,变成我们科研或业务中真正可用的日数据。
2. 核心概念与数据准备:理解你的“原料”
在动手之前,我们必须先搞清楚要处理的是什么“食材”。ERA5数据变量繁多,但大致可以分为两类,这对后续的聚合操作至关重要。
2.1 ERA5变量类型辨析:瞬时量与累积量
这是整个处理流程的基石,一旦搞错,结果全盘皆输。
瞬时量:指在某个特定时刻(如UTC 00:00, 01:00...)的大气状态。例如:
- 2米气温 (
2t) - 海平面气压 (
msl) - 10米风场的U/V分量 (
10u,10v) - 相对湿度 (
r)
对于瞬时量,获取日数据通常意味着计算日平均值。即,将一天内24个时刻的值相加后除以24。这代表了这一天气象要素的平均状态。
累积量:指从前一个时刻到当前时刻的累积总量。在ERA5中,这类数据通常带有“_accumulated”的描述。最典型的就是:
- 总降水 (
tp) - 地表净太阳辐射 (
ssr) - 地表净热辐射 (
str)
对于累积量,获取日数据意味着计算日总和。更精确地说,由于ERA5的逐小时累积量给出的是从“time-1 hour”到“time”这一小时的累积值,因此一天(UTC 00:00到次日00:00)的总量,应该是当日01:00到次日00:00这24个小时的累积值之和。这里有一个关键陷阱:如果你下载的数据是“逐小时”的,那么每个文件中的tp已经是小时累积量。但如果你下载的是“月度”数据,其中可能包含一个名为tp的日总量变量,那就不要再重复累加了。
注意:在CDS下载时,务必确认你选择的“时间聚合”类型。对于需要日总量的变量,应选择“逐小时”数据来自行聚合,这样灵活性最高。
2.2 数据下载与结构检查
假设我们已经从CDS下载了2023年1月全球的逐小时2米气温(2t)和总降水(tp)数据。文件可能被分割成多个NetCDF,例如era5_2t_tp_20230101-20230115.nc和era5_2t_tp_20230116-20230131.nc。
在开始聚合前,强烈建议先用轻量级工具检查一下文件结构:
# 使用ncdump查看文件头信息,了解变量和维度 ncdump -h era5_2t_tp_20230101-20230115.nc # 或者使用CDO的sinfo命令,信息更规整 cdo sinfo era5_2t_tp_20230101-20230115.nc这个步骤能帮你确认:时间维度的单位(是否是hours since ...)、变量名、是否有缺失值、数据的填充方式等。我曾经遇到过时间坐标不连续(因下载失败导致某小时数据缺失)的情况,提前检查可以避免后续聚合出错。
3. 方法一:使用CDO(Climate Data Operators)—— 瑞士军刀法
CDO是气象海洋领域数据处理的“瑞士军刀”,它通过命令行提供了一系列强大的统计和操作功能。其最大优点是高效、内存友好,因为它通常是流式处理数据,无需将整个数据集加载到内存中。
3.1 基础聚合命令与原理
对于瞬时量(如2t)求日平均:
cdo daymean input_hourly.nc output_daily_mean.nc这个命令背后,CDO会:
- 按照时间维度,识别出同一天的所有时间步(通常为24个)。
- 对每个网格点,分别计算这24个值的算术平均值。
- 输出一个新的NetCDF文件,时间维度从“小时”变为“天”。
对于累积量(如tp)求日总和:
cdo daysum input_hourly.nc output_daily_sum.nc同理,daysum操作会对每个网格点,将一天内的24个小时累积值进行求和,得到该日的总降水量。
3.2 多文件处理与批量操作实战
我们很少只处理一个文件。更常见的场景是处理数个月甚至数年的数据,它们被分成多个NetCDF文件。
方案A:使用cat或mergetime合并后再聚合这是最直观的方法,但适用于文件数量不多、总大小可控的情况。
# 首先按时间顺序合并所有小时文件 cdo mergetime era5_*.nc merged_hourly_all.nc # 然后进行日聚合 cdo daymean merged_hourly_all.nc final_daily_mean.nc缺点:合并成一个巨大的文件可能占用大量磁盘空间,且如果中间步骤出错,需要重头再来。
方案B:流式管道聚合(推荐)这是更优雅和高效的方式,尤其适合处理大量文件。CDO可以直接读取文件列表进行聚合。
# 一次性完成:按时间合并并计算日平均 cdo daymean -mergetime era5_20230101-20230115.nc era5_20230116-20230131.nc output_daily.nc # 或者使用通配符,但要确保文件顺序正确(按文件名排序) cdo daymean -mergetime era5_*.nc output_daily.nc这里的-符号是CDO的管道符,表示将前一个操作(mergetime)的输出,直接作为后一个操作(daymean)的输入,不生成中间文件。
方案C:循环处理再合并如果你的数据是按月存放,并且希望保持月度文件的组织方式,可以采用循环:
for month in {01..12}; do cdo daymean era5_2023${month}_hourly.nc era5_2023${month}_daily.nc done # 最后将12个日文件合并 cdo mergetime era5_2023*_daily.nc era5_2023_daily.nc3.3 CDO实战心得与避坑指南
- 时间坐标的陷阱:确保你的输入文件时间坐标是连续的,且没有重复。使用
cdo showdate input.nc可以快速检查。如果存在缺失,daymean会基于实际存在的小时数计算平均,可能导致错误。建议先用cdo setmissval,1e20 input.nc fixed.nc处理缺测值。 - 内存与磁盘空间:流式操作(
-)能极大节省磁盘IO。对于超大型数据,可以结合-f nc4选项指定输出NetCDF4格式,它支持压缩(如-z zip_6),能显著减少输出文件体积。 - 变量选择:如果文件中有多个变量,但你只想处理其中一个,可以使用
selvar命令先选择变量,避免不必要的IO。cdo daymean -selvar,tp merged_hourly_all.nc daily_tp_sum.nc - 性能调优:在拥有多核的服务器上,可以为CDO编译OpenMP支持,并通过设置环境变量
export OMP_NUM_THREADS=4来加速部分计算密集型操作。
4. 方法二:使用NCL(NCAR Command Language)—— 传统脚本法
NCL是一门为气象数据可视化与分析而生的解释型语言,其数组语法和丰富的内置函数对处理网格数据非常友好。虽然NCAR已宣布对其进入维护模式并推荐转向Python,但大量遗留脚本和其强大的文件读写能力,使其仍是许多业务系统或老牌科研团队的选择。
4.1 NCL脚本编写核心步骤
下面是一个完整的NCL脚本示例,用于计算2米气温的日平均和总降水的日总和。
load "$NCARG_ROOT/lib/ncarg/nclscripts/csm/contributed.ncl" begin ; 1. 设置输入输出文件路径 fname_in = "era5_2t_tp_20230101-20230115.nc" fname_out = "era5_daily_20230101-20230115.nc" ; 2. 打开文件,读取变量和时间 fin = addfile(fname_in, "r") t2m_hour = fin->t2m ; 假设变量名是t2m,请根据实际情况修改 tp_hour = fin->tp time_hour = fin->time lat = fin->latitude lon = fin->longitude ; 3. 获取时间信息并创建日维度 utc_date = cd_calendar(time_hour, 0) ; 将时间转换为年月日时分秒数组 year = tointeger(utc_date(:,0)) month = tointeger(utc_date(:,1)) day = tointeger(utc_date(:,2)) ; 构建一个唯一的“日标签”,例如20230101 date_str = sprinti("%0.4i", year) + sprinti("%0.2i", month) + sprinti("%0.2i", day) uniq_dates = get_unique_values(date_str) ; 需要 contributed.ncl 中的这个函数 ndays = dimsizes(uniq_dates) ; 4. 预定义日尺度数组 dims = dimsizes(t2m_hour) nlat = dims(1) nlon = dims(2) t2m_daily = new((/ndays, nlat, nlon/), typeof(t2m_hour), t2m_hour@_FillValue) tp_daily = new((/ndays, nlat, nlon/), typeof(tp_hour), tp_hour@_FillValue) copy_VarAtts(t2m_hour, t2m_daily) ; 复制属性 copy_VarAtts(tp_hour, tp_daily) t2m_daily!0 = "time" t2m_daily!1 = "latitude" t2m_daily!2 = "longitude" tp_daily!0 = "time" tp_daily!1 = "latitude" tp_daily!2 = "longitude" ; 5. 核心循环:按天聚合 do d = 0, ndays-1 ; 找到属于这一天的所有小时索引 day_indices = ind(date_str .eq. uniq_dates(d)) if(.not. any(ismissing(day_indices))) then ; 计算日平均气温 t2m_daily(d, :, :) = dim_avg_n_Wrap(t2m_hour(day_indices, :, :), 0) ; 计算日总降水 tp_daily(d, :, :) = dim_sum_n_Wrap(tp_hour(day_indices, :, :), 0) end if delete(day_indices) ; 及时清理内存 end do ; 6. 创建新的时间坐标(通常取每日的00:00或12:00作为代表) ; 这里简化处理,取每天第一个小时的时间作为日时间坐标 daily_time_ind = new(ndays, integer) do d=0, ndays-1 daily_time_ind(d) = min(ind(date_str .eq. uniq_dates(d))) end do time_daily = time_hour(daily_time_ind) time_daily@units = time_hour@units time_daily@calendar = time_hour@calendar ; 7. 关联维度坐标 t2m_daily&time = time_daily t2m_daily&latitude = lat t2m_daily&longitude = lon tp_daily&time = time_daily tp_daily&latitude = lat tp_daily&longitude = lon ; 8. 写入新文件 system("rm -f " + fname_out) ; 强制删除旧文件 fout = addfile(fname_out, "c") ; 定义文件维度 dim_names = (/"time", "latitude", "longitude"/) dim_sizes = (/ndays, nlat, nlon/) dim_unlim = (/True, False, False/) filedimdef(fout, dim_names, dim_sizes, dim_unlim) ; 定义变量 filevardef(fout, "time", typeof(time_daily), "time") filevardef(fout, "latitude", typeof(lat), "latitude") filevardef(fout, "longitude", typeof(lon), "longitude") filevardef(fout, "t2m_daily", typeof(t2m_daily), dim_names) filevardef(fout, "tp_daily", typeof(tp_daily), dim_names) ; 复制变量属性 filevarattdef(fout, "time", time_daily) filevarattdef(fout, "latitude", lat) filevarattdef(fout, "longitude", lon) filevarattdef(fout, "t2m_daily", t2m_daily) filevarattdef(fout, "tp_daily", tp_daily) ; 写入数据 fout->time = time_daily fout->latitude = lat fout->longitude = lon fout->t2m_daily = t2m_daily fout->tp_daily = tp_daily print("处理完成!输出文件:" + fname_out) end4.2 NCL方法优缺点与适用场景
优点:
- 控制力极强:你可以精确控制聚合的每一个逻辑,例如处理非24小时数据(如3小时数据)、自定义聚合函数(如日最高/最低温)、或处理不规则时间序列。
- 内存管理直观:通过分块读取或索引操作,可以处理大于内存的数据集。
- 与图形化无缝衔接:聚合后的数据可以立即用NCL强大的绘图功能进行可视化检查。
缺点:
- 代码冗长:相比于CDO的一行命令,NCL需要编写数十行脚本。
- 学习曲线:需要熟悉NCL的语法、数组索引和文件读写API。
- 生态衰退:官方支持减弱,新项目更推荐Python。
适用场景:当你的聚合逻辑非常复杂(例如,需要根据另一变量的阈值来有条件地聚合),或者你的整个数据处理分析流程已经建立在NCL生态中时,使用NCL是合理的选择。
5. 方法三:使用Python(xarray + dask)—— 现代灵活法
这是目前科研和业务中增长最快的方法。xarray库完美地封装了NetCDF数据模型,而dask库则提供了并行计算能力,两者结合使得在Python中处理大型气象数据变得既直观又高效。
5.1 基于xarray的核心聚合操作
首先确保安装环境:pip install xarray netcdf4 dask
基础聚合脚本示例:
import xarray as xr import numpy as np # 1. 打开数据集 # 使用open_mfdataset可以轻松打开多个文件,并自动按时间合并 ds_hourly = xr.open_mfdataset('era5_*.nc', combine='by_coords', chunks={'time': 240}) # 使用chunks参数启用dask惰性加载,这里将时间维度分块,每块240个时间步 # 2. 查看数据 print(ds_hourly) print(ds_hourly['t2m'].attrs) # 查看变量属性,确认是瞬时量还是累积量 # 3. 执行聚合操作 # 对于瞬时量(如t2m),计算日平均 ds_daily_mean = ds_hourly['t2m'].resample(time='1D').mean(dim='time') # 对于累积量(如tp),计算日总和 ds_daily_sum = ds_hourly['tp'].resample(time='1D').sum(dim='time') # 4. 将结果合并到一个新的Dataset中 ds_daily = xr.Dataset({'t2m_daily': ds_daily_mean, 'tp_daily': ds_daily_sum}) # 5. 写入新文件 # 设置编码进行压缩,节省空间 encoding = {var: {'zlib': True, 'complevel': 5} for var in ds_daily.data_vars} ds_daily.to_netcdf('era5_daily_output.nc', encoding=encoding) # 6. 关闭文件句柄(使用with语句更佳) ds_hourly.close()resample('1D')是核心方法,它先将数据按“1天”的频率重新分组,然后接.mean()或.sum()进行聚合。xarray会自动处理时间坐标。
5.2 利用Dask处理超大规模数据
当数据量远超单机内存时,Dask的优势就体现出来了。上面的open_mfdataset中已经使用了chunks参数,这表示数据没有被立即加载到内存,而是被分成了多个块(chunk)。所有的聚合操作(resample,mean,sum)都是惰性计算,只有在执行compute()或to_netcdf()时才会真正触发计算。
# 惰性计算示例:构建复杂的处理链 lazy_daily_mean = ds_hourly['t2m'].resample(time='1D').mean() lazy_daily_sum = ds_hourly['tp'].resample(time='1D').sum() # 此时没有实际计算发生 print(type(lazy_daily_mean)) # 输出:<class 'xarray.core.dataarray.DataArray'> # 方案A:直接写入文件,触发计算 lazy_daily_mean.to_netcdf('t2m_daily.nc', compute=True) # 方案B:显式计算并保存到内存(适用于后续还需多次操作的结果) daily_mean_computed = lazy_daily_mean.compute() # 现在daily_mean_computed是一个普通的numpy数组在内存中你可以通过配置Dask的分布式调度器,将计算任务分发到多核CPU甚至计算集群上,从而大幅提升处理TB级数据的速度。
5.3 Python方法进阶技巧与常见问题
- 时间轴对齐问题:ERA5数据的时间坐标通常是“
hours since 1900-01-01”。xarray在resample时能很好地理解这一点。但如果你发现聚合后的时间点不是你期望的(比如日数据的时间戳是00:00还是12:00),可以使用resample(time='1D', label='left')或label='right'来控制标签位置,使用closed='left'或'right'来控制区间闭合端。 - 处理缺失值:
resample操作默认会跳过全为NaN的组。如果你希望保留NaN组,需要设置skipna=False。但要注意,对于累积量求和,如果某天有部分小时数据缺失,skipna=False会导致该日总和为NaN。 - 自定义聚合函数:
resample后不仅可以接mean,sum,还可以接min,max,std,或者使用apply方法传入自定义函数,灵活性极高。# 计算日最高温和最低温 daily_tmax = ds_hourly['t2m'].resample(time='1D').max() daily_tmin = ds_hourly['t2m'].resample(time='1D').min() # 自定义函数:计算日温差 def daily_range(da): return da.max() - da.min() daily_trange = ds_hourly['t2m'].resample(time='1D').apply(daily_range) - 性能优化:
chunks的大小设置是关键。通常,一个chunk的大小应适合内存(如100MB-1GB)。对于时间序列,按时间分块是高效的。可以通过ds_hourly.chunks查看当前分块情况,并使用rechunk()方法调整。
6. 三种方法对比与选型建议
为了更直观地对比,我将三种方法的核心特点总结如下表:
| 特性维度 | CDO (气候数据运算符) | NCL (NCAR命令语言) | Python (xarray + dask) |
|---|---|---|---|
| 上手速度 | 极快,一行命令完成核心操作 | 较慢,需学习语法和API | 中等,需基础Python和库知识 |
| 处理效率 | 非常高,流式处理,内存占用低 | 中等,取决于脚本优化程度 | 高,结合Dask可并行,但内存管理需注意 |
| 功能灵活性 | 中等,受限于预定义运算符 | 极高,可编写任意复杂逻辑 | 极高,可无缝集成Python生态 |
| 大数据支持 | 优秀,原生支持流式处理 | 需手动分块处理 | 优秀,原生集成Dask并行框架 |
| 代码可读性 | 命令行,简洁但功能隐式 | 脚本较长,过程式编程 | 脚本清晰,声明式编程,易于理解 |
| 生态与未来 | 稳定,气象领域专用工具 | 维护中,不推荐新项目 | 活跃,主流趋势,社区庞大 |
| 调试便利性 | 较差,错误信息有时晦涩 | 中等,可逐行调试 | 好,可使用Python调试工具 |
| 典型适用场景 | 常规、批量的日/月/年统计 | 复杂、非标准的聚合需求,或遗留系统 | 现代数据分析流程,需与机器学习、可视化等深度集成 |
选型建议:
- 如果你是新手,或只想快速、稳定地完成标准的日聚合任务:无脑选择CDO。它的学习成本最低,效率最高,一行命令就能得到可靠结果。把时间花在分析结果上,而不是数据处理上。
- 如果你的聚合逻辑非常特殊(例如,只聚合降水大于0.1mm的那些小时,或需要复杂的时空条件判断),或者你所在的团队/项目已有成熟的NCL代码库:可以考虑使用NCL。它给你最大的控制权。
- 如果你正在进行一项新的研究,处理的数据量巨大(TB级),或者后续的分析、可视化、机器学习都计划在Python生态中进行:那么Python (xarray)是你的不二之选。它代表了当前地球科学数据计算的主流方向,灵活性和扩展性无可比拟。从长期来看,投资学习xarray的回报率最高。
在实际工作中,我经常混合使用这些工具。比如,用CDO进行数据的初步裁剪、格式转换和快速检查;用xarray+dask在Jupyter Notebook里进行交互式的探索性分析和复杂计算;而一些特定的、已成型的分析图表,可能还是会用NCL脚本来批量生成。工具是为人服务的,选择最适合当前任务和自身技能栈的那一个。
7. 常见问题排查与实战技巧实录
即使知道了方法,在实际操作中还是会遇到各种“坑”。下面是我和同事们总结的一些典型问题及解决方法。
7.1 时间处理相关错误
问题1:聚合后时间坐标错乱或不是整日。
- 现象:日数据文件里的时间显示为“2023-01-01 12:00:00”而不是“2023-01-01 00:00:00”。
- 原因与解决:
- CDO:CDO的
daymean默认输出时间戳是当天所有时间点的平均值。如果你希望代表日的是00:00,可以使用shifttime命令调整:cdo shifttime,-12hour daily_mean.nc daily_mean_00UTC.nc。 - xarray:使用
resample(time='1D', label='left', closed='left')。label='left'表示使用区间左边界(00:00)作为标签,closed='left'表示区间是左闭右开[00:00, 次日00:00),这符合大多数气象日界的定义。 - 核心:理解你需要的“日”是日历日(00-24 UTC)还是业务日(如12-12 UTC),并根据需求调整聚合逻辑和时间标签。
- CDO:CDO的
问题2:遇到闰秒或不连续时间轴导致聚合失败。
- 现象:CDO报错“Grids have different size”或xarray报错“Cannot resample”。
- 解决:首先用
cdo showtimestamp或xr.decode_cf()检查时间坐标是否连续、等间隔。对于ERA5,通常很规整。如果数据源本身有问题,可能需要先用cdo setgridtime或xarray的reindex进行时间轴修复。
7.2 数据精度与单位转换
问题:降水单位从“米”到“毫米”的转换。
- 说明:ERA5中的总降水(
tp)单位是“米”,这是国际单位制。但气象学中常用“毫米/天”。 - 操作:在聚合之后进行单位转换。
- CDO:
cdo mulc,1000 daily_tp_sum_m.nc daily_tp_sum_mm.nc - xarray:
ds_daily['tp_daily'] = ds_daily['tp_daily'] * 1000 - 切记:同时更新变量的
units属性,例如:tp_daily.attrs['units'] = 'mm day-1'。
- CDO:
7.3 内存不足与性能优化
问题:处理全球多年数据时内存爆炸(OOM)。
- CDO方案:CDO本身流式处理,内存压力小。但如果文件太多,
mergetime可能产生巨大的中间文件。最佳实践是使用管道,或分时段处理。# 不好的做法:先合并全年数据(巨大文件),再聚合 cdo mergetime *.nc huge.nc && cdo daymean huge.nc out.nc # 好的做法:流式管道,一次处理一个月 for year in {2020..2023}; do for month in {01..12}; do cdo daymean -mergetime era5_${year}${month}*.nc daily_${year}${month}.nc done done cdo mergetime daily_*.nc final_daily.nc - xarray+Dask方案:这是其优势所在。关键在于合理设置
chunks。
如果单机内存依然不足,可以考虑使用Dask的分布式调度器,连接到一个计算集群。# 错误的打开方式:直接加载到内存 ds = xr.open_mfdataset('era5_*.nc') # 如果文件很大,立刻OOM # 正确的打开方式:使用分块和惰性加载 ds = xr.open_mfdataset('era5_*.nc', chunks={'time': 240, 'latitude': 100, 'longitude': 100}) # 分块原则:使每个块的大小在10MB-100MB左右,便于在内存中操作。 # 可以通过 `ds.nbytes / 1e6` 估算数据总大小,通过 `ds.chunks` 查看分块情况。
7.4 结果验证:如何确保你的日数据是对的?
这是最后也是最关键的一步。不要假设程序运行没报错,结果就是对的。
- 抽样检查:选择一个你知道天气过程的区域和时间点。例如,你知道2023年7月20日北京下了一场大雨。
- 用
Panoply、ncview或Pythonmatplotlib快速可视化该日的降水场,看空间分布是否合理。 - 提取该格点的小时降水序列,手动计算日总和,与程序输出的日值对比。
- 用
- 气候态检查:计算一段时间(如一个月)的日数据空间平均,绘制时间序列。看看是否出现异常的跳变或恒值(这可能是缺测值处理不当或单位错误)。
- 与已知数据源对比:如果可能,将你处理得到的日平均气温、日总降水,与公开的ERA5日数据产品(如ERA5-Land)或站点观测数据进行粗略对比,检查量级和变化是否一致。
- 使用CDO的
diff命令:如果你用两种方法(如CDO和Python)处理了同一份数据,可以用cdo diff比较两个输出文件,看是否在机器精度内一致。cdo diff method1_daily.nc method2_daily.nc
数据处理是一个需要耐心和细心的工作。每次处理新数据或更换方法后,养成交叉验证的习惯,能帮你节省大量后续调试和论文返工的时间。
