简介:基于Python机器学习(ML)的天气预测与可视化项目源码及配套全部数据,主要面向计算机相关专业正在做课程设计、期末大作业的学生,也适合需要完整项目实战练习的初级学习者。项目覆盖数据抓取、清洗、特征处理、模型训练到可视化展示的完整流程,可帮助快速理解机器学习落地路径,也能直接用于毕设或课设的代码演示与扩展。压缩包共24个文件,以CSV数据文件、Python脚本、JPG结果图为主,另有Markdown说明文档、pickle模型文件以及HTML可视化页面,从数据到模型再到展示均有对应模块。资源包大小约1.42MB,下载即用,已经严格调试保证可运行。当前已有249人学习下载,适合希望快速获取可运行工程、并在此基础上进行功能优化和数据替换的读者。
1. 拿到「95 分大作业」zip:先别急着跑,先看它是否闭环
拿到基于 python 机器学习(ml) 的天气预测和天气可视化源码+全部数据(95分以上大作业).zip,第一反应别是搓手期待,而是先检查三样东西:数据是不是完整、代码是不是真能跑、可视化是不是能证明模型有效。以我帮人调过不少课程设计项目的经验看,这种压缩包的真正价值不在那个「95 分」,而在于它是否具备从原始数据到建模再到图表验证的完整闭环。很多人下载同类源码后直接跑一下就交了,结果换数据、改参数立刻翻车。这篇笔记就按这条闭环展开:数据怎么整理、特征怎么造、模型怎么选、可视化怎么做,以及最容易被扣分的几个隐藏坑。适合正在做天气预测大作业、或者想快速搭一个机器学习落地 demo 的人照着做。
2. 天气数据与特征工程:为什么同一个 CSV,有人能做出 95 分
2.1 先认字段:日级别天气预测最容易起步
天气预测类课程设计最常见的数据载体,是一份日级别的历史 CSV,一行代表一天。字段一般包括:日期(date)、当天最高气温(tmax)、最低气温(tmin)、天气现象(weather,晴/雨/阴这种文本)、湿度(humidity)、气压(pressure)、风向风力(wind_dir、wind_level)。如果压缩包里正好是这种结构,开局就很顺;如果给的是逐小时观测记录,你需要先按天聚合,再把最高温、最低温这些目标列提炼出来。
常见做法是先写几行代码确认数据到底长什么样,别急着建模:
import pandas as pd df = pd.read_csv( "weather.csv", parse_dates=["date"], # 解析成 datetime,后面才能做时间切分 encoding="utf-8" ) print(df.shape) print(df.head()) print(df.dtypes)parse_dates这一步比想象中重要。很多 CSV 里的日期是字符串,不转成 datetime,后面做时间切分、提取季节特征都会处处碰壁。df.shape先看规模,如果数据不足 300 行,模型几乎没有泛化能力,你应该优先去找更长年份的数据,而不是在调参上耗时间。
预测目标也最好先定下来。常见做法是预测「明天最高气温 tmax」,连续值回归,评分直观,报告好写;预测「是否下雨」则变成二分类,适合想换口味的人,但展示时多一张混淆矩阵更好。我这里按回归任务往下讲,分类版本只需要把评估指标换成准确率和 F1。
假如压缩包里的数据不多或者字段不全,常见做法是去公共气象历史数据网站按城市导出多年的日观测记录,清洗成上面这种 CSV。注意别只下一年的数据,通常三年起步、五年更稳,训练样本越足,后面模型误差才压得下来。拿到别人的源码也不要急着跑,先确认它的数据目录下是不是有完整 CSV;如果只有代码没有数据,跑通后换上自己的数据验证一次,才算真正吃透这套方案。
2.2 缺失值处理:fillna(0)是天气任务里的大忌
新手面对温度、湿度、气压里的 NaN,第一反应往往是fillna(0),这在天气任务里是灾难。气温出现 0℃ 并不极端,但它不是合理的中位数替代,会让模型在某些样本上严重失真。更稳的顺序是:先看缺失率,再按时间属性决定填充策略。
# 1. 缺失率检测 miss = df.isnull().mean() print(miss[miss > 0]) # 2. 先保证行顺序,插值才不会乱 df = df.sort_values("date").reset_index(drop=True) # 3. 连续字段按时间插值 df["tmax"] = df["tmax"].interpolate(method="time") df["humidity"] = df["humidity"].interpolate(method="time") df["pressure"] = df["pressure"].interpolate(method="time") # 4. 类别字段用前一天的值向前填充 df["weather"] = df["weather"].ffill()interpolate(method="time")会按时间间隔计算缺失值,比默认的线性插值更贴合气温日变化的规律。ffill()对类别字段是合理的默认选择:昨天晴、今天缺,很大程度上就是「维持原状」。
这里还要额外检查一类脏数据:tmax 小于 tmin 的记录,多半是录入错误,直接剔除比让模型硬学更安全。湿度超过 100、气压跑出正常范围也是类似情况,用布尔过滤比用「平均化」处理更干净:
# 过滤明显异常:最高温低于最低温的记录直接去掉 df = df[df["tmax"] >= df["tmin"]].copy() # 过滤湿度越界(如果原始数据有湿度字段) df = df[(df["humidity"] >= 0) & (df["humidity"] <= 100)].copy()提示:缺失率超过 40% 的字段,别硬留。课程设计看的是你「有没有处理缺失值」这个动作,不是一个坏字段被你硬救回来;没有把握就删列,写报告时还能强调你做了字段筛选。
2.3 特征工程:给模型一双看得懂「季节」的眼睛
原始 CSV 里日期只是一个字段,但模型不理解「5 月应该比 1 月热」。我们需要把日期拆成数值特征,让模型能找到季节规律。这个环节是拉开分数差距的关键。
# 从 date 里提取三个时序特征 df["dayofyear"] = df["date"].dt.dayofyear # 一年中的第几天,范围 1~365 df["month"] = df["date"].dt.month # 月份,1~12 df["weekday"] = df["date"].dt.weekday # 星期几,0~6 # 特征列 features = ["dayofyear", "month", "weekday", "humidity", "pressure"] X = df[features] y = df["tmax"]dayofyear是这类任务里性价比最高的特征:它给模型一条连续的季节曲线,模型可以从「第 200 天」推断出盛夏。weekday对天气预测的实际贡献有限,但它代表你考虑过周期性,写报告时不会被挑理。容易踩的坑是直接把「年份」加进特征:年份更像时间戳而不是预测因子,放进特征后模型容易在训练集和测试集之间学出假趋势。
文本字段weather也不能直接进模型。树模型(随机森林、GBDT)可以直接用整数编码:df["weather_code"] = df["weather"].astype("category").cat.codes;线性模型则建议做 one-hot。原因在于线性回归假设特征之间的数值大小有意义,把晴天编码成 1、雨天编码成 2,会让模型误以为雨天比晴天的数值更大;而树模型在分裂时只看类别边界,整数编码不伤效果。
如果想让线性模型更强一点,可以把日期循环特征也补上:dayofyear是连续递增的,12 月 31 日与 1 月 1 日在数值上隔了 364,但实际只差一天,正弦余弦成对特征能表达这种循环关系:
import numpy as np df["season_sin"] = np.sin(2 * np.pi * df["dayofyear"] / 365) df["season_cos"] = np.cos(2 * np.pi * df["dayofyear"] / 365) features_all = ["season_sin", "season_cos", "month", "weekday", "humidity", "pressure", "weather_code"]模型训练前还可以跑一次相关性检查,看看哪些特征和目标列的线性关系强,这个数字写进报告很有说服力:
corr = df[features_all + ["tmax"]].corr()["tmax"] print(corr.sort_values())相关性不为零不代表特征有用,但相关性接近零且本来就是冗余的字段,可以考虑从特征列表移除,减少模型过拟合风险。
2.4 样本量不够怎么办:聚合、补数据与客观预期
写这种课程设计,经常遇到一个现实约束:数据集只有 365 行(一年)。365 行做日级别预测,模型容易过拟合,测试集 20% 只剩 73 个样本,误差波动很大。应对方法有三条。
第一条,降低任务粒度,把日预测改成月均预测或周均预测,用groupby聚合后重新切分,样本数虽然更少,但噪声也小。第二条,拉长数据年限:很多公共气象历史数据能导出十年以上,拿到后按城市筛选即可。第三条,接受现实:一年数据不是不能做,只是报告里要写明数据规模限制,然后重点展示特征工程和可视化能力,老师通常不会因为客观数据量而扣分。
3. 机器学习建模:别急着上深度模型,先把 sklearn 的三板斧跑通
3.1 回归还是分类:先定题,再选模型
做天气预测,拿到数据后第一件事是回看目标列。预测「明天最高温多少度」是回归;预测「明天下不下雨」是分类;预测「晴、阴、雨、雪」则是多分类。三者评价指标不同,报告写法也完全不同。
| 任务 | 例子 | 模型出口 | 报告里最常放的指标 |
|---|---|---|---|
| 回归 | 明天 32℃ | 回归 | RMSE / MAE |
| 二分类 | 明天是否降雨 | 分类概率 | accuracy / F1 |
| 多分类 | 晴阴雨雪 | 多个类别概率 | 混淆矩阵 |
大多数天气预测课程设计都是回归任务。回归任务分数最直观:模型偏 1 度还是 2 度,老师一眼能看懂;如果数据里的目标列是「天气现象」这种离散值,再切回分类路线也不难,只是可视化部分要换成混淆矩阵热力图。
课程设计阶段完全不需要上深度学习。天气预测这种低维表格数据,sklearn 的模型在性能、训练速度和可解释性上都更合适;神经网络对环境配置要求高,答辩现场还容易因为依赖装不上而翻车。
3.2 先用线性回归打 baseline:分数不是越高越好,先知道底线
建模不要一上来就上随机森林。线性回归是天气预测这类低维表格数据的合理起点:它快、可解释,能告诉你哪些特征没有用。先跑通 baseline,再换集成模型,你才知道提升到底来自哪里。
from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, mean_absolute_error # 按时间切分:前 80% 训练,后 20% 测试 split_date = df["date"].quantile(0.8) train_mask = df["date"] < split_date model = LinearRegression() model.fit(X[train_mask], y[train_mask]) pred = model.predict(X[~train_mask]) print("RMSE: %.2f ℃" % mean_squared_error( y[~train_mask], pred, squared=False)) print("MAE: %.2f ℃" % mean_absolute_error(y[~train_mask], pred))这里的要点是:不能直接用train_test_split的默认参数,因为它默认shuffle=True,会把时间序列随机打乱。天气数据是强时间序列,跨越时间的随机抽样等于用 6 月的规律去考 1 月,分数虚高到没意义。这也是天气预测大作业里最隐蔽的翻车点,后文避坑清单还会再讲一次。
split_date = df["date"].quantile(0.8)的意思是取时间维度上的 80% 分位点作为切分边界:之前训练、之后测试。用 quantile 而不是写死年份,换数据集时不用改代码。
RMSE 和 MAE 的区别也要心里有数:MAE 是平均绝对误差,直观;RMSE 因为有平方项,对「大幅偏错」的样本更敏感。同一个模型,RMSE 通常大于 MAE,两个都报,老师会认为你理解评价指标的含义。
3.3 随机森林:三个参数足够撑起成绩
线性回归跑通后,换随机森林通常能把 RMSE 再压下去一截,代码改动也不大。天气数据特征少、样本量不大,随机森林完全够用,不一定要上更重的方案。
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=200, # 树的数量,100~300 之间收益最大 max_depth=8, # 限制树深,防止背下个别日期 min_samples_leaf=3, # 叶子至少 3 个样本,给预测做平滑 random_state=42, # 固定随机种子,保证答辩现场可复现 n_jobs=-1, # 用满 CPU 内核,提高训练速度 ) rf.fit(X[train_mask], y[train_mask]) pred_rf = rf.predict(X[~train_mask]) print("RF RMSE: %.2f ℃" % mean_squared_error( y[~train_mask], pred_rf, squared=False))max_depth=8:在特征只有 5 到 8 个的天气任务上,树深 8 已经足够表达非线性关系,再深就容易把特殊日期的噪声记下来,训练集误差很低、测试集误差反而升高。min_samples_leaf=3等价于对叶子节点的输出做一个小平滑,能明显压低预测方差。random_state=42不是随手写的:树模型初始化有随机性,不固定的话,你实验报告里写的 RMSE 和答辩现场跑出来的 RMSE 可能差一截,老师会直接质疑报告真实性。
如果时间和机器允许,还可以换GradientBoostingRegressor,通常比随机森林略强,但它对学习率敏感,训练也慢。课程设计阶段我的意见是:先让随机森林达到一个稳定数字,有余力再试提升树,不要一开始就在两个模型间反复横跳。
3.4 有时间再调参:网格搜索只扫训练集
如果随机森林的分数已经稳定,下一步可以做一个轻量级网格搜索,把参数选择从「拍脑袋」变成「有依据」。
from sklearn.model_selection import GridSearchCV param_grid = { "n_estimators": [100, 200], "max_depth": [6, 8, 10], "min_samples_leaf": [2, 3, 5], } gs = GridSearchCV( RandomForestRegressor(random_state=42), param_grid, scoring="neg_mean_squared_error", cv=3, n_jobs=-1, ) gs.fit(X[train_mask], y[train_mask]) print(gs.best_params_)注意这里有个时序数据的细节:GridSearchCV默认会用随机交叉验证,对时间序列并不合适。课程设计阶段更安全的做法是只把网格搜索作用在已经切好的训练集内部,比如cv=3但保持shuffle=False的自定义切分;或者干脆手动列出几个参数组合,跑完对比就行。网格搜索的价值在于证明你调过参,而不是追求绝对最优参数。
4. 天气可视化:三张图让老师一眼看出你「真的做过」
4.1 真实值 vs 预测值曲线:用颜色标注偏差方向
可视化不是堆图,而是给模型可信度做证据。第一张图永远是测试集上的真实温度与预测温度对比曲线。
import matplotlib.pyplot as plt # 中文字体设置:一套 rcParams 同时解决中文和负号 plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "WenQuanYi Zen Hei"] plt.rcParams["axes.unicode_minus"] = False test_df = df[~train_mask].copy() test_df["预测"] = pred_rf plt.figure(figsize=(12, 5)) plt.plot(test_df["date"], test_df["tmax"], label="真实最高温", linewidth=1.2) plt.plot(test_df["date"], test_df["预测"], label="模型预测", linewidth=1.2) # 预测偏高的区域标浅红,偏低的区间自然留白 plt.fill_between( test_df["date"], test_df["tmax"], test_df["预测"], where=(test_df["预测"] > test_df["tmax"]), color="red", alpha=0.15, label="预测偏高" ) plt.legend() plt.ylabel("温度(℃)") plt.tight_layout() plt.savefig("temperature_fit.png", dpi=200)fill_between的where参数接收一个布尔数组,把预测高于真实值的区域涂出来。这张图的意义在于:如果红色区域集中在冬天,说明模型对冷空气过程把握不住;如果集中在夏天,则可能是对极端高温的低估。图不在于多,在于能推动你下一步分析。
4.2 误差直方图:看看模型平均偏几度
折线图看走势,直方图看分布。误差直方图能快速回答「模型到底是偶尔大错还是稳定小错」。
residual = test_df["tmax"] - test_df["预测"] # 正值 = 预测偏低 plt.figure(figsize=(8, 4)) plt.hist(residual, bins=30, edgecolor="white", color="#4C72B0") plt.axvline(0, color="red", linestyle="--", linewidth=1.5) plt.xlabel("真实值 - 预测值(℃)") plt.ylabel("天数") plt.tight_layout() plt.savefig("residual_hist.png", dpi=200)如果直方图中心明显右移,说明模型系统性地低估了温度;如果直方图两端拖出长尾,说明极端天气样本是主要失分来源。绝大多数天气预测报告里,这一页是老师最愿意看的:它在证明你在看模型的失败模式,而不只是报一个漂亮数字。
4.3 特征重要性条形图:给特征选择一个实锤
树模型自带feature_importances_,画成水平条形图,能一眼看出模型主要依赖哪些特征。
importance = pd.Series( rf.feature_importances_, index=features ).sort_values() plt.figure(figsize=(8, 4)) importance.plot.barh() plt.xlabel("重要性") plt.tight_layout() plt.savefig("feature_importance.png", dpi=200)如果dayofyear排第一,说明季节因素主导温度变化,这个结果符合常识,报告里很好解释;如果pressure排在后面,也不用紧张,说明该数据集中气压对最高温的直接解释力弱。这张图还有一个隐藏用途:如果某个你本以为很重要的特征贡献接近 0,它往往提示你数据里有字段质量或编码问题,回去检查编码方式。
4.4 年度温度热力图:一眼看全全年规律
如果数据有多个年份,可以做一张按「年份 × 月份」聚合的月均最高温热力图,把季节性和年际差异摊在一张图里。
heat_data = df.pivot_table( index=df["date"].dt.year, columns=df["date"].dt.month, values="tmax", aggfunc="mean" ) plt.figure(figsize=(10, 4)) plt.imshow(heat_data.values, aspect="auto", cmap="YlOrRd") plt.colorbar(label="月均最高温(℃)") plt.xticks(range(12), [f"{i}月" for i in range(1, 13)]) plt.yticks(range(len(heat_data.index)), heat_data.index) plt.tight_layout() plt.savefig("temperature_heatmap.png", dpi=200)pivot_table在这里把数据重排成二维矩阵,imshow直接渲染成热力图。图本身没有直接展示模型效果,但它放在报告「数据探索」部分,能证明你对数据做过结构化的观察,这也是高分报告里常见的细节。
5. 避坑清单:天气预测大作业最常见的 5 个翻车现场
5.1 时间序列被随机切分:成绩虚高不是真本事
现象:训练时 RMSE 很漂亮,报告图表也好看,但换一段新日期数据预测立刻崩。
原因:用了train_test_split(X, y, test_size=0.2)且没关shuffle,测试集日期被打乱,相当于模型在训练时已经见过测试集「邻居日期」的气温规律,成绩虚高。
解决:按日期边界手工切分——训练集 2018 到 2022 年,测试集 2023 年整年;或者用下一章的滚动验证。
5.2 天气现象文本直接塞进模型
现象:fit时报could not convert string to float,或者转成整数后线性模型系数乱到没法解释。
原因:sklearn 不接受字符串特征;转成整数后,线性模型会把「晴天=1、雨天=2」理解为数值大小关系,产生错误推断。
解决:树模型用astype("category").cat.codes,线性模型用pd.get_dummies(..., drop_first=True),编码方式与模型匹配。
5.3 用同一年数据「预测」同一年
现象:报告写测试集是 2023 年,但训练集也是从 2023 年 1 月到 12 月随机抽的,模型成绩看起来很高,逻辑上完全站不住。
原因:本质上和 5.1 同源,都是时序泄漏,只是表现形态不同。
解决:严格按时间截止做切分,例如训练 2018 到 2022,测试只用 2023 全年;每次切分前先看一眼日期范围,确认训练集时间末尾早于测试集开头。
5.4 中文标签全部变方块
现象:图里所有中文标签都是□□□,负号显示成小方块。
原因:matplotlib 默认字体不含中文字形,Windows 上最常见。
解决:绘图脚本一开头就设置plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "WenQuanYi Zen Hei"],并加plt.rcParams["axes.unicode_minus"] = False;保存成图片时用dpi=200,分辨率够放进报告里。
5.5 没固定随机种子导致答辩现场翻车
现象:报告里 RMSE 是 1.80,现场演示跑出 2.31,老师开始质疑数据来源。
原因:随机森林、GBDT 内部有随机初始化,模型每次运行结果不同;train_test_split的随机打乱也会改变结果。
解决:所有涉及随机性的模型、切分步骤都固定同一个random_state=42。这是「可复现」最直观的考核点,也是实验报告和演示不一致最常见的幕后黑手。
提示:避坑清单里的 5.1 和 5.3 是同一类问题,很多高分源码包里其实已经写好了按时间切分的逻辑,但你没有读懂就直接跑,最容易中招。拿到别人的源码,第一件事是看懂它的训练集和测试集是怎么切的,再谈模型。
6. 进阶技巧:滚动验证与「人话化」指标——最后 5 分从这里抠
6.1 滚动验证:让评估结果更接近真实业务
固定一次切分只有一个分数,运气成分没被排除。滚动验证是这类大作业里成本最低又最能体现工程感的升级:对多个年份逐一做「用之前年份训练、预测该年份」的循环。
for test_year in [2023, 2024]: train = df[df["date"].dt.year < test_year] test = df[df["date"].dt.year == test_year] model_rv = RandomForestRegressor(n_estimators=100, random_state=42) model_rv.fit(train[features], train["tmax"]) pred_year = model_rv.predict(test[features]) rmse = mean_squared_error(test["tmax"], pred_year, squared=False) print(f"{test_year}: RMSE = {rmse:.2f} ℃")这段代码的关键在于每年的训练集都只使用该年份之前的数据,逐年推进,模拟真实部署中「今天预测明天」的感觉。如果 2024 的误差比 2023 大出一截,说明模型在你没覆盖的气候事件上泛化不足,而不是模型坏了——这个结论写进结课报告,比任何调参记录都有说服力。
6.2 把指标讲成人话
回归任务的报告里写「准确率 95%」是常见错误,准确率是分类指标,回归应该写误差。把 RMSE 翻译成人话,报告会立刻上一个档次:
- RMSE = 1.8℃,意思是大约 68% 的测试日,预测偏差落在 1.8℃ 以内;
- MAE = 1.4℃,直接说「平均偏 1.4 度」;
- 再补一句「80% 的天数误差在 2℃ 以内」,老师不用换算就能感受到模型质量。
我个人的习惯是:交一份实验报告前,把代码从头到尾重跑一遍,确认图片、RMSE、特征重要性图和报告里写的一致,再把random_state固定住。曾见过同学因为没固定随机种子,答辩现场跑出的分数和报告差了一倍,老师当场质疑模型造假,那场面真的很尴尬。固定随机种子这种几十秒就能做完的事,是我在这类项目上最想提醒你的血泪经验。希望帮到你。
本文还有配套的精品资源,点击获取