☰
数学建模水质预测与评估全流程:预处理、特征工程与模型落地
2026/10/11 21:27:42 网站建设 项目流程

简介:面向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, period=7): s = s.interpolate(method='linear', limit_direction='both') 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], period=7)

为什么用“同一小时的中位数”而不是“前一天同刻的值”?因为水质周期里既有24小时节律,也有天气、工艺调整带来的逐日差异,中位数比单日快照更稳。interpolate的limit_direction='both'保证序列开头和结尾的单个空洞也能被补上,但超过2小时的长空洞不要指望线性插值,它会把缺测段拉成一条直线,给后续模型注入大量平滑假象。

第二件事是离群值识别。浊度突然冲到几十NTU、余氯骤降到零,这类事件要么是传感器故障,要么是真实的水质波动。做预测时,真实波动要保留,传感器故障要修正。用全局3σ判断离群在时间序列上不靠谱——水质本身有周期波动,全局均值附近的正常高值会被误杀。我一般用滚动窗口内的中位数绝对偏差(MAD),相当于一个“局部稳健版3σ”。

# 用滚动MAD识别离群值:窗口取48小时,贴近两天周期 win = 48 median = df['turbidity'].rolling(win, center=True).median() mad = (df['turbidity'] - median).abs().rolling(win, center=True).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(method='ffill')被移除,老代码直接报错,统一写成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), sharex=True) 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[f'turb_lag{lag}'] = df_hourly['turbidity'].shift(lag) df_hourly[f'chlorine_lag{lag}'] = df_hourly['residual_chlorine'].shift(lag) # 滚动统计:过去3/6/24小时的均值与标准差,刻画近期波动 for w in [3, 6, 24]: df_hourly[f'turb_roll{w}_mean'] = df_hourly['turbidity'].rolling(w).mean() df_hourly[f'turb_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_estimators=800, learning_rate=0.03, num_leaves=31, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=0.5, random_state=42, 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, squared=False) 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(axis=0) k = 1 / np.log(len(X)) # 信息熵越小说明差异越大,权重越高 e = -k * (X * np.log(X + 1e-12)).sum(axis=0) 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(axis=0)) weighted = X_norm * w ideal_pos = weighted.max(axis=0) ideal_neg = weighted.min(axis=0) d_pos = np.sqrt(((weighted - ideal_pos) ** 2).sum(axis=1)) d_neg = np.sqrt(((weighted - ideal_neg) ** 2).sum(axis=1)) 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过拟合、图表对不上

坑4:LSTM训练损失下降,验证损失发散成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', index=False)

固定随机种子这件事,比赛越到后期越要命。同一份代码两次运行的RMSE相差0.005,本身不影响结论,但评审如果发现图表数字错位,会直接怀疑整个结果数据的真实性。宁可少调一轮参数,也要把“可复现”当成硬指标。

6. 从“能跑出数”到“交得出手”:结果数据与图表的可用性清单

比赛最后一天,代码写的再漂亮,如果结果数据和图表没有按照规范组织,交付时依然会手忙脚乱。我自己的习惯是:第一天就建好项目目录,训练代码、特征工程、结果输出分开存放,所有图从CSV读取,所有CSV只由一个主脚本生成。

# 绘图工具函数:所有论文图表统一走这个入口 def save_fig(fig, name): fig.savefig(f'figures/{name}.png', dpi=300, bbox_inches='tight') fig.savefig(f'figures/{name}.pdf', bbox_inches='tight')
附件路径内容易错点
data/clean/water_hourly.csv清洗并统一粒度后的数据保留原始值和清洗后值两列,不要原地覆盖
features/feature_table.csv特征工程后的宽表时间索引要完整,列名可读
results/pred_val.csv验证集预测值与真实值和论文中的RMSE/MAE数字严格对应
results/weights.csv熵权法权重与TOPSIS得分与论文表格顺序一致,指标名保持一致
figures/*.png论文用图统一dpi=300,中文标注提前处理字体

记得在最后半天做一次“空跑验证”:用固定随机种子从上到下执行一遍主脚本,确认所有CSV重新生成、所有图重新保存,然后逐一对照论文正文里的表格数字。这个过程看着枯燥,却实实在在救过我的比赛——有一次换了一个Python版本后,旧代码在resample时行为变化,全序列偏差了半个小时,要不是空跑时发现CSV时间和图对不上,整篇论文的预测结果都不成立。

至于附录代码,不要贴整份训练脚本,只放关键的数据预处理函数和模型训练代码,并保证它们与结果数据完全一致。这是竞赛里最容易被忽略却最被评审看重的地方。希望这个“先整理交付物再写代码”的习惯,也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询