☰
GEE中CHIRPS降水数据处理:日聚合、区域统计与趋势分析避坑
2026/11/3 4:11:48 网站建设 项目流程

1. CHIRPS 这套降水数据凭什么单独拿出来讲

第一次接触 CHIRPS 的人,多半是被它的名字劝退的——Climate Hazards Group InfraRed Precipitation with Station data,一长串英文拼下来,看起来像是某个课题组内部自用的数据产品。但真正在做降雨时序分析的人,绕一圈最后往往还是会回到它身上。原因很简单:它把卫星红外估算和地面站点观测做了融合校正,既解决了纯站点数据在山区、边境、荒漠这些地方稀疏到没法插值的问题,又比纯卫星产品在量级上更贴地。GEE 平台把这份数据挂成了公共资产,意味着你不需要下载几十 GB 的 NetCDF、不需要自己搭 FTP 定时任务、不需要写裁剪拼接脚本,几行代码就能拉出一条二十多年的降水序列。

这篇内容想解决的问题很具体:在 GEE 里把 CHIRPS 用对、用好。不是简单地贴两行ee.ImageCollection('UCSB-CHG/CHIRPS/DAILY')就完事,而是从数据集本身的边界条件讲起,说清楚它适合干什么、不适合干什么,然后一路走到日数据聚合、区域统计、趋势分析、导出成表,最后把我在实际项目里踩过的几个坑摊开讲。适合的读者大概是这几类:做过一点 GEE 基础操作但没系统用过降水产品的、要从降雨角度做区域气候特征描述的、需要批量统计多行政区多年降水指标的。纯新手也能跟下来,因为关键的 API 我都会解释它在干什么,而不是只丢代码。

1.1 0.05°、1981 年至今、50°S–50°N 这三个数字意味着什么

打开 CHIRPS 的元数据,最该先记住的是三组数字,它们直接决定了你的分析能不能做、结论能下到什么程度。

第一个是空间分辨率 0.05°,换算成距离大概在赤道附近 5.5 公里左右。这个尺度意味着什么?意味着它能刻画一个县域内部的降水梯度,但刻画不了一条山谷两侧的差异。如果你要做的是小流域坡面上的降水分布,5 公里格网明显不够用,得考虑降尺度或者换更高分辨率的产品;但如果你做的是省域、流域尺度、行政区的年降水统计,这个分辨率是相当舒服的——既不至于粗到把地形差异抹平,又不至于细到让单像元噪声主导结果。

第二个是时间起点 1981 年。这是它能做长期趋势分析的基础。三十年以上的序列,做线性趋势、做年代际对比、算气候态,样本量是够的。但要注意,早期年份的站点输入密度和后期差别很大,尤其是在站点稀少的区域,八十年代数据的可靠性会打折扣。我一般建议做趋势分析时把起始年份往后挪到 1991 年甚至 2001 年,代价是序列变短,收益是前后一致性更好。

第三个是覆盖范围 50°S 到 50°N。这条经常被忽略,然后以极其隐蔽的方式坑人。如果你的研究区有一角伸到了这个纬度带之外——比如北方某个高纬区域——那一角的返回值会是空的,而不是报错。空值混进平均值里,结果会被悄悄拉高或拉低,你还以为是自己算法写错了。

1.2 和另外几套常见降水产品摆在一起比

GEE 的资产目录里能拿来做降水的产品不止一套,选型的时候把它们放在一张表里看会清晰很多。

产品空间分辨率时间尺度时间跨度典型适用场景
CHIRPS0.05°日 / 五日 / 月1981 年至今中长期区域降水序列、干旱指数、趋势分析
再分析类产品0.25°–0.5°小时 / 日数十年需要气温、辐射、风速等多要素联合分析
卫星反演类产品0.1°小时 / 日近二十余年短时强降水、暴雨事件识别
站点插值类产品0.5°–1°月数十年至百年超大尺度气候态比对

这张表里最能说明问题的是第一行和最后一行的对比。站点插值类产品的优势是时间跨度长、量级准,劣势是空间分辨率粗,在站点稀疏区基本靠外推,山区误差感人。CHIRPS 的价值就在于它卡在中间:分辨率够细,时间够长,而且因为它本身就吸收了站点信息做校正,在量级上比我见过的一些纯卫星产品靠谱得多。

选型的判断逻辑我总结成一句话:如果你关心的是"某个区域某段时间下了多少雨",选 CHIRPS 大概率不会错;如果你关心的是"这场暴雨几点开始几点结束",那它帮不了你,日尺度已经是它的最细粒度,五日合成版本更是只适合做旬月尺度的分析。

1.3 什么时候不该用它

说完优点得说边界,不然用起来容易出问题。

第一种不该用的情况是研究区纬度超出覆盖带。前面提过,超出去的部分返回空值,而且不报错。判断方法很简单,加载之后先看一眼image.geometry()的边界,或者干脆算一次全域的reduceRegion看返回是不是 null。

第二种是需要小时级精度的事件分析。CHIRPS 的日值本质上是当日累计估算,它不告诉你降雨的时段分布。做城市内涝、山洪预警这类工作,日尺度完全不够。

第三种是对绝对量级要求极高的水量平衡核算。CHIRPS 是融合产品,在站点密集区表现很好,但在站点稀疏的高海拔区域,它的偏差可能达到百分之二三十。做水库入库径流估算这种对绝对量敏感的工作,最好拿实测站数据做一次偏差校正再用,别直接拿原始值算。

提示:任何融合类降水产品在山区都存在系统性偏差,通常是低估强降水、高估小雨日数。做趋势分析时这种偏差相对稳定,影响不大;做绝对量统计时就必须正视它。

2. 在 GEE 资产目录里把 CHIRPS 找对、筛对、读对

数据找错入口,后面全白搭。GEE 上 CHIRPS 相关资产不止一个,命名规则乍看相似,实际含义差很远。这一段把加载环节拆开讲,包括资产 ID 的辨认、元数据里必看的字段、过滤条件的写法,以及那个让无数人翻车的单位问题。

2.1 资产 ID 与不同时间聚合版本的差别

GEE 数据目录里能搜到的 CHIRPS 资产主要分两类:日尺度和五日合成尺度。日尺度资产的每张影像代表一天的降水,波段名为precipitation;五日合成资产每张影像覆盖连续五天,值也是这五天的累计。

这两个版本绝不只是"粒度不同"这么简单,它们在聚合逻辑上有本质区别。日尺度做月累计,你需要自己把当月每一天加起来;五日合成做月累计,你得处理"月末那几天凑不满一个五日块"的边界问题。我个人的偏好是一律用日尺度,因为聚合的完全控制权在自己手里,不会因为产品内置的分块方式产生人为断层。

判断自己加载的是哪个版本,最简单的办法是看集合里影像的数量级。跑一句:

var chg = ee.ImageCollection('UCSB-CHG/CHIRPS/DAILY'); print('影像总数', chg.size());

日尺度从 1981 年至今应该在万级数量,五日合成大概是它的五分之一。数量对不上,说明你加载的不是你以为的那个版本。

顺便说一句,资产 ID 的大小写和路径层级是敏感的,别凭记忆手敲。最稳妥的方式是在 GEE 代码编辑器的搜索框里输CHIRPS,从结果列表里点进去,让它自动生成加载语句。

2.2 元数据里那几个必须看的字段

加载完别急着往下写,先print一下集合和第一张影像的元数据。我通常固定看这几项:

  • system:time_start:确认时间戳解析正确,这是后面所有时间聚合的根基。
  • precipitation波段的存在性:有些版本可能有多个波段,select()时写错名字会静默返回空。
  • image.geometry():确认覆盖范围,顺手确认你的研究区是不是在覆盖带内。
  • 波段属性里的单位和缩放说明:这是下一小节的重点。

一个具体的检查动作是这样的:

var first = ee.ImageCollection('UCSB-CHG/CHIRPS/DAILY') .filterDate('2020-01-01', '2020-01-05') .first(); print('波段', first.bandNames()); print('时间', ee.Date(first.get('system:time_start'))); print('几何', first.geometry().bounds());

这三行输出看起来平淡无奇,但它能一次性排除掉三类最常见的低级错误:波段名拼错、时间过滤条件写错格式、研究区超出覆盖范围。

2.3 时间过滤与空间过滤的推荐写法

过滤顺序影响性能,虽然 GEE 的惰性求值会做一定优化,但把空间过滤放在时间过滤之前仍然是更好的习惯——先缩小影像集合,后面的计算量级直接降下来。

时间过滤的坑在于日期格式。filterDate接受的是字符串,格式必须是YYYY-MM-DD,写成2020/01/01或者20200101都不会报错,而是返回空集合。空集合往下走,sum()得到的是 null,最终结果全空,你排查半天代码逻辑,问题其实出在一个斜杠上。

空间过滤有两种思路。一种是filterBounds(roi),用几何对象过滤影像;另一种是直接用clip(roi)裁剪。前者的作用是减少参与计算的影像数量,后者是把结果限制在区域内。两个都该用,顺序是先 filterBounds 再 clip。只写 clip 不写 filterBounds,计算资源会浪费在全域影像上;只写 filterBounds 不写 clip,统计时会把你区域外的像元也算进去。

var roi = ee.Geometry.Rectangle([100, 25, 110, 35]); var monthly = ee.ImageCollection('UCSB-CHG/CHIRPS/DAILY') .filterBounds(roi) .filterDate('2001-01-01', '2021-01-01') .select('precipitation');

2.4 单位这件事,错了就是一个数量级

这是我在实际项目里见过最多的错误,没有之一。

CHIRPS 日尺度产品给的值是日降水强度,单位是毫米每天。也就是说,一张影像上那个数字的含义是"这天的降水折合下来平均每天多少毫米"。做日分析时直接用它没问题。但一旦你要做月累计,就必须乘以当月天数,而不是简单地把三十张日影像加起来。

很多人的写法是:

// 错误示范:直接求和,得到的是"日强度之和",不是月降水总量 var wrongMonthly = dailyColl.sum();

为什么错?因为日强度相加,物理意义上是把"每天的平均强度"叠加了三十次,量纲对不上。正确的做法是给每一天乘上一个系数。如果当月是 30 天,系数就是 30;如果是 31 天就是 31。CHIRPS 的日值本质上是"当日总量除以 1 天",所以把日值乘以 1 天的天数得到当日总量,再累加得到月总量——但因为它本身就是 mm/day 且每日一张,所以更简单的理解是:每日影像的值就是当日降水量(单位 mm),直接 sum 就得到月总量。

这里容易绕晕,我把话说透:对日尺度 CHIRPS,一张影像等于一天,值是当天降水毫米数,sum()三十张就是当月毫米数,直接加是对的。真正的陷阱在五日合成版本——那上面一张影像代表五天,若你把它当成日值去乘天数,结果直接放大五倍;反之若你把它当月度累计去做月度求和,又会小一个数量级。

所以第二条纪律是:先确认资产的时间尺度,再确认单位,最后才写聚合代码。我一般会在脚本开头用注释把这三件事写死,避免过段时间自己都忘了加载的是哪个版本。

提示:拿一个已知月降水量的站点数据做交叉验证,是判断单位是否搞错最快的办法。算出来的值如果是实测的三十倍或五倍,基本可以断定是尺度换算问题。

3. 日数据到月、年、季节序列的聚合路径

数据加载对了,接下来是把它变成能用的形态。降水分析里九成的工作都发生在这一步:把日数据揉成月、揉成季、揉成年,再在区域上做统计。这中间有几个选择点的差异比想象中大,下面逐个拆。

3.1 按月求和的两种写法与结果差异

第一种写法是按固定时间区间循环生成。用ee.List.sequence生成月份索引,每个月构造一次起止日期,过滤、求和、设置时间戳,最后拼成新集合。这种写法逻辑直白,可控性强,缺点是代码长,且容易在跨年边界上出错。

var months = ee.List.sequence(0, 239); // 2001-01 起 240 个月 var monthlyColl = ee.ImageCollection(months.map(function(n){ var start = ee.Date('2001-01-01').advance(n, 'month'); var end = start.advance(1, 'month'); var s = dailyColl.filterDate(start, end); return s.sum() .set('system:time_start', start.millis()) .set('month', start.get('month')) .set('year', start.get('year')); }));

第二种写法是用calendarRange或按年月分组。思路是先给每张日影像打上年月标签,再用ee.Join或者分组合并。这种写法代码更紧凑,但对分组边界的处理需要额外小心,尤其是在月末天数不一致、闰年二月这些情况下,稍有疏忽就会漏掉几天。

我实际用下来更倾向第一种。原因不在美观,而在于可追溯性:每个月独立计算,你可以随时把某个月的起止日期打印出来核对,出问题时能精确定位到是哪个月的过滤条件写歪了。分组写法一旦出现缺月,排查成本高得多。

不管哪种写法,有一条必须坚持:每张月合成影像都带上system:time_start。后续做时间序列图、做趋势分析,全都依赖这个属性。缺了它,集合就退化成无序的影像堆,时间维度的信息彻底丢失。

3.2 气候态与距平的构建顺序

算距平是降水分析的常规动作,但顺序搞反会得到完全错误的结果。

正确的链路是:先算逐年(或逐月)序列 → 再算多年均值 → 最后做减法。看起来天经地义,实际操作里的坑在于"多年均值"到底是在哪个维度上算的。

如果你的目标是"每年降水相对常年偏多偏少多少",那均值应该在年尺度上算,得到的是一个影像,代表多年平均年降水,然后拿每年的年降水影像减它。如果你的目标是"每年每个月的降水相对该月常年偏多偏少多少",那均值必须在月尺度上按月份分组算——一月有一个一月的气候态,七月有一个七月的气候态,把十二个月混在一起求平均是没有物理意义的。

后一种做法的实现方式是把月度集合按month属性过滤出十二个子集,分别求均值再合并:

var clim = ee.ImageCollection(ee.List.sequence(1, 12).map(function(m){ return monthlyColl.filter(ee.Filter.eq('month', m)).mean() .set('month', m); }));

得到十二张气候态影像后,再和月度序列做匹配减法。GEE 里没有直接的"按属性对齐相减"算子,常见做法是把月度集合 map 一遍,每次取出对应月份的均值影像:

var anomaly = monthlyColl.map(function(img){ var m = img.get('month'); var c = clim.filter(ee.Filter.eq('month', m)).first(); return img.subtract(c) .set('system:time_start', img.get('system:time_start')) .set('month', m); });

这段代码跑起来没问题,但要注意filter().first()在集合里频繁调用会拖慢速度。集合规模大的时候,改成先把气候态合成一张多波段影像再select对应波段,效率会明显好一些。

3.3 区域统计:reduceRegion 还是 reduceRegions

这两个 API 名字只差一个字母,用途完全不同,混用会导致结果形态完全不对。

reduceRegion作用在单个几何对象上,返回一个字典。适合"我要算这一个流域的多年平均降水"这种单点查询。参数里geometry、reducer、scale、maxPixels是必调的三个半,maxPixels不设大一点,区域一大就报超限。

reduceRegions作用在要素集合上,返回一个带属性的要素集合。适合"我要一次算出三十个县的年降水"这种批量场景。它的效率优势非常明显,因为 GEE 会并行处理各个要素。

var stats = monthlyColl.map(function(img){ return img.reduceRegions({ collection: counties, reducer: ee.Reducer.mean(), scale: 5566, tileScale: 4 }).map(function(f){ return f.set('date', img.get('system:time_start')); }); });

这里有两个细节值得说。scale我填的是 5566 米,这是 0.05° 在赤道附近对应的实际距离,用它可以让重采样尽量贴近原始格网,避免因为尺度设得太大而丢失空间信息。tileScale是为了缓解内存压力,值越大切片越细,代价是速度变慢。区域边界复杂、要素数量多的时候,把这个值调到 4 或 8 往往能让原本跑挂的任务跑通。

3.4 导出表格时 scale 与 crs 的取舍

统计做完要落地成表,Export.table.toDrive是标配。这里最该想清楚的是scale到底该填多少。

填得太小(比如 1000 米),计算量暴涨,导出任务容易超时失败;填得太大(比如 30000 米),结果会被过度平滑,区域内部的降水梯度信息基本丢光。我的经验值是原始分辨率的 1 到 2 倍之间,也就是 5000 到 11000 米这个区间,具体取值看区域大小和边界复杂度。

还有一个容易被忽略的点是crs。不指定的话默认用 EPSG:4326,这对降水统计来说通常够用。但如果你的边界数据本身是投影坐标系(比如某些本地平面坐标),不显式指定会导致重采样时出现细微的错位,在面积很小的行政区上可能表现为数值异常。稳妥的做法是统一显式写上crs: 'EPSG:4326',让所有环节的坐标系保持一致。

提示:导出前先在代码编辑器里print前几条结果看一眼数值范围。年降水量级应该在几百到两三千毫米之间(极端干旱区和热带雨林区除外)。如果打印出来是个位数或者五位数,八成是单位或尺度出了问题,别急着导。

4. 空间分布、趋势与干湿分级的分析链路

前面都是准备工作,真正出结论在这一段。降水的空间分析无非三类问题:哪里多哪里少、在变多还是变少、哪些年份属于异常。三类问题对应三种截然不同的计算方法,混着做很容易得出站不住脚的结论。

4.1 年降水趋势:线性回归与 Sen 斜率

趋势分析最直观的方法是逐像元做一元线性回归,把年份当自变量、年降水量当因变量,取斜率作为趋势强度。GEE 里有现成的 reducer 可以直接用。

思路是把年降水集合转成"每张影像多带一个年份波段"的形式,然后用linearFit:

var yearly = annualColl.map(function(img){ var y = ee.Date(img.get('system:time_start')).get('year'); return img.addBands(ee.Image.constant(y).rename('year').toFloat()); }); var trend = yearly.select(['year', 'precipitation']) .reduce(ee.Reducer.linearFit()); // 输出两个波段:scale(斜率,单位 mm/年)、offset(截距)

linearFit输出的scale波段就是斜率,单位是毫米每年。正数表示降水在增加,负数表示在减少。这个方法的优点是计算快、结果直观,缺点是对异常值极其敏感。序列里只要有一两年是极端丰水年或极端枯水年,斜率就可能被带偏。

所以我通常会再算一遍Sen 斜率。它是所有点对斜率的中位数,抗异常值能力强得多。GEE 提供了ee.Reducer.sensSlope(),用法和 linearFit 类似:

var sen = yearly.select('precipitation').reduce(ee.Reducer.sensSlope()); // 同样输出 slope 和 offset

两个结果对比着看很有价值。如果线性回归斜率很显著但 Sen 斜率接近零,说明趋势主要由少数几年贡献,稳健性存疑;如果两者方向和量级接近,结论就比较扎实。我在写分析报告时一般会把这个对比作为一段单独的说明,因为审阅的人往往会问"你的趋势是不是被某一年带出来的"。

4.2 显著性判断:GEE 内算 p 值还是导出到本地

斜率算出来不等于趋势成立。年降水量本身波动就大,三十年序列算出个 +2 mm/年的斜率,很可能纯粹是噪声。

GEE 里做显著性检验有两条路。第一条是在线算——用linearRegression拿到残差,再手动构造 t 统计量:

var linReg = yearly.select(['year', 'precipitation']) .reduce(ee.Reducer.linearRegression(1, 1)); // 需要自己取 residuals、算标准误、算 t 值、再转 p 值

这条路能走通,但代码冗长,而且 GEE 没有内置的 t 分布累积函数,得自己用不完全贝塔函数近似,实现起来容易出错。

第二条路是把逐像元的年降水序列导出,在本地做检验。具体做法是把研究区降采样成若干个样本点,每个点导出三十年的年降水值,形成一张"点 × 年"的宽表,然后在本地用统计工具算 Mann-Kendall 检验或者 Sen 斜率的置信区间。这条路工作量大一些,但结果的可信度和可解释性都更好,而且能顺带做序列的其他诊断(比如突变点检测)。

我的选择取决于项目性质:快速摸底用第一条路,正式成果用第二条路。给内部看的初判图,斜率加上一个粗略的显著性掩膜就够了;要写进正式报告、要被别人拿去引用的结果,老老实实做完整的检验。

还有一点必须提醒:逐像元做显著性检验会带来多重比较问题。几万个像元各做一次检验,即使全都没有真实趋势,按 5% 的显著性水平也会有一千多个像元"显著"。这不是数据的问题,是统计方法的问题。处理办法要么是做多重检验校正,要么是在解读时明确说明这一点,别把零星显著的像元当成整体趋势的证据。

4.3 距平百分比与干湿年识别

绝对距平(毫米)和相对距平(百分比)传达的信息不一样,两个都该算。

绝对距平告诉你"多了多少毫米",适合做水量平衡相关的讨论;相对距平告诉你"多了几成",适合做干湿等级划分。计算方法很直接:

var pctAnomaly = annualColl.map(function(img){ return img.subtract(climMean) .divide(climMean) .multiply(100) .set('system:time_start', img.get('system:time_start')); });

得到百分比距平后,按阈值划等级。常见的划分方式是:距平在 -10% 到 +10% 之间算正常,-25% 到 -10% 算偏干,低于 -25% 算干旱,对称的另一侧算偏湿和湿润。这套阈值不是金标准,不同气候区差异很大,干旱区对 10% 的变化可能毫无感觉,湿润区 10% 已经是明显异常。所以我一般会先画一条区域平均的距平时间曲线,看看实际波动幅度,再定阈值,而不是套用一个固定标准。

识别干湿年还有一个更严谨的做法是算标准化降水指数(SPI)。它需要对降水序列拟合一个概率分布(通常是 Gamma 分布),再转成标准正态分位数。GEE 里做这个比较别扭,因为分布拟合需要逐像元迭代,计算量大。更实际的做法是把月度序列导出到本地,用成熟的时间序列库算 SPI,再导回 GEE 或者直接在本地出图。

4.4 出图时的配色、图例与边界叠加

图好看不好看,某种程度上决定了结论能不能被接受,这不是虚的。

降水分布图的配色有一个基本原则:用单调渐变色,别用彩虹色。彩虹色在视觉上会制造出不存在的边界,红色和黄色之间的过渡区看起来像是有一个突变,但实际上数值是连续变化的。我一般用浅黄到深蓝的渐变,符合直觉——蓝色代表水,越深雨越多。

距平图的配色规则不一样,必须以零为中心做双向渐变。负值往红或褐走,正值往蓝或绿走,中间白色在零附近。这样一眼就能看出哪里偏干哪里偏湿。如果用单向渐变,正值和负值的视觉区分度会完全丧失。

图例的处理也有讲究。自动生成的图例往往刻度不合理,最好手动指定min、max和palette的断点。降水的max建议设成研究区实际最大值的 1.2 倍左右,太低会把大片区域压成同一个颜色,太高又会显得整体偏淡。

最后是边界叠加。用ee.Image.paint或者把区域边界作为FeatureCollection加进去,线宽别超过 2 像素,颜色用深灰或黑色,透明度调一点。边界太粗太黑会盖住下面的降水信息,看起来像是只有边界没有数据。

5. 几个真实踩过的坑和排查链路

这一段是整篇最想写的部分。前面讲的都是"应该怎么做",这里讲"做的时候会发生什么"。坑之所以是坑,是因为它们大多不报错,只是安静地给你一个错误的答案。

5.1 统计结果出现几万毫米的负值

第一次遇到这个问题是在算某个区域的年均降水,导出的表格里有一行显示 -9999。我当时以为是累加逻辑写错了,查了半天代码,最后发现是数据本身带的标记值。

CHIRPS 原始产品里用 -9999 表示缺测,虽然 GEE 上的资产在多数情况下已经做过处理,但在某些版本、某些边缘时段,这个值仍然会漏出来。它一旦进入mean()或者sum(),会把整片区域的结果拉成负数,而且因为它绝对值极大,会彻底主导统计结果。

排查链路是这样的:先打印区域内的最小值。跑一句img.reduceRegion({reducer: ee.Reducer.min(), ...}),如果看到 -9999 或者接近这个量级的负数,基本就锁定了。修复方式是在统计前加一道掩膜:

var clean = img.updateMask(img.gt(-100));

阈值取 -100 是个经验值,因为真实降水不会是负数,任何大于 -100 的负值都极其可疑,统一掩掉最省事。掩膜之后再算统计,结果就正常了。

这个坑的隐蔽之处在于,它不会让脚本报错,只会让结果错。如果没有做合理性检查(比如"年降水量应该在 0 到 5000 毫米之间"),很可能就这么错着用下去了。

5.2 边界跨出覆盖范围后的空结果

第二次踩坑是在处理一个北部区域的边界。脚本跑完没报错,但导出的属性表里降水字段全是 null。

排查的第一步是确认时间过滤对不对——打印集合的size(),发现有正常数量的影像,说明时间没问题。第二步是打印roi.intersects(first.geometry()),返回 false。到这里原因就清楚了:研究区完全落在了 50°N 以北,压根不在 CHIRPS 的覆盖带内。

如果是部分重叠,问题会更隐蔽——重叠那部分有值,不重叠那部分返回 null,最后reduceRegion算出来的均值只反映重叠区域,而你以为是整个区域的平均。这种情况我的处理方式是先算一次覆盖面积占比,占比低于某个阈值(比如 90%)就直接放弃这个数据集,改用其他产品,因为残缺的统计结果比没有结果更危险。

5.3 导出任务跑到一半失败

导出失败最常见的原因是maxPixels超限或者内存溢出。任务面板里会显示错误信息,通常包含 "User memory limit exceeded" 或者 "Computed value too large"。

前者说明单个计算任务的数据量太大,解决办法是提高tileScale,或者把时间范围拆成几段分别导出再合并。后者通常是输出分辨率设得太细——比如把三十年的日数据按 30 米分辨率导出,那确实撑不住。

还有一个不那么直观的原因:导出的几何对象太复杂。行政区边界如果有几万个顶点,reduceRegions在逐个要素处理时会非常吃力。解决办法是先做一次简化,用geometry.simplify(100)把顶点数降到合理范围。精度损失在降水统计这个场景里完全可以接受,因为一个像元就是五公里多,边界简化个一百米根本不影响结果。

5.4 不同 scale 下同一区域结果对不上

这个坑最让人困惑,因为两次计算用的是同一份数据、同一个区域、同一段时间,唯一的差别是scale参数,结果却差了百分之十几。

原因在于重采样的方式。GEE 在做区域统计时,会按你指定的scale把像元重新聚合。scale设得比原始分辨率大时,一个统计单元里会包含多个原始像元,取的是面积加权平均;设得比原始分辨率小时,会做插值重采样,可能产生原始数据里不存在的值。两种情况下得到的结果本来就不该完全一致。

我的处理原则是:同一个项目里,所有统计的scale必须统一,并且尽量贴近原始分辨率。如果需要和其他来源的数据对比,先确认对方的尺度,再决定是否要重采样对齐。另外,如果同一区域两次计算结果差异超过 5%,值得检查一下是不是某次漏了clip或者filterBounds,把区域外的像元算进来了。

6. 把一次性脚本沉淀成可复用工具

写到这里,单次分析的链路已经完整了。但实际项目里很少只分析一个区域一段时间,更多情况是几十个行政区、好几个时段、反复调整参数。这时候把逻辑封装起来,收益非常明显。

6.1 参数化降水统计函数的结构

一个好的封装应该把变化的部分抽成参数,把不变的部分固定下来。降水统计里变化的部分无非是:区域、时间范围、聚合粒度(月/年/季)、统计量(均值/总和/最大)。

function precipStats(roi, startDate, endDate, granularity, reducer){ var daily = ee.ImageCollection('UCSB-CHG/CHIRPS/DAILY') .filterBounds(roi) .filterDate(startDate, endDate) .select('precipitation') .map(function(img){ return img.updateMask(img.gt(-100)).clip(roi); }); var step = granularity === 'month' ? 1 : 12; var count = granularity === 'month' ? ee.Date(endDate).difference(ee.Date(startDate), 'month') : ee.Date(endDate).difference(ee.Date(startDate), 'year'); // 按步长聚合,返回带 time_start 的集合 // ... }

封装的时候有个细节值得坚持:函数内部一定要保留掩膜和裁剪。这两步是数据清洗的底线,不该交给调用方去记得。我见过太多因为复用时漏了updateMask而导致结果异常的案例。

6.2 多区域批处理与任务队列管理

区域多起来之后,导出任务的排队管理就成了问题。GEE 对同时运行的任务数量有上限,一次性提交几十个任务,后面的一直在排队,稍微出点错就全乱。

我的做法是分批次提交,每批十个左右,等前一批跑完再提交下一批。代码上可以用一个简单的循环加延迟,或者干脆手动分几次运行。听上去原始,但实际比写复杂的队列管理逻辑更可靠。

另一个技巧是先把结果合并成一个大的 FeatureCollection 再一次性导出,而不是每个区域导一个文件。这样任务数量少,后续在本地合并表格的工作量也小。代价是单个任务的数据量大,需要用tileScale来压住内存。

6.3 结果自检:抽样比对与合理性校验

最后说自检,这是最容易被跳过、也最不该跳过的一步。

我固定会做的三项检查:一是量级检查,年降水总量的空间均值是否落在合理区间;二是极值检查,最大值和最小值是否出现明显不可能的数值(比如负值或者超过五千毫米的日值);三是抽样比对,挑三五个区域,把 GEE 算出来的结果和公开的统计资料或者实测站数据对一下,偏差在可接受范围内才敢往下走。

第三项里,抽样点的选择有讲究。不要只挑数据质量最好的平原站,那等于自欺欺人。刻意挑一两个山区站、一两个边界附近的站,这些才是问题最容易暴露的地方。我在一次项目里就是因为只比对了平原站,结果交付后才发现山区区域的系统性低估,返工重做了整条链路。

提示:把自检的检查项写成固定的脚本片段存下来,每次新项目直接复用。检查这件事,靠自觉往往靠不住,靠工具才靠得住。

最后分享一个我自己的习惯:每次做完一个区域的完整分析,我会把整条链路(加载 → 掩膜 → 聚合 → 统计 → 导出)保存成一个模板脚本,参数全部提到开头,下次换区域只改开头那几行。这么做的直接好处是,下次遇到问题的时候,你能确定问题不出在链路本身,而是出在参数或者数据上,排查范围一下子就缩小了。踩过几次因为复用旧脚本却忘了改时间范围导致的错误之后,我对这套模板的依赖越来越重,也确实省下了大量重复劳动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询