简介:这是一份面向水利领域数据分析与机器学习研究者的完整实践资源,聚焦水电站入库流量预测问题,基于LightGBM梯度提升算法,使用Python完成从数据预处理、特征工程到模型训练与评估的全流程实现。资源以初赛与决赛赛题任务为线索,提供神经网络对比方案、预测报告文档及特征重要性可视化结果,可直接复现或迁移至相似水文时序预测场景。压缩包共25个文件,约7.76MB,包含8个xlsx入库流量与降雨数据表、4个csv预测提交文件、4个txt数据说明与运行说明、2个ipynb模型代码、1个docx详细报告及辅助md/png/html文件,目录按初赛与决赛分层组织,结构清晰。已有73人学习使用。对希望掌握LightGBM在水文领域应用、或备赛工程类数据竞赛的读者,这套资料提供了完整的赛题数据、代码与报告,能够节省大量数据整理和调参时间。
1. 用LightGBM预报水电站入库流量:这套“源码+数据集+报告”到底能解决什么问题
做水电站调度的人最怕半夜接到电话:上游来水突然从 800 涨到 2600,而手里的人工预报还停留在昨天下午的雨量数据上。传统水文模型要率定产流参数、汇流参数,一套下来少说两周,遇到资料短缺的中小流域根本跑不动。用Python实现基于LightGBM的水电站入库流量预测,核心思路是把入库流量当作一个可由历史流量、降雨、水位等因素回归出来的序列问题——不追求物理机制完全透明,只追求“未来6小时、24小时的入库流量能不能报准”。这套方案里,源码解决“怎么训练和预测”,数据集解决“拿什么训练”,报告文档解决“模型怎么验收、怎么交接给调度值班员”。适合水文预报工程师、水电站信息化项目承接方、以及做时序回归的算法工程师参考。它不替代洪水预报方案,但足以把常规入库流量预报从“靠人盯”变成“靠模型盯”。
2. 入库流量预测项目的数据准备:建数据集比调参更值得花时间
2.1 模型输入需要哪些数据源和字段
要做入库流量预测,首先得搞清楚“入库流量”这个目标值怎么来的。常见做法是用水库水位库容曲线反推:库水位变化乘以库面面积,加上出库流量,反算出时段入库流量。如果电站已经建了水情自动测报系统,数据库里通常直接有入库流量序列,那就省事很多。没有的话,就要用坝前水位和总出库流量自己推。
除了目标值本身,模型还需要三类输入特征。第一类是降雨量,最好用流域面雨量而不是单站雨量——把流域内多个雨量站的观测做泰森多边形加权或算术平均,能显著减少单站降雨的代表性偏差。第二类是上游站点的入库流量或水位,尤其是有上游梯级电站的流域,上游出库就是下游入库的主要来源之一。第三类是时间特征,比如月份、小时、是否汛期,这些看起来简单,但对捕捉季节性规律很有用。还有一个容易忽略的字段是气温,高寒地区有融雪性来水,气温对入库流量的影响不能忽略。数据集至少要覆盖一个完整的水文年,最好包含两到三个汛期,否则模型没见过大洪水,预报极端值就是空谈。
2.2 数据集清洗与缺失值处理:先看数据再谈模型
拿到原始数据后先别急着喂给LightGBM。水文遥测数据经常会缺测,遥测站断传、RTU故障、通信干扰,都会导致流量序列出现空洞。入库流量又是从水位反推的,水位计的精度直接影响目标值的可信度。我的处理顺序是:先做物理合理性检查,再做统计清洗,最后才做插补。
import pandas as pd import numpy as np # 读取原始遥测数据,time列统一成北京时间 df = pd.read_csv('reservoir_data.csv', parse_dates=['time']) df = df.set_index('time').sort_index() # 1. 物理合理性检查:入库流量不能为负,小时涨落变化不能超过设定阈值 df.loc[df['inflow'] < 0, 'inflow'] = np.nan df.loc[df['inflow'] > 50000, 'inflow'] = np.nan # 超出电站历史最大入库 # 2. 跳变检查:相邻时段流量差超过3倍标准差,标记为可疑 inflow_diff = df['inflow'].diff().abs() diff_threshold = 3 * inflow_diff.std() df.loc[inflow_diff > diff_threshold, 'inflow'] = np.nan # 3. 缺失值插补:线性插值为主,长于6小时的连续缺失标记为无效段 df['inflow'] = df['inflow'].interpolate(method='linear', limit=6) df['rainfall'] = df['rainfall'].fillna(0) # 降雨无雨即0,不参与插补 # 4. 删除仍包含NaN的连续长缺测段,避免插补出的假数据误导模型 df = df.dropna(subset=['inflow'])这段代码的逻辑分四层:先清理明显不合理的物理值,负流量直接置空;再用统计阈值识别跳变,水文观测里流量陡增陡降虽然存在,但超过三倍标准差的跳变大多是遥测误码;线性插补适合小时尺度的短期缺测,超过6小时连续缺测就要慎重,插出来的是“看起来平滑但没有物理信息”的假数据;最后删除长缺测段而不是强行插补,因为LightGBM不吃缺失值这一套,它虽然能处理NaN,但目标值里的插补误差会被模型当成真实规律学进去。降雨缺测填0是因为对于中小流域,小时尺度降雨缺测大概率就是无雨,填均值的错误更大。
2.3 时序数据集划分:为什么不能用随机打散
很多新手在这个项目上第一个翻车点就是数据集划分。入库流量序列是强自相关的,今天的流量和昨天的流量高度相关。如果像普通分类任务那样随机打散划分训练集和测试集,会出现一个严重问题:训练集里包含测试集时间前后的样本,模型相当于“偷看”了未来信息,测试指标会虚高得离谱,一到真实预报就崩盘。这就是典型的数据泄漏。
# 正确的时序划分:按时间顺序切分,前70%训练、后30%测试 train_end = int(len(df) * 0.7) train_df = df.iloc[:train_end] test_df = df.iloc[train_end:] # 如果想做更稳妥的验证,用TimeSeriesSplit做多折时序交叉验证 from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=3) for train_idx, val_idx in tscv.split(df): train_fold = df.iloc[train_idx] val_fold = df.iloc[val_idx] # 每折都是按时间顺序的,后面的折永远在时间上更“未来”这里的核心认知是:时序预测的验证集必须严格晚于训练集。TimeSeriesSplit和KFold的区别就在于前者永远不会把未来样本放进训练集,后者会。我用的时候一般会留出最后两年的数据做最终测试,前面的数据再做折内验证。注意一个隐藏问题:跨年验证时,训练集和验证集之间隔的时间越长,模型在验证集上的表现越接近真实部署场景——因为水位-流量关系会随库区淤积、上游人类活动缓慢漂移。所以做时序划分时,不要只切一次就定论,要留出“最像未来”的那段数据做最后检验。
3. 特征工程:把降雨、水位和历史流量变成LightGBM吃得懂的输入
3.1 滞后特征:入库流量预测最核心的输入
入库流量是有记忆的过程。一场降雨后,产流和汇流都需要时间,今天下午的雨可能要到明天凌晨才反映到入库流量上。滞后特征就是把过去若干小时的流量和降雨作为当前时刻的输入特征。滞后阶数的选择取决于流域面积和汇流时间:小流域汇流快,滞后6到12小时就够了;大流域汇流慢,需要考虑滞后24到72小时。我一般先做自相关分析,画出滞后1到72小时的ACF图,看自相关系数什么时候衰减到0.5以下,截断点就是滞后阶数的参考上限。
def build_lag_features(df, feature_cols, lags): """构建滞后特征,返回新的DataFrame""" df = df.copy() for col in feature_cols: for lag in lags: df[f'{col}_lag_{lag}h'] = df[col].shift(lag) return df # 流量滞后24小时以内的特征,降雨滞后到48小时(考虑汇流时间) df = build_lag_features(df, feature_cols=['inflow'], lags=[1, 3, 6, 12, 24]) df = build_lag_features(df, feature_cols=['rainfall'], lags=[3, 6, 12, 24, 48])滞后特征的关键参数是lag列表。流量滞后1小时、3小时、6小时能覆盖快速汇流的响应;12小时、24小时覆盖一般流域的日调节过程;降雨滞后到48小时是因为很多流域的降雨-流量响应存在明显延迟。注意shift之后前几个时间步会产生NaN,训练时要么删除,要么填充为所在列的前向均值。LightGBM能处理NaN,但特征列里NaN太多会把树的切分点搞乱,我习惯直接用前向填充,填0也行但效果差一些。
3.2 滚动统计特征:用滑动窗口捕捉趋势和态势
滞后特征只能告诉模型“过去某时刻是多少”,但无法表达“流量正在上涨还是回落”“涨势多猛”。滚动统计特征用滑动窗口内的均值、最大值、最小值、标准差来描述近期态势,相当于把流量过程线的形态信息喂给模型。窗口长度同样要匹配流域响应时间。
def build_rolling_features(df, col='inflow', windows=[6, 12, 24]): """滚动窗口统计特征,捕捉涨落趋势""" df = df.copy() for w in windows: df[f'{col}_rolling_mean_{w}h'] = df[col].rolling(window=w).mean() df[f'{col}_rolling_max_{w}h'] = df[col].rolling(window=w).max() df[f'{col}_rolling_std_{w}h'] = df[col].rolling(window=w).std() # 涨率特征:比24小时前涨了多少 df['inflow_rise_24h'] = df['inflow'] - df['inflow'].shift(24) return df滚动窗口最常用的是6小时、12小时、24小时三档。6小时窗口捕捉短时涨落,24小时窗口表达日尺度的趋势,标准差则反映来水的平稳程度——这对于判断是单峰洪水还是复式洪峰很有区分力。inflow_rise_24h这个特征很直观,但注意它和直接滞后24小时的特征高度相关,LightGBM对这种冗余不敏感,不过如果后面做特征筛选,这类衍生特征可以优先砍掉。有一点我踩过坑:滚动特征不能跨日界,比如窗口从今天14点回看24小时,包含了昨天14点到今天14点,这个没问题,但不要用那种会引入未来信息的中心窗口均值。
3.3 周期特征与外部变量:月份、小时这些“廉价特征”为什么有效
水文过程有明显的周期节律。汛期和枯期来水差异巨大,一天之内径流也可能受融雪、发电调峰影响呈现周期性波动。LightGBM是树模型,它无法像神经网络那样自动学习sin/cos形式的周期表示,所以要把周期拆成语义特征直接喂进去。
def build_time_features(df): """构建时间周期特征""" df = df.copy() df['hour'] = df.index.hour df['month'] = df.index.month df['day_of_year'] = df.index.dayofyear # 汛期标记:根据流域气候特征设置,这里假设6-9月为汛期 df['is_flood_season'] = df['month'].apply(lambda m: 1 if 6 <= m <= 9 else 0) # 季度作为类别特征 df['season'] = df['month'] % 12 // 3 + 1 return df时间特征里最重要的是month和hour,这两个字段LightGBM会当类别特征用。day_of_year可以作为连续特征,让模型学到年度内的渐进变化。is_flood_season是个强先验特征,虽然month特征已经隐含了汛期信息,但显式标注后模型更容易在树分裂时找到关键节点。这类特征看起来“太简单”,实际对树模型的增益非常明显,尤其当训练数据只有两三个汛期时,光靠month学汛期边界比靠tree自己组合要可靠得多。
3.4 特征构建的完整流程与顺序细节
在实际项目中,特征构建不是一次性写完函数就完事。我一般的顺序是:先合并外部数据源(上游站流量、面雨量、气温),再补全缺失值,然后构建滞后特征、滚动特征、时间特征,最后统一切分训练测试集。特征构建阶段最容易忽略的是时间对齐——上游站的观测时间可能和本站有系统偏差,遥测站上报间隔也可能不统一。处理办法是统一重置采样到小时刻度,用resample('1H').mean()聚合,上游流量用后向填充对齐到本站时刻。
# 上游站流量与本站对齐:按小时重采样后合并 upstream = pd.read_csv('upstream_inflow.csv', parse_dates=['time']) upstream = upstream.set_index('time').resample('1H').mean() upstream = upstream.reindex(df.index, method='ffill') # 合并到主特征表 df = df.join(upstream[['upstream_inflow']]) df[['inflow', 'upstream_inflow']] = df[['inflow', 'upstream_inflow']].fillna(method='ffill')这段代码解决的是多源数据时间对齐问题。resample('1H').mean()把上游站可能不规则的观测序列规范成小时刻度,reindex加ffill保证每个时刻都有上游站数据。特别提醒:如果在reindex时把上游站的未来数据带进来——比如上游站时间比本站早一个小时,直接合并会引入未来信息。对齐时一定要确认各站时区和上报延迟,这类问题在测试集上很难发现,因为树模型会把这种“伪特征”学得很准。
4. LightGBM回归模型训练:参数先验、早停策略与调参路径
4.1 LightGBM回归模型的核心参数先验,照着设不会翻车
把特征做好之后,模型训练本身反而是最顺的一步。LightGBM作为GBDT框架,对表格数据的拟合能力在中小样本场景下几乎是无脑选择。但参数设置不能照搬分类任务的默认配置,回归任务有自己的侧重点。
import lightgbm as lgb lgb_params = { 'objective': 'regression', 'metric': 'mae', 'learning_rate': 0.03, 'num_leaves': 31, 'max_depth': 5, 'min_child_samples': 30, 'subsample': 0.8, 'colsample_bytree': 0.8, 'reg_alpha': 0.1, 'reg_lambda': 0.1, 'verbose': -1, 'seed': 42 }这里的每个参数都有明确意图。objective选regression是均方误差损失,适合连续流量预测;metric选mae是因为入库流量预测更关注平均绝对偏差,RMSE会被极端洪水样本带偏,但下面会说明这不是绝对的——如果你更担心大洪水预报误差,可以换成rmse。learning_rate设为0.03是给后面的早停留空间,学习率太大会让最优迭代轮数过早收敛;num_leaves默认31可以,配合max_depth=5能有效防止叶节点过多导致过拟合;min_child_samples=30控制每个叶子最少样本量,水文数据噪声大,这个值设太小叶子会学进噪声;subsample和colsample_bytree都是0.8,每次建树只用80%的样本和80%的特征,增加随机性换泛化。
4.2 训练主流程:早停是最重要的一个参数
训练时要给LightGBM配早停(early stopping)。早停的意思是:每轮迭代后在验证集上计算指标,如果连续N轮指标不再改善,就停止训练并返回最优模型。对于时序预测,验证集用前面说的时序切分出的最后一段数据,不能是随机抽样验证集。
# 特征列列表 feature_cols = [c for c in df.columns if c not in ['inflow']] X = df[feature_cols].values y = df['inflow'].values # 按时间切分(这里用0.7作为训练/验证分界) split_idx = int(len(X) * 0.7) X_train, X_val = X[:split_idx], X[split_idx:] y_train, y_val = y[:split_idx], y[split_idx:] # LightGBM原生接口训练,使用早停 train_data = lgb.Dataset(X_train, label=y_train) val_data = lgb.Dataset(X_val, label=y_val, reference=train_data) model = lgb.train( lgb_params, train_data, num_boost_round=3000, valid_sets=[val_data], callbacks=[lgb.early_stopping(stopping_rounds=100), lgb.log_evaluation(100)] ) # 查看最优迭代轮数 print(f"最佳迭代轮数: {model.best_iteration}") print(f"验证集最佳MAE: {model.best_score['valid_0']['mae']}")这段代码的关键是early_stopping(stopping_rounds=100)——如果连续100轮验证集MAE没改善就停。3000轮是上限,实际上早停通常在600到1200轮之间就触发了。log_evaluation(100)表示每100轮打印一次训练日志,方便观察训练曲线。这里用lgb.train原生接口而不是LGBMRegressor的sklearn封装,原因在于原生接口能直接拿到best_iteration和best_score,这两个值在后续写报告文档时非常关键。
训练完后,用feature_importance查看哪些特征真正起作用,常见的做法是输出分裂增益(gain)排序:
# 特征重要性排序(gain类型) importance_df = pd.DataFrame({ 'feature': feature_cols, 'gain': model.feature_importance(importance_type='gain') }).sort_values('gain', ascending=False) print(importance_df.head(15))4.3 调参路径:网格搜索不是第一步
很多读者关心的“怎么调参”,我的回答是:先跑通基线,再按顺序调三个方向。第一步调num_leaves和max_depth,这两个参数控制模型容量,容量太大必过拟合,验证集MAE会在训练后期明显回升。第二步调min_child_samples,从30往100方向试,数值越大越保守。第三步调subsample和colsample_bytree,如果验证集和训练集指标差距大,就把这两个值从0.8往0.6方向降。学习率一般不调,0.03到0.05之间选一个就够,靠num_boost_round和早停配合。
# 用Optuna做精简参数搜索,减少人工试错 import optuna def objective(trial): params = { 'objective': 'regression', 'metric': 'mae', 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1), 'num_leaves': trial.suggest_int('num_leaves', 15, 100), 'max_depth': trial.suggest_int('max_depth', 3, 8), 'min_child_samples': trial.suggest_int('min_child_samples', 10, 100), 'subsample': trial.suggest_float('subsample', 0.5, 1.0), 'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0), 'verbose': -1 } cv_scores = lgb.cv( params, train_data, num_boost_round=1000, nfold=3, early_stopping_rounds=50, seed=42 ) return cv_scores['valid mae-mean'][-1] study = optuna.create_study(direction='minimize') study.optimize(objective, n_trials=30)这里有一个容易翻车的点:lgb.cv默认用KFold随机划分,对时序数据会泄漏。要么手动传入TimeSeriesSplit,要么就用前面自己切好的验证集做早停。我用Optuna的时候,n_trials一般控制在30次以内,每次训练跑得很快。如果数据集较大且特征较多,可以先用learning_rate=0.05缩小搜索空间。调参的目的是发现一个稳定的参数组合,而不是追求验证集指标无限低——时序预测的验证集本身就有随机波动。
5. 入库流量预测避坑指南:五个常见问题排查与解决
5.1 时序泄漏:验证集指标虚高,但真实预测一塌糊涂
现象:训练完模型后,验证集MAE非常漂亮,R² 达到0.98,可是一到真实预报就完全失真,尤其是洪峰预报偏差巨大。
原因:除了前面说的随机划分泄漏,还有一个隐蔽的泄漏通道——特征构建阶段用了未来信息。比如构建滚动特征时用了中心窗口(以当前时间为中心,两侧各取n小时),或者合并上游站数据时没有处理好时差,导致未来数据被带进特征。
解决:检查特征构建的每一行代码,确认所有shift和rolling操作都只使用历史数据。用feature_importance排查可疑特征——如果一个特征的重要性异常高,且这个特征是“当前时间之后的观测值”,基本可以断定泄漏。验证方法是故意在特征里加一个“标签滞后一阶”的特征(inflow_shift(-1)),如果模型把这个特征用上了且指标暴涨,说明泄漏通道存在。
5.2 极端洪水样本被淹没在MAE的“平均主义”里
现象:模型在中小流量段表现良好,但历史罕见的大洪水样本完全预测不出来,洪峰被平滑成一个“斜坡”。
原因:MAE和RMSE对每个样本的权重是相等的,但水文数据里大洪水样本极少。一个流域五年可能只有一两次超过5000立方米每秒的洪水,其余时间流量都在500以下。模型为了降低整体误差,会把精力全放在高频的中小流量样本上。
解决:一种做法是设置样本权重,对流量大的样本提高权重,比如weight = 1 + (inflow / inflow_mean) ** 1.5;另一种做法是分位数目标——用objective: 'quantile'配合alpha: 0.9预测高水位分位数,虽然预测的是一组分位数不是确定值,但对调度决策更有参考价值。还有一个实用技巧:把metric从mae换成rmse,RMSE对大误差样本更敏感,模型会潜意识地更重视大流量样本。
5.3 降雨-流量关系的“非平稳”漂移:训练集里的规律可能已经失效
现象:模型在历史测试集上表现很好,但投入实时预报后,某些时段的预测误差突然系统性变大,而且不是随机波动。
原因:流域下垫面变化了。比如上游新建了水库、流域内植被恢复、或者城市不透水面积增加,同样的降雨现在产生的径流过程已经和五年前不一样。又或者水库本身改变了调度方式,入库流量的过程形态被人工干预。
解决:做滚动重训练,每隔一个季度或半年用新增数据重新训练一次。LightGBM支持增量训练,init_model参数可以把旧模型权重作为新模型起点,配合新数据继续迭代几个百轮就能完成更新。更彻底的办法是定期检查数据分布漂移:比较近三个月的流量均值、降雨径流系数和训练集历史均值的差异,如果差异超过20%,就要重新做特征选择和模型训练了。水文模型率定有“延长系列再率定”的做法,机器学习模型同样需要这个习惯。
5.4 特征重要性被高相关特征误导
现象:特征重要性排序里,排在前列的全是近期流量滞后特征,降雨特征重要性很低,让人怀疑降雨对预测没什么用。
原因:流量滞后特征和当前流量高度相关,而且这类特征本身就携带了大量目标值的信息。LightGBM的分裂增益计算偏向于这种“直接复制”类特征,但这类特征在长预见期预报(预报未来24小时以上)中会快速失效——因为等到你真正需要预报的时候,这些滞后值还没发生。
解决:区分使用场景。短期预报(未来1到6小时)可以依赖流量滞后特征,中期预报(未来24小时以上)必须把降雨特征作为主力特征,并且用确定性或集合降雨预报作为输入。排查方法:把流量滞后特征全部去掉,只保留降雨和时序特征重新训练,看验证集MAE损失多少——如果损失不大,说明模型真正学到的规律是降雨驱动而不是历史流量复制。
5.5 训练集和预测集分布漂移:模型在“见过的流量区间”之外失灵
现象:模型上线初期运行正常,后来遇到一次超标准的极端洪水,模型给出的预报数值明显偏小,甚至低于历史实测最大值。
原因:LightGBM树模型没有外推能力。叶节点的输出是落在该叶子上的训练样本均值,如果输入特征组合对应的目标值超出了训练分布范围,模型的输出也只会落在训练集流量范围内,不可能预测出“训练集中从未出现过”的流量。
解决:做模型边界说明。在报告文档里明确标注模型的有效预报范围(比如训练集流量5%到95%分位数区间),超过这个区间就要报人工干预或触发传统水文模型预案。另一种做法是把目标值做对数变换,y_log = np.log1p(y),让模型在log空间里训练,这样大流量样本在log空间中的分布会平缓很多,模型对大流量的外推能力会有一定改善。但要注意,预测完要np.expm1还原,且这个变换会改变误差分布结构,评估指标要在还原后的空间中重新计算。
6. 让项目真正可交付:报告文档组织、模型验证与多步预报技巧
6.1 报告文档的骨架:把技术细节变成别人能复现的步骤
一套完整的源码和数据集交付,如果没有报告文档,价值会打对折。报告文档不是流水账,而是让别人在完全不知道你的代码逻辑的情况下,能够按文档复现出和你一样的结果。我写这类报告的习惯是固定五段式:数据说明、特征清单、参数记录、结果评估、部署说明。
| 章节 | 内容 | 关键细节 |
|---|---|---|
| 数据说明 | 数据来源、时间范围、字段含义、缺失率 | 写明遥测站名、库容曲线版本、时间分辨率 |
| 特征清单 | 所有特征名称、构建逻辑、参数 | 例如inflow_lag_6h:前6小时入库流量 |
| 参数记录 | 最终LightGBM参数、最优迭代轮数 | 含调参过程记录,方便回溯 |
| 结果评估 | 验证集MAE/RMSE/R²、分流量段误差 | 分汛期与枯期、大流量与小流量分别报告 |
| 部署说明 | 运行环境、依赖库版本、预测脚本用法 | 注明Python版本和LightGBM版本兼容性 |
报告里最重要的原则是“可重现”。每条特征都要写清构建公式,每个参数都要写清选择理由,每次调参都要留下历史记录。水文预报项目的特殊性在于它是要被审查的——调度规程要求预报方案有据可查,机器学习的“黑匣子”属性在水利行业会受到挑战。写清楚报告文档,是为了让使用者能在“黑匣子”外面看到透明的输入输出和可信的验证过程。
6.2 多步预报的递归策略:从单步预测到未来24小时
单步预测只能给出下一小时的入库流量,但调度值班需要未来6小时、12小时、24小时的入库流量过程。最常用且稳定的做法是递归预测:把预测出的下一时刻流量作为特征,滚动输入模型,生成后续时刻的预测。递归预测的问题是误差会累积,但它在工程上实现简单,配合降雨预报输入效果也不错。
def recursive_forecast(model, last_row, steps, feature_cols): """递归多步预测,返回未来steps小时的流量序列""" preds = [] row = last_row.copy() for i in range(steps): # 预测下一个时刻流量 next_pred = model.predict(row[feature_cols].values.reshape(1, -1))[0] preds.append(next_pred) # 关键:用预测值更新滞后特征列 row['inflow_lag_1h'] = next_pred for lag in [3, 6, 12, 24]: row[f'inflow_lag_{lag}h'] = row.get(f'inflow_lag_{lag - 1}h', row['inflow_lag_1h']) # 更新滚动特征 row['inflow_rolling_mean_6h'] = row[['inflow_lag_1h', 'inflow_lag_3h', 'inflow_lag_6h']].mean() row['inflow_rise_24h'] = next_pred - row['inflow_lag_24h'] return np.array(preds)递归预测的坑在参数更新逻辑。这一步最容易出错的是滞后特征的级联更新——本轮预测出的值,不仅要用在inflow_lag_1h上,还要沿着滞后链逐级传递,否则下一轮的特征组合就是错的。这个代码里我用了一个简化的更新逻辑,实际项目中建议把特征构建封装成同一个函数,训练前和预测时共用一套特征逻辑,避免特征定义不一致。
6.3 我常用的一个验证技巧:分流量段看误差,而不是只看一个MAE
我做入库流量预报项目收尾时,一定会做一张分流量段的误差表——把测试集按实测流量从小到大分成五档,分别统计每档的MAE和相对误差。因为调度关心的不是整体平均误差,而是“大流量时的误差能不能接受”。如果模型在5000立方米每秒以上的大流量段相对误差超过30%,哪怕整体MAE很漂亮,这个模型对于防洪调度也是不合格的。这个习惯帮我挡掉过好几次“整体指标很好看、实际没法用”的交付验收。希望这套基于Python和LightGBM的入库流量预测方案能帮你在自己的流域少踩几个坑。
本文还有配套的精品资源,点击获取