ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

天气预测实战:从数据清洗到模型部署的完整链路

2026/9/26 18:03:10 拓冰建站 浏览量
天气预测实战:从数据清洗到模型部署的完整链路 简介这份资源面向数据科学竞赛参与者、机器学习初学者及气象数据分析爱好者围绕天气预测这一典型时间序列建模任务提供了一套完整的竞赛项目文件。压缩包共9个文件约7.12MB包含csv格式的历史气象观测数据、h5模型文件、py处理脚本以及xml与iml等工程配置文件覆盖从数据读取、清洗到建模的完整流程。目前已有634人学习下载适合希望借助真实赛题练手、理解气象数据结构的读者。通过其中的训练数据与脚本读者可以实践缺失值与异常值处理、滑动窗口与季节性特征构造并尝试线性回归、随机森林、SVM及LSTM等模型对温度、降雨概率、风速等指标进行预测同时借助MSE、RMSE、MAE与R²等指标评估效果配合交叉验证与参数调优理解模型泛化能力从而系统掌握天气预测项目的建模思路与工程组织方式。1. 天气预测.rar一个压缩包背后藏着从数据到预报的完整链路拿到「天气预测.rar」这个标题的人大概率是在找一份能跑起来的天气预测代码或数据集。它可能是一个课程设计打包、一次 Kaggle 练手项目的归档也可能是某个气象站历史数据的压缩备份。不管里面装的是什么核心诉求都一样把历史气象数据喂给模型让它输出未来某一天的温度、湿度或者是否下雨。这件事听起来像玄学但拆开看就是一套标准的时序预测流程——数据清洗、特征构造、模型选型、滚动预测、误差评估。适合谁适合已经会 Python 基础、想找一个完整小项目练手的人也适合需要快速搭一个气象预测原型、验证业务可行性的工程师。下面我按自己实际做过的路径把这个压缩包该有的东西和该走的步骤讲清楚。2. 解压之后先别急着跑模型气象数据的清洗与特征构造2.1 压缩包里通常有什么以及第一眼该看什么一个典型的天气预测压缩包解压后大概率是这几类文件CSV 或 Excel 格式的历史观测数据、一个 Jupyter Notebook 或几个 .py 脚本、可能还有一份 README 或者 requirements.txt。历史数据常见的字段包括日期时间、气温、气压、湿度、风速、风向、降水量、天气现象编码。第一眼不要看代码先看数据的时间跨度和采样频率。如果只有三个月的数据做年度预测就是开玩笑如果是逐小时数据但缺了 30% 的湿度值后面所有模型都会带病工作。我一般会先跑一段极简的探查脚本把时间范围、缺失率、异常值分布一次性打出来。这一步花十分钟能省掉后面两小时的翻车排查。import pandas as pd import numpy as np # 读取压缩包解压后的主数据文件假设是 CSV df pd.read_csv(weather_history.csv, parse_dates[datetime]) df df.sort_values(datetime).set_index(datetime) # 基础探查时间范围、采样间隔、缺失率 print(时间范围:, df.index.min(), 到, df.index.max()) print(采样间隔统计:\n, df.index.to_series().diff().value_counts().head()) print(各列缺失率:\n, df.isnull().mean().sort_values(ascendingFalse)) # 数值列描述统计快速看异常值 print(df.describe().T[[mean, std, min, max]])这段代码的逻辑很直接先确保时间列被解析成 datetime 并设为索引然后看 diff 的分布确认采样是否均匀。如果 diff 里出现大量非 1 小时的值说明数据有断档或者混入了不同来源的记录。缺失率超过 20% 的列要么放弃要么用插值补但插值前必须确认缺失是随机的还是成片的——连续缺失超过 6 小时的线性插值会制造虚假的平滑段我一般直接标记为 NaN 让模型自己处理。参数上parse_dates要指定正确的列名不同来源的 CSV 可能叫 time、date、observation_time。如果时间列是字符串且格式不统一先用pd.to_datetime(df[time], formatmixed)兜底再检查有没有解析失败的 NaT。2.2 特征构造把原始气象记录变成模型能吃的时序样本原始数据里只有当前时刻的观测值但预测未来温度靠的是过去一段时间的趋势和周期。常见做法是构造三类特征滞后特征、滑动窗口统计量、时间编码。滞后特征就是 t-1、t-2、t-3 时刻的温度、气压等滑动窗口统计量是过去 6 小时、12 小时、24 小时的均值、最大值、最小值、标准差时间编码是把小时、星期、月份做 sin/cos 变换让模型理解周期性。# 构造滞后特征和滑动窗口特征 target_col temperature lags [1, 2, 3, 6, 12, 24] for lag in lags: df[f{target_col}_lag_{lag}] df[target_col].shift(lag) # 滑动窗口统计量 for window in [6, 12, 24]: df[f{target_col}_roll_mean_{window}] df[target_col].rolling(window).mean() df[f{target_col}_roll_std_{window}] df[target_col].rolling(window).std() df[f{target_col}_roll_max_{window}] df[target_col].rolling(window).max() df[f{target_col}_roll_min_{window}] df[target_col].rolling(window).min() # 时间周期编码 df[hour_sin] np.sin(2 * np.pi * df.index.hour / 24) df[hour_cos] np.cos(2 * np.pi * df.index.hour / 24) df[month_sin] np.sin(2 * np.pi * df.index.month / 12) df[month_cos] np.cos(2 * np.pi * df.index.month / 12) # 去掉因为 shift 和 rolling 产生的 NaN 行 df df.dropna() print(构造后特征维度:, df.shape)滞后阶数的选择要看预测跨度。如果预测未来 1 小时温度滞后 1 到 6 小时足够如果预测未来 24 小时至少要有 24 小时前的滞后和 24 小时窗口的统计量。滑动窗口的 std 和 max/min 能捕捉突变比如冷锋过境前气压骤降、风速突增这些是纯滞后特征抓不到的。时间编码用 sin/cos 而不是直接放小时数字是为了避免模型把 23 点和 0 点当成相距很远的值。注意rolling 之后 dropna 会丢掉前 24 行如果数据总量本来就少要权衡是否值得。我一般会保留这些行但把窗口统计量填成 NaN让后面的模型用掩码处理而不是直接删。3. 模型选型从线性回归到梯度提升天气预测到底该用哪个3.1 先跑一个基线线性回归和持久性模型任何时序预测项目第一步都是建立基线。天气预测里最朴素的基线叫持久性模型——预测未来温度等于当前温度。别笑很多复杂模型在短时预测上打不过这个基线。第二个基线是线性回归用上面构造的滞后和窗口特征直接拟合目标值。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error from sklearn.model_selection import TimeSeriesSplit # 按时间顺序切分不能随机打乱 split_idx int(len(df) * 0.8) train, test df.iloc[:split_idx], df.iloc[split_idx:] feature_cols [c for c in df.columns if c ! target_col] X_train, y_train train[feature_cols], train[target_col] X_test, y_test test[feature_cols], test[target_col] # 持久性基线 persistence_pred test[target_col].shift(1).dropna() persistence_mae mean_absolute_error(y_test[1:], persistence_pred) print(f持久性基线 MAE: {persistence_mae:.2f}) # 线性回归 lr LinearRegression() lr.fit(X_train, y_train) lr_pred lr.predict(X_test) print(f线性回归 MAE: {mean_absolute_error(y_test, lr_pred):.2f}) print(f线性回归 RMSE: {np.sqrt(mean_squared_error(y_test, lr_pred)):.2f})切分必须按时间顺序用 TimeSeriesSplit 或者手动按比例切。随机切分会让未来信息泄漏到训练集导致测试指标虚高上线后直接翻车。持久性基线的 MAE 如果比线性回归还低说明特征构造有问题或者预测跨度太短复杂模型没有发挥空间。3.2 梯度提升树和 LSTM什么时候值得上复杂模型线性回归跑通之后如果 MAE 满足不了需求再考虑 XGBoost、LightGBM 或者 LSTM。梯度提升树对表格型时序特征很友好训练快、调参相对直观、能输出特征重要性。LSTM 适合捕捉长程依赖但需要更多的数据量和调参耐心而且容易过拟合。import lightgbm as lgb # LightGBM 训练用早停防止过拟合 train_data lgb.Dataset(X_train, labely_train) valid_data lgb.Dataset(X_test, labely_test, referencetrain_data) params { objective: regression, metric: mae, boosting_type: gbdt, num_leaves: 31, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1 } model lgb.train( params, train_data, num_boost_round1000, valid_sets[valid_data], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] ) lgb_pred model.predict(X_test, num_iterationmodel.best_iteration) print(fLightGBM MAE: {mean_absolute_error(y_test, lgb_pred):.2f}) # 特征重要性看哪些特征真正在起作用 importance pd.DataFrame({ feature: feature_cols, importance: model.feature_importance(importance_typegain) }).sort_values(importance, ascendingFalse) print(importance.head(15))LightGBM 的关键参数里num_leaves控制模型复杂度31 是起步值数据量小就降到 15 以下learning_rate设 0.05 配合早停比 0.1 更稳feature_fraction和bagging_fraction做行采样和列采样防止过拟合。早停的 50 轮意味着验证集 MAE 连续 50 轮不下降就停这是防止过拟合的后悔药。特征重要性输出后如果排名靠前的全是滞后 1、2 的特征说明模型主要在抄最近的值对长程预测帮助有限。这时候要检查目标定义——如果预测的是未来 1 小时滞后 1 当然最重要如果预测未来 24 小时滞后 24 和窗口 24 的统计量应该排前面。LSTM 的输入需要做成三维张量样本数、时间步长、特征数我一般用 24 或 48 个时间步。但 LSTM 在气象数据上不一定比 LightGBM 好尤其是数据量低于 5 万条时调参成本高、收益不稳定。我的习惯是先用 LightGBM 把特征重要性摸清楚再决定要不要上 LSTM。4. 滚动预测与误差评估别用单次划分骗自己4.1 滚动预测的实现一步预测和多步预测的区别单次划分测试集只能看一个时间段的表現天气预测更关心模型在不同季节、不同天气过程下的稳定性。滚动预测的做法是用过去 N 天训练预测下一天然后窗口向前滑动重复这个过程。这样能模拟真实上线后的逐日更新场景。def rolling_forecast(df, feature_cols, target_col, train_days365, test_days30): 滚动预测每次用过去 train_days 训练预测下一天 results [] total_days len(df) // 24 # 假设逐小时数据 for day in range(train_days, total_days - test_days): train_start (day - train_days) * 24 train_end day * 24 test_start day * 24 test_end (day 1) * 24 train df.iloc[train_start:train_end] test df.iloc[test_start:test_end] model lgb.LGBMRegressor( num_leaves31, learning_rate0.05, n_estimators500, feature_fraction0.8, bagging_fraction0.8, verbose-1 ) model.fit(train[feature_cols], train[target_col]) pred model.predict(test[feature_cols]) results.append({ date: test.index[0].date(), mae: mean_absolute_error(test[target_col], pred), rmse: np.sqrt(mean_squared_error(test[target_col], pred)) }) return pd.DataFrame(results) # 执行滚动预测 rolling_results rolling_forecast(df, feature_cols, target_col) print(rolling_results.describe()) print(MAE 最高的 5 天:\n, rolling_results.nlargest(5, mae))这段代码的核心是每次只用当天之前的数据训练预测当天然后窗口前移。train_days 设 365 是希望模型见过完整的季节周期如果数据不足一年就设成数据总天数的 70%。test_days 控制滚动次数30 次能看出月度波动365 次能看全年。输出里 MAE 最高的几天往往是天气突变日比如寒潮、台风、强对流这些样本对模型来说是黑匣子需要单独分析。4.2 评估指标怎么选MAE、RMSE 和业务容忍度MAE 和 RMSE 的区别在于对极端误差的惩罚。RMSE 对大误差更敏感如果业务上不能接受某天温度预测偏了 5 度就用 RMSE 做主指标如果更关心平均表现MAE 更直观。我一般两个都看再补一个「误差在 2 度以内的天数占比」这个指标对业务方最好解释。# 计算误差在 2 度以内的占比 rolling_results[within_2c] rolling_results[mae] 2.0 print(f误差在 2°C 以内的天数占比: {rolling_results[within_2c].mean():.1%}) # 按月份看误差分布检查季节性偏差 rolling_results[month] pd.to_datetime(rolling_results[date]).dt.month monthly_mae rolling_results.groupby(month)[mae].mean() print(各月平均 MAE:\n, monthly_mae)如果某个月 MAE 明显高于其他月说明模型对那个季节的天气模式学习不足。比如夏季对流天气多、温度突变频繁模型如果只学了平滑趋势就会在那几个月翻车。解决办法是在特征里加入天气现象编码、气压变化率、露点温度差等能反映对流潜势的变量。提示滚动预测的计算量是单次划分的几十倍如果数据量大可以先用月度滚动代替逐日滚动快速看趋势再对问题月份做逐日细查。5. 避坑与排查天气预测项目里最容易翻车的 5 个地方5.1 时间泄漏特征里混入了未来信息现象测试集 MAE 低得离谱上线后误差翻倍。原因构造滑动窗口时用了centerTrue或者做归一化时用了全量数据的均值和方差。解决所有滚动统计必须用closedleft或者 shift 后再 rolling归一化参数只能从训练集计算再应用到测试集。5.2 缺失值插值过度把断档补成了假数据现象模型在数据断档后的第一天预测误差极大。原因连续缺失超过 6 小时用线性插值补出了一条直线模型学到了错误的平稳模式。解决连续缺失超过阈值比如 3 小时的段标记为独立特征或直接剔除该段样本不要强行插值。5.3 目标泄漏预测目标参与了特征构造现象特征重要性里目标列的某个滞后排第一但预测长跨度时表现很差。原因预测未来 24 小时温度却把 t-1 的温度作为特征而 t-1 在预测时根本不可知。解决明确预测起点所有特征的时间戳必须早于预测起点。多步预测时要么用递归预测把预测值当输入要么直接预测目标时刻不要混用。5.4 评估指标选错MAE 好看但业务不可用现象MAE 只有 1.5 度但业务方说预报经常不准。原因MAE 被大量平稳天气拉低了极端天气的误差被平均掉了。解决补充分位数误差比如 90% 分位误差和极端天气子集的 MAE单独看寒潮、高温、暴雨日的表现。5.5 模型更新频率训练一次用一年现象模型上线前三个月还行后面越来越差。原因气候背景和观测设备会漂移旧模型没跟上。解决设定滚动更新周期比如每月用最近 12 个月数据重新训练或者用在线学习方式增量更新。更新后先在影子模式跑一周对比新旧模型再切换。6. 把预测结果变成可用的预报后处理与置信区间模型输出的是一个点估计但实际预报需要给出范围。我一般用分位数回归或者对残差做 Bootstrap 来生成置信区间。LightGBM 支持objectivequantile分别训练 0.1、0.5、0.9 三个分位点就能得到 80% 置信区间。# 训练分位数模型输出预测区间 quantiles [0.1, 0.5, 0.9] models {} for q in quantiles: params_q params.copy() params_q[objective] quantile params_q[alpha] q models[q] lgb.train(params_q, train_data, num_boost_round500, valid_sets[valid_data], callbacks[lgb.early_stopping(30)]) # 预测并组装区间 preds {q: models[q].predict(X_test) for q in quantiles} result_df pd.DataFrame({ pred_median: preds[0.5], lower_80: preds[0.1], upper_80: preds[0.9], actual: y_test.values }) result_df[covered] (result_df[actual] result_df[lower_80]) \ (result_df[actual] result_df[upper_80]) print(f80% 置信区间覆盖率: {result_df[covered].mean():.1%})覆盖率应该接近 80%如果只有 60%说明区间太窄模型低估了不确定性如果 95%区间太宽预报没有区分度。调整方法是增加分位数模型的训练轮数或者放宽 alpha 的对称性。后处理还有一个技巧对预测结果做滑动平均。天气本身有惯性模型输出的逐小时预测可能抖动过大用 3 小时滑动平均平滑一下MAE 通常能降 5% 到 10%。但平滑窗口不能太大否则会抹掉锋面过境的突变信号。最后说一个我自己的习惯每次跑完滚动预测我会把误差最大的 10 天单独拉出来对照当天的天气图看是什么系统过境。十次里有八次能发现模型漏掉了某个关键特征比如露点差、海平面气压变化率、或者风向的突变。把这些特征补进去下一轮滚动预测的极端误差就会明显收窄。天气预测没有一劳永逸的模型只有不断对照实况、补特征、调参数的循环。希望帮到你。本文还有配套的精品资源点击获取