
简介针对水位预测场景的完整Python实现包面向有机器学习基础、希望快速搭建或复现时间序列预测模型的研究者与开发人员。资源提供可直接运行的源代码、预测模型文件与实验数据覆盖数据加载、模型训练、评估与预测的关键环节适用于水位、流量等水文时序建模任务。压缩包共9个文件大小约5.15MB其中模型权重h5文件有6个另有1个py主程序、1个用于探索和实验的Jupyter Notebook以及1个csv原始观测数据结构清晰便于按模块对照学习。目前已有446人学习下载可作为相关课程设计、毕业设计或工程预研的参考。资源内包含BiRNN、GRU及多种LSTM变体的训练权重下载后可直接加载模型进行水位预测也可以结合源码和Notebook调整网络结构、复现训练过程实验数据则可用于检验模型在不同输入条件下的表现。不同权重文件对应多种网络结构可横向对比效果便于后续优化整体上手门槛适中兼具入门与工程参考价值。1. 拿到「基于Python的水位预测系统源代码预测模型文件」先拆清楚这套东西能干什么水位预测是水文预报里最常被拿出来做落地验证的一类任务上游来水、降雨补给、蒸发和下渗共同决定河道水位而水位又直接影响防洪调度、闸门控制和风险预警。一套基于Python的水位预测系统通常包含三块资产可运行的源代码、一份训练好的预测模型文件、以及配套的训练/验证脚本。你拿到手的不是文档而是一个能直接预测未来几小时到几天水位的黑匣子问题是这个黑匣子怎么打开、怎么喂数据、怎么让它适配你自己的测站水位序列。这套系统最典型的形态是LSTM长短期记忆网络或其变体因为水位数据本质上是强时序相关的时间序列和股票价格、负荷预测属于同一类问题——你完全可以用「Python量化交易策略代码」那套回测思路来理解它历史数据进模型未来窗口出预测。适合的人群很明确搞水文预报的工程师、做智慧水利平台开发的团队、以及拿时间序列做论文实验的学生。它的核心价值不在于“预测”这个动作而在于模型文件里的权重已经沉淀了某个流域的水位变化规律你要做的第一件事是把它跑通并验证它在你的数据上准不准。2. 水位预测的模型选型为什么这类系统普遍落在LSTM而不是ARIMA上2.1 水位序列的三大数据特性决定了模型架构水位数据和普通的温度、湿度序列不一样它有很强的滞后效应一次强降雨之后河道水位不是立刻涨到峰值而是延迟数小时甚至数天才到达地下径流的补给还会让退水段拖得很长。这种滞后性用传统的时间序列模型描述起来很吃力因为ARIMA本质上是线性模型它假设未来的值是历史值的线性组合加上噪声项水位序列里的非线性突变——比如台风带来的陡涨陡落——线性模型几乎必翻车。LSTM解决的是另一个层面的问题它通过门控机制决定哪些历史信息要记住、哪些要遗忘因此能自动学到「三天前的降雨导致今天水位上涨」这种跨时间步的依赖。很多项目里会同时给出两份模型文件一份是LSTM的.h5一份是传统MLR多元线性回归的.pkl目的就是让你做对比实验先跑线性模型看下限再跑深度学习看提升空间。这也是水文预报领域的常见做法用简单模型当基准用复杂模型冲精度。还有一个容易被忽略的特性是数据长度。水位预测的训练集通常是按小时采样的序列一天24个点一年就是8760个点这个规模对LSTM来说非常友好。相比之下如果你想用Transformer那类大模型这个数据量是不够的模型很容易过拟合到记住训练集而不是学到水位变化规律。所以拿到源代码时先看一眼模型结构定义如果是tf.keras.layers.LSTM那说明作者选择了一条稳妥的路线如果出现了MultiHeadAttention你要警惕这个模型可能需要远多于你手头的数据才能训练出有效权重。2.2 LSTM的输入输出结构从数据重构到滑动窗口水位预测源代码里最核心的预处理函数一般叫create_sequences或build_dataset它的作用是把一维水位序列重构成监督学习需要的二维矩阵。这里有一个决定性的参数——时间步长lookback也就是用过去多少个小时的水位来预测未来某个时刻的水位。比如lookback72表示用过去三天的数据预测未来lookahead6表示预测六小时后的水位。def create_sequences(data, lookback72, lookahead6): 将一维水位序列转换为LSTM的输入输出对。 data: 归一化后的一维水位数组 lookback: 用过去多少个小时的水位作为特征 lookahead: 预测未来多少个小时的水位 返回: X的形状为(n_samples, lookback, 1)y的形状为(n_samples,) X, y [], [] for i in range(len(data) - lookback - lookahead): X.append(data[i:i lookback]) y.append(data[i lookback lookahead - 1]) return np.array(X), np.array(y)这个函数的逻辑可以用一句话概括把每个时间点当成一个样本用前lookback个时刻作为特征第lookbacklookahead个时刻作为标签。循环的终止条件是len(data) - lookback - lookahead而不是len(data) - lookback是为了保证标签不越界。lookback设太短模型看的信息太少预测峰值的滞后会很严重设太长训练样本数量急剧减少而且门控机制也没必要看那么久远的信息。做水位预测时我一般从48到96之间试用验证集损失来决定不是拍脑袋定。这里还要注意一个细节y取的是data[i lookback lookahead - 1]这是预测窗口的最后一个时刻而不是窗口的平均值或中间值。有的源代码会取lookahead窗口内所有时刻的均值作为标签那就变成「预测未来六小时的平均水位」和「预测第六小时的水位」是两个不同的任务评估时不能混着比。如果你拿到源码后想改预测目标先确认这个索引表达式。输入数据的形状也要对齐LSTM层要求的输入是三维的(样本数, 时间步长, 特征数)。水位预测只用「水位」这一个变量时特征数是1如果源码里还拼了降雨量、上游流量特征数就大于1。很多人在跑通预测后想加特征直接在原始DataFrame上多塞一列是不行的必须在create_sequences的循环里同时切片多个特征列并保证所有特征用同一套归一化参数。3. 在本地跑通这套源代码环境配置与最小执行路径3.1 先把Python环境清理干净虚拟环境、依赖版本和TensorFlow选型拿到源代码后的第一步不是急着运行predict.py而是把环境隔离出来。水位预测系统的依赖通常集中在requirements.txt里但这份文件里往往只写了包名没锁版本或者锁的版本太老装不上。最常见的翻车场景是TensorFlow 2.10之后在Windows上默认不带GPU支持而很多水位预测源码还是按tf.keras老接口写的新老版本混在一起直接导入失败。python -m venv water_level_env source water_level_env/bin/activate # Windows下用 water_level_env\Scripts\activate pip install --upgrade pip pip install tensorflow2.10.0 numpy1.24.3 pandas2.0.3 scikit-learn1.3.2 matplotlib3.7.4这里锁TensorFlow 2.10.0是有讲究的它是最后一个在Windows上原生支持CUDA的版本之后官方不再维护Windows GPU分发包装12.x你会面临一堆cudart64_*.dll not found的报错。如果你手头是NVIDIA显卡且想用GPU训练就按这个版本走如果是纯CPU跑预测TensorFlow 2.15以上也能用但部分老代码里tf.contrib这类接口已经被删干净了遇到就换回2.10。NumPy锁1.24.3是为了避开NumPy 2.0的兼容性断裂——很多老训练脚本里np.float的写法在2.0里已经不存在了。装完依赖后先跑一个最小导入测试来验证环境没问题再做任何正经事。# 验证环境完整性逐个导入核心库并打印版本 import tensorflow as tf import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler print(TensorFlow:, tf.__version__) print(NumPy:, np.__version__)如果TensorFlow能打印出2.10.0而不是报DLL load failed说明环境就位了。这一刻值得你花两分钟确认因为之后所有报错都可以先用「环境问题」还是「代码问题」来二分能省下大把排查时间。很多免费Python源码大全里的项目包得乱七八糟直接pip install -r requirements.txt往往会在版本冲突上卡半小时。3.2 加载预测模型文件.h5、.pkl与权重文件的三种加载姿势水位预测系统解压后模型文件一般躺在model/目录下文件名可能是lstm_water_level.h5、best_model.weights.h5也有可能是model.pkl。.h5是Keras模型的完整存档里面同时包含网络结构和权重.pkl则是用joblib或pickle保存的sklearn模型。加载方式完全不同写错一个函数就会报Unknown layer或者AttributeError。# 加载LSTM模型.h5文件 from tensorflow.keras.models import load_model model load_model(model/lstm_water_level.h5) print(model.summary()) # 打印网络结构确认层类型和参数数量 # 如果加载报错 Unknown layer说明代码里用了自定义层 # 需要改成: # from tensorflow.keras.utils import custom_object_scope # with custom_object_scope({AttentionLayer: AttentionLayer}): # model load_model(model/lstm_water_level.h5)加载.h5最常见的报错是Unknown layer原因是训练时用了自定义的Attention层或损失函数而加载脚本里没有注册这些对象。解决方式是用custom_object_scope把自定义类包起来前提是源码里确实定义了这些类。如果源码里没有对应定义这个.h5文件对你来说就是废的别花时间逆向直接重新训练。pkl文件的加载更简单但要区分是用pickle还是joblib保存的# 加载sklearn模型.pkl文件 import joblib mlr_model joblib.load(model/mlr_baseline.pkl) # 如果失败尝试: # import pickle # with open(model/mlr_baseline.pkl, rb) as f: # mlr_model pickle.load(f)joblib和pickle的不兼容是你大概率会遇到的一个坑前者对大数组做了内存映射优化后者是纯Python序列化。判断方法很简单报错信息里出现pickle.UnpicklingError就换另一种。无论哪种模型文件加载后都要做一次「试探预测」——喂一组形状正确的假数据看能不能出结果这能暴露输入维度不匹配的问题。比如LSTM模型期望输入形状是(batch, 72, 1)你喂了一维数组它会直接报Invalid argument这时候要把数据先reshape(-1, 72, 1)。3.3 用可视化脚本把预测曲线和真实值叠在一起验证模型没有白跑模型文件加载成功只代表文件没坏不代表预测结果能看。你需要复现源代码里的预测流程读数据、归一化、构造序列、预测、反归一化。这里最关键的技巧是预测用的归一化参数必须和训练时一致否则预测值会严重偏离真实水位量纲。import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import load_model # 1. 读取水位数据假设CSV包含time和water_level两列 df pd.read_csv(data/station_hourly.csv, parse_dates[time]) water_level df[water_level].values.reshape(-1, 1) # 2. 归一化——必须与训练时使用同一套scaler参数 scaler MinMaxScaler(feature_range(0, 1)) scaled_data scaler.fit_transform(water_level) # 3. 构造最后72个小时的输入序列 lookback 72 last_sequence scaled_data[-lookback:] X_pred last_sequence.reshape(1, lookback, 1) # 4. 预测并反归一化 model load_model(model/lstm_water_level.h5) pred_scaled model.predict(X_pred) pred_actual scaler.inverse_transform(pred_scaled.reshape(-1, 1)) print(f预测水位: {pred_actual[0][0]:.2f} m) # 5. 画图最近168小时实际水位 未来预测点 plt.figure(figsize(12, 5)) plt.plot(df[time][-168:], water_level[-168:], label实际水位, color#2c3e50) plt.axvline(xdf[time].iloc[-1], colorred, linestyle--, label预测起点) plt.scatter(df[time].iloc[-1], pred_actual[0][0], color#e74c3c, s80, label预测水位) plt.xlabel(时间); plt.ylabel(水位 (m)) plt.legend(); plt.grid(alpha0.3); plt.show()这段脚本的每一步都有明确目的第2步的fit_transform是把整个训练序列做归一化因为MinMaxScaler要看到全局的最小值和最大值才能正确缩放第3步取最后lookback条数据作为输入——预测未来水位只能依赖当前已有的水位观测值第4步的inverse_transform把预测结果从0~1区间还原成真实水位如果这一步缺失你会得到一串零点几的数在图上完全对不上刻度。可视化这一步是最能暴露问题的环节。如果预测点和历史曲线的末端衔接不上先检查归一化是否用了同一套scaler如果预测值明显偏低大概率是训练/预测时的lookback不一致。把这条可视化链路跑通了你对这套源代码的控制力才算真正建立起来。4. 用自己的水位数据进行二次训练数据格式、参数调整与效果提升4.1 数据预处理缺测值、异常水位和标准化策略跑通预测只是起点水位预测系统的真正工作是训练。你要用自己测站的数据重新训练模型这就要面对源代码不曾预料到的数据质量问题。水文数据比股票数据还脏——雨量站和水位站的设备断线、传输丢包、人工抄录错误都会在序列里留下缺口和毛刺。水位序列的缺测处理有一个原则短缺口用线性插值长缺口用前后同期的历史均值填充。原因在于水位变化是连续的一小时内的缺口用线性插值几乎不引入误差但连续缺测数天时线性插值会用一条直线把上下游强行缝起来这等于在序列里制造了一段虚假的平稳段LSTM会学着把这段「假平稳」当成规律。# 水位数据清洗缺测插值 异常值剔除 import pandas as pd import numpy as np df pd.read_csv(data/my_station.csv, parse_dates[time]) df df.set_index(time) # 1. 缺测处理短缺口线性插值长缺口6小时用前后各24小时均值填充 df[water_level] df[water_level].interpolate( methodlinear, limit6, limit_areainside ) for gap in df[df[water_level].isna()].index: before df[water_level].loc[:gap].dropna().iloc[-24:].mean() after df[water_level].loc[gap:].dropna().iloc[:24].mean() df.loc[gap, water_level] (before after) / 2 # 2. 异常值剔除超过前后6小时中位数 ± 3倍MAD中位数绝对偏差的点替换为NaN再插值 rolling_med df[water_level].rolling(13, centerTrue).median() mad (df[water_level] - rolling_med).abs().rolling(13, centerTrue).median() lower rolling_med - 3 * 1.4826 * mad upper rolling_med 3 * 1.4826 * mad df[clean_level] df[water_level].mask( (df[water_level] lower) | (df[water_level] upper) ) df[clean_level] df[clean_level].interpolate(methodlinear) # 3. 只看雨量数据里的毛刺水位在1小时内骤降超过2米优先标记为设备故障 diff df[clean_level].diff().abs() df[clean_level] df[clean_level].mask(diff 2.0) df[clean_level] df[clean_level].interpolate(methodlinear)MAD中位数绝对偏差比标准差更抗异常值污染因为中位数本身不受极端值影响。1.4826这个系数是把MAD调整到与标准差同尺度的经验常量。第三段的骤降检测针对的是水位计跳变——通常是传感器被漂浮物撞击或者传输误码导致的瞬移真实水位一小时下降两米在自然河道里极其罕见。这套清洗逻辑能处理掉大部分数据质量问题剩下的少数异常点只能靠人工看曲线判断没有万能脚本。清洗完数据之后才能做归一化。水位预测的标准做法是用MinMaxScaler缩放到0~1区间不要用StandardScaler——LSTM默认的激活函数tanh输出范围是-1到10~1的输入可以让梯度在早期训练时更平稳。这里还涉及一个顺序问题必须先在训练集上fit归一化参数再用同一套参数去transform验证集和测试集。如果你对全量数据做fit_transform再切分验证集的信息就泄露进了训练过程评估出的误差会偏乐观这在学术评审里叫数据泄露在水文工程里则意味着你的泛化能力是高估的。4.2 关键超参数时间步长、批大小、学习率和损失函数怎么调二次训练的核心工作是调整超参数。源码里默认给的那组参数只对数据集A有效换成你的测站后流域面积、河道比降、降雨响应速度完全变了必须重新调。首要调整目标是lookback山区性河流涨落快48小时窗口足够平原感潮河段水位变化缓慢lookback要拉到120小时甚至更长。# 二次训练配置示例基准参数 早停 模型检查点 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint from tensorflow.keras.optimizers import Adam model Sequential([ LSTM(64, return_sequencesTrue, input_shape(lookback, n_features)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dense(16, activationrelu), Dense(1) ]) optimizer Adam(learning_rate0.001) model.compile(optimizeroptimizer, lossmean_squared_error, metrics[mae]) # 训练集与验证集按时间顺序切分不能随机打乱 split_idx int(len(X) * 0.8) X_train, X_val X[:split_idx], X[split_idx:] y_train, y_val y[:split_idx], y[split_idx:] early_stop EarlyStopping( monitorval_loss, patience15, # 连续15个epoch验证损失不下降就停 restore_best_weightsTrue ) checkpoint ModelCheckpoint( model/my_station_best.h5, monitorval_loss, save_best_onlyTrue ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop, checkpoint], verbose1 )这份代码里有三个参数值得展开。第一learning_rate0.001是LSTM训练的默认安全起点但如果你的训练损失曲线震荡得厉害把它降到0.0005会立竿见影反之如果loss下降得像蜗牛爬可以试着提到0.005。第二batch_size32对小时级水位数据是合理默认值——数据量在几万条级别时32和64的差距不大但显存吃紧的机器上32更稳妥。第三patience15配合restore_best_weightsTrue能防止两个问题验证集上的过拟合loss反弹上升和训练后期权重在最优解附近来回震荡。restore_best_weights会回滚到验证损失最小的那一轮权重——这相当于给训练吃了颗后悔药。损失函数的选择值得多说几句。源码里大概率用的是mean_squared_errorMSE它让模型侧重于把大误差压小——这对防洪场景是对的因为水位峰值附近的预测误差才是致命的。但MSE会让模型变得保守在涨水段倾向于给出偏低的预测因为低估峰值比高估峰值的梯度更大。如果你更关心整体曲线贴合度可以换成mean_absolute_error它的梯度恒定不会特别偏向惩罚大误差。更进阶的用法是自定义quantile_loss来预测区间这个技巧在第6章展开。训练完成后检查history.history里的val_loss曲线如果训练损失降到很低但验证损失很高说明过拟合了如果两者都很高先怀疑数据预处理有没有做干净再怀疑lookback设置是否合理。模型文件输出到本地后不要急着删掉旧的预训练文件——在你的数据上重新训练出来的模型是判断源代码自带模型质量好坏的最佳基线。5. 水位预测从跑通到跑准的5个坑现象、原因和解决办法5.1 模型文件加载报错「Unknown layer」现象load_model(lstm_water_level.h5)直接抛出Unknown layer: Attention。原因训练时网络里使用了自定义Keras层如注意力层、自归一化层H5文件里存了层名但当前脚本没有定义这个层的类。load_model找不到注册的类就报未知层这在水位预测增强版源码里非常常见因为作者往往会加上注意力机制来提升峰值预测能力。解决仔细找一下源码包里有没有layers.py或custom_layers.py文件。如果有把它导入到当前脚本里再用custom_object_scope包住加载函数。没有的话唯一出路是用model tf.keras.models.load_model的参数custom_objects手动传入层类传到类的定义为空壳都不行必须是和训练时完全一致的实现。5.2 预测结果整体偏低洪水过程线明显被「削峰」现象预测曲线和真实水位趋势一致但每一处的峰值都矮一截谷值都高一点整体像被压扁了。原因这几乎可以锁定为损失函数用MSE的必然结果。MSE的目标函数期望最小化平方误差模型为了不犯大错会倾向于输出接近历史均值的保守预测——预测值向均值回归表现出来就是削峰。解决换用mean_absolute_error或者改用自定义的分位数损失来直接预测一个区间而不是单点。如果任务只关心峰值水位可以对峰值样本加权让损失函数在高峰期产生更大梯度。不要指望通过调LSTM神经元数量来根治这个问题模型结构再复杂也改变不了损失函数对保守预测的偏好。5.3 训练损失正常下降但预测的第一天出现「冷启动偏差」现象模型回测时每个预测片段的第一条预测值和真实值差很远但后续几步迅速贴合。原因LSTM的初始状态hidden state和cell state在训练时被初始化为零向量模型学到的规律是「从零状态开始经过若干步热身后进入正常预测」。而你的预测脚本每次只取最近lookback条数据喂进去做单步预测相当于每次都从零状态冷启动第一拍自然跑偏。解决把预测改成「滚动预热」模式——先取一段历史序列输入网络丢弃前几步输出只用最后的输出来作为正式预测起点。这在源代码里通常对应一个warm_up_steps参数设10到20之间就好。别用暴力方法把历史序列反复喂很多遍那样会加剧第一步的偏差。5.4 测试集误差明明很低换一个水文年数据就崩现象在2019年的数据上MAE只有10厘米换到2021年的数据直接变成40厘米。原因水位模型对训练集所在年份的降雨模式过拟合了。比如训练集多为枯水年模型对丰水年的洪水过程几乎没有经验或者训练集里包含了极端事件但验证集恰好避开了它。水文序列有天然的年代际波动这是统计模型无法回避的分布漂移问题。解决按年份做分组交叉验证而不是随机切分样本。把2020、2021、2022分别当验证集训练三次取三次误差的均值和方差来评估稳定性。如果特定年份误差骤增把该年份的降雨特征如累计降雨量、最大小时雨强作为额外特征加入模型让网络学会「看到大雨就提高水位响应权重」。5.5 用未来数据训练当前模型时间切分与踩坑边界现象回测效果极好实战一上线就拉胯。原因预处理时不小心用了全量数据的全局统计量——比如用全量数据的最大值做归一化这等于让模型在训练时偷看了未来的水位范围。另一个常见版本是在构造滑动窗口前没有按时间顺序切分训练/验证集而是用train_test_split的默认随机切分把时序打乱了。解决维护一个铁律任何归一化参数都只从训练集前80%部分计算验证集和测试集只做transform不做fit。序列切分也必须严格按时间顺序随机打乱在时间序列预测里属于数据泄露会让模型记住「随机邻居」而不是「时间邻居」。这个坑在源代码里通常不明显因为作者自己可能也没有踩过——拿到源码先检查切分逻辑再决定信不信它的验证指标。6. 把预测结果从单点升级为区间分位数损失函数和滚动回测水位预测的工程价值不在于输出一个数字而在于输出「未来6小时水位有90%的把握落在某个区间」。防洪调度要的是置信度——闸门开一孔还是两孔取决于水位是否可能突破警戒线而不只是期望值。把源代码里的单点预测改成区间预测用到的核心技巧是自定义分位数损失函数。import tensorflow as tf import numpy as np # 分位数损失预测水位的区间下界、中位数和上界 def quantile_loss(q): def loss(y_true, y_pred): # y_pred的形状为(batch, 3)分别对应q0.1, 0.5, 0.9三个分位数 error y_true - y_pred return tf.reduce_mean(tf.maximum(q * error, (q - 1) * error)) return loss # 模型输出层改为3个节点 model Sequential([ LSTM(64, return_sequencesTrue, input_shape(lookback, 1)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dense(16, activationrelu), Dense(3) # 同时输出10%、50%、90%三个分位数 ]) model.compile( optimizerAdam(learning_rate0.001), loss[quantile_loss(0.1), quantile_loss(0.5), quantile_loss(0.9)] )这个损失函数的数学含义是当预测值低于真实值时损失按q的比例放大高于真实值时按1-q放大。于是q0.1的模型会被训练得「宁可高估也不低估」给出的是偏保守的下界q0.9的相反给出偏激进的上界。把三个分位数同时画在图上你得到的是一个锥形的预测区间洪水越猛、水情不确定性越大区间就越宽。这比单点预测对决策者有用得多。区间模型训练好之后用滚动回测来验证它的可靠性把不含归一化参数的清洗数据按小时滑动每个时间点只预测未来6小时记录预测区间是否覆盖了真实水位。覆盖率的经验目标是90%分位数区间要对80%~90%的真实值完成覆盖——低于这个数说明区间太窄模型过度自信高于太多说明区间太宽模型等于是摆烂预测没有信息量。检查覆盖率的同时也要盯住区间的平均宽度宽度小于水位的量测误差通常是1~2厘米就说明模型在吹牛。做滚动回测时要注意每一步预测都只能使用该时间点之前的数据来更新归一化参数和构造序列预言未来的数据是自欺欺人。回测代码里我习惯把真实水位线画在图上作为背景把预测区间用半透明色带覆盖翻车的时候一眼就能看出来是哪一段出了问题。这套滚动验证机制也是判断一个水位预测系统能否投入生产的最后一道检验——不通过就回去调参数换数据过了再谈部署顺序不能反。我一直保持的习惯是任何水位预测模型的输出都必须在图上和原始水位序列叠加着看一遍单看指标数字看不出系统误差图上一眼就能看出削峰和滞后。这个习惯帮我挡掉了不少只在测试集上漂亮的模型。希望帮到你。本文还有配套的精品资源点击获取