简介:一套基于Python构建的水位预测系统,包含完整源代码与已训练好的预测模型文件。该系统围绕水位时间序列数据的处理与预测展开,适合水利信息化、智慧城市和时序数据挖掘方向的开发者、研究人员及学生用于算法验证与工程落地。压缩包内共9个文件,核心组成包括6个H5格式的深度学习模型(BiRNN、GRU、多种LSTM变体及SimpleRNN)、1个Python主程序、1个Jupyter Notebook演示文件及1个CSV原始水位数据集,整体仅5.15MB,便于快速下载与部署。资源提供可直接运行的多模型预测链路,用户既可使用预训练权重进行测试,也可借助Notebook修改参数重新训练;多模型并存的目录结构有助于横向对比不同循环神经网络在水位预测任务中的表现,并沉淀数据预处理、特征构造与模型评估的一体化思路。已有446人参与学习,适合需要获取可复现代码与基线模型的进阶学习者。
1. 水位预测系统:一份到手就能跑的 Python 时序预测完整工程
提到水位预测,很多人第一反应是先找数据、再搭模型,折腾几周连训练集都没配好。这套基于 Python 的水位预测系统不一样,它把数据预处理、滑动窗口、LSTM 训练、模型保存、加载预测整条链路封装成了可直接调用的脚本,压缩包里还自带训练好的预测模型文件——解压后改一下数据路径,当天就能跑出第一张预测曲线。
项目按单变量时间序列预测设计,输入某水文站的历史水位观测值,输出未来 6 个时间步的预测结果。模型推理不依赖 GPU,普通办公电脑就能跑。适合水利、水文方向需要量化预判水位趋势的开发者,也适合正在学时序预测、想找一个落地 LSTM 参考工程的人。
2. 项目架构与技术选型:从原始水位数据到预测曲线的完整链路
2.1 技术选型:为什么用 LSTM 而不是 ARIMA 或 Prophet
水位数据本质是等间隔采样的时间序列,常见预测路线有三条:ARIMA 这类统计模型、Prophet 这类加性模型、以及 LSTM 循环神经网络。这套项目选了 LSTM,原因很实际:水位序列通常有明显非线性特征,汛期涨得快、退得慢,还经常带突变,ARIMA 的差分机制对这种非平稳、非对称形态拟合很吃力。Prophet 对趋势和季节性的拟合不错,但对强自相关场景——"昨天水位决定了今天水位"这类关系,表达力不如循环网络。短时突变和突发涨落恰恰是水位预测里最不能丢的信息。
LSTM 的核心优势是门控机制,能记住长期依赖。水位预测里,前两天的水位对今天的影响远大于 30 天前,但固定窗口的 MLP 容易把短期噪声当特征,LSTM 可以在训练里自己学"该记多少、该忘多少"。代价是训练时间比 ARIMA 长,数据量少时容易过拟合。所以这套代码做了两个妥协:模型只叠两层 LSTM,每层 64/32 个单元,参数规模可控;训练前对缺失值比例做硬校验,数据太烂直接报错,不硬训练。这个取舍在真实项目里很重要——模型再强也顶不住源头数据是脏的。
| 方案 | 在这个场景的表现 | 主要问题 |
|---|---|---|
| ARIMA | 对线性平稳序列表现好 | 水位序列非平稳、有突变,差分后仍难拟合 |
| Prophet | 适合明显趋势加季节性 | 强自相关和短时突变表达力弱 |
| LSTM | 能拟合非线性与长期依赖 | 训练慢、小数据易过拟合 |
2.2 目录结构与文件职责
解压后目录结构如下,每个文件职责明确,改配置就能换数据、换站点:
| 路径 | 职责 |
|---|---|
| data/raw/ | 原始水位 CSV,按站点命名存放 |
| data/processed/ | 处理后中间数据与预测结果输出 |
| models/ | 训练好的 LSTM 模型文件与归一化器 |
| src/data_loader.py | 读取 CSV、缺失值检查、插值 |
| src/preprocess.py | 归一化、滑动窗口切分 |
| src/model.py | LSTM 结构定义与编译 |
| src/train.py | 训练主流程,模型落盘 |
| src/predict.py | 加载模型做预测并输出结果 |
| config.py | 所有可调参数的集中配置 |
| requirements.txt | Python 依赖清单 |
src 下的模块按调用顺序串联:train.py 先调 data_loader 读数据,再调 preprocess 做归一化和切窗,最后调 model 建模型训练;predict.py 则跳过训练,直接读 models/ 下的两个文件做推理。config.py 是全局参数出入口,改它一个文件就能控制窗口大小、预测步长、训练轮数,新手不用进源码找参数。第一次跑的时候 data/processed/ 可能还不存在,train.py 里用 os.makedirs(exist_ok=True) 自动创建,少一个手动建目录的步骤。
2.3 数据流向:一条水位序列如何变成预测值
完整链路是:读入 CSV → 缺失值线性插值 → MinMaxScaler 归一化 → 滑动窗口切出 (X, y) → 按时间切分训练集测试集 → reshape 成 (样本数, lookback, 1) 喂给 LSTM → 训练后保存模型和 scaler → 预测时加载模型,把最近 lookback 个点归一化后输入 → 反归一化得到真实水位值。
这里有两个设计细节值得注意。第一,归一化用 MinMaxScaler 而不是 StandardScaler,水位数据范围相对稳定,落在 [0,1] 区间后 LSTM 的 tanh 激活函数更容易收敛。第二,切分训练集测试集必须按时间顺序,不能像分类任务那样随机打乱——时序数据一旦 shuffle,模型就"偷看"了未来,验证指标全是假的。所以代码里 split 直接取前 80%,后面的 20% 严格后置。还有一个容易忽略的点:滑动窗口切出来的样本数量是 len(series) - lookback - horizon + 1,历史数据太短时样本量不够,训练效果会明显缩水,这也是为什么 data_loader 里的缺失值校验宁可严格一些。
3. 核心模块拆解:数据预处理、滑动窗口与 LSTM 训练的每一行代码
3.1 数据读取与缺失值处理
data_loader.py 负责把原始 CSV 转成干净的时序数据。水文站的水位数据经常有缺测,记录仪故障、传输丢包都会产生空值,这一层必须挡在训练之前。这里的插值策略选线性插值而不是均值填充,因为水位在短时间内的变化接近线性,均值填充会把突变给磨平,模型就学不到真实的涨落形态。
# src/data_loader.py import pandas as pd def load_water_level(file_path, threshold=0.1): df = pd.read_csv(file_path, parse_dates=['time'], index_col='time') series = df['water_level'].astype(float) missing_ratio = series.isna().sum() / len(series) if missing_ratio > threshold: raise ValueError(f"缺失值比例 {missing_ratio:.2%} 超过 {threshold:.0%},建议先补数再训练") series = series.interpolate(method='linear', limit_direction='both') series = series.replace([float('inf'), float('-inf')], float('nan')).ffill() series = series[~series.index.duplicated()].sort_index() return series逻辑说明:先把 time 列解析为索引,再把水位列转成 float;缺失比例超过 10% 直接抛异常,因为插值救不了大面积空洞。线性插值只适合短期缺测,limit_direction='both' 保证首尾边界也有值。最后把 inf 替换成 NaN 再前向填充,防止极端脏数据污染后续的归一化计算。参数说明:threshold 控制缺失容忍度,数据质量好可以调到 0.05,监测频率低的站点建议放宽到 0.2。
3.2 归一化与滑动窗口切分
preprocess.py 是训练前的核心,包含归一化和滑动窗口两个步骤。lookback 表示"看过去多少步",horizon 表示"预测未来多少步",这两个值直接决定训练样本的形态,也在 config.py 里暴露给使用者调整。
# src/preprocess.py import numpy as np from sklearn.preprocessing import MinMaxScaler def make_scaler(series_train): scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(series_train.reshape(-1, 1)) return scaler def build_sequence(series_scaled, lookback=24, horizon=6): X, y = [], [] for i in range(len(series_scaled) - lookback - horizon + 1): X.append(series_scaled[i : i + lookback]) y.append(series_scaled[i + lookback : i + lookback + horizon]) return np.array(X), np.array(y)逻辑说明:make_scaler 只对训练集做 fit,测试集和未来预测数据只用 transform,避免信息泄漏——如果 scaler 看过全量数据的最大值,测试集的归一化结果就"作弊"了,算出来的评估指标会虚高。build_sequence 用滑动窗口切样本,每个样本的形状是 (lookback,),标签是未来 horizon 步的序列,这样模型一次直接输出多条预测值。滑动窗口的步长默认是 1,相邻样本之间大量重叠,这是时序预测的常见做法,相当于把数据量放大了好几倍。参数说明:lookback=24 在按小时采样的数据里代表"用过去一天预测未来 6 小时";如果你的数据是 15 分钟一个点,想预测未来 1 小时就把 horizon 改成 4。
3.3 LSTM 模型结构与训练配置
model.py 里的网络结构刻意保持简单,两层 LSTM 加一个全连接输出层,把复杂度留给数据和调参。训练主流程在 train.py,所有超参数从 config.py 读取,保证换参数不用动源码。
# src/model.py from keras import Sequential from keras import layers def build_lstm_model(lookback, horizon): model = Sequential([ layers.Input(shape=(lookback, 1)), layers.LSTM(64, return_sequences=True), layers.Dropout(0.2), layers.LSTM(32), layers.Dense(horizon) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) return model# src/train.py import numpy as np import joblib from config import DATA_PATH, LOOKBACK, HORIZON, EPOCHS, BATCH_SIZE, TEST_RATIO from src.data_loader import load_water_level from src.preprocess import make_scaler, build_sequence from src.model import build_lstm_model series = load_water_level(DATA_PATH) split_idx = int(len(series) * (1 - TEST_RATIO)) train, test = series.iloc[:split_idx], series.iloc[split_idx:] scaler = make_scaler(train.values) train_scaled = scaler.transform(train.values.reshape(-1, 1)).flatten() test_scaled = scaler.transform(test.values.reshape(-1, 1)).flatten() X_train, y_train = build_sequence(train_scaled, LOOKBACK, HORIZON) X_test, y_test = build_sequence(test_scaled, LOOKBACK, HORIZON) X_train = X_train.reshape((-1, LOOKBACK, 1)) X_test = X_test.reshape((-1, LOOKBACK, 1)) model = build_lstm_model(LOOKBACK, HORIZON) history = model.fit(X_train, y_train, epochs=EPOCHS, batch_size=BATCH_SIZE, validation_data=(X_test, y_test), verbose=1) model.save('models/lstm_water_level.h5') joblib.dump(scaler, 'models/scaler.pkl')逻辑说明:训练脚本的核心是"先切分再归一化",顺序不能反——切分用的是原始序列索引,归一化用的是训练集 fit 出的 scaler。reshape 成 (样本, lookback, 1) 是因为 LSTM 要求输入带时间步维度,最后那个 1 是特征数量。水位预测是单特征,如果后续要加降雨量、上游流量,这里的输入要改成多列,模型输入层也要对应调整。参数说明:EPOCHS=50 是给 CPU 快速验证的量级,真实场景我一般提到 100 以上并配合 EarlyStopping;BATCH_SIZE=32 在几千条数据量时是稳妥值。训练完成后模型存成 .h5,scaler 用 joblib 存成 .pkl,这两个文件就是预测阶段的全部依赖。
4. 把项目跑起来:环境配置、训练复现与预测结果验证
4.1 环境准备与依赖安装
先看 requirements.txt,这份依赖是项目能复现的最小集合:
# requirements.txt pandas>=1.5 numpy>=1.23 tensorflow==2.10 scikit-learn>=1.1 matplotlib>=3.6 joblib>=1.2安装命令:
pip install -r requirements.txt版本是我在本地验证过的组合,TensorFlow 对 Python 版本有要求,2.x 系列在 Python 3.9 或 3.10 上兼容性最好,我一般在这两个版本上跑,少踩很多坑。装完之后用 python -c "import tensorflow as tf; print(tf.version)" 快速验证。如果机器上已经有别的深度学习框架,强烈建议用虚拟环境隔离,TensorFlow 的依赖和 PyTorch 混装容易出隐性问题。
提示:如果系统 Python 是 3.12,TensorFlow 2.10 装不上,把版本换成 tensorflow>=2.15 即可,模型结构和训练代码不用改。
4.2 训练:一条命令从 CSV 到模型落盘
训练前只需要改 config.py:
# config.py DATA_PATH = 'data/raw/station_001.csv' # 输入水位序列 LOOKBACK = 24 # 看过去 24 个时间步 HORIZON = 6 # 预测未来 6 个时间步 EPOCHS = 50 # 训练轮数 BATCH_SIZE = 32 # 批大小 TEST_RATIO = 0.2 # 后 20% 作测试集然后执行:
python src/train.py命令行会打印每个 epoch 的 loss 和 mae,训练结束后 models/ 目录下出现 lstm_water_level.h5 和 scaler.pkl 两个文件。CPU 训练几千条样本、50 个 epoch 大概几分钟,不用一上来就上 GPU。判断训练是否正常有个经验值:训练集 mae 和验证集 mae 都在下降且差距不大,说明模型在正常学习;如果验证集 mae 先降后升,说明开始过拟合,要把 epochs 回调或者把 Dropout 从 0.2 提到 0.3;如果两个 mae 从一开始就纹丝不动,回第 5 章查数据问题。
4.3 加载模型文件预测:不训练也能出结果
预测脚本 predict.py 读入模型和 scaler,输入最近 lookback 个实测点,输出未来 horizon 个预测值:
# src/predict.py import numpy as np import joblib from keras.models import load_model from config import LOOKBACK, HORIZON from src.data_loader import load_water_level series = load_water_level('data/raw/station_001.csv') model = load_model('models/lstm_water_level.h5') scaler = joblib.load('models/scaler.pkl') tail = series.values[-LOOKBACK:] tail_scaled = scaler.transform(tail.reshape(-1, 1)).flatten() x = tail_scaled.reshape(1, LOOKBACK, 1) pred_scaled = model.predict(x, verbose=0).flatten() pred = scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() print("未来", HORIZON, "个时间步预测值:", np.round(pred, 2))逻辑说明:预测阶段完全复用训练时的那套 transform,不重新 fit scaler,这是保证预测值和训练数据同分布的前提。inverse_transform 把 [0,1] 区间的输出还原成真实水位,单位是米。参数说明:tail 必须取序列最后 lookback 个点,不能从中间取——时序预测只认"最近的状态"。想画图对比,追加一段 matplotlib 就能出预测曲线:
import matplotlib.pyplot as plt history_plot = series.values[-120:] plt.plot(history_plot, label='历史水位') plt.plot(range(len(history_plot), len(history_plot) + HORIZON), pred, 'r-o', label='预测水位') plt.legend() plt.savefig('data/processed/prediction.png', dpi=150)跑通这一步,项目就从"源码"变成了"可用的预测工具"。想验证模型真实水平,可以拿测试集最后一段做回测:把 y_test 反归一化后和预测值对比,算 RMSE,一般水位预测的 RMSE 落在训练集水位标准差的 10%~20% 以内就算及格。
5. 避坑与排查:训练翻车和预测漂移的 5 条血泪经验
训练这事多少带点玄学,但水位预测的坑大多有固定套路。下面 5 条是我实际跑这套代码时踩过的,按"现象、原因、解决"记录。
5.1 训练 loss 直接变成 nan
现象:train.py 跑起来,第一个 epoch 的 loss 就是 nan,后面全是 nan。
原因:最常见是输入数据里混入 inf 或者极大异常值。归一化前数据没清洗干净,MinMaxScaler 会把异常值压成一个极端点,梯度计算直接溢出,整个网络的权重被污染。
解决:在 load_water_level 里强制 replace inf,再按分位数裁剪异常值。我一般还加一道检查:训练前对水位列做一次 (np.abs(series) > 1000).any() 判断,水文站水位超过 1000 米基本可以断定是脏数据,直接定位到具体时间点查看原始记录。
5.2 预测曲线总是"滞后一拍"
现象:模型在测试集上 mae 很低,但画出来的预测曲线整体向右平移,形状和真实曲线几乎一模一样,像是复制粘贴晚了一步。
原因:这是时序预测的经典陷阱。模型发现"预测值约等于前一个观测值"是最省力的拟合策略,MSE 损失下这种复制策略的误差最小,模型根本学不到涨落规律,只是学会了抄作业。
解决:改成多步直接输出,也就是这套代码的 HORIZON=6 方案,让模型一次输出未来 6 个点,逼迫它学趋势而不是复制。如果还滞后,考虑在 loss 里增加相邻步差分惩罚,或者把降雨量、上游流量这类外生特征加进输入。
5.3 反归一化后水位出现负值
现象:预测值数量级正常,但部分点落在负值区间,物理上说不通。
原因:scaler 在 fit 的时候看到了测试集或全量数据的最大值,把正常水位压缩得太窄,模型输出略低于 0 时反归一化就变成了负值。本质是归一化阶段的信息泄漏。
解决:严格只在训练集上 fit scaler,测试集和预测阶段只做 transform。代码里 make_scaler(train.values) 就是这么设计的,贪方便对整个 series 做 fit 是翻车起点。
5.4 加载 .h5 模型文件报错
现象:predict.py 执行 load_model 抛出 ValueError,提示 Unknown layer 或者 unrecognized object。
原因:模型文件是用某个 TensorFlow/Keras 版本训练的,加载环境的版本不一致,Keras 的序列化格式对版本很敏感,换个版本就认不出结构。
解决:训练和推理用同一版本,最稳的是照 requirements.txt 重建环境。如果必须跨版本,用新环境重新加载 .h5 再另存为 .keras 格式,一步到位。
5.5 CSV 时间不连续导致预测时间轴错位
现象:预测值能正常输出,但画到图上,预测点的时间戳和历史数据对不齐,整条曲线错位好几个点。
原因:CSV 里有重复时间戳,或者时间间隔不固定。读进来后 index 没排序去重,滑动窗口切出来的"最近 24 个点"实际跨越了更长的时间段,模型以为的"昨天"其实是"三天前"。
解决:读入后强制 reset_index、drop_duplicates、sort_index。data_loader 里的那两行去重排序代码就是干这个的,别删。
6. 二次开发:把单站预测改造成多站并联与接口服务
6.1 多站并联:一份代码训练 N 个站的模型
当前 train.py 一次只处理一个站点。改造思路是遍历 data/raw/ 下所有 CSV,每个站点独立训练、独立保存,模型文件名带上站点 ID:
import glob for csv_path in glob.glob('data/raw/station_*.csv'): station_id = csv_path.split('/')[-1].replace('.csv', '') DATA_PATH = csv_path # 复用 train.py 的完整流程 model.save(f'models/{station_id}_lstm.h5') joblib.dump(scaler, f'models/{station_id}_scaler.pkl')注意每个站点的水文特征不同,scaler 必须各存各的,共用同一个归一化器会让水位较低的站点预测值整体失真。
6.2 把预测封装成接口
实际业务里,预测结果通常要给下游系统调用。用 FastAPI 包一层是最快的方案,核心是保留 predict.py 的逻辑,暴露一个 POST 接口:输入最近 lookback 个实测值,返回未来 horizon 个预测值。做更长周期的展望时可以用"预测值回填":把刚预测出的第一个值拼进输入尾部,再预测下一个点,滚动往前走。回填超过 3 轮后误差会明显累积,结果只能当趋势参考,不能当精确值交给决策。
验证改造是否成功有个固定套路:先拿原模型在测试集上算一次 RMSE 作为基线,改造后每个站点的 RMSE 不高于基线 10% 才算通过。从那以后,我每次做水位预测的二次开发,都强制走一遍"先定基线、再改代码、最后对比 RMSE"的流程,翻车概率低了很多。希望帮到你。
本文还有配套的精品资源,点击获取