
最近整理旧项目文件翻到之前拿 Kerala 洪水数据集练手的一个机器学习项目。核心任务不复杂用机器学习算法对历史降雨和洪水标记做分类建模输入一年的十二个月降雨量输出当年发生洪水的概率。这个项目本质上就是一个典型的洪水暴雨内涝预测模型用真实公开数据集做训练很适合正在学分类算法、又想走通完整机器学习流程的朋友。今天把整个思路、数据细节、建模过程还有我踩过的坑完整地写一遍。这个数据集小样本就一百来条特征也只有降雨量跟动辄几万条样本的竞赛数据完全没法比。但这恰恰是它值得拿来练手的地方数据量小意味着能手动检查每一条样本标签分布不均衡又逼着你必须认真对待模型评估而不是看一眼准确率就交差。把这个项目做透比跑十个现成案例收获都要大。1. 项目整体设计与建模思路拆解1.1 为什么选 Kerala 洪水数据集先交代一下这个数据集的背景。2018年8月印度喀拉拉邦因为季风降雨异常遭遇了一场非常严重的洪水灾害人员伤亡和财产损失都很惨重。灾害发生后很多人开始关注一个朴素的问题能不能用历史降雨数据提前判断某一年发生洪水的风险到底有多高Kerala 洪水数据集就是在这样的背景下被整理出来的。数据集本身是一份 CSV 文件字段很直白年份、十二个月的降雨量、年降雨总量以及一列当年是否发生洪水的标记。样本时间范围大致从 1901 年到 2018 年前后总行数只有一百二十多条。放在机器学习工程里看这属于微型样本量任何复杂模型都可能直接过拟合。但这个数据集的价值在于它的“教学属性”。作为洪水预测模型的入门数据它的特征维度低、业务背景清晰、标签含义直观不需要做复杂的采样或清洗就能开工。对初学者来说这比一上来就扔给你几百个特征的大规模数据集友好得多。1.2 目标定义这是“年度风险评估”而不是“实时内涝预警”拿到数据后我做的第一件事不是跑模型而是把目标定义清楚。这个项目里标签是“当年是否发生洪水”所以模型回答的问题是“如果某年的降雨分布长这样该区域是否处于洪水高风险状态”而不是“明天下午三点某某街道会不会积水”。这两个是完全不同的任务。真正的暴雨内涝预测通常需要小时级别的降雨数据、地形高程、水系分布、排水管网容量、土壤饱和度等要素而且往往是时序模型。Kerala 数据集只有年度降雨量的粗粒度信息你没法用它做分钟级或小时级预警。把预期调到“历史风险复盘”和“趋势判断”上后面的建模才不会跑偏。这种目标澄清在实际项目里比模型调参重要得多。我见过不少新手拿到数据就开始堆模型最后发现业务方要的是一回事模型给的是另一回事。先对齐预期再谈技术实现。1.3 传统机器学习算法为什么是这个场景的合理选择这个项目没有考虑深度学习也没有引入复杂的水文物理模型原因很直接。深度学习需要大量数据一百多条样本丢进去网络还没学到规律就已经把训练集背下来了。水文物理模型精度高但需要降雨过程、流域参数、土壤类型等大量专业输入普通开发者既拿不到数据也缺乏水文建模背景。机器学习算法恰好卡在中间它能从有限的数据里提取降雨和洪水之间的统计规律训练速度快结果可解释还能直接做概率输出。我选了四种有代表性的算法做横向对比逻辑回归作为线性基线随机森林和 XGBoost 代表树模型SVM 负责验证小样本下边界划分能力。它们各自有各自的脾气放在同一个数据集上一跑差别马上就能看出来。2. 数据探索、清洗与特征处理2.1 拿到数据先做三件事看列、看分布、看标签我拿到数据后第一步是用 pandas 看一眼前几行确认每一列的含义。常见版本的字段大概是这样的字段含义YEAR年份如 1901、1902JAN ~ DEC各月降雨量单位毫米SUMS全年降雨量合计FLood当年是否发生洪水1 表示发生0 表示未发生标签列在原数据集里写成 FLood这里我按 Flood 理解就行。如果你下载的版本字段名有差异代码里做好兼容就行。第二步是看描述性统计。喀拉拉邦的降雨高度集中在季风季节六月到九月的降雨量占了全年很大比重。我把洪水年和非洪水年的月均降雨量拉出来对比发现洪水年的雨季月份平均值明显更高尤其七八月份差距最明显。这说明模型有可学的信号不是一个纯粹猜硬币的问题。第三步是数一数正负样本。数据里洪水标记为 1 的样本只占总量的大约百分之十几到二十典型的类别不平衡。如果我直接拿原始数据去训练模型大概率会消极怠工把所有样本都判成 0照样能拿到八九成的准确率。2.2 FLood 标签本身的坑别把结论说得太满做数据探索时我发现一个值得注意的问题这个数据集的标签和降雨量高度相关。换句话说某些年份被标记为洪水年很大程度上就是因为那一年雨季降雨量超标。这就会带来一个隐患——如果用十二个月降雨量去预测洪水标记模型学的实际上是“这组降雨数据在统计上更接近历史洪水年的形态”而不是真正理解了洪水发生的物理机制。我并不是说这个数据集不能用来建模而是提醒大家要清楚它的边界。它可以作为学习分类流程、练习模型评估的好素材但如果有人跑出 98% 的准确率就宣称“我解决了洪水预测问题”那结论显然站不住脚。做任何数据项目之前先搞清楚标签是怎么来的这是数据从业者的基本素养。2.3 特征处理这次我刻意做了减法很多人在特征工程阶段容易上头恨不得把能想到的特征全部塞进去。我在这个项目里刻意做了减法只保留十二个月降雨量作为核心特征申请移除掉三个容易惹麻烦的列。第一年份列直接去掉。如果不删树模型很容易学出“近年来洪水更多”这种时间趋势而不是学到降雨和洪水之间的关系换到未来年份预测时模型会失效。第二SUMS 年降雨总量也去掉因为它等于各月降雨量之和本质上把标签的一部分信息泄漏给了模型。留着它模型看似更强其实是走了捷径。第三没有对十二个月降雨量做 PCA 降维。虽然特征之间有相关性但样本太少降维带来的稳定性提升远不如保留原始字段可解释性来得实在。对于标准化这个动作我的处理方式是逻辑回归和 SVM 这类对特征尺度敏感的模型先做 StandardScaler随机森林和 XGBoost 这类树模型不需要标准化直接喂原始降雨量就行。不同模型各吃各的不吃同一份数据对比起来才公平。3. 模型选型与基础训练3.1 四个模型各自的定位我选的四个模型各有侧重整理下来大致是这样模型类型优势这个项目里的实际表现逻辑回归线性模型可解释性强训练快适合做基线中规中矩决策边界太简单随机森林Bagging 树模型抗过拟合能力强能输出特征重要性稳定默认参数下表现不错SVM核方法小样本分类效果好能处理非线性边界配合标准化后效果提升明显XGBoostBoosting 树模型表格数据常青树精度潜力高容易过拟合必须限制复杂度补充一句这类做对比的习惯不仅限于降雨预测。我之前看别人做基于机器学习的音乐风格分类算法设计与实现时也是同一个套路先跑几个不同类别的模型别一开始就把赌注押在某个复杂模型上。模型选型这件事很大程度上是“先广后深”的过程。3.2 数据划分这个数据集不能用随机切分一般分类任务里用 train_test_split 随机划分数据集大家习以为常。但这个数据集不能这么干因为年份之间存在时间顺序气候趋势会让相邻年份的降雨特征更相似。如果随机切分训练集里可能混着验证集年份“附近”的信息验证指标会虚高。我的做法是尽量尊重时间顺序。一是用前 80% 的年份做训练、后 20% 的年份做验证模拟真正对未来年份预测的场景二是用 GroupKFold 按年份分组做交叉验证确保同一年份的样本不会被同时分到训练集和验证集。两种方式结合使用结果更可信。下面是一个按年份分组的交叉验证框架核心代码大概是这样的import pandas as pd from sklearn.model_selection import GroupKFold from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report df pd.read_csv(kerala_flood.csv) feature_cols [JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC] label_col FLood if FLood in df.columns else Flood X df[feature_cols].copy() y df[label_col].astype(int) groups df[YEAR] gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(X, y, groups): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model RandomForestClassifier( n_estimators200, max_depth4, random_state42 ) model.fit(X_train, y_train) y_pred model.predict(X_val) print(classification_report(y_val, y_pred, zero_division0))注意 GroupKFold 的 split 方法里传了 groups 参数目的就是让同一年份的样本保持完整要么全在训练集要么全在验证集。3.3 第一轮训练结果高准确率是假象第一次跑完随机森林的准确率确实逼近九成看起来“效果不错”。但把混淆矩阵拉出来一看问题马上暴露模型几乎把验证集里的所有年份都判成了 0。也就是说它学会了“全说没洪水准确率也不低”因为数据里大多数年份确实没有洪水。这种模型放在真实场景里毫无用处——真正要预警的那几个高风险年份它一个都没抓住。查分类报告召回率特别难看洪水年的召回率低得可怜精确率更是直接归零。准确率这个指标在类别不平衡的数据集上是最容易骗人的数字。我很快把评估口径切换到 F1、召回率、精确率和混淆矩阵上来先回答“洪水年到底抓得住几只”再谈整体准确率。这个教训值得单独强调做任何分类项目先统计类别分布再选评估指标。如果样本不平衡准确率绝对是最后才看的指标。这是我反复跟别人强调的一点。4. 调参、阈值与评估优化4.1 灾害预警的评估逻辑宁可多报不可漏报在一般业务里模型误报太多会消耗人力客户体验也差。但洪水预测不一样漏掉一次真实洪水可能带来巨大损失而多报一次预警最多是让相关部门多做几轮巡查。基于这个业务逻辑我的优化目标从“准确率最高”变为“在可接受的误报范围内尽量拉高召回率”。机器学习算法默认用 0.5 作为类别划分概率阈值这在类别分布相对平衡时是合理的。但在这个数据集中洪水年本来就少模型输出的概率普遍偏低用默认阈值会把大量“有点可疑”的年份挡在预警门外。所以我把判决阈值从 0.5 往下调让更多概率中等偏上的样本进入“预警”区间。4.2 阈值下调的实测过程阈值调整的本质是重新选择精确率和召回率的平衡点。我用验证集上的概率输出做了一遍扫描从 0.1 到 0.5 逐步尝试看不同阈值下的精确率、召回率和 F1 值变化。import numpy as np import pandas as pd from sklearn.metrics import precision_score, recall_score, f1_score # model 已训练完成y_prob 是验证集预测的概率 y_prob model.predict_proba(X_val)[:, 1] best_f1 0 best_threshold 0.5 records [] for th in np.arange(0.10, 0.50, 0.01): y_pred (y_prob th).astype(int) p precision_score(y_val, y_pred, zero_division0) r recall_score(y_val, y_pred, zero_division0) f1 f1_score(y_val, y_pred, zero_division0) records.append((round(th, 2), round(p, 3), round(r, 3), round(f1, 3))) if f1 best_f1: best_f1 f1 best_threshold th result_df pd.DataFrame(records, columns[threshold, precision, recall, f1]) print(result_df) print(best threshold:, round(best_threshold, 2), f1:, round(best_f1, 3))从扫描结果可以清楚看到阈值在 0.5 附近时精确率确实很高但召回率被压得很低真正的洪水年大量漏报。随着阈值慢慢下调到 0.3 附近召回率开始明显抬升F1 值也随之上升。继续下探到 0.2 以下虽然召回率更高但精确率掉得厉害误报频次增加F1 反而开始回落。综合几次交叉验证的结果在我这次实验里阈值落在 0.27 到 0.32 之间是一个比较舒服的区间。这个范围内召回率能保持在一个相对可用的水平同时精确率不至于低到完全没法看。对这个数据集来说默认 0.5 的阈值基本等于宣告“除非证据非常确凿否则不报”在灾害预警场景下太保守了。4.3 用混淆矩阵复盘最终效果把阈值调整到 0.30 后我在某一折验证集上得到的混淆矩阵大致长这样预测为未发生洪水预测为发生洪水实际未发生洪水183实际发生洪水14样本数少矩阵里的数字看着很小信息量却很大。总共 26 个验证样本里模型成功抓住了 4 个真实洪水年中的大部分只漏了 1 个代价是误报了 3 个未洪水年份。对灾害预警来说这个结果是可以接受的宁可让非洪水年多响几次警报也不能让洪水年悄悄溜过去。这也是为什么我一直坚持用混淆矩阵而不是准确率来评估模型。准确率只告诉你整体对不对的比例而混淆矩阵会把“哪类错了、错得能不能接受”摊开给你看。5. 常见问题与避坑实录5.1 年份泄漏和特征泄漏最容易踩的两个坑我在实验过程中特意试过把 YEAR 列直接当作特征丢进去。结果很有意思模型找到了一个很偷懒的规律早年间很少标记洪水近几年标记变多所以只要把年份数大的样本预测为 1就能在验证集上刷出不错的成绩。问题是这个规律对未来没有任何预测价值它只是在拟合历史趋势不是在学习降雨和洪水的关系。真正做预测时谁也不知道未来几年的“年份编号”该对应什么风险水平。最终我把年份列剔除了。另一个更隐蔽的泄漏是 SUMS 年降雨总量列。它是由十二个月的降雨量直接加总得到的而洪水标签本身又和全年降雨量高度相关。把它放进特征里相当于提前告诉了模型“今年总雨量很大你看着办”。模型会优先抓住这个捷径导致真实月份特征学偏。训练时准确率很好看模型上线后一旦输入没有 SUMS 列的新数据立刻原形毕露。5.2 类别不平衡单靠调阈值远远不够调阈值能改变判决边界但不会让模型学到更多关于洪水年的模式。它是在“用同一个分类器选择更激进或更保守的输出方式”本质上属于事后策略。如果更根本地解决问题还得从训练环节入手。我在实验中加了 class_weightbalanced让模型在计算损失时自动给少数类更高的权重。这个改动很小但对召回率有帮助能减少洪水样本被忽略的倾向。还有一种常见做法是 SMOTE 过采样通过插值生成新的洪水样本来平衡类别比例。不过在这个数据集上样本量本身就小SMOTE 生成的样本可能在特征空间里离真实样本很近反而加剧过拟合。我的建议是小样本时优先用类别权重不要一上来就过采样。5.3 小样本交叉验证如何防过拟合这个项目样本量太少如果只做一次训练验证划分结果很容易被某几个运气成分左右。我最后采用的方法是多次交叉验证取平均同时严格控制模型复杂度。树模型方面随机森林的 max_depth 我控制在 3 到 5n_estimators 保持在 200 以内。太深的树在小样本上会把个别年份的细节当成全局规律。XGBoost 在这个数据集上特别容易过拟合我尝试过加深树深度训练集准确率一路冲到 100%验证集却在原地踏步。后来用学习率 0.05、最大深度 2 到 3、subsample 0.8 之后泛化表现才正常了一些。说白了小数据集上“模型复杂度别太高”比“模型能力很强”重要得多。5.4 从这个项目到真正的暴雨内涝预警还差什么前面反复强调过这个项目适合练手但它和真正用于防汛决策的暴雨内涝预测模型还有很大距离。如果要落地至少需要升级三块内容一是把年度数据换成小时级或至少日级的降雨数据二是引入地形、水系、排水管网等地理信息三是把简单的二分类改为时间序列预警模型实时输出未来若干小时的风险等级。做项目之前先想清楚这个差距就不会把一个练手项目包装成生产级系统了。如果把这个项目的完整流程迁移到别的分类场景里比如基于机器学习的音乐风格分类算法设计与实现你会发现要解决的问题高度相似先理解标签口径再做数据清洗然后选择合适的分类模型最后用正确的指标评估。尤其是类别不平衡和模型过拟合这两个问题几乎在任何真实数据集上都会碰到。这次项目做下来我最大的体会是模型不是越复杂越好评估不是准确率越高越好做实际项目更不能被数据集的表面结果骗了。先花半小时搞清楚标签怎么来的再花一小时认真设计验证方案效果比花一整天调参强得多。如果你也想拿这个数据集练手我的建议是先别急着跑模型静下心把每一条样本的分布看一遍把混淆矩阵研究明白你学到的东西会比模型本身多得多。