1. 为什么要在GEE里折腾逐日温度数据
做气候分析、农业估产、城市热岛研究或者疾病传播建模的人,几乎都绕不开一个基础需求:拿到某块区域从2000年至今、每一天的最高温和最低温。听起来简单,但真动手就知道坑不少。本地气象站数据稀疏、空间插值误差大、跨年份数据格式不统一,一套流程跑下来,数据还没开始分析,人已经先累趴了。
GEE(Google Earth Engine)在这件事上的价值就很直接了:它把多源遥感产品统一挂在云端,你写几行代码就能按时间、按区域批量拉取,不用自己下载几十GB的原始影像再慢慢裁剪。这篇内容就是围绕“下载2000年到2023年逐日最高温、最低温”这个具体任务,把产品选型、时间范围处理、区域裁剪、批量导出、常见报错这一整条链路拆开讲清楚。
适合谁来参考?如果你已经能打开GEE代码编辑器、跑过最简单的ImageCollection筛选,那这篇基本可以照着抄;如果你是完全的新手,也没关系,我会把每个参数为什么这么设、不这么设会出什么问题都讲明白。核心关键词就三个:逐日温度、最高温最低温、GEE批量导出,后面所有内容都围绕它们展开。
先说结论性的选型:逐日最高温和最低温,业界最常用的产品是Daymet V4(北美区域,1km,逐日)和ERA5-Land Daily Aggregated(全球,约9km,逐日聚合)。前者精度高但只覆盖北美,后者全球覆盖、时间从1950年至今,是绝大多数全球尺度研究的默认选择。标题里没限定区域,所以我按全球通用的ERA5-Land Daily来主讲,同时在选型章节把Daymet作为对照讲清楚,方便你按自己的研究区切换。
2. 数据产品选型与核心思路拆解
2.1 逐日温度产品的三个候选与取舍逻辑
在GEE里搜“daily temperature”,能跳出来一堆产品,但真正适合“2000到2023年逐日最高最低温”这个需求的,主要就三个方向。我把它们的核心参数摆在一起对比,你一眼就能看出差别。
| 产品 | 空间分辨率 | 时间覆盖 | 最高/最低温字段 | 覆盖范围 | 适用场景 |
|---|---|---|---|---|---|
| ERA5-Land Daily Aggregated | 约9km(0.1°) | 1950至今 | temperature_2m_max / temperature_2m_min | 全球 | 全球尺度、长时序、气候趋势 |
| Daymet V4 | 1km | 1980至今 | tmax / tmin | 北美 | 北美区域、高精度、生态农业 |
| MODIS MOD11A1 | 1km | 2000至今 | LST_Day_1km / LST_Night_1km | 全球 | 地表温度(非气温),城市热岛 |
这里有个特别容易踩的坑:MODIS的LST是地表温度(Land Surface Temperature),不是气象意义上的气温(2米高度空气温度)。很多新手看它分辨率高、时间从2000年开始,就直接拿来当“最高温最低温”用,结果做出来的结果和气象站对不上,被审稿人一顿质疑。地表温度和气温在裸土、城市下垫面差异能到十几度,这个必须分清。
所以选型逻辑很清晰:要全球、要长时序、要气象口径的气温,选ERA5-Land Daily;研究区在北美、追求1km精细度,选Daymet;只有明确要做地表温度分析,才碰MODIS LST。标题没限定区域,我主讲ERA5-Land Daily,这是最稳妥的通用解。
2.2 为什么是“Daily Aggregated”而不是原始ERA5-Land
ERA5-Land本身是逐小时的,GEE里对应的是ECMWF/ERA5_LAND/HOURLY。你完全可以自己从逐小时数据里算每日最大值和最小值,但没必要。官方已经提供了聚合好的日产品ECMWF/ERA5_LAND/DAILY_AGGR,它内部已经帮你做了“当日24小时取最大/最小”的运算,字段直接就是temperature_2m_max和temperature_2m_min。
自己聚合的问题在于:逐小时数据量是逐日的24倍,导出时请求的像素和时间片数量暴涨,很容易触发GEE的“计算超时”或“用户内存超限”。而且自己写reducer聚合,还得处理时区、缺测值这些细节,纯属给自己找麻烦。用官方聚合产品,既省算力又少出错,这是我在多个项目里验证过的选择。
注意:ERA5-Land Daily的时间标签是UTC日,如果你研究的是本地时区的“日最高温”,严格来说存在几个小时的偏移。对大多数气候趋势分析影响可忽略,但做极端天气事件逐日对齐时要留意这个细节。
2.3 整体流程的骨架设计
把任务拆开,其实是四步:定义研究区 → 筛选时间与产品 → 映射单位与裁剪 → 批量导出。每一步都有它的设计考量。
定义研究区用ee.Geometry或者导入的矢量边界,这一步决定了后面导出的空间范围,范围越大导出越慢、文件越大。筛选时间用filterDate('2000-01-01', '2024-01-01'),注意结束日期是开区间,写2024-01-01才能把2023-12-31包含进来,这是很多人少下一年数据的元凶。映射单位是因为ERA5-Land的温度单位是开尔文(K),要减273.15转摄氏度。批量导出则是把每年甚至每天拆成一个导出任务,避免单个任务过大。
这个骨架的好处是模块化:换区域只改第一步,换产品只改第二步,换单位只改映射函数,互不干扰。下面几章就按这个骨架逐层展开。
3. 核心细节解析与实操要点
3.1 研究区定义的三种写法与适用场景
研究区定义看着简单,但写法不同,后面导出的效率和结果差别很大。我常用三种写法。
第一种是矩形范围,用ee.Geometry.Rectangle([xmin, ymin, xmax, ymax]),坐标顺序是经度、纬度、经度、纬度。这种写法最快,适合大区域气候分析,比如整个华北平原或者某个流域的粗略范围。缺点是边界是矩形,会包含研究区外的像素,后续统计时得再裁一次。
第二种是导入矢量边界,在GEE的Assets里上传Shapefile或GeoJSON,然后ee.FeatureCollection('users/xxx/boundary').geometry()。这种最精确,适合行政区、流域这种不规则边界。缺点是矢量复杂时(比如几万个顶点的省界),clip操作会变慢,导出时可能超时。
第三种是用GEE自带的公共矢量,比如FAO/GAUL/2015/level1这种国界数据集,按名称筛选。适合快速试验,不用自己准备数据。
实操心得:如果研究区是复杂矢量,我一般先用geometry().bounds()取外接矩形去下载,下载完在本地用矢量再裁一次。这样GEE端的计算量小很多,导出成功率高,本地裁剪也就几秒钟的事。这个“先粗后精”的策略帮我省过无数次超时重试。
3.2 时间筛选的边界陷阱与年份循环
filterDate的结束日期是不包含的,这个规则必须刻在脑子里。想拿2000-01-01到2023-12-31的全部数据,正确写法是:
var tempCol = ee.ImageCollection('ECMWF/ERA5_LAND/DAILY_AGGR') .filterDate('2000-01-01', '2024-01-01') .select(['temperature_2m_max', 'temperature_2m_min']);如果你写成'2023-12-31',那2023年最后一天的数据就丢了,而且这种错误特别隐蔽,不逐日核对根本发现不了。我见过有人的论文数据少了一整年,就是因为这个。
另一个细节是,一次性导出23年逐日数据,任务量是8000多张影像。虽然GEE能处理,但单个导出任务扛不住这么大的量。我的做法是按年循环,每年生成一个导出任务,23个任务排队跑。这样单个任务失败只影响一年,重跑成本低。循环的写法:
var startYear = 2000; var endYear = 2023; for (var y = startYear; y <= endYear; y++) { var start = ee.Date.fromYMD(y, 1, 1); var end = ee.Date.fromYMD(y + 1, 1, 1); var yearly = tempCol.filterDate(start, end); // 后续导出逻辑 }用ee.Date.fromYMD而不是字符串拼接,能避免月份补零、闰年这些手工处理的麻烦,代码也更干净。
3.3 单位换算与缺测值处理
ERA5-Land的温度字段单位是开尔文。转摄氏度就是减273.15,用subtract(273.15)即可。但这里有个容易忽略的点:缺测值。ERA5-Land在水体、冰川边缘可能有无效像素,转完摄氏度后这些位置会变成异常大的负数(比如-273左右)。如果不处理,统计最小值时会被这些假值污染。
稳妥的做法是在映射时加一个有效性掩膜,或者导出后在本地用合理范围(比如-60到60摄氏度)过滤。GEE端可以这样写:
var toCelsius = function(image) { var tmax = image.select('temperature_2m_max').subtract(273.15); var tmin = image.select('temperature_2m_min').subtract(273.15); return tmax.addBands(tmin) .rename(['tmax_c', 'tmin_c']) .copyProperties(image, ['system:time_start']); };copyProperties这步很关键,它把时间戳带过去,否则导出后的影像丢失时间信息,后续按日分析就抓瞎了。这是我在早期项目里踩过的坑,导出的tif全是无名氏,根本对不上日期。
3.4 分辨率与导出的参数权衡
ERA5-Land原生分辨率是0.1度(约9km)。导出时scale参数设多少?设成原生10000米左右最合理,强行重采样到1km只会让文件变大、精度并不会真的提升,因为源数据就那个精度。我一般设scale: 10000,既尊重原始分辨率,又控制文件体积。
crs参数默认是EPSG:4326(WGS84经纬度),做全球或大区域分析够用。如果你的下游分析要用投影坐标系(比如UTM),可以在导出时指定,但要注意跨多个UTM带时会有变形,全球数据还是老老实实用4326。
maxPixels是个高频报错点。默认值约1e8,大区域导出经常不够,报“Image too large”。设成1e13基本能覆盖大多数区域,但别无脑设太大,太大反而可能因为请求超限被拒。我的经验值是区域小于一个省用默认,大于一个省设1e11到1e13。
4. 完整实操流程与批量导出实现
4.1 从零到导出任务的完整代码
把前面几章的碎片拼起来,给一份可以直接跑的完整脚本。这段代码按年循环,每年导出一个多波段(tmax、tmin)的GeoTIFF到云端硬盘。
// 1. 定义研究区(这里用矩形示例,替换成你的边界) var roi = ee.Geometry.Rectangle([100, 20, 120, 40]); // 2. 加载产品并筛选时间、波段 var tempCol = ee.ImageCollection('ECMWF/ERA5_LAND/DAILY_AGGR') .filterDate('2000-01-01', '2024-01-01') .select(['temperature_2m_max', 'temperature_2m_min']); // 3. 单位换算函数 var toCelsius = function(image) { var tmax = image.select('temperature_2m_max').subtract(273.15); var tmin = image.select('temperature_2m_min').subtract(273.15); return tmax.addBands(tmin) .rename(['tmax_c', 'tmin_c']) .copyProperties(image, ['system:time_start']); }; // 4. 按年循环导出 for (var y = 2000; y <= 2023; y++) { var start = ee.Date.fromYMD(y, 1, 1); var end = ee.Date.fromYMD(y + 1, 1, 1); var yearly = tempCol.filterDate(start, end).map(toCelsius); // 转成多波段单影像,每个波段对应一天 var stacked = yearly.toBands(); Export.image.toDrive({ image: stacked.clip(roi), description: 'temp_daily_' + y, folder: 'GEE_Temp', fileNamePrefix: 'temp_daily_' + y, region: roi, scale: 10000, crs: 'EPSG:4326', maxPixels: 1e13 }); }跑完之后在GEE的Tasks面板里会看到23个待运行任务,逐个点Run,或者用批量运行的方式排队。每个任务导出的是一张多波段影像,波段名形如tmax_c_0、tmin_c_0,数字对应集合里的序号,需要结合时间戳还原日期。
4.2 多波段堆叠的日期还原技巧
toBands()把影像集合压成一张多波段影像,但波段名里的序号和日期的对应关系,导出后容易乱。我的做法是额外导出一个CSV,记录每个序号对应的日期,或者更直接——按天单独导出,文件名里带日期。
按天导出的代码量稍大,但日期信息最可靠:
var dailyList = yearly.toList(yearly.size()); var n = yearly.size().getInfo(); for (var i = 0; i < n; i++) { var img = ee.Image(dailyList.get(i)); var date = img.date().format('YYYYMMdd').getInfo(); Export.image.toDrive({ image: img.clip(roi), description: 'temp_' + date, folder: 'GEE_Temp_Daily', fileNamePrefix: 'temp_' + date, region: roi, scale: 10000, maxPixels: 1e13 }); }注意getInfo()是同步调用,循环8000多次会很慢,甚至卡死浏览器。所以按天导出适合小区域、短时段;长时序还是按年堆叠 + 单独导日期表更实际。这个取舍要根据你的数据量来定,没有一刀切的方案。
4.3 导出后的本地校验
数据下下来别急着分析,先做三件事校验。第一,用QGIS或Python打开一张,看数值范围是否在合理区间(比如-50到50摄氏度),如果出现-273这种值,说明缺测没处理干净。第二,抽查几个像素和公开气象站数据对比,偏差在2到3度以内算正常(再分析数据本身有偏差)。第三,检查时间序列完整性,2000到2023年应该有8766天(含闰年),少几天要查是不是filterDate边界写错了。
我用Python做批量校验时,习惯用rasterio读一张算统计,再用xarray把整年拼成时间序列看有没有断档。这一步花十分钟,能避免后面分析全盘重来。
5. 常见问题与排查技巧实录
5.1 导出报错速查表
实际操作里报错五花八门,我把高频的整理成表,方便你对号入座。
| 报错信息 | 可能原因 | 解决办法 |
|---|---|---|
| Image too large | maxPixels不够或区域太大 | 提高maxPixels,或分块导出 |
| Computation timed out | 单任务影像太多 | 按年/按季度拆分任务 |
| User memory limit exceeded | 集合太大或toBands波段过多 | 减少单次处理量,分年循环 |
| No bands found | select字段名拼错 | 核对产品文档的波段名 |
| Export failed: region too large | 矢量边界太复杂 | 用bounds()外接矩形先导 |
“Computation timed out”是我遇到最多的。一开始我图省事,想一次性导出23年,结果任务跑了半小时直接超时。后来改成按年,每个任务几分钟就完成,成功率接近100%。这个教训就是:GEE的算力是共享的,别指望一个任务干完所有活,拆小是王道。
5.2 数值异常与时间错位的排查
有一类问题不报错,但结果不对,更隐蔽。比如导出的tmax比tmin还小,这通常是波段顺序搞反了,addBands的顺序和rename的顺序没对齐。再比如某一年数据全是同一个值,可能是filterDate的年份写错,筛到了空集合,toBands出来就是空或常量。
排查思路是先看集合大小:yearly.size().getInfo(),正常一年应该有365或366。如果是0,时间筛选有问题;如果远大于366,说明产品本身有重复影像(某些产品一天多景),需要去重。去重用yearly.distinct('system:time_start')或者按日期取first。
时间错位还有一种情况:导出的文件名日期和影像内容对不上。这多半是copyProperties没做,或者toBands后波段序号和日期映射错了。我的经验是,只要涉及批量导出,就一定单独维护一份“序号-日期”对照表,别指望从波段名反推。
5.3 几个能省大量时间的实操心得
第一个心得:先用小区域、短时段跑通全流程,再放大。我习惯先用一个月的矩形小区域测试,确认单位、波段、日期都对,再改成23年全区域。这样调试成本极低,不会因为一个参数错等半小时才发现。
第二个心得:导出任务用文件夹分类。GEE的Drive导出默认堆在根目录,23个文件混在一起很难找。folder参数设成按项目命名,fileNamePrefix带年份,下载后一目了然。
第三个心得:保留中间脚本版本。GEE的脚本是云端保存的,改错了不好回退。我习惯每个关键版本在本地存一份,或者用注释标记“v1按年导出”“v2按天导出”,出问题能快速对比。
第四个心得:注意配额。GEE的Drive导出有每日配额限制,23个大文件一天可能导不完,分批跑,别熬夜等。配额用完了第二天再跑,任务不会丢。
6. 换区域与换产品的适配思路
前面全按ERA5-Land全球产品讲,但你的研究区要是在北美,Daymet V4的1km精度明显更香。切换的逻辑其实很简单:换产品ID、换波段名、换单位。Daymet的温度单位直接是摄氏度,不用减273.15,字段是tmax和tmin,产品ID是NASA/ORNL/DAYMET_V4。其余的时间筛选、按年循环、导出参数完全复用。
var daymet = ee.ImageCollection('NASA/ORNL/DAYMET_V4') .filterDate('2000-01-01', '2024-01-01') .select(['tmax', 'tmin']);唯一要注意的是Daymet只覆盖北美,研究区超出范围会得到空集合,导出前先用size()确认一下。另外Daymet的空间分辨率1km,导出时scale设1000,文件会比ERA5-Land大不少,按年导出时单年文件可能上百MB,磁盘空间要留够。
这个“换产品只换三处”的思路,是我做多源数据对比时的标准操作。把产品相关的参数抽成变量放在脚本开头,切换时只改那几行,主体逻辑不动,既不容易出错,也方便做A/B对比。
7. 数据拿到之后能做什么
逐日最高最低温拿到手,能延伸的分析方向很多。最直接的是算月均、年均、生长季均温,做长期趋势分析;再进一步算日较差(tmax减tmin),研究昼夜温差变化;还可以基于阈值做极端高温日数统计、无霜期计算、积温累积。这些分析都建立在“逐日数据完整、单位正确、日期对齐”这三个前提上,所以前面那些校验步骤一个都不能省。
我自己在做农业相关分析时,最常用的是积温(GDD)计算,公式是max((tmax+tmin)/2 - 基准温度, 0)逐日累加。这个计算对逐日数据的完整性极其敏感,少几天数据积温就偏低,所以每次拿到数据第一件事就是查天数对不对。这个习惯帮我避免过好几次因为数据缺失导致的结论偏差。
如果后续要做空间可视化,把导出的GeoTIFF拖进QGIS,用时间管理器做逐日动画,或者用Python的xarray+matplotlib画时间序列热力图,都很直观。数据格式统一了,下游怎么玩都顺手。