数学建模水质预测与评估全流程:预处理、特征工程与模型落地
简介面向2026亚太杯数学建模竞赛A题的水厂水质预测与评估完整解决方案内含特等奖标准Word论文、Python与MATLAB双版本源码、全量数据与结果表格及配套思路解析可按参赛团队不同水平直接对照使用。资源共77个文件csv与xlsx为主要数据表覆盖最终结果、中间处理与对比分析py为带中文注释的模块化源码docx为规范排版论文模板png为可直接引用的可视化图另含一键运行脚本、日志与说明文档整体仅1.59MB。当前已有1200余人学习下载。完整度方面论文包含摘要、假设、建模求解与灵敏度分析等全部环节代码覆盖数据清洗、模型训练、启发式寻优及结果出图支持一键复现数据表可作论文正文或附件有效提升说服力。适合希望打破瓶颈的建模新手以及目标直指特等奖的精英团队快速完稿。1. 这场数学建模赛的A题真正要交的不是模型而是这套“预测评估”闭环2026亚太杯数学建模竞赛题A围绕自来水厂水质预测与评估出题本质上是一个带时间戳的回归预测加多维指标综合评估问题。拿到题先别急着找最新网络上的深度学习代码这类题拿奖的关键不在模型多新而在数据清洗、特征工程和结论表达是否经得起推敲。所谓“全套代码思路助攻论文结果数据”拆开看就是一条流水线读懂题目要预测什么、评估什么把原始水质数据处理成可用样本训练可解释的主模型再用综合评价方法算出可排序的结论最后把所有数字和图表整理成交付物。这篇笔记按这条路径写适合第一次打数学建模的学生照着跑通也适合想优化细节的熟手回来对照参数和边界。2. 把“预测与评估”拆成可实现的流水线任务识别与预处理2.1 拿到题目先别急着建模每年A题都在考“读题”数学建模竞赛的A题很少只问一个“预测未来值”它通常是三到四个问题连在一起先预测某几个水质指标的未来变化再对当前水质做综合评价最后给出可操作的工艺建议。很多队伍翻车就翻在只盯着预测把评估当成附件里的一个表格。拿到题目后我一般先做三件事确认数据是截面还是时间序列确认每个小问要输出“数值预测”还是“排序结论”确认评价对象是单个时刻的综合水质还是多个采样点、多个时段之间的横向比较。预处理又是一道隐形的分水岭。自来水厂水质数据看起来整齐实际上缺测、离群、指标之间采样频率不一致是常态。pH、浊度、余氯、溶解氧、电导率这些指标来自不同仪表有的在线监测每小时一条有的化验室数据四小时甚至一天才一条。不先解决采样频率对齐的问题后面所有特征工程都是在错位的数据上做文章。评审看论文时预处理细节常常是区分“套模板”和“真做过”的关键信号。2.2 水质数据预处理缺测、离群、对齐三件事第一件事是缺测值填充。水质数据的缺测往往是几小时到几十小时不等的连续空洞不是随机散点。短缺测用线性插值就够了长假缺测用“前若干天同一时刻的中位数”兜底。用全局均值填充是最省事但最害人的做法——水质有明显的昼夜周期全局均值会把凌晨和下午的正常波动全部抹平。import pandas as pd import numpy as np # 读入多指标时间序列time列解析为DatetimeIndex df pd.read_csv(water_quality.csv, parse_dates[time]) df df.set_index(time).sort_index() # 长度小于等于2小时的缺测线性插值 # 更长缺测用前7天同一小时的中位数兜底 def fill_missing(s, period7): s s.interpolate(methodlinear, limit_directionboth) med s.groupby([s.index.hour]).transform(median) return s.fillna(med) for col in [turbidity, ph, residual_chlorine, do]: df[col] fill_missing(df[col], period7)为什么用“同一小时的中位数”而不是“前一天同刻的值”因为水质周期里既有24小时节律也有天气、工艺调整带来的逐日差异中位数比单日快照更稳。interpolate的limit_directionboth保证序列开头和结尾的单个空洞也能被补上但超过2小时的长空洞不要指望线性插值它会把缺测段拉成一条直线给后续模型注入大量平滑假象。第二件事是离群值识别。浊度突然冲到几十NTU、余氯骤降到零这类事件要么是传感器故障要么是真实的水质波动。做预测时真实波动要保留传感器故障要修正。用全局3σ判断离群在时间序列上不靠谱——水质本身有周期波动全局均值附近的正常高值会被误杀。我一般用滚动窗口内的中位数绝对偏差MAD相当于一个“局部稳健版3σ”。# 用滚动MAD识别离群值窗口取48小时贴近两天周期 win 48 median df[turbidity].rolling(win, centerTrue).median() mad (df[turbidity] - median).abs().rolling(win, centerTrue).median() threshold 3 * 1.4826 * mad df[turb_abnormal] (df[turbidity] - median).abs() threshold # 对确定为传感器尖峰的异常点做截尾替换而不是直接删行 df.loc[df[turb_abnormal], turbidity] median[df[turb_abnormal]]滚动窗口越大对突变越迟钝48小时窗口适合捕捉“反冲洗后浊度冲高回落”这类工艺事件又不会把正常昼夜波动标成异常。注意1.4826是把MAD换算成标准差尺度的常数不乘它阈值就偏严。异常点处理上我倾向于“修正”而不是“删行”删行会在时间轴上留下空洞后续做滞后特征时会引入错位。2.3 用resample统一时间粒度别让4小时数据和1小时数据打架第三件事是采样频率对齐。在线仪表逐小时更新化验室数据四小时一次直接横向拼起来会制造大量重复值。正确做法是先统一到小时粒度低频列用ffill向下填充。这里有个版本坑pandas 2.2之后fillna(methodffill)被移除老代码直接报错统一写成df.ffill()更稳。# 统一到小时高频列取每小时均值低频列先重采样成小时空网格再ffill df_hourly df.resample(H).mean() # 化验室指标如COD、氨氮逐小时重采样后会产生NaN # 用ffill把上一时段的化验值带到下一时段但这会引入“滞后已知性” df_hourly[cod] df_hourly[cod].ffill() df_hourly[nh3n] df_hourly[nh3n].ffill()ffill看起来解决了对齐问题实际上埋了一个隐患填出来的值在预测时刻还没有“真实获得”。比如化验室早上8点出结果填到8点到11点模型拿11点的预测输入里却用了12点的化验值这就是数据泄漏。解决方法是给ffill得到的值做一步shift即只允许使用上一时刻已知的值。这个细节写进论文里是让评审认可数据严谨性的有效一招。3. 从时序数据里挖出能进模型的特征周期与滞后项3.1 先画图再建模型周期、相关性和异常事件的可视化探路水质数据里最值钱的信息是周期。自来水厂一天之内的用水量有早中晚三个高峰浊度、余氯跟着波动一周之内工作日和周末的用水结构不同雨季和干旱季的源水水质更是两套系统。不把这些周期转成特征再强的模型也只能学到一个平均水位。先画出两周的原始曲线再按小时聚合看昼夜模式这两张图既能帮你决定特征怎么构造后期还能直接放进论文的“数据探索”一节。import matplotlib.pyplot as plt import seaborn as sns # 看前两周的浊度和余氯原始曲线找突变事件和周期 fig, ax plt.subplots(2, 1, figsize(14, 6), sharexTrue) ax[0].plot(df_hourly.index[:14*24], df_hourly[turbidity][:14*24]) ax[0].set_ylabel(turbidity (NTU)) ax[1].plot(df_hourly.index[:14*24], df_hourly[residual_chlorine][:14*24]) ax[1].set_ylabel(residual chlorine (mg/L)) fig.tight_layout() # 按小时聚合看一天内是否存在稳定的峰谷 hourly_mean df_hourly.groupby(df_hourly.index.hour)[[turbidity, residual_chlorine]].mean() hourly_mean.plot(figsize(10, 4))除了趋势图至少还要画一张相关性热力图。余氯和浊度如果稳定负相关说明颗粒物正在消耗消毒剂这是一个可以写进论文的领域结论某个指标在滞后24小时处与目标指标的相关性高于其他滞后阶那就是“用昨天的同一时刻去预测今天”的量化证据。画热力图用seaborn一行代码但要注意先用dropna()把空行去掉否则corr()的结果里会出现大量NaN。3.2 三板斧特征滞后、滚动窗口、时间编码表格型时间序列任务里LightGBM这类树模型最擅长吃的是三类特征滞后值、滚动统计值、周期编码。滞后值告诉模型“上一小时发生了什么”滚动统计告诉模型“最近一段时间的趋势和波动”周期编码把“现在是几点、星期几”变成模型能直接切分的数值。# 滞后特征选1、2、3小时捕捉短期惯性24、168小时对齐日周期和周周期 for lag in [1, 2, 3, 24, 168]: df_hourly[fturb_lag{lag}] df_hourly[turbidity].shift(lag) df_hourly[fchlorine_lag{lag}] df_hourly[residual_chlorine].shift(lag) # 滚动统计过去3/6/24小时的均值与标准差刻画近期波动 for w in [3, 6, 24]: df_hourly[fturb_roll{w}_mean] df_hourly[turbidity].rolling(w).mean() df_hourly[fturb_roll{w}_std] df_hourly[turbidity].rolling(w).std() # 时间编码小时、星期几、是否周末树模型可直接用数值切分 df_hourly[hour] df_hourly.index.hour df_hourly[dayofweek] df_hourly.index.dayofweek df_hourly[is_weekend] (df_hourly[dayofweek] 5).astype(int)滞后项不是越多越好。加了168小时滞后等于让模型对比“上周同一时刻”对稳定水厂有效但会吃掉七天的数据长度数据只有一个月时滞后168小时会把样本量砍掉四分之一。滚动窗口超过24小时后均值基本被平滑成长期水平突变信息也一起丢了。我的习惯是先构造上述候选特征再用模型的特征重要性去筛而不是一上来就堆几十列。目标变量的构造同样要小心。如果要预测下一小时的余氯目标应该是当前余氯值的下一条记录也就是shift(-1)。构造完特征后一定要dropna()把头部因为shift产生的空行清掉。常见错误是忘了删最后一行——最后一条样本的y是未来值训练时看着合理实际是拿未来的答案教模型验证集指标会假性偏乐观。4. 预测、评估与排序LightGBM、熵权法与TOPSIS的落地方案4.1 预测模型LightGBM为主LSTM做对照水质预测的表格式数据我首选LightGBM而不是LSTM。这类题目的数据量通常是一个月到几个月的小时级数据最多几千行LSTM在小样本上很容易过拟合训练出来的loss曲线经常是标准的“U形澡盆”——训练集一路下降验证集先降后崩。LightGBM对缺失值宽容、训练快、自带 feature_importance能给出理由充分的特征解释这对竞赛评审来说非常重要。LSTM不是不能用而是应该放在“对照实验”的位置真正扛大梁的是树模型。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error # 训练数据按时间顺序切分绝不做随机打乱 cut int(len(X) * 0.8) X_train, X_val X.iloc[:cut], X.iloc[cut:] y_train, y_val y.iloc[:cut], y.iloc[cut:] model lgb.LGBMRegressor( n_estimators800, learning_rate0.03, num_leaves31, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.5, random_state42, verbose-1 ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], callbacks[lgb.early_stopping(50), lgb.log_evaluation(0)] ) pred model.predict(X_val) rmse mean_squared_error(y_val, pred, squaredFalse) mae mean_absolute_error(y_val, pred)early_stopping在验证集上连续50轮没有提升就停止训练既防止过拟合又省去手动选迭代次数的麻烦。学习率0.03配合800棵树是比较稳的起点如果数据量只有几百行num_leaves超过31就很容易把叶子切得太碎。subsample和colsample_bytree相当于给树模型加正则防止模型记住个别异常样本。评估指标用RMSE和MAE为主不要迷信MAPE——余氯浓度在0.1 mg/L附近时分子一抖动MAPE直接爆表不能反映真实水平。多步预测要不要做如果题目要求预测未来24小时常见做法是滚动预测把上一步的预测值当作最新的滞后特征逐小时推下去。这个方案简单但误差会随步长累积一般推到第6到12个小时之后曲线就开始偏离。更稳的做法是只预测下一时刻靠模型反复滚动并把最终曲线与真实数据画在一起让评审看到误差累积的边界。不要为了显得“完整”硬凑一个24步直接多输出的黑匣子解释不清反而扣分。4.2 评估模型用熵权法定权重再用TOPSIS排序“评估”这两个字在竞赛题里通常有两层意思给水质定等级或给多个水厂/多个时段排优劣。定等级需要对照标准比如《生活饮用水卫生标准》中的限值排序则要用到综合评价模型。常见做法是熵权法定权重加TOPSIS排序。熵权法的思想是某个指标在所有样本上的取值差异越大它携带的信息量越多权重就应该越高。这种方法不需要专家打分完全由数据驱动可复现性好。# 熵权法输入为(样本数, 指标数)的numpy数组已按正向/负向处理 def entropy_weight(X): # 先做归一化每个样本在指标上的占比 X X / X.sum(axis0) k 1 / np.log(len(X)) # 信息熵越小说明差异越大权重越高 e -k * (X * np.log(X 1e-12)).sum(axis0) w (1 - e) / (1 - e).sum() return w # TOPSIS计算各样本到正理想解和负理想解的加权距离 def topsis(X, w, is_pos): # is_pos标记正向指标成本型指标取倒数实现正向化 X np.where(is_pos, X, 1 / np.maximum(X, 1e-9)) X_norm X / np.sqrt((X ** 2).sum(axis0)) weighted X_norm * w ideal_pos weighted.max(axis0) ideal_neg weighted.min(axis0) d_pos np.sqrt(((weighted - ideal_pos) ** 2).sum(axis1)) d_neg np.sqrt(((weighted - ideal_neg) ** 2).sum(axis1)) score d_neg / (d_pos d_neg) return score水质评估里最容易被忽略的是“适度指标”问题。余氯和溶解氧都不是越大越好余氯过高会产生消毒副产物溶解氧过饱和也会影响口感这类指标对应的是一个最优区间。直接把它们当正向指标送进熵权法算出来的权重会和常识完全相反。做法是先做区间型正向化——在最优区间内取1越偏离区间取值越接近0再做归一化和熵权计算。这个处理既是踩坑点也是加分点写论文时单独列一小节说明评审会认为你真的理解了水质评价业务。模型分工总结预测题用LightGBM出主要数字LSTM做对照说明为什么不用深度模型评估题用熵权法处理权重争议用TOPSIS给出可排序的结论。两者通过结果数据衔接——预测出的未来指标值可以当作评估阶段的新样本输入形成一个“先预测后评估”的完整闭环。5. 水质预测与评估的5个高频踩坑现象、原因与解决办法5.1 数据处理带来的三个坑数据泄漏、滞后映射、区间指标坑1验证集指标漂亮提交后被指出数据泄漏。现象是训练集RMSE和验证集RMSE差距很小但换一段新数据预测效果立刻崩掉。原因多半是归一化或插补用了全量数据的统计量。我在处理时见到太多示例代码是把scaler.fit()放在切分数据之前这在时间序列上是致命的。解决办法是严格按时间切分归一化只在训练段上fit再用训练段的参数去transform验证段。# 错误写法先在全量上fit再切分 # scaler.fit(X) # X_all scaler.transform(X) # 正确写法切分后只在训练段拟合 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train scaler.fit_transform(X_train) X_val scaler.transform(X_val)坑2预测曲线整体滞后真实曲线约一小时。现象是RMSE不高但画出来的预测曲线明显比真实值晚一拍峰和谷都对不上。原因通常是目标构造用了shift(-1)但没删最后一行或者滞后特征里混进了目标列的未来值。这属于时间序列预测里最隐蔽的“未来信息穿透”。解决办法是建模前输出前五行的特征和目标做人工检查确认第t行的所有特征都是t时刻及以前已知的信息。坑3熵权法算出来的权重与专业常识完全相反。现象是pH权重最高、余氯权重最低论文里没法解释。原因要么是离群值没有清洗指标方差被传感器尖峰主导要么是没做适度指标正向化直接把原始值喂进去了。解决方法是先按第2章的滚动MAD清洗离群值再对余氯、溶解氧这类区间型指标做正向化最后才计算熵权。顺序调换过来权重分布会合理得多。5.2 模型与交付容易坑住人的两处LSTM过拟合、图表对不上坑4LSTM训练损失下降验证损失发散成U形曲线。现象是训练集loss一路走低验证集先降后涨典型的澡盆曲线。原因很简单样本量只有几千行LSTM的参数却成千上万噪声也被当成规律学进去了。不要在这个阶段和深度模型较劲LightGBM在同期数据上通常能拿到更好的RMSE而且特征重要性可直接写入论文。LSTM留在对照实验里写一句“在小样本场景下树模型优于LSTM”就能把这个问题说清楚。坑5论文里的结果图和附件的CSV数字对不上。现象是论文正文写“验证集RMSE为0.032”结果表里却是0.044图例的曲线和坐标轴的刻度也不一致。原因几乎都是出图时手动改过数据或者每次跑代码没有固定随机种子多跑一次数字就变了。解决办法是全程固定random_state所有模型的预测结果先写CSV出图时直接从CSV读取不经过任何手工抄录。# 统一做法先存数据再画图一张图只对应一个结果文件 pred_df pd.DataFrame({ time: X_val.index, actual: y_val.values, pred: pred }) pred_df.to_csv(results/pred_val.csv, indexFalse)固定随机种子这件事比赛越到后期越要命。同一份代码两次运行的RMSE相差0.005本身不影响结论但评审如果发现图表数字错位会直接怀疑整个结果数据的真实性。宁可少调一轮参数也要把“可复现”当成硬指标。6. 从“能跑出数”到“交得出手”结果数据与图表的可用性清单比赛最后一天代码写的再漂亮如果结果数据和图表没有按照规范组织交付时依然会手忙脚乱。我自己的习惯是第一天就建好项目目录训练代码、特征工程、结果输出分开存放所有图从CSV读取所有CSV只由一个主脚本生成。# 绘图工具函数所有论文图表统一走这个入口 def save_fig(fig, name): fig.savefig(ffigures/{name}.png, dpi300, bbox_inchestight) fig.savefig(ffigures/{name}.pdf, bbox_inchestight)附件路径内容易错点data/clean/water_hourly.csv清洗并统一粒度后的数据保留原始值和清洗后值两列不要原地覆盖features/feature_table.csv特征工程后的宽表时间索引要完整列名可读results/pred_val.csv验证集预测值与真实值和论文中的RMSE/MAE数字严格对应results/weights.csv熵权法权重与TOPSIS得分与论文表格顺序一致指标名保持一致figures/*.png论文用图统一dpi300中文标注提前处理字体记得在最后半天做一次“空跑验证”用固定随机种子从上到下执行一遍主脚本确认所有CSV重新生成、所有图重新保存然后逐一对照论文正文里的表格数字。这个过程看着枯燥却实实在在救过我的比赛——有一次换了一个Python版本后旧代码在resample时行为变化全序列偏差了半个小时要不是空跑时发现CSV时间和图对不上整篇论文的预测结果都不成立。至于附录代码不要贴整份训练脚本只放关键的数据预处理函数和模型训练代码并保证它们与结果数据完全一致。这是竞赛里最容易被忽略却最被评审看重的地方。希望这个“先整理交付物再写代码”的习惯也能帮到你。本文还有配套的精品资源点击获取