简介面向高校Python课程设计与期末大作业场景这份基于机器学习的气温预测与可视化项目完整覆盖数据爬取、模型构建、结果对比与界面展示全流程。作者以天气数据集为基础实现MLP、LSTM、随机森林、决策树、线性回归五种回归模型的系统对比其中LSTM在准确率和性能上表现最佳并配套GUI可视化界面适合Python与机器学习入门及进阶学习者部署参考。资源共38个文件压缩包约12.17MB包含10个py脚本、4个ipynb笔记本、3个pkl模型、2个h5权重、2个joblib标准化器、2个csv数据集及使用手册docx并附带训练过程截图便于按模块查阅与复现。目前已有426人学习下载。除完整源码与文档外还提供全国天气信息爬取、临沧历史天气爬取等数据采集脚本以及LSTM气温预测、3层MLP气温预测等专项实现代码注释详细、部署简单是课程设计或期末大作业的高分参考资料。1. 气温预测这门 Python 课设为什么值得认真做一次把“气温预测”和“机器学习可视化”放到一起是 Python 课程设计里少有的“低成本、高完成度”选题。数据在公开气象站点就能拿到特征是连续型数值加时间戳模型用线性回归就能跑通再把预测曲线和真实曲线叠在一起画出来一个“数据清洗—特征工程—模型训练—评估—可视化展示”的完整闭环就齐了。它不挑硬件、不挑深度学习框架一台普通笔记本装好 Python 和机器学习常用包就能交出能演示、能答辩、能写进简历的成品。这门课设真正考验人的不是算法有多深而是工程习惯数据怎么切、特征怎么造、参数怎么调、结果怎么解释。对想往数据分析与可视化方向走的初学者来说把这样一个项目从头到尾做一遍比堆一个花哨但说不清原理的“大项目”更能练基本功。2. 数据先行用 pandas 整理气温序列特征不是越多越好气温预测的第一步不是建模而是先把数据搞到手并整理成模型能吃的格式。很多人拿着源码直接跑结果数据路径不对、列名对不上、日期解析失败问题全出在这一步。这块内容看起来简单却决定了后面所有环节能不能顺利落地。2.1 气温数据从哪来公开数据集、自己抓取、还是构造序列常见做法有三种各有适用场景我建议按课程设计的时间预算来选。第一种是用公开气象数据集。国内外气象机构都有开放的历史观测数据包含日期、最高温、最低温、湿度、气压、风速等字段。优点是真实、有说服力答辩时老师问“数据哪来的”你能答得理直气壮缺点是数据文件通常很大需要做字段筛选和缺失值处理有时还要做单位换算。第二种是调用天气类 API 拉取历史数据适合有接口权限、想做一点工程化操作的人但要注意接口频率限制和返回字段的稳定性别在答辩前发现字段变了。第三种是构造模拟序列比如用正弦波加噪声生成一条规律性的温度曲线。这个方案最省事但答辩时容易被追问“数据是否真实”只适合实在拿不到数据的极端情况。无论选哪种最终都要整理成统一结构一行代表一天列包含日期和若干个气象特征目标列是“当天的平均气温”或“当天的最高气温”。课程设计里最常见的做法是取一列目标气温做回归预测输入特征用日期之外的气象要素。提示数据文件别放在代码目录的深层路径里。项目根目录下建一个 data/ 文件夹代码里统一用相对路径读取这样整个压缩包发给别人也能直接跑。2.2 特征工程把日期转成有序特征日期字符串不能直接喂给模型拿到原始数据后最容易被忽略的一步是日期处理。假设数据长这样datetmaxtminhumipresstavg2023-01-015.2-2.1631023.11.62023-01-026.0-1.5591022.42.2这里的 tavg 就是我们要预测的目标列。date 是字符串没法直接参与数值计算但“月份”“星期几”“一年中的第几天”这些衍生特征是气温预测里非常有效的信号。下面这段代码把日期拆成数值特征import pandas as pd import numpy as np df pd.read_csv(data/temperature.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 从日期中提取周期性特征 df[year] df[date].dt.year df[month] df[date].dt.month df[day_of_year] df[date].dt.dayofyear df[day_of_week] df[date].dt.dayofweek # 周一为0 # 把 day_of_year 转换成正弦/余弦特征表达“季节”的周期性 df[season_sin] np.sin(2 * np.pi * df[day_of_year] / 365.0) df[season_cos] np.cos(2 * np.pi * df[day_of_year] / 365.0) # 确认没有空值 df df.dropna().reset_index(dropTrue) print(df.head())这段代码里最关键的是 season_sin 和 season_cos。如果直接把 day_of_year 作为数值特征喂给线性模型模型会学到“第 200 天一定比第 10 天热”这种错误规律因为 12 月 31 日和 1 月 1 日在数值上差了 364但实际只隔一天。正弦余弦变换把“一年中的位置”映射到圆形空间上季节连续性就保住了。加上 year 是为了捕捉逐年气候波动比如某些年份整体偏暖。day_of_week 对气温预测的效果通常很弱但对风速、用电负荷这类目标会更有用保留它不影响结果。2.3 训练集/测试集划分时间序列必须按时间切不能随机打乱这一点必须提前说因为它是气温预测项目里最常见的扣分点。拿 sklearn 的 train_test_split 默认参数随机切分会从整段数据里随机抽 20% 当测试集看起来训练集和测试集指标都不错但一旦把测试集改成“最近 30 天”模型立刻暴露原形。原因很简单气温数据是时间序列相邻日期的气温高度相关。随机切分时测试集里的每个样本都在训练集里有“邻居”模型等于提前偷看了答案。气温预测项目的正确切法是直接按时间顺序切train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] # 特征列和目标列分开 feature_cols [year, month, day_of_year, day_of_week, season_sin, season_cos, tmax, tmin, humi, press] X_train train_df[feature_cols] y_train train_df[tavg] X_test test_df[feature_cols] y_test test_df[tavg]这里有个细节tmax 和 tmin 分别代表当天的最高温和最低温如果目标列 tavg 是平均气温那么 tmax、tmin 属于“同一天才能知道的信息”。在真实业务里预测明天平均气温时明天的 tmax 和 tmin 还没发生所以严格来说这两个特征属于未来信息。但课程设计阶段很多项目都会用它们做特征因为预测目标变成了“已知当天最高最低温估算平均温”。答辩时老师问到你能说清楚这个设定就行。如果想做得更严谨可以把特征改成前一天的最高最低温即做特征平移。3. 模型选型与训练从线性回归到随机森林基线对了再谈调参数据准备完毕进入模型环节。气温预测本质是一个回归任务回归家族的算法基本都能用。课程设计阶段不追求刷榜追求的是“每个算法都能讲清楚原理并且有一个明确的成绩对比”。所以我的建议是先跑一个最简单的线性回归当基线再上随机森林或梯度提升最后用一张表对比结果。3.1 选算法先跑线性回归当基线再上随机森林为什么先跑线性回归因为它能在一分钟之内跑通整个流程而且结果可解释。气温和湿度、气压、季节特征之间确实存在近似线性关系线性回归在气温预测上的成绩不会太差通常能做到平均绝对误差 2℃ 左右。有了这个基线你才知道后面换复杂模型到底提升了多少而不是盲目堆参数。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model_lr LinearRegression() model_lr.fit(X_train, y_train) y_pred_lr model_lr.predict(X_test) mae_lr mean_absolute_error(y_test, y_pred_lr) rmse_lr mean_squared_error(y_test, y_pred_lr) ** 0.5 r2_lr r2_score(y_test, y_pred_lr) print(fLinearRegression MAE{mae_lr:.2f} RMSE{rmse_lr:.2f} R2{r2_lr:.4f})这段训练代码刻意保持了最小化fit 一行predict 一行指标三行。如果你连这都跑不通先检查 X_train 和 y_train 的行数是否一致再检查特征列里有没有字符串或空值。模型训练成功之后输出三个指标的含义要分清MAE 是平均绝对误差单位是摄氏度最直观气温预测里 MAE 在 1.5℃ 以内就算不错RMSE 会放大个别大误差样本的影响如果 RMSE 明显大于 MAE说明有些天的预测偏差很大R² 接近 1 不代表绝对误差小它衡量的只是模型相对“纯用均值预测”的改进比例。3.2 上树模型随机森林和梯度提升的对比实验线性回归跑完接着上随机森林。随机森林对异常值不敏感能学到特征间的非线性关系比如“高温高湿时体感温度非线性上升”这类模式。梯度提升GradientBoostingRegressor在表格数据上通常比随机森林更准但对参数更敏感调参失败容易过拟合。from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor model_rf RandomForestRegressor( n_estimators300, max_depth12, min_samples_leaf4, random_state42, n_jobs-1 ) model_rf.fit(X_train, y_train) y_pred_rf model_rf.predict(X_test) model_gbdt GradientBoostingRegressor( n_estimators300, max_depth4, learning_rate0.06, random_state42 ) model_gbdt.fit(X_train, y_train) y_pred_gbdt model_gbdt.predict(X_test) for name, pred in [(RandomForest, y_pred_rf), (GradientBoosting, y_pred_gbdt)]: mae mean_absolute_error(y_test, pred) print(f{name} MAE{mae:.2f})参数这里重点说三个。n_estimators 是树的数量300 在这个数据量级下足够再大收益很小而且拖慢训练max_depth 是每棵树的深度随机森林设 12 左右够用梯度提升建议设小一些3 到 5 比较好因为梯度提升靠多棵树叠加单棵树太深会过拟合min_samples_leaf 控制叶子节点最少样本数设 4 到 6 能让预测更平滑避免个别极端天气样本把模型带偏。跑完三个模型后一定要把指标汇总成一张对比表这是课程设计报告里最有力的页面模型MAE (℃)RMSE (℃)R²LinearRegression1.872.410.942RandomForest1.522.030.961GradientBoosting1.461.950.964我这边的模拟结果是梯度提升略胜一筹但真实数据集上随机森林和梯度提升谁赢都不奇怪。如果你发现线性回归和随机森林差距极小很可能是特征没构造好或者数据量太小先回头检查特征工程而不是继续调参。3.3 用 TimeSeriesSplit 做交叉验证别用 K-Fold课程设计的训练流程里很多人会顺手用 K-Fold 交叉验证但对时间序列来说这是错误的。K-Fold 随机把样本分成 K 份每一折里都可能出现“训练集某一天的邻居在验证集里”的情况验证分数虚高。正确做法是用 TimeSeriesSplit它保证训练集永远在验证集之前from sklearn.model_selection import TimeSeriesSplit from sklearn.ensemble import RandomForestRegressor tscv TimeSeriesSplit(n_splits5) scores [] for train_idx, val_idx in tscv.split(X_train): X_t, X_v X_train.iloc[train_idx], X_train.iloc[val_idx] y_t, y_v y_train.iloc[train_idx], y_train.iloc[val_idx] model RandomForestRegressor(n_estimators200, max_depth10, min_samples_leaf4, random_state42, n_jobs-1) model.fit(X_t, y_t) pred model.predict(X_v) scores.append(mean_absolute_error(y_v, pred)) print(fTimeSeriesSplit MAE: {np.mean(scores):.2f} ± {np.std(scores):.2f})TimeSeriesSplit 的本质是按时间顺序切出 n_splits 个窗口第一个窗口训练集短、验证集靠后后面的窗口训练集越来越长。这样能模拟“用过去预测未来”的真实场景。注意代码里用了 iloc 而不是 loc因为索引在排序后仍然是连续的整数位置这样切分最安全。这组分数通常比最终测试集分数略差因为每个窗口的训练数据都比完整训练集少这是正常现象。4. 可视化落地把预测结果画成能“答辩”的图表模型训练完到了整个项目里最有展示度的一步可视化。课程设计答辩时老师不会盯着控制台里的准确率数字看而是看你的图表是否清晰、是否回答了问题。可视化要回答三个问题预测和真实值的贴合度如何、误差分布在哪些区间、模型依据什么特征做判断。4.1 预测曲线对比图真实值与预测值必须画在同一张图上最常见的可视化就是折线图。把测试集的真实气温和预测气温按日期顺序画成两条曲线一眼就能看出模型有没有跟随真实波动。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 中文字体 plt.rcParams[axes.unicode_minus] False # 正常显示负号 plt.figure(figsize(14, 5)) plt.plot(test_df[date], y_test, label真实气温, linewidth1.8) plt.plot(test_df[date], y_pred_gbdt, label预测气温, linewidth1.8, alpha0.85) plt.xlabel(日期) plt.ylabel(平均气温 (℃)) plt.title(测试集气温预测结果对比真实值 vs 预测值) plt.legend() plt.grid(alpha0.3) plt.tight_layout() plt.savefig(output/predict_compare.png, dpi150, bbox_inchestight) plt.show()这段代码里有两个容易踩坑的地方。第一中文字体必须一开始就设置否则标题和图例里的中文会变成方块第二savefig 的 bbox_inchestight 会裁掉多余留白保证图进到论文里不会边缘残缺。输出目录建议单独建一个 output/ 文件夹代码跑完自动把图存进去这在交作业时看起来非常规范。如果你的测试集时间跨度为几个月全部画在一张图里曲线会挤成一团。这时候可以只选取最近 60 天的数据段放大绘制保留整体图的同事再出一张局部放大图。答辩时两张图配合讲解先说整体趋势对得上再说局部细节的偏差来源。4.2 残差图和时间序列误差分析光看折线图远远不够折线图能看出趋势贴合度但看不出误差规律。残差图把真实值与预测值的差按时间顺序画出来能暴露模型在什么条件下系统性失效。比如夏天气温波动大时残差明显变大说明模型对极端高温的拟合不足。residual y_test.values - y_pred_gbdt fig, axes plt.subplots(1, 2, figsize(14, 4)) # 左侧残差时间序列 axes[0].plot(test_df[date], residual, colorcrimson, linewidth1.2) axes[0].axhline(y0, colorblack, linestyle--, linewidth0.8) axes[0].set_title(残差随时间变化) axes[0].set_xlabel(日期) axes[0].set_ylabel(真实值 - 预测值 (℃)) axes[0].grid(alpha0.3) # 右侧残差分布直方图 axes[1].hist(residual, bins30, edgecolorwhite, alpha0.8) axes[1].axvline(x0, colorblack, linestyle--, linewidth0.8) axes[1].set_title(残差分布直方图) axes[1].set_xlabel(残差 (℃)) axes[1].set_ylabel(频次) plt.tight_layout() plt.savefig(output/residual_analysis.png, dpi150, bbox_inchestight) plt.show()如果残差图呈现出“正残差集中在夏季、负残差集中在冬季”的规律说明模型没有完全学到季节变化的非线性特征下一版可以在特征里加入更高阶的温度交互项或者增大树模型的深度。如果残差直方图明显偏离钟形分布比如双峰就要怀疑数据里混入了不同来源的样本。这两张图画出来答辩时老师会认为你真的分析过模型行为而不是只跑了一个 predict。4.3 特征重要性可视化让模型从“黑匣子”变成可解释随机森林和梯度提升都有 feature_importances_ 属性能输出每个特征对预测的贡献权重。这往往是课设报告里最有“机器学习味道”的一张图因为它直接回答“模型靠什么信息做预测”。importances model_gbdt.feature_importances_ feat_names feature_cols idx_sorted np.argsort(importances)[::-1] plt.figure(figsize(8, 5)) plt.barh(range(len(idx_sorted)), importances[idx_sorted], colorsteelblue) plt.yticks(range(len(idx_sorted)), [feat_names[i] for i in idx_sorted]) plt.xlabel(特征重要性) plt.title(梯度提升模型的特征重要性) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(output/feature_importance.png, dpi150, bbox_inchestight) plt.show()通常你会发现 tmin 和 tmax 排在最前面season_sin / season_cos 紧随其后day_of_week 排在末尾。这符合预期当天的最高最低温本身就是平均气温的强相关变量季节特征提供了长期趋势信息。特征重要性图还有一个实际用途如果某个业务无关的特征排名异常高说明数据里有泄露需要回头检查特征构造逻辑。5. 避坑专题气温预测项目里最容易翻车的 5 个细节这部分是血泪经验总结。带过几年课程设计项目见过太多“源码能跑但分数不高”的作业问题几乎都集中在这几个地方。每一条都按踩坑现场、原因分析、解决方案来写。5.1 随机切分数据导致成绩虚高换最近数据就崩盘现象训练时 R² 高达 0.98自己觉得稳了但导师随机抽了最后 30 天做验证MAE 直接飙到 4℃ 以上。原因train_test_split 默认随机切分气温序列的相邻样本高度相关测试集与训练集之间存在大量信息重叠模型等于提前看到了“邻居”的答案。解决回到 2.3 节的按时间切分方案。这是气温预测项目第一原则训练集必须严格早于测试集。顺带提示做数据可视化分析时可以用全部数据但建模评估必须以时间切分为前提。5.2 日期字符串直接进模型训练直接报错现象明明照着教程写了模型代码fit 时却报 ValueError: could not convert string to float: 2023-01-01。原因pandas 读入的 date 列是字符串sklearn 的模型不接受非数值输入。有人把 date 直接用 LabelEncoder 编码成整数虽然能跑但模型会误以为日期数值有大小关系比如“2023-01-02 是 2023-01-01 的两倍”——这是完全错误的理解。解决要么删除 date 列只保留从日期里提取的 year、month、season_sin 等数值特征要么把 date 转成 datetime 类型后按 2.2 节处理。记住一句话特征必须是数值而且数值的含义必须符合物理意义。5.3 归一化时把训练集和测试集一起 fit预测结果虚高现象写完代码发现测试集 MAE 非常漂亮但把模型用到新数据上表现稀烂怀疑人生。原因很多初学教程会写 scaler.fit_transform(X)把整个 X包含训练和测试一起归一化。这会出两个问题一是测试集的信息渗入训练过程属于数据泄露二是新数据进来时如果超出之前 min-max 的范围归一化会得到超出 [0,1] 区间的数值模型面对没见过的大小会发懵。解决先 fit 训练集再用同一个 scaler 去 transform 测试集。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 这里只 transform不 fit # 后续训练全部用 X_train_scaled 和 X_test_scaled注意树模型随机森林、梯度提升不需要归一化因为决策树分裂时只比较阈值不受量纲影响。线性回归和后续要加的神经网络则需要归一化。如果你只用了树模型可以跳过这一步如果没有用归一化请务必在报告里说明理由。5.4 可视化中文乱码标题变方块负号变“-”现象plt.title(气温预测对比) 执行后图片标题是一排方框坐标轴的负号也变成奇怪的竖线。原因Matplotlib 默认字体不支持中文也没有正确渲染 Unicode 负号。这是可视化环节最常见的玄学问题和代码逻辑无关纯粹是环境配置。解决用 4.1 节的配置把字体指到系统自带的中文字体。plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, WenQuanYi Zen Hei] plt.rcParams[axes.unicode_minus] False如果服务器或纯净 Linux 环境没有这些字体可以先跑 fc-list :langzh 查看系统有哪些中文字体再把第一个参数改成实际存在的字体名。实操时建议在代码开头就全局配置别等画完几个图再返工。5.5 下载的“满分”源码跑不通依赖缺失、路径写死、单元格乱序现象下载的课程设计源码打开后jupyter notebook 从上到下跑一遍第三个单元格就报 ModuleNotFoundError换成自己电脑跑又报 FileNotFoundError因为代码里写的是作者本机的绝对路径。原因课程设计源码的作者通常在自己电脑上开发没有刻意保证可移植性。缺失的依赖包、写死的路径、乱序的执行单元格都会让代码换台电脑就翻车。解决拿到任何源码第一件事是重写一个 requirements.txt 并开一个干净环境。具体做法是新建 conda 环境把代码依赖的 pandas、scikit-learn、matplotlib 等包固定版本写进 requirements.txt然后把代码里所有硬编码的绝对路径改成 data/ 和 output/ 相对路径最后从头到尾按顺序跑一遍每跑一个模块确认输出符合预期再进下一个。这个习惯不仅适用于课程设计以后接手任何开源代码都用得上。6. 进阶一招用滚动预测把单点预测做成连续曲线前面做的都是“用测试集整段数据一次性预测每一天”这在工程上叫多步独立预测。但真实的气温预报场景不是这样的要预测明天就用截至今天的数据做推理要预测后天最合理的做法是先把明天的预测值拼进历史再往后推一步。这种递归滚动预测才是时间序列预测的实战形态也是课程设计里拉开档次的一个加分技巧。滚动预测的逻辑很简单假设用过去 7 天的数据作为特征窗口每次只预测下一天预测出来后把结果加入窗口滚动前进。def rolling_predict(model, scaler, history, n_days): history: list of dict, 每个元素是一天的特征值 n_days: 连续预测多少天 predictions [] for _ in range(n_days): # 取最近 7 天数据拼成一行特征 recent history[-7:] # 按模型的特征列顺序构造输入向量这里只做示意 X_input np.array([recent[-1][c] for c in feature_cols]).reshape(1, -1) # 如果是线性模型需要归一化树模型可以不处理 X_input_scaled scaler.transform(X_input) if scaler else X_input y_pred model.predict(X_input_scaled)[0] predictions.append(y_pred) # 关键一步把预测值当成“真实值”推进历史窗口 history.append({**recent[-1], tavg: y_pred}) return predictions这个循环的核心是 history.append 这一步预测结果被当作已知信息塞回窗口。它的问题也很明显预测误差会随步长累积预测到第 10 天时可能已经明显偏离真实趋势。所以在报告里滚动预测的评估一般只看前 3 到 7 步并单独画一张“第 1 天预测误差、第 2 天预测误差……”的曲线说明误差随步长增长的情况。这比一张完美的整体拟合图更能体现你对时间序列问题的理解。我最早做课设时也喜欢直接整段预测画出来的图完美贴合觉得模型很厉害。后来拿滚动预测一测第 5 天就已经跑偏了才意识到一次性预测和真实场景的差距。从那以后我看任何时间序列项目的源码第一件事就是看它怎么切数据、怎么组织预测流程而不是盯着 R² 数字看。如果你时间充裕强烈建议把滚动预测和一次性预测的误差做一张对比表放进报告里老师看到这一页分数基本就稳了。希望这个项目能帮你把机器学习的整个流程真正跑通也希望你做完之后不只是拿到一份能交差的源码而是可以跟别人讲清楚每一步为什么这么做。本文还有配套的精品资源点击获取
