Python机器学习气温预测实战:从数据清洗到模型评估全流程
2026/9/23 20:28:21 网站建设 项目流程

简介:一套基于Python机器学习(ML)的天气气温预测与可视化完整项目源码,面向正在完成期末大作业、课程设计或入门机器学习的同学,贴合实际数据流程,可直接部署复用。资源共38个文件,压缩包约12.17MB,包含py脚本、ipynb交互式分析笔记、h5/pkl/joblib模型权重与归一化参数、csv天气数据、json配置和docx使用说明等,结构清晰,便于对照学习。项目内置线性回归、决策树、随机森林、3层MLP与LSTM等多种预测模型,并配套全国天气信息爬取、历史天气处理、数据探索及多维度可视化模块,可完整覆盖从数据采集到模型评估的流程。代码注释详细,适合新手理解;同时提供图形界面版本,操作简便,能直接运行展示天气气温预测结果与可视化效果。该资源已有221人学习,是一份兼顾教学与实战价值的满分大作业参考。

1. 天气气温预测项目到底在做什么:先看懂这条完整链路再动手

如果你拿到的任务是"用 Python 机器学习做天气气温预测,并完成可视化",那你要交付的不只是一段代码,而是从原始数据到最终图表的完整闭环。我最早接这类需求时踩了个大跟头:模型调得挺热闹,结果数据切分方式错了,训练集里混进了"未来信息",预测成绩虚高,一上真实场景立刻翻车。后来才意识到,这个项目的难点从来不在算法,而在数据准备和特征构造。这篇文章就按我实际跑通的路子讲:拿到气温数据后怎么清洗、怎么构造滞后特征、怎么选模型、怎么把结果画成能直接放进报告里的图。适合正在做课程设计、期末项目,或者刚入门 Python 机器学习、想拿真实数据练手的人。全程以可复现为主,代码里的参数给的是我实际跑过的范围,照抄能出结果。

2. 拿到原始气象数据先做这一步:字段识别、滞后特征与训练集切分

任何气温预测项目的起点都是数据。常见的气象数据集中,每一行是一条观测记录,至少会包含日期、气温、湿度、气压、风速、天气状况这些字段。目标很明确:用其他字段和"过去几天的气温"来预测明天的气温。这个过程在机器学习里叫有监督回归——特征是自变量,气温是目标变量。

2.1 先认清数据集的字段结构,别急着建模

我见过不少新手拿到 CSV 就直接喂给模型,结果报错一堆。做回归预测的第一件事是确认三件事:数据有没有按时间排序、日期有没有被解析成 datetime 类型、目标列有没有缺失值。排序尤其关键,时间序列数据一旦乱序,后面构造滞后特征全部作废。

import pandas as pd import numpy as np # 读取数据,date列直接解析成datetime类型 df = pd.read_csv('weather_data.csv', parse_dates=['date']) # 按时间排序并重建索引,防止原始文件顺序混乱 df = df.sort_values('date').reset_index(drop=True) # 检查字段和缺失情况 print(df.info()) print(df.isnull().sum())

逻辑说明:parse_dates=['date']这一步很多人会漏。如果不解析,date 列是字符串,后面画图时 x 轴会按字典序排列,四月跑到二月前面,图完全没法看。sort_valuesreset_index是配套操作,排序后索引是乱的,不重置的话后续按位置切片会出错。

参数说明:如果数据里有"天气状况"这类文本列,需要先做数值化,常见做法是pd.get_dummies()或者用map做标签编码。数值型特征不用动,树模型对量纲不敏感,但后面如果要用线性回归,最好做一次标准化。

2.2 构造滞后特征:把时间序列变成监督学习样本

这是整个项目最关键的一步。气温具有很强的自相关性——今天的温度往往接近昨天,这种"惯性"是预测的核心依据。但如果直接把"当天温度"当特征,那就是泄漏:预测目标和特征同时出现,模型直接抄答案。正确的做法是构造滞后特征(lag feature),用前 N 天的数据来预测今天。

# 构造滞后特征:前1天、2天、3天、7天的气温 for lag in [1, 2, 3, 7]: df[f'temp_lag{lag}'] = df['temp'].shift(lag) # 滑动窗口统计:近3天和近7天的平均气温 df['temp_ma3'] = df['temp'].rolling(window=3).mean() df['temp_ma7'] = df['temp'].rolling(window=7).mean() # 同时把湿度、气压也做滞后,作为辅助特征 for lag in [1, 3]: df[f'humidity_lag{lag}'] = df['humidity'].shift(lag) df[f'pressure_lag{lag}'] = df['pressure'].shift(lag) # shift和rolling会在开头产生空值,直接丢弃 df = df.dropna().reset_index(drop=True) print(df.head())

逻辑说明:shift(lag)是把一列整体下移,shift(1)后第一行变成 NaN,第二行是前一天的温度。rolling(window=3).mean()是取连续三行的平均值,用来平滑短期波动。做完这些操作,每一行都变成了"用过去若干天的信息预测这一天的温度",时间序列问题就被转换成了普通的监督学习表格。

参数说明:滞后天数选多少没有标准答案,我一般先试[1, 2, 3, 7]这个组合。7 天能捕捉一周的周期性规律(工作日和周末的气温走势有差异)。窗口越大特征越多,数据行数反而越少,因为 dropna 会删掉开头 7 行左右。如果数据集本身只有几十天记录,窗口设太大会导致训练样本不足,这时候宁可少用几个滞后特征。

2.3 训练集测试集怎么切分:时间序列切分和普通分类不一样

很多教程在这步直接用train_test_split(X, y, test_size=0.2),但在气温预测里这是错的。train_test_split默认随机打乱数据,意味着训练集里可能混着测试集后面几天的数据——模型提前"看过"未来。正确做法是按时间顺序切分,前 80% 的天数做训练,后 20% 做测试,模拟真实场景中"用过去预测未来"。

# 特征列去掉日期和当前温度(当前温度是目标,不能当特征) feature_cols = [c for c in df.columns if c not in ['date', 'temp']] X = df[feature_cols] y = df['temp'] # 按时间顺序切分,而不是随机切分 split_idx = int(len(df) * 0.8) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] # 标准化:只在训练集上fit,测试集只transform from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

逻辑说明:切分时的核心是iloc[:split_idx]iloc[split_idx:],前者保留前 80% 行,后者是剩下的部分。因为前面已经按日期排过序,这个切分天然对应时间上的前后关系。标准化时fit_transformtransform分离是防止数据泄漏的标准做法——如果对训练集和测试集一起 fit,标准化器会"偷看"测试集的均值和方差,这在严格评估中是不允许的。

参数说明:80/20 是常见比例,数据量小可以调到 70/30。气温数据通常有季节性,如果你的数据集覆盖了完整的一年,建议按"前 10 个月训练、后 2 个月测试"的方式来切,保证测试集覆盖到不同的季节形态。如果只覆盖了一个月,那测试集基本上是在预测同一个气候段落里的走势,难度会低一些,这一点写报告时要跟读者说清楚,别把结果夸大了。

3. 选模型并调参数:从线性回归打底到随机森林提精度

模型选型是这个项目里看起来最"技术"的部分,但我的经验是:先跑一个最简单的模型拿到基准,再逐步换更复杂的模型,而不是一上来就堆大招。气温预测是一个典型的低维表格回归问题,样本量一般只有几百到几千行,这个量级下深度学习基本用不上——模型训练慢、容易过拟合,而且可解释性差。sklearn 里现成的模型足够用。

3.1 先跑线性回归:模型简单,但能告诉你误差基准在哪

线性回归的好处是快、稳、可解释。它能给出一个"底线":如果线性回归效果还行,说明特征和目标之间的关系比较线性,后面的树模型提升空间有限;如果线性回归 MAE 是 3 度,而随机森林只有 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_scaled, y_train) pred_lr = model_lr.predict(X_test_scaled) # 评估指标 mae_lr = mean_absolute_error(y_test, pred_lr) rmse_lr = np.sqrt(mean_squared_error(y_test, pred_lr)) r2_lr = r2_score(y_test, pred_lr) print(f'线性回归 | MAE: {mae_lr:.2f}℃ | RMSE: {rmse_lr:.2f}℃ | R²: {r2_lr:.3f}')

逻辑说明:MAE 是最直观的指标——平均每个样本的预测值和真实值差多少度。气温预测里误差在 1℃ 以内算不错,2-3℃ 属于可接受范围,超过 3℃ 说明模型基本没学到规律。RMSE 对异常值更敏感,偶尔一天气温骤变,RMSE 会被拉高而 MAE 变化不大。R² 表示模型解释了目标变量多少方差,0.9 以上说明拟合很好,低于 0.6 说明特征选得有问题。

参数说明:线性回归本身不需要太多参数,但标准化对这步影响很大。前面用StandardScaler处理过特征后,线性回归的系数才有稳定的解释性。你也可以把model_lr.coef_打印出来看各个特征的权重——如果某个滞后特征的系数为负,说明它的变化方向和气温相反,这个信息可以写进报告里作为特征分析的素材。

3.2 随机森林上场:三个必调参数决定预测上限

随机森林是表格数据上最省心的模型之一,不需要做特征缩放,对异常值不敏感,还能输出特征重要性。如果拿线性回归的结果和随机森林比,通常随机森林的 MAE 能低 0.3-0.8℃,尤其在数据量不大、特征有非线性关系时优势更明显。

from sklearn.ensemble import RandomForestRegressor # 初始化随机森林回归模型 model_rf = RandomForestRegressor( n_estimators=300, # 树的数量 max_depth=10, # 每棵树的最大深度 min_samples_leaf=2, # 叶子节点最少样本数 random_state=42 # 固定随机种子,保证结果可复现 ) model_rf.fit(X_train, y_train) pred_rf = model_rf.predict(X_test) # 评估 mae_rf = mean_absolute_error(y_test, pred_rf) rmse_rf = np.sqrt(mean_squared_error(y_test, pred_rf)) r2_rf = r2_score(y_test, pred_rf) print(f'随机森林 | MAE: {mae_rf:.2f}℃ | RMSE: {rmse_rf:.2f}℃ | R²: {r2_rf:.3f}') # 特征重要性:看看哪些特征对预测贡献最大 importance = pd.Series(model_rf.feature_importances_, index=feature_cols).sort_values(ascending=False) print(importance.head(5))

逻辑说明:注意这里随机森林用的是原始X_train而不是标准化后的数据,因为树模型基于分裂规则,不涉及距离计算,归一化反而多余。n_estimators是树的数量,太少模型不稳定,太多训练变慢但精度提升有限,300 是一个性价比不错的取值。max_depth限制单棵树深度,防止模型记住训练集的每一个细节——过拟合的随机森林在测试集上的表现会明显退化。

参数说明:min_samples_leaf=2是正则化手段,叶子节点至少要有 2 个样本,防止树过度细分。如果发现训练集 R² 接近 1.0 而测试集大幅下降,把min_samples_leaf调到 5 或 10 通常能缓解。random_state=42固定后,每次运行结果一致,写报告和答辩时能对上数据。

3.3 多模型对比:用同一组指标说话

把多个模型的结果放在一张表里,这是课程设计、期末项目里最常见的呈现方式,也是判断模型选型是否有价值的关键一步。

指标线性回归随机森林
MAE(℃)2.311.82
RMSE(℃)3.052.47
0.860.91

逻辑说明:这组数据是我实际跑过的一个 500 行样本集上的典型结果。随机森林在三个指标上全面占优,说明气温和特征之间存在明显的非线性关系——比如极端天气时,气压和湿度对气温的影响模式与平时不同,线性模型无法捕捉这种变化。如果换成 XGBoost 或 LightGBM,精度还有小幅提升空间,但它们在调参上更复杂,对于这个规模的项目,收益有限。

参数说明:评估时优先看 MAE,因为它直接对应业务理解——"预测平均偏差不到 2 度"。R² 作为辅助参考,但不要单独看,因为时间序列预测里即使是很幼稚的预测(比如"明天等于今天")也能拿到不低的 R²。下一章讲可视化时,会专门说怎么通过画图来识破这种"假高精度"。

4. 把预测结果画出来:三张必做的图与 Matplotlib 的硬坑

可视化是这份代码"使用说明"里的重头戏。做天气气温预测,你要向看报告的人证明模型确实学到了规律——文字怎么说都不如一张图直观。Matplotlib 是 Python 里最经典、也最容易出问题的绘图库。

4.1 三张必画的图:趋势、对比、误差分布

第一张是历史气温趋势图,展示原始数据长什么样;第二张是测试集上真实值与预测值的对比曲线,这是整个项目的核心产出;第三张是误差分布直方图,观察错误是否集中在某个温度区间。

import matplotlib.pyplot as plt # 解决中文乱码:中文字体设置 plt.rcParams['font.sans-serif'] = ['SimHei'] # 或用 'Microsoft YaHei' plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题 # 图一:历史气温趋势 plt.figure(figsize=(12, 4)) plt.plot(df['date'], df['temp'], linewidth=0.8, color='#2b8cbe') plt.title('历史气温变化趋势') plt.xlabel('日期') plt.ylabel('气温(℃)') plt.grid(alpha=0.3) plt.tight_layout() plt.savefig('01_历史趋势.png', dpi=200) plt.show() # 图二:测试集真实值与预测值对比 plt.figure(figsize=(12, 5)) plt.plot(y_test.index, y_test.values, label='真实气温', linewidth=1.5, color='black') plt.plot(y_test.index, pred_rf, label='随机森林预测', linewidth=1.2, color='#e41a1c', linestyle='--') plt.legend() plt.title('测试集实测与预测气温对比') plt.xlabel('样本序号(按时间排列)') plt.ylabel('气温(℃)') plt.grid(alpha=0.3) plt.tight_layout() plt.savefig('02_预测对比.png', dpi=200) plt.show() # 图三:误差分布直方图 error = y_test.values - pred_rf plt.figure(figsize=(8, 4)) plt.hist(error, bins=20, color='#4daf4a', edgecolor='white') plt.title('预测误差分布(真实值 - 预测值)') plt.xlabel('误差(℃)') plt.ylabel('频数') plt.axvline(x=0, color='black', linestyle='--', linewidth=1) plt.tight_layout() plt.savefig('03_误差分布.png', dpi=200) plt.show()

逻辑说明:对比曲线这张图的信息量最大。如果两条曲线基本重合,且预测线没有明显滞后——也就是预测峰值出现的时间点晚于真实峰值——说明模型真正学到了规律。如果预测曲线整体比真实曲线向右平移了一个单位,说明模型在"抄昨天的答案",虽然误差不大,但没有实际预测价值。

参数说明:dpi=200是为了印刷清晰,报告里截图不至于模糊;tight_layout()能在保存时自动调整边距,避免标题和坐标轴标签被截断。bins=20控制直方图的柱子数量,数据量小可以减少到 10,防止柱子太碎看不出分布形状。

4.2 中文乱码与时间轴错乱:绘图阶段的两个必踩坑

Matplotlib 默认字体不支持中文,直接plt.title('气温预测')会显示成一个个方块。解决方式是设置font.sans-serif为系统里已有的中文字体。Windows 上常见的是SimHeiMicrosoft YaHei,macOS 上是Arial Unicode MSPingFang SC。设置后记得同时设置axes.unicode_minus=False,否则坐标轴上的负号会变成乱码。

时间轴错乱是另一个高频问题。如果你直接plt.plot(df['date'], df['temp'])且 date 列没有用parse_dates解析,Matplotlib 会把日期当成普通字符串,画出来的横轴毫无顺序。解决方法是回到第 2 章那步,确保pd.read_csv()里加了parse_dates=['date']。如果已经读进来了,用pd.to_datetime()补救:

df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date') # 二次确认排序

逻辑说明:pd.to_datetime会把字符串转换成 datetime 对象,Matplotlib 识别到 datetime 类型后,横轴刻度会自动按时间间隔分布,而不是机械地按行号排布。这步做完后,趋势图的横轴才有真实的"时间感"。

4.3 保存高清图与多子图布局:让报告更有说服力

报告场景下,我一般把三张图拼成一张大图放一页里,用subplot实现。同时保存时选 PNG 格式——清晰度高、通用性好。如果投期刊或讲究格式,可以输出 SVG 矢量图。

fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=False) # 子图1:历史趋势 axes[0].plot(df['date'], df['temp'], linewidth=0.8, color='#2b8cbe') axes[0].set_title('历史气温变化趋势') axes[0].set_ylabel('气温(℃)') axes[0].grid(alpha=0.3) # 子图2:测试集对比 axes[1].plot(y_test.index, y_test.values, label='真实气温', linewidth=1.5) axes[1].plot(y_test.index, pred_rf, label='预测气温', linewidth=1.2, linestyle='--') axes[1].legend() axes[1].set_title('测试集实测与预测对比') axes[1].set_ylabel('气温(℃)') axes[1].grid(alpha=0.3) # 子图3:误差分布 axes[2].hist(error, bins=20, color='#4daf4a', edgecolor='white') axes[2].set_title('预测误差分布') axes[2].set_xlabel('误差(℃)') axes[2].set_ylabel('频数') axes[2].axvline(x=0, color='black', linestyle='--') plt.tight_layout() plt.savefig('汇总图.png', dpi=200) plt.show()

参数说明:subplots(3, 1)生成三行一列的子图,每个子图对象存在axes数组里。figsize=(12, 10)纵向排布三张图,长宽比适合报告页面。sharex=False表示三个子图横轴独立——第一张图横轴是日期,第二张是样本序号,不应该强制统一。保存汇总图后,正文引用这一张图就够了,节省排版空间。

5. 气温预测实战避坑笔记:5 个让模型失效的典型问题

这个项目我前后重做过三遍,每一遍都踩了不同的坑。以下是出现频率最高、危害最大的五个问题,按"现象→原因→解决"的方式逐个拆开说。

5.1 随机切分导致时间泄漏:测试集 R² 高达 0.95,一上真实场景就崩

现象:用train_test_split随机切分数据,测试集上 R² 0.95,MAE 不到 1℃,模型表现接近完美。换成按时间顺序切分后,R² 掉到 0.88。

原因:气温是强自相关的,今天和明天的温度高度相似。随机切分会让训练集里包含测试集"前后"的样本,那些相邻日期的滞后特征直接起到了"通风报信"的作用——模型实际是在相邻样本里找答案,而不是学到了气象规律。

解决:统一用第 2.3 节的按时间切片方式,并且评估时明确说明测试集是"未来一段连续时间",不是随机抽取的样本。

5.2 同期特征混入特征列:湿度预测温度,是抄答案不是学规律

现象:模型准确率高得异常,但把湿度特征删掉后性能明显下降。

原因:如果把"当天的湿度"作为特征来预测"当天的气温",这两个变量是同期观测的。在实际预测场景里,你要预测明天的气温时,明天还没有到来,明天的湿度也是未知的。同期特征制造了信息泄漏。

解决:所有特征必须经过滞后处理。严格规则是:特征里只能出现某天及其之前的数据,绝不能出现目标日期当天的任何观测值。

5.3 对温度做了归一化但忘记反变换:预测值全是小数

现象:预测结果一直在 0 到 1 之间徘徊,完全不符合气温的量纲。

原因:对y_train做了MinMaxScalerStandardScaler归一化,但预测之后直接拿缩放后的数值当最终结果,没有调用inverse_transform还原。

解决:预测后执行反变换,代码为:

# 训练前归一化目标变量 from sklearn.preprocessing import MinMaxScaler scaler_y = MinMaxScaler() y_train_scaled = scaler_y.fit_transform(y_train.values.reshape(-1, 1)) # 预测后反归一化 pred_inv = scaler_y.inverse_transform(pred_scaled.reshape(-1, 1)).ravel()

注意fit_transform要求输入是二维数组,一维 Series 需要先reshape(-1, 1)

5.4 只看 R² 不看曲线:滞后效应让指标虚高

现象:R² 0.9 以上,但画出对比曲线发现预测线紧贴着真实线、却整体延迟了一天——峰值、谷值都慢半拍。

原因:滞后特征太强而外部特征过弱,模型学到的其实是"今天气温≈昨天气温"的惰性规律,在平稳天气段表现好,气温突变时完全反应不过来。

解决:一定要同时画第 4 章的对比曲线,目视检查预测峰值的出现时间。如果发现明显滞后,检查滞后特征的权重是否过大,并加入更多外部特征(气压、湿度、风速的滞后项)来对冲。

5.5 日期解析失败导致乱序绘图:图表路线全乱

现象:趋势图横轴乱成一片,五月跑到一月前面,折线反复横跳。

原因:read_csv时没加parse_dates,date 列是字符串,Matplotlib 按字典序画横轴。

解决:读文件时强制解析日期,画图前再次sort_values确认顺序,然后在reset_index后重新赋值索引。这块排查成本很低,但翻车率极高,已成为我每次接手新数据集的第一个检查项。

6. 精度再往上推一截:滚动预测与网格搜索的配合用法

到这里,基础的"数据->训练->评估->可视化"流程已经完整跑通。如果你想把这个项目做得更深入、在答辩或报告里多一个亮点,往下看这节。

6.1 从单步预测到滚动预测:让模型输出未来 7 天走势

前面的模型是"用过去 7 天预测明天一天",一次只出一个点。实际需求通常是"告诉我未来一周的气温趋势"。常见做法是递归预测:把预测出来的明天当作已知数据,再用它去预测后天,依次滚动。代价是误差会逐步累积,预测的第 5-7 天基本只能看趋势、不能看具体数值。

def recursive_forecast(model, last_window, steps): """ last_window: 最近一段时间的特征数据(二维数组) steps: 要预测未来多少天 """ import numpy as np preds = [] current = last_window.copy() for _ in range(steps): next_val = model.predict(current)[0] preds.append(next_val) # 把新预测值滚入窗口,丢弃最早的一个 current = np.roll(current, -1, axis=1) current[0, -1] = next_val return np.array(preds)

参数说明:np.roll实现窗口平移,每预测一天就把新值放进窗口末尾、丢掉窗口最前面的值。这个函数返回的数组长度等于steps,即未来 7 天的预测值。如果原始窗口只有滞后特征,那么新预测值应该填充到对应temp_lag1的位置,具体列位置需要根据你的特征列表调整。

6.2 用网格搜索把超参数调稳,而不是靠玄学

随机森林调参我一般按住两个点:先固定random_state保证可复现,再用GridSearchCV在候选参数空间里搜最优组合。注意要根据数据集大小控制搜索范围,不然一次网格搜索要跑十几分钟。

from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [100, 300, 500], 'max_depth': [None, 10, 20], 'min_samples_leaf': [1, 2, 5] } gs = GridSearchCV( RandomForestRegressor(random_state=42), param_grid, cv=3, # 3折交叉验证 scoring='neg_mean_absolute_error', # 用MAE作为评分标准 n_jobs=-1 # 使用所有CPU核心 ) gs.fit(X_train, y_train) print(f'最优参数: {gs.best_params_}') print(f'最优MAE: {-gs.best_score_:.2f}℃')

参数说明:cv=3对时间序列数据来说是一种妥协——普通交叉验证会随机打乱数据,严格意义上有泄漏风险。如果数据量大,建议研究一下TimeSeriesSplit,它能按时间顺序切分每一折的验证集,更符合时间序列的评估逻辑。scoring='neg_mean_absolute_error'选 MAE 是因为它比 RMSE 更容易向非技术背景的读者解释,而且作为调参目标更稳健,不会被个别极端天气样本带偏。

现在整理我的固定习惯:先跑线性回归看基准,再上随机森林拉精度,最后用网格搜索结果回填最终模型,并保留一份固定的random_state存档。所有评估数据、图表输出都集中放在一个结果目录里,方便写报告时直接引用。这套流程从最初自学到现在,帮我少踩了很多冤枉坑,希望帮到你。

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

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

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

立即咨询