简介这份面向气候科学的机器学习项目资料包围绕干旱预测与理解展开适合具备Python基础、希望用机器学习方法处理气候数据的科研人员和进阶学习者。它提供端到端管道用于创建、相互比较和评估气候科学中的机器学习方法并统一训练与测试数据格式描述中的src模块按任务拆分便于按流程理解数据准备、方法对比与评估环节。包体约49.31MB页面暂未给出有效文件总数与文件类型明细下载后需以实际解压目录为准从标签与说明看内容以Jupyter Notebook实验记录和Python代码为主并包含环境配置方便复现实验。已有142人学习说明该方向有一定关注度。读者可获得一套可运行、可比较的气候机器学习流程框架以及统一的实验数据组织思路适合用于干旱预测方法调研、复现实验和二次开发。1. 干旱预测的真正瓶颈模型不是短板标签定义才是机器学习做干旱预测绝大多数人的第一反应是换模型从线性回归换到随机森林再从梯度提升树换到时序 Transformer指望验证集上的那条曲线再往上抬一截。但真正把这套基于 Jupyter Notebook 的干旱预测项目跑通一遍就会发现分数波动最大的地方根本不在模型层而在“什么算干旱”这个前置定义上。同样一份降水数据用 3 个月窗口和 12 个月窗口算出来的标签能让同一个模型的召回率相差一倍以上标准化参数如果越过了训练期边界验证集分数会漂亮到完全不可信。这个项目值得逐行读的原因就在这儿它把大量工程复杂度花在了标签工程、时间切分和数据对齐上而这些恰恰是教程里最容易被一句“构造特征后训练模型”带过的地方。搞懂这条链路比多调三组超参有用得多。2. 标签工程SPI 与 SPEI 怎么把降水翻译成可学习目标干旱不是一场暴雨就能定义的事件它是水分亏缺在几个月甚至几年尺度上慢慢累积的结果。这意味着模型要预测的东西本身就不是原始观测值而是一个经过统计变换的派生量。项目里所有 Notebook 的第一步都不是读特征而是算指数这个顺序不能颠倒。2.1 从原始降水到标准化指数滚动窗口与分布拟合标准化降水指数SPI的核心思路是某个月的降水本身没有可比性因为不同地区、不同月份的降水基线差异巨大但“这个月累计降水量在本站点历史同窗口长度分布中的分位”是可比的。所以计算分三步——先做尺度为 k 的滚动累加再对累加序列拟合一个偏态分布降水通常用 Gamma最后把累积概率映射回标准正态。import numpy as np from scipy.stats import gamma, norm def spi(precip, scale3, calib_idxNone): precip: 一维月降水序列按时间升序单位 mm scale: 滚动窗口长度3 表示 3 个月累积 calib_idx: 用于拟合分布的样本下标关键参数见 2.3 # 滚动累加得到每个时点的 k 个月水分收入 acc np.convolve(precip, np.ones(scale), modevalid) # 拟合 Gamma 分布floc0 强制下限为 0符合降水非负的物理约束 calib acc[calib_idx] shape, loc, theta gamma.fit(calib, floc0) # 累积概率 - 标准正态分位就是 SPI cdf gamma.cdf(acc, shape, locloc, scaletheta) # 裁剪防止 ppf 在 0/1 处返回 ±inf这类值会直接毒化后续标准化 cdf np.clip(cdf, 1e-4, 1 - 1e-4) return norm.ppf(cdf)这段代码里有三个容易被忽略的决定。第一modevalid让输出序列比输入短scale-1个点后面所有特征对齐都必须按这个偏移来索引错一位就等于把未来信息喂给模型。第二floc0不是可选项降水分布的下界是物理事实放任gamma.fit自由拟合位置参数在小样本站点上会拟合出负下界导致 CDF 在该区间无意义。第三np.clip那两行看起来像防御性编程实际上决定了后面标准化是否稳定——norm.ppf在概率恰好为 0 时返回负无穷一个-inf进入特征矩阵整个批次的均值方差都会变成nan。至于scale取多少项目里通常并行算 3、6、12 三个尺度分别对应农业干旱、水文干旱和长期水资源亏缺。它们在时间上的自相关结构完全不同3 个月尺度的序列自相关衰减很快12 个月尺度可能连续两年都在负值区间徘徊。如果用同一套切分和同一套滞后特征去套所有尺度短尺度模型会显得很差原因不在模型而在于标签本身的持续性没有被尊重。2.2 蒸散项为什么必须进来SPEI 的温度修正只算 SPI 的项目在温带湿润区往往还能看一到半干旱区就明显失准。原因很直白干旱是“收入减支出”的净效应降水只是收入侧温度通过潜在蒸散把支出侧拉高。同样一场 50 mm 的月降水在 15℃ 和 35℃ 下对土壤水分的意义完全不同。标准化降水蒸散指数SPEI就是把这一项补上先算水分平衡量P - PET再对平衡量做和 SPI 一样的“滚动累加 分布拟合 正态映射”。def water_balance(precip, temp, lat): # 简化的 Hargreaves 潜在蒸散只用温度和纬度不依赖完整气象要素 # 项目里用这种简化式换取在缺观测的站点也能算出一致的 PET ra extraterrestrial_radiation(lat) # 天文辐射按纬度和日序计算 tdiff temp_max - temp_min # 月平均日温差 pet 0.0023 * ra * (temp 17.8) * np.sqrt(tdiff) return precip - pet这段简化的价值不在精度而在一致性。如果某些站点用 Penman-Monteith需要风速、湿度、辐射全套要素另一些站点只能用 Hargreaves那么不同站点的标签口径就不一样了模型学到的很可能是“哪个站点用了哪套公式”而不是气候信号。项目统一走简化式是用一点绝对精度换取了跨区域的标签可比性——这是数据工程里非常典型的一笔取舍。还有一个副作用值得注意引入 PET 之后标签的分布尾部会变厚。极端高温叠加少雨时P - PET会冲到很负的位置Gamma 分布对这种负值区间拟合得很差。所以 SPEI 的常见实现会在拟合前把平衡量做一次平移或者改用对负值更友好的分布。这一步如果省掉模型在极端干旱样本上的预测会系统性偏低——而这恰恰是最需要它准的那部分样本。2.3 训练期与验证期的标准化参数隔离这是整篇里最容易被忽略、后果最严重的一条。gamma.fit用的是数据本身如果用全序列拟合那么验证期样本的分布信息已经通过shape和theta两个参数泄漏进了标签。表现出来就是验证集分数虚高上线后一泻千里。正确的做法是拟合分布参数的样本范围必须严格限制在训练期内。验证期和测试期的 SPI 值只能用训练期拟合出来的参数去做 CDF 变换。这跟标准化的StandardScaler必须在训练集上fit是同一个道理只不过这里被包装成了“算气候指数”很多人就忘了它也是个拟合过程。n len(precip) split int(n * 0.7) # 训练期下标注意要扣掉滚动窗口造成的偏移 calib_idx np.arange(0, split - scale) spi_train spi(precip[:split], scalescale, calib_idxcalib_idx) # 验证期复用训练期拟合出的参数 spi_full spi(precip, scalescale, calib_idxcalib_idx) spi_valid spi_full[split - scale 1:]这段代码的索引偏移是血泪经验。np.convolve的输出第 0 个元素对应输入的第scale-1个时点所以切分点必须同步平移否则训练集和验证集会在边界上重叠scale-1个月。在 12 个月尺度上这就是整整一年的标签重叠足够让模型“背下”验证期的答案。3. 特征与数据契约再分析数据怎么变成 Notebook 能跑的时间片标签定义清楚之后下一层问题是特征。项目的数据源是月尺度的格点再分析产品典型形态是一个三维数组时间 × 纬度 × 经度。直接把它摊平成二维矩阵喂模型会有三个具体障碍时间维上的自相关没被显式表达、空间维的样本量爆炸但有效信息有限、以及 Notebook 反复重跑时的数据一致性。3.1 时间对齐与滞后特征的构造顺序干旱的可预测性大量来自持续性过去 6 个月的降水亏缺本身就强烈预示未来 3 个月的状态。所以滞后特征不是可选项是主力。但构造顺序必须严格是“先算标签 → 再切分 → 再构造滞后 → 最后丢缺失”任何一步换位置都会引入泄漏。def build_features(df, drivers, lags(1, 2, 3, 6, 12)): df: 已按时间排序、已含标签列的 DataFrame drivers: 参与构造滞后的驱动变量列表 lags: 滞后阶数单位月 out df.copy() for col in drivers: for lag in lags: # shift(lag) 表示lag 个月前的值构造时只能用过去 out[f{col}_lag{lag}] out[col].shift(lag) # 滚动统计量同样只能向后看 out[f{col}_roll6] out[col].rolling(6, min_periods6).mean() return out两个细节。rolling的默认窗口是向过去看的这一点是对的但min_periods必须显式设成窗口长度否则序列开头会用不足 6 个点算出均值这批“半成品特征”在分布上和后面的样本不一致。另外滞后阶数里同时放了短阶1、2、3和长阶6、12是把不同时间尺度的记忆显式暴露给模型如果只放 1 阶树模型就得靠多层分裂去逼近一个平滑的滞后结构样本效率会低很多。顺序上还有一个隐式约束shift必须在样本切分之后、模型训练之前做但它引用的原始列必须来自完整序列。也就是说切分的是“样本行”不是“原始时间序列”——原始序列要完整保留否则切分边界附近的样本会凭空少掉十几个滞后特征被dropna顺手删掉。3.2 空间降维格点、区域均值与样本量再分析产品的空间分辨率通常在 0.25° 到 1° 之间一个省级区域就有几百上千个格点。直接按格点建模样本量看起来很大时间 × 格点但相邻格点的气候态高度相关有效样本量远小于名义样本量。这会导致交叉验证的方差被严重低估。项目里常见的处理是先做区域聚合按气候分区或者简单的空间块求均值把格点维压到几十个区域再以“区域 × 时间”作为样本单元。代价是丢失了区域内部的空间细节收益是样本之间的独立性大幅提升验证分数更接近真实泛化能力。另一种做法是保留格点但在切分时按空间分块——同一块内的格点要么全在训练集要么全在验证集。这在机制上等价于空间上的 group split能有效避免“模型记住了这个格点的地形然后拿去预测隔壁格点”这种伪泛化。处理方式名义样本量有效独立性适用场景逐格点建模极大低有海量独立观测、格点稀疏区域均值聚合中等中高区域尺度干旱预警空间分块切分大高保留空间细节且要可信验证选哪一种取决于最终要回答的问题。如果目标是“某区域未来三个月是否进入干旱”聚合就够如果目标是“画出干旱风险的空间分布图”就必须保留格点同时用空间分块切分来保证评估可信。3.3 Notebook 工作流里的数据契约与缓存Jupyter Notebook 做研究的最大风险不是代码写错而是“上一次跑出来的中间结果还留在内存里”。项目里的 Notebook 通常按阶段拆成多个数据读取与指数计算、特征构造、模型训练与评估。每个阶段之间用磁盘上的中间文件衔接而不是靠变量在 kernel 里传递。这个约定看起来笨但它解决了一个具体问题指数计算那一步可能要跑十几分钟如果每次都从头执行调试模型时人会不自觉地“跳过前面几格”于是用了上一次的旧标签去训新特征结果对不上还找不到原因。落盘之后每个阶段都有明确的输入输出文件名和校验字段时间范围、样本数、缺失率任何一个对不上下一步直接报错而不是安静地产出一个错误分数。缓存文件里建议至少记录三样东西标签的尺度参数k、标准化所用训练期的起止时间、以及特征列清单的哈希。前两个决定了标签是否可复现第三个决定了特征顺序错乱时能不能被发现——树模型对列顺序不敏感但一旦列名和列内容错位模型照样能训出一个“看起来正常”的分数。4. 验证为什么随机 K 折会给出乐观到离谱的分数如果只能从这套项目里带走一个工程习惯那就是时间序列的切分方式。用train_test_split(shuffleTrue)或者随机 K 折去评估干旱模型得到的数字基本没有参考价值——不是偏差一点是可能高到让整条技术路线被误判。4.1 时间块切分与 embargo时间序列样本之间存在自相关滞后特征又把这种相关显式地放大了一轮。随机切分会让训练集里出现“验证样本的前一个月和后一个月”模型只要学会插值就能拿到高分。正确的做法是按时间切成连续的块训练块在前、验证块在后并且在两块之间留一段隔离带embargo把跨边界的滞后特征全部切断。def blocked_splits(n_samples, n_folds5, embargo12): 返回 (train_idx, valid_idx) 列表按时间顺序推进 embargo: 训练块与验证块之间丢弃的样本数应 最大滞后阶数 fold_size n_samples // (n_folds 1) splits [] for i in range(1, n_folds 1): train_end i * fold_size valid_start train_end embargo valid_end min(valid_start fold_size, n_samples) if valid_start n_samples: break splits.append(( np.arange(0, train_end), np.arange(valid_start, valid_end) )) return splitsembargo取 12 是因为最大滞后阶数是 12 个月。这个数字不是拍脑袋如果一个验证样本的特征里包含它前 12 个月的值而训练集的最后一个样本距离它不到 12 个月那么训练样本和验证样本共享了同一段原始观测隔离就失效了。实践中还有个更保守的做法把 embargo 设成“最大滞后 标签窗口长度”因为标签本身是滚动累加出来的也带着k个月的记忆。这种切分带来的直接后果是每个 fold 的训练集都在增长最早期的 fold 训练样本很少分数天然偏低。所以报告结果时应该分 fold 列出来而不是只报一个平均值——平均值会把“早期样本不足导致的低分”和“模型真的退化”混在一起。4.2 分类阈值化与类别不平衡干旱事件在多数区域是少数类尤其在湿润年份占比可能低到 5% 以下。如果直接对连续指数做阈值化得到二分类标签然后训练分类器会遇到两个连锁问题一是正样本太少模型倾向于全部预测为负类也能拿到很高的准确率二是阈值本身是人为选的换一个阈值正负样本比例就全变了前后两次实验根本不可比。合理的做法是把阈值固定成气候学定义比如 SPI ≤ -1.0 为中度干旱并且在所有实验里保持不变。同时不要用准确率作为主指标改用对少数类敏感的量。类不平衡的处理也有讲究过采样在时间序列上会破坏自相关结构因为复制出来的样本和原样本在时间上完全重合等于把同一段历史用了两次相比之下调类别权重或者改用代价敏感的学习目标更安全。from sklearn.ensemble import HistGradientBoostingClassifier clf HistGradientBoostingClassifier( max_iter300, learning_rate0.05, # 用样本权重而不是重采样来处理不平衡避免破坏时间结构 class_weightbalanced, random_state42, )class_weightbalanced的效果是按类别频率反比加权机制上等价于把少数类的损失放大但样本在时间轴上的位置没有被动过自相关结构保持完整。这个选择在时序问题里几乎是默认答案除非你有明确的理由相信少数类样本被系统性欠采样了。4.3 指标选择从 ROC-AUC 到 Brier 技巧分数ROC-AUC 在类别极度不平衡时依然可能很好看因为它衡量的是排序能力而干旱预警真正在意的是“给的概率是否可信”。一个把所有样本都打成 0.3 的模型AUC 可能是 0.5但在业务上完全无用而一个 Brier 技巧分数BSS能直接告诉你这个概率预报比“直接报历史气候频率”这个基准强多少。def brier_skill_score(y_true, p_pred, p_clim): y_true: 0/1 标签 p_pred: 模型输出的正类概率 p_clim: 气候基准概率通常取训练期内该事件的发生频率 bs np.mean((p_pred - y_true) ** 2) bs_ref np.mean((p_clim - y_true) ** 2) # BSS 0 表示优于气候基准 0 表示还不如直接报历史频率 return 1 - bs / bs_refp_clim这个基准线是整个评估的锚点。它回答的是“如果我不做任何预测只按历史频率报一个常数概率能得多少分”。任何模型如果 BSS 不能稳定为正就说明它没有从特征里提取出超越气候态的信息这时候讨论模型结构没有意义。项目里通常还会额外报事件召回率在固定误报率下的命中率因为预警场景对漏报的容忍度远低于误报。5. 排查清单干旱预测项目里最容易翻车的五个地方5.1 验证分数高到不像话现象第一版模型跑出来 AUC 0.95 以上换数据集立刻掉到 0.6。原因最常见的两种泄漏——标准化/分布拟合用了全序列或者随机切分让相邻月份同时进了训练集和验证集。干旱标签的持续性极强相邻样本几乎等价随机切分等于开卷考试。解决把gamma.fit和任何 scaler 的fit范围显式限定在训练期内把train_test_split换成blocked_splits并确认 embargo 不小于最大滞后阶数。改完之后分数通常会掉一大截那个掉下来的数字才是真的。5.2 换一年就完全失效现象在同一年内做交叉验证表现稳定换到下一年的数据上预测全部偏向同一类。原因气候态漂移。训练期的降水基线和验证期不同而模型的输入特征里包含绝对值比如某月降水 80 mm这个绝对值在不同年代的意义不一样。如果特征没有做相对化处理模型学到的就是“80 mm 这个数”而不是“80 mm 相对于本期的位置”。解决把绝对量特征替换成距平或分位特征标准化参数固定在训练期同时在验证时按年份分组报告结果把“跨年退化”显式暴露出来而不是用总体平均掩盖。5.3 预测结果几乎全是同一类现象混淆矩阵里某一类占了 95% 以上模型看起来“很准”。原因类别不平衡叠加阈值选择不当。模型的原始输出概率可能是有区分度的但默认 0.5 的判定阈值在正样本占比 5% 的数据上几乎不可能被越过。解决先看概率分布再看阈值。把判定阈值调到使验证集上的事件召回率达到业务要求再报告对应的误报率。如果概率本身就没有区分度正负样本的概率分布几乎重合那是特征或标签的问题调阈值救不回来。5.4 同一个 Notebook 重跑两次结果不一样现象什么都没改两次运行的指标差了 0.03。原因随机种子没固定、并行计算的求和顺序不固定导致浮点结果有微小差异、或者上一步的中间文件被覆盖了但没重新读取。解决固定random_state在涉及并行的聚合处显式指定确定性选项更重要的是把每个阶段落盘并记录哈希让“这次跑的到底是哪份数据”这个问题有确定答案。这一点在研究型项目里比模型本身更值得投入。5.5 内存直接爆掉现象读入多变量、多年、高分辨率的格点数据后 kernel 崩溃。原因把整个三维数组一次性读进内存并转成 DataFrame。时间 × 纬度 × 经度 × 变量数很容易到几十 GB。解决按时间分块读取每个块内完成特征构造和聚合只保留聚合后的低维结果或者先做空间子集目标区域再读变量。不要在内存里做“先全读进来再筛”的操作这类项目的数据量不支持这种写法。6. 取舍为什么把复杂度花在数据侧比堆模型更划算把这条链路走完会发现一个有点反直觉的结论在这个问题上模型结构带来的提升远小于标签定义和验证方式带来的差异。同一个梯度提升树换一套 SPI 计算口径BSS 可以差出 0.2 以上而把树模型换成神经网络在数据规模有限、信噪比不高的干旱预测任务上通常只能带来个位数的相对提升还要付出可解释性和训练稳定性的代价。投入方向典型收益主要风险标签口径与尺度选择大直接影响可学习性口径不一致导致跨区域不可比切分与评估方式大决定分数是否可信保守切分导致早期 fold 样本不足特征与滞后结构中提升样本效率构造顺序错误引入泄漏模型结构与调参小到中边际递减在不可信的验证集上过拟合这个排序背后的机制是干旱预测的可预测性上限由气候系统的记忆性决定而记忆性是通过滞后特征和长尺度标签被显式表达出来的。模型只是在这个表达之上做拟合它对信息的挖掘能力受限于输入里到底有没有信息。所以合理的工程顺序是先把标签和切分做对让分数变得可信再把特征的时间结构做厚让信息尽量显式最后才在模型上做有限的选择——通常几类树模型加一个线性基线就足够覆盖大部分收益。我自己的习惯是每接手一份新的气候数据先不看模型而是先画三张图标签的时间序列、训练期与验证期的分布对比、以及滞后一阶的自相关系数。这三张图能提前暴露掉八成的坑——标签有没有异常跳变、分布有没有漂移、自相关有多强决定了 embargo 该留多长。这个习惯是从一次惨痛经历里长出来的当时一个模型在验证集上表现极好上线后连续三个月误报回头一查是分布拟合用了全序列验证期的分位信息提前泄进了训练标签。从那以后我每次构造任何“需要拟合参数”的特征都强制走一遍“fit 只在训练期、transform 才碰全量”的检查哪怕它看起来只是个指数计算。这套项目的价值也在这里它把这条容易被跳过的链路完整地铺在了 Notebook 里从指数计算、特征对齐、时间块切分到指标选择每一段都能对着代码验证机制。想深挖的话建议直接从标签计算和切分函数这两处读起把参数改一改、把切分换一换亲眼看分数怎么变——那比读十篇方法综述都直观。希望帮到你。本文还有配套的精品资源点击获取