简介:一套基于支持向量机(SVM)的降水量预测模型代码包,面向机器学习、人工智能及数据挖掘方向的初学者和研究人员,可用于算法复现、实验对比和毕业设计参考。资源内共 54 个文件,以 26 个 .m 主程序为核心,覆盖数据读取、特征处理、模型训练、预测与结果评估;配以 .mat 样本数据、.c/.h 辅助源码和 .mexw32 编译模块,便于理解 MATLAB 环境下 SVM 的调用方式与工程化打包流程;另有 readme、说明文档和若干 txt 文本记录,帮助快速定位关键参数。整个压缩包仅 291KB,结构轻量,适合快速下载、本地调试与二次开发。该资源已有 2423 人学习下载,是结合气象预测场景入门 SVM 建模的不错参考;代码从数据准备到指标输出链路完整,可直接修改数据路径与超参数,迁移到其他回归或分类任务中。
1. 一条SVM降水量预测代码跑通之前,先想清楚这三件事
把“基于SVM支持向量机算法的降水量预测模型代码”这个标题丢给我的时候,我第一反应不是什么核函数、什么调参技巧,而是先问一句:你手里那份日降水数据,零值占比到底有多高?大部分气象站的逐日降水记录,无雨天数能占到60%以上,这种分布会让SVM回归模型学成一个“总是预测平均雨量”的废物。网上能搜到的示例代码,多半是sklearn里三行SVR加一个随机切分,跑出来R²挺好看,挪到第二年的真实数据上立刻被打回原形。这篇文章不打算复述某份现成代码,而是按一个能落地的顺序重写一遍:降水序列怎么变成SVM能吃进去的监督样本,C、epsilon、gamma这些参数怎么设,以及模型上线前最容易翻车的几个现场。适合手里有气象站逐日降水记录、想立刻跑通并判断结果能不能用于业务的人。
2. 为什么SVM能预测降水量:先搞懂SVR回归与滑窗特征
2.1 SVR不是分类器:ε不敏感带、支持向量与“稀疏解”直觉
很多人一听到支持向量机,脑子里全是分类超平面、硬间隔那一套。做回归的SVR,逻辑其实不一样:分类是找一个超平面把两类样本尽可能拉开;而SVR是找一个“管道”,让大部分样本落在管道内部,只有管道外面的样本才产生损失。这个管道的半径叫ε,管道的中心就是回归曲线。落在带内的样本不参与模型决策,SVR最终只由管道边界上一小撮样本决定,所以它的解是稀疏的。落到降水预测上,这个特性很贴合实际:无雨的日子大多落在带内,真正影响模型形态的是那些强降水过程,模型不需要记住每一个晴天。
sklearn里的SVR底层走的是libsvm的SMO求解,可以把它理解成一种沿着坐标方向的梯度式迭代,用户不需要手写优化过程。但搞清楚这个背景能帮你定位问题:SVR的训练开销随样本量增长非常快,核矩阵计算复杂度差不多是O(n²)到O(n³),这也是后面“样本过万就卡死”那类翻车现场的根本原因。
那为什么降水预测适合用SVM而不是一上来就上XGBoost或Prophet?气象站点的有效记录往往只有几百到几千天,特征量在十几个到几十个之间。SVM在小样本、非线性场景下泛化能力比树模型稳,RBF核能把“前几天降水组合导致今天有雨”这类非线性关系映射到高维空间。树模型在大样本下对极端降水拟合更猛,但站点数据没那么多,SVM反而更容易被训练充分。
2.2 把单变量降水序列变成监督学习样本:滑窗法示例代码
SVM不理解“时间”,它只吃特征矩阵和标签。如果直接把日期序号当特征丢进去,模型学到的只是“月份和降水的大致关系”,完全丢掉了“昨天下了多少雨”这个最强信号。所以第一步必须把单变量时间序列转成监督学习格式,方法是滑窗法:用过去N天的降水值作为特征,预测当天的降水量。这也是单变量时间序列预测模型最常见的落地做法。
下面这段代码把降水列按滞后期拆成多列特征,同时生成一个三天滑动均值。
import pandas as pd import numpy as np def build_supervised(df, lag=3, window=3): data = df.copy() # 滞后特征:第t行的precip_lag_1等于t-1日的真实降水 for i in range(1, lag + 1): data[f"precip_lag_{i}"] = data["precip"].shift(i) # 滑动均值:先shift(1)再rolling,确保只用历史信息 data["precip_mean"] = data["precip"].shift(1).rolling(window, min_periods=1).mean() data = data.dropna() return data逻辑上要盯住两个地方。第一,shift(i)是把整个序列往下挪i行,所以某一行里的precip_lag_1在时间上永远早于当天的标签,不会泄露未来。第二,rolling(window)默认会把当前时刻也纳入均值计算,所以必须先执行一次shift(1)再做滚动,否则特征里就混进了当天的真实值,测试阶段模型等于作弊。dropna()会丢掉序列开头没有完整历史的那几行,缺失行数等于lag值。
lag和window是滑窗法的两个核心超参。日降水短期自相关一般能持续2到7天,梅雨、台风这类天气过程有7到14天的记忆。我一般会先把lag=3跑通全流程,然后分别试5和7做对比;滑动窗口window=3对日降水已经够用,太长的窗口会把短时强降水信号平均掉。
2.3 核函数怎么选:线性核、RBF核与样本量的匹配
SVM支持向量机做回归时,核函数决定了模型能表达多复杂的非线性关系。线性核等价于对历史降水做加权平均,训练快、参数少,适合特征维度较高而样本量有限的情况。RBF核是气象预测里最常用的选择,它能表达“前三天连续小雨之后第四天出现大雨”这类组合式非线性关系。各核的适用场景可以粗略按下表判断。
| 核函数 | 适用场景 | 关键参数 | 训练速度 |
|---|---|---|---|
| linear | 特征较多、样本数千级以下 | C | 快 |
| rbf | 中小样本、特征十几到几十维 | C、gamma、epsilon | 中等 |
| poly | 少用,降水数据容易出现数值溢出 | C、degree | 慢 |
需要注意,RBF核有两个玄学参数:C和gamma。C控制对误差的惩罚,C越大模型越会拼命拟合训练集里的极端降水过程;gamma控制核的径向作用范围,gamma越大模型只关注离预测点很近的样本,容易把单次暴雨过程当成全局规律。在后面的调参章节里,这两个参数会放进网格搜索一起优化。如果样本量超过两万,RBF核的训练时间会暴涨,这时优先考虑线性核,或者干脆考虑XGBoost回归预测模型。
如果手里只有降水这一列数据,单变量模型完全能做,而且通常能跑出一个有意义的基线。气象站的观测记录里往往还有气温、气压、湿度等列,建议先单变量跑通,再加一两个外生变量做对照实验。多加变量不保证变好,但“前一天闷热”这类湿度信号,确实能让模型学到降水前兆。
3. 示例代码讲解:数据清洗、特征构造与时序切分
3.1 读入降水记录:缺失与零值是两种完全不同的状态
第一步是把气象站原始记录读进来,先看两个比例:缺失率和零值占比。这两个数字直接影响后面的建模策略。
import pandas as pd df = pd.read_csv("daily_precip.csv", parse_dates=["date"], index_col="date") df = df.sort_index() print("缺失率:", df["precip"].isna().mean()) print("零值占比:", (df["precip"] == 0).mean())零值占比有物理含义,它表示“当天没下雨”,是有效观测;而NaN表示“这一天没有记录”,是缺测。这两者绝不能混为一谈。很多人图省事直接fillna(0),结果就是给模型灌进去大量虚假的无雨日,强降水的信号被进一步稀释。
缺测的处理要看连续缺测的长度。连续缺测不超过3天,线性插值是可以接受的;连续缺测超过5天,插值出来的曲线就是纯黑匣子,没有任何物理依据,我一般直接把这段整段剔除。
df["precip"] = df["precip"].interpolate(limit=3, limit_area="inside") df = df.dropna(subset=["precip"])interpolate(limit=3)表示最多只对连续3天的缺失做插值,超出部分保持NaN,随后被dropna剔除。limit_area="inside"限制只在序列中间插值,不延伸序列头部和尾部。跑完这段代码后,零值占比如果仍然超过60%,就要认真考虑后面讲的两段式方案,直接做SVR回归大概率只得到一个预测均值。
3.2 构造特征列:滞后降水、滑动窗口与外生变量
特征构造是整个流程里最影响模型上限的一步。滞后项捕获短期的自相关:昨天下了20毫米,今天还有没有雨?滑动窗口捕获天气过程的记忆:过去三天累计降雨量达到多少后,河流容易达到警戒水位。以下是我在降水预测里常用的特征集合。
# 滞后1到7天降水 for i in range(1, 8): df[f"precip_lag_{i}"] = df["precip"].shift(i) # 滑动均值与滑动最大值,注意先shift(1)再rolling for w in (3, 7): df[f"precip_mean_{w}"] = df["precip"].shift(1).rolling(w, min_periods=1).mean() df[f"precip_max_{w}"] = df["precip"].shift(1).rolling(w, min_periods=1).max() # 季节编码:用正弦余弦把一年中的第几天变成连续信号 doy = df.index.dayofyear df["doy_sin"] = np.sin(2 * np.pi * doy / 365.25) df["doy_cos"] = np.cos(2 * np.pi * doy / 365.25) # 外生变量示例:如果表里有湿度列,取前一天湿度 if "humidity" in df.columns: df["humidity_lag_1"] = df["humidity"].shift(1) df = df.dropna() feature_cols = [ "precip_lag_1", "precip_lag_2", "precip_lag_3", "precip_lag_4", "precip_lag_5", "precip_lag_6", "precip_lag_7", "precip_mean_3", "precip_mean_7", "precip_max_3", "precip_max_7", "doy_sin", "doy_cos", ] if "humidity_lag_1" in df.columns: feature_cols.append("humidity_lag_1") X = df[feature_cols].values y = df["precip"].values.reshape(-1, 1)precip_max_7这个特征容易被忽略,但它很有用:如果过去7天里出现过一次30毫米以上的强降水,土壤饱和、产流条件变化,后续降雨的响应会明显不同。SVM没法自己“想起来”几天前那场暴雨,你得把最大值显式喂给它。季节编码用正弦余弦成对出现,是因为单用dayofyear会把1月1日和12月31日变成数值上的两个极端,而正弦余弦能把首尾连接起来,模型不会认为这两个日期相差很远。注意特征列表的顺序要写死,后续做滚动预测时要按同样的顺序构造每一行。
3.3 归一化与切分:为什么时间序列不能用train_test_split
SVR对特征尺度极其敏感。降水的量纲是毫米,湿度的量纲是百分比,气温的量纲是摄氏度,如果直接丢进模型,核函数算距离时数值大的特征会主导一切。所以要做MinMax缩放,把特征和目标都压到0到1之间。
from sklearn.preprocessing import MinMaxScaler split = int(len(df) * 0.8) X_train_raw, X_test_raw = X[:split], X[split:] y_train_raw, y_test_raw = y[:split], y[split:] scaler_x = MinMaxScaler(feature_range=(0, 1)) scaler_y = MinMaxScaler(feature_range=(0, 1)) X_train = scaler_x.fit_transform(X_train_raw) X_test = scaler_x.transform(X_test_raw) y_train = scaler_y.fit_transform(y_train_raw).ravel() y_test = scaler_y.transform(y_test_raw).ravel()这里有两个必须遵守的纪律。第一,只能用训练集去fitscaler,再transform训练集和测试集;如果先对整个数据集fit_transform再切分,scaler就偷看了测试集的分布,相当于数据泄露。第二,切分必须按时间顺序,前80%训练、后20%测试。很多人习惯直接train_test_split,默认参数会随机打乱数据,让模型用过去的数据预测“未来”,这在时间序列里等于开了天窗。网格搜索阶段也不要再用默认的KFold,应该用TimeSeriesSplit,它的每一折都保证训练集在时间上严格早于验证集。
4. 训练SVM回归模型:C、epsilon、gamma与网格调参
4.1 最小可用的SVR训练与参数初值
特征构造完,先别急着调参,用一组保守的初始参数把全流程跑通。以下代码训练一个RBF核的SVR回归模型。
from sklearn.svm import SVR model = SVR( kernel="rbf", C=1.0, epsilon=0.1, gamma="scale", tol=1e-3, max_iter=10000, ) model.fit(X_train, y_train)参数的初始值不要乱拍。C=1.0是sklearn默认,对降水预测来说是个不过不失的起点。epsilon=0.1的含义要结合目标变量已归一化成0到1来理解:模型允许大约0.1的误差而不产生损失。如果epsilon设成0.001,模型会把大量零降水日当作必须拟合的对象,支持向量的数量暴增,训练变慢,还容易过拟合;设成0.1则给模型留了余量,注意力集中在“是否下雨”和“雨量级”上。gamma="scale"表示让sklearn根据特征数量自动计算一个合理的初始值,这一步避免了gamma初值拍脑袋。
max_iter=10000建议显式写出来,否则数据量稍大时libsvm可能提前停止却不报错。如果训练完成后警告说未收敛,优先检查特征是否做了归一化,而不是盲目调大迭代次数。
4.2 TimeSeriesSplit网格搜索,用MAE而不是R²评分
跑通基线后,用网格搜索找一组更稳的参数。这里有两个容易踩的坑:交叉验证要用时间序列切分,评分要用MAE而不是R²。
from sklearn.model_selection import GridSearchCV, TimeSeriesSplit param_grid = { "C": [0.1, 1, 10, 100], "gamma": [0.001, 0.01, 0.1, "scale"], "epsilon": [0.05, 0.1, 0.2], } tscv = TimeSeriesSplit(n_splits=5) gs = GridSearchCV( SVR(kernel="rbf"), param_grid, scoring="neg_mean_absolute_error", cv=tscv, n_jobs=-1, ) gs.fit(X_train, y_train)不使用默认的KFold是因为降水序列有强时间相关,随机切分会让模型在某几折里看到“未来”的数据,验证分数虚高,换到真实未来数据立刻崩掉。TimeSeriesSplit把训练集按时间切成5段,每次用前几段训练、后一段验证,模拟的是真实的预测场景。
评分为什么用平均绝对误差而不是R²?降水的分布是强右偏的,一年里可能只有十几天大雨,R²的计算会平方放大极端误差,导致网格搜索专门去拟合那几次暴雨过程,反而牺牲了绝大多数中小雨日的精度。水利调度这类业务最怕的就是大雨报成小雨,MAE直接衡量平均预报偏差,更贴合落地需求。如果样本量超过一万,RBF核的网格搜索会非常慢,可以先手动测C=1, gamma=0.01这一组,再围绕它缩小搜索范围。
4.3 逆变换与分雨日评估:模型好不好的真正口径
网格搜索会把最佳模型自动refit到全量训练集上,直接用gs.predict得到归一化尺度下的预测值,然后反变换回毫米。
y_pred_scaled = gs.predict(X_test) y_pred = scaler_y.inverse_transform(y_pred_scaled.reshape(-1, 1)).ravel() y_test_mm = scaler_y.inverse_transform(y_test.reshape(-1, 1)).ravel() from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae_all = mean_absolute_error(y_test_mm, y_pred) rmse = np.sqrt(mean_squared_error(y_test_mm, y_pred)) r2 = r2_score(y_test_mm, y_pred) hit = y_test_mm > 0.1 mae_rainy = mean_absolute_error(y_test_mm[hit], y_pred[hit]) print(f"全量MAE: {mae_all:.2f} mm, 有雨日MAE: {mae_rainy:.2f} mm, RMSE: {rmse:.2f} mm, R2: {r2:.3f}")这里的关键口径是“有雨日MAE”。全部样本的MAE很容易被大量零降水日拉低,模型只要疯狂报零,全量MAE也会很好看;但如果单独统计实际降水量超过0.1毫米的日子,模型的真实水平立刻现形。如果全量MAE是1.2毫米,有雨日MAE却高达6毫米,这个模型在业务上基本不可用。
5. 降水预测最容易翻车的5个现场:现象、原因、处理
5.1 预测曲线是一条水平线
现象:测试集的预测值全部趴在某个常数附近,比如1.8毫米,强降水过程被完全抹平,预测曲线像一条直线。
原因:降水数据零值占比过高,SVR的ε不敏感带把绝大多数样本“吸”到了中心区域。模型发现,无论特征怎么变,预测一个中间值都能让大部分样本落在带内,损失最小。
解决:改用两段式。先用SVM分类判断“是否下雨”,再对雨量>0.1毫米的日子单独训练SVR回归。分类器的代码很简单:
y_binary = (y_train_raw.ravel() > 0.1).astype(int) clf = SVR(C=1.0, gamma="scale") clf.fit(X_train, y_binary)SVR也可以当分类器用,输出的连续值靠近0表示无雨、靠近1表示有雨,取0.5作为阈值。第一段只回答“下不下”,第二段只回答“下多少”,两个模型各管一摊,比硬让一个SVR同时拟合零膨胀和强降水两端要稳得多。
5.2 训练集R²很漂亮,测试集R²为负
现象:训练集上R²超过0.9,测试集上R²变成负数,甚至比“每天预测历史均值”还差。
原因:十有八九是切分出了问题。要么用了默认的train_test_split随机打乱了时间顺序,要么特征构造时忘了shift(1)就做了滚动均值。还有一种隐蔽情况:数据原本没按日期排序,读进来之后没有sort_index,然后按行号切分,导致训练集和测试集日期交错。
解决:检查三件事——读入后是否按时间排了序,切分前是否禁止了shuffle,所有滚动特征是否都是在shift之后算的。再用一个最笨的基线模型做对照:拿历史同期均值当预测值,算它的MAE;SVM的测试集MAE如果连这个基线都打不过,说明特征或切分仍有问题,先别怀疑模型。
5.3 样本量一上万,RBF核SVR慢到怀疑人生
现象:几千个样本训练只要几秒;数据量过万之后,单次训练要几十秒,再叠加网格搜索,跑到凌晨都出不来,内存也飙到几个G。
原因:SVR求解时核矩阵的计算开销随样本量呈平方甚至立方增长。SVM的梯度下降类优化在特征上收敛很快,但核矩阵是绕不过去的瓶颈。
解决:按顺序试这三招。第一,降采样,保留最近两年完整数据,覆盖四季即可;第二,换线性核,配合标准化后效果往往不输RBF;第三,压缩网格规模,C、gamma各取3个值,一共18组。如果样本量超过5万,SVM已经不是一个合适的选择,该换XGBoost或Prophet就用,没必要和SVM硬刚。
5.4 反归一化后出现负降水
现象:测试集预测结果里出现-0.3毫米、-1.2毫米这种物理上不存在的负值。
原因:目标变量用MinMaxScaler压缩在0到1之间,但模型的预测值在边界外推时可能小于0,反变换回毫米就成了负数。
解决:最直接的办法是对预测结果做截断:
y_pred = np.clip(y_pred, a_min=0, a_max=None)但clip只是掩盖症状。更根本的解法是构造目标变量时做log1p变换,让模型在log空间里回归,反变换时用np.expm1还原。log变换能压缩小雨和暴雨之间的巨大尺度差,SVM回归出来的结果也更少出现负值。
5.5 网格搜索选出来的参数,换一年数据就废
现象:网格搜索在验证集上表现很好,参数选定后叠到完整测试集上,性能急剧下降,而且选出来的C和gamma往往特别大。
原因:两个问题叠加。一是用了默认KFold加R²评分,时间泄露和极端值主导让搜索过程过拟合了验证集。二是搜索范围给得太大,C和gamma的上限设到了1000甚至10000,模型有机会去死记某几次强降水个例。
解决:交叉验证换成TimeSeriesSplit,评分换成MAE,同时把参数范围限制在合理区间。C建议在0.1到10之间,gamma在0.001到0.1之间,因为特征已经归一化,gamma超过0.5就意味着模型只看离预测点极近的那几个样本,过拟合几乎是必然的。
6. 把SVM降水预测模型推进到业务:滚动预测与两段式判断
6.1 未来7天滚动预测:特征窗口每一步都替换成最新值
单日预测模型训练完毕,业务上往往要的是未来3到7天的降水量。滚动预测的做法不算复杂:预测出明天的雨量,把它当作“昨天的历史值”拼进特征窗口,再预测后天,如此循环。
def make_row_from_recent(recent, doy, feature_cols): r = list(recent)[-7:] row = { "precip_lag_1": r[-1], "precip_lag_2": r[-2], "precip_lag_3": r[-3], "precip_lag_4": r[-4], "precip_lag_5": r[-5], "precip_lag_6": r[-6], "precip_lag_7": r[-7], "precip_mean_3": np.mean(r[-3:]), "precip_mean_7": np.mean(r), "precip_max_3": np.max(r[-3:]), "precip_max_7": np.max(r), "doy_sin": np.sin(2 * np.pi * doy / 365.25), "doy_cos": np.cos(2 * np.pi * doy / 365.25), } return np.array([row[c] for c in feature_cols])每推进一步,就把刚生成的预测值追加到recent里,再用新的doy重新构造特征。这一步要特别小心特征列顺序,feature_cols必须和训练时完全一致,差一列模型就全错。滚动预测有个固有的折叠误差:步数越多,误差累积越大。我一般最多滚7天,超过7天直接改报天气过程趋势,不再报具体毫米数。
6.2 零膨胀数据的两段式:先用SVM判断下不下雨,再回归雨量
如果零值占比超过60%,最后再强调一次优先做两段式。先用SVM分类器判断未来24小时是否出现有效降水,阈值可以按业务需求调:防汛取0.1毫米,农业灌溉取5毫米。第一步判断通过,再调用回归模型预测具体雨量;第一步判断不通过,直接报0。
这种结构的落地经验是:分类器的准确率通常会比较好看,但重点关注的是漏报率,也就是该报的雨没报出来。分类器阈值可以往保守方向调,宁肯空报也不漏报,这是防汛调度里用血泪换来的教训。回到最初那个问题,值不值得照着这个方向投入?只要零值占比高,两段式几乎是SVM降水预测绕不开的解法。
我现在拿到这种“基于SVM支持向量机算法的降水量预测模型代码”的需求,最先看的一定是数据分布和切分方式,而不是急着调核函数。零膨胀不解决,C调出花来也是白搭。希望这些经验和代码能帮你少走一段弯路。
本文还有配套的精品资源,点击获取