简介:面向计算机专业正在准备毕业设计、课程设计或期末大作业的学生,这份高分项目资料基于Python完整实现了天气数据爬取、清洗、预测与可视化链路,评审分为98分,可直接作为项目参考或二次开发基础。包内共24个文件,涵盖Python源码(py)、天气数据集(csv)、训练模型(pkl)、网页可视化(html)、说明文档(md)以及运行效果截图(jpg)等,压缩包仅1.42MB,轻量易用。已有466人学习下载,项目含GetData、ProcessData、GetModel、main等核心脚本,配套date_train、date_test、china_today等多份数据文件,可帮助读者快速理解从数据采集到模型预测再到可视化展示的完整流程。对于需要动手实践Python数据分析与机器学习的学生,这份资料结构清晰、可运行性强,适合用于课程设计与毕业设计参考。
1. 天气预测与可视化项目的四条主线
这种标题的 Python 项目,多数是课程设计或毕业设计里"要有数据、要有模型、要有图"的综合性任务。拆开看,核心是四条线:历史天气数据怎么拿、怎么清洗成能喂给模型的时间序列,预测用哪种模型才既不翻车也不过度,以及最后怎么用图表把预测结果讲清楚。数据资料部分通常决定项目的下限——数据脏,后面模型和图表再漂亮也站不住;而源码组织和文档说明则决定答辩时能讲多深,这两个环节恰恰是"高分项目"和"能跑项目"的分水岭。
适合的读者分两类:一类是 Python 语法已经会了但没碰过时间序列的开发者,想用一个小项目把 pandas、statsmodels、深度学习框架串起来;另一类是打算在简历上放一条"数据清洗 + 预测建模 + 可视化展示"完整链路的从业者。下面按数据准备、模型实现、可视化、交付收尾的顺序展开,中间穿插可以直接抄的参数和踩坑点。
2. 天气数据清洗与时间序列预处理:预测结果的前置保障
2.1 数据字段设计与常见来源
天气预测项目的第一个技术决策是数据粒度。公开渠道能拿到的历史天气数据通常分三档:逐小时、逐日、逐月。预测未来 3 到 7 天气温走势时,逐日数据足够,而且能避开小时级数据里白天黑夜的周期性波动;逐月数据则太粗,基本只能看趋势。常见做法是直接找现成的历史气象 CSV,字段至少包含日期、平均气温、最高气温、最低气温、湿度、气压、风速。
| 字段 | 类型 | 说明 | 预测中的用途 |
|---|---|---|---|
| date | datetime | 观测日期 | 时间索引与趋势分解 |
| temp | float | 日平均气温(℃) | 预测目标值 |
| temp_max / temp_min | float | 当日最高 / 最低气温 | 辅助特征或第二目标 |
| humidity | float | 相对湿度(%) | 特征 |
| pressure | float | 海平面气压(hPa) | 特征 |
| wind_speed | float | 平均风速(m/s) | 特征 |
拿到数据后第一件事不是建模,而是确认时间索引是否连续。很多数据源存在节假日缺测、设备维护导致的连续空窗,直接建模会让模型的滞后项错位——上一行的值被当成上一天的值,误差会顺着时间轴一路传下去。
2.1.1 时间序列读取与连续性检查
用 pandas 读取时把日期列显式转成 DatetimeIndex,这是整个项目后面所有操作的前提。以下代码是数据准备阶段的第一段脚本:
import pandas as pd # 读取原始数据,date 列直接解析为时间类型 df = pd.read_csv('data/raw/weather_history.csv', parse_dates=['date']) # 按日期排序并设为索引 df.sort_values('date', inplace=True) df.set_index('date', inplace=True) # 检查时间索引是否连续(日频数据) full_range = pd.date_range(start=df.index.min(), end=df.index.max(), freq='D') missing_dates = full_range.difference(df.index) print(f'缺失日期数量: {len(missing_dates)}') print(missing_dates[:10])parse_dates参数让 pandas 在读取时就把日期列转成 datetime 类型,省去之后手动to_datetime的步骤。freq='D'表示按天生成完整日期序列,用difference找出原始索引里缺哪些天。这一段看起来不起眼,但缺日期不补,后面shift(1)构造滞后特征时会把上一行当作上一天,误差会直接传导进模型。
2.2 缺失值、异常值与重复值的处理策略
天气数据最常见的三种脏数据是:传感器瞬时故障产生的离群点、整段缺测的时间窗、以及同一天重复出现的重复记录。处理顺序建议是先查重、再补缺、最后做异常值替换。顺序反过来的风险是,重复记录会导致缺失值统计和异常值判断都失真。
import numpy as np # 1. 去除同日期重复的记录,保留第一条 df = df[~df.index.duplicated(keep='first')] # 2. 数值列缺失值先用时间线性插值 numeric_cols = ['temp', 'humidity', 'pressure', 'wind_speed'] df[numeric_cols] = df[numeric_cols].interpolate(method='time') # 3. 超过均值±3σ 的值视为异常,置空后向前填充 for col in numeric_cols: mean = df[col].mean() std = df[col].std() df.loc[np.abs(df[col] - mean) > 3 * std, col] = np.nan df[col] = df[col].fillna(method='ffill')interpolate(method='time')会按时间间隔比例估算缺失值,比单纯用前后均值更贴近真实变化趋势。3σ 规则对气温这类近似正态分布的字段有效,但风速和湿度偏态明显,可以改用百分位数截断,比如把超过 99.5% 分位数的值视为异常。注意最后用ffill是因为前两步之后残留的缺失值已经很少,向前填充是成本最低的兜底方案;如果某个字段缺失超过 20%,应该回源头找数据而不是硬填。
2.3 重采样与特征工程:让模型拿到有规律的时间序列
原始数据如果是小时级,需要先聚合到日级再进模型。聚合法则要跟字段含义匹配:气温用日均值、最高温和用max、风速用均值。这里有个容易犯的错:把小时数据 resample 成天的时候,直接用默认的 mean 会把最高最低温也算成平均,特征含义就变了。
# 小时级数据聚合到日级 df_daily = df.resample('D').agg({ 'temp': 'mean', 'temp_max': 'max', 'temp_min': 'min', 'humidity': 'mean', 'pressure': 'mean', 'wind_speed': 'mean', }) df_daily = df_daily.dropna(subset=['temp'])聚合完成后,给模型加两类特征:滞后特征和滑动统计特征。滞后特征告诉模型"昨天的气温是多少",滑动均值特征告诉模型"近一周的趋势方向"。LSTM 这类模型能自己从窗口里学到时间依赖,但 ARIMA 和普通回归模型非常依赖这些人工特征,少了它们预测基本靠猜。
# 特征工程:滞后 1 天 / 7 天 + 7 日滑动均值 df_daily['temp_lag1'] = df_daily['temp'].shift(1) df_daily['temp_lag7'] = df_daily['temp'].shift(7) df_daily['temp_ma7'] = df_daily['temp'].rolling(7).mean() # 只保留有完整历史特征的样本 df_feat = df_daily.dropna() print(df_feat.shape)shift(1)会把整列下移一行,rolling(7).mean()则是每 7 天一个窗口的均值,窗口含当前行。这两个操作都会让前几行产生 NaN,最后统一dropna删掉。特征粒度决定了模型上限:如果预测目标是未来 3 天均值,而特征只给了当天数据,模型等于在猜盲盒,这点在文档说明的数据分析部分值得专门写一段。
3. 天气预测模型代码实现:ARIMA 基线到 LSTM 扩展
3.1 先立基线:为什么天气预测从 ARIMA 开始
气象机构做预报用的是数值模式,但课程设计和工程演示根本不需要复现那一套。单变量气温预测里,ARIMA 是性价比最高的起点:数据要求低、训练快、参数含义能讲清楚。用它的前提是序列近似平稳,所以建模前必须做单位根检验,这一步在 Python 里一行调用就能完成。
from statsmodels.tsa.stattools import adfuller # ADF 检验,p 值小于 0.05 认为序列平稳 adf_result = adfuller(df_daily['temp'].dropna()) print('ADF p-value:', adf_result[1])p 值通常大于 0.05,说明原始气温序列不平稳——这很正常,气温有年度季节性。处理办法是对序列做一阶差分,差分后的序列再跑一次 ADF。实际操作里,我不建议手工反复试差分阶数,直接让 ARIMA 把差分阶数设为 1 即可,模型内部会完成差分,比手动处理更省事也更不容易出错。
3.1.1 ARIMA 参数确定:p、d、q 的取值策略
ARIMA 的三个参数含义:p是自回归项数,q是移动平均项数,d是差分阶数。确定p和q的标准做法是看 ACF 和 PACF 图的截尾情况,但这对新手不友好。工程上更快的路径是写一个循环,遍历若干组(p, q)组合,用 AIC 选最小的。AIC 同时惩罚拟合残差和参数数量,能够避免模型靠无脑加参数换取低误差。
import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings('ignore') best_aic = float('inf') best_order = None for p in range(0, 4): for q in range(0, 4): try: model = ARIMA(df_daily['temp'], order=(p, 1, q)) result = model.fit() if result.aic < best_aic: best_aic = result.aic best_order = (p, 1, q) except Exception: continue print('最优参数:', best_order, 'AIC:', best_aic) # 用最优参数重新拟合 final_model = ARIMA(df_daily['temp'], order=best_order) fitted = final_model.fit() print(fitted.summary())遍历 16 组参数在这个数据量下也就是几秒钟的事,远比人工看图靠谱。order=(p, 1, q)里的 1 就是差分阶数,处理非平稳性。fitted.summary()会输出每个系数的 p 值,如果某个系数显著性很差,可以把对应参数降一档重新拟合。这个细节写进文档说明,比贴十行理论都加分,因为评审能看出你确实调过参而不是跑通就完事。
3.2 预测未来 7 天并输出置信区间
ARIMA 拟合完成后,用get_forecast生成未来 7 天的预测值。这一步不建议用forecast()方法,因为它只给点预测不给区间。置信区间的上下界画在图上,能让用户直观看到预测的不确定性——离今天越远,区间越宽,这是天气预测可视化里最有信息量的一张图。
import pandas as pd # 预测未来 7 天 forecast_result = fitted.get_forecast(steps=7) pred_mean = forecast_result.predicted_mean conf_int = forecast_result.conf_int(alpha=0.2) # 生成未来日期索引 future_dates = pd.date_range( start=df_daily.index[-1] + pd.Timedelta(days=1), periods=7, freq='D' ) pred_df = pd.DataFrame({ 'pred': pred_mean.values, 'lower': conf_int.iloc[:, 0].values, 'upper': conf_int.iloc[:, 1].values }, index=future_dates) print(pred_df.round(2))alpha=0.2表示 80% 置信区间,比默认的 95% 更贴合天气预测的展示习惯——气象预报本身允许一定的误差带,太宽的区间画出来反而没有参考价值。conf_int返回的是两列 DataFrame,分别取第 0 列和第 1 列作为上下界。pd.Timedelta(days=1)从最后一天往后推一天,保证预测日期从明天开始,不会跟训练集最后一天重叠。
3.3 LSTM 扩展:窗口化、归一化与训练
如果项目要求模型看起来"更进一层",在 ARIMA 之上加一个 LSTM 分支就够了。LSTM 做单变量气温预测的套路很固定:MinMaxScaler 归一化、滑窗构造样本、按时间顺序切分训练集和测试集。有一条原则必须守住:归一化只能对训练集 fit,测试集用同一组参数 transform,否则会发生信息泄漏,测试误差会虚低,评审追问时很难解释清楚。
import numpy as np from sklearn.preprocessing import MinMaxScaler # 取温度序列,归一化到 [0,1] series = df_daily['temp'].values.reshape(-1, 1) scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(series) # 滑窗:用过去 7 天预测未来 1 天 def make_windows(data, window=7): X, y = [], [] for i in range(len(data) - window): X.append(data[i:i+window, 0]) y.append(data[i+window, 0]) return np.array(X), np.array(y) X, y = make_windows(scaled, window=7) # 按 8:2 切分,切分点必须落在时间轴后半段 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:]窗口大小window=7对应"用一周数据预测明天"。make_windows里data[i:i+window]是连续切片,第i+window个点作为标签,每个样本在时间上滑动一格,数据量约为原始序列长度减 7。切分用的是位置索引而不是随机函数,这是时间序列与普通机器学习分类任务最本质的区别——一旦打乱,模型就学会了"偷看未来"。
3.3.1 模型结构与训练参数
LSTM 结构不用太深,两层即可。第一层设return_sequences=True是为了把完整序列传给第二层 LSTM,最后一层用线性输出单元预测数值,因为温度预测是回归问题,输出层不加激活函数。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout # 输入形状: (样本数, 时间步长, 特征数) model = Sequential([ LSTM(64, return_sequences=True, input_shape=(7, 1)), Dropout(0.2), LSTM(32), Dropout(0.2), Dense(1) ]) model.compile(optimizer='adam', loss='mse') history = model.fit( X_train.reshape(-1, 7, 1), y_train, epochs=50, batch_size=32, validation_split=0.1, verbose=0 )epochs=50对这个数据量够用,跑太多会过拟合。Dropout(0.2)在两层之间随机丢弃 20% 的神经元,防止模型死记训练数据。validation_split=0.1从训练集尾部切 10% 做验证,用来判断什么时候该停——如果验证集 loss 在最后几个 epoch 不降反升,说明拟合过头,可以把 epochs 降到 30 重跑。预测时记得把结果反归一化回真实温度单位:
# 用训练好的模型预测,并还原到原始量纲 pred_scaled = model.predict(X_test.reshape(-1, 7, 1)) pred_temp = scaler.inverse_transform(pred_scaled).ravel() y_test_temp = scaler.inverse_transform(y_test.reshape(-1, 1)).ravel()3.4 模型评估指标:MAE 与 RMSE 的取舍
评估预测质量只用 R² 不够,天气预测更看重绝对误差。MAE 反映平均偏差,RMSE 对大的偏差敏感——一次差 5℃ 的预测会让 RMSE 涨一大截,而 MAE 只会平滑地增加。两个指标一起看,差异越大说明误差分布越不匀,存在少数极端偏差样本。
from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(y_test_temp, pred_temp) rmse = mean_squared_error(y_test_temp, pred_temp, squared=False) print(f'LSTM 测试集 MAE: {mae:.2f} ℃') print(f'LSTM 测试集 RMSE: {rmse:.2f} ℃')| 指标 | 数值含义 | 何时优先 |
|---|---|---|
| MAE | 平均每个预测点偏差多少 ℃ | 关注日常平均表现 |
| RMSE | 对大幅偏差惩罚更重 | 关注极端误差事件 |
| R² | 模型解释的方差比例 | 横向对比不同模型 |
个人习惯是 MAE 为主、RMSE 为辅。一天里气温波动本身就有好几摄氏度,MAE 能压到 1.5℃ 以内,这个模型在课程设计层面就合格了。ARIMA 和 LSTM 要用同一段测试集去对比,先跑出两套误差再决定文档里以哪个模型为主,这样得出的结论站得住。
4. 预测结果可视化:Matplotlib 静态图与 PyECharts 大屏
4.1 静态图表:历史趋势与预测区间的组合图
可视化的第一张图应该是"历史 + 预测 + 置信区间"三合一的组合图。它能回答一个核心问题:预测值放在整个历史走势里是否合理。用 Matplotlib 画这张图,关键点是历史数据用实线、预测用虚线、置信区间用半透明填充,三者颜色拉开层次。另一个容易被忽略的细节是只画最近一段历史,全量画出来曲线会被压缩成一条实心带。
import matplotlib.pyplot as plt # 中文字体设置,避免标签乱码 plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False fig, ax = plt.subplots(figsize=(14, 6)) # 只画最近 90 天历史,避免曲线被拉平 ax.plot(df_daily.index[-90:], df_daily['temp'][-90:], color='#2E86C1', linewidth=1.5, label='历史气温') ax.plot(pred_df.index, pred_df['pred'], color='#E74C3C', linewidth=2, linestyle='--', label='预测气温') ax.fill_between(pred_df.index, pred_df['lower'], pred_df['upper'], color='#E74C3C', alpha=0.15, label='80% 置信区间') ax.set_title('未来 7 天气温预测') ax.set_xlabel('日期') ax.set_ylabel('气温 (℃)') ax.legend() ax.grid(alpha=0.3) plt.xticks(rotation=45) plt.tight_layout() plt.savefig('output/temperature_forecast.png', dpi=150)fill_between的三个参数分别是 x 轴、下界、上界,alpha=0.15让填充区域不遮挡后面的网格线和数据线。dpi=150是折中值,插入文档够清晰,文件体积也可控。如果系统里没有中文字体,SimHei会失效,备选的Arial Unicode MS在 macOS 上常见,两个一起写能覆盖多数环境。
4.2 交互式可视化:从 PyECharts 到可适配的可视化大屏
如果项目展示环节需要"可视化大屏"效果,PyECharts 是比 Matplotlib 更省力的方案。它生成 HTML 文件,浏览器直接打开,自带缩放和悬浮提示,不需要前端基础。PyECharts 的坐标系和数据接口与 Matplotlib 完全不同,核心是add_xaxis和add_yaxis成对调用,数据以普通 Python 列表传入。
from pyecharts.charts import Line from pyecharts import options as opts # 准备展示数据 dates = list(pred_df.index.strftime('%m-%d')) pred_values = [round(v, 1) for v in pred_df['pred']] lower_values = [round(v, 1) for v in pred_df['lower']] upper_values = [round(v, 1) for v in pred_df['upper']] line = ( Line(init_opts=opts.InitOpts(width='900px', height='500px')) .add_xaxis(dates) .add_yaxis('预测气温', pred_values, is_smooth=True, linestyle_opts=opts.LineStyleOpts(width=3)) .add_yaxis('下界', lower_values, linestyle_opts=opts.LineStyleOpts(type_='dashed', width=1), label_opts=opts.LabelOpts(is_show=False)) .add_yaxis('上界', upper_values, linestyle_opts=opts.LineStyleOpts(type_='dashed', width=1), label_opts=opts.LabelOpts(is_show=False)) .set_global_opts( title_opts=opts.TitleOpts(title='未来 7 天气温预测走势'), tooltip_opts=opts.TooltipOpts(trigger='axis'), datazoom_opts=[opts.DataZoomOpts()], legend_opts=opts.LegendOpts(pos_top='5%'), ) ) line.render('output/forecast_interactive.html')datazoom_opts提供底部滚动条,数据长了可以拖动查看,这是大屏场景里的刚需。上下界曲线用label_opts把数值标签隐藏,避免图上数字密密麻麻影响可读性。多个图卡拼成大屏时,每个 PyECharts 图都是独立 HTML,常见做法是写一个简单的网格式页面,用 iframe 把各图表嵌进去,再统一设置深色背景和固定像素尺寸。大屏适配的关键是图表宽度用固定像素而不是百分比,否则不同分辨率下会产生横向滚动条。
4.3 预测误差分布图:验证模型而不是只展示结果
第四章里容易被忽视的是误差分布图。把测试集每个预测点的误差画成直方图,能直观看出模型是系统性偏高还是偏低。如果直方图整体往正方向偏移,说明模型系统性高估气温,需要回头检查序列是否做了中心化处理,或者训练集里是否混入了异常偏暖的样本。
import numpy as np errors = y_test_temp - pred_temp fig, ax = plt.subplots(figsize=(8, 4)) ax.hist(errors, bins=20, color='#5499C7', edgecolor='white', alpha=0.85) ax.axvline(0, color='#E74C3C', linestyle='--', linewidth=1.2) ax.set_title('预测误差分布') ax.set_xlabel('误差 (℃)') # 输出均值误差,接近 0 说明无系统偏差 print(f'偏差均值: {np.mean(errors):.3f} ℃')axvline在误差为 0 的位置画一条参考线,直方图主体落在参考线两侧、均值接近 0,模型就没有明显系统偏差。偏差均值这个数字可以顺手写进文档说明的数据分析部分,比只放一张预测图更有说服力,评审看到的是你做了结果校验而不是只交付了图片。
5. 源码目录规范、README 文档与预测误差验证技巧
5.1 让源码像"高分项目"的目录结构
项目评审时,先看目录再看代码。一套干净的目录结构比花哨的算法更能说明工程素养。常见做法是把数据、源码、输出、文档四类东西物理隔离,运行脚本时统一用相对路径,避免出现把个人电脑路径写死的代码,换台机器就崩。
weather-forecast/ ├── data/ │ ├── raw/ # 原始数据,只读不写 │ └── processed/ # 清洗后的数据 ├── src/ │ ├── __init__.py │ ├── data_clean.py # 数据清洗与特征工程 │ ├── train_arima.py # ARIMA 训练与预测 │ ├── train_lstm.py # LSTM 训练与预测 │ └── visualize.py # 图表生成 ├── output/ # 图片与 HTML 输出 ├── requirements.txt └── README.mdrequirements.txt列出项目依赖,评阅环境里pip install -r requirements.txt一步装齐。__init__.py让src变成可导入的 Python 包,train_arima.py里就能写from src.data_clean import load_processed_data,模块化程度立刻上一个档次,也方便评阅人按文件顺序读代码。
5.2 README 文档的三个必写段落
文档说明不需要长篇大论,三个段落缺一不可:环境要求与安装步骤、运行顺序、结果文件说明。运行顺序尤其重要,评审老师会照着 README 实际跑一遍,如果visualize.py在两个训练脚本之前运行,会因为缺少模型文件直接报错,印象分立刻打折。
README 里给出一段可复制的启动命令:
# 安装依赖 pip install -r requirements.txt # 按顺序执行 python src/data_clean.py python src/train_arima.py python src/train_lstm.py python src/visualize.py环境准备阶段如果机器上没有 Python,需要先按 python 安装教程配好 3.8 以上版本,并在虚拟环境里执行 pip 安装命令,避免依赖冲突污染系统环境。README 把这一步写进环境要求段落,评阅人就不会在第一步卡住,运行记录也会顺畅很多。
5.3 验证误差时最容易踩的三个坑
误差验证阶段有三个高频问题。一是用训练集误差替代测试集误差,数据量小的时候训练误差好看到失真,必须留出尾部一段真实数据做测试。二是在多步预测里把真实值喂进模型——比如预测第 3 天时把第 2 天的真实值作为特征,这是信息泄漏,会让误差虚低到几乎为零,答辩时一问一个准。三是归一化时对全量数据fit_transform,正确做法是只对训练集 fit,测试集用同一组 min 和 scale 参数 transform。
一个实用的补救验证技巧是滚动回测:每次只用窗口内的数据训练,预测下一天,然后窗口向后滑动一格。它比一次性切分测试集更贴近真实预报场景,代码量也就一个循环:
from statsmodels.tsa.arima.model import ARIMA def rolling_backtest(series, train_len=300, horizon=7): errors = [] for start in range(train_len, len(series) - horizon, horizon): train = series.iloc[start - train_len:start] test = series.iloc[start:start + horizon] model = ARIMA(train, order=best_order).fit() pred = model.forecast(horizon) errors.extend((test.values - pred.values).tolist()) return np.mean(np.abs(errors)) rolling_mae = rolling_backtest(df_daily['temp']) print(f'滚动回测 MAE: {rolling_mae:.2f} ℃')rolling_backtest里外层循环每次前进horizon天,训练集始终是紧挨着测试窗口的前train_len条数据。这样每个测试窗口的模型都是用"当时能拿到的数据"训练出来的,模拟了真实部署的预测条件。滚动 MAE 和一次性测试 MAE 的差距,能直接暴露模型对数据段选择的敏感度——差距大说明模型不稳定,需要回去调order参数或补特征。把这个验证结果写进 README 的数据分析段落,项目的可信度会明显高于只贴一张预测图的版本。
本文还有配套的精品资源,点击获取