简介:面向计算机科学及相关专业学生的课程设计项目资源包,聚焦气象数据预测分析与可视化展示,适合正在完成期末大作业、课程设计或寻求Python实践训练的学习者。压缩包共13个文件,包含5个Python源程序、3个Excel数据表、3个备份文件及1份项目文档,整体大小仅673KB,解压后即可直接运行。核心预测模块负责气象趋势分析,数据合并与读取模块处理北京天气和空气质量数据,可视化模块生成饼图等图形,污染趋势分析模块进一步探索污染变化,配套文档说明各文件用途及部署步骤。已有60人学习下载,能够帮助学习者快速掌握数据分析、爬虫采集、可视化全流程。通过该项目可以熟悉从天气数据抓取、Excel合并预处理到预测建模与图表展示的完整思路,源码结构清晰,便于直接调试和二次开发,加深对数据科学实践工具链的理解。
1. 基于Python的天气数据预测与可视化系统实现:课程设计别再只画三张折线图了
每年答辩季都能看到一类“标准课程设计”:标题挂着“预测”和“可视化”,打开一看是两张 Matplotlib 折线图和一段读 CSV 的代码。这个题目真正的价值在于它覆盖了一条完整的数据项目链路:从请求天气数据、洗掉脏数据、训练一个能说清参数含义的预测模型,再到把结果变成能交互的图表。整条链路跑通,你的收获会超过对着教程抄十遍爬虫。这篇文章按我实际做过的方案走,从数据获取讲到 ARIMA 预测再到 Flask + ECharts 可视化,连答辩时最容易被问倒的坑一起拆掉。无论是课程设计还是练手项目,按这个路径复现一遍,你拿到的是一套能讲清楚“为什么”的系统,而不是一堆靠运气跑通的代码。
2. 系统拆解与算法选型:预测不是拍脑袋,可视化不是堆图表
2.1 天气数据从哪来:公开 API、爬虫、离线文件三选一
天气数据获取没有银弹,常见做法是三条路:公开 API、网页爬虫、离线数据集。课程设计里我一般优先推荐公开 API,理由是稳定性和可解释性。以 OpenWeatherMap 的 5 天/3 小时预报接口为例,注册后拿一个免费 key,就能按城市 ID 拉取 JSON 格式的结构化数据,温度、湿度、风速、天气描述都齐了。这个免费额度对做预测完全够用,每分钟 60 次调用的限制,做课程设计一天也碰不到几次。
爬虫这条路适合作为“备选方案”写进系统设计文档里,但真不建议作为主数据源。中国天气网这类站点有反爬策略,你费劲解析 HTML 拿到的字段还不一定比 API 全;如果对方页面结构变了,答辩前一晚数据源挂掉,心态直接崩。离线文件(比如从气象数据平台下载的历史 CSV)作为补充可以,但题目既然叫“系统实现”,数据链路里最好有一节是“实时请求”,不然评委一句“数据从哪来”你就只能含糊过去。
表:三种数据获取方式对比
| 方式 | 稳定性 | 开发成本 | 适合场景 |
|---|---|---|---|
| 公开 API | 高 | 低,一个 requests 脚本即可 | 主数据源 |
| 网页爬虫 | 低,页面改版即失效 | 高,需处理反爬和解析 | 展示爬虫能力,不建议当主食 |
| 离线 CSV | 高 | 最低,pd.read_csv 直接读 | 补充数据集、验证模型 |
选型落到代码上,核心就三步:发请求、解析 JSON、转 DataFrame。下面这段是我常用的最小可用脚本,把 API 返回的数据整理成“每小时一条温度记录”的表格,后面直接喂给预测模型。
import requests import pandas as pd from datetime import datetime API_KEY = "你的_api_key" # 北京的城市ID是 1816670,在 OpenWeatherMap 的 city.list 里查 CITY_ID = "1816670" url = ( f"http://api.openweathermap.org/data/2.5/forecast" f"?id={CITY_ID}&appid={API_KEY}&units=metric&lang=zh_cn" ) resp = requests.get(url, timeout=10) data = resp.json() # 逐条提取预报时间与温度,生成 DataFrame records = [] for item in data["list"]: ts = item["dt"] # Unix 时间戳 dt = datetime.fromtimestamp(ts) records.append({ "time": dt, "temp": item["main"]["temp"], "humidity": item["main"]["humidity"], }) df = pd.DataFrame(records) df["time"] = pd.to_datetime(df["time"]) print(df.head())参数说明两个容易看漏的地方:units=metric是让返回的温度用摄氏度,默认是开尔文,差 273.15 度,这个坑很隐蔽;lang=zh_cn把天气描述换成中文,但注意main字段里的温度数值不受 lang 影响。timeout=10是给请求加个上限,防止网络抖动时脚本卡死。拿到df之后先print(df.head())确认列名和时间格式,再往下走。
2.2 预测算法怎么选:把线性回归、ARIMA、LSTM 放到天秤上
很多课程设计的毛病是“万物皆 LSTM”——不管数据量多少,先搭个神经网络再说,结果训练出来一条水平线,答辩时只能含糊几句。做预测选型要看两个硬指标:数据量和数据形态。
天气温度是典型的单变量时间序列,有日周期(白天热晚上冷)和季节趋势(夏天高冬天低)。如果你的历史数据只有三百条左右,LSTM 这类深度学习模型基本是翻车现场,参数量比样本量还大,不做过拟合实验根本压不住。我一般把选型按阶梯来:第一档是线性回归,拿时间特征(小时、月份)做 baseline,让学生明白“均值预测”是地板;第二档是 ARIMA,这是时间序列预测的经典方案,参数有明确含义(差分阶数、自回归阶数、移动平均阶数),答辩时能聊的东西多;第三档才轮到 LSTM,适合数据量充足、你想往深度学习方向延伸的场景。
做课程设计时 ARIMA 的性价比最高。它不需要 GPU,pmdarima 库的auto_arima能自动定阶,statsmodels可以画 ACF/PACF 图做诊断。代价是它对数据的平稳性有要求,需要先差分,这部分是理论分,写进报告里能显深度。线性回归作为对照模型来用,如果 ARIMA 连线性回归都跑不过,说明你的数据本身没有太多可预测的信息,这个结论也值一个章节去分析。
2.3 可视化框架选型:课程设计的性价比排名
可视化是答辩时的“门面”,框架选错了会拖慢进度。按性价比排:纯 Matplotlib 适合快速看数据分布和模型诊断;Pyecharts 上手快、图表交互效果好(缩放、提示框都有),适合做前端展示;Flask + ECharts 工作量大一些,但把预测系统做成了“可操作的网站”,演示效果直接提升一个档次。
我的建议是“Matplotlib 做诊断图、Flask + ECharts 做交付页面”。Matplotlib 负责在调试阶段快速画温度曲线、ACF 图、残差图,让你自己看清楚数据长什么样;最终展示系统用 Flask 起一个本地网页,ECharts 在前端渲染图表,城市切换、预测区间这些交互都能做,评委体验好。Pyecharts 也不是不能用,但它本质是“生成 HTML 代码嵌入页面”,如果你想自定义布局,不如直接从 ECharts 的 option 配置写起。
3. 数据清洗与探索:把天气数据收拾到能喂给模型的干净状态
3.1 爬虫获取的数据先做三件事:去重、补缺、剔异常
API 返回的数据相对规整,但课程设计为了凑“数据处理”章节,很多人会故意或者无意引入脏数据。常见的脏数据问题有:同一时刻重复记录(重试请求导致)、某个时间点温度缺失(接口限流或网络抖动)、异常值(传感器故障记录的 999.9 或 -999)。清洗三板斧按顺序来:先drop_duplicates去重,再处理缺失值,最后剔异常值。
import numpy as np # 1. 去重:按时间列去掉完全重复的记录 df = df.drop_duplicates(subset=["time"], keep="last") # 2. 缺失值处理:先把无意义的填充值替换为 NaN,再按时间插值 # OpenWeatherMap 有些字段可能返回 null df["temp"] = df["temp"].replace([999.9, -999.9], np.nan) df["temp"] = df["temp"].interpolate(method="time") # 3. 异常值检测:3sigma 原则,超过均值 3 倍标准差视为异常 mean = df["temp"].mean() std = df["temp"].std() lower, upper = mean - 3 * std, mean + 3 * std df = df[(df["temp"] >= lower) & (df["temp"] <= upper)]清洗的逻辑讲究“可追溯”。interpolate(method="time")是按时间间隔线性插值,比method="linear"更适合时间序列,因为它会考虑相邻点的时间差,而不是把两个点机械地取平均。3sigma 原则的阈值不是拍脑袋定的,天气温度分布相对集中,取 3 倍标准差已经能滤掉传感器误报,太低的阈值(1.5 倍)会把正常降温误杀。清洗完检查一下df.shape,如果删掉的记录超过 5%,就要回看数据源是不是出问题了。
3.2 特征工程:从时间戳里榨出可用的预测信号
原始的时间戳是一个 Unix 整数,直接扔给模型没有意义。做时间序列预测可以从时间戳里拆出两类特征:周期性特征和时间上下文。周期性特征是核心,比如小时(0-23)决定了日内温度曲线,月份(1-12)反映了季节趋势。
# 拆出周期特征 df["hour"] = df["time"].dt.hour df["month"] = df["time"].dt.month # 把日期转成数值特征,供回归模型使用 df["dayofyear"] = df["time"].dt.dayofyear # 温度做一阶差分,看序列是否平稳 df["temp_diff1"] = df["temp"].diff().dropna()这里的dayofyear是给线性回归用的连续特征,表示一年中的第几天,模型可以从它学到“越靠近夏天越热”的趋势。hour和month是周期变量,直接用数值喂给线性回归会有问题——23 点和 0 点相差 1 小时,但数值差了 23,模型会产生错误的“跳变”语义。所以如果要认真做线性回归,应该把小时转成 sin/cos 特征。ARIMA 不需要这些手工特征,它有内在的差分机制去捕捉趋势和季节,所以这些特征主要是给 baseline 模型和可视化分析用的。
做完特征工程,画一张按小时聚合的箱线图,基本能看到温度数据的分布形态:白天箱体窄(温度稳定),夜间箱体宽(辐射降温差异大)。这个观察可以写进报告的数据探索章节,证明你“看过数据”而不只是“跑过代码”。
4. 预测模型落地:用 ARIMA 做温度预测的调参实战
4.1 先画图再定阶:ACF 和 PACF 怎么读
ARIMA 定阶是个老生常谈又绕不开的话题。auto_arima能帮你省钱,但答辩老师一句“p、q 怎么选的”就能问倒一批人。所以光会调库不够,得能讲清楚为什么选这个参数。
先对差分后的序列画 ACF 和 PACF 图。两个图怎么读:PACF 图在 lag 1 处出现一个明显的尖峰,后面截尾到零,说明自回归项 p 取 1;ACF 图拖尾(缓慢衰减),则移动平均项 q 可能为 0。反过来也一样。如果 ACF 很快截尾、PACF 拖尾,那是 MA 模型的标志。
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import matplotlib.pyplot as plt # 对一阶差分序列画图 diff_series = df["temp"].diff().dropna() fig, axes = plt.subplots(1, 2, figsize=(12, 4)) plot_acf(diff_series, ax=axes[0], lags=30) plot_pacf(diff_series, ax=axes[1], lags=30) plt.show()参数lags=30表示看 30 个滞后期的相关性,这个数字不是越大越好。温度数据有 24 小时周期,lags 至少要覆盖 24 个点才能看到日周期的影响。如果 ACF 图在 lag 24 处有明显尖峰,说明数据存在 24 步的季节性,这时候可以考虑要不要加seasonal=True来建模。不过课程设计的数据通常只有几天到几周,日周期信号可能不够强,强行季节建模反而过拟合。
4.2 用 auto_arima 定阶,但要留个心眼
pmdarima的auto_arima是时间序列选手的后悔药,它会遍历候选参数组合,按 AIC 选最优。但完全交给它也有风险:样本太少时容易选出过拟合的高阶模型。我一般会限住搜索范围,宁可选一个稍微“笨”一点的模型。
from pmdarima import auto_arima # 训练集:用前面 80% 的时间段 train_size = int(len(df) * 0.8) train, test = df["temp"].iloc[:train_size], df["temp"].iloc[train_size:] # 限制参数搜索范围,避免高阶过拟合 model = auto_arima( train, start_p=1, max_p=5, start_q=1, max_q=5, d=None, # 自动测差分 seasonal=False, # 数据量小,先不考虑季节项 trace=True, # 打印搜索过程 error_action="ignore", suppress_warnings=True, stepwise=True ) print(model.summary())d=None是让库自动测试差分阶数,通常温度序列一阶差分就变平稳,取 1 就够了。seasonal=False在数据量不足时是安全保障,不然模型会把噪声当成季节信号,预测值在测试集上飘得没边。stepwise=True是启发式搜索,速度快很多;如果数据量小,可以开stepwise=False做全搜索,但耗时指数级上升。
模型训练完,立刻看model.summary()输出里的 P 值。Ljung-Box 检验的 p 值要大于 0.05,说明残差是白噪声。P 值小于 0.05 意味着模型没把信息榨干净,还有模式没抓到,这时需要调高 p 或 q。
4.3 训练、预测、评估:别让 RMSE 成为自欺欺人的数字
时间序列切训练集和测试集,一定不能随机切,而是要按时间顺序切。很多课程设计死在这一步:用train_test_split(test_size=0.2)随机切分,训练集里混着测试集的数据,模型“偷看”了未来,预测准确率高得离谱,答辩一问就露馅。
from sklearn.metrics import mean_squared_error import numpy as np # 时间序列必须按顺序切分,不能随机切 train, test = df["temp"].iloc[:train_size], df["temp"].iloc[train_size:] # 预测测试集长度对应的步数 predictions = model.predict(n_periods=len(test)) rmse = np.sqrt(mean_squared_error(test, predictions)) print(f"RMSE: {rmse:.2f}") # 把预测结果和真实值画在一起 plt.figure(figsize=(10, 4)) plt.plot(test.index, test.values, label="真实值") plt.plot(test.index, predictions, label="预测值") plt.legend() plt.show()这段代码有两个值得注意的点。第一,model是在训练集上拟合的,n_periods=len(test)表示预测未来 N 个点,这里的 N 是测试集长度,不是随便定的。第二,RMSE的单位和温度一致,所以 “RMSE=1.5” 意味着平均误差 1.5 摄氏度。如果 RMSE 超过 5 摄氏度,先别急着调参,回去看数据清洗那一步是不是漏了什么——比如测试集时段有天气突变(冷空气过境),这类极端事件用 ARIMA 几乎是预测不了的,这时把“模型对极端天气的迟钝”写进报告的分析里,反而是加分项。
5. 避坑:课程设计里躲不开的五个坑
5.1 城市 ID 查错导致请求 404
现象:requests.get返回 404,程序直接抛异常退出。原因:OpenWeatherMap 的城市标识是专门的 ID 编码,不是城市名也不是邮编,很多人拿北京拼音或者邮政编码去试。解决:去官方city.list.json.gz文件里搜,或者用http://api.openweathermap.org/data/2.5/weather?q=Beijing&appid=...按城市名查,让接口帮你解析。注意q参数不支持中文城市名,要用拼音。
5.2 时区偏移让曲线整体平移
现象:画出来的温度曲线整体比实际热,峰值出现在凌晨。原因:OpenWeatherMap 返回的dt是 UTC 时间戳,直接fromtimestamp拿到的是你的本地时间,但如果你在代码里先转字符串再转 datetime,中间时区被吞掉就会偏移 8 小时。解决:用datetime.fromtimestamp(ts, tz=timezone(timedelta(hours=8)))带时区转换,或统一用 UTC 存储,画图时再映射到北京时间。做预测时时间对齐是硬前提,错 8 个小时,模型学到的“日周期”全废。
5.3 auto_arima 选择了过大的 p、q 导致预测值漂移
现象:预测曲线在测试集上先是一段直线,然后突然发散成巨大的波动。原因:小样本下 auto_arima 全搜索可能选出高阶模型,参数多、样本少,估计方差极大。解决:限制max_p=5, max_q=5,并优先用stepwise=True;另外可以人工读 ACF/PACF 图去矫正 auto_arima 的结果,如果 ACF 显示 q=2 够用,就算 AIC 再小也不要直接取 p=5 的模型。
5.4 中文图表方块字体
现象:Matplotlib 图上坐标轴的中文全是小方块。原因:Matplotlib 默认字体不支持中文,这是环境问题不是代码问题。解决:在绘图文件顶部加三行设置:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 黑体 plt.rcParams["axes.unicode_minus"] = False # 让负号正常显示如果服务器上没装 SimHei,可以用plt.rcParams["font.sans-serif"] = ["WenQuanYi Zen Hei"]或者上传一个simhei.ttf文件手动注册。这个坑在答辩演示时特别致命,图表全是方块,代码写得再好也白搭。
5.5 数据泄漏:随机切分训练集和测试集
现象:训练 RMSE 和测试 RMSE 都小得不可思议。原因:train_test_split默认随机打乱数据,时间序列被打乱后,模型在“预测”时偷看了附近的真实值。解决:时间序列切分必须用ts.split或者手动按位置切。如果你用了类似“前 80% 训练、后 20% 测试”的切法还可以更严谨一点:用滚动窗口交叉验证,避免只在某一个固定时间段上验证模型。
6. 进阶验证:用滚动预测还原真实场景,别让答辩问倒你
固定切分训练集和测试集有一个问题:它只验证了模型在一个时间段上的表现,可天气系统是动态的,今天训练明天预测,每天都在变。滚动预测(也叫 walk-forward validation)更贴近真实使用:从某一天开始,每次用截至当前的全部历史数据预测下一天,然后真实值到达后把它并进历史数据,再预测再往后一天。这种验证方式能评估模型在长期运行中的真实表现,也是答辩时展示系统“能用”的核心证据。
from copy import deepcopy history = list(df["temp"].iloc[:train_size]) rolling_preds, rolling_trues = [], [] for i in range(train_size, len(df)): # 每次都用当前全部历史数据重训模型 model = auto_arima( history, start_p=1, max_p=3, start_q=1, max_q=3, d=1, seasonal=False, stepwise=True, suppress_warnings=True, error_action="ignore" ) pred = model.predict(n_periods=1)[0] true_val = df["temp"].iloc[i] rolling_preds.append(pred) rolling_trues.append(true_val) history.append(true_val) rolling_rmse = np.sqrt(mean_squared_error(rolling_trues, rolling_preds)) print(f"滚动预测 RMSE: {rolling_rmse:.2f}")d=1是这里唯一的硬编码参数,因为固定切分时已经确认一阶差分足够。每次循环重训一个模型,开销很大但数据量小没关系。如果滚动 RMSE 比固定切分的 RMSE 明显大,不是代码错了,而是说明模型在“没见过未来”的情况下预测能力下降,这反而是个值得写进报告的真实结论:预测系统应该定期用新数据重训,而不是一次训练终身使用。答辩时老师如果问“系统怎么投入日常使用”,把这段逻辑讲清楚,比背十页算法原理都管用。
我个人被问倒过的经历是“你的模型最多能预测几天”。我用两天的数据训练,然后预测了 7 天,RMSE 大得离谱。后来才意识到,天气预测的极限和数据的可预测性有关,ARIMA 预测步数越长,误差累积越大,这是模型特性不是 bug。把“有效预测时长”和“误差累积规律”写进报告,比硬撑着说“系统能预测未来一个月”老实得多,也安全得多。这个项目做到这里才算真正闭环:数据能拿、能洗、能预测、能展示,每个结论都有依据而不是靠感觉说话。希望帮到你。
本文还有配套的精品资源,点击获取