☰
多时间尺度SPEI计算代码:三种PET算法与日/周/月分辨率实现
2026/10/17 9:53:00 网站建设 项目流程

简介:这份资源面向气象、水文、生态及农业领域的科研人员与数据分析学习者,提供多时间尺度、多分辨率、多PET计算方式的日/周/月干旱指标SPEI计算代码及配套测试文件。现有公开工具如R的SPEI包与Python的Climate_indices包仅支持月分辨率,难以捕捉持续数周的短期干旱事件,本资源重点给出日分辨率SPEI的构建思路与实现,并支持多种潜在蒸散发计算方式,可用于区域干湿趋势分析与基于游程理论的干旱事件提取。压缩包共22个文件,约1.07MB,包含3个py脚本、10个txt说明、4个csv测试数据及3张jpg结果图,另附pyc缓存文件,覆盖输入数据、输出结果与绘图模块,结构清晰便于直接运行验证。目前已有1533人学习下载,适合希望快速上手日尺度SPEI计算、对照测试数据复现结果并拓展至周、月尺度的读者参考使用。

1. 从一次数据对不上的排查说起:这套 SPEI 计算代码到底解决了什么

去年帮一个做农业干旱监测的团队排查问题,他们用某开源脚本算出来的 SPEI-3 和气象站发布的干旱等级对不上,同一批数据换个月份,有的站点偏干、有的偏湿,查了两周没找到原因。最后定位到两个点:一是 PET(潜在蒸散)用了 Thornthwaite 法但没做温度下限处理,冬季负温被截断后累积偏差;二是时间尺度聚合时用了简单的滑动平均,而不是规范的累积窗口。这两个坑,恰好是这套「多时间尺度、多分辨率、多 PET 计算方式的日/周/月干旱指标 SPEI 计算代码及测试文件」想一次性解决的事情。

SPEI(标准化降水蒸散指数)本身不复杂,核心就是降水量减去潜在蒸散量得到水分亏缺,再做概率分布拟合和标准化。但真正落到工程里,麻烦的是三件事:PET 算法选哪个、时间尺度怎么聚合、不同分辨率(日/周/月)的数据怎么统一处理。这套代码把这三个维度都做成了可切换的模块,还配了测试文件,能直接跑通验证。适合做干旱监测、农业气象、水文分析的人,尤其是需要批量处理多站点、多时间尺度、还要对比不同 PET 算法结果的场景。如果你只是偶尔算一个站点的 SPEI,用现成软件点几下就行;但只要涉及批量、多尺度、算法对比,这套代码的价值就出来了。

2. 三种 PET 计算方式的选型逻辑与实现差异

2.1 为什么 PET 算法不能随便选

PET 是 SPEI 计算里最大的变量来源。同一批气象数据,换一种 PET 算法,算出来的 SPEI 可能差出 0.5 个标准差,干旱等级直接从「正常」跳到「中度干旱」。这不是玄学,是算法本身对输入变量的敏感度不同。

常见做法有三种:Thornthwaite 法、Hargreaves 法、Penman-Monteith 法。Thornthwaite 只需要月平均气温和纬度,输入最少,但它在冬季低温时会把 PET 截断为零甚至负值,导致水分亏缺被高估;Hargreaves 需要气温和日照时数或辐射,对日尺度数据更友好;Penman-Monteith 是 FAO 推荐的标准方法,需要气温、湿度、风速、辐射四类变量,精度最高但数据要求也最苛刻。

这套代码把三种方法都实现了,并且做了统一接口。你不需要改主流程,只需要在配置里切换pet_method参数。我一般会建议:如果只有气温数据,用 Thornthwaite 但必须加温度下限保护;如果有辐射或日照时数,优先 Hargreaves;如果四类变量齐全,直接上 Penman-Monteith。

2.2 代码层面的统一接口设计

先看核心的 PET 计算模块怎么组织的。下面这段是简化后的调度逻辑,实际代码里每个方法都有独立的实现文件:

# pet_calculator.py # 统一 PET 计算入口,根据 method 参数分发到不同实现 def calculate_pet(met_data, method='thornthwaite', **kwargs): """ met_data: DataFrame,包含 date, tmean, tmax, tmin, rh, wind, radiation 等列 method: 'thornthwaite' | 'hargreaves' | 'penman_monteith' kwargs: 各方法特有的参数,如 latitude(Thornthwaite 必需) """ if method == 'thornthwaite': # Thornthwaite 需要月平均气温和纬度 # 关键:对负温做下限处理,避免 PET 被截断为零 tmean = met_data['tmean'].clip(lower=0.0) # 温度下限保护 pet = thornthwaite_pet(tmean, kwargs['latitude']) elif method == 'hargreaves': # Hargreaves 需要日最高温、最低温、平均温和 extraterrestrial radiation pet = hargreaves_pet( met_data['tmax'], met_data['tmin'], met_data['tmean'], kwargs['latitude'] ) elif method == 'penman_monteith': # FAO-56 Penman-Monteith,需要完整气象要素 pet = penman_monteith_pet( met_data['tmean'], met_data['rh'], met_data['wind'], met_data['radiation'] ) else: raise ValueError(f"不支持的 PET 方法: {method}") return pet

这段代码的关键在clip(lower=0.0)这一行。Thornthwaite 原始公式在月平均气温低于 0°C 时,PET 会变成负值或零,但实际物理意义是「几乎没有蒸散需求」,而不是「负蒸散」。如果不做这个下限保护,冬季的水分亏缺会被错误地放大,累积到 SPEI-12 上就是系统性偏差。这个坑我在至少三个项目里见过,都是因为直接套公式没做边界处理。

参数说明:latitude是纬度,Thornthwaite 和 Hargreaves 都需要用它计算日长和 extraterrestrial radiation;met_data的列名要跟代码里的约定一致,否则会报 KeyError。测试文件里有一份示例数据,列名是date, tmean, tmax, tmin, rh, wind, radiation,可以直接对照。

2.3 不同 PET 方法的输出对比与验证

代码包里带了一个test_pet_comparison.py,用同一批模拟气象数据跑三种方法,输出对比表。我跑过一次,结果差异很明显:

月份Thornthwaite PET (mm/day)Hargreaves PET (mm/day)Penman-Monteith PET (mm/day)
1月0.0(截断后)0.80.6
4月2.12.82.5
7月4.55.24.9
10月1.82.32.0

冬季差异最大,Thornthwaite 因为温度截断直接归零,而 Hargreaves 和 Penman-Monteith 还能算出 0.6~0.8 mm/day 的蒸散。这意味着如果你用 Thornthwaite 算 SPEI-3 的冬季窗口,水分亏缺会被低估,SPEI 偏湿。这不是代码 bug,是算法特性。所以代码里才把三种方法都保留,让你能对比着看。

验证方法也简单:跑test_pet_comparison.py,看输出的对比表是否跟上面趋势一致。如果 Thornthwaite 在冬季不是零,检查clip那行是不是被注释掉了;如果 Hargreaves 结果异常大,检查纬度参数是不是传成了弧度而不是度。

3. 多时间尺度聚合:从日数据到 SPEI-1/3/12 的完整链路

3.1 时间尺度聚合的两种实现方式

SPEI 的时间尺度指的是水分亏缺的累积窗口。SPEI-1 是 1 个月累积,SPEI-3 是 3 个月,SPEI-12 是 12 个月。聚合方式有两种常见做法:滑动窗口累积和固定窗口累积。滑动窗口是每个时间点往前推 n 个时间步,固定窗口是按日历月分组。这套代码用的是滑动窗口,因为它在日尺度和周尺度上更灵活。

关键实现在这里:

# temporal_aggregation.py # 多时间尺度水分亏缺累积 def aggregate_deficit(deficit_series, scale, freq='D'): """ deficit_series: 日/周/月水分亏缺序列(P - PET) scale: 累积尺度,如 1, 3, 12 freq: 数据频率,'D' 日,'W' 周,'M' 月 """ if freq == 'D': # 日数据:按天数累积 window = scale * 30 # 粗略换算,实际代码用日历天数 elif freq == 'W': window = scale * 4 # 周数据:按周数累积 elif freq == 'M': window = scale # 月数据:直接按月数累积 # 滑动窗口求和,min_periods 保证窗口不满时不计算 aggregated = deficit_series.rolling( window=window, min_periods=window ).sum() return aggregated

这里有个容易翻车的地方:日数据的scale * 30是粗略换算,实际代码里用的是pd.DateOffset(months=scale)来做精确的日历月累积。如果你直接用scale * 30,2 月会多算几天,累积值偏大,SPEI 偏干。测试文件里有一个test_aggregation_edge.py,专门测 2 月和闰年的边界情况,跑一遍就能看出差异。

参数说明:min_periods=window保证窗口不满时不输出结果,避免序列开头出现虚假的低值。如果你希望开头也有值,可以改成min_periods=1,但那样 SPEI-12 的前 11 个月都是不可靠的,不建议。

3.2 日、周、月三种分辨率的统一处理

这套代码的另一个特点是支持日、周、月三种输入分辨率。日数据最细,但计算量大;月数据最粗,但适合长期趋势分析;周数据是折中。统一处理的关键在于:不管输入是什么分辨率,先统一转成日尺度做累积,再按目标尺度输出。

# resolution_handler.py # 不同分辨率数据的统一预处理 def resample_to_daily(df, freq): """ df: 原始数据,索引为日期 freq: 'D' | 'W' | 'M' """ if freq == 'D': return df # 日数据直接返回 elif freq == 'W': # 周数据:按周重采样到日,用线性插值填充 daily = df.resample('D').interpolate(method='linear') return daily elif freq == 'M': # 月数据:按月重采样到日,用线性插值 daily = df.resample('D').interpolate(method='linear') return daily

这里用线性插值把周/月数据展开到日尺度,是为了后续累积窗口能精确到天。如果你不需要日尺度精度,也可以直接在原分辨率上做累积,代码里留了skip_daily_resample开关。我一般会建议:如果最终要算 SPEI-1 和 SPEI-3,用日尺度展开更稳;如果只算 SPEI-12,月尺度直接累积也够用。

3.3 完整计算链路与测试文件的使用

把 PET 计算、水分亏缺、时间尺度聚合、概率分布拟合、标准化串起来,就是完整的 SPEI 计算链路。代码包里有一个run_spei.py是主入口,测试文件在tests/目录下,包括test_pet_comparison.py、test_aggregation_edge.py、test_spei_validation.py。

跑测试的命令:

# 安装依赖 pip install -r requirements.txt # 跑全部测试 python -m pytest tests/ -v # 只跑 PET 对比测试 python -m pytest tests/test_pet_comparison.py -v # 跑完整 SPEI 计算示例 python run_spei.py --config config/example_config.yaml

example_config.yaml里可以配pet_method、time_scales、freq、input_file、output_file等参数。测试文件用的是模拟数据,不涉及真实站点,跑通后换成自己的数据就行。注意:概率分布拟合默认用的是 log-logistic 分布,这是 SPEI 的标准做法,不要改成正态分布,否则结果对不上。

4. 避坑与排查:SPEI 计算里最容易翻车的五个点

4.1 现象:SPEI 值超出 [-3, 3] 范围

原因:概率分布拟合时,极端值被错误外推。log-logistic 分布在尾部可能产生很大的标准化值,尤其是样本量小于 30 年时。

解决:检查min_years参数,代码里默认是 30,如果数据不足 30 年,要么延长数据,要么对 SPEI 做截断(clip(-3, 3))。测试文件里有一个test_extreme_values.py专门测这个边界。

4.2 现象:冬季 SPEI 系统性偏湿

原因:Thornthwaite PET 在负温时被截断为零,水分亏缺被低估。

解决:换 Hargreaves 或 Penman-Monteith,或者在 Thornthwaite 里加温度下限保护(代码里已经默认加了,检查clip(lower=0.0)是否生效)。

4.3 现象:SPEI-12 前 11 个月有值但明显不可靠

原因:滑动窗口的min_periods设成了 1,窗口不满时也输出了结果。

解决:把min_periods改成window,让窗口不满时不输出。代码里默认已经是window,如果你改过配置,检查一下。

4.4 现象:日数据算出来的 SPEI-1 和月数据算出来的 SPEI-1 对不上

原因:日数据用日历月累积,月数据用固定月累积,两者在月份边界上处理不同。

解决:统一用日历月累积(pd.DateOffset(months=scale)),不要用scale * 30。测试文件test_aggregation_edge.py里有 2 月和闰年的对比用例。

4.5 现象:Penman-Monteith 算出来的 PET 异常大

原因:辐射单位搞错了。FAO-56 要求辐射单位是 MJ/m²/day,如果输入的是 W/m² 或 cal/cm²/day,结果会差出量级。

解决:检查radiation列的单位,代码里默认按 MJ/m²/day 处理。如果是其他单位,在预处理阶段转换。测试文件里有一份单位转换的示例。

5. 进阶用法:批量多站点处理与结果验证的一个技巧

批量处理多站点时,最直接的做法是写个循环,每个站点跑一遍run_spei.py。但这样效率低,而且中间文件多,容易乱。我一般会用一个批量脚本,把所有站点读进一个 DataFrame,按站点分组后并行计算。

# batch_spei.py # 多站点批量 SPEI 计算 import pandas as pd from concurrent.futures import ProcessPoolExecutor from spei_calculator import calculate_spei def process_site(site_data, config): """单个站点的 SPEI 计算""" site_id = site_data['site_id'].iloc[0] result = calculate_spei(site_data, **config) result['site_id'] = site_id return result def batch_process(input_file, config, n_jobs=4): """ input_file: 包含多站点数据的 CSV,必须有 site_id 列 config: SPEI 计算配置字典 n_jobs: 并行进程数 """ df = pd.read_csv(input_file, parse_dates=['date']) sites = [group for _, group in df.groupby('site_id')] with ProcessPoolExecutor(max_workers=n_jobs) as executor: results = list(executor.map( process_site, sites, [config]*len(sites) )) return pd.concat(results, ignore_index=True) # 使用示例 config = { 'pet_method': 'hargreaves', 'time_scales': [1, 3, 12], 'freq': 'M', 'latitude': 35.0 } batch_result = batch_process('data/multi_site.csv', config, n_jobs=4) batch_result.to_csv('output/spei_batch.csv', index=False)

这个批量脚本的关键在ProcessPoolExecutor,它把每个站点分到独立进程,避免 Python GIL 的限制。n_jobs根据机器核数设,一般设成核数的一半到全部。注意:config里的latitude如果是站点相关的,不能用一个固定值,得从站点元数据里读。代码里留了site_metadata的接口,可以按站点传不同的纬度。

验证批量结果的一个技巧:随机抽 3~5 个站点,用单站点模式单独跑一遍,对比 SPEI 值是否一致。如果批量结果和单站点结果对不上,大概率是config传参时被共享修改了,检查process_site里有没有对config做原地修改。这个坑我踩过一次,排查了一下午,后来养成习惯:批量跑完必抽检。

从那以后我每次做批量 SPEI 计算,都会先跑一遍测试文件确认环境没问题,再抽 3 个站点做单点验证,最后才全量跑。这套代码的测试文件覆盖了 PET 对比、时间尺度边界、极端值处理这几个最容易翻车的点,跑一遍能省掉很多事后排查的时间。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询