前一段时间有个做户外活动的朋友跟我吐槽,说活动日期老是撞上天气突变,平台上的天气预报又不准,一场活动说取消就取消,损失不小。我当时就在想,与其眼巴巴等着别人给的预报,不如自己抓数据、自己分析、自己做个小范围的气温预测模型。于是就有了这个“基于Python的天气数据分析预测系统”。说白了,就是自己动手写一套从数据采集、清洗、分析到建模预测的完整流程,最后用图表把未来几天的趋势画出来。这套东西特别适合刚学完Python基础、想找实战项目的同学,也适合工作中需要做数据分析和简单预测的人,比如活动策划、物流调度、农业种植、电力负荷估算这些场景。我先把整套项目的设计和核心代码逻辑拆开讲一遍,再把我实际踩过的坑和排查思路整理出来,希望能帮你少绕弯路。
1. 项目整体设计与思路拆解
1.1 先搞清楚预测系统到底要做什么
很多人一上来就想搞深度学习、神经网络,其实没必要。天气预测这事,如果只做短期气温趋势,传统机器学习模型完全够用,而且训练快、可解释性强,出了问题也容易定位。我这个系统核心就干四件事:抓取历史天气数据、清洗数据、构造特征、训练模型并预测未来气温走势。目标很明确,不要贪多,先做一个能用的最小闭环。
确定了目标之后,就要考虑数据从哪来。常见的路子有三种:爬第三方天气网站、调用免费天气API、找现成的历史数据集。我当时为了练手,先用了requests库直接爬公开天气页面,后来又换了免费的Open-Meteo API做数据源。爬虫的好处是灵活,什么字段都能抓;坏处是容易被反爬限制,而且要考虑对方网站的结构变化。API的好处是数据格式规整、稳定,通常有历史数据接口也有未来预报接口,省掉不少解析功夫。如果你只是想做分析建模,我建议直接用API,别跟反爬较劲。
1.2 方案选型:为什么用Python这一套组合
这个项目的技术栈不算复杂:Python做主语言,requests或者urllib做数据采集,pandas做数据处理,scikit-learn做模型训练,matplotlib做可视化。有人会问,为什么要用pandas而不是直接操作列表?因为天气数据是典型的结构化表格数据,有日期、最高温度、最低温度、天气现象、湿度、风速等字段,pandas的DataFrame天生就是干这个的。筛选、分组、滞后字段生成、缺失值填充,十行以内就能搞定,如果用纯Python列表,这些操作会让你写到怀疑人生。
至于为什么选scikit-learn而不是自己手写模型,或者上深度学习框架,原因也很实际:数据量就几百条到几千条,用线性回归、随机森林这类经典模型已经能取得不错的效果。深度学习需要的数据量和调参成本太高,模型解释性是负资产,对一个小系统来说纯属杀鸡用牛刀。我后续会专门说模型的选择,这里先不展开。
1.3 系统架构和模块划分
整个系统我拆成了四个模块:采集模块负责把历史数据和未来预报数据拉下来,清洗模块做格式规范、缺失值处理、异常值剔除,特征工程模块负责把日期转换成星期、月份、滞后温度差等特征,模型与输出模块负责训练、评估和画图。这样做的好处是每一块都可以独立调试,爬虫挂了不影响建模,清洗逻辑改了模型也能继续用。我见过不少新手把一个系统所有代码堆在一个脚本里,一旦出了问题,改一处崩一片,排查起来特别痛苦。所以从项目一开始,就要养成模块化的习惯。
2. 环境准备与数据获取
2.1 Python环境搭建和依赖安装
这一步看似基础,但很多人卡得莫名其妙。如果你电脑上装的是Python 3.8以上版本,其实大部分库都能直接支持。我建议你用虚拟环境,别把包一股脑装到全局环境里,不然不同项目依赖冲突起来会让人崩溃。用venv或者conda都行,我个人习惯用conda,因为它在Windows下处理某些库的底层依赖更方便。
装包的时候,国内用户直接用pip install容易超时或者下载慢,我建议配置一下国内镜像源。比如在命令行里执行:
pip install pandas scikit-learn matplotlib requests -i https://pypi.tuna.tsinghua.edu.cn/simple这里我踩过一个坑:新电脑上直接pip install scikit-learn,结果因为Python版本是3.12,某几个旧版本的依赖编译不兼容,报了一堆红字。后来换成了指定版本号,比如scikit-learn==1.3.2,问题就解决了。所以装包的时候,如果遇到编译错误,优先检查Python版本和库版本是否匹配,不要盲目升级到最新版。
2.2 数据源选择:API和爬虫怎么权衡
我最初是用爬虫去抓天气网站的,抓到的是HTML页面,需要解析字段,代码里充满了各种find和正则,维护成本很高。后来换成Open-Meteo,这个API无需密钥,支持历史数据和未来预报,返回JSON格式,还允许指定经纬度、地区、时间范围和需要的气象变量。调用方式非常简单,就是一个返回JSON的URL:
https://archive-api.open-meteo.com/v1/archive?latitude=39.90&longitude=116.40&start_date=2023-01-01&end_date=2023-12-31&daily=temperature_2m_max,temperature_2m_min&timezone=Asia%2FShanghai返回内容用requests.get()拿下来,再调json()方法解析,直接就能转成DataFrame。这样数据获取的时间从一个多小时压缩到几十秒,稳定性也大大提升。如果你非要用爬虫,我提醒一句:一定要设置合理的请求头和访问频率,不然很容易被对方拒绝访问,严重的还可能被拉黑IP。
2.3 采集模块的具体实现
我用requests写了一个简单的封装函数,只干一件事:给定开始日期、结束日期、经纬度,返回一个清洗好的DataFrame。核心代码大概是这样的:
import requests import pandas as pd def fetch_weather_history(latitude, longitude, start_date, end_date): url = "https://archive-api.open-meteo.com/v1/archive" params = { "latitude": latitude, "longitude": longitude, "start_date": start_date, "end_date": end_date, "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max", "timezone": "Asia/Shanghai" } resp = requests.get(url, params=params, timeout=30) resp.raise_for_status() data = resp.json()["daily"] df = pd.DataFrame(data) df["date"] = pd.to_datetime(df["time"]) df = df.drop(columns=["time"]) return df这一步有个小细节容易被忽略:不管API返回的日期字段是字符串还是时间戳,都要显式转换成pandas的Datetime类型,不然后面按日期分组、画图时横轴排序都是乱的。我自己刚开始就是没转换,结果画出来的趋势线跟波浪线一样,上下乱跳,找了好久才定位到是日期类型的问题。
3. 数据清洗与特征处理
3.1 用DataFrame摸清数据家底
数据拿到手之后,第一步不是急着建模,而是先做全面检查。我习惯用df.head()、df.info()、df.describe()三件套看数据长什么样。df.info()能看出每列有没有缺失值、数据类型对不对,df.describe()能看到温度、降水、风速这些数值列的分布,比如最大值是不是明显离谱。这一步能帮你提前发现数据问题,省得建模出来效果莫名其妙。
比如有一次我拉到的数据里出现了-999这样的填充值,明显是API缺失值的占位符。如果不清理,模型就会把-999当作真实温度样本,预测结果直接跑偏。这种情况就需要用np.where把-999替换成NaN,然后再统一做缺失值处理。
3.2 缺失值和异常值处理
缺失值处理的原则是:不能不做,但也不能乱做。对于温度这种连续变量,我一般用前后几天的均值填充,或者直接用ffill方法向上填充,因为温度在短期内变化是连续的,前一天和后一天的温度往往比较接近。如果你用整个列的平均值填充,遇到季节变化大的情况,反而会把数据搞失真。代码很简单:
df["temperature_2m_max"] = df["temperature_2m_max"].replace(-999, pd.NA) df["temperature_2m_max"] = df["temperature_2m_max"].ffill().bfill()异常值的判断我倾向于先用可视化的方式看分布,比如画个箱线图,如果有温度在夏天出现-30度这种明显不合理的点,就直接剔除。判断标准可以结合实际地区的气候范围,不用搞太复杂的算法,毕竟天气数据质量整体还是比较靠谱的。
3.3 构造特征:让模型有更多线索可挖
很多新手忽略特征工程,直接把原始日期和温度丢给模型,效果自然很一般。我做的第一版模型也是这么干的,预测出来的结果基本是前一天数值的平移,完全体现不出变化趋势。后来我在特征里加入了星期、月份、季节、前一天的温差,甚至几日滑动平均,效果立刻不一样了。
构造特征的核心思路是:让模型能识别出周期性规律。比如月份这个特征,对模型来说是一月份还是六月份,温差差异非常明显。星期特征则能捕捉周中和周末的细微差异。代码上就是几行pandas的操作:
df["month"] = df["date"].dt.month df["day_of_week"] = df["date"].dt.dayofweek df["temp_diff"] = df["temperature_2m_max"] - df["temperature_2m_min"] df["prev_max"] = df["temperature_2m_max"].shift(1) df["rolling_avg_3"] = df["temperature_2m_max"].rolling(3).mean()这里注意shift(1)生成的prev_max,在数据第一行会变成NaN,所以用完滞后特征之后需要再清洗一次缺失值。滚动均值是很好的平滑特征,能把噪声去掉,让模型学到的趋势更稳健。
4. 模型构建与预测评估
4.1 划分数据集:别让未来数据偷看答案
建模前最重要的一件事就是划分训练集和测试集,但天气数据跟普通表格数据不一样,它是有时间顺序的,不能随便随机打乱。如果随机切分,模型会看到未来的数据去预测过去的数据,准确率虚高,一到真正预测未来时就露馅。这个叫数据泄漏,是时序预测最常见的坑。
正确做法是按照时间顺序划分,比如前80%的时间段做训练,后20%做验证。我常这么写:
train_size = int(len(df) * 0.8) train_df = df.iloc[:train_size] test_df = df.iloc[train_size:]还有一种更稳的方案是时序交叉验证,比如TimeSeriesSplit,不过对这个小项目来说,简单的时间切分已经够用了。特征列和标签列要分开定义,标签是你要预测的气温值,特征是你构造的那些维度。注意把date列从特征里去掉,模型不需要直接用日期字符串,日期只是用来对齐预测结果的。
4.2 多模型对比:不要只认准一个模型
我一开始只用线性回归,结果发现效果一般,毕竟温度和时间、其他特征之间的关系不完全是线性的。后来我同时跑了决策树、随机森林和支持向量回归,用同样的训练集和测试集对比,发现随机森林在这份数据上表现最好,线性回归次之,决策树单树容易过拟合。
具体实现用scikit-learn非常方便:
from sklearn.ensemble import RandomForestRegressor from sklearn.linear_model import LinearRegression from sklearn.svm import SVR models = { "linear": LinearRegression(), "rf": RandomForestRegressor(n_estimators=100, random_state=42), "svr": SVR(kernel="rbf") } for name, model in models.items(): model.fit(X_train, y_train) score = model.score(X_test, y_test) print(f"{name}: {score:.3f}")scikit-learn的score()函数返回的是R平方分数,越接近1越好。不要只看这一个指标,我一般还看均方误差和平均绝对误差,因为R方在样本量小的时候容易欺骗人。平均绝对误差更直观,比如误差两度就是两度。
4.3 评估指标与简单调参思路
评价模型好坏不要只盯着测试集,还得看训练集和测试集差距。如果训练集上表现极好,测试集上却一塌糊涂,那就是过拟合。我调参的第一步不是网格搜索,而是先看模型对数据的拟合状态。比如决策树的高度限制一下,随机森林的树数量调整一下,往往就能改善。
调参可以用GridSearchCV,但数据量小的时候网格搜索很容易变成抖机灵,调出来的参数换了数据就不行。我更建议先做特征筛选,去掉无关特征,再去网格搜索范围设置小一点,比如随机森林只看50、100、150三档。简单来说,模型复杂度要跟数据量匹配,别拿几百条数据硬套一个高复杂度模型。
5. 可视化与结果呈现
5.1 matplotlib画预测对比图
模型训练完,最终要能直观看出预测和实际的对比。我用matplotlib画了两条折线,一条是真实温度,一条是预测温度。最基础画法其实很简单:
import matplotlib.pyplot as plt plt.figure(figsize=(12, 5)) plt.plot(test_df["date"], y_test, label="actual", linewidth=2) plt.plot(test_df["date"], y_pred, label="predicted", linestyle="--") plt.xlabel("date") plt.ylabel("temperature") plt.legend() plt.title("Temperature Forecast vs Actual") plt.tight_layout() plt.show()这里有个非常常见的问题,就是横坐标日期太多、太密集,标签叠在一起变成一片黑,根本看不清。解决办法有两个,一是设置刻度间隔,只显示一部分日期;二是旋转刻度标签。我通常两个一起用,效果最好:
import matplotlib.dates as mdates plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=7)) plt.xticks(rotation=45)图不能光给自己看,如果给不懂技术的朋友看,最好把中文字体处理好,不然默认字体显示不了中文,全是方块。Windows下可以用simhei,Linux下可能要装中文字体,这一步记得检查。
5.2 导出结果到Excel或CSV
预测结果不能只停留在内存里,我用to_csv()把预测值和真实值存到文件里,方便后续查看或直接发给相关同事。代码非常简单:
result = pd.DataFrame({ "date": test_df["date"], "actual_max_temp": y_test, "predicted_max_temp": y_pred }) result.to_csv("weather_forecast_result.csv", index=False, encoding="utf-8-sig")这里有个小坑:Windows下的Excel打开CSV文件,如果不加utf-8-sig参数,中文文件名和内容会乱码。这个编码参数如果你不知道,十有八九会踩到。
5.3 用图表发现模型问题
画图还有一个重要用途是帮助调参。我跑完模型后,如果发现预测曲线整体比实际偏低,往往是因为模型学到的平均温度偏低,这时候考虑添加截距项或者检查是否有特征把平均值带偏了。如果预测曲线的波动比实际小很多,则说明模型平滑过头了,可以考虑增加滞后特征或者改用对突变更敏感的模型。多画几次图,你就能慢慢培养出"看线识病"的感觉。
6. 常见问题与排查技巧实录
6.1 环境配置阶段的高频坑
这个项目最常见的安装问题集中在numpy和scikit-learn的版本冲突上,尤其在Python 3.12以上版本,某些库还没完全适配。遇到这类问题,我给你的建议是第一看Python版本,第二看库的官方文档支持的版本范围。装库时不要一股脑装最新版,优先装已经验证过兼容的版本组合。
还有一个常见问题就是pip下载超时。除了换镜像源,还可以给pip加超时时间参数,比如--timeout=60。跑代码之前,在命令行先执行一下import pandas和import sklearn,能提前发现环境问题,别到时候把报错跟业务逻辑混在一起,排查起来头大。
6.2 数据与模型层面的经典错误
我在开发过程中整理了一个问题速查表,每次项目卡住时都会对照一下,效率特别高:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 预测曲线是一条水平线 | 特征与标签关系没学出来,或特征全被丢弃 | 检查特征列是否包含日期等无效字段,重新做特征工程 |
| 训练集评分很高,测试集评分很低 | 过拟合,模型太复杂或数据量太少 | 减少特征维度,限制模型深度,改用随机森林并调n_estimators |
| 数据导入后全是NaN | API返回字段名变了,或者JSON解析出问题 | 打印原始JSON,确认字段名;用pd.json_normalize转成DataFrame |
| 模型预测值整体偏高或偏低 | 数据存在季节性偏差,或训练集和测试集分布差异大 | 检查时间段是否跨季节,尽量让训练集包含完整周期 |
| matplotlib图表中文乱码 | 默认字体不支持中文 | 设置中文字体,或确认系统安装过中文字体文件 |
| 日期横坐标重叠看不清 | 数据量大、刻度过密 | 用DayLocator设置间隔,再用rotation调整角度 |
6.3 排查流程与实操心得
当你拿到一个报错,不要急着搜解决方案,先做三件事:读最后一行报错信息,看是哪个模块抛出的;看报错有没有提示具体的文件名和行号,直接跳过去;打印出错位置附近的关键变量的shape和值。很多所谓的疑难杂症,其实都是DataFrame列名拼错了,或者多了一个空格导致列不存在。
我个人比较推荐在代码里多写assert语句,比如确认df没有空值、确认训练集和测试集长度符合预期。这些断言能在早期发现问题,而不是等到模型跑完才看到结果异常。项目到了后期,数据清洗的函数可能比自己预想的复杂,这时候写单元测试能帮你省下大把时间。别把单元测试当成负担,对一个小项目来说,几个简单的assert就够用了。
从实际效果来看,这套系统的预测准确度在短期气温趋势上能控制在误差两度左右,应对日常活动安排、出行参考已经足够。如果你后续想继续扩展,建议往这几个方向走:一是接入实时预报接口做滚动预测,二是用Prophet或LightGBM这类更强的时间序列工具替换现在的模型,三是把预测结果做进一个简单的Flask网页服务,让朋友输入城市名就能看到预测曲线。我这个项目的代码整体没有特别华丽的技巧,但每一步都是踏踏实实踩出来的,希望这份复盘能帮你节省一些自己摸索的时间。