机器学习大作业实战:天气预测时间序列建模与避坑指南
简介机器学习大作业-预测天气.zip 是一份面向机器学习课程大作业的实践资料聚焦如何利用历史气象数据构建天气预测模型。压缩包大小约603KB内部文件结构虽未详细列出但结合主题推测包含数据集、Python代码、实验报告等材料适合机器学习初学者和需要完成同类课题的学生参考。资源围绕天气预测的完整流程展开涉及数据预处理、特征工程、线性回归/决策树/LSTM等模型选择、训练验证、超参数调优、模型解释与部署等关键环节。通过研读这份大作业读者可以系统理解时间序列预测的实现步骤学会处理缺失值、构造滑动平均等特征并掌握用MSE、RMSE等指标评估模型性能的方法对完成课程项目和提升工程实践能力都很有帮助。目前已有7276人浏览学习是同类资源中较受关注的一份。1. 机器学习大作业里的「预测天气」难点不在模型上「机器学习大作业-预测天气」这种题听起来像标准回归练习真做起来才会意识到数据处理、时间切片、特征构造、模型验收每一环都能卡人。尤其是预测天气这类时间序列任务模型跑通不难难的是验证集分数好看一放到未来几天真实预测就露馅。下面按一门机器学习课程设计最常见的落地路径来拆——把原始天气数据处理成可训练样本、构造滞后特征和周期特征、在几个机器学习模型之间选型做对比最后把最容易翻车的检查点过一遍。适合准备交机器学习期末大作业、或者想用完整应用流程练手的读者照着做能跑通并拿到合理分数也知道哪些地方不值得过度优化。2. 天气数据处理先过清洗、时间对齐、按时间划分三关拿到数据第一件事不是跑模型而是搞清楚这张表的结构。气象数据通常来自公开气象站的逐时或逐日观测记录CSV 里常见字段有气温、露点、湿度、气压、风速、降雨量。如果这里不把时间列和对齐关系理清楚后面所有滞后特征都是错的而且错得很隐蔽。2.1 数据长什么样每一列对训练意味着什么先确认三件事时间粒度是逐时还是逐日、目标列是哪一列、数据覆盖了多久。逐时数据一行是一小时逐日数据一行是一天。作业要求「预测明天最高气温」时目标列要把逐时数据重采样成每天的最高温要求「预测下一小时温度」时目标列就是温度序列本身。两种任务的特征窗口完全不同先别急着写代码。import pandas as pd df pd.read_csv(weather_data.csv) df[date] pd.to_datetime(df[date]) # 字符串时间戳转成 datetime df df.sort_values(date).reset_index(dropTrue) # 按时间排序并重排索引 print(df.shape) print(df.columns.tolist()) print(df[date].min(), -, df[date].max())pd.to_datetime负责解析时间戳如果 CSV 里年月日分列就先合并再解析。排序这步不能省气象站在导出数据时未必按时间顺序排不排序的话后面shift出来的滞后特征会全错位。reset_index(dropTrue)让行号变成连续的方便之后用位置切片。时间列解析成datetime64后建议顺手跑一下df[date].is_monotonic_increasing返回False说明排序没生效立刻排查。这个阶段还要区分「内生变量」和「外生变量」。温度预测里温度自身的滞后值是最强特征湿度、气压、风速属于外生辅助特征有帮助但不是主力。作业里如果限定「用过去 24 小时预测未来 24 小时」特征就要按 24 小时的窗口来构造不是把整个历史平均一下丢进去。2.2 缺失值处理连续缺 6 小时以上插值不如删除气象站偶尔会停机数据缺口最常见。一两个小时的缺口用前向填充或线性插值都能接受连续十几小时的缺口任何插值都会造出一段「模型里不存在于现实的平滑曲线」。训练时模型把这段曲线当成特征学了真实预测时根本没有这种模式等于白喂噪声。# 短缺口线性插值两侧都允许填充 for col in [temp, humidity, pressure]: df[col] df[col].interpolate(limit_directionboth) # 长缺口连续缺失超过 6 小时的行直接剔除 miss df[temp].isna().astype(int) block miss.groupby((miss 0).cumsum()).transform(sum) df df[block 6].reset_index(dropTrue)interpolate()默认按索引做线性插值对小时级数据足够limit_directionboth让序列开头和结尾的缺失也能被填上。连续缺失统计这段核心逻辑是把缺失段按连续块分组算每块缺失总数块内缺失数大于等于 6 的整块删掉。阈值取 6 是我常用的值有 3 年数据删几十行无所谓如果只有 3 个月数据可以放宽到 12。实际取舍看数据总量。缺口长度处理方式原因1~2 小时线性插值或前向填充短期渐变误差可接受3~6 小时插值后保留信息有限但还能用大于 6 小时整段删除插值会伪造平滑曲线异常值也要顺手处理。气象数据里偶尔会出现温度突然跳到 80 度的仪器故障值这种脉冲对回归模型伤害很大。我习惯用 24 小时窗口的中位数滤波med df[temp].rolling(24, centerTrue, min_periods12).median() mad (df[temp] - med).abs().rolling(24, centerTrue, min_periods12).median() df[temp] df[temp].where((df[temp] - med).abs() 4 * mad 1, med)这是 Hampel 滤波的简化版。窗口内取中位数再用中位数绝对偏差判断当前值是不是离群点是的话用中位数替换。4 * mad 1里的加 1 是绝对容差防止窗口内数值几乎全相等时 MAD 接近 0把正常波动误杀。这里必须强调这种平滑只处理特征列不要对标签列做。标签被平滑后模型学到的是「平滑过的世界」真实预测时根本不存在这种平滑RMSE 虚高就是这么来的。2.3 划分训练集和测试集按时间切不按随机 shuffle时间序列大作业里最容易被扣分、也最容易被忽略的就是数据集划分。很多教程里的train_test_split默认shuffleTrue直接用在天气数据上等于把未来混进训练。天气数据相邻行高度相关随机打乱后测试集里会出现大量「训练集样本的邻居」模型评估变成了开卷考试。split_date 2023-12-31 23:00:00 train df[df[date] split_date].reset_index(dropTrue) test df[df[date] split_date].reset_index(dropTrue) X_train, y_train train[FEATURES], train[target] X_test, y_test test[FEATURES], test[target]按时间切分后split_date要写进实验报告。留测试集多久也有讲究只留 3 天可能正好撞上连续晴天分数异常好看我一般留最近 30 天数据总量小就留最近 10 天。西瓜书里讲模型评估的「留出法」前提是样本独立同分布时间序列的样本之间有先后依赖直接套随机划分会在方法论上被扣分。注意train_test_split默认shuffleTrue时间序列数据不要直接拿它做默认划分。先看你的split_date在哪再说模型分数。划分方式测试集性质后果随机 shuffle测试集混入训练集相邻时刻分数虚高真实数据一测就打回原形按时间切分测试集完全在未来分数接近真实部署表现3. 预测天气的特征工程滞后、滑动窗口、周期编码一个都不能少数据干净后决定模型上限的是特征工程。天气预测和普通回归作业最大的差别在于特征之间有时间结构。只把今天的湿度、气压丢给模型去预测明天温度基本是在盲猜真正让模型「开窍」的是把过去的信息搬到当前时刻来。3.1 滞后特征为什么 lag_1 和 lag_24 是两套信息滞后特征就是把目标变量过去某个时刻的值当成当前时刻的特征。温度自相关很强今天晚上 8 点的温度既和昨晚 8 点有关也和今天下午 7 点有关。lag_1抓短期惯性lag_24抓昼夜周期lag_48和lag_72抓天气过程的持续性。for lag in [1, 24, 48, 72]: df[ftemp_lag_{lag}] df[temp].shift(lag)shift(lag)把整列向下移动 lag 行。当前行的temp_lag_24就是 24 小时前的温度。方向问题上shift(-1)会拿未来值这是时间泄漏的头号来源后面避坑章节细说。逐时数据里 lag 取 24 是因为一天 24 小时逐日数据就取 1 和 7「昨天」和「上周同一天」。特征名含义用途temp_lag_1上一小时温度短期惯性temp_lag_24昨天同一时刻温度昼夜周期temp_lag_48前天同一时刻温度天气过程持续性temp_lag_72三天前同一时刻温度中期趋势作业里常见的设计是「用过去 24 小时预测未来 24 小时」。这时要理解一个边界当前时刻的温度属于过去信息可以用但「当前时刻往后 24 小时」的滞后特征在推理时根本不存在。我一般会先把特征统一构造在「可观测」的时间位置上再切训练测试集避免模型在推理时找不齐特征。这里多花半小时想清楚后面少走一整晚弯路。3.2 滑动窗口特征把「过去 6 小时在升温还是降温」告诉模型滞后特征只给了单个时刻的快照气象变化看的却是过程。过去 6 小时温度一路下滑说明冷空气正在过境这比「此刻多少度」更能预示未来趋势。滑动窗口把一段时间的均值、标准差、最大最小值压缩成特征模型才能感知到趋势。# 训练阶段可以 centerTrue 减少相位偏移 df[temp_mean_6h] df[temp].rolling(6, centerTrue, min_periods3).mean() df[temp_std_6h] df[temp].rolling(6, centerTrue, min_periods3).std() df[temp_min_6h] df[temp].rolling(6, centerTrue, min_periods3).min() df[temp_max_6h] df[temp].rolling(6, centerTrue, min_periods3).max()centerTrue让窗口对齐到中心点减少特征相对真实时间的相位偏移min_periods3允许窗口内只有 3 个有效值也参与计算序列开头不会产生一堆 NaN。但如果你要做滚动预测或部署centerTrue会用到未来 3 小时的信息推理时这些值不存在。严格做法是训练时也改成右对齐加 shiftdf[temp].rolling(6).mean().shift(1)保证预测 t1 时只用 t 及之前的数据。单次切分评估时 center 的偏差看不出来一旦做多步滚动问题立刻暴露。再补一个温差特征比单纯滞后更直观df[temp_diff_24h] df[temp] - df[temp_lag_24]24 小时温差直接反映天气过程在回暖还是降温模型学这个比同时看两个滞后列容易得多。这个特征在冷空气过境、连续降温的场景下重要性往往排进前三。3.3 时间周期编码小时和季节要用 sin/cos不要用 0~23 的整数时间本身是最被低估的特征。小时 0 和 23数值上差 23但气象意义上只差 1 小时。直接用「小时 13」这种整数喂给线性模型模型会认为 0 和 23 离得很远学出错误的关系。把小时和儒略日映射到圆周上用 sin/cos 两列表达周期位置是目前最稳的编码方式。import numpy as np hour df[date].dt.hour df[date].dt.minute / 60 doy df[date].dt.dayofyear df[hour_sin] np.sin(2 * np.pi * hour / 24) df[hour_cos] np.cos(2 * np.pi * hour / 24) df[doy_sin] np.sin(2 * np.pi * doy / 365) df[doy_cos] np.cos(2 * np.pi * doy / 365)分母是周期长度。逐时数据小时周期是 24年度周期是 365逐日数据就不用做小时编码了但可以加「星期几」的 one-hot。sin 和 cos 必须成对使用只用 sin 会在 0 点和 24 点之间产生断裂。树模型对这种编码不像线性模型那么敏感但做了总比不做稳尤其你还要用线性回归当基线。降雨量这类偏态特征我也单独处理大部分小时降雨量为 0直接把原始降雨量喂给线性模型会造成特征分布极偏。我一般额外构造一个「是否有降雨」的 0/1 特征树模型可以同时吃这两个线性模型只用 0/1 那列。4. 机器学习模型怎么选线性回归打底树模型做主力特征工程做完才轮到模型。天气预测的作业里模型选型有个明确的优先级先线性回归验证特征再随机森林和 XGBoost 做主力深度学习放在最后考虑。直接上 LSTM 不是不行但课程设计阶段解释成本高、调参成本更高表格型特征用树模型往往更划算。4.1 线性回归先跑通它是特征工程的对错标尺线性回归在天气预测上通常是弱基线但它便宜、可解释、对特征工程的问题非常敏感。如果线性回归分数一塌糊涂大概率是特征没构造对而不是模型不够强。反过来如果线性回归远好于「用昨天温度直接预测」的基线说明滞后特征和周期编码起作用了再接树模型才有意义。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, mean_absolute_error lr LinearRegression() lr.fit(X_train, y_train) y_pred lr.predict(X_test) rmse mean_squared_error(y_test, y_pred, squaredFalse) mae mean_absolute_error(y_test, y_pred) print(fLinearRegression: RMSE{rmse:.2f}, MAE{mae:.2f})mean_squared_error(..., squaredFalse)返回 RMSE。新版 sklearn 里这个参数有变动换成root_mean_squared_error更稳具体看环境版本。线性回归对特征尺度不敏感但解释系数时需要量纲一致如果后面要加 Ridge 或 Lasso 正则先做StandardScaler再训练。这里要泼一盆冷水线性回归很难直接赢到最后但它能告诉你特征工程有没有做对。如果 RMSE 和用昨天温度直接预测差不多说明特征里没有提供足够的新信息回到第 3 章找原因而不是急着换模型。4.2 随机森林和 XGBoost表格型天气数据上的主力模型天气数据洗干净后是典型的结构化表格随机森林几乎不用调参就能跑适合当第一版XGBoost 上限更高但参数要稍微收紧。先说随机森林from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth10, min_samples_leaf3, n_jobs-1, random_state42, ) rf.fit(X_train, y_train) rf_pred rf.predict(X_test)max_depth10控制单棵树深度太深容易过拟合min_samples_leaf3强制叶子节点至少 3 个样本是防止回归树在极端样本上钻牛角尖的最直接手段n_jobs-1用满 CPU 核心。这三个参数在几万行的天气数据上够用不需要再折腾。XGBoost 的关键参数建议这么设import xgboost as xgb xgb_model xgb.XGBRegressor( n_estimators300, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_lambda1.0, random_state42, early_stopping_rounds20, ) xgb_model.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse, )参数建议范围作用n_estimators200~500树的数量配 early stopping 使用max_depth4~8树的深度过大必过拟合learning_rate0.02~0.1步长越小越稳但需要更多树subsample0.7~0.9每棵树行采样比例colsample_bytree0.7~0.9每棵树特征采样比例reg_lambda1.0~2.0L2 正则early_stopping_rounds20是验证集指标连续 20 轮不提升就停比手动固定树数量省心得多。注意 xgboost 版本对early_stopping_rounds的写法有变化老版本要单独传evals新版本可以直接传eval_set跑之前看下环境里的版本。树模型有一个边界必须知道它无法外推。如果测试期温度超过训练期的历史极值预测值会被拉向训练集里最接近的样本。树模型擅长在见过的范围内插值不擅长预测破纪录的天气。作业里如果测试阶段正好赶上异常高温这个缺陷会在残差里非常显眼。4.3 评估指标RMSE、MAE以及「比昨天直接预测强多少」作业报告里常见只写一个 RMSE。RMSE 对大误差敏感测试期只要有一天气温突降 8 度RMSE 会被拉高一截MAE 受单个极端样本影响小得多。两个指标一起报能看出误差是「普遍偏 1~2 度」还是「大部分时候准、偶尔差得离谱」。更重要的指标是和基线比提升多少。天气预测最简单的基线就是「用昨天的温度直接当今天的预测」不需要任何机器学习。模型如果连这个基线都打不过说明整个流程里有环节出了问题。baseline_rmse mean_squared_error(y_test, test[temp_lag_24], squaredFalse) improve (baseline_rmse - rmse) / baseline_rmse * 100 print(f模型 RMSE{rmse:.2f}, 基线 RMSE{baseline_rmse:.2f}, 提升 {improve:.1f}%)test[temp_lag_24]如果因为切分出现前 24 行为 NaN要先test[temp_lag_24] test[temp].shift(24)或者评估时从第 24 行开始。这个细节写进报告很加分。提升超过 15% 基本说明模型学到了真实信号如果是负数优先回头查下一章的两个泄漏问题而不是调参。5. 预测天气的五个常见问题现象、原因、解决方案下面这五个问题是我在课程设计和帮别人排查大作业时踩过、也看别人踩过无数次的坑。每一条按「现象 → 原因 → 解决」整理可以直接对着自己的结果自查。5.1 时间泄漏滞后特征里混进了未来数据现象验证集 RMSE 低到离谱比如误差都在 0.1 度以内看起来已经「完美预测天气」。换个时间段再测模型立刻失效。原因最常见的是shift(-1)把未来数据拉进了特征。构造滞后特征时方向写反当前行的temp_lag_1实际是下一小时的温度模型相当于在测试时直接抄答案。另一个来源是滚动窗口用了centerTrue没做 shift窗口右边界伸到了未来。解决写一个泄漏自检看训练集最后一行的特征和目标值的关系check X_train.tail(1) print(check[[temp_lag_1, temp_lag_24]]) print(y_train.tail(1))训练集最后一行的滞后特征对应的应该是「更早时刻」的值如果temp_lag_24的数值和目标值几乎完全一致shift 方向一定反了。我一般在构造完所有特征后把这两行打出来看一遍确保特征时间早于目标时间再往下走。5.2 随机打乱训练集验证分数虚高的头号元凶现象用train_test_split(X, y, test_size0.2)跑出来的分数很高提交给评分系统或换一段真实未来数据后分数完全对不上。原因这个函数默认shuffleTrue。天气数据相邻样本高度相关随机打乱后测试集里大量样本的相邻时刻出现在训练集里。模型在评估时见过它们等于开卷考试。这是时间序列数据在通用机器学习教程里最常被误用的地方。解决按时间点切分并把split_date写进实验报告。如果要做更严格的评估用时间序列交叉验证把数据按时间顺序分成 K 段每次只用更早的段训练、更晚的段验证逐段滚动。大作业阶段一次时间切分通常够用K 折时间交叉验证留给数据更充裕的场景。简单判断法如果随机划分下的 RMSE 明显好于时间划分下的结果先怀疑评估方式而不是模型本身。5.3 递归预测误差累积一步准、三步飘现象单步评估 RMSE 2.5 度看起来很健康但作业要求预测未来 7 天画出来的曲线从第 3 天开始明显发散误差一天比一天大。原因多步预测有两种常见做法。一种是一次性输出未来 K 个时刻另一种是递归——把上一步的预测值回填进特征再预测下一步。递归方式里第一步的误差会作为输入进入第二步误差像滚雪球一样放大。天气系统本身对初始条件敏感累积误差是正常现象但模型的滞后特征越强这种放大越明显。解决把评估口径分开。单步 RMSE 和滚动多步 RMSE 都报告让评分人看到模型的能力边界。如果作业允许用直接预测策略训练 K 个模型分别预测未来第 1、2……K 小时每个模型只做一步。这样第 3 小时的预测不会吃第 2 小时预测误差的累积。代价是训练 K 个模型但课程设计的数据量通常不大完全跑得动。5.4 模型退化成「预测昨天」分数不差但曲线整体错位现象RMSE 不高但把预测值和真实值画在一起发现预测曲线像是真实曲线整体向右平移了半天或一天模型输出的基本是昨天同一时刻的温度。原因滞后 24 小时特征信息量太大模型偷懒学了「今天的温度和昨天差不多」这条捷径。天气平稳时这条捷径很有效但冷空气一来就全线崩溃。树模型在特征重要性里会把temp_lag_24排到第一这本身就是信号。解决做一个无滞后对照实验。临时去掉temp_lag_24和temp_lag_48只保留短期滞后和其他气象特征重训一版。如果分数几乎不变说明模型本质上在复制昨天如果分数明显变差说明模型在依赖周期信息的同时也在学新东西。看残差也行按小时分组统计残差如果出现系统性偏移比如固定高估后半夜、低估白天说明模型没学到昼夜温差变化。5.5 验收标准和指标不匹配你报的 RMSE 不是老师要的 MAE现象作业报告写了 RMSE3.2 度自我感觉良好老师评分标准是「预测最高气温的绝对误差 2 度以内算合格」最后分数很难看。原因天气预测大作业的验收口径往往比一个 RMSE 复杂。有的课程设计看重未来 7 天最高温的 MAE有的看是否捕获降温过程有的要求画预测曲线并做误差分析。RMSE 对极端天气敏感一次强冷空气就可能让 RMSE 从 2.0 跳到 3.5但 MAE 只小幅上升。解决动手前先确认选题的验收口径。没有明确口径时把 RMSE、MAE、基线提升率三个指标一起报附一段测试期最差一天的案例分析说明误差最大那天发生了什么。预测目标定义成「逐时温度」还是「每日最高温」模型难度和评估方式差很多这属于课程设计选题阶段就要想清楚的事。6. 拿到预测结果后用滚动验证给模型做终检单次切分不可信前面的训练/测试一次性切分只能证明模型在「某一个未来时间段」表现如何。天气有季节性一次切分可能正好落在平稳期分数虚高也可能正好撞上强对流分数虚低两种都不能代表真实水平。更稳的验证方式是把测试集按 7 天一组切块每一块都用它之前的所有数据训练预测完后窗口向前滚动。roll_pred, roll_true [], [] for start in range(0, len(test) - 7 * 24, 7 * 24): train_part pd.concat([train, test.iloc[:start]]) test_part test.iloc[start:start 7 * 24] model xgb.XGBRegressor( n_estimators200, max_depth6, learning_rate0.05, random_state42, ) model.fit(train_part[FEATURES], train_part[target]) pred model.predict(test_part[FEATURES]) roll_pred.extend(pred) roll_true.extend(test_part[target].tolist()) roll_rmse mean_squared_error(roll_true, roll_pred, squaredFalse)7 * 24是 7 天逐时数据的行数换成逐日数据就是 7test.iloc[:start]把已经「过去」的测试段并入训练模拟真实上线时每天都能拿到新数据的场景。这个循环每轮重训一个模型数据量不大时完全跑得动。数据量大就固定模型只滚动预测不重训速度快很多但结果会略保守。验证完把滚动 RMSE 和单次切分的 RMSE 放一起对比。两者差在 5% 以内说明模型稳定滚动验证明显更差说明第一次切分恰好选了一段好预测的天气模型实际没那么强。不要只盯着 RMSE 这个黑匣子——把滚动预测的残差按天画出来看看误差是不是集中在某几类天气过程里这比调参更能说明问题。我自己在课程设计里就吃过这个亏交稿前单次切分 RMSE 很漂亮答辩现场换了一段数据后成绩直接掉了一大截。从那以后时间序列任务我再也不只看一次切分的结果所有评估都改成滚动验证才敢拿出手。希望这个习惯能帮你在机器学习大作业里少走一段弯路。本文还有配套的精品资源点击获取