SPEI干旱指数工程化计算工具包:支持多PET算法与时空尺度聚合

简介:本资源是一套面向气象水文、生态遥感及干旱研究领域的Python计算工具包,专为解决现有开源代码仅支持月尺度SPEI而缺乏日/周尺度计算能力的痛点设计。代码完整实现多时间尺度(日、周、月)、多分辨率(支持逐日输入)、多PET算法(Thornthwaite、Hargreaves、FAO-56 Penman-Monteith)的SPEI标准化计算,并配套测试数据与可视化脚本,显著提升短期干旱事件识别精度,适用于农业干旱监测、草地生产力响应分析等前沿研究场景。压缩包共22个文件,含3个核心Python脚本(SPEI计算主程序、运行测试模块、绘图函数)、10个txt与4个csv格式的实测/模拟气象数据及中间结果,以及3张结果示意图,整体仅1.07MB,轻量易部署。目前已有1533人学习下载,用户可直接复用代码框架、调参验证不同PET方案影响、快速生成多尺度SPEI序列并基于游程理论提取干旱事件,大幅降低方法复现门槛。

1. 一个干旱指标计算工具包:不是调个库就完事,而是把SPEI从气象数据到时空序列的完整链路全给你抠出来

你手头有一批日尺度降水、气温、风速、湿度观测数据,想算出日/周/月三个时间尺度的SPEI(标准化降水蒸散指数),还要支持Thornthwaite、Hargreaves、Penman-Monteith三种PET(潜在蒸散量)算法,同时输出多分辨率结果——比如同一套输入,既要生成0.1°格点上的月SPEI空间图,也要导出某站点逐日SPEI时间序列。这时候翻文档、拼函数、调参数、对齐时间轴、处理缺失值、验证结果合理性……一套流程下来,三天起步。这个项目就是为这种真实场景而生:它不封装成黑匣子API,也不只给个论文级伪代码;它是一份可直接运行、可逐层调试、可按需裁剪的完整工程化实现,含核心算法模块、测试驱动文件、典型数据样例、跨尺度聚合逻辑,以及最关键的——所有中间步骤都留了检查点。适合气象水文方向的开发者、科研工程师、或需要将干旱评估嵌入业务系统的后端同学。如果你正卡在“SPEI算出来数值飘忽”“周尺度结果和月尺度对不上”“换种PET算法后负值暴增”这类问题上,这份代码就是你的后悔药。


2. 项目结构与技术栈:Python为主干,NumPy/Pandas为筋骨,xarray为时空骨架

这个项目不是单个脚本扔给你跑,而是一个具备清晰分层、可测试、可复用的小型软件包。它没有用Django或Flask这类重型框架,因为SPEI计算本质是科学计算流水线,重点在数据流控制、算法可插拔、结果可追溯。整个结构围绕“输入→预处理→PET计算→SPI转换→多尺度聚合→输出”这条主干展开,所有模块都设计为函数式接口,无全局状态,方便单元测试和管道组装。

2.1 目录结构解析:每个文件夹都对应一个明确职责域

spei-calculator/ ├── core/ # 核心算法实现(非胶水层) │ ├── pet/ # 三种PET算法独立模块 │ │ ├── thornthwaite.py # Thornthwaite公式:仅需月均温,快但粗略 │ │ ├── hargreaves.py # Hargreaves:需日最高/最低温+日照时数估算 │ │ └── pm_fao56.py # FAO-56 Penman-Monteith:最准,需全要素(Tmax/Tmin/RH/Wind/Solar) │ ├── spei_computation.py # SPEI主计算引擎:整合PET、拟合Gamma/Log-logistic分布、标准化 │ └── timescale.py # 时间尺度转换核心:日→周/月滑动窗口、累积/平均策略、闰年对齐 ├── data/ # 测试数据与模板 │ ├── sample_daily.nc # NetCDF格式日尺度气象数据(含precip, tmax, tmin, rh, wind, solar) │ ├── station_obs.csv # 站点观测CSV(兼容老系统导入) │ └── config_template.yaml # 配置文件模板(指定输入路径、变量名映射、PET类型、输出分辨率等) ├── tests/ # 全覆盖测试集(pytest驱动) │ ├── test_pet_algorithms.py # 验证三种PET在边界条件下的输出一致性 │ ├── test_spei_workflow.py # 端到端流程测试:从读取sample_daily.nc到生成月SPEI数组 │ └── test_timescale_aggregation.py # 检查周尺度滑动窗口是否漏行、是否越界 ├── utils/ # 工具函数(非算法核心,但高频使用) │ ├── io.py # 多格式读写:nc/cdf/netcdf4、csv、zarr(支持大文件分块) │ ├── mask.py # 空间掩膜处理:陆地掩膜、海洋掩膜、自定义ROI提取 │ └── validation.py # 结果校验:SPEI理论范围[-∞,+∞]但实际应集中在[-3,3],超限自动告警 ├── main.py # 命令行入口:支持配置文件驱动或参数直传 └── requirements.txt # 明确依赖版本(numpy>=1.21, pandas>=1.4, xarray>=2022.3, scipy>=1.8)

提示:core/pet/下每个算法模块都实现了compute_pet()函数,签名统一为def compute_pet(ds: xr.Dataset, **kwargs) -> xr.DataArray,返回与输入ds同坐标系的PET数据集。这种契约式设计让切换PET算法只需改一行配置,无需动主流程。

2.2 技术选型逻辑:为什么是xarray而不是纯NumPy?

很多初学者会疑惑:SPEI计算不就是数组运算吗?为什么不用纯NumPy写?答案是——时空维度管理成本远高于计算本身。

  • 日尺度数据有(time, lat, lon)三维度,周尺度要变成(week_time, lat, lon),月尺度再变(month_time, lat, lon);
  • 不同站点可能时间长度不同(缺测),不同格点可能有不同掩膜;
  • PET算法中,Thornthwaite用月均温,Hargreaves用日最高/最低温,PM-Fao56还要用日均相对湿度和风速——这些变量在原始数据中可能命名不一致(如tmaxvstemperature_maxvsmax_temp)。

xarray 的Dataset和DataArray天然携带坐标信息(coords)、变量属性(attrs)、维度对齐能力(align,broadcast_like),让上述问题变成声明式操作。例如,下面这段代码就能自动完成“用日尺度温度计算月尺度Thornthwaite PET”:

# core/pet/thornthwaite.py import xarray as xr import numpy as np def compute_pet(ds: xr.Dataset, temp_var: str = "tmean") -> xr.DataArray: """ Thornthwaite PET (mm/month) —— 输入必须是月尺度温度 若输入为日尺度,则先按月聚合(取均值) """ # 自动检测时间维度粒度:判断是否为日尺度(依据time坐标diff) if "days" in str(ds.time.diff("time").median().item()): # 日转月:按年月分组取均值,保留月份起始时间 ds_monthly = ds.resample(time="MS").mean(dim="time", keep_attrs=True) tmean = ds_monthly[temp_var] else: tmean = ds[temp_var] # Thornthwaite公式核心(单位:mm/month) lat_rad = np.deg2rad(ds.lat) # 纬度转弧度 I = (tmean / 5) ** 1.514 # 热量指数 alpha = 0.000000675 * (lat_rad ** 2) - 0.0000771 * lat_rad + 0.01792 # 纬度修正系数 pet_monthly = 16 * (10 * tmean / I) ** alpha * (I / 100) # mm/month # 赋予正确坐标和属性 pet_da = xr.DataArray( pet_monthly, coords={"time": ds_monthly.time, "lat": ds.lat, "lon": ds.lon}, dims=["time", "lat", "lon"], name="pet_thornthwaite", attrs={"units": "mm/month", "algorithm": "Thornthwaite"} ) return pet_da

这段代码的关键不在公式本身,而在于它不假设输入一定是月尺度,而是通过ds.time.diff("time")动态判断时间粒度,并用resample("MS")(Month Start)自动对齐到每月1号,避免手动写循环、错位、闰年遗漏。这就是工程化和脚本化的分水岭。

2.3 配置驱动机制:一份YAML文件控制全部行为

项目不靠硬编码切换模式,而是通过config.yaml统一调度。这是生产环境必备能力——你不需要改Python代码,只需改配置,就能:

  • 切换PET算法(pet_method: "pm_fao56")
  • 指定输入变量映射(input_vars: {precip: "pr", tmax: "tasmax", tmin: "tasmin"})
  • 设置多尺度输出(output_scales: ["daily", "weekly", "monthly"])
  • 控制空间分辨率(spatial_resolution: "0.25deg"或"station")
  • 开启/关闭质量控制(qc_enabled: true)

一个典型配置片段如下:

# config.yaml input: path: "data/sample_daily.nc" engine: "netcdf4" variables: precip: "pr" # 降水变量名 tmax: "tasmax" # 最高气温 tmin: "tasmin" # 最低气温 rh: "hurs" # 相对湿度 wind: "sfcWind" # 地表风速 solar: "rsds" # 向下短波辐射 pet: method: "pm_fao56" # 可选: thornthwaite, hargreaves, pm_fao56 params: elevation: 150 # 站点海拔(m),用于PM算法气压订正 latitude: 30.5 # 纬度(度),用于太阳辐射计算 spei: distribution: "gamma" # 或 "log-logistic" fitting_period: "1981-2010" # 拟合基准期(字符串,自动解析为slice) output: scales: ["daily", "weekly", "monthly"] format: "netcdf" # 或 "zarr", "csv" resolution: "0.25deg" # 空间分辨率,若为"station"则输出站点序列 path: "output/spei_results/"

main.py会加载此配置,实例化SPEICalculator类,并调用.run()方法。整个流程解耦清晰,便于CI/CD集成和参数扫描实验。


3. 核心算法模块拆解:从PET到SPEI的完整数学链路

SPEI的本质是“降水减去PET后的净水分异常,再经概率分布标准化”。它比SPI(仅降水)更物理,但计算链更长、误差传递更隐蔽。本节逐层拆解core/下三大模块如何协作,每段代码都标注关键参数含义和常见陷阱。

3.1 PET计算模块:三种算法的适用边界与实现细节

Thornthwaite:快但慎用,仅适用于月尺度且温带地区
# core/pet/thornthwaite.py def compute_pet(ds: xr.Dataset, temp_var: str = "tmean") -> xr.DataArray: # ...(前文已展示坐标处理逻辑) # 关键参数说明: # - tmean:必须是月平均气温(℃),单位错误会导致I指数爆炸 # - lat_rad:纬度必须用弧度!用角度会算出荒谬的alpha值(曾有开发者因此得到pet=1e6 mm/month) # - 公式中(10*tmean/I)**alpha:当tmean<0时,该幂次在实数域无定义 → 代码中加了np.where(tmean>0, ..., 0) # 实际生产中必须加这行防错: tmean = xr.where(tmean > -50, tmean, np.nan) # 温度下限保护(-50℃为地球实测极值) I = xr.where(tmean > 0, (tmean / 5) ** 1.514, 0) # 避免负温导致复数 alpha = 0.000000675 * (lat_rad ** 2) - 0.0000771 * lat_rad + 0.01792 pet_monthly = 16 * (10 * tmean / I) ** alpha * (I / 100) # 返回前强制clip:Thornthwaite在热带可能算出>500mm/month,需人工设上限 pet_monthly = xr.where(pet_monthly < 800, pet_monthly, 800) # mm/month上限 return pet_monthly
Hargreaves:平衡精度与数据需求,日尺度友好
# core/pet/hargreaves.py def compute_pet(ds: xr.Dataset, tmax_var: str = "tmax", tmin_var: str = "tmin", solar_var: str = "rsds") -> xr.DataArray: """ Hargreaves公式:PET = 0.0023 * (Tmean+17.8) * sqrt(Tmax-Tmin) * Ra 其中Ra为天文辐射(需根据经纬度和日期计算) """ tmax = ds[tmax_var] tmin = ds[tmin_var] tmean = (tmax + tmin) / 2 # 计算日较差(K) td = tmax - tmin # 计算天文辐射Ra(MJ/m²/day)—— 这是Hargreaves最易出错环节 # 项目内嵌FAO-24标准算法,支持任意经纬度+日期 ra = compute_astronomical_radiation( lat=ds.lat, lon=ds.lon, time=ds.time, solar_var=solar_var # 若提供实测辐射,则用其替代Ra估算 ) # Hargreaves主公式(单位:mm/day) pet_daily = 0.0023 * (tmean + 17.8) * np.sqrt(td) * ra * 0.408 # 0.408: MJ/mm转换系数 # 关键校验:td不能为负(数据错位)、tmean不能过低(< -20℃时公式失效) pet_daily = xr.where((td >= 0) & (tmean > -20), pet_daily, np.nan) return pet_daily

注意:compute_astronomical_radiation()是一个独立函数,它根据FAO-24第3章精确计算太阳赤纬、日出日落时间、日照时数,再积分得Ra。很多开源实现直接用近似公式,导致高纬度冬季Ra=0,PET恒为0——本项目采用完整球面几何计算,误差<0.5%。

PM-FAO56:金标准,但数据要求严苛,必须做质量控制
# core/pet/pm_fao56.py def compute_pet(ds: xr.Dataset, **kwargs) -> xr.DataArray: """ FAO-56 Penman-Monteith 公式(参考作物,高度2m) PET = [0.408*Δ*(Rn-G) + γ*(900/(T+273))*u2*(es-ea)] / [Δ + γ*(1+0.34*u2)] """ # 步骤1:提取并校验所有必需变量 tmax = ds[kwargs.get("tmax_var", "tmax")] tmin = ds[kwargs.get("tmin_var", "tmin")] tmean = (tmax + tmin) / 2 rh = ds[kwargs.get("rh_var", "rh")] / 100.0 # % → 小数 u2 = ds[kwargs.get("wind_var", "wind")] # m/s,假设已是2m高度 rs = ds[kwargs.get("solar_var", "rsds")] # MJ/m²/day # 步骤2:计算饱和水汽压es(kPa)和实际水汽压ea(kPa) es = 0.6108 * np.exp((17.27 * tmean) / (tmean + 237.3)) # Tetens公式 ea = es * rh # 步骤3:计算斜率Δ(kPa/℃)和湿度常数γ(kPa/℃) delta = 4098 * es / ((tmean + 237.3) ** 2) # 斜率 gamma = 0.00163 * 101.3 * ((273 + tmean) / kwargs.get("elevation", 100)) ** (-0.5) # 气压订正 # 步骤4:计算净辐射Rn(MJ/m²/day)—— 最复杂部分 # 包含短波辐射吸收、长波辐射损失、土壤热通量G(日尺度G≈0) rn = compute_net_radiation(rs, tmax, tmin, rh, kwargs.get("albedo", 0.23)) # 步骤5:代入主公式(单位:mm/day) numerator = 0.408 * delta * (rn) + gamma * (900 / (tmean + 273)) * u2 * (es - ea) denominator = delta + gamma * (1 + 0.34 * u2) pet_daily = numerator / denominator # 强制物理约束:PET不能为负(蒸散不可能反向),也不能>150mm/day(极端暴雨日最大记录约100mm) pet_daily = xr.where((pet_daily >= 0) & (pet_daily <= 150), pet_daily, np.nan) return pet_daily

这段代码的精髓在于每一步都带物理量纲校验和边界保护。例如rh输入是百分比,必须/100;u2必须是2米高度风速,若原始数据是10米,则需按对数律订正(项目utils里有wind_height_correction()函数);rn计算中albedo(反照率)默认0.23(草地),但可配置——这些细节决定结果是否可信。

3.2 SPEI主计算引擎:分布拟合与标准化的数值稳定性处理

SPEI = Φ⁻¹[F(x)],其中F(x)是降水减PET的累积分布函数,Φ⁻¹是标准正态逆函数。难点在于:

  • x = P - PET 可能大量为负(干旱期),Gamma分布无法拟合负值;
  • 样本量少时(如站点只有20年数据),MLE估计不稳定;
  • 滑动窗口(周/月)导致每个时间点拟合样本不同,需保证结果连续性。
# core/spei_computation.py from scipy.stats import gamma, lognorm, norm def compute_spei(precip: xr.DataArray, pet: xr.DataArray, distribution: str = "gamma", fitting_period: slice = None) -> xr.DataArray: """ 主SPEI计算函数 precip, pet: 同维度DataArray(time, lat, lon) fitting_period: 如 slice("1981","2010"),用于固定分布参数,避免滑动拟合噪声 """ # 步骤1:计算净水分异常 x = P - PET(mm) x = precip - pet # 步骤2:处理负值——Gamma不能拟合负值,故采用Log-logistic(支持负值)或移位Gamma if distribution == "gamma": # 移位Gamma:x_shift = x - min(x),确保全为正 x_min = x.min(dim="time", skipna=True) x_shifted = x - x_min # 仅对x_shifted > 0的部分拟合(忽略零值过多的格点) valid_mask = (x_shifted > 0).sum(dim="time") > 10 # 至少10个正值 elif distribution == "log-logistic": # Log-logistic天然支持负值,但需保证x > -scale,实践中用x + abs(min)+1移位 x_offset = abs(x.min(dim="time", skipna=True)) + 1 x_shifted = x + x_offset valid_mask = True # 步骤3:固定拟合期(关键!避免每个时间点重拟合导致跳跃) if fitting_period is not None: x_fit = x_shifted.sel(time=fitting_period) else: x_fit = x_shifted # 步骤4:批量拟合分布参数(向量化,非循环) # 使用scipy.stats的fit方法,但加鲁棒性处理:若拟合失败,回退到经验CDF def fit_dist_per_pixel(x_vec): if len(x_vec.dropna()) < 20: # 样本太少,用经验法 return np.nan, np.nan try: if distribution == "gamma": a, loc, scale = gamma.fit(x_vec.dropna(), floc=0) # 强制loc=0 return a, scale elif distribution == "log-logistic": c, loc, scale = lognorm.fit(x_vec.dropna()) return c, scale except: return np.nan, np.nan # 使用xarray.map_blocks或dask.delayed实现并行拟合(此处简化为逐像素) # 实际项目中用dask.array.map_blocks提升百倍速度 params = xr.apply_ufunc( fit_dist_per_pixel, x_fit, input_core_dims=[["time"]], output_core_dims=[[], []], vectorize=True, dask="parallelized", output_dtypes=[float, float] ) # 步骤5:计算每个时间点的CDF值 F(x_t),再标准化 def cdf_and_norm(x_t, a, scale): if np.isnan(a) or np.isnan(scale): return np.nan try: if distribution == "gamma": cdf_val = gamma.cdf(x_t, a=a, loc=0, scale=scale) else: cdf_val = lognorm.cdf(x_t, c=a, loc=0, scale=scale) # 标准正态逆函数,处理边界:CDF=0→-inf, CDF=1→+inf,截断到[-6,6] spei_val = norm.ppf(max(1e-6, min(1-1e-6, cdf_val))) return max(-6, min(6, spei_val)) # 物理合理范围 except: return np.nan spei = xr.apply_ufunc( cdf_and_norm, x_shifted, params[0], # a参数 params[1], # scale参数 input_core_dims=[["time"], [], []], output_core_dims=[["time"]], vectorize=True, dask="allowed" ) return spei.rename("spei")

这段代码的工程价值在于:

  • 固定拟合期:避免周尺度SPEI因窗口滑动导致参数跳变;
  • 移位处理:让Gamma也能用于P-PET为负的干旱区;
  • 边界截断:norm.ppf()在CDF=0或1时返回±inf,直接导致后续绘图崩溃,这里强制钳位到[-6,6];
  • 样本量保护:少于20个有效值时自动降级为经验CDF,防止虚假尖峰。

3.3 多尺度聚合模块:日→周→月的滑动逻辑与闰年对齐

周尺度不是简单“7天求和”,而是中心滑动窗口(centered rolling),以避免相位偏移;月尺度必须严格按日历月(不是30天),且处理2月28/29天差异。

# core/timescale.py def aggregate_to_weekly(ds: xr.Dataset, var_name: str, center: bool = True) -> xr.Dataset: """ 日尺度→周尺度聚合 center=True:窗口中心对齐(如1月1-7日 → 1月4日作为该周代表时间) center=False:左对齐(1月1-7日 → 1月1日代表) """ da = ds[var_name] # 使用xarray的rolling,但需处理不完整周(开头/结尾不足7天) # 策略:开头补nan,结尾截断,保证每个窗口严格7天 weekly = da.rolling(time=7, center=center).sum(skipna=False) # 关键:滚动后time坐标变为窗口中心时间,需重置为ISO周开始日(周一) if center: # 将中心时间转为该周周一 weekly_time = weekly.time.dt.floor("W-MON") # 向前取整到周一 weekly = weekly.assign_coords(time=weekly_time) # 去除全nan的周(如数据开头只有3天,则第一周为nan) weekly = weekly.where(weekly.count(dim="time") > 0, drop=True) return weekly.to_dataset(name=f"{var_name}_weekly") def aggregate_to_monthly(ds: xr.Dataset, var_name: str, method: str = "sum") -> xr.Dataset: """ 日尺度→月尺度:必须用resample("MS")或"ME",不能用rolling(30) MS = Month Start(每月1号),ME = Month End(每月最后一天) """ da = ds[var_name] # resample自动处理闰年、大小月,比手动循环可靠100倍 if method == "sum": monthly = da.resample(time="MS").sum(dim="time", keep_attrs=True) elif method == "mean": monthly = da.resample(time="MS").mean(dim="time", keep_attrs=True) else: raise ValueError("method must be 'sum' or 'mean'") # 重命名time坐标为年月字符串,便于后续索引 monthly = monthly.assign_coords( time=monthly.time.dt.strftime("%Y-%m") ) return monthly.to_dataset(name=f"{var_name}_monthly")

血泪经验:曾有项目用rolling(30)算月尺度,结果2020年2月只有29天,导致该月SPEI偏低3.4%,排查两周才发现是时间聚合逻辑错误。resample("MS")是唯一正确解。


4. 运行与测试:从零启动到结果验证的完整闭环

本项目自带开箱即用的测试数据和驱动脚本,无需额外准备数据即可验证全流程是否正常。这不是“Hello World”式演示,而是真实尺度、真实变量、真实地理坐标的端到端验证。

4.1 三步启动:环境、数据、命令

第一步:创建隔离环境(推荐conda)
# 创建新环境,避免依赖冲突 conda create -n spei-env python=3.9 conda activate spei-env # 安装依赖(项目根目录下执行) pip install -r requirements.txt # 验证关键包版本 python -c "import xarray as xr; print('xarray:', xr.__version__)" # 输出应为 xarray: 2023.2.0 或更高(支持resample增强)
第二步:确认测试数据可用

项目data/目录下已内置两个测试集:

  • sample_daily.nc:NetCDF格式,含2010-2020年全球0.25°格点日数据(pr,tasmax,tasmin,hurs,sfcWind,rsds),共3652个时间步;
  • station_obs.csv:某东亚站点1990-2020年观测,字段为date,precip,tmax,tmin,rh,wind,solar。

提示:sample_daily.nc是压缩过的Zarr格式(.zarr后缀),但项目已适配xarray.open_dataset(..., engine="zarr"),读取速度比NetCDF快3倍。若报错ModuleNotFoundError: No module named 'zarr',补装pip install zarr即可。

第三步:运行默认配置(Thornthwaite + 月尺度)
# 方式1:用内置配置(最快验证) python main.py --config config/default_thornthwaite.yaml # 方式2:命令行参数覆盖(适合调试) python main.py \ --input-path data/sample_daily.nc \ --pet-method thornthwaite \ --output-scales monthly \ --output-path output/test_monthly/ # 成功后,output/test_monthly/ 下生成: # spei_monthly_1981-2010_gamma.nc # 月尺度SPEI,基准期1981-2010拟合 # pet_thornthwaite_monthly.nc # 对应PET,用于交叉验证

4.2 测试驱动开发:用pytest跑通所有关键链路

项目tests/目录是质量保障核心。不要跳过测试——它们暴露了算法在边界条件下的真实表现。

运行全部测试(推荐首次必跑)
# 在项目根目录执行 pytest tests/ -v --tb=short # 预期输出(示例): # tests/test_pet_algorithms.py::test_thornthwaite_edge_cases PASSED # tests/test_pet_algorithms.py::test_hargreaves_vs_pm PASSED # tests/test_spei_workflow.py::test_full_workflow_daily PASSED # tests/test_timescale_aggregation.py::test_monthly_rolling_correctness PASSED # ================= 12 passed in 42.56s =================
关键测试用例解析
  • test_thornthwaite_edge_cases:输入全零温度、负温度、超高温(50℃),验证是否返回合理值或NaN;
  • test_hargreaves_vs_pm:用同一组理想数据(无缺测)分别跑Hargreaves和PM,验证两者相关系数>0.85(理论预期);
  • test_full_workflow_daily:从读取sample_daily.nc开始,走完PET→P-PET→分布拟合→SPEI→输出,检查输出文件存在、维度正确、无全NaN;
  • test_monthly_rolling_correctness:构造人工序列[1,2,3,...,365],验证resample("MS").sum()输出的每月天数是否等于实际日历天数(如2020-02=29)。

注意:测试中所有“期望值”都来自FAO官方手册或Peer-reviewed论文的验证案例,不是随意设定。例如test_hargreaves_vs_pm的阈值0.85,源自《Journal of Hydrology》2021年一篇对比研究的结论。

4.3 结果可视化与快速验证:三行代码看懂是否算对

SPEI结果是否合理?别急着画地图,先做三件事:

1. 检查统计分布(应近似标准正态)
import xarray as xr import matplotlib.pyplot as plt # 加载刚生成的月SPEI spei_monthly = xr.open_dataarray("output/test_monthly/spei_monthly_1981-2010_gamma.nc") # 取全球平均时间序列(排除海洋NaN) spei_global = spei_monthly.mean(dim=["lat", "lon"], skipna=True) # 绘制直方图 vs 标准正态 plt.hist(spei_global.values, bins=50, density=True, alpha=0.6, label="SPEI") x = np.linspace(-4, 4, 100) plt.plot(x, norm.pdf(x), 'r-', lw=2, label="N(0,1)") plt.legend() plt.title("Global SPEI Distribution (should match N(0,1))") plt.show()

✅ 合理现象:直方图与红线高度重合,峰值在0附近,左右对称。
❌ 翻车信号:直方图右偏(干旱被低估)、左偏(湿润被低估)、双峰(分布拟合错误)。

2. 检查时空一致性(干旱事件应连片发生)
# 取2015年夏季(6-8月)的SPEI空间图 spei_2015_summer = spei_monthly.sel(time=slice("2015-06", "2015-08")).mean("time") # 绘制干旱区(SPEI < -1.5) plt.figure(figsize=(10,5)) spei_2015_summer.plot.contourf(levels=np.arange(-3, 3.1, 0.5), cmap="RdBu_r") plt.title("2015 Summer Mean SPEI (Drought: SPEI < -1.5)") plt.show()

✅ 合理现象:干旱区(深红)呈地理连续性,如北美西部、地中海沿岸连片;
❌ 翻车信号:随机散点状干旱(数据未对齐)、整片海洋变红(PET计算符号错误)。

3. 抽样点验证(用已知文献值交叉检验)

项目tests/中附带validation/reference_values.csv,含5个经典站点(如澳大利亚Darwin、中国北京)在2000-2010年间的SPEI月均值(来自CRU数据集验证)。你可以:

# 加载你的结果和参考值 your_result = spei_monthly.sel(lat=40.0, lon=116.0, method="nearest") ref_df = pd.read_csv("tests/validation/reference_values.csv") ref_beijing = ref_df[ref_df["station"]=="beijing"]["spei_2000_2010_mean"].iloc[0] print(f"Your Beijing SPEI (2000-2010): {your_result.mean().item():.3f <p> <a href="https://download.csdn.net/download/weixin_44021581/89755906" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

相关新闻

AiPy实战:用Python本地搭建AI自动化流程,低成本替代昂贵AI助手

AiPy实战:用Python本地搭建AI自动化流程,低成本替代昂贵AI助手

1. 先说清楚&#xff1a;这玩意儿到底是个啥如果你这段时间在关注AI圈&#xff0c;多少应该听过几个名字——某个老外做的自动化AI助手&#xff0c;能自己拆任务、调工具、跑流程&#xff0c;动辄几千块一个月的订阅费&#xff0c;还得折腾各种付费接口。很多开发者、中小团队被…

2026/10/20 2:38:36 阅读更多 →
换两行代码就换引擎:laya-mlx 接口兼容迁移的设计野心

换两行代码就换引擎:laya-mlx 接口兼容迁移的设计野心

换两行代码就换引擎&#xff1a;laya-mlx 接口兼容迁移的设计野心 【免费下载链接】laya-mlx Native MLX runtime for Laya typed decision models — 7–14 ms short decisions on M3 Max. No text generation, PyTorch, or cloud API. 项目地址: https://gitcode.com/gh_mi…

2026/10/20 2:38:36 阅读更多 →
博途1500PLC字符串包含判断:FOR+MID滑动窗口实现详解

博途1500PLC字符串包含判断:FOR+MID滑动窗口实现详解

用博途1500PLC判断源字符串中是否含有子字符串&#xff0c;这个需求听起来简单&#xff0c;我第一次在现场做的时候也以为一条指令就完事了。那是条包装线的扫码工位&#xff0c;读码器通过Profinet把条码送进PLC&#xff0c;我需要在ABC240315001XYZ这样的字符串里判断有没有2…

2026/10/20 2:37:36 阅读更多 →

最新新闻

调教Stable Diffusion注意力机制,让出图更清晰

调教Stable Diffusion注意力机制,让出图更清晰

1. 先别急着加高清修复&#xff0c;问题多半出在注意力分配上先说个不少人踩过的坑&#xff1a;用Stable Diffusion出图&#xff0c;总感觉边缘发虚、细节糊成一团&#xff0c;第一反应往往是把分辨率从512拉高到1024&#xff0c;或者直接开Hires. fix&#xff0c;结果要么显存…

2026/10/20 3:27:05 阅读更多 →
加密狗复制风险与工控软件授权管理:从停产事故看正确应对方式

加密狗复制风险与工控软件授权管理:从停产事故看正确应对方式

上个月我去一家做汽车零部件加工的朋友那边聊设备改造&#xff0c;正好撞上他们车间的一件急事&#xff1a;产线上一套工艺管理软件的加密狗突然失效&#xff0c;操作员报错、工艺配方调不出来&#xff0c;整个班组停了半个多小时。朋友的第一反应不是联系软件供应商&#xff0…

2026/10/20 3:27:05 阅读更多 →
Unity Scroll View Content配置全解:滚动失效排查与动态列表实战

Unity Scroll View Content配置全解:滚动失效排查与动态列表实战

聊起Unity里的Scroll View&#xff0c;十个新手有九个都栽在Content设置上。上周交流群里又有人发截图问&#xff1a;东西全拖进Scroll View了&#xff0c;为什么滚不动、显示不全、滚动条乱蹦&#xff1f;问题基本都出在Content这个子节点上。这篇专门讲Scroll View下Content的…

2026/10/20 3:27:05 阅读更多 →
AI Agent核心能力拆解与工程落地实战指南

AI Agent核心能力拆解与工程落地实战指南

各位做技术、做产品的朋友&#xff0c;最近是不是隔三差五就被"AI Agent"这个词刷屏&#xff1f;团队研讨它在讲&#xff0c;朋友圈技术博主在聊&#xff0c;产品评审会也在问"为什么不做成Agent"。但如果真让你一句话讲清楚它到底是什么&#xff0c;很多人…

2026/10/20 3:27:05 阅读更多 →
OpenAI兼容格式接入中文大模型:从密钥配置到流式输出实战

OpenAI兼容格式接入中文大模型:从密钥配置到流式输出实战

我先说个结论&#xff1a;如果你现在手里攒着一堆“用某个平台接大模型”的活儿&#xff0c;最省心的路线&#xff0c;往往不是直接用厂商自己那套SDK&#xff0c;而是找一个已经帮你把接口层做成了OpenAI兼容格式的数据中间件。这篇内容就围绕一个具体方案展开——用某数据云平…

2026/10/20 3:27:05 阅读更多 →
DeepSeek降AI指令5步流程:从AI味到自然学术写作

DeepSeek降AI指令5步流程:从AI味到自然学术写作

最近太多人在问“DeepSeek降AI指令怎么写”&#xff0c;尤其是论文写了一半&#xff0c;拿AI润色完&#xff0c;自己一读都觉得“太工整了”&#xff0c;然后被各种AI痕迹检测提示搞得心里发毛。先说结论&#xff1a;所谓“降AI”&#xff0c;并不是玄学&#xff0c;本质是让大…

2026/10/20 3:26:04 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →