天气预测机器学习实战:从数据清洗到可视化完整教程
2026/9/12 20:41:47 网站建设 项目流程

简介:基于Python的机器学习天气预测与数据可视化项目,面向毕业设计、期末大作业与课程设计场景,适合具备一定Python基础、希望快速搭建完整预测与可视化流程的读者。压缩包共24个文件,包含4个Python源码、4个CSV数据文件、12张图表图像、1个HTML前端页面,以及说明文档与模型文件,整体仅1.42MB,结构清晰,便于部署复现。源码配有注释,从数据清洗、模型训练到结果可视化均有完整实现,涵盖GetData、ProcessData、GetModel、main等关键模块,可帮助新手理解天气预测任务的数据流转与建模思路。项目另附天气网展示页面和图表输出,便于直接用于汇报展示;已有403人学习下载,属于导师认可的高分项目,适合作为课程设计参考,也可在此基础上扩展算法、调整特征或接入实时数据以提升预测表现。

1. 为什么天气预测能撑起一个高分项目

“天气预测”四个字听起来像气象台的专利,但放到机器学习课程项目里,它是少有的能同时覆盖回归、分类、时间序列和数据可视化四个知识点的题目。你需要清洗两年日观测记录,剔除传感器故障带来的脏数据,把日期转换成特征,再训练模型预测第二天的最高气温,最后把对比结果画成图。这条链路走通,数据进、结论出的闭环就成了。

选它的另一个理由是汇报友好。评分人不一定懂气象,但都能判断“预测 26 度,实际 28 度”这个误差是否合理。指标不用解释太久,看图就明白了,而能“看图说话”正是数据可视化这个题眼的价值。同一份数据,特征工程和评测方式不同,分数差距可以很大。

如果你正在准备课程设计、毕业设计或复试机试,这篇博文按“数据管道—建模—可视化—项目收尾”的顺序展开,每一章都带代码和参数说明,直接对照改成自己的项目即可。

2. 构建数据管道:用 Python 把天气数据变成训练样本

2.1 数据源与目录设计:自带数据集再加可选增量脚本

标题既然强调“完整源码+全部数据”,项目交付时就应该把数据作为仓库的一部分,同时保留一个更新脚本。这样评委第一次运行不依赖外网,想换城市或扩时间范围时又可以随时拉新数据。我一般会在项目里放一个这样的结构:

weather_project/ ├── data/ │ ├── raw_weather.csv │ └── processed/ # 清洗后的中间结果 ├── scripts/ │ ├── fetch_weather.py # 可选:拉取最新观测 │ ├── prepare_data.py # 清洗与特征工程 │ └── train_model.py # 训练与评估 └── outputs/ # 图、模型、指标统一输出

这样安排有两个好处:评分人只跑 prepare 和 train 就能复现全部结果,不需要联网;想验证模型对新数据的表现时,执行 fetch 脚本更新 raw_weather.csv 即可。数据获取脚本独立还有一个工程上的意义:原始数据只读,清洗结果输出到 processed,后续实验全部从 processed 读取,避免反复执行清洗步骤产生不一致。

数据获取脚本的常见形态是调公开天气接口。下面这段用 open-meteo 的 archive 接口拉两年逐日气象观测,不需要注册 API key,适合做课程设计:

# fetch_weather.py —— 拉取过去两年的逐日观测 import requests import pandas as pd from datetime import datetime, timedelta end = datetime.now() start = end - timedelta(days=730) url = "https://archive-api.open-meteo.com/v1/archive" params = { "latitude": 39.9042, "longitude": 116.4074, "start_date": start.strftime("%Y-%m-%d"), "end_date": end.strftime("%Y-%m-%d"), "daily": [ "temperature_2m_max", "temperature_2m_min", "precipitation_sum", "wind_speed_10m_max", "weather_code", ], "timezone": "Asia/Shanghai", } data = requests.get(url, params=params, timeout=30).json()["daily"] df = pd.DataFrame(data).rename(columns={"time": "date"}) df.to_csv("data/raw_weather.csv", index=False) print(f"saved {len(df)} rows")

代码逻辑不复杂,但有两个参数容易漏。第一,timezone 必须显式指定,否则接口默认 UTC,中国区域日期整体偏移 8 小时,后面做日期特征全部错位。第二,训练集必须用 archive 历史归档接口,不能用 forecast 预报接口,因为预报值是模型要逼近的目标,拿它当特征会产生标签泄漏。字段落盘前顺手把 time 重命名为 date,能省掉清洗阶段一遍列名映射。

拉回来的字段结构大致如下,这也是常见气象数据集的最小字段集:

列名含义是否入模
date观测日期转成周期特征
temperature_2m_max当日最高气温回归任务标签、分类任务特征
temperature_2m_min当日最低气温特征
precipitation_sum24 小时累计降雨量特征,且是分类标签来源
wind_speed_10m_max10 米高度最大风速特征
weather_codeWMO 天气现象编码过滤或分组使用

最后一行 weather_code 是标准化的天气现象编码,晴天为 0,降雨从 51 起,它直接进模型意义不大,更适合做数据筛选条件,比如只保留特定天气状态,或者派生一个“是否极端天气”的二值特征。

2.2 数据清洗:缺失值、异常值、重复记录分开处理

真实观测数据最常见的三个问题是传感器断传导致整行缺失、异常高温记录、以及同一日期重复存储。清洗顺序建议固定为先去重排序,再过滤异常,最后补缺失:

# prepare_data.py —— 清洗与特征衍生 import numpy as np import pandas as pd df = pd.read_csv("data/raw_weather.csv") df["date"] = pd.to_datetime(df["date"]) # 1) 按日期去重并保证时间索引连续 df = df.drop_duplicates(subset="date").sort_values("date").reset_index(drop=True) # 2) 最高气温做物理范围过滤,超出范围视为传感器故障 df = df[(df["temperature_2m_max"] >= -60) & (df["temperature_2m_max"] <= 55)] # 3) 数值列线性插值;降水列不做插值 num_cols = ["temperature_2m_min", "temperature_2m_max", "wind_speed_10m_max"] df[num_cols] = df[num_cols].interpolate(limit_direction="both") df["precipitation_sum"] = df["precipitation_sum"].fillna(0)

注意降水列的处理逻辑和其他数值列不一样。降水为 0 是有效观测,代表晴天,不是缺失。如果对降水列做线性插值,很可能在连续晴天中间补出一个 2 毫米的小雨,污染后续的降雨分类模型,所以降水缺失宁可填 0。温度列插值也有前提,连续缺失超过 7 天说明设备长时间离线,那段数据应该整体舍弃,插值会造出虚假的平滑趋势。

这个阶段最容易犯的错是“先插值再去重”。如果重复行里有真实值和异常值,插值会先把异常值算进邻近点,再去重时污染已经扩散。所以顺序必须固定为去重、过滤、插值三步。

2.3 特征衍生:滞后值、滑动平均与周期编码

天气数据是时间序列,模型不能只知道“今天”,至少要让模型知道“昨天发生了什么”“过去一周大概什么水平”“现在是一年中的哪个季节”。三个常用特征组件分别是滞后特征、滑动平均和周期编码:

# 在 prepare_data.py 中继续 df["temp_lag_1"] = df["temperature_2m_max"].shift(1) df["temp_ma7"] = df["temperature_2m_max"].rolling(7).mean() day_of_year = df["date"].dt.dayofyear df["sin_day"] = np.sin(2 * np.pi * day_of_year / 365.25) df["cos_day"] = np.cos(2 * np.pi * day_of_year / 365.25) # 分类任务的标签:明天是否有降水 df["rain_tomorrow"] = (df["precipitation_sum"].shift(-1) > 0.1).astype(int) df = df.dropna(subset=["temp_lag_1", "temp_ma7"])

shift(1) 取前一天最高气温,rolling(7) 取过去一周平均,这两个特征对温度预测的贡献通常最大。周期编码用 sin 和 cos 两个维度表示“一年中的第几天”,如果只保留 sin,12 月 31 日和 1 月 1 日几乎相等没问题,但 3 月和 9 月也会出现相同值,模型分不清春季和秋季。sin/cos 组合本质上把日期投射到圆上的一个点,使得季节距离连续单调。

还有一个方向问题。滞后特征用 shift(1),取的是过去数据;分类标签 rain_tomorrow 用 shift(-1),取的是未来数据。特征必须全部来自过去,标签可以来自未来,但两列不能混进同一个特征矩阵。如果误把 shift(-1) 的结果放进 X,模型等价于偷看了明天的答案,测试集指标会虚高到不真实。

2.4 训练集与验证集拆分:按时间切,不要随机打乱

初学机器学习的人最常在这个环节翻车。天气数据带时间顺序,随机 train_test_split 会把 1 月的数据分进训练集,2 月的数据分进验证集,模型学到的其实是对“邻近日期”的记忆,而不是泛化能力。正确做法是使用 TimeSeriesSplit,或者至少保证验证集时间上晚于训练集:

from sklearn.model_selection import TimeSeriesSplit X = df[["temp_lag_1", "temp_ma7", "sin_day", "cos_day", "temperature_2m_min", "wind_speed_10m_max"]] y = df["temperature_2m_max"] tscv = TimeSeriesSplit(n_splits=5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): print(f"fold {fold + 1}: " f"train {train_idx.min()}~{train_idx.max()}, " f"val {val_idx.min()}~{val_idx.max()}")

TimeSeriesSplit 的每一折训练集都比上一折多一段数据,验证集始终紧跟在训练集之后,模拟了“用过去预测未来”的真实场景。后面所有模型评测都基于这个切分方式,指标才有报告价值。

3. 机器学习建模:回归和分类两个任务分别怎么做

3.1 先把预测目标定义清楚

天气预测在机器学习里不是一个任务,而是两个。预测明天最高气温是回归任务,输出连续数值;预测明天下不下雨是分类任务,输出 0 或 1 的概率。两个任务共用同一套特征矩阵,只是标签列不同。这个设定对课程设计非常有利:一套数据预处理流程,能产出两个完整实验,报告里的实验对比部分天然就多一个维度。

回归任务关注误差的大小,分类任务关注排序能力和阈值选择。下面分开建模,先做回归,再做分类。

3.2 回归任务:用一个评估函数统一比较模型

先写一个统一的交叉验证评估函数,后续所有回归模型都走这个入口,避免每换一个模型就复制一段评测代码:

from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error def evaluate_regressor(model, X, y): mae_list = [] for train_idx, val_idx in tscv.split(X): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] y_train, y_val = y.iloc[train_idx], y.iloc[val_idx] model.fit(X_train, y_train) pred = model.predict(X_val) mae_list.append(mean_absolute_error(y_val, pred)) return np.mean(mae_list) linear = make_pipeline(StandardScaler(), LinearRegression()) print("linear MAE:", evaluate_regressor(linear, X, y)) rf = RandomForestRegressor( n_estimators=200, max_depth=8, min_samples_leaf=5, random_state=42, ) print("random forest MAE:", evaluate_regressor(rf, X, y))

选择平均绝对误差 MAE 作为主指标,是因为温度预测的误差以摄氏度为衡量单位,MAE 含义直白,评委一听就懂。RMSE 会放大离群日的惩罚,在天气这种噪声较大的场景里容易让模型去迁就极端天气。线性回归在温度预测上的 MAE 通常落在 2 到 3 度,随机森林可以压到 2 度以内,具体数值和城市气候有关,但结构性的结论是一致的:树模型对非线性关系和特征交互更友好。

随机森林的三个参数需要理解而不是照抄。n_estimators=200 在日尺度天气预报上已经足够,加到 1000 棵提升有限但训练时间线性增长。max_depth=8 限制单棵树深度,防止树去记忆某一天的极端气温。min_samples_leaf=5 保证每个叶子节点至少 5 个样本,让预测结果更平滑,避免在寒潮和热浪这种异常日上抖动太剧烈。

3.3 分类任务:降雨概率预测用 ROC-AUC 而不是准确率

降水预测的标签是“明天是否降雨”,这类数据通常存在类别不平衡:多数城市一年中晴天多于雨天。如果直接看准确率,模型全部预测“不下雨”也可能拿到 70% 的准确率,但没有任何实用价值。正确做法是用 ROC-AUC 评估排序能力,用混淆矩阵观察误报和漏报:

from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score target = df["rain_tomorrow"] clf = RandomForestClassifier( n_estimators=300, max_depth=6, min_samples_leaf=10, random_state=42, ) auc_list = [] for train_idx, val_idx in tscv.split(X): model = clf model.fit(X.iloc[train_idx], target.iloc[train_idx]) prob = model.predict_proba(X.iloc[val_idx])[:, 1] auc_list.append(roc_auc_score(target.iloc[val_idx], prob)) print("rain AUC:", np.mean(auc_list))

分类器用 predict_proba 输出降雨概率,而不是直接输出 0 或 1,因为 AUC 的计算依赖概率排序,不需要先定阈值。这种评估方式贴合实际使用场景:气象预报通常给出降水概率,再由人决定是否带伞。max_depth=6 比回归模型更浅,原因同样是防止模型记住某些特定年份的偶然天气组合。

两个任务的对比可以整理成一张表,放进项目报告里:

比较维度温度回归降雨分类
标签类型连续数值二值类别
主指标MAEROC-AUC
特征要求滞后、均值、周期同上,对降水滞后更敏感
常见错误插值温度导致预测过平滑类别不平衡下只看准确率

3.4 时间序列交叉验证下最容易犯的错

用 KFold 替代 TimeSeriesSplit 是时间序列项目里最典型的问题。KFold 随机打乱数据后,折叠内部可能同时出现 1 月和 7 月的数据,模型在训练时已经“见过”验证集附近的样本分布,评估结果偏乐观。对天气这种强自相关的数据,偏差尤其明显,因为相邻日期的气温高度相似。

一个可复现的验证方法是把 KFold 的 MAE 和 TimeSeriesSplit 的 MAE 同时打印出来,前者通常会明显优于后者。这个对比本身就可以写进项目报告的“评估方法”章节,用来解释你为什么选择按时间切分而非随机切分。

4. 数据可视化:让真实值和预测值在同一张图里直接对话

4.1 静态底图:Matplotlib 画预测对比与残差分布

建模完成后,第一步可视化一定是真实值与预测值的时间序列对比。注意这里要画验证集上的预测,而不是训练集上的拟合值,否则图上的贴合程度没有说服力:

import matplotlib.pyplot as plt val_idx = list(tscv.split(X))[-1][1] # 取最后一折验证集 X_val = X.iloc[val_idx] y_val = y.iloc[val_idx] pred = rf.fit(X.iloc[:val_idx[0]], y.iloc[:val_idx[0]]).predict(X_val) val_df = df.iloc[val_idx].copy() val_df["pred"] = pred fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True) axes[0].plot(val_df["date"], val_df["temperature_2m_max"], label="真实值", linewidth=1.5) axes[0].plot(val_df["date"], val_df["pred"], label="预测值", linewidth=1.5, alpha=0.8) axes[0].set_title("最高气温预测 vs 真实值") axes[0].legend() residual = val_df["temperature_2m_max"] - val_df["pred"] axes[1].hist(residual, bins=30, edgecolor="white") axes[1].set_title("预测残差分布(真实值 - 预测值)") plt.tight_layout() plt.savefig("outputs/temp_forecast.png", dpi=150)

残差直方图如果近似以 0 为中心的正态分布,说明模型没有系统性偏差。如果整体偏正,模型系统性地低估温度;偏负则高估。这类信息很难从指标里直接读出来,但图上非常直观。两张图上下拼接,纵轴自动对齐,适合直接放进论文或答辩 PPT。如果季风条件下的系统偏差存在,你会看到残差分布虽然对称但方差偏大,说明模型没能捕捉部分气候波动。

4.2 交互式面板:Plotly 做带时间范围选择的气象大屏

静态图适合论文,交互图适合现场演示。Plotly 的 write_html 能生成一个独立 HTML 文件,评分人双击即可打开,不需要安装 Jupyter 或额外服务。用两个子图叠加气温和降水,把“降水后气温下降”这类现象直接展示出来:

import plotly.graph_objects as go from plotly.subplots import make_subplots fig = make_subplots(specs=[[{"secondary_y": True}]]) fig.add_trace( go.Scatter(x=val_df["date"], y=val_df["temperature_2m_max"], name="真实最高温", line=dict(color="#1f77b4")), secondary_y=False, ) fig.add_trace( go.Scatter(x=val_df["date"], y=val_df["pred"], name="预测最高温", line=dict(color="#ff7f0e", dash="dash")), secondary_y=False, ) fig.add_trace( go.Scatter(x=val_df["date"], y=val_df["precipitation_sum"], name="日降水量", line=dict(color="#2ca02c")), secondary_y=True, ) fig.update_layout(hovermode="x unified", title="气象预测与观测对比") fig.write_html("outputs/weather_dashboard.html")

hovermode="x unified" 让鼠标划过时同时显示同一时间的温度和降水两个数值。看板里如果还想达到企业级数据可视化大屏的展示效果,可以把 Plotly 的图拆分到多个 HTML 区块,再用 Flask 或 Streamlit 拼装成整屏布局。原理都一样:图表组件化,数据对接同一个 DataFrame。

4.3 特征重要性:让模型决策过程变成答辩素材

随机森林自带特征重要性输出,这是课程项目里性价比最高的可视化素材。用水平条形图画出来,可以直接支撑“模型学到了什么”这段论述:

importances = pd.Series( rf.feature_importances_, index=X.columns, ).sort_values(ascending=True) importances.plot.barh(figsize=(8, 4)) plt.xlabel("feature importance") plt.tight_layout() plt.savefig("outputs/feature_importance.png", dpi=150)

在温度预测任务里,你通常会发现 temp_lag_1 和 sin_day/cos_day 排在前列,这是符合气象常识的:昨天的气温是今天气温最强的单变量信号,季节周期是第二个强信号。把这些观察到的话写进报告,比单纯贴一张图更有说服力。

5. 把“高分项目”和普通作业拉开差距的 3 个细节

5.1 模型持久化:训练一次,处处复用

很多项目把训练和预测写在同一段脚本里,每次运行都重新训练,这在演示时很浪费时间,也容易出现结果不可复现的问题。规范做法是把训练好的模型和指标落盘:

from joblib import dump dump(rf, "outputs/model_rf.joblib") dump({"mae": np.mean(mae_list), "features": list(X.columns)}, "outputs/metrics_rf.json")

模型文件加指标文件一起输出后,再做预测或写演示页面时,只加载不重训。joblib 对 numpy 数组序列化的效率比 pickle 高,是 scikit-learn 生态里的常用做法。指标单独存 JSON 格式,方便后续在不同模型间做横向对比,也可以让自动化评分程序直接读取。

5.2 一个入口脚本跑完整条流程

加分的关键一步是把 fetch、prepare、train 封装成一个带参数的入口。用 argparse 控制模型选择和是否重新拉取数据:

# train_model.py import argparse parser = argparse.ArgumentParser() parser.add_argument("--model", default="rf", choices=["rf", "linear"]) parser.add_argument("--city", default="beijing") args = parser.parse_args()

这样整个项目就可以用一行命令复现全部实验。课程设计答辩时,现场演示“python train_model.py --model rf --city beijing”然后展示输出结果,比一步步点 Jupyter 单元格专业得多,也给评分人留下工程规范的印象。

5.3 月度误差分析:把模型失效场景找出来

比较推荐的收尾技巧是对预测误差做月度聚合,输出一张误差分布表。按月份分组计算平均绝对误差,定位模型在哪些月份表现差、为什么差:

val_df["month"] = val_df["date"].dt.month monthly_mae = ( (val_df["temperature_2m_max"] - val_df["pred"]) .abs() .groupby(val_df["month"]) .mean() ) monthly_mae.to_csv("outputs/monthly_mae.csv")

夏季雷阵雨集中、春秋两季气温波动大,通常会造成 MAE 明显抬升。这张表配合特征重要性图,能支撑起报告里“局限性与改进方向”整节内容。如果误差集中在换季月份,说明周期特征还不足以描述天气的突变,下一步可以考虑加入气压或露点温度特征,这也为论文留出了明确的后续工作空间。

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

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

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

立即咨询